From e9b21595c201ea6f33be47462caf5cb6e1a3b063 Mon Sep 17 00:00:00 2001 From: Orim Dominic Date: Tue, 11 Feb 2025 12:35:55 +0100 Subject: [PATCH 01/43] chore: set up repository with React + Vite --- web/ui/dashboard-react/.editorconfig | 16 + web/ui/dashboard-react/.gitignore | 24 + web/ui/dashboard-react/.prettierrc | 18 + web/ui/dashboard-react/README.md | 7 + web/ui/dashboard-react/eslint.config.js | 34 + web/ui/dashboard-react/index.html | 14 + web/ui/dashboard-react/package-lock.json | 5129 +++++++++++++++++ web/ui/dashboard-react/package.json | 32 + web/ui/dashboard-react/public/favicon.ico | Bin 0 -> 15406 bytes web/ui/dashboard-react/src/App.css | 42 + web/ui/dashboard-react/src/App.tsx | 16 + .../src/assets/convoy-logo-full-new.svg | 30 + web/ui/dashboard-react/src/index.css | 68 + web/ui/dashboard-react/src/main.tsx | 18 + web/ui/dashboard-react/src/vite-env.d.ts | 1 + web/ui/dashboard-react/tsconfig.app.json | 30 + web/ui/dashboard-react/tsconfig.json | 7 + web/ui/dashboard-react/tsconfig.node.json | 24 + web/ui/dashboard-react/vite.config.ts | 7 + 19 files changed, 5517 insertions(+) create mode 100644 web/ui/dashboard-react/.editorconfig create mode 100644 web/ui/dashboard-react/.gitignore create mode 100644 web/ui/dashboard-react/.prettierrc create mode 100644 web/ui/dashboard-react/README.md create mode 100644 web/ui/dashboard-react/eslint.config.js create mode 100644 web/ui/dashboard-react/index.html create mode 100644 web/ui/dashboard-react/package-lock.json create mode 100644 web/ui/dashboard-react/package.json create mode 100644 web/ui/dashboard-react/public/favicon.ico create mode 100644 web/ui/dashboard-react/src/App.css create mode 100644 web/ui/dashboard-react/src/App.tsx create mode 100644 web/ui/dashboard-react/src/assets/convoy-logo-full-new.svg create mode 100644 web/ui/dashboard-react/src/index.css create mode 100644 web/ui/dashboard-react/src/main.tsx create mode 100644 web/ui/dashboard-react/src/vite-env.d.ts create mode 100644 web/ui/dashboard-react/tsconfig.app.json create mode 100644 web/ui/dashboard-react/tsconfig.json create mode 100644 web/ui/dashboard-react/tsconfig.node.json create mode 100644 web/ui/dashboard-react/vite.config.ts diff --git a/web/ui/dashboard-react/.editorconfig b/web/ui/dashboard-react/.editorconfig new file mode 100644 index 0000000000..0792692308 --- /dev/null +++ b/web/ui/dashboard-react/.editorconfig @@ -0,0 +1,16 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/web/ui/dashboard-react/.gitignore b/web/ui/dashboard-react/.gitignore new file mode 100644 index 0000000000..a547bf36d8 --- /dev/null +++ b/web/ui/dashboard-react/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/web/ui/dashboard-react/.prettierrc b/web/ui/dashboard-react/.prettierrc new file mode 100644 index 0000000000..0dad762c18 --- /dev/null +++ b/web/ui/dashboard-react/.prettierrc @@ -0,0 +1,18 @@ +{ + "singleQuote": true, + "tabWidth": 4, + "printWidth": 100, + "semi": true, + "endOfLine": "auto", + "arrowParens": "avoid", + "bracketSpacing": true, + "htmlWhitespaceSensitivity": "ignore", + "insertPragma": false, + "jsxSingleQuote": false, + "proseWrap": "preserve", + "quoteProps": "as-needed", + "requirePragma": false, + "trailingComma": "all", + "useTabs": true, + "vueIndentScriptAndStyle": false +} diff --git a/web/ui/dashboard-react/README.md b/web/ui/dashboard-react/README.md new file mode 100644 index 0000000000..576272eb35 --- /dev/null +++ b/web/ui/dashboard-react/README.md @@ -0,0 +1,7 @@ +# Convoy Dashboard + +## Setting Up Locally + +1. Install dependencies with your JavaScript package manager +2. Run the `dev` script with `npm run dev` +3. Visit [http://localhost:5006](http://localhost:5006) diff --git a/web/ui/dashboard-react/eslint.config.js b/web/ui/dashboard-react/eslint.config.js new file mode 100644 index 0000000000..add53cd1a7 --- /dev/null +++ b/web/ui/dashboard-react/eslint.config.js @@ -0,0 +1,34 @@ +import js from '@eslint/js'; +import globals from 'globals'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import tseslint from 'typescript-eslint'; +import react from 'eslint-plugin-react'; + +export default tseslint.config( + { ignores: ['dist'] }, + { + settings: { react: { version: '18.3' } }, + extends: [js.configs.recommended, ...tseslint.configs.strictTypeChecked], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 'latest', + globals: globals.browser, + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname + } + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + react + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], + ...react.configs.recommended.rules, + ...react.configs['jsx-runtime'].rules + } + } +); diff --git a/web/ui/dashboard-react/index.html b/web/ui/dashboard-react/index.html new file mode 100644 index 0000000000..d96c668ddb --- /dev/null +++ b/web/ui/dashboard-react/index.html @@ -0,0 +1,14 @@ + + + + + + + + Convoy + + +
+ + + diff --git a/web/ui/dashboard-react/package-lock.json b/web/ui/dashboard-react/package-lock.json new file mode 100644 index 0000000000..f6dc961982 --- /dev/null +++ b/web/ui/dashboard-react/package-lock.json @@ -0,0 +1,5129 @@ +{ + "name": "dashboard-react", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dashboard-react", + "version": "0.0.0", + "dependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@eslint/js": "^9.19.0", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", + "@vitejs/plugin-react": "^4.3.4", + "eslint": "^9.19.0", + "eslint-plugin-react": "^7.37.4", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.18", + "globals": "^15.14.0", + "prettier": "^3.5.0", + "typescript": "~5.7.2", + "typescript-eslint": "^8.22.0", + "vite": "^6.1.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", + "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.8.tgz", + "integrity": "sha512-l+lkXCHS6tQEc5oUpK28xBOZ6+HwaH7YwoYQbLFiYb4nS2/l1tKnZEtEWkD0GuiYdvArf9qBS0XlQGXzPMsNqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.8", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.7", + "@babel/parser": "^7.26.8", + "@babel/template": "^7.26.8", + "@babel/traverse": "^7.26.8", + "@babel/types": "^7.26.8", + "@types/gensync": "^1.0.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.8.tgz", + "integrity": "sha512-ef383X5++iZHWAXX0SXQR6ZyQhw/0KtTkrTz61WXRhFM6dhpHulO/RJz79L8S6ugZHJkOOkUrUdxgdF2YiPFnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.26.8", + "@babel/types": "^7.26.8", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", + "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.26.5", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.7.tgz", + "integrity": "sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.8.tgz", + "integrity": "sha512-TZIQ25pkSoaKEYYaHbbxkfL36GNsQ6iFiBbeuzAkLnXayKR1yP1zFe+NxuZWWsUyvt8icPU9CCq0sgWGXR1GEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.9.tgz", + "integrity": "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.9.tgz", + "integrity": "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.8.tgz", + "integrity": "sha512-iNKaX3ZebKIsCvJ+0jd6embf+Aulaa3vNBqZ41kM7iTWjx5qzWKXGHiJUW3+nTpQ18SG11hdF8OAzKrpXkb96Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.26.8", + "@babel/types": "^7.26.8" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.8.tgz", + "integrity": "sha512-nic9tRkjYH0oB2dzr/JoGIm+4Q6SuYeLEiIiZDwBscRMYFJ+tMAz98fuel9ZnbXViA2I0HVSSRRK8DW5fjXStA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.8", + "@babel/parser": "^7.26.8", + "@babel/template": "^7.26.8", + "@babel/types": "^7.26.8", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.8.tgz", + "integrity": "sha512-eUuWapzEGWFEpHFxgEaBG8e3n6S8L3MSu0oda755rOfabWPnh0Our1AozNFVUxGFIhbKgd1ksprsoDGMinTOTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", + "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", + "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", + "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", + "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", + "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", + "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", + "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", + "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", + "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", + "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", + "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", + "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", + "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", + "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", + "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", + "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", + "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", + "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", + "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", + "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", + "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", + "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", + "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", + "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", + "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", + "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", + "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.11.0.tgz", + "integrity": "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", + "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.20.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.20.0.tgz", + "integrity": "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", + "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.10.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", + "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", + "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.34.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.6.tgz", + "integrity": "sha512-+GcCXtOQoWuC7hhX1P00LqjjIiS/iOouHXhMdiDSnq/1DGTox4SpUvO52Xm+div6+106r+TcvOeo/cxvyEyTgg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.34.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.6.tgz", + "integrity": "sha512-E8+2qCIjciYUnCa1AiVF1BkRgqIGW9KzJeesQqVfyRITGQN+dFuoivO0hnro1DjT74wXLRZ7QF8MIbz+luGaJA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.34.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.6.tgz", + "integrity": "sha512-z9Ib+OzqN3DZEjX7PDQMHEhtF+t6Mi2z/ueChQPLS/qUMKY7Ybn5A2ggFoKRNRh1q1T03YTQfBTQCJZiepESAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.34.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.6.tgz", + "integrity": "sha512-PShKVY4u0FDAR7jskyFIYVyHEPCPnIQY8s5OcXkdU8mz3Y7eXDJPdyM/ZWjkYdR2m0izD9HHWA8sGcXn+Qrsyg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.34.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.6.tgz", + "integrity": "sha512-YSwyOqlDAdKqs0iKuqvRHLN4SrD2TiswfoLfvYXseKbL47ht1grQpq46MSiQAx6rQEN8o8URtpXARCpqabqxGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.34.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.6.tgz", + "integrity": "sha512-HEP4CgPAY1RxXwwL5sPFv6BBM3tVeLnshF03HMhJYCNc6kvSqBgTMmsEjb72RkZBAWIqiPUyF1JpEBv5XT9wKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.34.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.6.tgz", + "integrity": "sha512-88fSzjC5xeH9S2Vg3rPgXJULkHcLYMkh8faix8DX4h4TIAL65ekwuQMA/g2CXq8W+NJC43V6fUpYZNjaX3+IIg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.34.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.6.tgz", + "integrity": "sha512-wM4ztnutBqYFyvNeR7Av+reWI/enK9tDOTKNF+6Kk2Q96k9bwhDDOlnCUNRPvromlVXo04riSliMBs/Z7RteEg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.34.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.6.tgz", + "integrity": "sha512-9RyprECbRa9zEjXLtvvshhw4CMrRa3K+0wcp3KME0zmBe1ILmvcVHnypZ/aIDXpRyfhSYSuN4EPdCCj5Du8FIA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.34.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.6.tgz", + "integrity": "sha512-qTmklhCTyaJSB05S+iSovfo++EwnIEZxHkzv5dep4qoszUMX5Ca4WM4zAVUMbfdviLgCSQOu5oU8YoGk1s6M9Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.34.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.6.tgz", + "integrity": "sha512-4Qmkaps9yqmpjY5pvpkfOerYgKNUGzQpFxV6rnS7c/JfYbDSU0y6WpbbredB5cCpLFGJEqYX40WUmxMkwhWCjw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.34.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.6.tgz", + "integrity": "sha512-Zsrtux3PuaxuBTX/zHdLaFmcofWGzaWW1scwLU3ZbW/X+hSsFbz9wDIp6XvnT7pzYRl9MezWqEqKy7ssmDEnuQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.34.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.6.tgz", + "integrity": "sha512-aK+Zp+CRM55iPrlyKiU3/zyhgzWBxLVrw2mwiQSYJRobCURb781+XstzvA8Gkjg/hbdQFuDw44aUOxVQFycrAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.34.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.6.tgz", + "integrity": "sha512-WoKLVrY9ogmaYPXwTH326+ErlCIgMmsoRSx6bO+l68YgJnlOXhygDYSZe/qbUJCSiCiZAQ+tKm88NcWuUXqOzw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.34.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.6.tgz", + "integrity": "sha512-Sht4aFvmA4ToHd2vFzwMFaQCiYm2lDFho5rPcvPBT5pCdC+GwHG6CMch4GQfmWTQ1SwRKS0dhDYb54khSrjDWw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.34.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.6.tgz", + "integrity": "sha512-zmmpOQh8vXc2QITsnCiODCDGXFC8LMi64+/oPpPx5qz3pqv0s6x46ps4xoycfUiVZps5PFn1gksZzo4RGTKT+A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.34.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.6.tgz", + "integrity": "sha512-3/q1qUsO/tLqGBaD4uXsB6coVGB3usxw3qyeVb59aArCgedSF66MPdgRStUd7vbZOsko/CgVaY5fo2vkvPLWiA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.34.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.6.tgz", + "integrity": "sha512-oLHxuyywc6efdKVTxvc0135zPrRdtYVjtVD5GUm55I3ODxhU/PwkQFD97z16Xzxa1Fz0AEe4W/2hzRtd+IfpOA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.34.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.6.tgz", + "integrity": "sha512-0PVwmgzZ8+TZ9oGBmdZoQVXflbvuwzN/HRclujpl4N/q3i+y0lqLw8n1bXA8ru3sApDjlmONaNAuYr38y1Kr9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/gensync": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/gensync/-/gensync-1.0.4.tgz", + "integrity": "sha512-C3YYeRQWp2fmq9OryX+FoDy8nXS6scQ7dPptD8LnFDAUNcKWJjXQKDNJD3HVm+kOUsXhTOkpi69vI4EuAr95bA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.0.8", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.8.tgz", + "integrity": "sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.0.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.3.tgz", + "integrity": "sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.24.0.tgz", + "integrity": "sha512-aFcXEJJCI4gUdXgoo/j9udUYIHgF23MFkg09LFz2dzEmU0+1Plk4rQWv/IYKvPHAtlkkGoB3m5e6oUp+JPsNaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.24.0", + "@typescript-eslint/type-utils": "8.24.0", + "@typescript-eslint/utils": "8.24.0", + "@typescript-eslint/visitor-keys": "8.24.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.24.0.tgz", + "integrity": "sha512-MFDaO9CYiard9j9VepMNa9MTcqVvSny2N4hkY6roquzj8pdCBRENhErrteaQuu7Yjn1ppk0v1/ZF9CG3KIlrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.24.0", + "@typescript-eslint/types": "8.24.0", + "@typescript-eslint/typescript-estree": "8.24.0", + "@typescript-eslint/visitor-keys": "8.24.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.24.0.tgz", + "integrity": "sha512-HZIX0UByphEtdVBKaQBgTDdn9z16l4aTUz8e8zPQnyxwHBtf5vtl1L+OhH+m1FGV9DrRmoDuYKqzVrvWDcDozw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.24.0", + "@typescript-eslint/visitor-keys": "8.24.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.24.0.tgz", + "integrity": "sha512-8fitJudrnY8aq0F1wMiPM1UUgiXQRJ5i8tFjq9kGfRajU+dbPyOuHbl0qRopLEidy0MwqgTHDt6CnSeXanNIwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.24.0", + "@typescript-eslint/utils": "8.24.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.24.0.tgz", + "integrity": "sha512-VacJCBTyje7HGAw7xp11q439A+zeGG0p0/p2zsZwpnMzjPB5WteaWqt4g2iysgGFafrqvyLWqq6ZPZAOCoefCw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.24.0.tgz", + "integrity": "sha512-ITjYcP0+8kbsvT9bysygfIfb+hBj6koDsu37JZG7xrCiy3fPJyNmfVtaGsgTUSEuTzcvME5YI5uyL5LD1EV5ZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.24.0", + "@typescript-eslint/visitor-keys": "8.24.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.24.0.tgz", + "integrity": "sha512-07rLuUBElvvEb1ICnafYWr4hk8/U7X9RDCOqd9JcAMtjh/9oRmcfN4yGzbPVirgMR0+HLVHehmu19CWeh7fsmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.24.0", + "@typescript-eslint/types": "8.24.0", + "@typescript-eslint/typescript-estree": "8.24.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.24.0.tgz", + "integrity": "sha512-kArLq83QxGLbuHrTMoOEWO+l2MwsNS2TGISEdx8xgqpkbytB07XmlQyQdNDrCc1ecSqx0cnmhGvpX+VBwqqSkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.24.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz", + "integrity": "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.26.0", + "@babel/plugin-transform-react-jsx-self": "^7.25.9", + "@babel/plugin-transform-react-jsx-source": "^7.25.9", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.14.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001699", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001699.tgz", + "integrity": "sha512-b+uH5BakXZ9Do9iK+CkDmctUSEqZl+SP056vc5usa0PL+ev5OHw003rZXcnjNDv3L8P5j6rwT6C0BPKSikW08w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.97", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.97.tgz", + "integrity": "sha512-HKLtaH02augM7ZOdYRuO19rWDeY+QSJ1VxnXFa/XDFLf07HvM90pALIJFgrO+UVaajI3+aJMMpojoUTLZyQ7JQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-abstract": { + "version": "1.23.9", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", + "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.0", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-regex": "^1.2.1", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.0", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.3", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.18" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", + "@esbuild/linux-ia32": "0.24.2", + "@esbuild/linux-loong64": "0.24.2", + "@esbuild/linux-mips64el": "0.24.2", + "@esbuild/linux-ppc64": "0.24.2", + "@esbuild/linux-riscv64": "0.24.2", + "@esbuild/linux-s390x": "0.24.2", + "@esbuild/linux-x64": "0.24.2", + "@esbuild/netbsd-arm64": "0.24.2", + "@esbuild/netbsd-x64": "0.24.2", + "@esbuild/openbsd-arm64": "0.24.2", + "@esbuild/openbsd-x64": "0.24.2", + "@esbuild/sunos-x64": "0.24.2", + "@esbuild/win32-arm64": "0.24.2", + "@esbuild/win32-ia32": "0.24.2", + "@esbuild/win32-x64": "0.24.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.20.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.0.tgz", + "integrity": "sha512-aL4F8167Hg4IvsW89ejnpTwx+B/UQRzJPGgbIOl+4XqffWsahVVsLEWoZvnrVuwpWmnRd7XeXmQI1zlKcFDteA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.19.0", + "@eslint/core": "^0.11.0", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "9.20.0", + "@eslint/plugin-kit": "^0.2.5", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.1", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.4.tgz", + "integrity": "sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.1.0.tgz", + "integrity": "sha512-mpJRtPgHN2tNAvZ35AMfqeB3Xqeo273QxrHJsbBEPWODRM4r0yB6jfoROqKEYrOn27UtRPpcpHc2UqyBSuUNTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.19.tgz", + "integrity": "sha512-eyy8pcr/YxSYjBoqIFSrlbn9i/xvxUFa8CjzAYo9cFjgGXqq1hyjihcpZvxRLalpaWmueWR81xn7vuKmAFijDQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", + "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz", + "integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", + "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", + "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "function-bind": "^1.1.2", + "get-proto": "^1.0.0", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "15.14.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz", + "integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", + "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.2.tgz", + "integrity": "sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.0.tgz", + "integrity": "sha512-quyMrVt6svPS7CjQ9gKb3GLEX/rl3BCL2oa/QkNcXv4YNVBC9olt3s+H7ukto06q7B1Qz46PbrKLO34PR6vXcA==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", + "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", + "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.25.0" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.34.6", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.6.tgz", + "integrity": "sha512-wc2cBWqJgkU3Iz5oztRkQbfVkbxoz5EhnCGOrnJvnLnQ7O0WhQUYyv18qQI79O8L7DdHrrlJNeCHd4VGpnaXKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.34.6", + "@rollup/rollup-android-arm64": "4.34.6", + "@rollup/rollup-darwin-arm64": "4.34.6", + "@rollup/rollup-darwin-x64": "4.34.6", + "@rollup/rollup-freebsd-arm64": "4.34.6", + "@rollup/rollup-freebsd-x64": "4.34.6", + "@rollup/rollup-linux-arm-gnueabihf": "4.34.6", + "@rollup/rollup-linux-arm-musleabihf": "4.34.6", + "@rollup/rollup-linux-arm64-gnu": "4.34.6", + "@rollup/rollup-linux-arm64-musl": "4.34.6", + "@rollup/rollup-linux-loongarch64-gnu": "4.34.6", + "@rollup/rollup-linux-powerpc64le-gnu": "4.34.6", + "@rollup/rollup-linux-riscv64-gnu": "4.34.6", + "@rollup/rollup-linux-s390x-gnu": "4.34.6", + "@rollup/rollup-linux-x64-gnu": "4.34.6", + "@rollup/rollup-linux-x64-musl": "4.34.6", + "@rollup/rollup-win32-arm64-msvc": "4.34.6", + "@rollup/rollup-win32-ia32-msvc": "4.34.6", + "@rollup/rollup-win32-x64-msvc": "4.34.6", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/scheduler": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", + "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", + "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.24.0.tgz", + "integrity": "sha512-/lmv4366en/qbB32Vz5+kCNZEMf6xYHwh1z48suBwZvAtnXKbP+YhGe8OLE2BqC67LMqKkCNLtjejdwsdW6uOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.24.0", + "@typescript-eslint/parser": "8.24.0", + "@typescript-eslint/utils": "8.24.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", + "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.1.0.tgz", + "integrity": "sha512-RjjMipCKVoR4hVfPY6GQTgveinjNuyLw+qruksLDvA5ktI1150VmcMBKmQaEWJhg/j6Uaf6dNCNA0AfdzUb/hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.24.2", + "postcss": "^8.5.1", + "rollup": "^4.30.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz", + "integrity": "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/web/ui/dashboard-react/package.json b/web/ui/dashboard-react/package.json new file mode 100644 index 0000000000..73dd69d48d --- /dev/null +++ b/web/ui/dashboard-react/package.json @@ -0,0 +1,32 @@ +{ + "name": "dashboard-react", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --port 5006", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview", + "prettify": "prettier --log-level=warn --cache --write ./src && echo 'prettify complete!'" + }, + "dependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@eslint/js": "^9.19.0", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", + "@vitejs/plugin-react": "^4.3.4", + "eslint": "^9.19.0", + "eslint-plugin-react": "^7.37.4", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.18", + "globals": "^15.14.0", + "prettier": "^3.5.0", + "typescript": "~5.7.2", + "typescript-eslint": "^8.22.0", + "vite": "^6.1.0" + } +} diff --git a/web/ui/dashboard-react/public/favicon.ico b/web/ui/dashboard-react/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..aa20ce169205634ee68e3a0d3fb8fea1d9c63ca4 GIT binary patch literal 15406 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 +
+ + Convoy logo + +
+ + ); +} + +export default App; diff --git a/web/ui/dashboard-react/src/assets/convoy-logo-full-new.svg b/web/ui/dashboard-react/src/assets/convoy-logo-full-new.svg new file mode 100644 index 0000000000..31d71d993e --- /dev/null +++ b/web/ui/dashboard-react/src/assets/convoy-logo-full-new.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/ui/dashboard-react/src/index.css b/web/ui/dashboard-react/src/index.css new file mode 100644 index 0000000000..9cfcb00f24 --- /dev/null +++ b/web/ui/dashboard-react/src/index.css @@ -0,0 +1,68 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/web/ui/dashboard-react/src/main.tsx b/web/ui/dashboard-react/src/main.tsx new file mode 100644 index 0000000000..9ee358255e --- /dev/null +++ b/web/ui/dashboard-react/src/main.tsx @@ -0,0 +1,18 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import './index.css'; +import App from './App.tsx'; + +let root = document.getElementById('root'); +if (!root) { + const div = document.createElement('div'); + div.id = 'root'; + document.body.prepend(div); + root = document.getElementById('root') as HTMLElement; +} + +createRoot(root).render( + + + , +); diff --git a/web/ui/dashboard-react/src/vite-env.d.ts b/web/ui/dashboard-react/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/web/ui/dashboard-react/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/web/ui/dashboard-react/tsconfig.app.json b/web/ui/dashboard-react/tsconfig.app.json new file mode 100644 index 0000000000..d080730a4a --- /dev/null +++ b/web/ui/dashboard-react/tsconfig.app.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "verbatimModuleSyntax": true, + "sourceMap": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + "noImplicitReturns": true, + "forceConsistentCasingInFileNames": true, + }, + "include": ["src"] +} diff --git a/web/ui/dashboard-react/tsconfig.json b/web/ui/dashboard-react/tsconfig.json new file mode 100644 index 0000000000..1ffef600d9 --- /dev/null +++ b/web/ui/dashboard-react/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/web/ui/dashboard-react/tsconfig.node.json b/web/ui/dashboard-react/tsconfig.node.json new file mode 100644 index 0000000000..4345dd5658 --- /dev/null +++ b/web/ui/dashboard-react/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2022", + "lib": ["ESNext"], + "module": "NodeNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "nodenext", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/web/ui/dashboard-react/vite.config.ts b/web/ui/dashboard-react/vite.config.ts new file mode 100644 index 0000000000..8b0f57b91a --- /dev/null +++ b/web/ui/dashboard-react/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}) From 59c04c4e6dcf0d9088e87bde57f996798fc3da95 Mon Sep 17 00:00:00 2001 From: Orim Dominic Adah Date: Tue, 18 Feb 2025 23:36:29 +0100 Subject: [PATCH 02/43] chore: set up shadcn (#2242) --- web/ui/dashboard-react/.gitignore | 1 + web/ui/dashboard-react/components.json | 21 + web/ui/dashboard-react/package-lock.json | 5129 ----------------- web/ui/dashboard-react/package.json | 19 +- web/ui/dashboard-react/postcss.config.cjs | 6 + web/ui/dashboard-react/src/App.css | 42 - web/ui/dashboard-react/src/App.tsx | 20 +- web/ui/dashboard-react/src/assets/.gitkeep | 0 .../dashboard-react/src/assets/css/fonts.css | 492 ++ .../dashboard-react/src/assets/css/forms.css | 32 + .../src/assets/fonts/Menlo-Regular.woff | Bin 0 -> 236936 bytes ...K3iLTeHuS_fvQtMwCp50KnMa0ZL7W0Q5n-wU.woff2 | Bin 0 -> 17040 bytes ...wrK3iLTeHuS_fvQtMwCp50KnMa1ZL7W0Q5nw.woff2 | Bin 0 -> 37780 bytes ...K3iLTeHuS_fvQtMwCp50KnMa1pL7W0Q5n-wU.woff2 | Bin 0 -> 21960 bytes ...K3iLTeHuS_fvQtMwCp50KnMa25L7W0Q5n-wU.woff2 | Bin 0 -> 57244 bytes ...K3iLTeHuS_fvQtMwCp50KnMa2JL7W0Q5n-wU.woff2 | Bin 0 -> 26728 bytes ...K3iLTeHuS_fvQtMwCp50KnMa2ZL7W0Q5n-wU.woff2 | Bin 0 -> 11924 bytes ...K3iLTeHuS_fvQtMwCp50KnMa2pL7W0Q5n-wU.woff2 | Bin 0 -> 8652 bytes .../{ => img/svg}/convoy-logo-full-new.svg | 0 .../src/components/ui/.gitkeep | 0 .../src/components/ui/button.tsx | 55 + web/ui/dashboard-react/src/index.css | 186 +- web/ui/dashboard-react/src/lib/utils.ts | 6 + web/ui/dashboard-react/tailwind.config.js | 253 + web/ui/dashboard-react/tsconfig.app.json | 7 +- web/ui/dashboard-react/tsconfig.json | 8 +- web/ui/dashboard-react/vite.config.ts | 6 + 27 files changed, 1044 insertions(+), 5239 deletions(-) create mode 100644 web/ui/dashboard-react/components.json delete mode 100644 web/ui/dashboard-react/package-lock.json create mode 100644 web/ui/dashboard-react/postcss.config.cjs delete mode 100644 web/ui/dashboard-react/src/App.css create mode 100644 web/ui/dashboard-react/src/assets/.gitkeep create mode 100644 web/ui/dashboard-react/src/assets/css/fonts.css create mode 100644 web/ui/dashboard-react/src/assets/css/forms.css create mode 100644 web/ui/dashboard-react/src/assets/fonts/Menlo-Regular.woff create mode 100644 web/ui/dashboard-react/src/assets/fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa0ZL7W0Q5n-wU.woff2 create mode 100644 web/ui/dashboard-react/src/assets/fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7W0Q5nw.woff2 create mode 100644 web/ui/dashboard-react/src/assets/fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1pL7W0Q5n-wU.woff2 create mode 100644 web/ui/dashboard-react/src/assets/fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7W0Q5n-wU.woff2 create mode 100644 web/ui/dashboard-react/src/assets/fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2JL7W0Q5n-wU.woff2 create mode 100644 web/ui/dashboard-react/src/assets/fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2ZL7W0Q5n-wU.woff2 create mode 100644 web/ui/dashboard-react/src/assets/fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2pL7W0Q5n-wU.woff2 rename web/ui/dashboard-react/src/assets/{ => img/svg}/convoy-logo-full-new.svg (100%) create mode 100644 web/ui/dashboard-react/src/components/ui/.gitkeep create mode 100644 web/ui/dashboard-react/src/components/ui/button.tsx create mode 100644 web/ui/dashboard-react/src/lib/utils.ts create mode 100644 web/ui/dashboard-react/tailwind.config.js diff --git a/web/ui/dashboard-react/.gitignore b/web/ui/dashboard-react/.gitignore index a547bf36d8..b02a1ff770 100644 --- a/web/ui/dashboard-react/.gitignore +++ b/web/ui/dashboard-react/.gitignore @@ -11,6 +11,7 @@ node_modules dist dist-ssr *.local +package-lock.json # Editor directories and files .vscode/* diff --git a/web/ui/dashboard-react/components.json b/web/ui/dashboard-react/components.json new file mode 100644 index 0000000000..51d59d26d8 --- /dev/null +++ b/web/ui/dashboard-react/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/index.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/web/ui/dashboard-react/package-lock.json b/web/ui/dashboard-react/package-lock.json deleted file mode 100644 index f6dc961982..0000000000 --- a/web/ui/dashboard-react/package-lock.json +++ /dev/null @@ -1,5129 +0,0 @@ -{ - "name": "dashboard-react", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "dashboard-react", - "version": "0.0.0", - "dependencies": { - "react": "^19.0.0", - "react-dom": "^19.0.0" - }, - "devDependencies": { - "@eslint/js": "^9.19.0", - "@types/react": "^19.0.8", - "@types/react-dom": "^19.0.3", - "@vitejs/plugin-react": "^4.3.4", - "eslint": "^9.19.0", - "eslint-plugin-react": "^7.37.4", - "eslint-plugin-react-hooks": "^5.0.0", - "eslint-plugin-react-refresh": "^0.4.18", - "globals": "^15.14.0", - "prettier": "^3.5.0", - "typescript": "~5.7.2", - "typescript-eslint": "^8.22.0", - "vite": "^6.1.0" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", - "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.8.tgz", - "integrity": "sha512-l+lkXCHS6tQEc5oUpK28xBOZ6+HwaH7YwoYQbLFiYb4nS2/l1tKnZEtEWkD0GuiYdvArf9qBS0XlQGXzPMsNqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.8", - "@babel/helper-compilation-targets": "^7.26.5", - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.7", - "@babel/parser": "^7.26.8", - "@babel/template": "^7.26.8", - "@babel/traverse": "^7.26.8", - "@babel/types": "^7.26.8", - "@types/gensync": "^1.0.0", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.8.tgz", - "integrity": "sha512-ef383X5++iZHWAXX0SXQR6ZyQhw/0KtTkrTz61WXRhFM6dhpHulO/RJz79L8S6ugZHJkOOkUrUdxgdF2YiPFnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.26.8", - "@babel/types": "^7.26.8", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", - "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.26.5", - "@babel/helper-validator-option": "^7.25.9", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", - "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", - "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", - "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", - "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.7.tgz", - "integrity": "sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.8.tgz", - "integrity": "sha512-TZIQ25pkSoaKEYYaHbbxkfL36GNsQ6iFiBbeuzAkLnXayKR1yP1zFe+NxuZWWsUyvt8icPU9CCq0sgWGXR1GEw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.26.8" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.9.tgz", - "integrity": "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.9.tgz", - "integrity": "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.8.tgz", - "integrity": "sha512-iNKaX3ZebKIsCvJ+0jd6embf+Aulaa3vNBqZ41kM7iTWjx5qzWKXGHiJUW3+nTpQ18SG11hdF8OAzKrpXkb96Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/parser": "^7.26.8", - "@babel/types": "^7.26.8" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.8.tgz", - "integrity": "sha512-nic9tRkjYH0oB2dzr/JoGIm+4Q6SuYeLEiIiZDwBscRMYFJ+tMAz98fuel9ZnbXViA2I0HVSSRRK8DW5fjXStA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.8", - "@babel/parser": "^7.26.8", - "@babel/template": "^7.26.8", - "@babel/types": "^7.26.8", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/types": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.8.tgz", - "integrity": "sha512-eUuWapzEGWFEpHFxgEaBG8e3n6S8L3MSu0oda755rOfabWPnh0Our1AozNFVUxGFIhbKgd1ksprsoDGMinTOTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", - "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", - "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", - "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", - "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", - "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", - "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", - "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", - "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", - "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", - "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", - "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", - "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", - "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", - "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", - "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", - "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", - "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", - "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", - "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", - "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", - "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", - "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", - "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", - "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", - "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", - "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", - "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.6", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.11.0.tgz", - "integrity": "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", - "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/js": { - "version": "9.20.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.20.0.tgz", - "integrity": "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", - "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.10.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", - "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", - "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.6.tgz", - "integrity": "sha512-+GcCXtOQoWuC7hhX1P00LqjjIiS/iOouHXhMdiDSnq/1DGTox4SpUvO52Xm+div6+106r+TcvOeo/cxvyEyTgg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.6.tgz", - "integrity": "sha512-E8+2qCIjciYUnCa1AiVF1BkRgqIGW9KzJeesQqVfyRITGQN+dFuoivO0hnro1DjT74wXLRZ7QF8MIbz+luGaJA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.6.tgz", - "integrity": "sha512-z9Ib+OzqN3DZEjX7PDQMHEhtF+t6Mi2z/ueChQPLS/qUMKY7Ybn5A2ggFoKRNRh1q1T03YTQfBTQCJZiepESAg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.6.tgz", - "integrity": "sha512-PShKVY4u0FDAR7jskyFIYVyHEPCPnIQY8s5OcXkdU8mz3Y7eXDJPdyM/ZWjkYdR2m0izD9HHWA8sGcXn+Qrsyg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.6.tgz", - "integrity": "sha512-YSwyOqlDAdKqs0iKuqvRHLN4SrD2TiswfoLfvYXseKbL47ht1grQpq46MSiQAx6rQEN8o8URtpXARCpqabqxGQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.6.tgz", - "integrity": "sha512-HEP4CgPAY1RxXwwL5sPFv6BBM3tVeLnshF03HMhJYCNc6kvSqBgTMmsEjb72RkZBAWIqiPUyF1JpEBv5XT9wKQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.6.tgz", - "integrity": "sha512-88fSzjC5xeH9S2Vg3rPgXJULkHcLYMkh8faix8DX4h4TIAL65ekwuQMA/g2CXq8W+NJC43V6fUpYZNjaX3+IIg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.6.tgz", - "integrity": "sha512-wM4ztnutBqYFyvNeR7Av+reWI/enK9tDOTKNF+6Kk2Q96k9bwhDDOlnCUNRPvromlVXo04riSliMBs/Z7RteEg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.6.tgz", - "integrity": "sha512-9RyprECbRa9zEjXLtvvshhw4CMrRa3K+0wcp3KME0zmBe1ILmvcVHnypZ/aIDXpRyfhSYSuN4EPdCCj5Du8FIA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.6.tgz", - "integrity": "sha512-qTmklhCTyaJSB05S+iSovfo++EwnIEZxHkzv5dep4qoszUMX5Ca4WM4zAVUMbfdviLgCSQOu5oU8YoGk1s6M9Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.6.tgz", - "integrity": "sha512-4Qmkaps9yqmpjY5pvpkfOerYgKNUGzQpFxV6rnS7c/JfYbDSU0y6WpbbredB5cCpLFGJEqYX40WUmxMkwhWCjw==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.6.tgz", - "integrity": "sha512-Zsrtux3PuaxuBTX/zHdLaFmcofWGzaWW1scwLU3ZbW/X+hSsFbz9wDIp6XvnT7pzYRl9MezWqEqKy7ssmDEnuQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.6.tgz", - "integrity": "sha512-aK+Zp+CRM55iPrlyKiU3/zyhgzWBxLVrw2mwiQSYJRobCURb781+XstzvA8Gkjg/hbdQFuDw44aUOxVQFycrAg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.6.tgz", - "integrity": "sha512-WoKLVrY9ogmaYPXwTH326+ErlCIgMmsoRSx6bO+l68YgJnlOXhygDYSZe/qbUJCSiCiZAQ+tKm88NcWuUXqOzw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.6.tgz", - "integrity": "sha512-Sht4aFvmA4ToHd2vFzwMFaQCiYm2lDFho5rPcvPBT5pCdC+GwHG6CMch4GQfmWTQ1SwRKS0dhDYb54khSrjDWw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.6.tgz", - "integrity": "sha512-zmmpOQh8vXc2QITsnCiODCDGXFC8LMi64+/oPpPx5qz3pqv0s6x46ps4xoycfUiVZps5PFn1gksZzo4RGTKT+A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.6.tgz", - "integrity": "sha512-3/q1qUsO/tLqGBaD4uXsB6coVGB3usxw3qyeVb59aArCgedSF66MPdgRStUd7vbZOsko/CgVaY5fo2vkvPLWiA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.6.tgz", - "integrity": "sha512-oLHxuyywc6efdKVTxvc0135zPrRdtYVjtVD5GUm55I3ODxhU/PwkQFD97z16Xzxa1Fz0AEe4W/2hzRtd+IfpOA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.6.tgz", - "integrity": "sha512-0PVwmgzZ8+TZ9oGBmdZoQVXflbvuwzN/HRclujpl4N/q3i+y0lqLw8n1bXA8ru3sApDjlmONaNAuYr38y1Kr9w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", - "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.20.6", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", - "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.20.7" - } - }, - "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/gensync": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@types/gensync/-/gensync-1.0.4.tgz", - "integrity": "sha512-C3YYeRQWp2fmq9OryX+FoDy8nXS6scQ7dPptD8LnFDAUNcKWJjXQKDNJD3HVm+kOUsXhTOkpi69vI4EuAr95bA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "19.0.8", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.8.tgz", - "integrity": "sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.0.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.3.tgz", - "integrity": "sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.24.0.tgz", - "integrity": "sha512-aFcXEJJCI4gUdXgoo/j9udUYIHgF23MFkg09LFz2dzEmU0+1Plk4rQWv/IYKvPHAtlkkGoB3m5e6oUp+JPsNaQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.24.0", - "@typescript-eslint/type-utils": "8.24.0", - "@typescript-eslint/utils": "8.24.0", - "@typescript-eslint/visitor-keys": "8.24.0", - "graphemer": "^1.4.0", - "ignore": "^5.3.1", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.24.0.tgz", - "integrity": "sha512-MFDaO9CYiard9j9VepMNa9MTcqVvSny2N4hkY6roquzj8pdCBRENhErrteaQuu7Yjn1ppk0v1/ZF9CG3KIlrTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.24.0", - "@typescript-eslint/types": "8.24.0", - "@typescript-eslint/typescript-estree": "8.24.0", - "@typescript-eslint/visitor-keys": "8.24.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.24.0.tgz", - "integrity": "sha512-HZIX0UByphEtdVBKaQBgTDdn9z16l4aTUz8e8zPQnyxwHBtf5vtl1L+OhH+m1FGV9DrRmoDuYKqzVrvWDcDozw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.24.0", - "@typescript-eslint/visitor-keys": "8.24.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.24.0.tgz", - "integrity": "sha512-8fitJudrnY8aq0F1wMiPM1UUgiXQRJ5i8tFjq9kGfRajU+dbPyOuHbl0qRopLEidy0MwqgTHDt6CnSeXanNIwA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "8.24.0", - "@typescript-eslint/utils": "8.24.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.24.0.tgz", - "integrity": "sha512-VacJCBTyje7HGAw7xp11q439A+zeGG0p0/p2zsZwpnMzjPB5WteaWqt4g2iysgGFafrqvyLWqq6ZPZAOCoefCw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.24.0.tgz", - "integrity": "sha512-ITjYcP0+8kbsvT9bysygfIfb+hBj6koDsu37JZG7xrCiy3fPJyNmfVtaGsgTUSEuTzcvME5YI5uyL5LD1EV5ZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.24.0", - "@typescript-eslint/visitor-keys": "8.24.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.24.0.tgz", - "integrity": "sha512-07rLuUBElvvEb1ICnafYWr4hk8/U7X9RDCOqd9JcAMtjh/9oRmcfN4yGzbPVirgMR0+HLVHehmu19CWeh7fsmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.24.0", - "@typescript-eslint/types": "8.24.0", - "@typescript-eslint/typescript-estree": "8.24.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.24.0.tgz", - "integrity": "sha512-kArLq83QxGLbuHrTMoOEWO+l2MwsNS2TGISEdx8xgqpkbytB07XmlQyQdNDrCc1ecSqx0cnmhGvpX+VBwqqSkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.24.0", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@vitejs/plugin-react": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz", - "integrity": "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.26.0", - "@babel/plugin-transform-react-jsx-self": "^7.25.9", - "@babel/plugin-transform-react-jsx-source": "^7.25.9", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.14.2" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" - } - }, - "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "is-array-buffer": "^3.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.findlast": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", - "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", - "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", - "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", - "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", - "es-errors": "^1.3.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/async-function": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", - "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", - "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001699", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001699.tgz", - "integrity": "sha512-b+uH5BakXZ9Do9iK+CkDmctUSEqZl+SP056vc5usa0PL+ev5OHw003rZXcnjNDv3L8P5j6rwT6C0BPKSikW08w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, - "license": "MIT" - }, - "node_modules/data-view-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/inspect-js" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.97", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.97.tgz", - "integrity": "sha512-HKLtaH02augM7ZOdYRuO19rWDeY+QSJ1VxnXFa/XDFLf07HvM90pALIJFgrO+UVaajI3+aJMMpojoUTLZyQ7JQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/es-abstract": { - "version": "1.23.9", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", - "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.2", - "arraybuffer.prototype.slice": "^1.0.4", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "data-view-buffer": "^1.0.2", - "data-view-byte-length": "^1.0.2", - "data-view-byte-offset": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-set-tostringtag": "^2.1.0", - "es-to-primitive": "^1.3.0", - "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.0", - "get-symbol-description": "^1.1.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "internal-slot": "^1.1.0", - "is-array-buffer": "^3.0.5", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.2", - "is-regex": "^1.2.1", - "is-shared-array-buffer": "^1.0.4", - "is-string": "^1.1.1", - "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.0", - "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.3", - "object-keys": "^1.1.1", - "object.assign": "^4.1.7", - "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.3", - "safe-array-concat": "^1.1.3", - "safe-push-apply": "^1.0.0", - "safe-regex-test": "^1.1.0", - "set-proto": "^1.0.0", - "string.prototype.trim": "^1.2.10", - "string.prototype.trimend": "^1.0.9", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.3", - "typed-array-byte-length": "^1.0.3", - "typed-array-byte-offset": "^1.0.4", - "typed-array-length": "^1.0.7", - "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.18" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-iterator-helpers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", - "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", - "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.0.3", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.6", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "iterator.prototype": "^1.1.4", - "safe-array-concat": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", - "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.0" - } - }, - "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/esbuild": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", - "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.24.2", - "@esbuild/android-arm": "0.24.2", - "@esbuild/android-arm64": "0.24.2", - "@esbuild/android-x64": "0.24.2", - "@esbuild/darwin-arm64": "0.24.2", - "@esbuild/darwin-x64": "0.24.2", - "@esbuild/freebsd-arm64": "0.24.2", - "@esbuild/freebsd-x64": "0.24.2", - "@esbuild/linux-arm": "0.24.2", - "@esbuild/linux-arm64": "0.24.2", - "@esbuild/linux-ia32": "0.24.2", - "@esbuild/linux-loong64": "0.24.2", - "@esbuild/linux-mips64el": "0.24.2", - "@esbuild/linux-ppc64": "0.24.2", - "@esbuild/linux-riscv64": "0.24.2", - "@esbuild/linux-s390x": "0.24.2", - "@esbuild/linux-x64": "0.24.2", - "@esbuild/netbsd-arm64": "0.24.2", - "@esbuild/netbsd-x64": "0.24.2", - "@esbuild/openbsd-arm64": "0.24.2", - "@esbuild/openbsd-x64": "0.24.2", - "@esbuild/sunos-x64": "0.24.2", - "@esbuild/win32-arm64": "0.24.2", - "@esbuild/win32-ia32": "0.24.2", - "@esbuild/win32-x64": "0.24.2" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.20.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.0.tgz", - "integrity": "sha512-aL4F8167Hg4IvsW89ejnpTwx+B/UQRzJPGgbIOl+4XqffWsahVVsLEWoZvnrVuwpWmnRd7XeXmQI1zlKcFDteA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.11.0", - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.20.0", - "@eslint/plugin-kit": "^0.2.5", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.1", - "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.2.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-react": { - "version": "7.37.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.4.tgz", - "integrity": "sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.8", - "array.prototype.findlast": "^1.2.5", - "array.prototype.flatmap": "^1.3.3", - "array.prototype.tosorted": "^1.1.4", - "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.2.1", - "estraverse": "^5.3.0", - "hasown": "^2.0.2", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.8", - "object.fromentries": "^2.0.8", - "object.values": "^1.2.1", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.5", - "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.12", - "string.prototype.repeat": "^1.0.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.1.0.tgz", - "integrity": "sha512-mpJRtPgHN2tNAvZ35AMfqeB3Xqeo273QxrHJsbBEPWODRM4r0yB6jfoROqKEYrOn27UtRPpcpHc2UqyBSuUNTw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" - } - }, - "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.19.tgz", - "integrity": "sha512-eyy8pcr/YxSYjBoqIFSrlbn9i/xvxUFa8CjzAYo9cFjgGXqq1hyjihcpZvxRLalpaWmueWR81xn7vuKmAFijDQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "eslint": ">=8.40" - } - }, - "node_modules/eslint-scope": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", - "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.14.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz", - "integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", - "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", - "dev": true, - "license": "ISC" - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "functions-have-names": "^1.2.3", - "hasown": "^2.0.2", - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-intrinsic": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", - "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "function-bind": "^1.1.2", - "get-proto": "^1.0.0", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-symbol-description": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "15.14.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz", - "integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/has-bigints": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/internal-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-async-function": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-function": "^1.0.0", - "call-bound": "^1.0.3", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-data-view": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-finalizationregistry": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-string": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/iterator.prototype": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", - "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "get-proto": "^1.0.0", - "has-symbols": "^1.1.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsx-ast-utils": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", - "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "dev": true, - "license": "MIT" - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", - "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.values": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", - "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/own-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.6", - "object-keys": "^1.1.1", - "safe-push-apply": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/postcss": { - "version": "8.5.2", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.2.tgz", - "integrity": "sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.8", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.0.tgz", - "integrity": "sha512-quyMrVt6svPS7CjQ9gKb3GLEX/rl3BCL2oa/QkNcXv4YNVBC9olt3s+H7ukto06q7B1Qz46PbrKLO34PR6vXcA==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/react": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", - "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", - "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.25.0" - }, - "peerDependencies": { - "react": "^19.0.0" - } - }, - "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/react-refresh": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", - "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.1", - "which-builtin-type": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve": { - "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rollup": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.6.tgz", - "integrity": "sha512-wc2cBWqJgkU3Iz5oztRkQbfVkbxoz5EhnCGOrnJvnLnQ7O0WhQUYyv18qQI79O8L7DdHrrlJNeCHd4VGpnaXKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.6" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.34.6", - "@rollup/rollup-android-arm64": "4.34.6", - "@rollup/rollup-darwin-arm64": "4.34.6", - "@rollup/rollup-darwin-x64": "4.34.6", - "@rollup/rollup-freebsd-arm64": "4.34.6", - "@rollup/rollup-freebsd-x64": "4.34.6", - "@rollup/rollup-linux-arm-gnueabihf": "4.34.6", - "@rollup/rollup-linux-arm-musleabihf": "4.34.6", - "@rollup/rollup-linux-arm64-gnu": "4.34.6", - "@rollup/rollup-linux-arm64-musl": "4.34.6", - "@rollup/rollup-linux-loongarch64-gnu": "4.34.6", - "@rollup/rollup-linux-powerpc64le-gnu": "4.34.6", - "@rollup/rollup-linux-riscv64-gnu": "4.34.6", - "@rollup/rollup-linux-s390x-gnu": "4.34.6", - "@rollup/rollup-linux-x64-gnu": "4.34.6", - "@rollup/rollup-linux-x64-musl": "4.34.6", - "@rollup/rollup-win32-arm64-msvc": "4.34.6", - "@rollup/rollup-win32-ia32-msvc": "4.34.6", - "@rollup/rollup-win32-x64-msvc": "4.34.6", - "fsevents": "~2.3.2" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-array-concat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "has-symbols": "^1.1.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-push-apply": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/scheduler": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", - "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/string.prototype.matchall": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", - "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "regexp.prototype.flags": "^1.5.3", - "set-function-name": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.repeat": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", - "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-data-property": "^1.1.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-object-atoms": "^1.0.0", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/ts-api-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", - "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.15", - "reflect.getprototypeof": "^1.0.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typescript": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", - "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.24.0.tgz", - "integrity": "sha512-/lmv4366en/qbB32Vz5+kCNZEMf6xYHwh1z48suBwZvAtnXKbP+YhGe8OLE2BqC67LMqKkCNLtjejdwsdW6uOQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.24.0", - "@typescript-eslint/parser": "8.24.0", - "@typescript-eslint/utils": "8.24.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" - } - }, - "node_modules/unbox-primitive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-bigints": "^1.0.2", - "has-symbols": "^1.1.0", - "which-boxed-primitive": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", - "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/vite": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.1.0.tgz", - "integrity": "sha512-RjjMipCKVoR4hVfPY6GQTgveinjNuyLw+qruksLDvA5ktI1150VmcMBKmQaEWJhg/j6Uaf6dNCNA0AfdzUb/hQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.24.2", - "postcss": "^8.5.1", - "rollup": "^4.30.1" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-bigint": "^1.1.0", - "is-boolean-object": "^1.2.1", - "is-number-object": "^1.1.1", - "is-string": "^1.1.1", - "is-symbol": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "function.prototype.name": "^1.1.6", - "has-tostringtag": "^1.0.2", - "is-async-function": "^2.0.0", - "is-date-object": "^1.1.0", - "is-finalizationregistry": "^1.1.0", - "is-generator-function": "^1.0.10", - "is-regex": "^1.2.1", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.1.0", - "which-collection": "^1.0.2", - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz", - "integrity": "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/web/ui/dashboard-react/package.json b/web/ui/dashboard-react/package.json index 73dd69d48d..502bf26472 100644 --- a/web/ui/dashboard-react/package.json +++ b/web/ui/dashboard-react/package.json @@ -11,20 +11,31 @@ "prettify": "prettier --log-level=warn --cache --write ./src && echo 'prettify complete!'" }, "dependencies": { - "react": "^19.0.0", - "react-dom": "^19.0.0" + "@radix-ui/react-slot": "^1.1.2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.475.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "tailwind-merge": "^3.0.1", + "tailwindcss-animate": "^1.0.7" }, "devDependencies": { "@eslint/js": "^9.19.0", - "@types/react": "^19.0.8", - "@types/react-dom": "^19.0.3", + "@tailwindcss/container-queries": "^0.1.1", + "@types/node": "^22.13.1", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", "eslint": "^9.19.0", "eslint-plugin-react": "^7.37.4", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.18", "globals": "^15.14.0", + "postcss": "^8.5.2", "prettier": "^3.5.0", + "tailwindcss": "^3.4.17", "typescript": "~5.7.2", "typescript-eslint": "^8.22.0", "vite": "^6.1.0" diff --git a/web/ui/dashboard-react/postcss.config.cjs b/web/ui/dashboard-react/postcss.config.cjs new file mode 100644 index 0000000000..f1c8dac8d2 --- /dev/null +++ b/web/ui/dashboard-react/postcss.config.cjs @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + } +} diff --git a/web/ui/dashboard-react/src/App.css b/web/ui/dashboard-react/src/App.css deleted file mode 100644 index df674c0d89..0000000000 --- a/web/ui/dashboard-react/src/App.css +++ /dev/null @@ -1,42 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/web/ui/dashboard-react/src/App.tsx b/web/ui/dashboard-react/src/App.tsx index c042a179a3..88fd167054 100644 --- a/web/ui/dashboard-react/src/App.tsx +++ b/web/ui/dashboard-react/src/App.tsx @@ -1,15 +1,19 @@ -import convoyLogo from './assets/convoy-logo-full-new.svg'; -import './App.css'; +import convoyLogo from './assets/img/svg/convoy-logo-full-new.svg'; +import { Button } from './components/ui/button'; function App() { return ( - <> -
- - Convoy logo - +
+
+ convoy +

+ The complete solution for secure, scalable, and reliable webhook delivery. +

+
+ +
- +
); } diff --git a/web/ui/dashboard-react/src/assets/.gitkeep b/web/ui/dashboard-react/src/assets/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/web/ui/dashboard-react/src/assets/css/fonts.css b/web/ui/dashboard-react/src/assets/css/fonts.css new file mode 100644 index 0000000000..bf1606b916 --- /dev/null +++ b/web/ui/dashboard-react/src/assets/css/fonts.css @@ -0,0 +1,492 @@ +/* cyrillic-ext */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 100; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2JL7W0Q5n-wU.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 100; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa0ZL7W0Q5n-wU.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 100; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2ZL7W0Q5n-wU.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 100; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1pL7W0Q5n-wU.woff2) format('woff2'); + unicode-range: U+0370-03FF; +} +/* vietnamese */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 100; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2pL7W0Q5n-wU.woff2) format('woff2'); + unicode-range: + U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, + U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 100; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7W0Q5n-wU.woff2) format('woff2'); + unicode-range: + U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, + U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 100; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7W0Q5nw.woff2) format('woff2'); + unicode-range: + U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, + U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 200; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2JL7W0Q5n-wU.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 200; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa0ZL7W0Q5n-wU.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 200; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2ZL7W0Q5n-wU.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 200; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1pL7W0Q5n-wU.woff2) format('woff2'); + unicode-range: U+0370-03FF; +} +/* vietnamese */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 200; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2pL7W0Q5n-wU.woff2) format('woff2'); + unicode-range: + U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, + U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 200; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7W0Q5n-wU.woff2) format('woff2'); + unicode-range: + U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, + U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 200; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7W0Q5nw.woff2) format('woff2'); + unicode-range: + U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, + U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 300; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2JL7W0Q5n-wU.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 300; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa0ZL7W0Q5n-wU.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 300; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2ZL7W0Q5n-wU.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 300; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1pL7W0Q5n-wU.woff2) format('woff2'); + unicode-range: U+0370-03FF; +} +/* vietnamese */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 300; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2pL7W0Q5n-wU.woff2) format('woff2'); + unicode-range: + U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, + U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 300; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7W0Q5n-wU.woff2) format('woff2'); + unicode-range: + U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, + U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 300; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7W0Q5nw.woff2) format('woff2'); + unicode-range: + U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, + U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2JL7W0Q5n-wU.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa0ZL7W0Q5n-wU.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2ZL7W0Q5n-wU.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1pL7W0Q5n-wU.woff2) format('woff2'); + unicode-range: U+0370-03FF; +} +/* vietnamese */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2pL7W0Q5n-wU.woff2) format('woff2'); + unicode-range: + U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, + U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7W0Q5n-wU.woff2) format('woff2'); + unicode-range: + U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, + U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7W0Q5nw.woff2) format('woff2'); + unicode-range: + U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, + U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2JL7W0Q5n-wU.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa0ZL7W0Q5n-wU.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2ZL7W0Q5n-wU.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1pL7W0Q5n-wU.woff2) format('woff2'); + unicode-range: U+0370-03FF; +} +/* vietnamese */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2pL7W0Q5n-wU.woff2) format('woff2'); + unicode-range: + U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, + U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7W0Q5n-wU.woff2) format('woff2'); + unicode-range: + U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, + U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7W0Q5nw.woff2) format('woff2'); + unicode-range: + U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, + U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2JL7W0Q5n-wU.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa0ZL7W0Q5n-wU.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2ZL7W0Q5n-wU.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1pL7W0Q5n-wU.woff2) format('woff2'); + unicode-range: U+0370-03FF; +} +/* vietnamese */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2pL7W0Q5n-wU.woff2) format('woff2'); + unicode-range: + U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, + U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7W0Q5n-wU.woff2) format('woff2'); + unicode-range: + U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, + U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7W0Q5nw.woff2) format('woff2'); + unicode-range: + U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, + U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2JL7W0Q5n-wU.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa0ZL7W0Q5n-wU.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2ZL7W0Q5n-wU.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1pL7W0Q5n-wU.woff2) format('woff2'); + unicode-range: U+0370-03FF; +} +/* vietnamese */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2pL7W0Q5n-wU.woff2) format('woff2'); + unicode-range: + U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, + U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7W0Q5n-wU.woff2) format('woff2'); + unicode-range: + U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, + U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url(../fonts/inter/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7W0Q5nw.woff2) format('woff2'); + unicode-range: + U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, + U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: 'Menlo Regular'; + font-style: normal; + font-weight: normal; + src: + local('Menlo Regular'), + url('../fonts/Menlo-Regular.woff') format('woff'); +} diff --git a/web/ui/dashboard-react/src/assets/css/forms.css b/web/ui/dashboard-react/src/assets/css/forms.css new file mode 100644 index 0000000000..0b539abfcd --- /dev/null +++ b/web/ui/dashboard-react/src/assets/css/forms.css @@ -0,0 +1,32 @@ +/* select.ng-invalid.ng-touched, +input.ng-invalid.ng-touched, +textarea.ng-invalid.ng-touched { + @apply border-danger-100; +} */ + +.active { + @apply transition-all duration-300 relative after:bottom-0 after:h-[3px] after:w-full after:left-0 after:right-0 after:bg-primary-100 after:absolute after:rounded-tl-16px after:rounded-tr-16px; + + span { + @apply font-semibold text-primary-100 transition-all duration-300; + } +} +input::-webkit-outer-spin-button, +input::-webkit-inner-spin-button, +input[type='checkbox'] { + -webkit-appearance: none; + appearance: none; +} + +input[type='number'] { + -moz-appearance: textfield; +} + +input[type='search']::-webkit-search-decoration, +input[type='search']::-webkit-search-cancel-button, +input[type='search']::-webkit-search-results-button, +input[type='search']::-webkit-search-results-decoration, +input[type='time']::-webkit-calendar-picker-indicator { + display: none; + -webkit-appearance: none; +} diff --git a/web/ui/dashboard-react/src/assets/fonts/Menlo-Regular.woff b/web/ui/dashboard-react/src/assets/fonts/Menlo-Regular.woff new file mode 100644 index 0000000000000000000000000000000000000000..d4cf0829f07a2e81e440aff63c3d2b1e55f82499 GIT binary patch literal 236936 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 Gf&T+&LIH*V literal 0 HcmV?d00001 diff --git a/web/ui/dashboard-react/assets/img/public-layout.png b/web/ui/dashboard-react/assets/img/public-layout.png new file mode 100644 index 0000000000000000000000000000000000000000..ff5b251975f93c7e1274a3c025834927f0aa045a GIT binary patch literal 8699 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 z + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/ui/dashboard-react/assets/svg/pattern.svg b/web/ui/dashboard-react/assets/svg/pattern.svg new file mode 100644 index 0000000000..7d075be313 --- /dev/null +++ b/web/ui/dashboard-react/assets/svg/pattern.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/ui/dashboard-react/components.json b/web/ui/dashboard-react/components.json index 51d59d26d8..275d4164cc 100644 --- a/web/ui/dashboard-react/components.json +++ b/web/ui/dashboard-react/components.json @@ -18,4 +18,4 @@ "hooks": "@/hooks" }, "iconLibrary": "lucide" -} \ No newline at end of file +} diff --git a/web/ui/dashboard-react/eslint.config.js b/web/ui/dashboard-react/eslint.config.js index add53cd1a7..c83bcb9b35 100644 --- a/web/ui/dashboard-react/eslint.config.js +++ b/web/ui/dashboard-react/eslint.config.js @@ -2,6 +2,7 @@ import js from '@eslint/js'; import globals from 'globals'; import reactHooks from 'eslint-plugin-react-hooks'; import reactRefresh from 'eslint-plugin-react-refresh'; +import eslint from '@eslint/js'; import tseslint from 'typescript-eslint'; import react from 'eslint-plugin-react'; @@ -9,26 +10,44 @@ export default tseslint.config( { ignores: ['dist'] }, { settings: { react: { version: '18.3' } }, - extends: [js.configs.recommended, ...tseslint.configs.strictTypeChecked], + extends: [ + js.configs.recommended, + eslint.configs.recommended, + tseslint.configs.recommended, + // ...tseslint.configs.strictTypeChecked + ], + parser: '@typescript-eslint/parser', files: ['**/*.{ts,tsx}'], languageOptions: { ecmaVersion: 'latest', globals: globals.browser, parserOptions: { project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname - } + tsconfigRootDir: import.meta.dirname, + }, }, plugins: { 'react-hooks': reactHooks, 'react-refresh': reactRefresh, - react + react, }, rules: { ...reactHooks.configs.recommended.rules, - 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], ...react.configs.recommended.rules, - ...react.configs['jsx-runtime'].rules - } - } + ...react.configs['jsx-runtime'].rules, + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + ignoreRestSiblings: true, + caughtErrors: 'none', + }, + ], + }, + }, ); diff --git a/web/ui/dashboard-react/index.html b/web/ui/dashboard-react/index.html index d96c668ddb..492e025a98 100644 --- a/web/ui/dashboard-react/index.html +++ b/web/ui/dashboard-react/index.html @@ -9,6 +9,593 @@

+ + diff --git a/web/ui/dashboard-react/package.json b/web/ui/dashboard-react/package.json index 502bf26472..8482c2ab5a 100644 --- a/web/ui/dashboard-react/package.json +++ b/web/ui/dashboard-react/package.json @@ -1,43 +1,53 @@ { - "name": "dashboard-react", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite --port 5006", - "build": "tsc -b && vite build", - "lint": "eslint .", - "preview": "vite preview", - "prettify": "prettier --log-level=warn --cache --write ./src && echo 'prettify complete!'" - }, - "dependencies": { - "@radix-ui/react-slot": "^1.1.2", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "lucide-react": "^0.475.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "tailwind-merge": "^3.0.1", - "tailwindcss-animate": "^1.0.7" - }, - "devDependencies": { - "@eslint/js": "^9.19.0", - "@tailwindcss/container-queries": "^0.1.1", - "@types/node": "^22.13.1", - "@types/react": "^18.3.18", - "@types/react-dom": "^18.3.5", - "@vitejs/plugin-react": "^4.3.4", - "autoprefixer": "^10.4.20", - "eslint": "^9.19.0", - "eslint-plugin-react": "^7.37.4", - "eslint-plugin-react-hooks": "^5.0.0", - "eslint-plugin-react-refresh": "^0.4.18", - "globals": "^15.14.0", - "postcss": "^8.5.2", - "prettier": "^3.5.0", - "tailwindcss": "^3.4.17", - "typescript": "~5.7.2", - "typescript-eslint": "^8.22.0", - "vite": "^6.1.0" - } + "name": "dashboard-react", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --port 6005", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview", + "prettify": "prettier --log-level=warn --cache --write ./src && echo 'prettify complete!'" + }, + "dependencies": { + "@hookform/resolvers": "^4.0.0", + "@radix-ui/colors": "^3.0.0", + "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-slot": "^1.1.2", + "@tanstack/react-router": "^1.106.0", + "axios": "^1.7.9", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.475.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-hook-form": "^7.54.2", + "tailwind-merge": "^3.0.1", + "tailwindcss-animate": "^1.0.7", + "zod": "^3.24.2" + }, + "devDependencies": { + "@eslint/js": "^9.19.0", + "@tailwindcss/container-queries": "^0.1.1", + "@tanstack/router-devtools": "^1.106.0", + "@tanstack/router-plugin": "^1.106.0", + "@types/node": "^22.13.1", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "@typescript-eslint/parser": "^8.24.1", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "eslint": "^9.19.0", + "eslint-plugin-react": "^7.37.4", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.18", + "globals": "^15.14.0", + "postcss": "^8.5.2", + "prettier": "^3.5.0", + "tailwindcss": "^3.4.17", + "typescript": "~5.7.2", + "typescript-eslint": "^8.22.0", + "vite": "^6.1.0" + } } diff --git a/web/ui/dashboard-react/src/App.tsx b/web/ui/dashboard-react/src/App.tsx deleted file mode 100644 index 88fd167054..0000000000 --- a/web/ui/dashboard-react/src/App.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import convoyLogo from './assets/img/svg/convoy-logo-full-new.svg'; -import { Button } from './components/ui/button'; - -function App() { - return ( -
-
- convoy -

- The complete solution for secure, scalable, and reliable webhook delivery. -

-
- -
-
-
- ); -} - -export default App; diff --git a/web/ui/dashboard-react/src/app/__root.tsx b/web/ui/dashboard-react/src/app/__root.tsx new file mode 100644 index 0000000000..c8fec1007e --- /dev/null +++ b/web/ui/dashboard-react/src/app/__root.tsx @@ -0,0 +1,22 @@ +import { lazy, Suspense } from 'react'; +import { createRootRoute, Outlet } from '@tanstack/react-router'; +import { isProductionMode } from '@/lib/env'; + +const TanStackRouterDevTools = isProductionMode + ? () => null + : lazy(() => + import('@tanstack/router-devtools').then(res => ({ + default: res.TanStackRouterDevtools, + })), + ); + +export const Route = createRootRoute({ + component: () => ( + <> + + + + + + ), +}); diff --git a/web/ui/dashboard-react/src/app/forgot-password.tsx b/web/ui/dashboard-react/src/app/forgot-password.tsx new file mode 100644 index 0000000000..4b513f7c31 --- /dev/null +++ b/web/ui/dashboard-react/src/app/forgot-password.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router'; + +export const Route = createFileRoute('/forgot-password')({ + component: RouteComponent, +}); + +function RouteComponent() { + return

Forgot Password

; +} diff --git a/web/ui/dashboard-react/src/app/index.tsx b/web/ui/dashboard-react/src/app/index.tsx new file mode 100644 index 0000000000..7aa57d61ce --- /dev/null +++ b/web/ui/dashboard-react/src/app/index.tsx @@ -0,0 +1,19 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { router } from '@/lib/router'; +import { ensureCanAccessPrivatePages } from '@/lib/auth'; + +export const Route = createFileRoute('/')({ + beforeLoad() { + ensureCanAccessPrivatePages(); + router.navigate({ + to: '/projects', + // @ts-expect-error `pathname` is a defined route + from: router.state.location.pathname, + }); + }, + component: Index, +}); + +function Index() { + return
; +} diff --git a/web/ui/dashboard-react/src/app/login.tsx b/web/ui/dashboard-react/src/app/login.tsx new file mode 100644 index 0000000000..fe698b93e1 --- /dev/null +++ b/web/ui/dashboard-react/src/app/login.tsx @@ -0,0 +1,363 @@ +import { Button } from '@/components/ui/button'; +import { + FormField, + FormItem, + FormLabel, + FormControl, + FormMessageWithErrorIcon, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Form } from '@/components/ui/form'; +import { cn } from '@/lib/utils'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { EyeIcon, EyeOffIcon } from 'lucide-react'; +import { useEffect, useReducer, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { createFileRoute, useNavigate } from '@tanstack/react-router'; +import { z } from 'zod'; +import { ConvoyLoader } from '@/components/ConvoyLoader'; +import * as loginService from '@/services/login.service'; +import * as signUpService from '@/services/signup.service'; +import * as privateService from '@/services/private.service'; +import * as licensesService from '@/services/licenses.service'; + +import type { UseFormReturn } from 'react-hook-form'; + +const formSchema = z.object({ + email: z.string().email('Please enter your email'), + password: z.string().min(1, 'Please enter your password'), +}); + +type EmailInputFieldProps = { + form: UseFormReturn>; +}; + +function EmailInputField({ form }: EmailInputFieldProps) { + return ( + ( + +
+ Email +
+ + + + +
+ )} + /> + ); +} + +type PasswordInputFieldProps = { + form: UseFormReturn>; +}; + +function PasswordInputField({ form }: PasswordInputFieldProps) { + const [isPasswordVisible, setIsPasswordVisible] = useState(false); + + return ( + ( + +
+ + Password + +
+ +
+ + +
+
+ +
+ )} + /> + ); +} + +function ForgotPasswordSection() { + const navigate = useNavigate(); + + function navigateToPasswordPage() { + navigate({ + from: '/login', + to: '/forgot-password', + }); + } + + return ( +
+ Forgot password? + +
+ ); +} + +function LoginButton(props: { isButtonEnabled?: boolean }) { + const { isButtonEnabled } = props; + + return ( + + ); +} + +function LoginWithSAMLButton() { + async function login() { + localStorage.setItem('AUTH_TYPE', 'login'); + + try { + const res = await loginService.loginWithSAML(); + const { redirectUrl } = res.data; + window.open(redirectUrl); + } catch (error) { + // TODO should notify user here with UI + throw error; + } + } + + return ( + + ); +} + +function SignUpButton() { + const navigate = useNavigate(); + + function navigateToSignUpPage() { + navigate({ + from: '/login', + to: '/signup', + }); + } + + return ( + + ); +} + +type ReducerPayload = Partial<{ + isSignUpEnabled: boolean; + isFetchingConfig: boolean; + isLoadingProject: boolean; + hasCreateUserLicense: boolean; + isLoginButtonEnabled: boolean; +}>; + +const initialReducerState = { + isSignUpEnabled: false, + isFetchingConfig: false, + isLoadingProject: false, + isLoginButtonEnabled: true, + hasCreateUserLicense: false, +}; + +function reducer(state: ReducerPayload, payload: ReducerPayload) { + return { + ...state, + ...payload, + }; +} + +function LoginPage() { + const navigate = useNavigate(); + const [state, dispatchState] = useReducer(reducer, initialReducerState); + + useEffect(function () { + getSignUpConfig(); + licensesService.setLicenses(); + const hasCreateUserLicense = licensesService.hasLicense('CREATE_USER'); + dispatchState({ hasCreateUserLicense }); + }, []); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + email: '', + password: '', + }, + mode: 'onTouched', + }); + + async function login(values: z.infer) { + dispatchState({ isLoginButtonEnabled: false }); + + try { + await loginService.login(values); + dispatchState({ isLoadingProject: true }); + await getOrganisations(); + dispatchState({ isLoginButtonEnabled: true, isLoadingProject: false }); + + navigate({ + to: '/', + from: '/login', + }); + } catch (err) { + // TODO notify user using the UI + console.error(login.name, err); + } + } + + async function getSignUpConfig() { + dispatchState({ isFetchingConfig: true }); + try { + const { data } = await signUpService.getSignUpConfig(); + dispatchState({ isSignUpEnabled: data }); + } catch (err) { + // TODO notify user using the UI + console.error(getSignUpConfig.name, err); + } finally { + dispatchState({ isFetchingConfig: false }); + } + } + + async function getOrganisations() { + try { + await privateService.getOrganisations({ refresh: true }); + } catch (err) { + console.error(getOrganisations.name, err); + } + } + + return ( + <> +
+ +
+
+ convoy logo + +
+
+ void form.handleSubmit(login)(...args)} + > + + + + + + + + + + + + + {state.isSignUpEnabled && state.hasCreateUserLicense && ( + + )} +
+
+
+
+ + + + ); +} + +export const Route = createFileRoute('/login')({ + component: LoginPage, +}); + +// TODO loginService and other impure extraneous deps should be injected as a +// dependency for testing and flexibility/maintainability diff --git a/web/ui/dashboard-react/src/app/projects/index.tsx b/web/ui/dashboard-react/src/app/projects/index.tsx new file mode 100644 index 0000000000..95fa9320f9 --- /dev/null +++ b/web/ui/dashboard-react/src/app/projects/index.tsx @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { ensureCanAccessPrivatePages } from '@/lib/auth'; + +export const Route = createFileRoute('/projects/')({ + beforeLoad() { + ensureCanAccessPrivatePages(); + }, + component: RouteComponent, +}); + +function RouteComponent() { + return

Projects

; +} diff --git a/web/ui/dashboard-react/src/app/signup.tsx b/web/ui/dashboard-react/src/app/signup.tsx new file mode 100644 index 0000000000..b6eb89fceb --- /dev/null +++ b/web/ui/dashboard-react/src/app/signup.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router'; + +export const Route = createFileRoute('/signup')({ + component: SignUpPage, +}); + +function SignUpPage() { + return

Sign Up

; +} diff --git a/web/ui/dashboard-react/src/assets/img/svg/convoy-logo-full-new.svg b/web/ui/dashboard-react/src/assets/img/svg/convoy-logo-full-new.svg deleted file mode 100644 index 31d71d993e..0000000000 --- a/web/ui/dashboard-react/src/assets/img/svg/convoy-logo-full-new.svg +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/ui/dashboard-react/src/components/ConvoyLoader.tsx b/web/ui/dashboard-react/src/components/ConvoyLoader.tsx new file mode 100644 index 0000000000..7784d773a8 --- /dev/null +++ b/web/ui/dashboard-react/src/components/ConvoyLoader.tsx @@ -0,0 +1,29 @@ +import { cn } from '@/lib/utils'; + +type ConvoyLoaderProps = { + isTransparent: boolean; + position?: 'absolute' | 'fixed' | 'relative'; + isVisible?: boolean; +}; + +export function ConvoyLoader(props: ConvoyLoaderProps) { + const { isTransparent, position = 'absolute', isVisible = false } = props; + + if (isVisible == false) return null; + + return ( +
+ loader +
+ ); +} diff --git a/web/ui/dashboard-react/src/components/ui/button.tsx b/web/ui/dashboard-react/src/components/ui/button.tsx index 4127e856b3..85a64c2be9 100644 --- a/web/ui/dashboard-react/src/components/ui/button.tsx +++ b/web/ui/dashboard-react/src/components/ui/button.tsx @@ -9,12 +9,14 @@ const buttonVariants = cva( { variants: { variant: { - default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90', + default: + 'bg-primary text-primary-foreground shadow hover:bg-primary/90', destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90', outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground', - secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80', + secondary: + 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80', ghost: 'hover:bg-accent hover:text-accent-foreground', link: 'text-primary underline-offset-4 hover:underline', }, diff --git a/web/ui/dashboard-react/src/components/ui/form.tsx b/web/ui/dashboard-react/src/components/ui/form.tsx new file mode 100644 index 0000000000..d91624bed8 --- /dev/null +++ b/web/ui/dashboard-react/src/components/ui/form.tsx @@ -0,0 +1,188 @@ +import * as React from 'react'; +import * as LabelPrimitive from '@radix-ui/react-label'; +import { Slot } from '@radix-ui/react-slot'; +import { Controller, FormProvider, useFormContext } from 'react-hook-form'; + +import { cn } from '@/lib/utils'; +import { Label } from '@/components/ui/label'; + +import type { ControllerProps, FieldPath, FieldValues } from 'react-hook-form'; + +const Form = FormProvider; + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +> = { + name: TName; +}; + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue, +); + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + ...props +}: ControllerProps) => { + return ( + + + + ); +}; + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext); + const itemContext = React.useContext(FormItemContext); + const { getFieldState, formState } = useFormContext(); + + const fieldState = getFieldState(fieldContext.name, formState); + + if (!fieldContext) { + throw new Error('useFormField should be used within '); + } + + const { id } = itemContext; + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + }; +}; + +type FormItemContextValue = { + id: string; +}; + +const FormItemContext = React.createContext( + {} as FormItemContextValue, +); + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId(); + + return ( + +
+ + ); +}); +FormItem.displayName = 'FormItem'; + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField(); + + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/web/ui/dashboard-react/src/index.css b/web/ui/dashboard-react/src/index.css index 8201aa85df..853195bbe6 100644 --- a/web/ui/dashboard-react/src/index.css +++ b/web/ui/dashboard-react/src/index.css @@ -169,3 +169,14 @@ @apply text-neutral-6; } } + + + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/web/ui/dashboard-react/src/lib/constants.ts b/web/ui/dashboard-react/src/lib/constants.ts index 48b482bf6a..9b6accbc3c 100644 --- a/web/ui/dashboard-react/src/lib/constants.ts +++ b/web/ui/dashboard-react/src/lib/constants.ts @@ -1,3 +1,5 @@ export const CONVOY_AUTH_KEY = 'CONVOY_AUTH'; export const CONVOY_AUTH_TOKENS_KEY = 'CONVOY_AUTH_TOKENS'; -export const CONVOY_LAST_AUTH_LOCATION = "CONVOY_LAST_AUTH_LOCATION" +export const CONVOY_LAST_AUTH_LOCATION_KEY = 'CONVOY_LAST_AUTH_LOCATION'; +export const CONVOY_DASHBOARD_DOMAIN = 'dashboard.convoy.io'; +export const CONVOY_LICENSES_KEY = 'CONVOY_LICENSES'; diff --git a/web/ui/dashboard-react/src/routes.gen.ts b/web/ui/dashboard-react/src/routes.gen.ts index 872b9b2c66..bb57e11c93 100644 --- a/web/ui/dashboard-react/src/routes.gen.ts +++ b/web/ui/dashboard-react/src/routes.gen.ts @@ -13,6 +13,7 @@ import { Route as rootRoute } from './app/__root' import { Route as SignupImport } from './app/signup' import { Route as LoginImport } from './app/login' +import { Route as GetStartedImport } from './app/get-started' import { Route as ForgotPasswordImport } from './app/forgot-password' import { Route as IndexImport } from './app/index' import { Route as ProjectsIndexImport } from './app/projects/index' @@ -31,6 +32,12 @@ const LoginRoute = LoginImport.update({ getParentRoute: () => rootRoute, } as any) +const GetStartedRoute = GetStartedImport.update({ + id: '/get-started', + path: '/get-started', + getParentRoute: () => rootRoute, +} as any) + const ForgotPasswordRoute = ForgotPasswordImport.update({ id: '/forgot-password', path: '/forgot-password', @@ -67,6 +74,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ForgotPasswordImport parentRoute: typeof rootRoute } + '/get-started': { + id: '/get-started' + path: '/get-started' + fullPath: '/get-started' + preLoaderRoute: typeof GetStartedImport + parentRoute: typeof rootRoute + } '/login': { id: '/login' path: '/login' @@ -96,6 +110,7 @@ declare module '@tanstack/react-router' { export interface FileRoutesByFullPath { '/': typeof IndexRoute '/forgot-password': typeof ForgotPasswordRoute + '/get-started': typeof GetStartedRoute '/login': typeof LoginRoute '/signup': typeof SignupRoute '/projects': typeof ProjectsIndexRoute @@ -104,6 +119,7 @@ export interface FileRoutesByFullPath { export interface FileRoutesByTo { '/': typeof IndexRoute '/forgot-password': typeof ForgotPasswordRoute + '/get-started': typeof GetStartedRoute '/login': typeof LoginRoute '/signup': typeof SignupRoute '/projects': typeof ProjectsIndexRoute @@ -113,6 +129,7 @@ export interface FileRoutesById { __root__: typeof rootRoute '/': typeof IndexRoute '/forgot-password': typeof ForgotPasswordRoute + '/get-started': typeof GetStartedRoute '/login': typeof LoginRoute '/signup': typeof SignupRoute '/projects/': typeof ProjectsIndexRoute @@ -120,13 +137,26 @@ export interface FileRoutesById { export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/forgot-password' | '/login' | '/signup' | '/projects' + fullPaths: + | '/' + | '/forgot-password' + | '/get-started' + | '/login' + | '/signup' + | '/projects' fileRoutesByTo: FileRoutesByTo - to: '/' | '/forgot-password' | '/login' | '/signup' | '/projects' + to: + | '/' + | '/forgot-password' + | '/get-started' + | '/login' + | '/signup' + | '/projects' id: | '__root__' | '/' | '/forgot-password' + | '/get-started' | '/login' | '/signup' | '/projects/' @@ -136,6 +166,7 @@ export interface FileRouteTypes { export interface RootRouteChildren { IndexRoute: typeof IndexRoute ForgotPasswordRoute: typeof ForgotPasswordRoute + GetStartedRoute: typeof GetStartedRoute LoginRoute: typeof LoginRoute SignupRoute: typeof SignupRoute ProjectsIndexRoute: typeof ProjectsIndexRoute @@ -144,6 +175,7 @@ export interface RootRouteChildren { const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, ForgotPasswordRoute: ForgotPasswordRoute, + GetStartedRoute: GetStartedRoute, LoginRoute: LoginRoute, SignupRoute: SignupRoute, ProjectsIndexRoute: ProjectsIndexRoute, @@ -161,6 +193,7 @@ export const routeTree = rootRoute "children": [ "/", "/forgot-password", + "/get-started", "/login", "/signup", "/projects/" @@ -172,6 +205,9 @@ export const routeTree = rootRoute "/forgot-password": { "filePath": "forgot-password.tsx" }, + "/get-started": { + "filePath": "get-started.tsx" + }, "/login": { "filePath": "login.tsx" }, diff --git a/web/ui/dashboard-react/src/services/http.service.ts b/web/ui/dashboard-react/src/services/http.service.ts index 4353fa7dfb..b3e3d8c22b 100644 --- a/web/ui/dashboard-react/src/services/http.service.ts +++ b/web/ui/dashboard-react/src/services/http.service.ts @@ -1,7 +1,7 @@ import axios from 'axios'; import { router } from '../lib/router'; import { isProductionMode } from '@/lib/env'; -import { CONVOY_LAST_AUTH_LOCATION } from '@/lib/constants'; +import { CONVOY_LAST_AUTH_LOCATION_KEY } from '@/lib/constants'; import type { HttpResponse } from '@/models/global.model'; @@ -182,7 +182,7 @@ export async function request(requestDetails: { const requestHeader = { Authorization: `Bearer ${getToken() || authDetails().access_token || ''}`, - ...(isProductionMode && { 'X-Convoy-Version': '2024-04-01' }), + ...(isProductionMode && { 'X-Convoy-Version': '2024-04-01' }), // TODO confirm from @RT if this is permitted on the server }; try { @@ -208,7 +208,7 @@ export async function request(requestDetails: { export function logUserOut() { // save previous location before session timeout if (!router.state.location.pathname.startsWith('/login')) { - localStorage.setItem(CONVOY_LAST_AUTH_LOCATION, location.href); + localStorage.setItem(CONVOY_LAST_AUTH_LOCATION_KEY, location.href); } // then move user to login page diff --git a/web/ui/dashboard-react/src/services/hubspot.service.ts b/web/ui/dashboard-react/src/services/hubspot.service.ts new file mode 100644 index 0000000000..f38e6013c5 --- /dev/null +++ b/web/ui/dashboard-react/src/services/hubspot.service.ts @@ -0,0 +1,31 @@ +import axios from 'axios'; + +type SendWelcomeEmailParams = { + email: string; + firstname: string; + lastname: string; +}; + +function createWelcomeEmailUrl(args: SendWelcomeEmailParams) { + const { email, firstname, lastname } = args; + return `https://faas-fra1-afec6ce7.doserverless.co/api/v1/web/fn-8f44e6aa-e5d6-4e31-b781-5080c050bb37/welcome-user/welcome-mail?email=${email}&firstname=${firstname}&lastname=${lastname}`; +} + +export async function sendWelcomeEmail( + args: SendWelcomeEmailParams, + deps: { httpGet: typeof axios.get } = { httpGet: axios.create().get }, +) { + try { + const { data } = await deps.httpGet(createWelcomeEmailUrl(args)); + + return data; + } catch (err) { + if (axios.isAxiosError(err)) { + console.log('hubspot error message: ', err.message); + return err.message; + } + + console.log('hubspot unexpected error: ', err); + throw new Error('An hubspot unexpected error occurred'); + } +} diff --git a/web/ui/dashboard-react/src/services/licenses.service.ts b/web/ui/dashboard-react/src/services/licenses.service.ts index 4a9b089929..2091057c01 100644 --- a/web/ui/dashboard-react/src/services/licenses.service.ts +++ b/web/ui/dashboard-react/src/services/licenses.service.ts @@ -1,4 +1,6 @@ import { request } from './http.service'; +import { isProductionMode } from '@/lib/env'; +import { CONVOY_LICENSES_KEY } from '@/lib/constants'; type License = Record; @@ -26,20 +28,21 @@ export async function setLicenses( if (res) { const allowedLicenses = Object.entries(res.data).reduce>( (acc, [key, { allowed }]) => { + if (!isProductionMode) return acc.concat(key); if (allowed) return acc.concat(key); return acc; }, [], ); - localStorage.setItem('licenses', JSON.stringify(allowedLicenses)); + localStorage.setItem(CONVOY_LICENSES_KEY, JSON.stringify(allowedLicenses)); } } type LicenseKey = 'CREATE_USER' | 'CREATE_PROJECT' | 'CREATE_ORG'; export function hasLicense(license: LicenseKey): boolean { - const savedLicenses = localStorage.getItem('licenses'); + const savedLicenses = localStorage.getItem(CONVOY_LICENSES_KEY); if (savedLicenses) { const licenses: Array = JSON.parse(savedLicenses); diff --git a/web/ui/dashboard-react/src/services/signup.service.ts b/web/ui/dashboard-react/src/services/signup.service.ts index f63e1f2121..fceeac4c2c 100644 --- a/web/ui/dashboard-react/src/services/signup.service.ts +++ b/web/ui/dashboard-react/src/services/signup.service.ts @@ -1,7 +1,5 @@ import { request } from './http.service'; -import type { HttpResponse } from '@/models/global.model'; - type SignUpParams = { email: string; password: string; @@ -20,23 +18,32 @@ export async function signUp( method: 'post', }); + localStorage.setItem('CONVOY_AUTH', JSON.stringify(res.data)); + // TODO set type for res.data + // @ts-expect-error coming to this soonest TODO + localStorage.setItem('CONVOY_AUTH_TOKENS', JSON.stringify(res.data.token)); + return res; } -export async function getSignUpConfig(): Promise> { - // deps: { httpReq: typeof request } = { httpReq: request }, - // const res = await deps.httpReq({ - // url: '/configuration/is_sign_up_enabled', - // method: 'get', - // }); +export async function getSignUpConfig( + deps: { httpReq: typeof request } = { httpReq: request }, +) { + const res = await deps.httpReq({ + url: '/configuration/is_signup_enabled', + method: 'get', + }); - return { data: true, message: '', status: true }; + return res; } export async function signUpWithSAML( deps: { httpReq: typeof request } = { httpReq: request }, ) { - const res = await deps.httpReq({ url: '/auth/sso', method: 'get' }); + const res = await deps.httpReq<{ redirectUrl: string }>({ + url: '/auth/sso', + method: 'get', + }); return res; } From 8ba08aae54792f55b231fc72d5b7fa3ded75bb68 Mon Sep 17 00:00:00 2001 From: Orim Dominic Adah Date: Mon, 3 Mar 2025 21:05:56 +0100 Subject: [PATCH 05/43] feat: dashboard UI framework (#2253) * feat: copy migration files to container (#2238) * fix: put event types creation behind the license (#2239) * Add manual instrumentation (#2240) * fix: add manual instrumentation * fix: pass context to manual instrumentation * fix: add manual instrumentation for sentry tracer backend * fix: add generic function for manual instrumentation * fix: add context to db hooks; add span to http client * fix: use convoy tracer backend to build trace * fix: fixed dispatcher tests * fix: add http event trace breakdown * Re-enable Meta-Events (#2244) * fix: fix queries that weren't using indexes because of its column type * fix: re-enable meta-events; event delivery meta-events should run in the foreground. * fix: reindex index :) * Improve Tracing: Add Event ID To Workflow Stages (#2245) * improve tracing; add event id to workflow stages * add tracer mocks to tests * resolve PR comments * Update Changelog (#2247) * update changelog * Bump version to v25.2.1 * fixed rate limit not deleting for projects and endpoints (#2246) * fixed rate limit not deleting for projects and endpoints * fixed lint issues * fixed test * refactor: identifier names, prettier * feat: include necessary ui components * refactor: services * refactor: services * feat: add supporting functions * feat: set up to navigation and side bar --------- Co-authored-by: Subomi Oluwalana Co-authored-by: Raymond Tukpe Co-authored-by: Raymond Tukpe Co-authored-by: Smart Mekiliuwa --- CHANGELOG.md | 443 +++++----- VERSION | 2 +- api/server_suite_test.go | 4 +- cmd/hooks/hooks.go | 1 + cmd/worker/worker.go | 29 +- database/hooks/hooks.go | 9 +- database/listener/endpoint_listener.go | 17 +- database/listener/event_delivery_listener.go | 8 +- database/listener/project_listener.go | 7 +- database/postgres/endpoint.go | 6 +- database/postgres/event_delivery.go | 3 +- database/postgres/postgres_test.go | 2 +- database/postgres/project.go | 3 +- datastore/models.go | 4 +- ee/VERSION | 2 +- generate.go | 1 + internal/pkg/keys/hcpvault_test.go | 1 - internal/pkg/socket/client_test.go | 8 +- internal/pkg/tracer/datadog.go | 76 +- internal/pkg/tracer/noop.go | 20 + internal/pkg/tracer/otel.go | 33 +- internal/pkg/tracer/sentry.go | 33 +- internal/pkg/tracer/tracer.go | 24 +- mocks/tracer.go | 101 ++- net/dispatcher.go | 179 +++- net/dispatcher_test.go | 21 +- package-lock.json | 3 - release.Dockerfile | 15 +- services/create_meta_event.go | 6 +- services/create_meta_event_test.go | 3 +- services/update_endpoint.go | 142 ++-- sql/1739961493.sql | 81 ++ testcon/docker_e2e_integration_test_helper.go | 2 +- .../dashboard-react/assets/svg/user-icon.svg | 4 + web/ui/dashboard-react/package.json | 8 + web/ui/dashboard-react/src/app/__root.tsx | 2 +- .../src/app/forgot-password.tsx | 2 +- .../dashboard-react/src/app/get-started.tsx | 2 +- web/ui/dashboard-react/src/app/index.tsx | 6 +- web/ui/dashboard-react/src/app/login.tsx | 6 +- .../organisations/$organisationId/index.tsx | 9 + .../$organisationId/settings.tsx | 11 + .../src/app/organisations/index.tsx | 9 + .../src/app/projects/index.tsx | 7 +- .../dashboard-react/src/app/user-settings.tsx | 10 + .../{ConvoyLoader.tsx => convoy-loader.tsx} | 0 .../src/components/dashboard-header.tsx | 257 ++++++ .../src/components/dashboard-sidebar.tsx | 343 ++++++++ .../src/components/layout/dashboard.tsx | 42 + .../src/components/ui/avatar.tsx | 48 ++ .../src/components/ui/collapsible.tsx | 9 + .../src/components/ui/command.tsx | 151 ++++ .../src/components/ui/dialog.tsx | 122 +++ .../src/components/ui/dropdown-menu.tsx | 201 +++++ .../src/components/ui/popover.tsx | 31 + .../src/components/ui/separator.tsx | 29 + .../src/components/ui/sheet.tsx | 140 ++++ .../src/components/ui/sidebar.tsx | 780 ++++++++++++++++++ .../src/components/ui/skeleton.tsx | 15 + .../src/components/ui/tooltip.tsx | 30 + .../dashboard-react/src/hooks/use-mobile.tsx | 19 + web/ui/dashboard-react/src/index.css | 16 + web/ui/dashboard-react/src/lib/auth.ts | 4 +- web/ui/dashboard-react/src/lib/constants.ts | 2 + web/ui/dashboard-react/src/lib/pipes.ts | 15 + .../src/models/organisation.model.ts | 18 +- .../src/models/project.model.ts | 80 ++ web/ui/dashboard-react/src/routes.gen.ts | 109 ++- .../src/services/auth.service.ts | 88 ++ .../src/services/http.service.ts | 100 +-- .../src/services/licenses.service.ts | 35 +- .../src/services/login.service.ts | 2 +- .../src/services/organisations.service.ts | 71 ++ .../src/services/private.service.ts | 43 - .../src/services/projects.service.ts | 52 ++ web/ui/dashboard-react/tailwind.config.js | 38 +- web/ui/dashboard-react/tsconfig.app.json | 2 +- web/ui/dashboard-react/tsconfig.json | 2 +- .../create-endpoint.component.html | 12 +- .../create-endpoint.component.ts | 43 +- .../create-project-component.component.html | 26 +- .../create-project-component.component.ts | 38 +- worker/consumer.go | 8 +- worker/task/expire_secret_test.go | 2 +- .../task/process_broadcast_event_creation.go | 45 +- .../process_broadcast_event_creation_test.go | 9 +- worker/task/process_dynamic_event_creation.go | 42 +- .../process_dynamic_event_creation_test.go | 14 +- worker/task/process_event_channel.go | 66 +- worker/task/process_event_creation.go | 32 +- worker/task/process_event_creation_test.go | 197 +++-- worker/task/process_event_delivery.go | 60 +- worker/task/process_event_delivery_test.go | 52 +- worker/task/process_meta_event.go | 16 +- worker/task/process_retry_event_delivery.go | 56 +- .../task/process_retry_event_delivery_test.go | 158 ++-- worker/task/retention_policies_test.go | 2 +- 97 files changed, 4285 insertions(+), 852 deletions(-) create mode 100644 internal/pkg/tracer/noop.go delete mode 100644 package-lock.json create mode 100644 sql/1739961493.sql create mode 100644 web/ui/dashboard-react/assets/svg/user-icon.svg create mode 100644 web/ui/dashboard-react/src/app/organisations/$organisationId/index.tsx create mode 100644 web/ui/dashboard-react/src/app/organisations/$organisationId/settings.tsx create mode 100644 web/ui/dashboard-react/src/app/organisations/index.tsx create mode 100644 web/ui/dashboard-react/src/app/user-settings.tsx rename web/ui/dashboard-react/src/components/{ConvoyLoader.tsx => convoy-loader.tsx} (100%) create mode 100644 web/ui/dashboard-react/src/components/dashboard-header.tsx create mode 100644 web/ui/dashboard-react/src/components/dashboard-sidebar.tsx create mode 100644 web/ui/dashboard-react/src/components/layout/dashboard.tsx create mode 100644 web/ui/dashboard-react/src/components/ui/avatar.tsx create mode 100644 web/ui/dashboard-react/src/components/ui/collapsible.tsx create mode 100644 web/ui/dashboard-react/src/components/ui/command.tsx create mode 100644 web/ui/dashboard-react/src/components/ui/dialog.tsx create mode 100644 web/ui/dashboard-react/src/components/ui/dropdown-menu.tsx create mode 100644 web/ui/dashboard-react/src/components/ui/popover.tsx create mode 100644 web/ui/dashboard-react/src/components/ui/separator.tsx create mode 100644 web/ui/dashboard-react/src/components/ui/sheet.tsx create mode 100644 web/ui/dashboard-react/src/components/ui/sidebar.tsx create mode 100644 web/ui/dashboard-react/src/components/ui/skeleton.tsx create mode 100644 web/ui/dashboard-react/src/components/ui/tooltip.tsx create mode 100644 web/ui/dashboard-react/src/hooks/use-mobile.tsx create mode 100644 web/ui/dashboard-react/src/lib/pipes.ts create mode 100644 web/ui/dashboard-react/src/models/project.model.ts create mode 100644 web/ui/dashboard-react/src/services/auth.service.ts create mode 100644 web/ui/dashboard-react/src/services/organisations.service.ts delete mode 100644 web/ui/dashboard-react/src/services/private.service.ts create mode 100644 web/ui/dashboard-react/src/services/projects.service.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c7e1a816cd..c4387221ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,184 +1,218 @@ +# 25.2.1 + +### Features + +- Improve Tracing: Add Event ID To Workflow Stages #2245 #2240 #2244 + +### Enhancements + +- Updated credential encryption #2233 #2231 +- Updated read-replicas to rely on feature flags #2226 + +### Bug Fixes + +- Fix panic in backup project data handler #2232 + # 25.1.1 ### Features -- Added database read replica support #2195 -- Create event types whenever a subscription is created or updated #2201 -- Localhost endpoints can now be configured #2216 -- Added credential encryption lifecycle management #2202 #2204 #2215 #2213 -- Added Retention Policies with Partitioning #2194 #2198 + +- Added database read replica support #2195 +- Create event types whenever a subscription is created or updated #2201 +- Localhost endpoints can now be configured #2216 +- Added credential encryption lifecycle management #2202 #2204 #2215 #2213 +- Added Retention Policies with Partitioning #2194 #2198 ### Enhancements -- Add workflow to build rolling images #2217 #2218 -- Remove cache implementation #2206 -- added host to init sso flow #2219 + +- Add workflow to build rolling images #2217 #2218 +- Remove cache implementation #2206 +- added host to init sso flow #2219 ### Bug Fixes -- Remove multiple subscription for an endpoint setting #2197 -- Fixed a bug where some gzipped bytes couldn't be inserted into the db column #2214 #2203 -- Fixed duplicate metrics collection #2220 #2221 -- Remove acknowledged_at from the event search query #2205 -- Fixed a bug where the subscription filter form didn't persist deleted configurations #2210 #2209 -- Fixed portal link UI issues with subscriptions, endpoints, and license check #2208 -- Fixed portal link creation by owner id #2207 - -# 24.11.1 - -### Features -- Added endpoint circuit breaking. #2120 -- Added rate limiting to server apis. #2166 -- Added allow and block IP Lists #2169 -- Added event types #2180 -- Added Enterprise SSO #2175 + +- Remove multiple subscription for an endpoint setting #2197 +- Fixed a bug where some gzipped bytes couldn't be inserted into the db column #2214 #2203 +- Fixed duplicate metrics collection #2220 #2221 +- Remove acknowledged_at from the event search query #2205 +- Fixed a bug where the subscription filter form didn't persist deleted configurations #2210 #2209 +- Fixed portal link UI issues with subscriptions, endpoints, and license check #2208 +- Fixed portal link creation by owner id #2207 + +# 24.11.1 + +### Features + +- Added endpoint circuit breaking. #2120 +- Added rate limiting to server apis. #2166 +- Added allow and block IP Lists #2169 +- Added event types #2180 +- Added Enterprise SSO #2175 ### Enhancements -- Removed retry configuration from subscriptions. #2161 -- Redesigned event processing system to use a workflow approach. #2131 -- Dashboard UI Improvements #2167 -- Added missing index for fetching delivery attempts #2172 -- Added version header to responses #2174 -- Optimized queries for prometheus metrics #2179 -### Bug Fixes -- Fix flatten multiple top level operator bug #2168 -- Delete an invitation after decline or accept #2171 +- Removed retry configuration from subscriptions. #2161 +- Redesigned event processing system to use a workflow approach. #2131 +- Dashboard UI Improvements #2167 +- Added missing index for fetching delivery attempts #2172 +- Added version header to responses #2174 +- Optimized queries for prometheus metrics #2179 + +### Bug Fixes + +- Fix flatten multiple top level operator bug #2168 +- Delete an invitation after decline or accept #2171 # 24.9.2 ### Enhancements -- Set CORS allow origin to the configured convoy instance host #2152 -- Properly format token expiry time forgot/reset password email #2152 -- Restrict loading of the Convoy dashboard (except portal link dashboards) in an iframe #2153 + +- Set CORS allow origin to the configured convoy instance host #2152 +- Properly format token expiry time forgot/reset password email #2152 +- Restrict loading of the Convoy dashboard (except portal link dashboards) in an iframe #2153 # 24.9.1 ### License Changes -- Moved ee rbac into community and renamed to multiplayer mode #2146 -- Setting the worker/agent execution mode can only be done using a license #2155 + +- Moved ee rbac into community and renamed to multiplayer mode #2146 +- Setting the worker/agent execution mode can only be done using a license #2155 ### Features -- Added support for inbound webhooks that use urlencoded content type #2119 + +- Added support for inbound webhooks that use urlencoded content type #2119 ### Enhancements -- HTTP ingest now uses the instance ingest rate #2156 -- Show endpoint authentication config in the UI #2141 + +- HTTP ingest now uses the instance ingest rate #2156 +- Show endpoint authentication config in the UI #2141 ### Bug Fixes -- Make forgot password response obscure #2144 + +- Make forgot password response obscure #2144 # 24.8.2 ### Bug Fixes -- fixed a retention policy bug where only empty files were uploading to s3 + +- fixed a retention policy bug where only empty files were uploading to s3 # 24.8.1 ### Features -- Added end to end latency metrics #2079 -- Add support for inbound webhooks that use form data #1998 -- Added e2e test suite using test containers #2083 -- Add license feature gating #2114 #2132 #2134 -- Change License to Elastic License v2.0 #2124 + +- Added end to end latency metrics #2079 +- Add support for inbound webhooks that use form data #1998 +- Added e2e test suite using test containers #2083 +- Add license feature gating #2114 #2132 #2134 +- Change License to Elastic License v2.0 #2124 ### Enhancements -- Move retention policy to instance config #2011 -- Update event data openapi types #2088 -- Refactor agent, worker and ingest entry points #2082 -- Refactored Exponential Backoff Implementation #2073 -- Remove instance configuration page #2085 -- Set default signature value to advanced from UI #2090 -- Add fanout for pubsub ingest #2099 -- Events ingested in incoming projects would now respond with a 413 for oversized payloads #2095 -- The agent component can now bootstrap a fresh instance #2111 -- Don't return an error when an owner id has no registered endpoints #2112 -- Split delivery attempts from event deliveries #2092 -- Add auth to metrics and queue monitoring routes #2115 -- Updated integration test suite #2100 -- Refactored feature flags implementation #2105 -- Push docker images to DockerHub #2122 -- Add owner id to event delivery response #2129 +- Move retention policy to instance config #2011 +- Update event data openapi types #2088 +- Refactor agent, worker and ingest entry points #2082 +- Refactored Exponential Backoff Implementation #2073 +- Remove instance configuration page #2085 +- Set default signature value to advanced from UI #2090 +- Add fanout for pubsub ingest #2099 +- Events ingested in incoming projects would now respond with a 413 for oversized payloads #2095 +- The agent component can now bootstrap a fresh instance #2111 +- Don't return an error when an owner id has no registered endpoints #2112 +- Split delivery attempts from event deliveries #2092 +- Add auth to metrics and queue monitoring routes #2115 +- Updated integration test suite #2100 +- Refactored feature flags implementation #2105 +- Push docker images to DockerHub #2122 +- Add owner id to event delivery response #2129 ### Bug Fixes -- Fixed a bug in positional array filter #2086 -- Fix count & batch retry queries #2089 -- Fixed a bug where api responses from v2024-04-01 to v2024-01-01 were not properly migrated #2087 -- Update UI Dependencies #2097 -- Fixed a migration bug where default column values were not set #2103 -- Fixed a bug where the wrong delay duration was used when scheduling an event delivery for retry #2110 -- Fixed a bug where other events were retried from a portal link because the endpoint filter wasn't applied #2116 -- +- Fixed a bug in positional array filter #2086 +- Fix count & batch retry queries #2089 +- Fixed a bug where api responses from v2024-04-01 to v2024-01-01 were not properly migrated #2087 +- Update UI Dependencies #2097 +- Fixed a migration bug where default column values were not set #2103 +- Fixed a bug where the wrong delay duration was used when scheduling an event delivery for retry #2110 +- Fixed a bug where other events were retried from a portal link because the endpoint filter wasn't applied #2116 # 24.6.4 -- fixed a bug where the pubsub ingester won't start when there aren't any projects +- fixed a bug where the pubsub ingester won't start when there aren't any projects # 24.6.3 ### Bug Fixes -- Fixed api migration bug #2087 + +- Fixed api migration bug #2087 # 24.6.2 ### Bug Fixes -- Remove default config value for advanced signatures #2090 +- Remove default config value for advanced signatures #2090 # 24.6.1 ### Features -- Add rate limiter to event entry points, HTTP APIs, and message brokers #2072 #2035 -- Add retry queue #2058 -- Add search bar for subscriptions #2062 -- Use asynq's exponential backoff for event delivery retries #2052 -- Add PyroScope profiling #1737 -- Added Prometheus metrics #2005 -- Endpoint subscriptions can now be created and viewed on portal links #2015 -- Subscriptions can now be filtered by name #2014 + +- Add rate limiter to event entry points, HTTP APIs, and message brokers #2072 #2035 +- Add retry queue #2058 +- Add search bar for subscriptions #2062 +- Use asynq's exponential backoff for event delivery retries #2052 +- Add PyroScope profiling #1737 +- Added Prometheus metrics #2005 +- Endpoint subscriptions can now be created and viewed on portal links #2015 +- Subscriptions can now be filtered by name #2014 ### Enhancements -- Optimise flatten and compare packages #2066 #2077 -- Changed the Postgres driver to pgx #2064 -- Optimized dashboard query and run time #2070 -- Fixed order of endpoints and subscriptions #2060 -- Optimise Subscription loader queries #2056 -- Make dispatcher proxy less strict, allowing requests to be sent if the proxy URL is invalid #2059 -- Load subscriptions synchronously on worker startup #2053 -- Remove cache from userRepo since it's not a hot path #2050 -- Added cache to retrieving subscriptions for broadcast #2044 -- Refactored net.Dispatcher - - Create dispatcher once #2043 - - Fixed a bug where we were closing the response body late #2029 -- Refactor ProcessBroadcastEventCreation and ProcessEventCreation handlers - - Deduplicate endpoint IDs #2024 -- Refactor Repository - - Add FindEventDeliveryByIDSlim #2054 - - Refactored event creation to insert in bulk #2038 - - Add indexes for FetchSubscriptionsForBroadcast #2033 + +- Optimise flatten and compare packages #2066 #2077 +- Changed the Postgres driver to pgx #2064 +- Optimized dashboard query and run time #2070 +- Fixed order of endpoints and subscriptions #2060 +- Optimise Subscription loader queries #2056 +- Make dispatcher proxy less strict, allowing requests to be sent if the proxy URL is invalid #2059 +- Load subscriptions synchronously on worker startup #2053 +- Remove cache from userRepo since it's not a hot path #2050 +- Added cache to retrieving subscriptions for broadcast #2044 +- Refactored net.Dispatcher + - Create dispatcher once #2043 + - Fixed a bug where we were closing the response body late #2029 +- Refactor ProcessBroadcastEventCreation and ProcessEventCreation handlers + - Deduplicate endpoint IDs #2024 +- Refactor Repository + - Add FindEventDeliveryByIDSlim #2054 + - Refactored event creation to insert in bulk #2038 + - Add indexes for FetchSubscriptionsForBroadcast #2033 ### Bug Fixes -- Fix dashboard summary filters #2071 -- Fix events summary date filter #2069 -- Fixed panic that occurred when updating project when setting the SSL config #2055 -- Fixed a bug where we were creating events for the number of matched subscriptions instead of the number of matched endpoints #2025 -- Fixed a bug where we would try to filter even when none is set on the subscription #2027 -- Fixed a bug where the default log level was set to debug #2013 -- Fixed a bug where portal links won't load in iframes because of a missing project reference #2008 + +- Fix dashboard summary filters #2071 +- Fix events summary date filter #2069 +- Fixed panic that occurred when updating project when setting the SSL config #2055 +- Fixed a bug where we were creating events for the number of matched subscriptions instead of the number of matched endpoints #2025 +- Fixed a bug where we would try to filter even when none is set on the subscription #2027 +- Fixed a bug where the default log level was set to debug #2013 +- Fixed a bug where portal links won't load in iframes because of a missing project reference #2008 # 24.5.1 ### Features -- Create default org when bootstrapping Convoy for the first time. #1991 -- Display user-settings page when there are no organizations. #1999 + +- Create default org when bootstrapping Convoy for the first time. #1991 +- Display user-settings page when there are no organizations. #1999 ### Enhancements -- Use transactions in ProcessBroadcastEventCreation to prevent a race condition. #1994 -- Update copy for the kafka source form giving more information and linking to docs. #2000 -- Update Endpoint table with its ID on the dashboard. #1988 + +- Use transactions in ProcessBroadcastEventCreation to prevent a race condition. #1994 +- Update copy for the kafka source form giving more information and linking to docs. #2000 +- Update Endpoint table with its ID on the dashboard. #1988 ### Bug fixes -- Fixed a panic that would occur when request body is a string during subscription filtering. #1992 -- Fixed a bug where the response from the pause endpoint api wasn't versioned correctly. #2001 + +- Fixed a panic that would occur when request body is a string during subscription filtering. #1992 +- Fixed a bug where the response from the pause endpoint api wasn't versioned correctly. #2001 # 24.4.1 @@ -187,136 +221,143 @@ > [!NOTE] > All API Changes are backward-compatible, so you shouldn't need to change any code to get them to work, however, you need to specify the version (2024-04-01) in your convoy.json. -- changed endpoint `title` to `name` -- changes endpoint `target_url` to `url` +- changed endpoint `title` to `name` +- changes endpoint `target_url` to `url` ### Features -- Implemented an in-memory store for data plane #1932 -- Re-implement rate limiter using postgres #1937 #1950 -- Add the ability to mutate payloads from message broker sources using javascript functions #1954 #1956 #1958 -- Add project config for enforcing https endpoints #1955 #1957 -- Add documentation to request models #1959 -- + +- Implemented an in-memory store for data plane #1932 +- Re-implement rate limiter using postgres #1937 #1950 +- Add the ability to mutate payloads from message broker sources using javascript functions #1954 #1956 #1958 +- Add project config for enforcing https endpoints #1955 #1957 +- Add documentation to request models #1959 +- ### Enhancements -- Encode Postgres connection string credentials #1936 -- Update endpoint `title` to `name` and `target_url` to `url` #1945 -- Enqueue Stuck Event Deliveries #1977 + +- Encode Postgres connection string credentials #1936 +- Update endpoint `title` to `name` and `target_url` to `url` #1945 +- Enqueue Stuck Event Deliveries #1977 ### Bug Fixes -- Fixed a bug where telemetry wasn't being sent to PostHog #1944 -- Fixed a bug where the signature modal in the project settings doesn't dismiss after saving. #1939 -- Fixed a bug where project settings were not displayed properly on the dashboard #1953 -- Fixed a bug where a failed subscription filter will stop all subscribers from a broadcast event from receiving the event #1962 -- Fixed open telemetry tls configuration #1966 -- Fixed a bug where a created or updated subscription didn't show the nested values #1970 -- Fixed endpoints count query for portal links #1973 -- Added data plane capabilities back to the worker which was unintentionally removed #1974 + +- Fixed a bug where telemetry wasn't being sent to PostHog #1944 +- Fixed a bug where the signature modal in the project settings doesn't dismiss after saving. #1939 +- Fixed a bug where project settings were not displayed properly on the dashboard #1953 +- Fixed a bug where a failed subscription filter will stop all subscribers from a broadcast event from receiving the event #1962 +- Fixed open telemetry tls configuration #1966 +- Fixed a bug where a created or updated subscription didn't show the nested values #1970 +- Fixed endpoints count query for portal links #1973 +- Added data plane capabilities back to the worker which was unintentionally removed #1974 # 24.1.4 -- [Enhancement] Add custom headers to dynamic event #1923 -- [Feature] Add broadcast event api #1913 +- [Enhancement] Add custom headers to dynamic event #1923 +- [Feature] Add broadcast event api #1913 # 24.1.3 ### Bug Fixes -- fixed api versioning bug to correctly retrieve the instance api version #1918 + +- fixed api versioning bug to correctly retrieve the instance api version #1918 # 24.1.2 -### Bug Fixes -- fixed sync bug in the oss telemetry library #1906 +### Bug Fixes + +- fixed sync bug in the oss telemetry library #1906 -# 24.1.1 +# 24.1.1 ### API Changes > [!NOTE] > All API Changes are backward-compatible, so you shouldn't need to change any code to get them to work, however, you need to specify which version you're running in your convoy.json. -- changed `http_timeout` and `rate_limit_duration` in endpoints from duration string to `int`. -- changed the default signature format from `simple` to `advanced`. -- stripped out unnecessary fields from dynamic api endpoint. +- changed `http_timeout` and `rate_limit_duration` in endpoints from duration string to `int`. +- changed the default signature format from `simple` to `advanced`. +- stripped out unnecessary fields from dynamic api endpoint. ### Features -- added support for OpenTelemetry #1865 -- added support for sentry as a tracing backend #1865 -- added support for api versioning using rolling versioning strategy backwards compatible #1871 + +- added support for OpenTelemetry #1865 +- added support for sentry as a tracing backend #1865 +- added support for api versioning using rolling versioning strategy backwards compatible #1871 ### Bug Fixes -- added `eventType` to `QueryListEventDelivery` #1843 -- fixed source and subscription forms #1876 -- fixed source and endpoint dropdown with search box #1850 -- fixed retrieving portal links by `endpoints` or `owner_id` #1894 -- update endpoints in cache when all the endpoints are re-enabled #1847 -- update subscription endpoint metadata when a subscription is updated #1891 -- fixed event deliveries pagination #1846 -- delete invite after cancellation #1860 -- enabled multi-user invite without refresh #1861 -- set `event_id` in events filter #1866 + +- added `eventType` to `QueryListEventDelivery` #1843 +- fixed source and subscription forms #1876 +- fixed source and endpoint dropdown with search box #1850 +- fixed retrieving portal links by `endpoints` or `owner_id` #1894 +- update endpoints in cache when all the endpoints are re-enabled #1847 +- update subscription endpoint metadata when a subscription is updated #1891 +- fixed event deliveries pagination #1846 +- delete invite after cancellation #1860 +- enabled multi-user invite without refresh #1861 +- set `event_id` in events filter #1866 ### Enhancements -- improved dynamic api support #1884 -- improved endpoints api #1870 -- QoL improvements to the api layer #1851 -- QoL improvements to retention policies export worker #1882 +- improved dynamic api support #1884 +- improved endpoints api #1870 +- QoL improvements to the api layer #1851 +- QoL improvements to retention policies export worker #1882 # 23.11.1 -- [Feature] add cache to the organisations and api key repositories, add profiling route #1822 -- [Feature] Record event delivery latency #1830 -- [Enhancement] Improve event deliveries filtering #1824 #1840 -- [Enhancement] UI layout redesign #1815 -- [Enhancement] Move scheduler functionality into server #1835 -- [Bugfix] Fixed endpoint enabling and disabling #1837 +- [Feature] add cache to the organisations and api key repositories, add profiling route #1822 +- [Feature] Record event delivery latency #1830 +- [Enhancement] Improve event deliveries filtering #1824 #1840 +- [Enhancement] UI layout redesign #1815 +- [Enhancement] Move scheduler functionality into server #1835 +- [Bugfix] Fixed endpoint enabling and disabling #1837 # 23.10.1 -- [Feature] Added bootstrap cli for user account creation #1773 -- [Feature] Add prefix configuration to S3 Config #1812 -- [Enhancement] Added TLS option for SMTP config #1784 -- [Enhancement] Added support for multi-architecture docker images #1790 -- [Enhancement] Improved docker compose to use named volumes #1804 -- [Enhancement] Replaced Flipt for a custom feature flag implementation #1797 -- [Enhancement] Added several performance improvements with caches and reduced db calls #1765 #1783 -- [Enhancement] Optimise source loader query #1806 -- [Bugfix] Added separate port for `ingest` cli #1795 -- [Bugfix] Add support for Idempotency keys in message broker integration #1800 -- [Bugfix] Fixed concurrency bug where wrong source name is show in the event log #1800 -- [Bugfix] Fixed role check for updating organization name #1805 -- [Bugfix] Fixed a bug with the portal link delete button #1807 -- [Bugfix] Fixed a bug with the endpoint config button #1810 -- [Bugfix] Removed onclickout function for dialogs #1808 -- [Bugfix] Generate exponential back-off rate limits from intervalSeconds and Limt #1813 +- [Feature] Added bootstrap cli for user account creation #1773 +- [Feature] Add prefix configuration to S3 Config #1812 +- [Enhancement] Added TLS option for SMTP config #1784 +- [Enhancement] Added support for multi-architecture docker images #1790 +- [Enhancement] Improved docker compose to use named volumes #1804 +- [Enhancement] Replaced Flipt for a custom feature flag implementation #1797 +- [Enhancement] Added several performance improvements with caches and reduced db calls #1765 #1783 +- [Enhancement] Optimise source loader query #1806 +- [Bugfix] Added separate port for `ingest` cli #1795 +- [Bugfix] Add support for Idempotency keys in message broker integration #1800 +- [Bugfix] Fixed concurrency bug where wrong source name is show in the event log #1800 +- [Bugfix] Fixed role check for updating organization name #1805 +- [Bugfix] Fixed a bug with the portal link delete button #1807 +- [Bugfix] Fixed a bug with the endpoint config button #1810 +- [Bugfix] Removed onclickout function for dialogs #1808 +- [Bugfix] Generate exponential back-off rate limits from intervalSeconds and Limt #1813 # 23.9.2 -- [Enhancement] Show invite url on teams invite page -- [Bugfix] Handle nullable 'Function' field in worker handler +- [Enhancement] Show invite url on teams invite page +- [Bugfix] Handle nullable 'Function' field in worker handler # 23.09.1 -- [Feature] Add event payload transform functionality #1755 #1761 -- [Enhancement] Add tail mode for events and event deliveries #1753 -- [Enhancement] Expose rate limiting for endpoints #1754 -- [Bugfix] Use the different queue instance when starting stream server #1769 -- [Bugfix] Return an appropriate error instead of nil the process event delivery #1756 -- [Bugfix] Add permissions when creating and revoking API keys #1762 -- [Bugfix] Add QueueUrl nil check in SQS handler #1763 -- [Bugfix] Update endpoints migration query #1768 +- [Feature] Add event payload transform functionality #1755 #1761 +- [Enhancement] Add tail mode for events and event deliveries #1753 +- [Enhancement] Expose rate limiting for endpoints #1754 +- [Bugfix] Use the different queue instance when starting stream server #1769 +- [Bugfix] Return an appropriate error instead of nil the process event delivery #1756 +- [Bugfix] Add permissions when creating and revoking API keys #1762 +- [Bugfix] Add QueueUrl nil check in SQS handler #1763 +- [Bugfix] Update endpoints migration query #1768 # 23.08.2 -- [Feature] Postgres Full Text Search Reimplementation #1734 #1751 #1750 -- [Feature] Add tail mode for events and event deliveries #1753 -- [Enhancement] Paused events polling when searching and filtering on the Event Log #1744 -- [Enhancement] Added an edit endpoint button in event delivery page #1738 -- [Enhancement] Added a tooltip for Retry and Force Retry buttons #1741 -- [Bugfix] Fixed a bug where the subscription filter editor UI was unresponsive #1747 -- [Bugfix] Fixed a bug where the Batch Replay button on the events log would not replay events #1740 -- [Bugfix] Fixed a bug in the process event delivery handler that caused events to stay in the `Scheduled` state #1756 +- [Feature] Postgres Full Text Search Reimplementation #1734 #1751 #1750 +- [Feature] Add tail mode for events and event deliveries #1753 +- [Enhancement] Paused events polling when searching and filtering on the Event Log #1744 +- [Enhancement] Added an edit endpoint button in event delivery page #1738 +- [Enhancement] Added a tooltip for Retry and Force Retry buttons #1741 +- [Bugfix] Fixed a bug where the subscription filter editor UI was unresponsive #1747 +- [Bugfix] Fixed a bug where the Batch Replay button on the events log would not replay events #1740 +- [Bugfix] Fixed a bug in the process event delivery handler that caused events to stay in the `Scheduled` state #1756 # 23.08.1 @@ -356,7 +397,6 @@ - [Bugfix] Fixed issue with overriding config with cli flags #1668 - [Bugfix] Fixed issue with HTTP timeout validation #1680 - # 23.06.2 - [Enhancement] Improved logging to include response body #1655 @@ -380,7 +420,6 @@ - [Bugfix] Fixed event delivery filtering by status #1626 - [Bugfix] Fixed index on organisations invite index #1603 #1607 - [Bugfix] Link ownerID to portal links without endpoints #1638 - # 23.05.5 diff --git a/VERSION b/VERSION index fb448fb016..7cef2a0945 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v25.1.1 +v25.2.1 diff --git a/api/server_suite_test.go b/api/server_suite_test.go index 19cc4a4e21..a638ab7a9a 100644 --- a/api/server_suite_test.go +++ b/api/server_suite_test.go @@ -5,10 +5,12 @@ package api import ( "bytes" + "context" "encoding/json" "fmt" "github.com/frain-dev/convoy/internal/pkg/fflag" "github.com/frain-dev/convoy/internal/pkg/keys" + "io" "math/rand" "net/http" @@ -97,7 +99,7 @@ func getDB() database.Database { _ = os.Setenv("TZ", "") // Use UTC by default :) dbHooks := hooks.Init() - dbHooks.RegisterHook(datastore.EndpointCreated, func(data interface{}, changelog interface{}) {}) + dbHooks.RegisterHook(datastore.EndpointCreated, func(ctx context.Context, data interface{}, changelog interface{}) {}) pDB = db }) diff --git a/cmd/hooks/hooks.go b/cmd/hooks/hooks.go index c2cd1e2f16..96e9007e8e 100644 --- a/cmd/hooks/hooks.go +++ b/cmd/hooks/hooks.go @@ -232,6 +232,7 @@ func PreRun(app *cli.App, db *postgres.Postgres) func(cmd *cobra.Command, args [ if err != nil { return err } + // todo(raymond): I don't think the check below needs to exist since we perform the check in the tracer.Init() above. if cfg.Tracer.Type == config.DatadogTracerProvider && !app.Licenser.DatadogTracing() { lo.Error("your instance does not have access to datadog tracing, upgrade to access this feature") _ = app.TracerBackend.Shutdown(context.Background()) diff --git a/cmd/worker/worker.go b/cmd/worker/worker.go index e08757a590..3273373346 100644 --- a/cmd/worker/worker.go +++ b/cmd/worker/worker.go @@ -276,6 +276,8 @@ func StartWorker(ctx context.Context, a *cli.App, cfg config.Configuration, inte a.Licenser, featureFlag, net.LoggerOption(lo), + net.TracerOption(a.TracerBackend), + net.DetailedTraceOption(true), net.ProxyOption(cfg.Server.HTTP.HttpProxy), net.AllowListOption(cfg.Dispatcher.AllowList), net.BlockListOption(cfg.Dispatcher.BlockList), @@ -364,8 +366,8 @@ func StartWorker(ctx context.Context, a *cli.App, cfg config.Configuration, inte attemptRepo, circuitBreakerManager, featureFlag, - a.TracerBackend, - ), newTelemetry) + a.TracerBackend), + newTelemetry) consumer.RegisterHandlers(convoy.CreateEventProcessor, task.ProcessEventCreation( defaultCh, @@ -375,7 +377,10 @@ func StartWorker(ctx context.Context, a *cli.App, cfg config.Configuration, inte eventDeliveryRepo, a.Queue, subRepo, - deviceRepo, a.Licenser), newTelemetry) + deviceRepo, + a.Licenser, + a.TracerBackend), + newTelemetry) consumer.RegisterHandlers(convoy.RetryEventProcessor, task.ProcessRetryEventDelivery( endpointRepo, @@ -388,8 +393,8 @@ func StartWorker(ctx context.Context, a *cli.App, cfg config.Configuration, inte attemptRepo, circuitBreakerManager, featureFlag, - a.TracerBackend, - ), newTelemetry) + a.TracerBackend), + newTelemetry) consumer.RegisterHandlers(convoy.CreateBroadcastEventProcessor, task.ProcessBroadcastEventCreation( broadcastCh, @@ -400,7 +405,9 @@ func StartWorker(ctx context.Context, a *cli.App, cfg config.Configuration, inte a.Queue, subRepo, deviceRepo, - a.Licenser), newTelemetry) + a.Licenser, + a.TracerBackend), + newTelemetry) consumer.RegisterHandlers(convoy.CreateDynamicEventProcessor, task.ProcessDynamicEventCreation( dynamicCh, @@ -410,7 +417,10 @@ func StartWorker(ctx context.Context, a *cli.App, cfg config.Configuration, inte eventDeliveryRepo, a.Queue, subRepo, - deviceRepo, a.Licenser), newTelemetry) + deviceRepo, + a.Licenser, + a.TracerBackend), + newTelemetry) if a.Licenser.RetentionPolicy() { consumer.RegisterHandlers(convoy.RetentionPolicies, task.RetentionPolicies(rd, ret), nil) @@ -425,7 +435,10 @@ func StartWorker(ctx context.Context, a *cli.App, cfg config.Configuration, inte eventDeliveryRepo, a.Queue, subRepo, - deviceRepo, a.Licenser), newTelemetry) + deviceRepo, + a.Licenser, + a.TracerBackend), + newTelemetry) consumer.RegisterHandlers(convoy.MonitorTwitterSources, task.MonitorTwitterSources(a.DB, a.Queue, rd), nil) diff --git a/database/hooks/hooks.go b/database/hooks/hooks.go index 30c5f0785c..4c12a82014 100644 --- a/database/hooks/hooks.go +++ b/database/hooks/hooks.go @@ -1,13 +1,14 @@ package hooks import ( + "context" "errors" "sync/atomic" "github.com/frain-dev/convoy/datastore" ) -type hookMap map[datastore.HookEventType]func(data interface{}, changelog interface{}) +type hookMap map[datastore.HookEventType]func(ctx context.Context, data interface{}, changelog interface{}) type Hook struct { fns hookMap @@ -30,13 +31,13 @@ func Init() *Hook { return &Hook{fns: hookMap{}} } -func (h *Hook) Fire(eventType datastore.HookEventType, data interface{}, changelog interface{}) { +func (h *Hook) Fire(ctx context.Context, eventType datastore.HookEventType, data interface{}, changelog interface{}) { if fn, ok := h.fns[eventType]; ok { - fn(data, changelog) + fn(ctx, data, changelog) } } -func (h *Hook) RegisterHook(eventType datastore.HookEventType, fn func(data interface{}, changelog interface{})) { +func (h *Hook) RegisterHook(eventType datastore.HookEventType, fn func(ctx context.Context, data interface{}, changelog interface{})) { h.fns[eventType] = fn hookSingleton.Store(h) } diff --git a/database/listener/endpoint_listener.go b/database/listener/endpoint_listener.go index fc8e922ff2..cc3de91000 100644 --- a/database/listener/endpoint_listener.go +++ b/database/listener/endpoint_listener.go @@ -1,6 +1,7 @@ package listener import ( + "context" "github.com/frain-dev/convoy/datastore" "github.com/frain-dev/convoy/pkg/log" "github.com/frain-dev/convoy/queue" @@ -16,26 +17,26 @@ func NewEndpointListener(queue queue.Queuer, projectRepo datastore.ProjectReposi return &EndpointListener{mEvent: mEvent} } -func (e *EndpointListener) AfterCreate(data interface{}, _ interface{}) { - e.metaEvent(string(datastore.EndpointCreated), data) +func (e *EndpointListener) AfterCreate(ctx context.Context, data interface{}, _ interface{}) { + e.metaEvent(ctx, datastore.EndpointCreated, data) } -func (e *EndpointListener) AfterUpdate(data interface{}, _ interface{}) { - e.metaEvent(string(datastore.EndpointUpdated), data) +func (e *EndpointListener) AfterUpdate(ctx context.Context, data interface{}, _ interface{}) { + e.metaEvent(ctx, datastore.EndpointUpdated, data) } -func (e *EndpointListener) AfterDelete(data interface{}, _ interface{}) { - e.metaEvent(string(datastore.EndpointDeleted), data) +func (e *EndpointListener) AfterDelete(ctx context.Context, data interface{}, _ interface{}) { + e.metaEvent(ctx, datastore.EndpointDeleted, data) } -func (e *EndpointListener) metaEvent(eventType string, data interface{}) { +func (e *EndpointListener) metaEvent(ctx context.Context, eventType datastore.HookEventType, data interface{}) { endpoint, ok := data.(*datastore.Endpoint) if !ok { log.Errorf("invalid type for event - %s", eventType) return } - if err := e.mEvent.Run(eventType, endpoint.ProjectID, endpoint); err != nil { + if err := e.mEvent.Run(ctx, string(eventType), endpoint.ProjectID, endpoint); err != nil { log.WithError(err).Error("endpoint meta event failed") } } diff --git a/database/listener/event_delivery_listener.go b/database/listener/event_delivery_listener.go index 9d8e06b7f8..eec0706b50 100644 --- a/database/listener/event_delivery_listener.go +++ b/database/listener/event_delivery_listener.go @@ -47,7 +47,7 @@ func NewEventDeliveryListener(queue queue.Queuer, projectRepo datastore.ProjectR return &EventDeliveryListener{mEvent: mEvent, attemptsRepo: attemptsRepo} } -func (e *EventDeliveryListener) AfterUpdate(data interface{}, _ interface{}) { +func (e *EventDeliveryListener) AfterUpdate(ctx context.Context, data interface{}, _ interface{}) { eventDelivery, ok := data.(*datastore.EventDelivery) if !ok { log.Error("invalid type for event - eventdelivery.updated") @@ -55,7 +55,7 @@ func (e *EventDeliveryListener) AfterUpdate(data interface{}, _ interface{}) { } mEventDelivery := getMetaEventDelivery(eventDelivery) - attempts, err := e.attemptsRepo.FindDeliveryAttempts(context.Background(), mEventDelivery.UID) + attempts, err := e.attemptsRepo.FindDeliveryAttempts(ctx, mEventDelivery.UID) if err != nil { log.WithError(err).Error("event delivery meta event failed") } @@ -65,14 +65,14 @@ func (e *EventDeliveryListener) AfterUpdate(data interface{}, _ interface{}) { } if eventDelivery.Status == datastore.SuccessEventStatus { - err := e.mEvent.Run(string(datastore.EventDeliverySuccess), eventDelivery.ProjectID, mEventDelivery) + err = e.mEvent.Run(ctx, string(datastore.EventDeliverySuccess), eventDelivery.ProjectID, mEventDelivery) if err != nil { log.WithError(err).Error("event delivery meta event failed") } } if eventDelivery.Status == datastore.FailureEventStatus { - err := e.mEvent.Run(string(datastore.EventDeliveryFailed), eventDelivery.ProjectID, mEventDelivery) + err = e.mEvent.Run(ctx, string(datastore.EventDeliveryFailed), eventDelivery.ProjectID, mEventDelivery) if err != nil { log.WithError(err).Error("event delivery meta event failed") } diff --git a/database/listener/project_listener.go b/database/listener/project_listener.go index 9c056cedf4..2e421a1ffa 100644 --- a/database/listener/project_listener.go +++ b/database/listener/project_listener.go @@ -1,6 +1,7 @@ package listener import ( + "context" "encoding/json" "time" @@ -19,11 +20,11 @@ func NewProjectListener(queue queue.Queuer) *ProjectListener { return &ProjectListener{queue: queue} } -func (e *ProjectListener) AfterUpdate(data interface{}, changelog interface{}) { - e.run(string(datastore.ProjectUpdated), data, changelog) +func (e *ProjectListener) AfterUpdate(ctx context.Context, data interface{}, changelog interface{}) { + e.run(ctx, datastore.ProjectUpdated, data, changelog) } -func (e *ProjectListener) run(eventType string, data interface{}, changelog interface{}) { +func (e *ProjectListener) run(_ context.Context, eventType datastore.HookEventType, data interface{}, changelog interface{}) { project, ok := data.(*datastore.Project) if !ok { log.Errorf("invalid type for project - %s", eventType) diff --git a/database/postgres/endpoint.go b/database/postgres/endpoint.go index 7ba8217fd8..33db453b55 100644 --- a/database/postgres/endpoint.go +++ b/database/postgres/endpoint.go @@ -305,7 +305,7 @@ func (e *endpointRepo) CreateEndpoint(ctx context.Context, endpoint *datastore.E return ErrEndpointNotCreated } - go e.hook.Fire(datastore.EndpointCreated, endpoint, nil) + go e.hook.Fire(context.Background(), datastore.EndpointCreated, endpoint, nil) return nil } @@ -418,7 +418,7 @@ func (e *endpointRepo) UpdateEndpoint(ctx context.Context, endpoint *datastore.E return ErrEndpointNotUpdated } - go e.hook.Fire(datastore.EndpointUpdated, endpoint, nil) + go e.hook.Fire(context.Background(), datastore.EndpointUpdated, endpoint, nil) return nil } @@ -467,7 +467,7 @@ func (e *endpointRepo) DeleteEndpoint(ctx context.Context, endpoint *datastore.E return err } - go e.hook.Fire(datastore.EndpointDeleted, endpoint, nil) + go e.hook.Fire(context.Background(), datastore.EndpointDeleted, endpoint, nil) return nil } diff --git a/database/postgres/event_delivery.go b/database/postgres/event_delivery.go index 97980bec29..a701a89d17 100644 --- a/database/postgres/event_delivery.go +++ b/database/postgres/event_delivery.go @@ -555,7 +555,8 @@ func (e *eventDeliveryRepo) UpdateEventDeliveryMetadata(ctx context.Context, pro return ErrEventDeliveryAttemptsNotUpdated } - go e.hook.Fire(datastore.EventDeliveryUpdated, delivery, nil) + e.hook.Fire(ctx, datastore.EventDeliveryUpdated, delivery, nil) + return nil } diff --git a/database/postgres/postgres_test.go b/database/postgres/postgres_test.go index 76e860b959..26841f7ba1 100644 --- a/database/postgres/postgres_test.go +++ b/database/postgres/postgres_test.go @@ -71,7 +71,7 @@ func getDB(t *testing.T) (database.Database, func()) { var err error dbHooks := hooks.Init() - dbHooks.RegisterHook(datastore.EndpointCreated, func(data interface{}, changelog interface{}) {}) + dbHooks.RegisterHook(datastore.EndpointCreated, func(ctx context.Context, data interface{}, changelog interface{}) {}) _db, err = NewDB(getConfig()) require.NoError(t, err) diff --git a/database/postgres/project.go b/database/postgres/project.go index 54a963aa04..8e0248cdb9 100644 --- a/database/postgres/project.go +++ b/database/postgres/project.go @@ -423,7 +423,8 @@ func (p *projectRepo) UpdateProject(ctx context.Context, project *datastore.Proj return err } - go p.hook.Fire(datastore.ProjectUpdated, project, changelog) + go p.hook.Fire(context.Background(), datastore.ProjectUpdated, project, changelog) + return nil } diff --git a/datastore/models.go b/datastore/models.go index 6aa7434385..4e9421fd3c 100644 --- a/datastore/models.go +++ b/datastore/models.go @@ -6,12 +6,13 @@ import ( "encoding/json" "errors" "fmt" - cb "github.com/frain-dev/convoy/pkg/circuit_breaker" "math" "net/http" "strings" "time" + cb "github.com/frain-dev/convoy/pkg/circuit_breaker" + "github.com/frain-dev/convoy/pkg/flatten" "github.com/oklog/ulid/v2" @@ -963,6 +964,7 @@ type EventDelivery struct { Headers httpheader.HTTPHeader `json:"headers" db:"headers"` URLQueryParams string `json:"url_query_params" db:"url_query_params"` IdempotencyKey string `json:"idempotency_key" db:"idempotency_key"` + // Deprecated: Latency is deprecated. Latency string `json:"latency" db:"latency"` LatencySeconds float64 `json:"latency_seconds" db:"latency_seconds"` diff --git a/ee/VERSION b/ee/VERSION index 8cb7a16c78..0c970dbbd3 100644 --- a/ee/VERSION +++ b/ee/VERSION @@ -1 +1 @@ -v25.1.1 Enterprise Edition +v25.2.1 Enterprise Edition diff --git a/generate.go b/generate.go index 191add2520..a90fe0a1f3 100644 --- a/generate.go +++ b/generate.go @@ -11,3 +11,4 @@ package convoy //go:generate mockgen --source internal/pkg/dedup/dedup.go --destination mocks/dedup.go -package mocks //go:generate mockgen --source internal/pkg/memorystore/table.go --destination mocks/table.go -package mocks //go:generate mockgen --source internal/pkg/license/license.go --destination mocks/license.go -package mocks +//go:generate mockgen --source internal/pkg/tracer/tracer.go --destination mocks/tracer.go -package mocks diff --git a/internal/pkg/keys/hcpvault_test.go b/internal/pkg/keys/hcpvault_test.go index 2829ebacef..fcfb903bf3 100644 --- a/internal/pkg/keys/hcpvault_test.go +++ b/internal/pkg/keys/hcpvault_test.go @@ -37,7 +37,6 @@ func TestNewHCPVaultKeyManagerEnv(t *testing.T) { key, err := h.GetCurrentKeyFromCache() assert.Nil(t, err) assert.NotEmpty(t, key) - t.Logf("Retrieved key: %s", key) // Happy path for setting a key newKey := "from-test-" + time.Now().String() diff --git a/internal/pkg/socket/client_test.go b/internal/pkg/socket/client_test.go index 1aec338ad5..c811888941 100644 --- a/internal/pkg/socket/client_test.go +++ b/internal/pkg/socket/client_test.go @@ -16,10 +16,9 @@ import ( func provideClient(r *Repo, c WebSocketConnection) *Client { return &Client{ - conn: c, - deviceID: "1234", - deviceRepo: r.DeviceRepo, - // EventTypes: []string{"*"}, + conn: c, + deviceID: "1234", + deviceRepo: r.DeviceRepo, eventDeliveryRepo: r.EventDeliveryRepo, Device: &datastore.Device{ UID: "1234", @@ -349,7 +348,6 @@ func TestParseTime(t *testing.T) { client := provideClient(r, c) since, err := client.parseTime(tt.args.message) - t.Log(since) if tt.args.wantErr { require.Error(t, tt.args.err, err) diff --git a/internal/pkg/tracer/datadog.go b/internal/pkg/tracer/datadog.go index a4b58c9798..25cde82e21 100644 --- a/internal/pkg/tracer/datadog.go +++ b/internal/pkg/tracer/datadog.go @@ -2,14 +2,18 @@ package tracer import ( "context" + "fmt" + "strconv" + "time" + "github.com/DataDog/datadog-go/v5/statsd" "github.com/frain-dev/convoy/config" "github.com/frain-dev/convoy/datastore" "github.com/frain-dev/convoy/internal/pkg/license" - "github.com/frain-dev/convoy/net" "github.com/frain-dev/convoy/pkg/log" "go.opentelemetry.io/otel" - "time" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" ddotel "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/opentelemetry" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" @@ -59,20 +63,16 @@ func (dt *DatadogTracer) Type() config.TracerProvider { return config.DatadogTracerProvider } -func (dt *DatadogTracer) Capture(project *datastore.Project, targetURL string, resp *net.Response, duration time.Duration) { +func (dt *DatadogTracer) CaptureDelivery(ctx context.Context, project *datastore.Project, targetURL string, status string, statusCode int, bodyLength int, duration time.Duration) { if !dt.Licenser.DatadogTracing() { return } - var status string - var statusCode int - if resp != nil { - status = resp.Status - statusCode = resp.StatusCode - } + + traceId := getDatadogTraceID(ctx) + fmt.Printf("%s\n", traceId) + dt.RecordLatency(project.UID, targetURL, status, duration) - if resp != nil { - dt.RecordThroughput(project.UID, targetURL, len(resp.Body)) - } + dt.RecordThroughput(project.UID, targetURL, bodyLength) dt.RecordRequestTotal(project.UID, targetURL) if statusCode > 299 { dt.RecordErrorRate(project.UID, targetURL, statusCode) @@ -126,3 +126,55 @@ func (dt *DatadogTracer) RecordThroughput(projectID string, url string, dataSize log.Errorf("Error recording throughput: %s", err) } } + +func getDatadogTraceID(ctx context.Context) string { + spanCtx := trace.SpanContextFromContext(ctx) + if spanCtx.HasTraceID() { + // Since datadog trace provider (ddtrace) uses big endian uint64 for the trace ID, we must first to first convert it back to uint64. + traceID := spanCtx.TraceID() + traceIDRaw := [16]byte(traceID) + traceIDUint64 := byteArrToUint64(traceIDRaw[8:]) + traceIDStr := strconv.FormatUint(traceIDUint64, 10) + return traceIDStr + } + return "" +} + +func byteArrToUint64(buf []byte) uint64 { + var x uint64 + for i, b := range buf { + x = x<<8 + uint64(b) + if i == 7 { + return x + } + } + return x +} + +func (dt *DatadogTracer) Capture(ctx context.Context, name string, attributes map[string]interface{}, startTime time.Time, endTime time.Time) { + if !dt.Licenser.DatadogTracing() { + return + } + + _, span := otel.Tracer("").Start(ctx, name, trace.WithTimestamp(startTime)) + // End span with provided end time + defer span.End(trace.WithTimestamp(endTime)) + + // Convert and set attributes + attrs := make([]attribute.KeyValue, 0, len(attributes)) + for k, v := range attributes { + switch val := v.(type) { + case string: + attrs = append(attrs, attribute.String(k, val)) + case int: + attrs = append(attrs, attribute.Int(k, val)) + case int64: + attrs = append(attrs, attribute.Int64(k, val)) + case float64: + attrs = append(attrs, attribute.Float64(k, val)) + case bool: + attrs = append(attrs, attribute.Bool(k, val)) + } + } + span.SetAttributes(attrs...) +} diff --git a/internal/pkg/tracer/noop.go b/internal/pkg/tracer/noop.go new file mode 100644 index 0000000000..a0e7763200 --- /dev/null +++ b/internal/pkg/tracer/noop.go @@ -0,0 +1,20 @@ +package tracer + +import ( + "context" + "time" + + "github.com/frain-dev/convoy/config" +) + +// NoOpBackend is a no-operation tracer backend implementation. +type NoOpBackend struct{} + +func (NoOpBackend) Init(string) error { return nil } +func (NoOpBackend) Type() config.TracerProvider { + return "" +} +func (NoOpBackend) Capture(context.Context, string, map[string]interface{}, time.Time, time.Time) {} +func (NoOpBackend) Shutdown(context.Context) error { + return nil +} diff --git a/internal/pkg/tracer/otel.go b/internal/pkg/tracer/otel.go index 133aafcb25..d5773ae941 100644 --- a/internal/pkg/tracer/otel.go +++ b/internal/pkg/tracer/otel.go @@ -3,14 +3,15 @@ package tracer import ( "context" "errors" + "time" + + "go.opentelemetry.io/otel/trace" + "github.com/frain-dev/convoy" "github.com/frain-dev/convoy/config" - "github.com/frain-dev/convoy/datastore" - "github.com/frain-dev/convoy/net" "github.com/frain-dev/convoy/util" "go.opentelemetry.io/otel" "google.golang.org/grpc/credentials" - "time" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/exporters/otlp/otlptrace" @@ -105,9 +106,33 @@ func (ot *OTelTracer) Init(componentName string) error { func (ot *OTelTracer) Type() config.TracerProvider { return config.OTelTracerProvider } -func (ot *OTelTracer) Capture(*datastore.Project, string, *net.Response, time.Duration) { +func (ot *OTelTracer) Capture(ctx context.Context, name string, attributes map[string]interface{}, startTime time.Time, endTime time.Time) { + _, span := otel.Tracer("").Start(ctx, name, + trace.WithTimestamp(startTime)) + + // End span with provided end time + defer span.End(trace.WithTimestamp(endTime)) + + // Convert and set attributes + attrs := make([]attribute.KeyValue, 0, len(attributes)) + for k, v := range attributes { + switch val := v.(type) { + case string: + attrs = append(attrs, attribute.String(k, val)) + case int: + attrs = append(attrs, attribute.Int(k, val)) + case int64: + attrs = append(attrs, attribute.Int64(k, val)) + case float64: + attrs = append(attrs, attribute.Float64(k, val)) + case bool: + attrs = append(attrs, attribute.Bool(k, val)) + } + } + span.SetAttributes(attrs...) } + func (ot *OTelTracer) Shutdown(ctx context.Context) error { return ot.ShutdownFn(ctx) } diff --git a/internal/pkg/tracer/sentry.go b/internal/pkg/tracer/sentry.go index f11f20b32f..b0ab9e2bc8 100644 --- a/internal/pkg/tracer/sentry.go +++ b/internal/pkg/tracer/sentry.go @@ -2,15 +2,16 @@ package tracer import ( "context" + "time" + "github.com/frain-dev/convoy/config" - "github.com/frain-dev/convoy/datastore" - "github.com/frain-dev/convoy/net" "github.com/getsentry/sentry-go" sentryotel "github.com/getsentry/sentry-go/otel" "go.opentelemetry.io/otel" - "time" + "go.opentelemetry.io/otel/attribute" sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/trace" ) type SentryTracer struct { @@ -58,9 +59,33 @@ func (st *SentryTracer) Init(componentName string) error { func (st *SentryTracer) Type() config.TracerProvider { return config.SentryTracerProvider } -func (st *SentryTracer) Capture(*datastore.Project, string, *net.Response, time.Duration) { +func (st *SentryTracer) Capture(ctx context.Context, name string, attributes map[string]interface{}, startTime time.Time, endTime time.Time) { + _, span := otel.Tracer("").Start(ctx, name, + trace.WithTimestamp(startTime)) + + // End span with provided end time + defer span.End(trace.WithTimestamp(endTime)) + + // Convert and set attributes + attrs := make([]attribute.KeyValue, 0, len(attributes)) + for k, v := range attributes { + switch val := v.(type) { + case string: + attrs = append(attrs, attribute.String(k, val)) + case int: + attrs = append(attrs, attribute.Int(k, val)) + case int64: + attrs = append(attrs, attribute.Int64(k, val)) + case float64: + attrs = append(attrs, attribute.Float64(k, val)) + case bool: + attrs = append(attrs, attribute.Bool(k, val)) + } + } + span.SetAttributes(attrs...) } + func (st *SentryTracer) Shutdown(ctx context.Context) error { return st.ShutdownFn(ctx) } diff --git a/internal/pkg/tracer/tracer.go b/internal/pkg/tracer/tracer.go index c54b2d08c3..200e1bc445 100644 --- a/internal/pkg/tracer/tracer.go +++ b/internal/pkg/tracer/tracer.go @@ -3,11 +3,10 @@ package tracer import ( "context" "errors" - "github.com/frain-dev/convoy/datastore" + "time" + "github.com/frain-dev/convoy/internal/pkg/license" - "github.com/frain-dev/convoy/net" "github.com/frain-dev/convoy/pkg/log" - "time" "github.com/frain-dev/convoy/config" "go.opentelemetry.io/otel/trace" @@ -38,28 +37,15 @@ func FromContext(ctx context.Context) trace.Tracer { return nil } -// Backend is an abstraction for tracng backend (Datadog, Sentry, ...) +// Backend is an abstraction for tracing backend (Datadog, Sentry, ...) type Backend interface { Init(componentName string) error Type() config.TracerProvider - Capture(*datastore.Project, string, *net.Response, time.Duration) + Capture(ctx context.Context, name string, attributes map[string]interface{}, startTime time.Time, endTime time.Time) Shutdown(ctx context.Context) error } -type NoOpBackend struct{} - -func (NoOpBackend) Init(componentName string) error { return nil } -func (NoOpBackend) Type() config.TracerProvider { - return "" -} -func (NoOpBackend) Capture(*datastore.Project, string, *net.Response, time.Duration) { - -} -func (NoOpBackend) Shutdown(context.Context) error { - return nil -} - -// Global tracer Init function +// Init is a global tracer initialization function func Init(tCfg config.TracerConfiguration, componentName string, licenser license.Licenser) (Backend, error) { switch tCfg.Type { case config.SentryTracerProvider: diff --git a/mocks/tracer.go b/mocks/tracer.go index 9233144eab..8fcceba915 100644 --- a/mocks/tracer.go +++ b/mocks/tracer.go @@ -1,105 +1,96 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: tracer/tracer.go +// Source: internal/pkg/tracer/tracer.go +// +// Generated by this command: +// +// mockgen --source internal/pkg/tracer/tracer.go --destination mocks/tracer.go -package mocks +// // Package mocks is a generated GoMock package. package mocks import ( context "context" - http "net/http" reflect "reflect" + time "time" - newrelic "github.com/newrelic/go-agent/v3/newrelic" + config "github.com/frain-dev/convoy/config" gomock "go.uber.org/mock/gomock" ) -// MockTracer is a mock of Tracer interface. -type MockTracer struct { +// MockBackend is a mock of Backend interface. +type MockBackend struct { ctrl *gomock.Controller - recorder *MockTracerMockRecorder + recorder *MockBackendMockRecorder } -// MockTracerMockRecorder is the mock recorder for MockTracer. -type MockTracerMockRecorder struct { - mock *MockTracer +// MockBackendMockRecorder is the mock recorder for MockBackend. +type MockBackendMockRecorder struct { + mock *MockBackend } -// NewMockTracer creates a new mock instance. -func NewMockTracer(ctrl *gomock.Controller) *MockTracer { - mock := &MockTracer{ctrl: ctrl} - mock.recorder = &MockTracerMockRecorder{mock} +// NewMockBackend creates a new mock instance. +func NewMockBackend(ctrl *gomock.Controller) *MockBackend { + mock := &MockBackend{ctrl: ctrl} + mock.recorder = &MockBackendMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockTracer) EXPECT() *MockTracerMockRecorder { +func (m *MockBackend) EXPECT() *MockBackendMockRecorder { return m.recorder } -// NewContext mocks base method. -func (m *MockTracer) NewContext(ctx context.Context, txn *newrelic.Transaction) context.Context { +// Capture mocks base method. +func (m *MockBackend) Capture(ctx context.Context, name string, attributes map[string]any, startTime, endTime time.Time) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "NewContext", ctx, txn) - ret0, _ := ret[0].(context.Context) - return ret0 + m.ctrl.Call(m, "Capture", ctx, name, attributes, startTime, endTime) } -// NewContext indicates an expected call of NewContext. -func (mr *MockTracerMockRecorder) NewContext(ctx, txn interface{}) *gomock.Call { +// Capture indicates an expected call of Capture. +func (mr *MockBackendMockRecorder) Capture(ctx, name, attributes, startTime, endTime any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewContext", reflect.TypeOf((*MockTracer)(nil).NewContext), ctx, txn) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Capture", reflect.TypeOf((*MockBackend)(nil).Capture), ctx, name, attributes, startTime, endTime) } -// RequestWithTransactionContext mocks base method. -func (m *MockTracer) RequestWithTransactionContext(r *http.Request, txn *newrelic.Transaction) *http.Request { +// Init mocks base method. +func (m *MockBackend) Init(componentName string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RequestWithTransactionContext", r, txn) - ret0, _ := ret[0].(*http.Request) + ret := m.ctrl.Call(m, "Init", componentName) + ret0, _ := ret[0].(error) return ret0 } -// RequestWithTransactionContext indicates an expected call of RequestWithTransactionContext. -func (mr *MockTracerMockRecorder) RequestWithTransactionContext(r, txn interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RequestWithTransactionContext", reflect.TypeOf((*MockTracer)(nil).RequestWithTransactionContext), r, txn) -} - -// SetWebRequestHTTP mocks base method. -func (m *MockTracer) SetWebRequestHTTP(r *http.Request, txn *newrelic.Transaction) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "SetWebRequestHTTP", r, txn) -} - -// SetWebRequestHTTP indicates an expected call of SetWebRequestHTTP. -func (mr *MockTracerMockRecorder) SetWebRequestHTTP(r, txn interface{}) *gomock.Call { +// Init indicates an expected call of Init. +func (mr *MockBackendMockRecorder) Init(componentName any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetWebRequestHTTP", reflect.TypeOf((*MockTracer)(nil).SetWebRequestHTTP), r, txn) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockBackend)(nil).Init), componentName) } -// SetWebResponse mocks base method. -func (m *MockTracer) SetWebResponse(w http.ResponseWriter, txn *newrelic.Transaction) http.ResponseWriter { +// Shutdown mocks base method. +func (m *MockBackend) Shutdown(ctx context.Context) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SetWebResponse", w, txn) - ret0, _ := ret[0].(http.ResponseWriter) + ret := m.ctrl.Call(m, "Shutdown", ctx) + ret0, _ := ret[0].(error) return ret0 } -// SetWebResponse indicates an expected call of SetWebResponse. -func (mr *MockTracerMockRecorder) SetWebResponse(w, txn interface{}) *gomock.Call { +// Shutdown indicates an expected call of Shutdown. +func (mr *MockBackendMockRecorder) Shutdown(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetWebResponse", reflect.TypeOf((*MockTracer)(nil).SetWebResponse), w, txn) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Shutdown", reflect.TypeOf((*MockBackend)(nil).Shutdown), ctx) } -// StartTransaction mocks base method. -func (m *MockTracer) StartTransaction(name string) *newrelic.Transaction { +// Type mocks base method. +func (m *MockBackend) Type() config.TracerProvider { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "StartTransaction", name) - ret0, _ := ret[0].(*newrelic.Transaction) + ret := m.ctrl.Call(m, "Type") + ret0, _ := ret[0].(config.TracerProvider) return ret0 } -// StartTransaction indicates an expected call of StartTransaction. -func (mr *MockTracerMockRecorder) StartTransaction(name interface{}) *gomock.Call { +// Type indicates an expected call of Type. +func (mr *MockBackendMockRecorder) Type() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartTransaction", reflect.TypeOf((*MockTracer)(nil).StartTransaction), name) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Type", reflect.TypeOf((*MockBackend)(nil).Type)) } diff --git a/net/dispatcher.go b/net/dispatcher.go index b6e9a5e4b8..238d4a5b81 100644 --- a/net/dispatcher.go +++ b/net/dispatcher.go @@ -8,8 +8,10 @@ import ( "encoding/json" "errors" "fmt" + "os" + "github.com/frain-dev/convoy/internal/pkg/fflag" - "github.com/stealthrocket/netjail" + "io" "net/http" "net/http/httptrace" @@ -17,9 +19,13 @@ import ( "net/url" "time" + "github.com/stealthrocket/netjail" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "github.com/frain-dev/convoy/internal/pkg/license" "github.com/frain-dev/convoy" + "github.com/frain-dev/convoy/internal/pkg/tracer" "github.com/frain-dev/convoy/pkg/httpheader" "github.com/frain-dev/convoy/pkg/log" "github.com/frain-dev/convoy/util" @@ -30,34 +36,89 @@ var ( ErrBlockListIsRequired = errors.New("blocklist is required") ErrLoggerIsRequired = errors.New("logger is required") ErrInvalidIPPrefix = errors.New("invalid IP prefix") + ErrTracerIsRequired = errors.New("tracer cannot be nil") ) type DispatcherOption func(d *Dispatcher) error +// CustomTransport wraps both netjail.Transport and otelhttp.Transport +type CustomTransport struct { + otelTransport *otelhttp.Transport + netJailTransport *netjail.Transport + vanillaTransport *http.Transport +} + +// RoundTrip executes a single HTTP transaction +func (c *CustomTransport) RoundTrip(req *http.Request) (*http.Response, error) { + return c.otelTransport.RoundTrip(req) +} + +// NewNetJailTransport creates a new CustomTransport with a netJailTransport +func NewNetJailTransport(netJailTransport *netjail.Transport) *CustomTransport { + otelTransport := otelhttp.NewTransport( + netJailTransport, + otelhttp.WithSpanNameFormatter(func(operation string, r *http.Request) string { + return fmt.Sprintf("%s webhook.dispatch", r.Method) + }), + otelhttp.WithFilter(func(r *http.Request) bool { + return true + }), + ) + return &CustomTransport{ + otelTransport: otelTransport, + netJailTransport: netJailTransport, + } +} + +// NewVanillaTransport creates a new CustomTransport with a default transport +func NewVanillaTransport(transport *http.Transport) *CustomTransport { + otelTransport := otelhttp.NewTransport( + transport, + otelhttp.WithSpanNameFormatter(func(operation string, r *http.Request) string { + return fmt.Sprintf("%s webhook.dispatch", r.Method) + }), + otelhttp.WithFilter(func(r *http.Request) bool { + return true + }), + ) + return &CustomTransport{ + otelTransport: otelTransport, + vanillaTransport: transport, + } +} + +type DetailedTraceConfig struct { + Enabled bool +} + type Dispatcher struct { // gating mechanisms ff *fflag.FFlag l license.Licenser - logger *log.Logger - transport *http.Transport - client *http.Client - rules *netjail.Rules + logger *log.Logger + transport *http.Transport + client *http.Client + rules *netjail.Rules + tracer tracer.Backend + detailedTrace DetailedTraceConfig } func NewDispatcher(l license.Licenser, ff *fflag.FFlag, options ...DispatcherOption) (*Dispatcher, error) { d := &Dispatcher{ - l: l, ff: ff, + l: l, + logger: log.NewLogger(os.Stdout), + tracer: tracer.NoOpBackend{}, client: &http.Client{}, rules: &netjail.Rules{}, transport: &http.Transport{ - MaxIdleConns: 100, - IdleConnTimeout: 10 * time.Second, - MaxIdleConnsPerHost: 10, - TLSHandshakeTimeout: 3 * time.Second, + MaxIdleConns: 1000, + IdleConnTimeout: 30 * time.Second, + MaxIdleConnsPerHost: 100, + TLSHandshakeTimeout: 5 * time.Second, ExpectContinueTimeout: 1 * time.Second, - DisableCompression: true, + DisableCompression: false, }, } @@ -78,9 +139,9 @@ func NewDispatcher(l license.Licenser, ff *fflag.FFlag, options ...DispatcherOpt } if ff.CanAccessFeature(fflag.IpRules) && l.IpRules() { - d.client.Transport = netJailTransport + d.client.Transport = NewNetJailTransport(netJailTransport) } else { - d.client.Transport = d.transport + d.client.Transport = NewVanillaTransport(d.transport) } return d, nil @@ -187,6 +248,25 @@ func LoggerOption(logger *log.Logger) DispatcherOption { } } +// TracerOption sets a custom tracer backend for the Dispatcher +func TracerOption(tracer tracer.Backend) DispatcherOption { + return func(d *Dispatcher) error { + if tracer == nil { + return ErrTracerIsRequired + } + + d.tracer = tracer + return nil + } +} + +func DetailedTraceOption(enabled bool) DispatcherOption { + return func(d *Dispatcher) error { + d.detailedTrace.Enabled = enabled + return nil + } +} + func (d *Dispatcher) validateProxy(proxyURL string) (*url.URL, bool, error) { if !util.IsStringEmpty(proxyURL) { pUrl, err := url.Parse(proxyURL) @@ -245,7 +325,10 @@ func (d *Dispatcher) SendRequest(ctx context.Context, endpoint, method string, j r.URL = req.URL r.Method = req.Method - err = d.do(req, r, maxResponseSize) + err = d.do(ctx, req, r, maxResponseSize) + if err != nil { + return r, err + } return r, err } @@ -272,15 +355,67 @@ func defaultUserAgent() string { return "Convoy/" + convoy.GetVersion() } -func (d *Dispatcher) do(req *http.Request, res *Response, maxResponseSize int64) error { - trace := &httptrace.ClientTrace{ - GotConn: func(connInfo httptrace.GotConnInfo) { - res.IP = connInfo.Conn.RemoteAddr().String() - d.logger.Debugf("IP address resolved to: %s", connInfo.Conn.RemoteAddr()) - }, - } +func (d *Dispatcher) do(ctx context.Context, req *http.Request, res *Response, maxResponseSize int64) error { + if d.detailedTrace.Enabled { + trace := &httptrace.ClientTrace{ + DNSStart: func(info httptrace.DNSStartInfo) { + attrs := map[string]interface{}{ + "dns.host": info.Host, + "event": "dns_start", + } + d.tracer.Capture(ctx, "dns_lookup_start", attrs, time.Now(), time.Now()) + }, + DNSDone: func(info httptrace.DNSDoneInfo) { + attrs := map[string]interface{}{ + "dns.addresses": fmt.Sprintf("%v", info.Addrs), + "dns.error": fmt.Sprintf("%v", info.Err), + "event": "dns_done", + } + d.tracer.Capture(ctx, "dns_lookup_done", attrs, time.Now(), time.Now()) + }, + ConnectStart: func(network, addr string) { + attrs := map[string]interface{}{ + "net.network": network, + "net.addr": addr, + "event": "connect_start", + } + d.tracer.Capture(ctx, "connect_start", attrs, time.Now(), time.Now()) + }, + ConnectDone: func(network, addr string, err error) { + attrs := map[string]interface{}{ + "net.network": network, + "net.addr": addr, + "error": fmt.Sprintf("%v", err), + "event": "connect_done", + } + d.tracer.Capture(ctx, "connect_done", attrs, time.Now(), time.Now()) + }, + TLSHandshakeStart: func() { + attrs := map[string]interface{}{ + "event": "tls_handshake_start", + } + d.tracer.Capture(ctx, "tls_handshake_start", attrs, time.Now(), time.Now()) + }, + TLSHandshakeDone: func(state tls.ConnectionState, err error) { + attrs := map[string]interface{}{ + "tls.version": state.Version, + "tls.cipher_suite": state.CipherSuite, + "error": fmt.Sprintf("%v", err), + "event": "tls_handshake_done", + } + d.tracer.Capture(ctx, "tls_handshake_done", attrs, time.Now(), time.Now()) + }, + GotFirstResponseByte: func() { + attrs := map[string]interface{}{ + "event": "first_byte_received", + } + d.tracer.Capture(ctx, "first_byte", attrs, time.Now(), time.Now()) + }, + } - req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace)) + ctx = httptrace.WithClientTrace(ctx, trace) + req = req.WithContext(ctx) + } response, err := d.client.Do(req) if err != nil { diff --git a/net/dispatcher_test.go b/net/dispatcher_test.go index 4f533c6026..377726d418 100644 --- a/net/dispatcher_test.go +++ b/net/dispatcher_test.go @@ -5,15 +5,17 @@ import ( "context" "crypto/rand" "encoding/json" - "github.com/frain-dev/convoy/internal/pkg/fflag" - "github.com/frain-dev/convoy/pkg/log" - "github.com/stealthrocket/netjail" "net/http" "net/http/httptest" "os" "testing" "time" + "github.com/frain-dev/convoy/internal/pkg/fflag" + "github.com/frain-dev/convoy/internal/pkg/tracer" + "github.com/frain-dev/convoy/pkg/log" + "github.com/stealthrocket/netjail" + "github.com/frain-dev/convoy/internal/pkg/license" "github.com/frain-dev/convoy/mocks" @@ -24,6 +26,7 @@ import ( "github.com/jarcoal/httpmock" "github.com/frain-dev/convoy/config" + "github.com/stretchr/testify/require" ) @@ -285,7 +288,7 @@ func TestDispatcher_SendRequest(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - d := &Dispatcher{client: client, logger: log.NewLogger(os.Stdout), ff: fflag.NewFFlag([]string{})} + d := &Dispatcher{client: client, logger: log.NewLogger(os.Stdout), ff: fflag.NewFFlag([]string{}), tracer: tracer.NoOpBackend{}} if tt.nFn != nil { deferFn := tt.nFn() @@ -378,8 +381,16 @@ func TestNewDispatcher(t *testing.T) { require.NoError(t, err) + // Access the custom transport + customTransport, ok := d.client.Transport.(*CustomTransport) + require.True(t, ok, "Transport should be of type *CustomTransport") + + // Access the netjail.Transport + netJailTransport := customTransport.netJailTransport + require.NotNil(t, netJailTransport, "Underlying transport should be of type *netjail.Transport") + if tt.wantProxy { - require.NotNil(t, d.client.Transport.(*netjail.Transport).New().Proxy) + require.NotNil(t, netJailTransport.New().Proxy) } }) } diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 48e341a095..0000000000 --- a/package-lock.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "lockfileVersion": 1 -} diff --git a/release.Dockerfile b/release.Dockerfile index 7eac60c62f..4c5e7bb2f9 100644 --- a/release.Dockerfile +++ b/release.Dockerfile @@ -6,8 +6,21 @@ ARG IMAGE_SHA # Set an environment variable using the ARG ENV CORE_GATEWAY_IMAGE_SHA=${IMAGE_SHA} +# Copy the Convoy binary COPY convoy /cmd + +# Copy the migrations directory +COPY sql/ /sql/ + +# Copy the startup script COPY configs/local/start.sh /start.sh -RUN chmod +x /cmd + +# Set permissions +RUN chmod +x /cmd /start.sh + +# Install necessary dependencies RUN apk add --no-cache gcompat + +# Set the startup command CMD ["/start.sh"] + diff --git a/services/create_meta_event.go b/services/create_meta_event.go index 75e0a08f5f..64d1dd95d8 100644 --- a/services/create_meta_event.go +++ b/services/create_meta_event.go @@ -25,8 +25,8 @@ func NewMetaEvent(queue queue.Queuer, projectRepo datastore.ProjectRepository, m return &MetaEvent{queue: queue, projectRepo: projectRepo, metaEventRepo: metaEventRepo} } -func (m *MetaEvent) Run(eventType string, projectID string, data interface{}) error { - project, err := m.projectRepo.FetchProjectByID(context.Background(), projectID) +func (m *MetaEvent) Run(ctx context.Context, eventType string, projectID string, data interface{}) error { + project, err := m.projectRepo.FetchProjectByID(ctx, projectID) if err != nil { return err } @@ -79,7 +79,7 @@ func (m *MetaEvent) Run(eventType string, projectID string, data interface{}) er UpdatedAt: time.Now(), } - err = m.metaEventRepo.CreateMetaEvent(context.Background(), metaEvent) + err = m.metaEventRepo.CreateMetaEvent(ctx, metaEvent) if err != nil { log.WithError(err).Error("failed to create meta event") return err diff --git a/services/create_meta_event_test.go b/services/create_meta_event_test.go index 5f76b7ee00..ff743b5a0b 100644 --- a/services/create_meta_event_test.go +++ b/services/create_meta_event_test.go @@ -1,6 +1,7 @@ package services import ( + "context" "testing" "github.com/frain-dev/convoy/datastore" @@ -122,7 +123,7 @@ func Test_MetaEvent_Run(t *testing.T) { tt.qFn(mE) } - err := mE.Run(tt.args.eventType, tt.args.projectID, tt.args.data) + err := mE.Run(context.Background(), tt.args.eventType, tt.args.projectID, tt.args.data) require.Nil(t, err) }) } diff --git a/services/update_endpoint.go b/services/update_endpoint.go index 431b6dbafa..08e399ca60 100644 --- a/services/update_endpoint.go +++ b/services/update_endpoint.go @@ -1,109 +1,107 @@ package services import ( - "context" - "time" + "context" + "time" - "github.com/frain-dev/convoy" + "github.com/frain-dev/convoy" - "github.com/frain-dev/convoy/internal/pkg/license" + "github.com/frain-dev/convoy/internal/pkg/license" - "github.com/frain-dev/convoy/pkg/log" + "github.com/frain-dev/convoy/pkg/log" - "github.com/frain-dev/convoy/api/models" - "github.com/frain-dev/convoy/cache" - "github.com/frain-dev/convoy/datastore" - "github.com/frain-dev/convoy/util" + "github.com/frain-dev/convoy/api/models" + "github.com/frain-dev/convoy/cache" + "github.com/frain-dev/convoy/datastore" + "github.com/frain-dev/convoy/util" ) type UpdateEndpointService struct { - Cache cache.Cache - EndpointRepo datastore.EndpointRepository - ProjectRepo datastore.ProjectRepository - Licenser license.Licenser - - E models.UpdateEndpoint - Endpoint *datastore.Endpoint - Project *datastore.Project + Cache cache.Cache + EndpointRepo datastore.EndpointRepository + ProjectRepo datastore.ProjectRepository + Licenser license.Licenser + + E models.UpdateEndpoint + Endpoint *datastore.Endpoint + Project *datastore.Project } func (a *UpdateEndpointService) Run(ctx context.Context) (*datastore.Endpoint, error) { - url, err := util.ValidateEndpoint(a.E.URL, a.Project.Config.SSL.EnforceSecureEndpoints) - if err != nil { - return nil, &ServiceError{ErrMsg: err.Error()} - } + url, err := util.ValidateEndpoint(a.E.URL, a.Project.Config.SSL.EnforceSecureEndpoints) + if err != nil { + return nil, &ServiceError{ErrMsg: err.Error()} + } - a.E.URL = url + a.E.URL = url - endpoint := a.Endpoint + endpoint := a.Endpoint - endpoint, err = a.EndpointRepo.FindEndpointByID(ctx, endpoint.UID, a.Project.UID) - if err != nil { - return nil, &ServiceError{ErrMsg: err.Error()} - } + endpoint, err = a.EndpointRepo.FindEndpointByID(ctx, endpoint.UID, a.Project.UID) + if err != nil { + return nil, &ServiceError{ErrMsg: err.Error()} + } - endpoint, err = a.updateEndpoint(endpoint, a.E, a.Project) - if err != nil { - return nil, &ServiceError{ErrMsg: err.Error()} - } + endpoint, err = a.updateEndpoint(endpoint, a.E, a.Project) + if err != nil { + return nil, &ServiceError{ErrMsg: err.Error()} + } - err = a.EndpointRepo.UpdateEndpoint(ctx, endpoint, endpoint.ProjectID) - if err != nil { - log.FromContext(ctx).WithError(err).Error("failed to update endpoint") - return endpoint, &ServiceError{ErrMsg: "an error occurred while updating endpoints", Err: err} + err = a.EndpointRepo.UpdateEndpoint(ctx, endpoint, endpoint.ProjectID) + if err != nil { + log.FromContext(ctx).WithError(err).Error("failed to update endpoint") + return endpoint, &ServiceError{ErrMsg: "an error occurred while updating endpoints", Err: err} - } + } - return endpoint, nil + return endpoint, nil } func (a *UpdateEndpointService) updateEndpoint(endpoint *datastore.Endpoint, e models.UpdateEndpoint, project *datastore.Project) (*datastore.Endpoint, error) { - endpoint.Url = e.URL - endpoint.Description = e.Description + endpoint.Url = e.URL + endpoint.Description = e.Description - endpoint.Name = *e.Name + endpoint.Name = *e.Name - if e.SupportEmail != nil && a.Licenser.AdvancedEndpointMgmt() { - endpoint.SupportEmail = *e.SupportEmail - } + if e.SupportEmail != nil && a.Licenser.AdvancedEndpointMgmt() { + endpoint.SupportEmail = *e.SupportEmail + } - if e.SlackWebhookURL != nil && a.Licenser.AdvancedEndpointMgmt() { - endpoint.SlackWebhookURL = *e.SlackWebhookURL - } + if e.SlackWebhookURL != nil && a.Licenser.AdvancedEndpointMgmt() { + endpoint.SlackWebhookURL = *e.SlackWebhookURL + } - if e.RateLimit != 0 { - endpoint.RateLimit = e.RateLimit - } + if e.RateLimit >= 0 { + endpoint.RateLimit = e.RateLimit + } - if e.RateLimitDuration != 0 { - endpoint.RateLimitDuration = e.RateLimitDuration - } + endpoint.RateLimitDuration = e.RateLimitDuration - if e.AdvancedSignatures != nil && project.Type == datastore.OutgoingProject { - endpoint.AdvancedSignatures = *e.AdvancedSignatures - } + if e.AdvancedSignatures != nil && project.Type == datastore.OutgoingProject { + endpoint.AdvancedSignatures = *e.AdvancedSignatures + } - if e.HttpTimeout != 0 { - endpoint.HttpTimeout = e.HttpTimeout + if e.HttpTimeout != 0 { + endpoint.HttpTimeout = e.HttpTimeout - if !a.Licenser.AdvancedEndpointMgmt() { - // switch to default timeout - endpoint.HttpTimeout = convoy.HTTP_TIMEOUT - } - } + if !a.Licenser.AdvancedEndpointMgmt() { + // switch to default timeout + endpoint.HttpTimeout = convoy.HTTP_TIMEOUT + } + } - if !util.IsStringEmpty(e.OwnerID) { - endpoint.OwnerID = e.OwnerID - } + if !util.IsStringEmpty(e.OwnerID) { + endpoint.OwnerID = e.OwnerID + } - auth, err := ValidateEndpointAuthentication(e.Authentication.Transform()) - if err != nil { - return nil, err - } + auth, err := ValidateEndpointAuthentication(e.Authentication.Transform()) + if err != nil { + return nil, err + } - endpoint.Authentication = auth + endpoint.Authentication = auth - endpoint.UpdatedAt = time.Now() + endpoint.UpdatedAt = time.Now() - return endpoint, nil + return endpoint, nil } diff --git a/sql/1739961493.sql b/sql/1739961493.sql new file mode 100644 index 0000000000..56d77317b5 --- /dev/null +++ b/sql/1739961493.sql @@ -0,0 +1,81 @@ +-- +migrate Up +ALTER TABLE convoy.projects DROP CONSTRAINT IF EXISTS projects_project_configuration_id_fkey; +ALTER TABLE convoy.meta_events DROP CONSTRAINT IF EXISTS meta_events_project_id_fkey; +ALTER TABLE convoy.sources DROP CONSTRAINT IF EXISTS sources_source_verifier_id_fkey; + +-- +migrate Up +ALTER TABLE convoy.projects + ALTER COLUMN project_configuration_id TYPE VARCHAR USING project_configuration_id::VARCHAR; + +ALTER TABLE convoy.meta_events + ALTER COLUMN project_id TYPE VARCHAR USING project_id::VARCHAR; + +ALTER TABLE convoy.sources + ALTER COLUMN source_verifier_id TYPE VARCHAR USING source_verifier_id::VARCHAR; + +-- +migrate Up +ALTER TABLE convoy.projects + ADD CONSTRAINT projects_project_configuration_id_fkey + FOREIGN KEY (project_configuration_id) + REFERENCES convoy.project_configurations(id) + ON DELETE CASCADE; + +ALTER TABLE convoy.meta_events + ADD CONSTRAINT meta_events_project_id_fkey + FOREIGN KEY (project_id) + REFERENCES convoy.projects(id) + ON DELETE CASCADE; + +ALTER TABLE convoy.sources + ADD CONSTRAINT sources_source_verifier_id_fkey + FOREIGN KEY (source_verifier_id) + REFERENCES convoy.source_verifiers(id) + ON DELETE SET NULL; + +-- +migrate Up +REINDEX INDEX convoy.idx_sources_source_verifier_id; + +-- +migrate Up +REINDEX TABLE convoy.projects; +REINDEX TABLE convoy.meta_events; +REINDEX TABLE convoy.sources; + +-- +migrate Down +ALTER TABLE convoy.projects DROP CONSTRAINT IF EXISTS projects_project_configuration_id_fkey; +ALTER TABLE convoy.meta_events DROP CONSTRAINT IF EXISTS meta_events_project_id_fkey; +ALTER TABLE convoy.sources DROP CONSTRAINT IF EXISTS sources_source_verifier_id_fkey; + +-- +migrate Down +ALTER TABLE convoy.projects + ALTER COLUMN project_configuration_id TYPE CHAR(26) USING project_configuration_id::CHAR(26); +ALTER TABLE convoy.meta_events + ALTER COLUMN project_id TYPE CHAR(26) USING project_id::CHAR(26); +ALTER TABLE convoy.sources + ALTER COLUMN source_verifier_id TYPE CHAR(26) USING source_verifier_id::CHAR(26); + +-- +migrate Down +ALTER TABLE convoy.projects + ADD CONSTRAINT projects_project_configuration_id_fkey + FOREIGN KEY (project_configuration_id) + REFERENCES convoy.project_configurations(id) + ON DELETE CASCADE; + +ALTER TABLE convoy.meta_events + ADD CONSTRAINT meta_events_project_id_fkey + FOREIGN KEY (project_id) + REFERENCES convoy.projects(id) + ON DELETE CASCADE; + +ALTER TABLE convoy.sources + ADD CONSTRAINT sources_source_verifier_id_fkey + FOREIGN KEY (source_verifier_id) + REFERENCES convoy.source_verifiers(id) + ON DELETE SET NULL; + +-- +migrate Down +REINDEX INDEX convoy.idx_sources_source_verifier_id; + +-- +migrate Down +REINDEX TABLE convoy.projects; +REINDEX TABLE convoy.meta_events; +REINDEX TABLE convoy.sources; diff --git a/testcon/docker_e2e_integration_test_helper.go b/testcon/docker_e2e_integration_test_helper.go index bc16e218ae..9612e0f089 100644 --- a/testcon/docker_e2e_integration_test_helper.go +++ b/testcon/docker_e2e_integration_test_helper.go @@ -138,7 +138,7 @@ func getDB() database.Database { _ = os.Setenv("TZ", "") // Use UTC by default :) dbHooks := hooks.Init() - dbHooks.RegisterHook(datastore.EndpointCreated, func(data interface{}, changelog interface{}) {}) + dbHooks.RegisterHook(datastore.EndpointCreated, func(ctx context.Context, data interface{}, changelog interface{}) {}) pDB = db }) diff --git a/web/ui/dashboard-react/assets/svg/user-icon.svg b/web/ui/dashboard-react/assets/svg/user-icon.svg new file mode 100644 index 0000000000..50e7b0482d --- /dev/null +++ b/web/ui/dashboard-react/assets/svg/user-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/web/ui/dashboard-react/package.json b/web/ui/dashboard-react/package.json index 8482c2ab5a..9f1c20e9a9 100644 --- a/web/ui/dashboard-react/package.json +++ b/web/ui/dashboard-react/package.json @@ -13,12 +13,20 @@ "dependencies": { "@hookform/resolvers": "^4.0.0", "@radix-ui/colors": "^3.0.0", + "@radix-ui/react-avatar": "^1.1.3", + "@radix-ui/react-collapsible": "^1.1.3", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-popover": "^1.1.6", + "@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-tooltip": "^1.1.8", "@tanstack/react-router": "^1.106.0", "axios": "^1.7.9", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cmdk": "^1.0.0", "lucide-react": "^0.475.0", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/web/ui/dashboard-react/src/app/__root.tsx b/web/ui/dashboard-react/src/app/__root.tsx index c8fec1007e..9f7bcda9de 100644 --- a/web/ui/dashboard-react/src/app/__root.tsx +++ b/web/ui/dashboard-react/src/app/__root.tsx @@ -15,7 +15,7 @@ export const Route = createRootRoute({ <> - + {/* */} ), diff --git a/web/ui/dashboard-react/src/app/forgot-password.tsx b/web/ui/dashboard-react/src/app/forgot-password.tsx index 4b513f7c31..f9f88000c4 100644 --- a/web/ui/dashboard-react/src/app/forgot-password.tsx +++ b/web/ui/dashboard-react/src/app/forgot-password.tsx @@ -5,5 +5,5 @@ export const Route = createFileRoute('/forgot-password')({ }); function RouteComponent() { - return

Forgot Password

; + return

Forgot Password

; } diff --git a/web/ui/dashboard-react/src/app/get-started.tsx b/web/ui/dashboard-react/src/app/get-started.tsx index 38a5467ccf..0990b90d9c 100644 --- a/web/ui/dashboard-react/src/app/get-started.tsx +++ b/web/ui/dashboard-react/src/app/get-started.tsx @@ -5,5 +5,5 @@ export const Route = createFileRoute('/get-started')({ }); function RouteComponent() { - return

Get started!

; + return

Get started!

; } diff --git a/web/ui/dashboard-react/src/app/index.tsx b/web/ui/dashboard-react/src/app/index.tsx index 7aa57d61ce..45420efa5f 100644 --- a/web/ui/dashboard-react/src/app/index.tsx +++ b/web/ui/dashboard-react/src/app/index.tsx @@ -5,11 +5,7 @@ import { ensureCanAccessPrivatePages } from '@/lib/auth'; export const Route = createFileRoute('/')({ beforeLoad() { ensureCanAccessPrivatePages(); - router.navigate({ - to: '/projects', - // @ts-expect-error `pathname` is a defined route - from: router.state.location.pathname, - }); + router.navigate({ to: '/projects' }); }, component: Index, }); diff --git a/web/ui/dashboard-react/src/app/login.tsx b/web/ui/dashboard-react/src/app/login.tsx index a70ef7bfcc..7b141ef987 100644 --- a/web/ui/dashboard-react/src/app/login.tsx +++ b/web/ui/dashboard-react/src/app/login.tsx @@ -15,10 +15,10 @@ import { useEffect, useReducer, useState } from 'react'; import { useForm } from 'react-hook-form'; import { createFileRoute, useNavigate } from '@tanstack/react-router'; import { z } from 'zod'; -import { ConvoyLoader } from '@/components/ConvoyLoader'; +import { ConvoyLoader } from '@/components/convoy-loader'; import * as loginService from '@/services/login.service'; import * as signUpService from '@/services/signup.service'; -import * as privateService from '@/services/private.service'; +import * as organisationService from '@/services/organisations.service'; import * as licensesService from '@/services/licenses.service'; import type { UseFormReturn } from 'react-hook-form'; @@ -304,7 +304,7 @@ function LoginPage() { async function getOrganisations() { try { - await privateService.getOrganisations({ refresh: true }); + await organisationService.getOrganisations({ refresh: true }); } catch (err) { console.error(getOrganisations.name, err); } diff --git a/web/ui/dashboard-react/src/app/organisations/$organisationId/index.tsx b/web/ui/dashboard-react/src/app/organisations/$organisationId/index.tsx new file mode 100644 index 0000000000..7b644a65d5 --- /dev/null +++ b/web/ui/dashboard-react/src/app/organisations/$organisationId/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/organisations/$organisationId/')({ + component: RouteComponent, +}) + +function RouteComponent() { + return

Organisation Page

+} diff --git a/web/ui/dashboard-react/src/app/organisations/$organisationId/settings.tsx b/web/ui/dashboard-react/src/app/organisations/$organisationId/settings.tsx new file mode 100644 index 0000000000..47ef114208 --- /dev/null +++ b/web/ui/dashboard-react/src/app/organisations/$organisationId/settings.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/organisations/$organisationId/settings')( + { + component: RouteComponent, + }, +) + +function RouteComponent() { + return

Organisation Settings

+} diff --git a/web/ui/dashboard-react/src/app/organisations/index.tsx b/web/ui/dashboard-react/src/app/organisations/index.tsx new file mode 100644 index 0000000000..a48614b7a0 --- /dev/null +++ b/web/ui/dashboard-react/src/app/organisations/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router'; + +export const Route = createFileRoute('/organisations/')({ + component: RouteComponent, +}); + +function RouteComponent() { + return

Organisations

; +} diff --git a/web/ui/dashboard-react/src/app/projects/index.tsx b/web/ui/dashboard-react/src/app/projects/index.tsx index 95fa9320f9..5e42a94a51 100644 --- a/web/ui/dashboard-react/src/app/projects/index.tsx +++ b/web/ui/dashboard-react/src/app/projects/index.tsx @@ -1,5 +1,6 @@ import { createFileRoute } from '@tanstack/react-router'; import { ensureCanAccessPrivatePages } from '@/lib/auth'; +import { Dashboard } from '@/components/layout/dashboard'; export const Route = createFileRoute('/projects/')({ beforeLoad() { @@ -9,5 +10,9 @@ export const Route = createFileRoute('/projects/')({ }); function RouteComponent() { - return

Projects

; + return ( + + {/*
Page
*/} +
+ ); } diff --git a/web/ui/dashboard-react/src/app/user-settings.tsx b/web/ui/dashboard-react/src/app/user-settings.tsx new file mode 100644 index 0000000000..e558054d4e --- /dev/null +++ b/web/ui/dashboard-react/src/app/user-settings.tsx @@ -0,0 +1,10 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/user-settings')({ + component: RouteComponent, +}) + +function RouteComponent() { + return

User Settings

; + +} diff --git a/web/ui/dashboard-react/src/components/ConvoyLoader.tsx b/web/ui/dashboard-react/src/components/convoy-loader.tsx similarity index 100% rename from web/ui/dashboard-react/src/components/ConvoyLoader.tsx rename to web/ui/dashboard-react/src/components/convoy-loader.tsx diff --git a/web/ui/dashboard-react/src/components/dashboard-header.tsx b/web/ui/dashboard-react/src/components/dashboard-header.tsx new file mode 100644 index 0000000000..c017b5a28c --- /dev/null +++ b/web/ui/dashboard-react/src/components/dashboard-header.tsx @@ -0,0 +1,257 @@ +import { useEffect, useState } from 'react'; +import { + SidebarIcon, + ChevronDown, + CirclePlusIcon, + SettingsIcon, +} from 'lucide-react'; +import * as authService from '@/services/auth.service'; +import * as organisationsService from '@/services/organisations.service'; + +import { Button } from '@/components/ui/button'; +import { Avatar, AvatarFallback } from '@/components/ui/avatar'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { useSidebar } from '@/components/ui/sidebar'; + +import convoyLogo from '../../assets/svg/logo.svg'; +import userProfileIcon from '../../assets/svg/user-icon.svg'; + +import * as transform from '@/lib/pipes'; +import type { Organisation } from '@/models/organisation.model'; +import { useNavigate } from '@tanstack/react-router'; + +export function DashboardHeader() { + const { toggleSidebar } = useSidebar(); + const navigate = useNavigate({ from: '/projects' }); + // TODO move this to a hook for organisations + const [organisations, setOrganisations] = useState>([]); + const [selectedOrganisation, setSelectedOrganisation] = + useState(null); + const [isLoadingOrganisations, setIsLoadingOrganisations] = useState(false); + const [currentUser] = useState(authService.getCachedAuthProfile()); + + useEffect(() => { + setIsLoadingOrganisations(true); + organisationsService + .getOrganisations({ refresh: true }) + .then(res => setOrganisations(res.content)) + .catch(console.error) + .finally(() => { + setIsLoadingOrganisations(false); + setSelectedOrganisation(organisationsService.getCachedOrganisation()); + }); + + return () => {}; + }, []); + + function setCurrentOrganisation(org: Organisation) { + console.log( + `todo: set ${org.name} with id = ${org.uid} as current organisation`, + ); + } + + return ( +
+
+
+ + Convoy + +
+ +
+
+ + +
+
+ ); +} diff --git a/web/ui/dashboard-react/src/components/dashboard-sidebar.tsx b/web/ui/dashboard-react/src/components/dashboard-sidebar.tsx new file mode 100644 index 0000000000..4adc2c37bd --- /dev/null +++ b/web/ui/dashboard-react/src/components/dashboard-sidebar.tsx @@ -0,0 +1,343 @@ +import { z } from 'zod'; +import { useForm } from 'react-hook-form'; +import { useState, useEffect } from 'react'; +import { zodResolver } from '@hookform/resolvers/zod'; + +import { BookOpen, Check, ChevronsUpDown, HelpCircle } from 'lucide-react'; + +import { Button } from '@/components/ui/button'; +import { + Sidebar, + SidebarGroup, + SidebarContent, + SidebarFooter, + SidebarGroupContent, +} from '@/components/ui/sidebar'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/tooltip'; +import { + Popover, + PopoverTrigger, + PopoverContent, +} from '@/components/ui/popover'; +import { + Command, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, +} from '@/components/ui/command'; +import { FormField, FormItem, FormControl, Form } from '@/components/ui/form'; + +import { cn } from '@/lib/utils'; +import * as transform from '@/lib/pipes'; +import * as projectsService from '@/services/projects.service'; +import * as organisationsService from '@/services/organisations.service'; + +import type { ComponentProps } from 'react'; +import type { Project } from '@/models/project.model'; + +function FooterLinks() { + return ( + + ); +} + +function ProjectLinks() { + + const links = [ + { + name: 'Event Deliveries', + route: '/', + }, + { + name: 'Sources', + route: '/', + }, + { + name: 'Subscriptions', + route: '/', + }, + { + name: 'Endpoints', + route: '/', + }, + { + name: 'Events Log', + route: '/', + }, + { + name: 'Meta Events', + route: '/', + }, + { + name: 'Project Settings', + route: '/', + }, + ]; + + const [currentProject] = useState(projectsService.getCachedProject()); + + return ( + <> + {currentProject ? ( + + ) : null} + + ); +} + +const FormSchema = z.object({ + project: z.string({ + required_error: 'Please select a project.', + }), +}); + +function ProjectsList() { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [projects, setProjects] = useState>([]); + const [selectedProject, setSelectedProject] = useState(null); + + useEffect(() => { + projectsService + .getProjects({ refresh: true }) + .then(data => { + setProjects(data); + setSelectedProject(data?.at(0) || null); + projectsService.setCachedProject(data?.at(0) || null) + }) + .catch(console.error); + }, []); + + const form = useForm>({ + resolver: zodResolver(FormSchema), + }); + + return ( + <> + {selectedProject ? ( +
+ + ( + + + + + + + + + + { + form.setValue( + 'project', + (e.target as HTMLInputElement).value, + ); + }} + /> + + + {projects.length + ? `No projects found for '${field.value}'` + : 'No projects to filter'} + + + {projects.length > 0 && + projects.map(p => ( + { + setSelectedProject(p); + projectsService.setCachedProject(p) + setIsPopoverOpen(false); + }} + > +
+ + + + {transform.truncateProjectName(p.name)} +
+ + +
+ ))} +
+
+
+
+
+
+ )} + /> + + + ) : null} + + ); +} + +export function DashboardSidebar({ ...props }: ComponentProps) { + const [cachedOrganisation] = useState( + organisationsService.getCachedOrganisation(), + ); + return ( + + ); +} diff --git a/web/ui/dashboard-react/src/components/layout/dashboard.tsx b/web/ui/dashboard-react/src/components/layout/dashboard.tsx new file mode 100644 index 0000000000..807602a450 --- /dev/null +++ b/web/ui/dashboard-react/src/components/layout/dashboard.tsx @@ -0,0 +1,42 @@ +import { DashboardSidebar } from '@/components/dashboard-sidebar'; +import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar'; +import { DashboardHeader } from '../dashboard-header'; + +const dashboardHeaderWidth = '1440px'; +const dashboardHeaderTotalWidth = '1488px'; + +export function Dashboard(/* props: { children: ReactNode } */) { + return ( +
+ + +
+
+ + + {/*
*/} + {/* {props.children} */} +
+
+
+
+
+
+
+
+
+
+
+
+

Convoy Version

+
+ {/*
*/} + +
+
+ +
+ ); +} diff --git a/web/ui/dashboard-react/src/components/ui/avatar.tsx b/web/ui/dashboard-react/src/components/ui/avatar.tsx new file mode 100644 index 0000000000..991f56ecb1 --- /dev/null +++ b/web/ui/dashboard-react/src/components/ui/avatar.tsx @@ -0,0 +1,48 @@ +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/web/ui/dashboard-react/src/components/ui/collapsible.tsx b/web/ui/dashboard-react/src/components/ui/collapsible.tsx new file mode 100644 index 0000000000..a23e7a2812 --- /dev/null +++ b/web/ui/dashboard-react/src/components/ui/collapsible.tsx @@ -0,0 +1,9 @@ +import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" + +const Collapsible = CollapsiblePrimitive.Root + +const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger + +const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent + +export { Collapsible, CollapsibleTrigger, CollapsibleContent } diff --git a/web/ui/dashboard-react/src/components/ui/command.tsx b/web/ui/dashboard-react/src/components/ui/command.tsx new file mode 100644 index 0000000000..0db642a6e9 --- /dev/null +++ b/web/ui/dashboard-react/src/components/ui/command.tsx @@ -0,0 +1,151 @@ +import * as React from "react" +import { type DialogProps } from "@radix-ui/react-dialog" +import { Command as CommandPrimitive } from "cmdk" +import { Search } from "lucide-react" + +import { cn } from "@/lib/utils" +import { Dialog, DialogContent } from "@/components/ui/dialog" + +const Command = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Command.displayName = CommandPrimitive.displayName + +const CommandDialog = ({ children, ...props }: DialogProps) => { + return ( + + + + {children} + + + + ) +} + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)) + +CommandInput.displayName = CommandPrimitive.Input.displayName + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandList.displayName = CommandPrimitive.List.displayName + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)) + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandGroup.displayName = CommandPrimitive.Group.displayName + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +CommandSeparator.displayName = CommandPrimitive.Separator.displayName + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandItem.displayName = CommandPrimitive.Item.displayName + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +CommandShortcut.displayName = "CommandShortcut" + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +} diff --git a/web/ui/dashboard-react/src/components/ui/dialog.tsx b/web/ui/dashboard-react/src/components/ui/dialog.tsx new file mode 100644 index 0000000000..1647513ece --- /dev/null +++ b/web/ui/dashboard-react/src/components/ui/dialog.tsx @@ -0,0 +1,122 @@ +"use client" + +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/web/ui/dashboard-react/src/components/ui/dropdown-menu.tsx b/web/ui/dashboard-react/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000000..082639fb59 --- /dev/null +++ b/web/ui/dashboard-react/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,201 @@ +"use client" + +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { Check, ChevronRight, Circle } from "lucide-react" + +import { cn } from "@/lib/utils" + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + svg]:size-4 [&>svg]:shrink-0", + inset && "pl-8", + className + )} + {...props} + /> +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/web/ui/dashboard-react/src/components/ui/popover.tsx b/web/ui/dashboard-react/src/components/ui/popover.tsx new file mode 100644 index 0000000000..d82e714959 --- /dev/null +++ b/web/ui/dashboard-react/src/components/ui/popover.tsx @@ -0,0 +1,31 @@ +import * as React from "react" +import * as PopoverPrimitive from "@radix-ui/react-popover" + +import { cn } from "@/lib/utils" + +const Popover = PopoverPrimitive.Root + +const PopoverTrigger = PopoverPrimitive.Trigger + +const PopoverAnchor = PopoverPrimitive.Anchor + +const PopoverContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + + + +)) +PopoverContent.displayName = PopoverPrimitive.Content.displayName + +export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } diff --git a/web/ui/dashboard-react/src/components/ui/separator.tsx b/web/ui/dashboard-react/src/components/ui/separator.tsx new file mode 100644 index 0000000000..6d7f12265b --- /dev/null +++ b/web/ui/dashboard-react/src/components/ui/separator.tsx @@ -0,0 +1,29 @@ +import * as React from "react" +import * as SeparatorPrimitive from "@radix-ui/react-separator" + +import { cn } from "@/lib/utils" + +const Separator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>( + ( + { className, orientation = "horizontal", decorative = true, ...props }, + ref + ) => ( + + ) +) +Separator.displayName = SeparatorPrimitive.Root.displayName + +export { Separator } diff --git a/web/ui/dashboard-react/src/components/ui/sheet.tsx b/web/ui/dashboard-react/src/components/ui/sheet.tsx new file mode 100644 index 0000000000..272cb721eb --- /dev/null +++ b/web/ui/dashboard-react/src/components/ui/sheet.tsx @@ -0,0 +1,140 @@ +"use client" + +import * as React from "react" +import * as SheetPrimitive from "@radix-ui/react-dialog" +import { cva, type VariantProps } from "class-variance-authority" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Sheet = SheetPrimitive.Root + +const SheetTrigger = SheetPrimitive.Trigger + +const SheetClose = SheetPrimitive.Close + +const SheetPortal = SheetPrimitive.Portal + +const SheetOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SheetOverlay.displayName = SheetPrimitive.Overlay.displayName + +const sheetVariants = cva( + "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out", + { + variants: { + side: { + top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", + bottom: + "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", + left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", + right: + "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", + }, + }, + defaultVariants: { + side: "right", + }, + } +) + +interface SheetContentProps + extends React.ComponentPropsWithoutRef, + VariantProps {} + +const SheetContent = React.forwardRef< + React.ElementRef, + SheetContentProps +>(({ side = "right", className, children, ...props }, ref) => ( + + + + + + Close + + {children} + + +)) +SheetContent.displayName = SheetPrimitive.Content.displayName + +const SheetHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +SheetHeader.displayName = "SheetHeader" + +const SheetFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +SheetFooter.displayName = "SheetFooter" + +const SheetTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SheetTitle.displayName = SheetPrimitive.Title.displayName + +const SheetDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SheetDescription.displayName = SheetPrimitive.Description.displayName + +export { + Sheet, + SheetPortal, + SheetOverlay, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, +} diff --git a/web/ui/dashboard-react/src/components/ui/sidebar.tsx b/web/ui/dashboard-react/src/components/ui/sidebar.tsx new file mode 100644 index 0000000000..886340ce17 --- /dev/null +++ b/web/ui/dashboard-react/src/components/ui/sidebar.tsx @@ -0,0 +1,780 @@ +'use client'; + +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { type VariantProps, cva } from 'class-variance-authority'; +import { PanelLeft } from 'lucide-react'; + +import { useIsMobile } from '@/hooks/use-mobile'; +import { cn } from '@/lib/utils'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Separator } from '@/components/ui/separator'; +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from '@/components/ui/sheet'; +import { Skeleton } from '@/components/ui/skeleton'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/tooltip'; + +const SIDEBAR_COOKIE_NAME = 'sidebar_state'; +const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; +const SIDEBAR_WIDTH = '18rem'; +const SIDEBAR_WIDTH_MOBILE = '18rem'; +const SIDEBAR_WIDTH_ICON = '3rem'; +const SIDEBAR_KEYBOARD_SHORTCUT = 'b'; + +type SidebarContext = { + state: 'expanded' | 'collapsed'; + open: boolean; + setOpen: (open: boolean) => void; + openMobile: boolean; + setOpenMobile: (open: boolean) => void; + isMobile: boolean; + toggleSidebar: () => void; +}; + +const SidebarContext = React.createContext(null); + +function useSidebar() { + const context = React.useContext(SidebarContext); + if (!context) { + throw new Error('useSidebar must be used within a SidebarProvider.'); + } + + return context; +} + +const SidebarProvider = React.forwardRef< + HTMLDivElement, + React.ComponentProps<'div'> & { + defaultOpen?: boolean; + open?: boolean; + onOpenChange?: (open: boolean) => void; + } +>( + ( + { + defaultOpen = true, + open: openProp, + onOpenChange: setOpenProp, + className, + style, + children, + ...props + }, + ref, + ) => { + const isMobile = useIsMobile(); + const [openMobile, setOpenMobile] = React.useState(false); + + // This is the internal state of the sidebar. + // We use openProp and setOpenProp for control from outside the component. + const [_open, _setOpen] = React.useState(defaultOpen); + const open = openProp ?? _open; + const setOpen = React.useCallback( + (value: boolean | ((value: boolean) => boolean)) => { + const openState = typeof value === 'function' ? value(open) : value; + if (setOpenProp) { + setOpenProp(openState); + } else { + _setOpen(openState); + } + + // This sets the cookie to keep the sidebar state. + document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; + }, + [setOpenProp, open], + ); + + // Helper to toggle the sidebar. + const toggleSidebar = React.useCallback(() => { + return isMobile ? setOpenMobile(open => !open) : setOpen(open => !open); + }, [isMobile, setOpen, setOpenMobile]); + + // Adds a keyboard shortcut to toggle the sidebar. + React.useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if ( + event.key === SIDEBAR_KEYBOARD_SHORTCUT && + (event.metaKey || event.ctrlKey) + ) { + event.preventDefault(); + toggleSidebar(); + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [toggleSidebar]); + + // We add a state so that we can do data-state="expanded" or "collapsed". + // This makes it easier to style the sidebar with Tailwind classes. + const state = open ? 'expanded' : 'collapsed'; + + const contextValue = React.useMemo( + () => ({ + state, + open, + setOpen, + isMobile, + openMobile, + setOpenMobile, + toggleSidebar, + }), + [ + state, + open, + setOpen, + isMobile, + openMobile, + setOpenMobile, + toggleSidebar, + ], + ); + + return ( + + +
+ {children} +
+
+
+ ); + }, +); +SidebarProvider.displayName = 'SidebarProvider'; + +const Sidebar = React.forwardRef< + HTMLDivElement, + React.ComponentProps<'div'> & { + side?: 'left' | 'right'; + variant?: 'sidebar' | 'floating' | 'inset'; + collapsible?: 'offcanvas' | 'icon' | 'none'; + } +>( + ( + { + side = 'left', + variant = 'sidebar', + collapsible = 'offcanvas', + className, + children, + ...props + }, + ref, + ) => { + const { isMobile, state, openMobile, setOpenMobile } = useSidebar(); + + if (collapsible === 'none') { + return ( +
+ {children} +
+ ); + } + + if (isMobile) { + return ( + + + + Sidebar + Displays the mobile sidebar. + +
{children}
+
+
+ ); + } + + return ( +
+ {/* This is what handles the sidebar gap on desktop */} +
+ +
+ ); + }, +); +Sidebar.displayName = 'Sidebar'; + +const SidebarTrigger = React.forwardRef< + React.ElementRef, + React.ComponentProps +>(({ className, onClick, ...props }, ref) => { + const { toggleSidebar } = useSidebar(); + + return ( + + ); +}); +SidebarTrigger.displayName = 'SidebarTrigger'; + +const SidebarRail = React.forwardRef< + HTMLButtonElement, + React.ComponentProps<'button'> +>(({ className, ...props }, ref) => { + const { toggleSidebar } = useSidebar(); + + return ( +
-
-
- + + +
@@ -369,13 +371,13 @@

Event Types

- - + + + + + organisation + + + Your organisation information will help us to know how to get you + set up. + + +
+
+ + void form.handleSubmit(createOrganisation)(...args) + } + > + ( + +
+ + What is your business's name? + +
+ + + + +
+ )} + /> +
+ + + + +
+ + +
+
+ +
+ ); +} + +function RouteComponent() { + const [isLoadingOrganisations, setIsLoadingOrganisations] = + useState(false); + const [organisations, setOrganisations] = useState([]); + + // TODO use a hook for organisations and projects + const [currentProject /* setCurrentProject */] = useState( + projectsService.getCachedProject(), + ); + + useEffect(() => { + getOrganisations(); + + return () => { + // clear all requests on unmount component + }; + }, []); + + function getOrganisations() { + setIsLoadingOrganisations(true) + orgsService + .getOrganisations({ refresh: true }) + .then(({ content }) => setOrganisations(content)) + // TODO use toast component to show UI error on all catch(error) where necessary + .catch(console.error) + .finally(() => { + setIsLoadingOrganisations(false) + }) + } + + if (isLoadingOrganisations) return ; + + if (organisations.length == 0) + return ( + { + console.log('org created'); + getOrganisations() + }} + /> + ); + + if (!currentProject) { + return ( +

+ TODO: create project component +

+ ); + } + + return ( + +

+
+ ); +} diff --git a/web/ui/dashboard-react/src/app/projects/index.tsx b/web/ui/dashboard-react/src/app/projects/index.tsx deleted file mode 100644 index 5e42a94a51..0000000000 --- a/web/ui/dashboard-react/src/app/projects/index.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router'; -import { ensureCanAccessPrivatePages } from '@/lib/auth'; -import { Dashboard } from '@/components/layout/dashboard'; - -export const Route = createFileRoute('/projects/')({ - beforeLoad() { - ensureCanAccessPrivatePages(); - }, - component: RouteComponent, -}); - -function RouteComponent() { - return ( - - {/*
Page
*/} -
- ); -} diff --git a/web/ui/dashboard-react/src/app/signup.tsx b/web/ui/dashboard-react/src/app/signup.tsx index 3384aa5981..d60d5d89b2 100644 --- a/web/ui/dashboard-react/src/app/signup.tsx +++ b/web/ui/dashboard-react/src/app/signup.tsx @@ -2,9 +2,11 @@ import { useState } from 'react'; import { z } from 'zod'; import { cn } from '@/lib/utils'; import { createFileRoute, useNavigate } from '@tanstack/react-router'; -import * as signUpService from '@/services/signup.service'; + +import * as authService from '@/services/auth.service'; import * as hubSpotService from '@/services/hubspot.service'; import * as licensesService from '@/services/licenses.service'; + import { router } from '@/lib/router'; import { CONVOY_DASHBOARD_DOMAIN } from '@/lib/constants'; import { zodResolver } from '@hookform/resolvers/zod'; @@ -253,7 +255,7 @@ function SignUpWithSAMLButton() { localStorage.setItem('AUTH_TYPE', 'signup'); try { - const { data } = await signUpService.signUpWithSAML(); + const { data } = await authService.signUpWithSAML(); window.open(data.redirectUrl, '_blank'); } catch (err) { // TODO show user on the UI @@ -303,7 +305,7 @@ function SignUpPage() { const { email, firstName, lastName, orgName, password } = values; try { - await signUpService.signUp({ + await authService.signUp({ email, password, org_name: orgName, diff --git a/web/ui/dashboard-react/src/components/dashboard-header.tsx b/web/ui/dashboard-react/src/components/dashboard-header.tsx deleted file mode 100644 index c017b5a28c..0000000000 --- a/web/ui/dashboard-react/src/components/dashboard-header.tsx +++ /dev/null @@ -1,257 +0,0 @@ -import { useEffect, useState } from 'react'; -import { - SidebarIcon, - ChevronDown, - CirclePlusIcon, - SettingsIcon, -} from 'lucide-react'; -import * as authService from '@/services/auth.service'; -import * as organisationsService from '@/services/organisations.service'; - -import { Button } from '@/components/ui/button'; -import { Avatar, AvatarFallback } from '@/components/ui/avatar'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import { useSidebar } from '@/components/ui/sidebar'; - -import convoyLogo from '../../assets/svg/logo.svg'; -import userProfileIcon from '../../assets/svg/user-icon.svg'; - -import * as transform from '@/lib/pipes'; -import type { Organisation } from '@/models/organisation.model'; -import { useNavigate } from '@tanstack/react-router'; - -export function DashboardHeader() { - const { toggleSidebar } = useSidebar(); - const navigate = useNavigate({ from: '/projects' }); - // TODO move this to a hook for organisations - const [organisations, setOrganisations] = useState>([]); - const [selectedOrganisation, setSelectedOrganisation] = - useState(null); - const [isLoadingOrganisations, setIsLoadingOrganisations] = useState(false); - const [currentUser] = useState(authService.getCachedAuthProfile()); - - useEffect(() => { - setIsLoadingOrganisations(true); - organisationsService - .getOrganisations({ refresh: true }) - .then(res => setOrganisations(res.content)) - .catch(console.error) - .finally(() => { - setIsLoadingOrganisations(false); - setSelectedOrganisation(organisationsService.getCachedOrganisation()); - }); - - return () => {}; - }, []); - - function setCurrentOrganisation(org: Organisation) { - console.log( - `todo: set ${org.name} with id = ${org.uid} as current organisation`, - ); - } - - return ( -
-
-
- - Convoy - -
- -
-
- - -
-
- ); -} diff --git a/web/ui/dashboard-react/src/components/dashboard-sidebar.tsx b/web/ui/dashboard-react/src/components/dashboard-sidebar.tsx deleted file mode 100644 index 4adc2c37bd..0000000000 --- a/web/ui/dashboard-react/src/components/dashboard-sidebar.tsx +++ /dev/null @@ -1,343 +0,0 @@ -import { z } from 'zod'; -import { useForm } from 'react-hook-form'; -import { useState, useEffect } from 'react'; -import { zodResolver } from '@hookform/resolvers/zod'; - -import { BookOpen, Check, ChevronsUpDown, HelpCircle } from 'lucide-react'; - -import { Button } from '@/components/ui/button'; -import { - Sidebar, - SidebarGroup, - SidebarContent, - SidebarFooter, - SidebarGroupContent, -} from '@/components/ui/sidebar'; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '@/components/ui/tooltip'; -import { - Popover, - PopoverTrigger, - PopoverContent, -} from '@/components/ui/popover'; -import { - Command, - CommandInput, - CommandList, - CommandEmpty, - CommandGroup, - CommandItem, -} from '@/components/ui/command'; -import { FormField, FormItem, FormControl, Form } from '@/components/ui/form'; - -import { cn } from '@/lib/utils'; -import * as transform from '@/lib/pipes'; -import * as projectsService from '@/services/projects.service'; -import * as organisationsService from '@/services/organisations.service'; - -import type { ComponentProps } from 'react'; -import type { Project } from '@/models/project.model'; - -function FooterLinks() { - return ( - - ); -} - -function ProjectLinks() { - - const links = [ - { - name: 'Event Deliveries', - route: '/', - }, - { - name: 'Sources', - route: '/', - }, - { - name: 'Subscriptions', - route: '/', - }, - { - name: 'Endpoints', - route: '/', - }, - { - name: 'Events Log', - route: '/', - }, - { - name: 'Meta Events', - route: '/', - }, - { - name: 'Project Settings', - route: '/', - }, - ]; - - const [currentProject] = useState(projectsService.getCachedProject()); - - return ( - <> - {currentProject ? ( - - ) : null} - - ); -} - -const FormSchema = z.object({ - project: z.string({ - required_error: 'Please select a project.', - }), -}); - -function ProjectsList() { - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const [projects, setProjects] = useState>([]); - const [selectedProject, setSelectedProject] = useState(null); - - useEffect(() => { - projectsService - .getProjects({ refresh: true }) - .then(data => { - setProjects(data); - setSelectedProject(data?.at(0) || null); - projectsService.setCachedProject(data?.at(0) || null) - }) - .catch(console.error); - }, []); - - const form = useForm>({ - resolver: zodResolver(FormSchema), - }); - - return ( - <> - {selectedProject ? ( -
- - ( - - - - - - - - - - { - form.setValue( - 'project', - (e.target as HTMLInputElement).value, - ); - }} - /> - - - {projects.length - ? `No projects found for '${field.value}'` - : 'No projects to filter'} - - - {projects.length > 0 && - projects.map(p => ( - { - setSelectedProject(p); - projectsService.setCachedProject(p) - setIsPopoverOpen(false); - }} - > -
- - - - {transform.truncateProjectName(p.name)} -
- - -
- ))} -
-
-
-
-
-
- )} - /> - - - ) : null} - - ); -} - -export function DashboardSidebar({ ...props }: ComponentProps) { - const [cachedOrganisation] = useState( - organisationsService.getCachedOrganisation(), - ); - return ( - - ); -} diff --git a/web/ui/dashboard-react/src/components/dashboard.tsx b/web/ui/dashboard-react/src/components/dashboard.tsx new file mode 100644 index 0000000000..08230808ab --- /dev/null +++ b/web/ui/dashboard-react/src/components/dashboard.tsx @@ -0,0 +1,631 @@ +import { z } from 'zod'; +import { useForm } from 'react-hook-form'; +import { useEffect, useState } from 'react'; +import { useNavigate } from '@tanstack/react-router'; +import { zodResolver } from '@hookform/resolvers/zod'; + +import { + SidebarIcon, + ChevronDown, + CirclePlusIcon, + SettingsIcon, + BookOpen, + Check, + ChevronsUpDown, + HelpCircle, +} from 'lucide-react'; + +import { Button } from '@/components/ui/button'; +import { Avatar, AvatarFallback } from '@/components/ui/avatar'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { + useSidebar, + Sidebar, + SidebarGroup, + SidebarContent, + SidebarFooter, + SidebarGroupContent, + SidebarProvider, + SidebarInset, +} from '@/components/ui/sidebar'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/tooltip'; +import { + Popover, + PopoverTrigger, + PopoverContent, +} from '@/components/ui/popover'; +import { + Command, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, +} from '@/components/ui/command'; +import { FormField, FormItem, FormControl, Form } from '@/components/ui/form'; + +import { cn } from '@/lib/utils'; +import { Route } from '@/app/__root'; +import * as transform from '@/lib/pipes'; +import * as authService from '@/services/auth.service'; +import * as projectsService from '@/services/projects.service'; +import * as organisationsService from '@/services/organisations.service'; + +import convoyLogo from '../../assets/svg/logo.svg'; +import userProfileIcon from '../../assets/svg/user-icon.svg'; + +import type { Project } from '@/models/project.model'; +import type { ComponentProps, ReactNode } from 'react'; +import type { Organisation } from '@/models/organisation.model'; + +function HeaderLeftLogo() { + const { toggleSidebar } = useSidebar(); + + return ( +
+ + Convoy + +
+ +
+
+ ); +} + +function HeaderRightProfile() { + const [currentUser] = useState(authService.getCachedAuthProfile()); + + if (!currentUser) + return ( +
  • + skeleton +
  • + ); + + return ( +
  • + + + + + + +
    +

    + {currentUser.first_name || ''} {currentUser.last_name || ''} +

    +

    + {currentUser.email} +

    +
    +
    + + + + My account + + + + + + Logout + + +
    +
    +
  • + ); +} + +function HeaderRightOrganisation() { + const navigate = useNavigate({ from: Route.fullPath }); + + // TODO move this to a hook for organisations - useOrganisation + const [organisations, setOrganisations] = useState>([]); + const [selectedOrganisation, setSelectedOrganisation] = + useState(null); + const [isLoadingOrganisations, setIsLoadingOrganisations] = useState(false); + + useEffect(() => { + setIsLoadingOrganisations(true); + organisationsService + .getOrganisations({ refresh: true }) + .then(({ content }) => setOrganisations(content)) + .catch(console.error) + .finally(() => { + setIsLoadingOrganisations(false); + setSelectedOrganisation(organisationsService.getCachedOrganisation()); + }); + + return () => {}; + }, []); + + function setCurrentOrganisation(org: Organisation) { + console.log( + `todo: set ${org.name} with id = ${org.uid} as current organisation`, + ); + } + + if (isLoadingOrganisations) return

    skeleton

    ; + if (!selectedOrganisation) return null; + + return ( +
  • + + + + + + + + + Your organisations ({organisations.length}) + + {/* TODO there is a 'padlockicon Business' overlay here. Check and create */} + + + + + + navigate({ to: '/projects' })} + > + + + + + + + + + + +
      + {organisations + .filter(org => org.uid != selectedOrganisation.uid) + .toSorted((oA, oB) => { + if (oA.name < oB.name) return -1; + if (oA.name > oB.name) return 1; + return 0; + }) + .map(org => { + return ( + setCurrentOrganisation(org)} + > + + + ); + })} +
    +
    + + + +
    + + + Add {organisations.length == 0 ? 'an' : 'another'} organisation + +
    +
    +
    +
    +
  • + ); +} + +function HeaderRight() { + return ( + + ); +} + +export function DashboardHeader() { + return ( +
    +
    + + +
    +
    + ); +} + +const FormSchema = z.object({ + project: z.string({ + required_error: 'Please select a project.', + }), +}); + +function ProjectsList() { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [projects, setProjects] = useState>([]); + const [selectedProject, setSelectedProject] = useState(null); + + useEffect(() => { + projectsService + .getProjects({ refresh: true }) + .then(data => { + setProjects(data); + setSelectedProject(data?.at(0) || null); + projectsService.setCachedProject(data?.at(0) || null); + }) + .catch(console.error); + }, []); + + const form = useForm>({ + resolver: zodResolver(FormSchema), + }); + + return ( + <> + {selectedProject ? ( +
    + + ( + + + + + + + + + + { + form.setValue( + 'project', + (e.target as HTMLInputElement).value, + ); + }} + /> + + + {projects.length + ? `No projects found for '${field.value}'` + : 'No projects to filter'} + + + {projects.length > 0 && + projects.map(p => ( + { + setSelectedProject(p); + projectsService.setCachedProject(p); + setIsPopoverOpen(false); + }} + > +
    + + + + {transform.truncateProjectName(p.name)} +
    + + +
    + ))} +
    +
    +
    +
    +
    +
    + )} + /> + + + ) : null} + + ); +} + +function ProjectLinks() { + const links = [ + { + name: 'Event Deliveries', + route: '/', + }, + { + name: 'Sources', + route: '/', + }, + { + name: 'Subscriptions', + route: '/', + }, + { + name: 'Endpoints', + route: '/', + }, + { + name: 'Events Log', + route: '/', + }, + { + name: 'Meta Events', + route: '/', + }, + { + name: 'Project Settings', + route: '/', + }, + ]; + + const [currentProject] = useState(projectsService.getCachedProject()); + + return ( + <> + {currentProject ? ( + + ) : null} + + ); +} + +function FooterLinks() { + return ( + + ); +} + +export function DashboardSidebar({ ...props }: ComponentProps) { + const [cachedOrganisation] = useState( + organisationsService.getCachedOrganisation(), + ); + return ( + + ); +} + +export function DashboardLayout(props: { children: ReactNode }) { + return ( +
    + + +
    + + {props.children} +
    +
    +
    + ); +} diff --git a/web/ui/dashboard-react/src/components/layout/dashboard.tsx b/web/ui/dashboard-react/src/components/layout/dashboard.tsx deleted file mode 100644 index 807602a450..0000000000 --- a/web/ui/dashboard-react/src/components/layout/dashboard.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { DashboardSidebar } from '@/components/dashboard-sidebar'; -import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar'; -import { DashboardHeader } from '../dashboard-header'; - -const dashboardHeaderWidth = '1440px'; -const dashboardHeaderTotalWidth = '1488px'; - -export function Dashboard(/* props: { children: ReactNode } */) { - return ( -
    - - -
    -
    - - - {/*
    */} - {/* {props.children} */} -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -

    Convoy Version

    -
    - {/*
    */} - -
    -
    - -
    - ); -} diff --git a/web/ui/dashboard-react/src/components/ui/dialog.tsx b/web/ui/dashboard-react/src/components/ui/dialog.tsx index 1647513ece..857b16335d 100644 --- a/web/ui/dashboard-react/src/components/ui/dialog.tsx +++ b/web/ui/dashboard-react/src/components/ui/dialog.tsx @@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef<
    ); } diff --git a/web/ui/dashboard-react/src/components/dashboard.tsx b/web/ui/dashboard-react/src/components/dashboard.tsx index 2bfaf2c52b..393ae16c8d 100644 --- a/web/ui/dashboard-react/src/components/dashboard.tsx +++ b/web/ui/dashboard-react/src/components/dashboard.tsx @@ -1,8 +1,8 @@ import { z } from 'zod'; import { useForm } from 'react-hook-form'; import { useEffect, useState } from 'react'; -import { useNavigate } from '@tanstack/react-router'; import { zodResolver } from '@hookform/resolvers/zod'; +import { useNavigate, Link } from '@tanstack/react-router'; import { SidebarIcon, @@ -57,16 +57,13 @@ import { } from '@/components/ui/command'; import { FormField, FormItem, FormControl, Form } from '@/components/ui/form'; -import { useOrganisationContext } from '@/contexts/organisation'; - import { cn } from '@/lib/utils'; import { Route } from '@/app/__root'; import * as transform from '@/lib/pipes'; -import { useAuth } from '@/hooks/use-auth'; import * as authService from '@/services/auth.service'; import * as projectsService from '@/services/projects.service'; -import * as licensesService from '@/services/licenses.service'; import * as orgsService from '@/services/organisations.service'; +import { useLicenseStore, useOrganisationStore } from '@/store'; import convoyLogo from '../../assets/svg/logo.svg'; import userProfileIcon from '../../assets/svg/user-icon.svg'; @@ -74,36 +71,14 @@ import userProfileIcon from '../../assets/svg/user-icon.svg'; import type { Project } from '@/models/project.model'; import type { ComponentProps, ReactNode } from 'react'; -function HeaderLeftLogo() { - const { toggleSidebar } = useSidebar(); - - return ( -
    - - Convoy - -
    - -
    -
    - ); -} - function HeaderRightProfile() { - const { getCurrentUser } = useAuth(); - const [currentUser] = useState(getCurrentUser()); + const { auth } = Route.useRouteContext(); + const currentUser = auth?.getCurrentUser() || null; if (!currentUser) return (
  • + {/* TODO code this out */} skeleton
  • ); @@ -123,21 +98,21 @@ function HeaderRightProfile() {

    - {currentUser.first_name || ''} {currentUser.last_name || ''} + {currentUser?.first_name || ''} {currentUser?.last_name || ''}

    - {currentUser.email} + {currentUser?.email}

    - My account - + { - getOrganisations(); - return () => {}; - }, []); + const [canCreateOrg] = useState(licenses.includes('CREATE_ORG')); - function getOrganisations() { - setIsLoadingOrganisations(true); + async function reloadOrganisations() { orgsService - .getOrganisations({ refresh: true }) - .then(({ content }) => { - setOrganisations(content); + .getOrganisations() + .then(pgOrgs => { + setPaginatedOrgs(pgOrgs); + setOrg(pgOrgs.content.at(0) || null); }) - .catch(console.error) - .finally(() => { - setIsLoadingOrganisations(false); - }); + // TODO use toast component to show UI error on all catch(error) where necessary + .catch(console.error); } - // TODO code out this skeleton part - if (isLoadingOrganisations) return

    skeleton

    ; - if (!currentOrganisation) return null; + if (!org) return null; return (
  • @@ -199,12 +160,10 @@ function HeaderRightOrganisation() { > - {transform.getInitials(currentOrganisation.name.split(' '))} + {transform.getInitials(org.name.split(' '))} - - {currentOrganisation?.name} - + {org?.name} @@ -212,7 +171,7 @@ function HeaderRightOrganisation() { - Your organisations ({organisations.length}) + Your organisations ({paginatedOrgs.content.length}) {/* TODO there is a 'padlockicon Business' overlay here. Check and create */} @@ -230,15 +189,15 @@ function HeaderRightOrganisation() { > - {transform.getInitials(currentOrganisation.name.split(' '))} + {transform.getInitials(org.name.split(' '))} - {currentOrganisation.name} + {org.name} @@ -250,19 +209,19 @@ function HeaderRightOrganisation() {
      - {organisations - .filter(org => org.uid != currentOrganisation.uid) - .toSorted((oA, oB) => { - if (oA.name < oB.name) return -1; - if (oA.name > oB.name) return 1; + {paginatedOrgs.content + .filter(_ => _.uid != org.uid) + .toSorted((orgA, orgB) => { + if (orgA.name < orgB.name) return -1; + if (orgA.name > orgB.name) return 1; return 0; }) - .map(org => { + .map(_org => { return ( setCurrentOrganisation(org)} + onClick={() => setOrg(_org)} > @@ -301,14 +260,14 @@ function HeaderRightOrganisation() { size={20} /> - Add {organisations.length == 0 ? 'an' : 'another'}{' '} + Add {paginatedOrgs.content.length == 0 ? 'an' : 'another'}{' '} organisation
  • } isDialogOpen={isDialogOpen} - onOrgCreated={getOrganisations} + onOrgCreated={reloadOrganisations} setIsDialogOpen={setIsDialogOpen} /> @@ -328,11 +287,30 @@ function HeaderRight() { ); } -export function DashboardHeader() { +export function DashboardHeader(props: { showToggleSidebarButton: boolean }) { + const { toggleSidebar } = useSidebar(); + return (
    - +
    + + Convoy + + {props.showToggleSidebarButton ? ( +
    + +
    + ) : null} +
    @@ -528,12 +506,12 @@ function ProjectLinks() { return (
  • {/* TODO change to link route */} - {link.name} - +
  • ); })} @@ -576,18 +554,13 @@ function FooterLinks() { } export function DashboardSidebar({ ...props }: ComponentProps) { - const { currentOrganisation, setOrganisations } = useOrganisationContext(); + const { org } = useOrganisationStore(); + const { licenses } = useLicenseStore(); const [canCreateProject] = useState( - licensesService.hasLicense('CREATE_PROJECT'), + licenses.includes('CREATE_PROJECT'), ); - useEffect(() => { - orgsService.getOrganisations({ refresh: false }).then(setOrganisations); - - return () => {}; - }, []); - return (
    ); } return

    TODO: create project default UI

    ; } + +// FIX Semantic HTML says anchors shoudld be anchors and buttons, buttons. +// Style an anchor tag like a button if you want it to look like one. +// See https://stackoverflow.com/q/64443645 diff --git a/web/ui/dashboard-react/src/app/projects_/new.tsx b/web/ui/dashboard-react/src/app/projects_/new.tsx new file mode 100644 index 0000000000..8353ff73f3 --- /dev/null +++ b/web/ui/dashboard-react/src/app/projects_/new.tsx @@ -0,0 +1,1069 @@ +import { z } from 'zod'; +import { useEffect, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { createFileRoute, Link } from '@tanstack/react-router'; + +import { CopyIcon } from 'lucide-react'; + +import { + Dialog, + DialogTrigger, + DialogContent, + DialogTitle, + DialogDescription, + DialogClose, + DialogHeader, + DialogFooter, +} from '@/components/ui/dialog'; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from '@/components/ui/accordion'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Form } from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { + FormField, + FormItem, + FormLabel, + FormControl, + FormMessageWithErrorIcon, +} from '@/components/ui/form'; +import { DashboardLayout } from '@/components/dashboard'; + +import { cn } from '@/lib/utils'; +import { ensureCanAccessPrivatePages } from '@/lib/auth'; +import * as projectsService from '@/services/projects.service'; + +import modalCloseIcon from '../../../assets/svg/modal-close-icon.svg'; +import successAnimation from '../../../assets/img/success.gif'; + +export const Route = createFileRoute('/projects_/new')({ + component: CreateNewProject, + beforeLoad({ context }) { + ensureCanAccessPrivatePages(context.auth?.getTokens().isLoggedIn); + }, +}); + +const CreateProjectFormSchema = z.object({ + name: z + .string({ + required_error: 'Project name is required', + }) + .min(1, 'Project name is required'), + type: z + .enum(['incoming', 'outgoing', ''], { + required_error: 'Please select a project type', + }) + .refine(v => v.length != 0, { + message: 'Choose a project webhook type', + }), + config: z + .object({ + search_policy: z + .object({ + isEnabled: z.boolean().optional(), + search_policy: z + .string() + .optional() + .transform(v => (!v?.length ? undefined : `${v}h`)), + }) + .transform(search => (search?.isEnabled ? search : undefined)) + .refine( + search => { + if (search?.isEnabled && !search.search_policy) return false; + return true; + }, + { + message: 'Invalid Search Policy', + path: ['search_policy'], + }, + ), + strategy: z + .object({ + isEnabled: z.boolean().optional(), + duration: z + .string() + .optional() + .transform(v => (!v?.length ? undefined : Number(v))), + retry_count: z + .string() + .optional() + .transform(v => (!v?.length ? undefined : Number(v))), + type: z + .enum(['linear', 'exponential', '']) + .optional() + .transform(v => (v?.length == 0 ? undefined : v)), + }) + .optional() + .transform(strategy => (strategy?.isEnabled ? strategy : undefined)) + .refine( + strategy => { + if ( + strategy?.isEnabled && + (!strategy?.duration || !strategy.retry_count || !strategy.type) + ) + return false; + + return true; + }, + strategy => { + if (!strategy?.duration) { + return { + message: 'Invalid retry logic duration', + path: ['duration'], + }; + } + if (!strategy?.type) { + return { + message: 'Invalid retry logic mechanism', + path: ['type'], + }; + } + if (!strategy?.retry_count) { + return { + message: 'Invalid retry logic limit', + path: ['retry_count'], + }; + } + return { + message: '', + }; + }, + ), + signature: z + .object({ + isEnabled: z.boolean().optional(), + header: z + .string() + .optional() + .transform(v => (v?.length == 0 ? undefined : v)), + encoding: z + .enum(['hex', 'base64', '']) + .optional() + .transform(v => (v?.length == 0 ? undefined : v)), + hash: z + .enum(['SHA256', 'SHA512', '']) + .optional() + .transform(v => (v?.length == 0 ? undefined : v)), + }) + .optional() + .transform(sig => (sig?.isEnabled ? sig : undefined)) + .refine( + sig => { + if (sig?.isEnabled && (!sig.header || !sig.encoding || !sig.hash)) + return false; + return true; + }, + sig => { + if (!sig?.header) { + return { message: 'Invalid signature header', path: ['header'] }; + } + if (!sig?.encoding) { + return { + message: 'Invalid signature encoding', + path: ['encoding'], + }; + } + if (!sig?.hash) { + return { message: 'Invalid signature hash', path: ['hash'] }; + } + return { message: '' }; + }, + ), + ratelimit: z + .object({ + isEnabled: z.boolean().optional(), + count: z + .string() + .optional() + .transform(v => (!v?.length ? undefined : Number(v))), + duration: z + .string() + .optional() + .transform(v => (!v?.length ? undefined : Number(v))), + }) + .optional() + .transform(ratelimit => (ratelimit?.isEnabled ? ratelimit : undefined)) + .refine( + ratelimit => { + const isInvalidRateLimitOpts = + ratelimit?.isEnabled && + (!ratelimit?.count || !ratelimit?.duration); + + return !isInvalidRateLimitOpts; + }, + ratelimit => { + if (ratelimit?.count == undefined) + return { message: 'Invalid rate limit count', path: ['count'] }; + + if (ratelimit?.duration == undefined) + return { + message: 'Invalid rate limit duration', + path: ['duration'], + }; + + return { message: '' }; + }, + ), + }) + .optional(), +}); + +function CreateNewProject() { + const [hasCreatedProject, setHasCreatedProject] = useState(false); + const [projectkey, setProjectkey] = useState(''); + const form = useForm>({ + resolver: zodResolver(CreateProjectFormSchema), + defaultValues: { + name: '', + type: '', + config: { + search_policy: { + isEnabled: false, + search_policy: '', + }, + ratelimit: { + isEnabled: false, + // @ts-expect-error the input values are strings, so this is correct. There is transform that converts this to a number + count: '', + // @ts-expect-error the input values are strings, so this is correct. There is transform that converts this to a number + duration: '', + }, + strategy: { + isEnabled: false, + // @ts-expect-error the input values are strings, so this is correct. There is transform that converts this to a number + duration: '', + // @ts-expect-error the input values are strings, so this is correct. There is transform that converts this to a number + retry_count: '', + type: 'linear', + }, + signature: { + isEnabled: false, + header: '', + encoding: '', + hash: '', + }, + }, + }, + mode: 'onTouched', + }); + + const selectedWebhookType = form.watch('type'); + const shouldShowRetryConfig = form.watch('config.strategy.isEnabled'); + const shouldShowRateLimit = form.watch('config.ratelimit.isEnabled'); + const shouldShowSearchPolicy = form.watch('config.search_policy.isEnabled'); + const shouldShowSigFormat = form.watch('config.signature.isEnabled'); + + useEffect(() => { + if (selectedWebhookType != 'outgoing') { + form.setValue('config.signature.isEnabled', false); + } + }, [selectedWebhookType]); + + async function createProject( + values: z.infer, + ) { + let payload = { + name: values.name, + type: values.type as Exclude< + z.infer['type'], + '' + >, + config: {} as z.infer['config'], + }; + + // @ts-expect-error it works. source: trust the code + payload = Object.entries(values.config).reduce((acc, [key, val]) => { + if (!values.config) return acc; + + // @ts-expect-error it works. source: trust the code + if (!values.config[key] || values.config[key]['isEnabled'] === false) { + return acc; + } + + // @ts-expect-error it works. source: trust me + delete values.config[key]['isEnabled']; + return { + ...acc, + config: { + ...acc.config, + [key]: val, + }, + }; + }, payload); + + try { + const { api_key } = await projectsService.createProject({ + name: payload.name, + type: payload.type, + // @ts-expect-error it works. source: track/debug the code + config: { + ...payload.config, + ...(payload.config?.search_policy?.search_policy && { + search_policy: payload.config?.search_policy + ?.search_policy as `${string}h`, + }), + ...(payload.config?.signature && { + signature: { + header: payload.config.signature.header, + versions: [ + { + hash: payload.config.signature.hash, + encoding: payload.config.signature.encoding, + }, + ], + }, + }), + }, + }); + setProjectkey(api_key.key); + setHasCreatedProject(true); + form.reset(); + } catch (error) { + // TODO: notify UI of error + console.error(error); + } + } + + const webhookTypeOptions = [ + { + type: 'incoming', + desc: 'Create an incoming webhooks project to proxy events from third-party providers to your endpoints.', + }, + { + type: 'outgoing', + desc: 'Create an outgoing webhooks project to publish events to your customer-provided endpoints.', + }, + ]; + + return ( + +
    +
    + + go to projects page + +

    Create Project

    +
    + +

    + A project represents the top level namespace for grouping event + sources, applications, endpoints and events. +

    + +
    + + void form.handleSubmit(createProject)(...args) + } + > +
    + ( + +
    + + Project name + +
    + + + + +
    + )} + /> + + ( + +

    + Project type +

    +
    + {webhookTypeOptions.map(({ type, desc }) => { + return ( + + + + ); + })} +
    + +
    + )} + /> + +
    + + + + + + + +
    + + + {shouldShowRetryConfig ? ( + + + Retry Configuration + + +

    + Retry Logic (add tooltip here) +

    +
    + ( + +
    + + Mechanism + +
    + + +
    + )} + /> + + ( + +
    + + Duration + +
    + +
    + + + sec + +
    +
    + +
    + )} + /> + + ( + +
    + + Limit + +
    + + + + +
    + )} + /> +
    +
    +
    + ) : null} + + {shouldShowRateLimit ? ( + + + Rate Limit + + +

    + Rate Limit Parameters (add tooltip here) +

    +
    + ( + +
    + + Duration + +
    + +
    + + + sec + +
    +
    + +
    + )} + /> + + ( + +
    + + Limit + +
    + + + + +
    + )} + /> +
    +
    +
    + ) : null} + + {shouldShowSearchPolicy ? ( + + + Search Policy + + +

    + {/* TODO don't forget to add tooltip here */} + Search Period (add tooltip here) +

    +
    + ( + + +
    + + + hour(s) + +
    +
    + +
    + )} + /> +
    +
    +
    + ) : null} + + {selectedWebhookType == 'outgoing' && shouldShowSigFormat ? ( + + + Signature Format + + +

    + Signature Details (add tooltip here) +

    +
    + ( + +
    + + Header + +
    + + + + +
    + )} + /> + + ( + +
    + + Encoding + +
    + + +
    + )} + /> + + ( + +
    + + Hash + +
    + + +
    + )} + /> +
    +
    +
    + ) : null} +
    +
    + +
    + +
    +
    + +
    + + + + + + warning +

    + Project Created Successfully +

    +
    + +
    +

    Your API Key has also been created.

    +

    Please copy this key and save it somewhere safe.

    +
    + +
    + + {projectkey} + + +
    +
    +
    +
    + + + + + +
    +
    +
    + ); +} diff --git a/web/ui/dashboard-react/src/components/dashboard.tsx b/web/ui/dashboard-react/src/components/dashboard.tsx index 393ae16c8d..15b92d5a72 100644 --- a/web/ui/dashboard-react/src/components/dashboard.tsx +++ b/web/ui/dashboard-react/src/components/dashboard.tsx @@ -554,6 +554,7 @@ function FooterLinks() { } export function DashboardSidebar({ ...props }: ComponentProps) { + const navigate = useNavigate(); const { org } = useOrganisationStore(); const { licenses } = useLicenseStore(); @@ -574,6 +575,7 @@ export function DashboardSidebar({ ...props }: ComponentProps) {
    diff --git a/web/ui/dashboard-react/src/components/ui/accordion.tsx b/web/ui/dashboard-react/src/components/ui/accordion.tsx new file mode 100644 index 0000000000..e1797c9345 --- /dev/null +++ b/web/ui/dashboard-react/src/components/ui/accordion.tsx @@ -0,0 +1,55 @@ +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDown } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Accordion = AccordionPrimitive.Root + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = "AccordionItem" + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + +)) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
    {children}
    +
    +)) +AccordionContent.displayName = AccordionPrimitive.Content.displayName + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/web/ui/dashboard-react/src/components/ui/radio-group.tsx b/web/ui/dashboard-react/src/components/ui/radio-group.tsx new file mode 100644 index 0000000000..9d3a26e6ec --- /dev/null +++ b/web/ui/dashboard-react/src/components/ui/radio-group.tsx @@ -0,0 +1,42 @@ +import * as React from "react" +import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" +import { Circle } from "lucide-react" + +import { cn } from "@/lib/utils" + +const RadioGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + ) +}) +RadioGroup.displayName = RadioGroupPrimitive.Root.displayName + +const RadioGroupItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + + + + + ) +}) +RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName + +export { RadioGroup, RadioGroupItem } diff --git a/web/ui/dashboard-react/src/components/ui/select.tsx b/web/ui/dashboard-react/src/components/ui/select.tsx new file mode 100644 index 0000000000..218e7186e9 --- /dev/null +++ b/web/ui/dashboard-react/src/components/ui/select.tsx @@ -0,0 +1,157 @@ +import * as React from "react" +import * as SelectPrimitive from "@radix-ui/react-select" +import { Check, ChevronDown, ChevronUp } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Select = SelectPrimitive.Root + +const SelectGroup = SelectPrimitive.Group + +const SelectValue = SelectPrimitive.Value + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1", + className + )} + {...props} + > + {children} + + + + +)) +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + + {children} + + + + +)) +SelectContent.displayName = SelectPrimitive.Content.displayName + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectLabel.displayName = SelectPrimitive.Label.displayName + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +SelectItem.displayName = SelectPrimitive.Item.displayName + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectSeparator.displayName = SelectPrimitive.Separator.displayName + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +} diff --git a/web/ui/dashboard-react/src/index.css b/web/ui/dashboard-react/src/index.css index d5d13fa749..a0fa74a6d3 100644 --- a/web/ui/dashboard-react/src/index.css +++ b/web/ui/dashboard-react/src/index.css @@ -186,13 +186,11 @@ } } - - @layer base { - * { - @apply border-border outline-ring/50; + * { + @apply border-border outline-ring/50; } - body { - @apply bg-background text-foreground; + body { + @apply bg-background text-foreground; } } diff --git a/web/ui/dashboard-react/src/models/organisation.model.ts b/web/ui/dashboard-react/src/models/organisation.model.ts index c4ab8ba882..decb690e5c 100644 --- a/web/ui/dashboard-react/src/models/organisation.model.ts +++ b/web/ui/dashboard-react/src/models/organisation.model.ts @@ -22,7 +22,6 @@ export interface Organisation { name: string; custom_domain: null | string; assigned_domain: null | String; - members: Member[] | null; /** * Date string */ diff --git a/web/ui/dashboard-react/src/models/project.model.ts b/web/ui/dashboard-react/src/models/project.model.ts index 66cac7b1be..5fe5587f2b 100644 --- a/web/ui/dashboard-react/src/models/project.model.ts +++ b/web/ui/dashboard-react/src/models/project.model.ts @@ -49,13 +49,6 @@ export interface Project { } | null; } -export interface Version { - created_at: Date; - encoding: string; - hash: string; - uid: string; -} - export interface MetaEvent { attempt: { request_http_header: object; @@ -78,3 +71,79 @@ export interface MetaEvent { uid: string; updated_at: string; } + +export interface CreateProjectResponse { + api_key: ApiKey; + project: { + uid: string; + name: string; + logo_url: string; + organisation_id: string; + type: string; + config: Config; + statistics: any; + retained_events: number; + created_at: string; + updated_at: string; + deleted_at: null | string; + }; +} + +interface ApiKey { + name: string; + role: Role; + key_type: string; + expires_at: null | string; + /** + * Shown only once on the UI + */ + key: string; + uid: string; + created_at: string; +} + +interface Role { + type: 'admin' | 'super_admin' | 'member'; + project: string; +} + +interface Config { + max_payload_read_size: number; + replay_attacks_prevention_enabled: boolean; + add_event_id_trace_headers: boolean; + disable_endpoint: boolean; + multiple_endpoint_subscriptions: boolean; + search_policy: string; + ssl: Ssl; + ratelimit: Ratelimit; + strategy: Strategy; + signature: Signature; + meta_event: any; +} + +interface Ssl { + enforce_secure_endpoints: boolean; +} + +interface Ratelimit { + count: number; + duration: number; +} + +interface Strategy { + type: string; + duration: number; + retry_count: number; +} + +interface Signature { + header: string; + versions: Version[]; +} + +interface Version { + uid: string; + hash: string; + encoding: string; + created_at: string; +} diff --git a/web/ui/dashboard-react/src/routes.gen.ts b/web/ui/dashboard-react/src/routes.gen.ts index 13ba2e163c..381cb58960 100644 --- a/web/ui/dashboard-react/src/routes.gen.ts +++ b/web/ui/dashboard-react/src/routes.gen.ts @@ -20,6 +20,7 @@ import { Route as ForgotPasswordImport } from './app/forgot-password' import { Route as ProjectsRouteImport } from './app/projects/route' import { Route as IndexImport } from './app/index' import { Route as ProjectsIndexImport } from './app/projects/index' +import { Route as ProjectsNewImport } from './app/projects_/new' // Create/Update Routes @@ -77,6 +78,12 @@ const ProjectsIndexRoute = ProjectsIndexImport.update({ getParentRoute: () => ProjectsRouteRoute, } as any) +const ProjectsNewRoute = ProjectsNewImport.update({ + id: '/projects_/new', + path: '/projects/new', + getParentRoute: () => rootRoute, +} as any) + // Populate the FileRoutesByPath interface declare module '@tanstack/react-router' { @@ -137,6 +144,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof UserSettingsImport parentRoute: typeof rootRoute } + '/projects_/new': { + id: '/projects_/new' + path: '/projects/new' + fullPath: '/projects/new' + preLoaderRoute: typeof ProjectsNewImport + parentRoute: typeof rootRoute + } '/projects/': { id: '/projects/' path: '/' @@ -170,6 +184,7 @@ export interface FileRoutesByFullPath { '/settings': typeof SettingsRoute '/signup': typeof SignupRoute '/user-settings': typeof UserSettingsRoute + '/projects/new': typeof ProjectsNewRoute '/projects/': typeof ProjectsIndexRoute } @@ -181,6 +196,7 @@ export interface FileRoutesByTo { '/settings': typeof SettingsRoute '/signup': typeof SignupRoute '/user-settings': typeof UserSettingsRoute + '/projects/new': typeof ProjectsNewRoute '/projects': typeof ProjectsIndexRoute } @@ -194,6 +210,7 @@ export interface FileRoutesById { '/settings': typeof SettingsRoute '/signup': typeof SignupRoute '/user-settings': typeof UserSettingsRoute + '/projects_/new': typeof ProjectsNewRoute '/projects/': typeof ProjectsIndexRoute } @@ -208,6 +225,7 @@ export interface FileRouteTypes { | '/settings' | '/signup' | '/user-settings' + | '/projects/new' | '/projects/' fileRoutesByTo: FileRoutesByTo to: @@ -218,6 +236,7 @@ export interface FileRouteTypes { | '/settings' | '/signup' | '/user-settings' + | '/projects/new' | '/projects' id: | '__root__' @@ -229,6 +248,7 @@ export interface FileRouteTypes { | '/settings' | '/signup' | '/user-settings' + | '/projects_/new' | '/projects/' fileRoutesById: FileRoutesById } @@ -242,6 +262,7 @@ export interface RootRouteChildren { SettingsRoute: typeof SettingsRoute SignupRoute: typeof SignupRoute UserSettingsRoute: typeof UserSettingsRoute + ProjectsNewRoute: typeof ProjectsNewRoute } const rootRouteChildren: RootRouteChildren = { @@ -253,6 +274,7 @@ const rootRouteChildren: RootRouteChildren = { SettingsRoute: SettingsRoute, SignupRoute: SignupRoute, UserSettingsRoute: UserSettingsRoute, + ProjectsNewRoute: ProjectsNewRoute, } export const routeTree = rootRoute @@ -272,7 +294,8 @@ export const routeTree = rootRoute "/login", "/settings", "/signup", - "/user-settings" + "/user-settings", + "/projects_/new" ] }, "/": { @@ -302,6 +325,9 @@ export const routeTree = rootRoute "/user-settings": { "filePath": "user-settings.tsx" }, + "/projects_/new": { + "filePath": "projects_/new.tsx" + }, "/projects/": { "filePath": "projects/index.tsx", "parent": "/projects" diff --git a/web/ui/dashboard-react/src/services/auth.service.ts b/web/ui/dashboard-react/src/services/auth.service.ts index 8bf2a446f7..3d0985c6e9 100644 --- a/web/ui/dashboard-react/src/services/auth.service.ts +++ b/web/ui/dashboard-react/src/services/auth.service.ts @@ -215,16 +215,19 @@ export async function getUserRole( } } -async function getUserPermissions() { +export async function getUserPermissions(): Promise> { const role = await getUserRole({ userID: getCachedAuthProfile()?.uid }); switch (role) { case 'SUPER_ADMIN': - return permissions[role].concat(permissions.ADMIN, permissions.MEMBER); + return permissions[role].concat( + permissions.ADMIN, + permissions.MEMBER, + ) as Permission[]; case 'ADMIN': - return permissions[role].concat(permissions.MEMBER); + return permissions[role].concat(permissions.MEMBER) as Permission[]; default: - return permissions.MEMBER; + return permissions.MEMBER as Permission[]; } } diff --git a/web/ui/dashboard-react/src/services/projects.service.ts b/web/ui/dashboard-react/src/services/projects.service.ts index 0bf91a775b..4ad1d36679 100644 --- a/web/ui/dashboard-react/src/services/projects.service.ts +++ b/web/ui/dashboard-react/src/services/projects.service.ts @@ -1,12 +1,50 @@ import { request } from '@/services/http.service'; import { CONVOY_CURRENT_PROJECT } from '@/lib/constants'; -import type { Project } from '@/models/project.model'; +import type { Project, CreateProjectResponse } from '@/models/project.model'; // TODO use state management let projects: Array = []; let projectDetails: Project | null = null; +type CreateProjectParams = { + name: string; + type: 'incoming' | 'outgoing'; + config: { + strategy?: { + duration: number; + retry_count: number; + type: 'linear' | 'exponential'; + }; + signature?: { + header: string; + versions: Array<{ + hash: 'SHA256' | 'SHA512'; + encoding: 'base64' | 'hex'; + }>; + }; + ratelimit?: { + count: number; + duration: number; + }; + search_policy?: `${string}h`; + }; +}; + +export async function createProject( + reqDetails: CreateProjectParams, + deps: { httpReq: typeof request } = { httpReq: request }, +) { + const response = await deps.httpReq({ + url: `/projects`, + body: reqDetails, + method: 'post', + level: 'org', + }); + + return response.data; +} + export async function getProjects( reqDetails: { refresh?: boolean }, deps: { httpReq: typeof request } = { httpReq: request }, diff --git a/web/ui/dashboard-react/tailwind.config.js b/web/ui/dashboard-react/tailwind.config.js index aea19ea640..ce85ec2566 100644 --- a/web/ui/dashboard-react/tailwind.config.js +++ b/web/ui/dashboard-react/tailwind.config.js @@ -51,6 +51,28 @@ module.exports = { ring: 'hsl(var(--sidebar-ring))', }, }, + keyframes: { + 'accordion-down': { + from: { + height: '0', + }, + to: { + height: 'var(--radix-accordion-content-height)', + }, + }, + 'accordion-up': { + from: { + height: 'var(--radix-accordion-content-height)', + }, + to: { + height: '0', + }, + }, + }, + animation: { + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out', + }, }, fontWeight: { thin: '100', From 6fa5de032c6692e82166d29c100496888da30951 Mon Sep 17 00:00:00 2001 From: Orim Dominic Date: Wed, 12 Mar 2025 13:17:56 +0100 Subject: [PATCH 16/43] fix: inconsistent state management of orgs --- web/ui/dashboard-react/src/app/projects/index.tsx | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/web/ui/dashboard-react/src/app/projects/index.tsx b/web/ui/dashboard-react/src/app/projects/index.tsx index 9c182fe5a7..8a8999d7ae 100644 --- a/web/ui/dashboard-react/src/app/projects/index.tsx +++ b/web/ui/dashboard-react/src/app/projects/index.tsx @@ -21,17 +21,12 @@ export const Route = createFileRoute('/projects/')({ ensureCanAccessPrivatePages(context.auth?.getTokens().isLoggedIn); }, async loader() { - // TODO fix the bug where projects don't show up quickly - const { org, paginatedOrgs } = useOrganisationStore.getState(); + const pgOrgs = await orgsService.getOrganisations(); - if (!org || !paginatedOrgs.content.length) { - const pgOrgs = await orgsService.getOrganisations(); - - useOrganisationStore.setState({ - paginatedOrgs: pgOrgs, - org: pgOrgs.content.at(0) || null, - }); - } + useOrganisationStore.setState({ + paginatedOrgs: pgOrgs, + org: pgOrgs.content.at(0) || null, + }); }, component: ProjectIndexPage, }); From 1f95acef88b6f46998aa88bdc93acd2c186614f2 Mon Sep 17 00:00:00 2001 From: Orim Dominic Adah Date: Wed, 12 Mar 2025 18:07:41 +0100 Subject: [PATCH 17/43] fix: inconsistent organisation and project states (#2261) * fix: HTML error in dialog component * fix: inconsistent state management of projects and orgs --- .../src/app/projects/index.tsx | 40 ++++++------- .../src/app/projects/route.tsx | 1 - .../dashboard-react/src/app/projects_/new.tsx | 24 ++++---- web/ui/dashboard-react/src/app/settings.tsx | 3 +- .../src/components/dashboard.tsx | 58 +++++++++---------- .../src/services/http.service.ts | 16 ++--- .../src/services/projects.service.ts | 29 +--------- web/ui/dashboard-react/src/store/index.ts | 31 +++++++++- 8 files changed, 102 insertions(+), 100 deletions(-) diff --git a/web/ui/dashboard-react/src/app/projects/index.tsx b/web/ui/dashboard-react/src/app/projects/index.tsx index 8a8999d7ae..2b1d437544 100644 --- a/web/ui/dashboard-react/src/app/projects/index.tsx +++ b/web/ui/dashboard-react/src/app/projects/index.tsx @@ -1,55 +1,53 @@ -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { createFileRoute, useNavigate } from '@tanstack/react-router'; import { Button } from '@/components/ui/button'; import { ConvoyLoader } from '@/components/convoy-loader'; import { CreateOrganisation } from '@/components/create-organisation'; +import { + useLicenseStore, + useOrganisationStore, + useProjectStore, +} from '@/store'; import * as authService from '@/services/auth.service'; import { ensureCanAccessPrivatePages } from '@/lib/auth'; import * as projectsService from '@/services/projects.service'; -import { useLicenseStore, useOrganisationStore } from '@/store'; import * as orgsService from '@/services/organisations.service'; import plusCircularIcon from '../../../assets/svg/add-circlar-icon.svg'; import projectsEmptyImg from '../../../assets/svg/events-empty-state-image.svg'; -import type { Project } from '@/models/project.model'; - export const Route = createFileRoute('/projects/')({ beforeLoad({ context }) { ensureCanAccessPrivatePages(context.auth?.getTokens().isLoggedIn); }, async loader() { const pgOrgs = await orgsService.getOrganisations(); - useOrganisationStore.setState({ paginatedOrgs: pgOrgs, - org: pgOrgs.content.at(0) || null, }); + + const projects = await projectsService.getProjects(); + useProjectStore.setState({ projects }); + + const userPerms = await authService.getUserPermissions(); + + return { + canCreateProject: userPerms.includes('Project Settings|MANAGE'), + canCreateOrg: useLicenseStore.getState().licenses.includes('CREATE_ORG'), + }; }, component: ProjectIndexPage, }); function ProjectIndexPage() { const navigate = useNavigate(); - const { licenses } = useLicenseStore(); - const [canCreateProject, setCanCreateProject] = useState(false); + const { project } = useProjectStore(); const [isDialogOpen, setIsDialogOpen] = useState(false); + const { canCreateOrg, canCreateProject } = Route.useLoaderData(); const { org, setOrg, setPaginatedOrgs } = useOrganisationStore(); - const [canCreateOrg] = useState(licenses.includes('CREATE_ORG')); const [isLoadingOrganisations, setIsLoadingOrganisations] = useState(false); - // TODO use a state management lib for projects like zustand - const [currentProject] = useState( - projectsService.getCachedProject(), - ); - - useEffect(function () { - (async function () { - const userPerms = await authService.getUserPermissions(); - setCanCreateProject(userPerms.includes('Project Settings|MANAGE')); - })(); - }, []); async function reloadOrganisations() { setIsLoadingOrganisations(true); @@ -95,7 +93,7 @@ function ProjectIndexPage() { /> ); - if (!currentProject) { + if (!project) { return (
    - + warning -

    + Project Created Successfully -

    +
    - +
    -

    Your API Key has also been created.

    -

    Please copy this key and save it somewhere safe.

    + Your API Key has also been created. + Please copy this key and save it somewhere safe.
    -
    +
    {projectkey} @@ -1035,19 +1038,18 @@ function CreateNewProject() { type="button" variant="ghost" size="sm" - className="asbolute right-[1%] top-0 h-full py-2 hover:bg-transparent" + className="asbolute right-[1%] top-0 h-full py-2 hover:bg-transparent pr-1 pl-0" onClick={() => { window.navigator.clipboard.writeText(projectkey).then(); // TODO show toast message on copy successful }} >
    - +
    -
    +
    + + +
    + + ); +} + +export const Route = createFileRoute('/projects_/$projectId/settings')({ + beforeLoad({ context }) { + ensureCanAccessPrivatePages(context.auth?.getTokens().isLoggedIn); + }, + async loader({ params }) { + const project = await projectsService.getProject(params.projectId); + // TODO handle error, and in other places too + return { project }; + }, + component: ProjectSettings, +}); + +const tabs = [ + { + name: 'Projects', + value: 'projects', + icon: Home, + component: ProjectConfig, + }, + { + name: 'Endpoints', + value: 'endpoints', + icon: User, + component: ProjectConfig, + }, + { + name: 'Meta Events', + value: 'meta-events', + icon: Bot, + component: ProjectConfig, + }, + { + name: 'Secrets', + value: 'secrets', + icon: Settings, + component: ProjectConfig, + }, +]; + +function ProjectSettings() { + const { project } = Route.useLoaderData(); + + return ( + +
    +
    +

    Project Settings

    +
    + + + {tabs.map(tab => ( + + {tab.name} + + ))} + + +
    + {tabs.map(tab => ( + + + + ))} +
    +
    +
    +
    +
    +
    + ); +} diff --git a/web/ui/dashboard-react/src/app/projects_/new.tsx b/web/ui/dashboard-react/src/app/projects_/new.tsx index 72d7a908b5..e56711a986 100644 --- a/web/ui/dashboard-react/src/app/projects_/new.tsx +++ b/web/ui/dashboard-react/src/app/projects_/new.tsx @@ -1,6 +1,6 @@ import { z } from 'zod'; -import { useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; +import { useEffect, useState } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { createFileRoute, Link } from '@tanstack/react-router'; @@ -11,7 +11,6 @@ import { DialogTrigger, DialogContent, DialogTitle, - DialogDescription, DialogClose, DialogHeader, DialogFooter, @@ -42,6 +41,7 @@ import { import { DashboardLayout } from '@/components/dashboard'; import { cn } from '@/lib/utils'; +import * as authService from '@/services/auth.service'; import { ensureCanAccessPrivatePages } from '@/lib/auth'; import * as projectsService from '@/services/projects.service'; @@ -49,10 +49,17 @@ import modalCloseIcon from '../../../assets/svg/modal-close-icon.svg'; import successAnimation from '../../../assets/img/success.gif'; export const Route = createFileRoute('/projects_/new')({ - component: CreateNewProject, beforeLoad({ context }) { ensureCanAccessPrivatePages(context.auth?.getTokens().isLoggedIn); }, + async loader() { + const userPerms = await authService.getUserPermissions(); + + return { + canCreateProject: userPerms.includes('Project Settings|MANAGE'), + }; + }, + component: CreateNewProject, }); const CreateProjectFormSchema = z.object({ @@ -222,7 +229,9 @@ const CreateProjectFormSchema = z.object({ function CreateNewProject() { const [hasCreatedProject, setHasCreatedProject] = useState(false); + const [isCreatingProject, setIsCreatingProject] = useState(false); const [projectkey, setProjectkey] = useState(''); + const { canCreateProject } = Route.useLoaderData(); const form = useForm>({ resolver: zodResolver(CreateProjectFormSchema), defaultValues: { @@ -274,6 +283,7 @@ function CreateNewProject() { async function createProject( values: z.infer, ) { + setIsCreatingProject(true); let payload = { name: values.name, type: values.type as Exclude< @@ -333,6 +343,8 @@ function CreateNewProject() { } catch (error) { // TODO: notify UI of error console.error(error); + } finally { + setIsCreatingProject(false); } } @@ -470,7 +482,7 @@ function CreateNewProject() { />
    + {/* TODO add tooltip here for when button is disabled */} } isDialogOpen={isDialogOpen} @@ -392,7 +393,7 @@ function ProjectsList() { { form.setValue( @@ -462,6 +463,7 @@ function ProjectsList() { function ProjectLinks() { const { project } = useProjectStore(); + const links = [ { name: 'Event Deliveries', @@ -489,7 +491,7 @@ function ProjectLinks() { }, { name: 'Project Settings', - route: '/', + route: `/projects/${project?.uid}/settings`, }, ]; @@ -500,11 +502,14 @@ function ProjectLinks() {
      {links.map(link => { return ( -
    • +
    • {/* TODO change to link route */} {link.name} @@ -593,7 +598,7 @@ export function DashboardSidebar({ ...props }: ComponentProps) {

      {!org ? 'An organisation is required to create projects on Convoy.' - : 'You are not licensed to create a project'} + : 'Available on Business'}

      ) : null} @@ -628,7 +633,7 @@ export function DashboardLayout(props: {
      -
      +
      {props.showSidebar ? : null} {props.children}
      diff --git a/web/ui/dashboard-react/src/index.css b/web/ui/dashboard-react/src/index.css index a0fa74a6d3..d556b2ba25 100644 --- a/web/ui/dashboard-react/src/index.css +++ b/web/ui/dashboard-react/src/index.css @@ -126,22 +126,6 @@ @apply bg-background text-foreground; } - h1 { - @apply text-h1; - } - - h2 { - @apply text-h2; - } - - h3 { - @apply text-h3; - } - - h4 { - @apply text-h4; - } - dialog:modal { @apply max-w-full max-h-full; } diff --git a/web/ui/dashboard-react/src/lib/constants.ts b/web/ui/dashboard-react/src/lib/constants.ts index af5f985a8f..6ef9f99c16 100644 --- a/web/ui/dashboard-react/src/lib/constants.ts +++ b/web/ui/dashboard-react/src/lib/constants.ts @@ -2,6 +2,3 @@ export const CONVOY_AUTH_KEY = 'CONVOY_AUTH'; export const CONVOY_AUTH_TOKENS_KEY = 'CONVOY_AUTH_TOKENS'; export const CONVOY_LAST_AUTH_LOCATION_KEY = 'CONVOY_LAST_AUTH_LOCATION'; export const CONVOY_DASHBOARD_DOMAIN = 'dashboard.convoy.io'; -export const CONVOY_LICENSES_KEY = 'CONVOY_LICENSES'; -export const CONVOY_ORG_KEY = 'CONVOY_ORG'; -export const CONVOY_CURRENT_PROJECT="CONVOY_CURRENT_PROJECT" diff --git a/web/ui/dashboard-react/src/models/project.model.ts b/web/ui/dashboard-react/src/models/project.model.ts index 5fe5587f2b..93ea0e431e 100644 --- a/web/ui/dashboard-react/src/models/project.model.ts +++ b/web/ui/dashboard-react/src/models/project.model.ts @@ -11,7 +11,6 @@ export interface Project { config: { disable_endpoint: boolean; retention_policy_enabled: boolean; - DisableEndpoint: boolean; replay_attacks: boolean; search_policy: string; ratelimit: { @@ -19,7 +18,7 @@ export interface Project { duration: number; }; strategy: { - type: string; + type: 'linear' | 'exponential'; retry_count: number; duration: number; }; @@ -27,9 +26,7 @@ export interface Project { header: string; versions: Version[]; }; - ssl: { - enforce_secure_endpoints: boolean; - }; + ssl: Ssl; meta_event: { event_type: string[] | null; is_enabled: boolean; @@ -37,6 +34,7 @@ export interface Project { type: string; url: string; }; + multiple_endpoint_subscriptions: boolean; // retention_policy: { // policy: string; // search_policy: string; @@ -118,7 +116,7 @@ interface Config { ratelimit: Ratelimit; strategy: Strategy; signature: Signature; - meta_event: any; + meta_event: MetaEvent; } interface Ssl { diff --git a/web/ui/dashboard-react/src/routes.gen.ts b/web/ui/dashboard-react/src/routes.gen.ts index 381cb58960..4e54c2977c 100644 --- a/web/ui/dashboard-react/src/routes.gen.ts +++ b/web/ui/dashboard-react/src/routes.gen.ts @@ -21,6 +21,7 @@ import { Route as ProjectsRouteImport } from './app/projects/route' import { Route as IndexImport } from './app/index' import { Route as ProjectsIndexImport } from './app/projects/index' import { Route as ProjectsNewImport } from './app/projects_/new' +import { Route as ProjectsProjectIdSettingsImport } from './app/projects_/$projectId/settings' // Create/Update Routes @@ -84,6 +85,12 @@ const ProjectsNewRoute = ProjectsNewImport.update({ getParentRoute: () => rootRoute, } as any) +const ProjectsProjectIdSettingsRoute = ProjectsProjectIdSettingsImport.update({ + id: '/projects_/$projectId/settings', + path: '/projects/$projectId/settings', + getParentRoute: () => rootRoute, +} as any) + // Populate the FileRoutesByPath interface declare module '@tanstack/react-router' { @@ -158,6 +165,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ProjectsIndexImport parentRoute: typeof ProjectsRouteImport } + '/projects_/$projectId/settings': { + id: '/projects_/$projectId/settings' + path: '/projects/$projectId/settings' + fullPath: '/projects/$projectId/settings' + preLoaderRoute: typeof ProjectsProjectIdSettingsImport + parentRoute: typeof rootRoute + } } } @@ -186,6 +200,7 @@ export interface FileRoutesByFullPath { '/user-settings': typeof UserSettingsRoute '/projects/new': typeof ProjectsNewRoute '/projects/': typeof ProjectsIndexRoute + '/projects/$projectId/settings': typeof ProjectsProjectIdSettingsRoute } export interface FileRoutesByTo { @@ -198,6 +213,7 @@ export interface FileRoutesByTo { '/user-settings': typeof UserSettingsRoute '/projects/new': typeof ProjectsNewRoute '/projects': typeof ProjectsIndexRoute + '/projects/$projectId/settings': typeof ProjectsProjectIdSettingsRoute } export interface FileRoutesById { @@ -212,6 +228,7 @@ export interface FileRoutesById { '/user-settings': typeof UserSettingsRoute '/projects_/new': typeof ProjectsNewRoute '/projects/': typeof ProjectsIndexRoute + '/projects_/$projectId/settings': typeof ProjectsProjectIdSettingsRoute } export interface FileRouteTypes { @@ -227,6 +244,7 @@ export interface FileRouteTypes { | '/user-settings' | '/projects/new' | '/projects/' + | '/projects/$projectId/settings' fileRoutesByTo: FileRoutesByTo to: | '/' @@ -238,6 +256,7 @@ export interface FileRouteTypes { | '/user-settings' | '/projects/new' | '/projects' + | '/projects/$projectId/settings' id: | '__root__' | '/' @@ -250,6 +269,7 @@ export interface FileRouteTypes { | '/user-settings' | '/projects_/new' | '/projects/' + | '/projects_/$projectId/settings' fileRoutesById: FileRoutesById } @@ -263,6 +283,7 @@ export interface RootRouteChildren { SignupRoute: typeof SignupRoute UserSettingsRoute: typeof UserSettingsRoute ProjectsNewRoute: typeof ProjectsNewRoute + ProjectsProjectIdSettingsRoute: typeof ProjectsProjectIdSettingsRoute } const rootRouteChildren: RootRouteChildren = { @@ -275,6 +296,7 @@ const rootRouteChildren: RootRouteChildren = { SignupRoute: SignupRoute, UserSettingsRoute: UserSettingsRoute, ProjectsNewRoute: ProjectsNewRoute, + ProjectsProjectIdSettingsRoute: ProjectsProjectIdSettingsRoute, } export const routeTree = rootRoute @@ -295,7 +317,8 @@ export const routeTree = rootRoute "/settings", "/signup", "/user-settings", - "/projects_/new" + "/projects_/new", + "/projects_/$projectId/settings" ] }, "/": { @@ -331,6 +354,9 @@ export const routeTree = rootRoute "/projects/": { "filePath": "projects/index.tsx", "parent": "/projects" + }, + "/projects_/$projectId/settings": { + "filePath": "projects_/$projectId/settings.tsx" } } } diff --git a/web/ui/dashboard-react/src/services/projects.service.ts b/web/ui/dashboard-react/src/services/projects.service.ts index e15413a69e..580e55210a 100644 --- a/web/ui/dashboard-react/src/services/projects.service.ts +++ b/web/ui/dashboard-react/src/services/projects.service.ts @@ -64,3 +64,53 @@ export async function getProject( return res.data; } + +type UpdateProjectParams = { + name: string; + type: 'incoming' | 'outgoing'; + config: { + strategy?: { + duration: number; + retry_count: number; + type: 'linear' | 'exponential'; + }; + signature?: { + header: string; + versions: Array<{ + hash: 'SHA256' | 'SHA512'; + encoding: 'base64' | 'hex'; + }>; + }; + ratelimit?: { + count: number; + duration: number; + }; + search_policy?: `${string}h`; + disable_endpoint: boolean; + multiple_endpoint_subscriptions: boolean; + ssl?: { + enforce_secure_endpoints: boolean; + }; + meta_event?: { + event_type: string[] | null; + is_enabled: boolean; + secret: string; + type: string; + url: string; + }; + }; +}; + +export async function updateProject( + update: UpdateProjectParams, + deps: { httpReq: typeof request } = { httpReq: request }, +) { + const res = await deps.httpReq({ + method: 'put', + url: '', + body: update, + level: 'org_project', + }); + + return res.data; +} diff --git a/web/ui/dashboard-react/src/store/index.ts b/web/ui/dashboard-react/src/store/index.ts index da4af3283e..69de06d431 100644 --- a/web/ui/dashboard-react/src/store/index.ts +++ b/web/ui/dashboard-react/src/store/index.ts @@ -1,36 +1,19 @@ import { create } from 'zustand'; -import { persist, createJSONStorage } from 'zustand/middleware'; - -import { - CONVOY_LICENSES_KEY, - CONVOY_ORG_KEY, - CONVOY_CURRENT_PROJECT, -} from '@/lib/constants'; import type { Project } from '@/models/project.model'; import type { PaginatedResult } from '@/models/global.model'; import type { LicenseKey } from '@/services/licenses.service'; import type { Organisation } from '@/models/organisation.model'; -// window.sessionStorage is used so that if the page is refreshed, app will use new data from server - type LicenseStore = { licenses: Array; setLicenses: (keys: Array) => void; }; -export const useLicenseStore = create()( - persist( - set => ({ - licenses: [], - setLicenses: keys => set({ licenses: keys }), - }), - { - name: CONVOY_LICENSES_KEY, - storage: createJSONStorage(() => sessionStorage), - }, - ), -); +export const useLicenseStore = create()(set => ({ + licenses: [], + setLicenses: keys => set({ licenses: keys }), +})); type OrganisationStore = { /** The current organisation in use */ @@ -41,29 +24,21 @@ type OrganisationStore = { setPaginatedOrgs: (pgOrgs: PaginatedResult) => void; }; -export const useOrganisationStore = create()( - persist( - set => ({ - org: null, - setOrg: org => set({ org }), - paginatedOrgs: { - content: [], - pagination: { - per_page: 0, - has_next_page: false, - has_prev_page: false, - prev_page_cursor: '', - next_page_cursor: '', - }, - }, - setPaginatedOrgs: pgOrgs => set({ paginatedOrgs: pgOrgs }), - }), - { - name: CONVOY_ORG_KEY, - storage: createJSONStorage(() => sessionStorage), +export const useOrganisationStore = create()(set => ({ + org: null, + setOrg: org => set({ org }), + paginatedOrgs: { + content: [], + pagination: { + per_page: 0, + has_next_page: false, + has_prev_page: false, + prev_page_cursor: '', + next_page_cursor: '', }, - ), -); + }, + setPaginatedOrgs: pgOrgs => set({ paginatedOrgs: pgOrgs }), +})); type ProjectStore = { /** The current project in use */ @@ -74,17 +49,9 @@ type ProjectStore = { setProjects: (projects: Array) => void; }; -export const useProjectStore = create()( - persist( - set => ({ - project: null, - projects: [], - setProject: project => set({ project }), - setProjects: projects => set({ projects }), - }), - { - name: CONVOY_CURRENT_PROJECT, - storage: createJSONStorage(() => sessionStorage), - }, - ), -); +export const useProjectStore = create()(set => ({ + project: null, + projects: [], + setProject: project => set({ project }), + setProjects: projects => set({ projects }), +})); From 2184a595f8b12c4af77a03f15eec38d94d8652b1 Mon Sep 17 00:00:00 2001 From: Orim Dominic Date: Fri, 14 Mar 2025 11:18:14 +0100 Subject: [PATCH 21/43] fix: use session storage --- web/ui/dashboard-react/src/lib/constants.ts | 1 + web/ui/dashboard-react/src/store/index.ts | 75 ++++++++++++++------- 2 files changed, 52 insertions(+), 24 deletions(-) diff --git a/web/ui/dashboard-react/src/lib/constants.ts b/web/ui/dashboard-react/src/lib/constants.ts index 6ef9f99c16..9b6accbc3c 100644 --- a/web/ui/dashboard-react/src/lib/constants.ts +++ b/web/ui/dashboard-react/src/lib/constants.ts @@ -2,3 +2,4 @@ export const CONVOY_AUTH_KEY = 'CONVOY_AUTH'; export const CONVOY_AUTH_TOKENS_KEY = 'CONVOY_AUTH_TOKENS'; export const CONVOY_LAST_AUTH_LOCATION_KEY = 'CONVOY_LAST_AUTH_LOCATION'; export const CONVOY_DASHBOARD_DOMAIN = 'dashboard.convoy.io'; +export const CONVOY_LICENSES_KEY = 'CONVOY_LICENSES'; diff --git a/web/ui/dashboard-react/src/store/index.ts b/web/ui/dashboard-react/src/store/index.ts index 69de06d431..02ba9ed45b 100644 --- a/web/ui/dashboard-react/src/store/index.ts +++ b/web/ui/dashboard-react/src/store/index.ts @@ -1,4 +1,7 @@ import { create } from 'zustand'; +import { persist, createJSONStorage } from 'zustand/middleware'; + +import { CONVOY_LICENSES_KEY } from '@/lib/constants'; import type { Project } from '@/models/project.model'; import type { PaginatedResult } from '@/models/global.model'; @@ -10,10 +13,18 @@ type LicenseStore = { setLicenses: (keys: Array) => void; }; -export const useLicenseStore = create()(set => ({ - licenses: [], - setLicenses: keys => set({ licenses: keys }), -})); +export const useLicenseStore = create()( + persist( + set => ({ + licenses: [], + setLicenses: keys => set({ licenses: keys }), + }), + { + name: CONVOY_LICENSES_KEY, + storage: createJSONStorage(() => sessionStorage), + }, + ), +); type OrganisationStore = { /** The current organisation in use */ @@ -24,21 +35,29 @@ type OrganisationStore = { setPaginatedOrgs: (pgOrgs: PaginatedResult) => void; }; -export const useOrganisationStore = create()(set => ({ - org: null, - setOrg: org => set({ org }), - paginatedOrgs: { - content: [], - pagination: { - per_page: 0, - has_next_page: false, - has_prev_page: false, - prev_page_cursor: '', - next_page_cursor: '', +export const useOrganisationStore = create()( + persist( + set => ({ + org: null, + setOrg: org => set({ org }), + paginatedOrgs: { + content: [], + pagination: { + per_page: 0, + has_next_page: false, + has_prev_page: false, + prev_page_cursor: '', + next_page_cursor: '', + }, + }, + setPaginatedOrgs: pgOrgs => set({ paginatedOrgs: pgOrgs }), + }), + { + name: 'CONVOY_ORG', + storage: createJSONStorage(() => sessionStorage), }, - }, - setPaginatedOrgs: pgOrgs => set({ paginatedOrgs: pgOrgs }), -})); + ), +); type ProjectStore = { /** The current project in use */ @@ -49,9 +68,17 @@ type ProjectStore = { setProjects: (projects: Array) => void; }; -export const useProjectStore = create()(set => ({ - project: null, - projects: [], - setProject: project => set({ project }), - setProjects: projects => set({ projects }), -})); +export const useProjectStore = create()( + persist( + set => ({ + project: null, + projects: [], + setProject: project => set({ project }), + setProjects: projects => set({ projects }), + }), + { + name: 'CONVOY_PROJECT', + storage: createJSONStorage(() => sessionStorage), + }, + ), +); From d65df0d08434e796c8c910ab17cace5a94c4418c Mon Sep 17 00:00:00 2001 From: Orim Dominic Adah Date: Fri, 14 Mar 2025 14:24:10 +0100 Subject: [PATCH 22/43] feat: delete project (#2263) * fix: linting errors * feat: delete project --- web/ui/dashboard-react/eslint.config.js | 2 +- web/ui/dashboard-react/src/app/index.tsx | 4 +- web/ui/dashboard-react/src/app/login.tsx | 1 + .../src/app/projects/index.tsx | 35 ++- .../src/app/projects_/$projectId/settings.tsx | 128 +++++++++- web/ui/dashboard-react/src/app/settings.tsx | 8 +- web/ui/dashboard-react/src/app/signup.tsx | 1 + .../src/components/create-organisation.tsx | 2 +- .../src/components/ui/badge.tsx | 54 ++-- .../src/components/ui/button.tsx | 2 +- .../src/components/ui/command.tsx | 240 +++++++++--------- web/ui/dashboard-react/src/lib/pipes.ts | 3 +- web/ui/dashboard-react/src/main.tsx | 2 +- .../src/models/global.model.ts | 8 +- .../src/models/organisation.model.ts | 2 +- .../src/models/project.model.ts | 14 +- .../src/services/http.service.ts | 38 ++- .../src/services/licenses.service.ts | 57 ++--- .../src/services/projects.service.ts | 13 + 19 files changed, 380 insertions(+), 234 deletions(-) diff --git a/web/ui/dashboard-react/eslint.config.js b/web/ui/dashboard-react/eslint.config.js index c83bcb9b35..2b7ccc59b9 100644 --- a/web/ui/dashboard-react/eslint.config.js +++ b/web/ui/dashboard-react/eslint.config.js @@ -16,7 +16,7 @@ export default tseslint.config( tseslint.configs.recommended, // ...tseslint.configs.strictTypeChecked ], - parser: '@typescript-eslint/parser', + // parser: '@typescript-eslint/parser', files: ['**/*.{ts,tsx}'], languageOptions: { ecmaVersion: 'latest', diff --git a/web/ui/dashboard-react/src/app/index.tsx b/web/ui/dashboard-react/src/app/index.tsx index 1c7420a412..59f310f065 100644 --- a/web/ui/dashboard-react/src/app/index.tsx +++ b/web/ui/dashboard-react/src/app/index.tsx @@ -4,7 +4,7 @@ import { router } from '@/lib/router'; import { ensureCanAccessPrivatePages } from '@/lib/auth'; export const Route = createFileRoute('/')({ - beforeLoad({context}) { + beforeLoad({ context }) { ensureCanAccessPrivatePages(context.auth?.getTokens().isLoggedIn); router.navigate({ to: '/projects' }); }, @@ -14,3 +14,5 @@ export const Route = createFileRoute('/')({ function Index() { return
      ; } + +// TODO: across all pages, use URL as state manager for ids diff --git a/web/ui/dashboard-react/src/app/login.tsx b/web/ui/dashboard-react/src/app/login.tsx index 5b82e35f85..38649912ba 100644 --- a/web/ui/dashboard-react/src/app/login.tsx +++ b/web/ui/dashboard-react/src/app/login.tsx @@ -189,6 +189,7 @@ function LoginWithSAMLButton() { window.open(redirectUrl); } catch (error) { // TODO should notify user here with UI + console.error(error); throw error; } } diff --git a/web/ui/dashboard-react/src/app/projects/index.tsx b/web/ui/dashboard-react/src/app/projects/index.tsx index 892b8b2d0b..9c6430f099 100644 --- a/web/ui/dashboard-react/src/app/projects/index.tsx +++ b/web/ui/dashboard-react/src/app/projects/index.tsx @@ -81,24 +81,23 @@ function ProjectIndexPage() { onOrgCreated={reloadOrganisations} isDialogOpen={isDialogOpen} setIsDialogOpen={setIsDialogOpen} - children={ - - } - /> + > + + ); if (!project) { diff --git a/web/ui/dashboard-react/src/app/projects_/$projectId/settings.tsx b/web/ui/dashboard-react/src/app/projects_/$projectId/settings.tsx index 35eb3adfcc..4c5f300fbd 100644 --- a/web/ui/dashboard-react/src/app/projects_/$projectId/settings.tsx +++ b/web/ui/dashboard-react/src/app/projects_/$projectId/settings.tsx @@ -2,7 +2,7 @@ import { z } from 'zod'; import { useState } from 'react'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; -import { createFileRoute } from '@tanstack/react-router'; +import { createFileRoute, useNavigate } from '@tanstack/react-router'; import { Bot, Home, Settings, User } from 'lucide-react'; @@ -14,6 +14,16 @@ import { FormControl, FormMessageWithErrorIcon, } from '@/components/ui/form'; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; import { Accordion, AccordionContent, @@ -35,10 +45,13 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; import { cn } from '@/lib/utils'; import { useProjectStore } from '@/store/index'; import { ensureCanAccessPrivatePages } from '@/lib/auth'; +import * as authService from '@/services/auth.service'; import * as projectsService from '@/services/projects.service'; import type { Project } from '@/models/project.model'; +import warningAnimation from '../../../../assets/img/warning-animation.gif'; + const ProjectConfigFormSchema = z.object({ name: z .string({ @@ -201,10 +214,13 @@ const ProjectConfigFormSchema = z.object({ .optional(), }); -function ProjectConfig(props: { project: Project }) { +function ProjectConfig(props: { project: Project; canManageProject: boolean }) { const [_project, set_Project] = useState(props.project); const { setProject, setProjects } = useProjectStore(); const [isUpdatingProject, setIsUpdatingProject] = useState(false); + const [isDeletingProject, setIsDeletingProject] = useState(false); + const navigate = useNavigate(); + const form = useForm>({ resolver: zodResolver(ProjectConfigFormSchema), defaultValues: { @@ -317,12 +333,28 @@ function ProjectConfig(props: { project: Project }) { } catch (error) { // TODO: notify UI of error console.error(error); - debugger; } finally { setIsUpdatingProject(false); } } + async function deleteProject() { + setIsDeletingProject(true); + try { + await projectsService.deleteProject(_project.uid); + const projects = await projectsService.getProjects(); + setProjects(projects); + setProject(projects.at(0) || null); + navigate({ to: '/projects' }); + } catch (error) { + // TODO notify UI + console.error(error); + } finally { + setIsDeletingProject(false); + // TODO notify UI + } + } + return (

      Project Configuration

      @@ -925,7 +957,11 @@ function ProjectConfig(props: { project: Project }) {
      + +
      + +
      +

      + Danger Zone +

      +
      +

      + Deleting this project will delete all of it's data including + events, apps, subscriptions and configurations. +

      + + Are you sure you want to delete this project? + +
      + + + + + + + + warning + + + Are you sure you want to deactivate “{_project?.name}”? + + +
      +

      + This action is irreversible. +

      + + + +
      + + + + + +
      +
      +
      ); } @@ -945,8 +1049,11 @@ export const Route = createFileRoute('/projects_/$projectId/settings')({ }, async loader({ params }) { const project = await projectsService.getProject(params.projectId); - // TODO handle error, and in other places too - return { project }; + // TODO handle error, and in other loaders too + const canManageProject = await authService.ensureUserCanAccess( + 'Project Settings|MANAGE', + ); + return { project, canManageProject }; }, component: ProjectSettings, }); @@ -979,7 +1086,7 @@ const tabs = [ ]; function ProjectSettings() { - const { project } = Route.useLoaderData(); + const { project, canManageProject } = Route.useLoaderData(); return ( @@ -1004,10 +1111,13 @@ function ProjectSettings() { ))} -
      +
      {tabs.map(tab => ( - + ))}
      diff --git a/web/ui/dashboard-react/src/app/settings.tsx b/web/ui/dashboard-react/src/app/settings.tsx index 1b8b0b9a97..946bd27d1a 100644 --- a/web/ui/dashboard-react/src/app/settings.tsx +++ b/web/ui/dashboard-react/src/app/settings.tsx @@ -89,7 +89,7 @@ function SettingsPage() { useEffect(() => { organisationForm.setValue('orgId', org?.uid || ''); organisationForm.setValue('orgName', org?.name || ''); - }, [org?.uid]); + }, [org, organisationForm]); async function updateOrganisation( values: z.infer, @@ -129,10 +129,12 @@ function SettingsPage() { setPaginatedOrgs({ pagination: paginatedOrgs.pagination, - content: paginatedOrgs.content.filter((_org) => org?.uid != _org.uid), + content: paginatedOrgs.content.filter(_org => org?.uid != _org.uid), }); - setOrg(paginatedOrgs.content.filter((_org) => org?.uid != _org.uid)[0] || null); + setOrg( + paginatedOrgs.content.filter(_org => org?.uid != _org.uid)[0] || null, + ); } catch (error) { console.error(error); } finally { diff --git a/web/ui/dashboard-react/src/app/signup.tsx b/web/ui/dashboard-react/src/app/signup.tsx index c195e605ce..317d10808e 100644 --- a/web/ui/dashboard-react/src/app/signup.tsx +++ b/web/ui/dashboard-react/src/app/signup.tsx @@ -269,6 +269,7 @@ function SignUpWithSAMLButton() { window.open(data.redirectUrl, '_blank'); } catch (err) { // TODO show user on the UI + console.error(err); throw err; } } diff --git a/web/ui/dashboard-react/src/components/create-organisation.tsx b/web/ui/dashboard-react/src/components/create-organisation.tsx index 2e7577d10c..afc8243274 100644 --- a/web/ui/dashboard-react/src/components/create-organisation.tsx +++ b/web/ui/dashboard-react/src/components/create-organisation.tsx @@ -90,7 +90,7 @@ export function CreateOrganisationDialog(props: CreateOrganisationDialogProps) {
      - What is your business's name? + What is your business's name?
      diff --git a/web/ui/dashboard-react/src/components/ui/badge.tsx b/web/ui/dashboard-react/src/components/ui/badge.tsx index e87d62bf1a..98b81d3dad 100644 --- a/web/ui/dashboard-react/src/components/ui/badge.tsx +++ b/web/ui/dashboard-react/src/components/ui/badge.tsx @@ -1,36 +1,36 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" +import * as React from 'react'; +import { cva, type VariantProps } from 'class-variance-authority'; -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils'; const badgeVariants = cva( - "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", - { - variants: { - variant: { - default: - "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", - secondary: - "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", - destructive: - "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", - outline: "text-foreground", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) + 'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', + { + variants: { + variant: { + default: + 'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80', + secondary: + 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80', + destructive: + 'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80', + outline: 'text-foreground', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +); export interface BadgeProps - extends React.HTMLAttributes, - VariantProps {} + extends React.HTMLAttributes, + VariantProps {} function Badge({ className, variant, ...props }: BadgeProps) { - return ( -
      - ) + return ( +
      + ); } -export { Badge, badgeVariants } +export { Badge }; diff --git a/web/ui/dashboard-react/src/components/ui/button.tsx b/web/ui/dashboard-react/src/components/ui/button.tsx index 85a64c2be9..fa509522f5 100644 --- a/web/ui/dashboard-react/src/components/ui/button.tsx +++ b/web/ui/dashboard-react/src/components/ui/button.tsx @@ -54,4 +54,4 @@ const Button = React.forwardRef( ); Button.displayName = 'Button'; -export { Button, buttonVariants }; +export { Button }; diff --git a/web/ui/dashboard-react/src/components/ui/command.tsx b/web/ui/dashboard-react/src/components/ui/command.tsx index 0db642a6e9..136312fddb 100644 --- a/web/ui/dashboard-react/src/components/ui/command.tsx +++ b/web/ui/dashboard-react/src/components/ui/command.tsx @@ -1,151 +1,151 @@ -import * as React from "react" -import { type DialogProps } from "@radix-ui/react-dialog" -import { Command as CommandPrimitive } from "cmdk" -import { Search } from "lucide-react" +import * as React from 'react'; +import { type DialogProps } from '@radix-ui/react-dialog'; +import { Command as CommandPrimitive } from 'cmdk'; +import { Search } from 'lucide-react'; -import { cn } from "@/lib/utils" -import { Dialog, DialogContent } from "@/components/ui/dialog" +import { cn } from '@/lib/utils'; +import { Dialog, DialogContent } from '@/components/ui/dialog'; const Command = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - -)) -Command.displayName = CommandPrimitive.displayName + +)); +Command.displayName = CommandPrimitive.displayName; const CommandDialog = ({ children, ...props }: DialogProps) => { - return ( - - - - {children} - - - - ) -} + return ( + + + + {children} + + + + ); +}; const CommandInput = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( -
      - - -
      -)) - -CommandInput.displayName = CommandPrimitive.Input.displayName +
      + + +
      +)); + +CommandInput.displayName = CommandPrimitive.Input.displayName; const CommandList = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - -)) + +)); -CommandList.displayName = CommandPrimitive.List.displayName +CommandList.displayName = CommandPrimitive.List.displayName; const CommandEmpty = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >((props, ref) => ( - -)) + +)); -CommandEmpty.displayName = CommandPrimitive.Empty.displayName +CommandEmpty.displayName = CommandPrimitive.Empty.displayName; const CommandGroup = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - -)) - -CommandGroup.displayName = CommandPrimitive.Group.displayName + +)); + +CommandGroup.displayName = CommandPrimitive.Group.displayName; const CommandSeparator = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - -)) -CommandSeparator.displayName = CommandPrimitive.Separator.displayName + +)); +CommandSeparator.displayName = CommandPrimitive.Separator.displayName; const CommandItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - -)) - -CommandItem.displayName = CommandPrimitive.Item.displayName + +)); + +CommandItem.displayName = CommandPrimitive.Item.displayName; const CommandShortcut = ({ - className, - ...props + className, + ...props }: React.HTMLAttributes) => { - return ( - - ) -} -CommandShortcut.displayName = "CommandShortcut" + return ( + + ); +}; +CommandShortcut.displayName = 'CommandShortcut'; export { - Command, - CommandDialog, - CommandInput, - CommandList, - CommandEmpty, - CommandGroup, - CommandItem, - CommandShortcut, - CommandSeparator, -} + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +}; diff --git a/web/ui/dashboard-react/src/lib/pipes.ts b/web/ui/dashboard-react/src/lib/pipes.ts index 80cb336c82..736c5d7d6b 100644 --- a/web/ui/dashboard-react/src/lib/pipes.ts +++ b/web/ui/dashboard-react/src/lib/pipes.ts @@ -7,9 +7,8 @@ export function getInitials(names: Array) { return initials[0] + initials[initials.length - 1]; } - export function truncateProjectName(name: string) { - let formattedName = name.substring(0, 27); + const formattedName = name.substring(0, 27); if (name > formattedName) return formattedName + '...'; return formattedName; } diff --git a/web/ui/dashboard-react/src/main.tsx b/web/ui/dashboard-react/src/main.tsx index 97b876715b..e72ec98ec7 100644 --- a/web/ui/dashboard-react/src/main.tsx +++ b/web/ui/dashboard-react/src/main.tsx @@ -14,7 +14,7 @@ if (!root) { root = document.getElementById('root') as HTMLElement; } -function App() { +export function App() { const auth = useAuth(); return ; } diff --git a/web/ui/dashboard-react/src/models/global.model.ts b/web/ui/dashboard-react/src/models/global.model.ts index b81b2a9d88..6f48973ee2 100644 --- a/web/ui/dashboard-react/src/models/global.model.ts +++ b/web/ui/dashboard-react/src/models/global.model.ts @@ -1,7 +1,7 @@ export interface HttpResponse { data: T; message: string; - error?: any; + error?: unknown; status: boolean; } @@ -13,7 +13,7 @@ type Pagination = { next_page_cursor: string; }; -export interface PaginatedResult{ - content: Array - pagination: Pagination +export interface PaginatedResult { + content: Array; + pagination: Pagination; } diff --git a/web/ui/dashboard-react/src/models/organisation.model.ts b/web/ui/dashboard-react/src/models/organisation.model.ts index decb690e5c..577a5a46df 100644 --- a/web/ui/dashboard-react/src/models/organisation.model.ts +++ b/web/ui/dashboard-react/src/models/organisation.model.ts @@ -21,7 +21,7 @@ export interface Organisation { OwnerID: string; name: string; custom_domain: null | string; - assigned_domain: null | String; + assigned_domain: null | string; /** * Date string */ diff --git a/web/ui/dashboard-react/src/models/project.model.ts b/web/ui/dashboard-react/src/models/project.model.ts index 93ea0e431e..60e91d3e76 100644 --- a/web/ui/dashboard-react/src/models/project.model.ts +++ b/web/ui/dashboard-react/src/models/project.model.ts @@ -1,3 +1,9 @@ +type Statistics = { + events_exist: boolean; + subscriptions_exist: boolean; + endpoints_exist: boolean; +} | null; + export interface Project { uid: string; name: string; @@ -40,11 +46,7 @@ export interface Project { // search_policy: string; // }; }; - statistics: { - events_exist: boolean; - subscriptions_exist: boolean; - endpoints_exist: boolean; - } | null; + statistics: Statistics; } export interface MetaEvent { @@ -79,7 +81,7 @@ export interface CreateProjectResponse { organisation_id: string; type: string; config: Config; - statistics: any; + statistics: Statistics; retained_events: number; created_at: string; updated_at: string; diff --git a/web/ui/dashboard-react/src/services/http.service.ts b/web/ui/dashboard-react/src/services/http.service.ts index 34653ac593..963f731686 100644 --- a/web/ui/dashboard-react/src/services/http.service.ts +++ b/web/ui/dashboard-react/src/services/http.service.ts @@ -57,9 +57,13 @@ export function buildRequestQuery( return cleanedQueryString + queryString; } +type ReqLevel = 'org' | 'org_project'; + export function buildRequestPath( - level?: 'org' | 'org_project', + level?: ReqLevel, deps: { + // FIX: these values should be passed in, not gotten from cache because of + // inconsistencies getCachedProjectId: () => string; getCachedOrganisationId: () => string; } = { @@ -92,7 +96,13 @@ export function buildRequestPath( return ''; } -export function buildURL(requestDetails: any): string { +type ReqDetails = { + isOut?: boolean; + url: string; + level?: ReqLevel; + query?: Record; +}; +export function buildURL(requestDetails: ReqDetails): string { if (requestDetails.isOut) return requestDetails.url; if (getToken()) @@ -120,7 +130,7 @@ export function setupAxios( error => { if (axios.isAxiosError(error)) { const errorResponse = error.response; - let errorMessage = errorResponse?.data + const errorMessage = errorResponse?.data ? errorResponse.data.message : error.message; @@ -141,10 +151,9 @@ export function setupAxios( } if (!requestDetails.hideNotification) { - let errorMessage: string; - error.error?.message - ? (errorMessage = error.error?.message) - : (errorMessage = 'An error occured, please try again'); + const errorMessage = error.error?.message + ? error.error?.message + : 'An error occured, please try again'; // TODO GeneralService.showNotification; for now console.error(errorMessage); } @@ -159,10 +168,21 @@ export function setupAxios( export async function request( requestDetails: { url: string; - body?: any; + body?: Record< + string, + | string + | number + | object + | undefined + | null + | Record + >; method: 'get' | 'post' | 'delete' | 'put'; hideNotification?: boolean; - query?: Record; + query?: Record< + string, + Record + >; level?: 'org' | 'org_project'; isOut?: boolean; }, diff --git a/web/ui/dashboard-react/src/services/licenses.service.ts b/web/ui/dashboard-react/src/services/licenses.service.ts index 7acdc7aa89..940f087013 100644 --- a/web/ui/dashboard-react/src/services/licenses.service.ts +++ b/web/ui/dashboard-react/src/services/licenses.service.ts @@ -23,33 +23,30 @@ export async function getLicenses( return allowedLicenses; } -const LICENSES = [ - 'ADVANCED_ENDPOINT_MANAGEMENT', - 'ADVANCED_MESSAGE_BROKER', - 'ADVANCED_SUBSCRIPTIONS', - 'ADVANCED_WEBHOOK_ARCHIVING', - 'ADVANCED_WEBHOOK_FILTERING', - 'AGENT_EXECUTION_MODE', - 'ASYNQ_MONITORING', - 'AUDIT_LOGS', - 'CIRCUIT_BREAKING', - 'CONSUMER_POOL_TUNING', - 'CREATE_ORG', - 'CREATE_PROJECT', - 'CREATE_USER', - 'CREDENTIAL_ENCRYPTION', - 'DATADOG_TRACING', - 'ENTERPRISE_SSO', - 'EVENT_CATALOGUE', - 'EXPORT_PROMETHEUS_METRICS', - 'INGEST_RATE', - 'IP_RULES', - 'MULTI_PLAYER_MODE', - 'PORTAL_LINKS', - 'SYNCHRONOUS_WEBHOOKS', - 'USE_FORWARD_PROXY', - 'WEBHOOK_ANALYTICS', - 'WEBHOOK_TRANSFORMATIONS', -] as const; - -export type LicenseKey = (typeof LICENSES)[number]; +export type LicenseKey = + | 'ADVANCED_ENDPOINT_MANAGEMENT' + | 'ADVANCED_MESSAGE_BROKER' + | 'ADVANCED_SUBSCRIPTIONS' + | 'ADVANCED_WEBHOOK_ARCHIVING' + | 'ADVANCED_WEBHOOK_FILTERING' + | 'AGENT_EXECUTION_MODE' + | 'ASYNQ_MONITORING' + | 'AUDIT_LOGS' + | 'CIRCUIT_BREAKING' + | 'CONSUMER_POOL_TUNING' + | 'CREATE_ORG' + | 'CREATE_PROJECT' + | 'CREATE_USER' + | 'CREDENTIAL_ENCRYPTION' + | 'DATADOG_TRACING' + | 'ENTERPRISE_SSO' + | 'EVENT_CATALOGUE' + | 'EXPORT_PROMETHEUS_METRICS' + | 'INGEST_RATE' + | 'IP_RULES' + | 'MULTI_PLAYER_MODE' + | 'PORTAL_LINKS' + | 'SYNCHRONOUS_WEBHOOKS' + | 'USE_FORWARD_PROXY' + | 'WEBHOOK_ANALYTICS' + | 'WEBHOOK_TRANSFORMATIONS'; diff --git a/web/ui/dashboard-react/src/services/projects.service.ts b/web/ui/dashboard-react/src/services/projects.service.ts index 580e55210a..b968cb2e8f 100644 --- a/web/ui/dashboard-react/src/services/projects.service.ts +++ b/web/ui/dashboard-react/src/services/projects.service.ts @@ -114,3 +114,16 @@ export async function updateProject( return res.data; } + +export async function deleteProject( + _uid: string, + deps: { httpReq: typeof request } = { httpReq: request }, +) { + const res = await deps.httpReq({ + url: '', + method: 'delete', + level: 'org_project', + }); + + return res.data; +} From 85414a5e7e1d39dcbed994c7f85caffb0518a47e Mon Sep 17 00:00:00 2001 From: Orim Dominic Date: Fri, 14 Mar 2025 14:37:55 +0100 Subject: [PATCH 23/43] refactor: update projects w/out api call --- .../src/app/projects_/$projectId/settings.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/web/ui/dashboard-react/src/app/projects_/$projectId/settings.tsx b/web/ui/dashboard-react/src/app/projects_/$projectId/settings.tsx index 4c5f300fbd..cd710ee445 100644 --- a/web/ui/dashboard-react/src/app/projects_/$projectId/settings.tsx +++ b/web/ui/dashboard-react/src/app/projects_/$projectId/settings.tsx @@ -216,7 +216,7 @@ const ProjectConfigFormSchema = z.object({ function ProjectConfig(props: { project: Project; canManageProject: boolean }) { const [_project, set_Project] = useState(props.project); - const { setProject, setProjects } = useProjectStore(); + const { setProject, setProjects, projects } = useProjectStore(); const [isUpdatingProject, setIsUpdatingProject] = useState(false); const [isDeletingProject, setIsDeletingProject] = useState(false); const navigate = useNavigate(); @@ -342,9 +342,8 @@ function ProjectConfig(props: { project: Project; canManageProject: boolean }) { setIsDeletingProject(true); try { await projectsService.deleteProject(_project.uid); - const projects = await projectsService.getProjects(); - setProjects(projects); - setProject(projects.at(0) || null); + setProjects(projects.filter(p => p.uid != _project.uid)); + setProject(projects.at(1) || null); navigate({ to: '/projects' }); } catch (error) { // TODO notify UI From 36a30b4b1d43a480abc01a5bde18ad69c766cc69 Mon Sep 17 00:00:00 2001 From: Orim Dominic Date: Sat, 15 Mar 2025 15:51:31 +0100 Subject: [PATCH 24/43] fix: unable to update project caused by sig header field --- .../src/app/projects_/$projectId/settings.tsx | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/web/ui/dashboard-react/src/app/projects_/$projectId/settings.tsx b/web/ui/dashboard-react/src/app/projects_/$projectId/settings.tsx index cd710ee445..1b5799d9da 100644 --- a/web/ui/dashboard-react/src/app/projects_/$projectId/settings.tsx +++ b/web/ui/dashboard-react/src/app/projects_/$projectId/settings.tsx @@ -310,15 +310,13 @@ function ProjectConfig(props: { project: Project; canManageProject: boolean }) { // @ts-expect-error it has to be this way for the API signature: payload.config?.signature ? { - signature: { - header: payload.config.signature.header, - versions: [ - { - hash: payload.config.signature.hash, - encoding: payload.config.signature.encoding, - }, - ], - }, + header: payload.config.signature.header, + versions: [ + { + hash: payload.config.signature.hash, + encoding: payload.config.signature.encoding, + }, + ], } : _project.config.signature, disable_endpoint: _project.config.disable_endpoint, From 18fc65342e392ce05784c5eeef1da45d42fb76bd Mon Sep 17 00:00:00 2001 From: Orim Dominic Adah Date: Sun, 16 Mar 2025 11:28:26 +0100 Subject: [PATCH 25/43] feat: project signature config (#2265) * feat: display project signature config * feat: navigate to /projects on select project * fix: set proper UI versions for signatures * fix: display latest version in form * feat: project signature config * fix: lint error --- configs/local/convoy.json | 5 +- .../src/app/projects_/$projectId/settings.tsx | 386 +++++++++++++++++- .../dashboard-react/src/app/projects_/new.tsx | 2 +- .../src/components/dashboard.tsx | 3 +- .../src/components/ui/sheet.tsx | 222 +++++----- .../src/components/ui/table.tsx | 122 ++++++ web/ui/dashboard-react/src/lib/pipes.ts | 12 + .../src/models/project.model.ts | 2 +- .../create-project-component.component.ts | 35 +- .../src/app/services/http/http.service.ts | 11 +- 10 files changed, 646 insertions(+), 154 deletions(-) create mode 100644 web/ui/dashboard-react/src/components/ui/table.tsx diff --git a/configs/local/convoy.json b/configs/local/convoy.json index 63c498e4b1..58c10c6caf 100644 --- a/configs/local/convoy.json +++ b/configs/local/convoy.json @@ -1,5 +1,5 @@ { - "host": "localhost:5005", + "host": "*", "database": { "host": "pgbouncer", "username": "convoy", @@ -26,5 +26,6 @@ "prometheus", "circuit-breaker", "full-text-search" - ] + ], + "license_key": "CD2A07-0199FB-AD5CA1-536859-09CC01-V3" } diff --git a/web/ui/dashboard-react/src/app/projects_/$projectId/settings.tsx b/web/ui/dashboard-react/src/app/projects_/$projectId/settings.tsx index 1b5799d9da..dda60e9ebb 100644 --- a/web/ui/dashboard-react/src/app/projects_/$projectId/settings.tsx +++ b/web/ui/dashboard-react/src/app/projects_/$projectId/settings.tsx @@ -4,7 +4,7 @@ import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { createFileRoute, useNavigate } from '@tanstack/react-router'; -import { Bot, Home, Settings, User } from 'lucide-react'; +import { Bot, Home, Settings, User, Plus } from 'lucide-react'; import { Form, @@ -14,6 +14,24 @@ import { FormControl, FormMessageWithErrorIcon, } from '@/components/ui/form'; +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTrigger, + SheetTitle, + SheetClose, +} from '@/components/ui/sheet'; +import { + Table, + TableBody, + TableCaption, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; import { Dialog, DialogClose, @@ -43,6 +61,7 @@ import { DashboardLayout } from '@/components/dashboard'; import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; import { cn } from '@/lib/utils'; +import { toMMMDDYYYY } from '@/lib/pipes'; import { useProjectStore } from '@/store/index'; import { ensureCanAccessPrivatePages } from '@/lib/auth'; import * as authService from '@/services/auth.service'; @@ -228,7 +247,12 @@ function ProjectConfig(props: { project: Project; canManageProject: boolean }) { config: { search_policy: { isEnabled: !!_project.config.search_policy.length, - search_policy: _project.config.search_policy, + search_policy: _project.config.search_policy.length + ? _project.config.search_policy.substring( + 0, + _project.config.search_policy.length - 1, + ) + : '', }, ratelimit: { isEnabled: true, @@ -249,9 +273,13 @@ function ProjectConfig(props: { project: Project; canManageProject: boolean }) { isEnabled: _project.type == 'outgoing', header: _project.config.signature.header, // @ts-expect-error a default value exists, even for incoming projects - encoding: _project.config.signature.versions.at(0)?.encoding, + encoding: _project.config.signature.versions.at( + _project.config.signature.versions.length - 1, + )?.encoding, // @ts-expect-error a default value exists, even for incoming projects - hash: _project.config.signature.versions.at(0)?.hash, + hash: _project.config.signature.versions.at( + _project.config.signature.versions.length - 1, + )?.hash, }, }, }, @@ -304,9 +332,8 @@ function ProjectConfig(props: { project: Project; canManageProject: boolean }) { type: _project.type, config: { ...payload.config, - search_policy: (payload.config?.search_policy?.isEnabled - ? payload.config.search_policy.search_policy - : _project.config.search_policy) as `${string}h`, + search_policy: payload.config?.search_policy + ?.search_policy as `{string}h`, // @ts-expect-error it has to be this way for the API signature: payload.config?.signature ? { @@ -1040,6 +1067,311 @@ function ProjectConfig(props: { project: Project; canManageProject: boolean }) { ); } +function groupItemsByDate( + items: Array, + sortOrder: 'desc' | 'asc' = 'desc', +) { + const groupsObj = Object.groupBy(items, ({ created_at }) => + toMMMDDYYYY(created_at), + ); + + const sortedGroup = new Map(); + + Object.keys(groupsObj) + .sort((dateA, dateB) => { + if (sortOrder == 'desc') { + return Number(new Date(dateB)) - Number(new Date(dateA)); + } + return Number(new Date(dateA)) - Number(new Date(dateB)); + }) + .reduce((acc, dateKey) => { + return acc.set(dateKey, groupsObj[dateKey] as typeof items); + }, sortedGroup); + + return sortedGroup; +} + +const NewSignatureFormSchema = z.object({ + encoding: z + .enum(['hex', 'base64', '']) + .optional() + .transform(v => (v?.length == 0 ? undefined : v)) + .refine( + v => { + if (!v) return false; + return true; + }, + { + message: 'Select encoding type', + }, + ), + hash: z + .enum(['SHA256', 'SHA512', '']) + .optional() + .transform(v => (v?.length == 0 ? undefined : v)) + .refine( + v => { + if (!v) return false; + return true; + }, + { + message: 'Please select hash', + }, + ), +}); + +function SignatureHistoryConfig(props: { + project: Project; + canManageProject: boolean; +}) { + const { project } = props; + const [isAddingVersion, setIsAddingVersion] = useState(false); + const { setProjects, projects, setProject } = useProjectStore(); + // TODO update UI on new project added + + const form = useForm>({ + resolver: zodResolver(NewSignatureFormSchema), + defaultValues: { + encoding: '', + hash: '', + }, + mode: 'onTouched', + }); + + async function addSignatureVersion( + version: z.infer, + ) { + try { + setIsAddingVersion(true); + const updated = await projectsService.updateProject({ + ...project, + config: { + ...project.config, + signature: { + ...project.config.signature, + versions: [ + // @ts-expect-error this works + ...project.config.signature.versions.map(v => ({ + encoding: v.encoding, + hash: v.hash, + })), + // @ts-expect-error this works + version, + ], + }, + }, + }); + setProjects(projects.map(p => (p.uid == updated.uid ? updated : p))); + setProject(updated); + } catch (err) { + console.error(err); + } finally { + setIsAddingVersion(false); + } + } + + if (project.type == 'incoming') return null; + + return ( +
      +
      +

      Project Signature History

      + + + + + + + New Signature + + Add a new signature + + +
      +
      +
      + + void form.handleSubmit(addSignatureVersion)(...args) + } + > + ( + +
      + + Encoding + +
      + + +
      + )} + /> + + ( + +
      + + Hash + +
      + + +
      + )} + /> + +
      + + + + + +
      + + +
      +
      +
      +
      + +
      + + + {project.name} signature history + + + + + header + + + version + + + hash + + + encoding + + + + + {Array.from( + groupItemsByDate(project.config.signature.versions), + ).map(([dateKey, sigs]) => { + const val = [ + + + {dateKey} + + + + + , + ].concat( + sigs.map((sig, i) => ( + + + {project.config.signature.header} + + v{i + 1} + {sig.hash} + + {sig.encoding} + + + )), + ); + + return val; + })} + +
      +
      +
      + ); +} + export const Route = createFileRoute('/projects_/$projectId/settings')({ beforeLoad({ context }) { ensureCanAccessPrivatePages(context.auth?.getTokens().isLoggedIn); @@ -1057,28 +1389,46 @@ export const Route = createFileRoute('/projects_/$projectId/settings')({ const tabs = [ { - name: 'Projects', + name: 'Project', value: 'projects', icon: Home, component: ProjectConfig, + projectTypes: ['incoming', 'outgoing'], + }, + { + name: 'Signature History', + value: 'signature-history', + icon: Home, + component: SignatureHistoryConfig, + projectTypes: ['outgoing'], }, { name: 'Endpoints', value: 'endpoints', icon: User, component: ProjectConfig, + projectTypes: ['incoming', 'outgoing'], }, { name: 'Meta Events', value: 'meta-events', icon: Bot, component: ProjectConfig, + projectTypes: ['incoming', 'outgoing'], + }, + { + name: 'Event Types', + value: 'event-types', + icon: Home, + component: ProjectConfig, + projectTypes: ['outgoing'], }, { name: 'Secrets', value: 'secrets', icon: Settings, component: ProjectConfig, + projectTypes: ['incoming', 'outgoing'], }, ]; @@ -1097,15 +1447,17 @@ function ProjectSettings() { className="w-full flex items-start gap-4 justify-center" > - {tabs.map(tab => ( - - {tab.name} - - ))} + {tabs + .filter(tab => tab.projectTypes.includes(project.type)) + .map(tab => ( + + {tab.name} + + ))}
      diff --git a/web/ui/dashboard-react/src/app/projects_/new.tsx b/web/ui/dashboard-react/src/app/projects_/new.tsx index e56711a986..1a7687588f 100644 --- a/web/ui/dashboard-react/src/app/projects_/new.tsx +++ b/web/ui/dashboard-react/src/app/projects_/new.tsx @@ -278,7 +278,7 @@ function CreateNewProject() { if (selectedWebhookType != 'outgoing') { form.setValue('config.signature.isEnabled', false); } - }, [selectedWebhookType]); + }, [selectedWebhookType, form]); async function createProject( values: z.infer, diff --git a/web/ui/dashboard-react/src/components/dashboard.tsx b/web/ui/dashboard-react/src/components/dashboard.tsx index 0c16bcd061..3dbbc29dc9 100644 --- a/web/ui/dashboard-react/src/components/dashboard.tsx +++ b/web/ui/dashboard-react/src/components/dashboard.tsx @@ -339,6 +339,7 @@ const FormSchema = z.object({ function ProjectsList() { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const { project, projects, setProject } = useProjectStore(); + const navigate = useNavigate(); const form = useForm>({ resolver: zodResolver(FormSchema), @@ -394,7 +395,6 @@ function ProjectsList() { { form.setValue( 'project', @@ -418,6 +418,7 @@ function ProjectsList() { onSelect={() => { setProject(p); setIsPopoverOpen(false); + navigate({ to: '/projects' }); }} >
      diff --git a/web/ui/dashboard-react/src/components/ui/sheet.tsx b/web/ui/dashboard-react/src/components/ui/sheet.tsx index 272cb721eb..2317170fe0 100644 --- a/web/ui/dashboard-react/src/components/ui/sheet.tsx +++ b/web/ui/dashboard-react/src/components/ui/sheet.tsx @@ -1,140 +1,140 @@ -"use client" +'use client'; -import * as React from "react" -import * as SheetPrimitive from "@radix-ui/react-dialog" -import { cva, type VariantProps } from "class-variance-authority" -import { X } from "lucide-react" +import * as React from 'react'; +import * as SheetPrimitive from '@radix-ui/react-dialog'; +import { cva, type VariantProps } from 'class-variance-authority'; +import { X } from 'lucide-react'; -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils'; -const Sheet = SheetPrimitive.Root +const Sheet = SheetPrimitive.Root; -const SheetTrigger = SheetPrimitive.Trigger +const SheetTrigger = SheetPrimitive.Trigger; -const SheetClose = SheetPrimitive.Close +const SheetClose = SheetPrimitive.Close; -const SheetPortal = SheetPrimitive.Portal +const SheetPortal = SheetPrimitive.Portal; const SheetOverlay = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - -)) -SheetOverlay.displayName = SheetPrimitive.Overlay.displayName + +)); +SheetOverlay.displayName = SheetPrimitive.Overlay.displayName; const sheetVariants = cva( - "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out", - { - variants: { - side: { - top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", - bottom: - "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", - left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", - right: - "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", - }, - }, - defaultVariants: { - side: "right", - }, - } -) + 'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out', + { + variants: { + side: { + top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top', + bottom: + 'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom', + left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm', + right: + 'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm', + }, + }, + defaultVariants: { + side: 'right', + }, + }, +); interface SheetContentProps - extends React.ComponentPropsWithoutRef, - VariantProps {} + extends React.ComponentPropsWithoutRef, + VariantProps {} const SheetContent = React.forwardRef< - React.ElementRef, - SheetContentProps ->(({ side = "right", className, children, ...props }, ref) => ( - - - - - - Close - - {children} - - -)) -SheetContent.displayName = SheetPrimitive.Content.displayName + React.ElementRef, + SheetContentProps +>(({ side = 'right', className, children, ...props }, ref) => ( + + + + + + Close + + {children} + + +)); +SheetContent.displayName = SheetPrimitive.Content.displayName; const SheetHeader = ({ - className, - ...props + className, + ...props }: React.HTMLAttributes) => ( -
      -) -SheetHeader.displayName = "SheetHeader" +
      +); +SheetHeader.displayName = 'SheetHeader'; const SheetFooter = ({ - className, - ...props + className, + ...props }: React.HTMLAttributes) => ( -
      -) -SheetFooter.displayName = "SheetFooter" +
      +); +SheetFooter.displayName = 'SheetFooter'; const SheetTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - -)) -SheetTitle.displayName = SheetPrimitive.Title.displayName + +)); +SheetTitle.displayName = SheetPrimitive.Title.displayName; const SheetDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - -)) -SheetDescription.displayName = SheetPrimitive.Description.displayName + +)); +SheetDescription.displayName = SheetPrimitive.Description.displayName; export { - Sheet, - SheetPortal, - SheetOverlay, - SheetTrigger, - SheetClose, - SheetContent, - SheetHeader, - SheetFooter, - SheetTitle, - SheetDescription, -} + Sheet, + SheetPortal, + SheetOverlay, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, +}; diff --git a/web/ui/dashboard-react/src/components/ui/table.tsx b/web/ui/dashboard-react/src/components/ui/table.tsx new file mode 100644 index 0000000000..3665dc2628 --- /dev/null +++ b/web/ui/dashboard-react/src/components/ui/table.tsx @@ -0,0 +1,122 @@ +import * as React from 'react'; + +import { cn } from '@/lib/utils'; + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
      + + +)); +Table.displayName = 'Table'; + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableHeader.displayName = 'TableHeader'; + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableBody.displayName = 'TableBody'; + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0', + className, + )} + {...props} + /> +)); +TableFooter.displayName = 'TableFooter'; + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableRow.displayName = 'TableRow'; + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes + // eslint-disable-next-line react/prop-types +>(({ className, ...props }, ref) => ( +
      [role=checkbox]]:translate-y-[2px]', + className, + )} + {...props} + /> +)); +TableHead.displayName = 'TableHead'; + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes + // eslint-disable-next-line react/prop-types +>(({ className, ...props }, ref) => ( + [role=checkbox]]:translate-y-[2px]', + className, + )} + {...props} + /> +)); +TableCell.displayName = 'TableCell'; + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
      +)); +TableCaption.displayName = 'TableCaption'; + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +}; diff --git a/web/ui/dashboard-react/src/lib/pipes.ts b/web/ui/dashboard-react/src/lib/pipes.ts index 736c5d7d6b..32cc07d3ff 100644 --- a/web/ui/dashboard-react/src/lib/pipes.ts +++ b/web/ui/dashboard-react/src/lib/pipes.ts @@ -12,3 +12,15 @@ export function truncateProjectName(name: string) { if (name > formattedName) return formattedName + '...'; return formattedName; } + +/** + * @param {string} date an ISO date string + * @example + * // returns 'Nov 3, 1994' + * toMMM_DD_YYYY("1994-11-03T00:00:00Z") + */ +export function toMMMDDYYYY(date: string) { + return new Intl.DateTimeFormat('en-US', { dateStyle: 'medium' }).format( + new Date(date), + ); +} diff --git a/web/ui/dashboard-react/src/models/project.model.ts b/web/ui/dashboard-react/src/models/project.model.ts index 60e91d3e76..617514015d 100644 --- a/web/ui/dashboard-react/src/models/project.model.ts +++ b/web/ui/dashboard-react/src/models/project.model.ts @@ -141,7 +141,7 @@ interface Signature { versions: Version[]; } -interface Version { +export interface Version { uid: string; hash: string; encoding: string; diff --git a/web/ui/dashboard/src/app/private/components/create-project-component/create-project-component.component.ts b/web/ui/dashboard/src/app/private/components/create-project-component/create-project-component.component.ts index ba694d2f1c..b9d11041cd 100644 --- a/web/ui/dashboard/src/app/private/components/create-project-component/create-project-component.component.ts +++ b/web/ui/dashboard/src/app/private/components/create-project-component/create-project-component.component.ts @@ -91,7 +91,7 @@ export class CreateProjectComponent implements OnInit { { uid: 'strategy', name: 'Retry Config', show: false, deleted: false }, { uid: 'ratelimit', name: 'Rate Limit', show: false, deleted: false }, { uid: 'search_policy', name: 'Search Policy', show: false, deleted: false }, - { uid: 'signature', name: 'Signature Format', show: false, deleted: false }, + { uid: 'signature', name: 'Signature Format', show: false, deleted: false } ]; public rbacService = inject(RbacService); tabs: TAB[] = [ @@ -106,7 +106,7 @@ export class CreateProjectComponent implements OnInit { events = ['endpoint.created', 'endpoint.deleted', 'endpoint.updated', 'eventdelivery.success', 'eventdelivery.failed', 'project.updated']; eventTypes: EVENT_TYPE[] = []; selectedEventType!: EVENT_TYPE; - rateLimitDeleted = false; + rateLimitDeleted = false; constructor( private formBuilder: FormBuilder, @@ -147,16 +147,16 @@ export class CreateProjectComponent implements OnInit { toggleConfigForm(configValue: string, deleted?: boolean) { this.configurations.forEach(config => { if (config.uid === configValue) { - config.show = !config.show; - config.deleted = deleted ?? false; - } + config.show = !config.show; + config.deleted = deleted ?? false; + } }); } setConfigFormDeleted(configValue: string, deleted: boolean) { this.configurations.forEach(config => { if (config.uid === configValue) { - config.deleted = deleted; - } + config.deleted = deleted; + } }); } @@ -164,9 +164,9 @@ export class CreateProjectComponent implements OnInit { return this.configurations.find(config => config.uid === configValue)?.show || false; } - configDeleted(configValue: string): boolean { - return this.configurations.find(config => config.uid === configValue)?.deleted || false; - } + configDeleted(configValue: string): boolean { + return this.configurations.find(config => config.uid === configValue)?.deleted || false; + } async getProjectDetails() { try { @@ -250,16 +250,15 @@ export class CreateProjectComponent implements OnInit { if (typeof this.projectForm.value.config.ratelimit.count === 'string') this.projectForm.value.config.ratelimit.count = parseInt(this.projectForm.value.config.ratelimit.count); if (typeof this.projectForm.value.config.search_policy === 'number') this.projectForm.value.config.search_policy = `${this.projectForm.value.config.search_policy}h`; + if (!this.showConfig('ratelimit') && this.configDeleted('ratelimit')) { + this.projectForm.value.config.ratelimit.count = 0; + this.projectForm.value.config.ratelimit.duration = 0; + this.projectForm.value.config.ratelimit = null; - if (!this.showConfig('ratelimit') && this.configDeleted('ratelimit')) { - this.projectForm.value.config.ratelimit.count = 0; - this.projectForm.value.config.ratelimit.duration = 0; - this.projectForm.value.config.ratelimit = null; - - this.projectForm.get('config.ratelimit')?.patchValue({ count: 0, duration: 0 }); + this.projectForm.get('config.ratelimit')?.patchValue({ count: 0, duration: 0 }); - this.setConfigFormDeleted('ratelimit', false); - } + this.setConfigFormDeleted('ratelimit', false); + } this.isCreatingProject = true; diff --git a/web/ui/dashboard/src/app/services/http/http.service.ts b/web/ui/dashboard/src/app/services/http/http.service.ts index 6b0f05c617..d7531dd4e5 100644 --- a/web/ui/dashboard/src/app/services/http/http.service.ts +++ b/web/ui/dashboard/src/app/services/http/http.service.ts @@ -15,7 +15,12 @@ export class HttpService { token = this.route.snapshot.queryParams?.token; checkTokenTimeout: any; - constructor(private router: Router, private generalService: GeneralService, private route: ActivatedRoute, private projectService: ProjectService) {} + constructor( + private router: Router, + private generalService: GeneralService, + private route: ActivatedRoute, + private projectService: ProjectService + ) {} authDetails() { const authDetails = localStorage.getItem('CONVOY_AUTH_TOKENS'); @@ -102,8 +107,8 @@ export class HttpService { const http = this.setupAxios({ hideNotification: requestDetails.hideNotification }); const requestHeader = { - Authorization: `Bearer ${this.token || this.authDetails()?.access_token}`, - 'X-Convoy-Version': '2024-04-01' + Authorization: `Bearer ${this.token || this.authDetails()?.access_token}` + // 'X-Convoy-Version': '2024-04-01' }; // process URL From 90b24f463d3c1f972a0e3ca6b19adbdc6adb36f7 Mon Sep 17 00:00:00 2001 From: Orim Dominic Adah Date: Sun, 16 Mar 2025 20:56:54 +0100 Subject: [PATCH 26/43] feat: project endpoint config (#2267) --- web/ui/dashboard-react/package.json | 1 + .../src/app/projects_/$projectId/settings.tsx | 152 ++++++++++++++++-- .../src/components/ui/switch.tsx | 27 ++++ 3 files changed, 166 insertions(+), 14 deletions(-) create mode 100644 web/ui/dashboard-react/src/components/ui/switch.tsx diff --git a/web/ui/dashboard-react/package.json b/web/ui/dashboard-react/package.json index e145fe4f26..fa00390cb7 100644 --- a/web/ui/dashboard-react/package.json +++ b/web/ui/dashboard-react/package.json @@ -24,6 +24,7 @@ "@radix-ui/react-select": "^2.1.6", "@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-switch": "^1.1.3", "@radix-ui/react-tabs": "^1.1.3", "@radix-ui/react-tooltip": "^1.1.8", "@tanstack/react-router": "^1.106.0", diff --git a/web/ui/dashboard-react/src/app/projects_/$projectId/settings.tsx b/web/ui/dashboard-react/src/app/projects_/$projectId/settings.tsx index dda60e9ebb..882e34eddc 100644 --- a/web/ui/dashboard-react/src/app/projects_/$projectId/settings.tsx +++ b/web/ui/dashboard-react/src/app/projects_/$projectId/settings.tsx @@ -12,6 +12,7 @@ import { FormItem, FormLabel, FormControl, + FormDescription, FormMessageWithErrorIcon, } from '@/components/ui/form'; import { @@ -55,6 +56,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; +import { Switch } from '@/components/ui/switch'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { DashboardLayout } from '@/components/dashboard'; @@ -71,6 +73,21 @@ import type { Project } from '@/models/project.model'; import warningAnimation from '../../../../assets/img/warning-animation.gif'; +export const Route = createFileRoute('/projects_/$projectId/settings')({ + beforeLoad({ context }) { + ensureCanAccessPrivatePages(context.auth?.getTokens().isLoggedIn); + }, + async loader({ params }) { + const project = await projectsService.getProject(params.projectId); + // TODO handle error, and in other loaders too + const canManageProject = await authService.ensureUserCanAccess( + 'Project Settings|MANAGE', + ); + return { project, canManageProject }; + }, + component: ProjectSettings, +}); + const ProjectConfigFormSchema = z.object({ name: z .string({ @@ -1372,21 +1389,128 @@ function SignatureHistoryConfig(props: { ); } -export const Route = createFileRoute('/projects_/$projectId/settings')({ - beforeLoad({ context }) { - ensureCanAccessPrivatePages(context.auth?.getTokens().isLoggedIn); - }, - async loader({ params }) { - const project = await projectsService.getProject(params.projectId); - // TODO handle error, and in other loaders too - const canManageProject = await authService.ensureUserCanAccess( - 'Project Settings|MANAGE', - ); - return { project, canManageProject }; - }, - component: ProjectSettings, +const EndpointsConfigFormSchema = z.object({ + disable_endpoint: z.boolean(), + enforce_secure_endpoints: z.boolean(), }); +function EndpointsConfig(props: { + project: Project; + canManageProject: boolean; +}) { + const { project, canManageProject } = props; + const [isUpdating, setIsUpdating] = useState(false); + const { setProject, setProjects, projects } = useProjectStore(); + + const form = useForm>({ + resolver: zodResolver(EndpointsConfigFormSchema), + defaultValues: { + disable_endpoint: project.config.disable_endpoint, + enforce_secure_endpoints: project.config.ssl.enforce_secure_endpoints, + }, + }); + + async function updateDisableEndpoint(value: boolean) { + setIsUpdating(true); + try { + project.config.disable_endpoint = value; + // @ts-expect-error this works + const updated = await projectsService.updateProject(project); + setProjects(projects.map(p => (p.uid == updated.uid ? updated : p))); + setProject(updated); + } catch (error) { + console.error(error); + } finally { + setIsUpdating(false); + } + } + + async function updateEnforceSecureEndpoints(value: boolean) { + setIsUpdating(true); + try { + project.config.ssl.enforce_secure_endpoints = value; + // @ts-expect-error this works + const updated = await projectsService.updateProject(project); + setProjects(projects.map(p => (p.uid == updated.uid ? updated : p))); + setProject(updated); + } catch (error) { + console.error(error); + } finally { + setIsUpdating(false); + } + } + + return ( +
      +

      Endpoint Configurations

      + +
      + +
      +
      + ( + +
      + + Disable Failing Endpoint + + + Toggling this configuration on will automatically + disable all endpoints in this project with failure + response until requests to them are manually retried + +
      + + { + field.onChange(e); + await updateDisableEndpoint(!field.value); + }} + /> + +
      + )} + /> + ( + +
      + + Allow Only HTTPS Secure Endpoints + + + Toggling this will allow only HTTPS secure endpoints, + this allows only TLS connections to your endpoints. + +
      + + { + field.onChange(e); + await updateEnforceSecureEndpoints(!field.value); + }} + /> + +
      + )} + /> +
      +
      +
      + +
      + ); +} + const tabs = [ { name: 'Project', @@ -1406,7 +1530,7 @@ const tabs = [ name: 'Endpoints', value: 'endpoints', icon: User, - component: ProjectConfig, + component: EndpointsConfig, projectTypes: ['incoming', 'outgoing'], }, { diff --git a/web/ui/dashboard-react/src/components/ui/switch.tsx b/web/ui/dashboard-react/src/components/ui/switch.tsx new file mode 100644 index 0000000000..455c23b6cd --- /dev/null +++ b/web/ui/dashboard-react/src/components/ui/switch.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import * as SwitchPrimitives from "@radix-ui/react-switch" + +import { cn } from "@/lib/utils" + +const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +Switch.displayName = SwitchPrimitives.Root.displayName + +export { Switch } From 87fa88f6e5e24a7b35665103cfe8812f68fabae2 Mon Sep 17 00:00:00 2001 From: Orim Dominic Adah Date: Mon, 17 Mar 2025 20:14:39 +0100 Subject: [PATCH 27/43] feat: project meta events config (#2268) --- .../src/app/projects_/$projectId/settings.tsx | 387 +++++++++++++++++- .../src/models/project.model.ts | 15 +- 2 files changed, 379 insertions(+), 23 deletions(-) diff --git a/web/ui/dashboard-react/src/app/projects_/$projectId/settings.tsx b/web/ui/dashboard-react/src/app/projects_/$projectId/settings.tsx index 882e34eddc..fb5a43b3ee 100644 --- a/web/ui/dashboard-react/src/app/projects_/$projectId/settings.tsx +++ b/web/ui/dashboard-react/src/app/projects_/$projectId/settings.tsx @@ -1,10 +1,10 @@ import { z } from 'zod'; -import { useState } from 'react'; import { useForm } from 'react-hook-form'; +import { useState, useCallback, useMemo } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { createFileRoute, useNavigate } from '@tanstack/react-router'; -import { Bot, Home, Settings, User, Plus } from 'lucide-react'; +import { Bot, Home, Settings, User, Plus, X } from 'lucide-react'; import { Form, @@ -15,6 +15,7 @@ import { FormDescription, FormMessageWithErrorIcon, } from '@/components/ui/form'; +import { Command as CommandPrimitive } from 'cmdk'; import { Sheet, SheetContent, @@ -56,23 +57,34 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; +import { + Command, + CommandGroup, + CommandItem, + CommandList, +} from '@/components/ui/command'; +import { Badge } from '@/components/ui/badge'; import { Switch } from '@/components/ui/switch'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { DashboardLayout } from '@/components/dashboard'; import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; + import { cn } from '@/lib/utils'; import { toMMMDDYYYY } from '@/lib/pipes'; import { useProjectStore } from '@/store/index'; import { ensureCanAccessPrivatePages } from '@/lib/auth'; import * as authService from '@/services/auth.service'; import * as projectsService from '@/services/projects.service'; +import { EventTypes } from '@/models/project.model'; -import type { Project } from '@/models/project.model'; +import type { KeyboardEvent } from 'react'; +import type { EventType, Project } from '@/models/project.model'; import warningAnimation from '../../../../assets/img/warning-animation.gif'; + export const Route = createFileRoute('/projects_/$projectId/settings')({ beforeLoad({ context }) { ensureCanAccessPrivatePages(context.auth?.getTokens().isLoggedIn); @@ -1399,7 +1411,12 @@ function EndpointsConfig(props: { canManageProject: boolean; }) { const { project, canManageProject } = props; - const [isUpdating, setIsUpdating] = useState(false); + const [isUpdatingDisableEndpoint, setIsUpdatingDisableEndpoint] = + useState(false); + const [ + isUpdatingEnforceSecureEndpoints, + setIsUpdatingEnforceSecureEndpoints, + ] = useState(false); const { setProject, setProjects, projects } = useProjectStore(); const form = useForm>({ @@ -1411,7 +1428,7 @@ function EndpointsConfig(props: { }); async function updateDisableEndpoint(value: boolean) { - setIsUpdating(true); + setIsUpdatingDisableEndpoint(true); try { project.config.disable_endpoint = value; // @ts-expect-error this works @@ -1421,12 +1438,12 @@ function EndpointsConfig(props: { } catch (error) { console.error(error); } finally { - setIsUpdating(false); + setIsUpdatingDisableEndpoint(false); } } async function updateEnforceSecureEndpoints(value: boolean) { - setIsUpdating(true); + setIsUpdatingEnforceSecureEndpoints(true); try { project.config.ssl.enforce_secure_endpoints = value; // @ts-expect-error this works @@ -1436,7 +1453,7 @@ function EndpointsConfig(props: { } catch (error) { console.error(error); } finally { - setIsUpdating(false); + setIsUpdatingEnforceSecureEndpoints(false); } } @@ -1453,19 +1470,21 @@ function EndpointsConfig(props: { name="disable_endpoint" render={({ field }) => ( -
      +
      Disable Failing Endpoint + + Toggling this configuration on will automatically + disable all endpoints in this project with failure + response until requests to them are manually retried + - - Toggling this configuration on will automatically - disable all endpoints in this project with failure - response until requests to them are manually retried -
      { field.onChange(e); @@ -1476,23 +1495,26 @@ function EndpointsConfig(props: { )} /> + ( -
      +
      Allow Only HTTPS Secure Endpoints + + Toggling this will allow only HTTPS secure endpoints, + this allows only TLS connections to your endpoints. + - - Toggling this will allow only HTTPS secure endpoints, - this allows only TLS connections to your endpoints. -
      { field.onChange(e); @@ -1511,6 +1533,327 @@ function EndpointsConfig(props: { ); } +const MetaEventsConfigFormSchema = z + .object({ + is_enabled: z.boolean(), + secret: z.string().transform(v => (v ? v : '')), + url: z.string().transform(v => (v ? v : '')), + event_type: z + .array(z.enum(EventTypes), { + message: 'Event type(s) is required', + }) + .nullable(), + }) + .refine( + config => { + if (!config.is_enabled) return true; + + return !!config.url?.length && !!config.event_type?.length; + }, + config => { + if (!config.url?.length) { + return { + message: 'URL is required', + path: ['url'], + }; + } + + if (!config.event_type?.length) { + return { + message: 'At least one event type is required', + path: ['event_type'], + }; + } + + return { message: '' }; + }, + ) + .transform(config => { + if (!config.is_enabled) { + return { + is_enabled: config.is_enabled, + secret: '', + url: '', + event_type: null, + }; + } + + return config; + }); + +function MetaEventsConfig(props: { + project: Project; + canManageProject: boolean; +}) { + const { project, canManageProject } = props; + const [isUpdating, setIsUpdating] = useState(false); + const { setProject, setProjects, projects } = useProjectStore(); + + const [isMultiSelectOpen, setIsMultiSelectOpen] = useState(false); + const [selectedEventTypes, setSelectedEventTypes] = useState( + project.config.meta_event.event_type ?? [], + ); + const [inputValue, setInputValue] = useState(''); + const handleUnselect = useCallback((eventType: EventType) => { + setSelectedEventTypes(prev => prev.filter(s => s !== eventType)); + }, []); + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Backspace' && selectedEventTypes.length > 0) { + setSelectedEventTypes(prev => prev.slice(0, -1)); + return true; + } + return false; + }, + [selectedEventTypes], + ); + const filteredEventTypes = useMemo( + () => + EventTypes.filter(eventType => !selectedEventTypes.includes(eventType)), + [selectedEventTypes], + ); + + const form = useForm>({ + resolver: zodResolver(MetaEventsConfigFormSchema), + defaultValues: { + is_enabled: project.config.meta_event.is_enabled, + url: project.config.meta_event.url, + secret: project.config.meta_event.secret, + event_type: + project.config.meta_event.event_type === null + ? [] + : project.config.meta_event.event_type, + }, + }); + + const isEventTypeEnabled = form.watch('is_enabled'); + + async function updateMetaEventsConfig( + metaEvent: z.infer, + ) { + setIsUpdating(true); + try { + project.config.meta_event = { ...metaEvent, type: '' }; + // @ts-expect-error this works + const updated = await projectsService.updateProject(project); + setProjects(projects.map(p => (p.uid == updated.uid ? updated : p))); + setProject(updated); + } catch (error) { + console.error(error); + } finally { + setIsUpdating(false); + } + } + + return ( +
      +

      Meta Event Configurations

      + +
      + + form.handleSubmit(updateMetaEventsConfig)(...args) + } + > +
      + ( + +
      + + Enable Meta Events + + + Meta events allows you to receive webhook events based on + events happening in your projects including webhook event + activities. + +
      + + + +
      + )} + /> +
      +
      + + {isEventTypeEnabled && ( +
      +

      + Meta Events Configurations +

      + ( + +
      + + Webhook URL + +
      + + + + +
      + )} + /> + + ( + +
      + + Endpoint Secret + +
      + + + + +
      + )} + /> + + ( + +
      + + Select events to listen to + +
      + +
      +
      + {selectedEventTypes.map(eventType => { + return ( + + {eventType} + { + e.preventDefault(); + }} + onClick={() => { + handleUnselect(eventType); + field.onChange( + selectedEventTypes.filter( + s => s !== eventType, + ), + ); + }} + /> + + ); + })} + { + const isRemoveAction = handleKeyDown(e); + if (isRemoveAction) { + field.onChange(selectedEventTypes.slice(0, -1)); + } + }} + onValueChange={setInputValue} + value={inputValue} + onBlur={() => setIsMultiSelectOpen(false)} + onFocus={() => setIsMultiSelectOpen(true)} + placeholder="" + className="ml-2 flex-1 bg-transparent outline-none placeholder:text-muted-foreground" + /> +
      +
      +
      + + {isMultiSelectOpen && !!filteredEventTypes.length && ( +
      + + {filteredEventTypes.map(eventType => { + return ( + { + e.preventDefault(); + }} + onSelect={() => { + setInputValue(''); + setSelectedEventTypes(prev => { + field.onChange([...prev, eventType]); + return [...prev, eventType]; + }); + }} + className={'cursor-pointer'} + > + {eventType} + + ); + })} + +
      + )} +
      +
      +
      + +
      + )} + /> +
      + )} + +
      + +
      +
      + +
      + ); +} + const tabs = [ { name: 'Project', @@ -1537,7 +1880,7 @@ const tabs = [ name: 'Meta Events', value: 'meta-events', icon: Bot, - component: ProjectConfig, + component: MetaEventsConfig, projectTypes: ['incoming', 'outgoing'], }, { diff --git a/web/ui/dashboard-react/src/models/project.model.ts b/web/ui/dashboard-react/src/models/project.model.ts index 617514015d..63a2ed8468 100644 --- a/web/ui/dashboard-react/src/models/project.model.ts +++ b/web/ui/dashboard-react/src/models/project.model.ts @@ -34,11 +34,13 @@ export interface Project { }; ssl: Ssl; meta_event: { - event_type: string[] | null; + // FIXME I think `event_type` should be `event_types` or `types` + event_type: Array | null; is_enabled: boolean; secret: string; type: string; url: string; + // pub_sub: null; // It's in the backend but not in the frontend model }; multiple_endpoint_subscriptions: boolean; // retention_policy: { @@ -147,3 +149,14 @@ export interface Version { encoding: string; created_at: string; } + +export const EventTypes = [ + 'endpoint.created', + 'endpoint.deleted', + 'endpoint.updated', + 'eventdelivery.success', + 'eventdelivery.failed', + 'project.updated', +] as const; + +export type EventType = typeof EventTypes[number] From bc976fb8002bc708a81556ae724c30add6344c6a Mon Sep 17 00:00:00 2001 From: Orim Dominic Date: Tue, 18 Mar 2025 15:31:44 +0100 Subject: [PATCH 28/43] chore: remove keys --- configs/local/convoy.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/configs/local/convoy.json b/configs/local/convoy.json index 58c10c6caf..63c498e4b1 100644 --- a/configs/local/convoy.json +++ b/configs/local/convoy.json @@ -1,5 +1,5 @@ { - "host": "*", + "host": "localhost:5005", "database": { "host": "pgbouncer", "username": "convoy", @@ -26,6 +26,5 @@ "prometheus", "circuit-breaker", "full-text-search" - ], - "license_key": "CD2A07-0199FB-AD5CA1-536859-09CC01-V3" + ] } From 32661d33b29d23f2218537492f9b89430d68f842 Mon Sep 17 00:00:00 2001 From: Orim Dominic Adah Date: Tue, 18 Mar 2025 17:24:47 +0100 Subject: [PATCH 29/43] feat: project event types config (#2271) * feat: create event type * feat: display list of event types * feat: deprecate event type --- .../assets/img/events-log-empty-state.png | Bin 0 -> 7526 bytes .../src/app/projects_/$projectId/settings.tsx | 483 +++++++++++++++++- .../src/models/project.model.ts | 16 +- .../src/services/projects.service.ts | 51 +- 4 files changed, 533 insertions(+), 17 deletions(-) create mode 100644 web/ui/dashboard-react/assets/img/events-log-empty-state.png diff --git a/web/ui/dashboard-react/assets/img/events-log-empty-state.png b/web/ui/dashboard-react/assets/img/events-log-empty-state.png new file mode 100644 index 0000000000000000000000000000000000000000..7a51e744777f50e06c77d4ac4a5dd850c4881909 GIT binary patch literal 7526 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 O@I*uJQON_lfBpx}vZpHm literal 0 HcmV?d00001 diff --git a/web/ui/dashboard-react/src/app/projects_/$projectId/settings.tsx b/web/ui/dashboard-react/src/app/projects_/$projectId/settings.tsx index fb5a43b3ee..c21ea1dce4 100644 --- a/web/ui/dashboard-react/src/app/projects_/$projectId/settings.tsx +++ b/web/ui/dashboard-react/src/app/projects_/$projectId/settings.tsx @@ -4,7 +4,16 @@ import { useState, useCallback, useMemo } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { createFileRoute, useNavigate } from '@tanstack/react-router'; -import { Bot, Home, Settings, User, Plus, X } from 'lucide-react'; +import { + Bot, + Home, + Settings, + User, + Plus, + X, + PencilLine, + Trash2, +} from 'lucide-react'; import { Form, @@ -70,20 +79,20 @@ import { Button } from '@/components/ui/button'; import { DashboardLayout } from '@/components/dashboard'; import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; - import { cn } from '@/lib/utils'; import { toMMMDDYYYY } from '@/lib/pipes'; import { useProjectStore } from '@/store/index'; import { ensureCanAccessPrivatePages } from '@/lib/auth'; import * as authService from '@/services/auth.service'; import * as projectsService from '@/services/projects.service'; -import { EventTypes } from '@/models/project.model'; +import * as licenseService from '@/services/licenses.service' +import { MetaEventTypes } from '@/models/project.model'; import type { KeyboardEvent } from 'react'; -import type { EventType, Project } from '@/models/project.model'; +import type { EventType, MetaEventType, Project } from '@/models/project.model'; import warningAnimation from '../../../../assets/img/warning-animation.gif'; - +import eventTypesEmptyState from '../../../../assets/img/events-log-empty-state.png'; export const Route = createFileRoute('/projects_/$projectId/settings')({ beforeLoad({ context }) { @@ -95,7 +104,14 @@ export const Route = createFileRoute('/projects_/$projectId/settings')({ const canManageProject = await authService.ensureUserCanAccess( 'Project Settings|MANAGE', ); - return { project, canManageProject }; + + const licenses = await licenseService.getLicenses() + + const { event_types } = await projectsService.getEventTypes( + params.projectId, + ); + + return { project, canManageProject, eventTypes: event_types, hasAdvancedSubscriptions: licenses.includes('ADVANCED_SUBSCRIPTIONS') }; }, component: ProjectSettings, }); @@ -1539,7 +1555,7 @@ const MetaEventsConfigFormSchema = z secret: z.string().transform(v => (v ? v : '')), url: z.string().transform(v => (v ? v : '')), event_type: z - .array(z.enum(EventTypes), { + .array(z.enum(MetaEventTypes), { message: 'Event type(s) is required', }) .nullable(), @@ -1590,11 +1606,11 @@ function MetaEventsConfig(props: { const { setProject, setProjects, projects } = useProjectStore(); const [isMultiSelectOpen, setIsMultiSelectOpen] = useState(false); - const [selectedEventTypes, setSelectedEventTypes] = useState( + const [selectedEventTypes, setSelectedEventTypes] = useState( project.config.meta_event.event_type ?? [], ); const [inputValue, setInputValue] = useState(''); - const handleUnselect = useCallback((eventType: EventType) => { + const handleUnselect = useCallback((eventType: MetaEventType) => { setSelectedEventTypes(prev => prev.filter(s => s !== eventType)); }, []); const handleKeyDown = useCallback( @@ -1609,7 +1625,9 @@ function MetaEventsConfig(props: { ); const filteredEventTypes = useMemo( () => - EventTypes.filter(eventType => !selectedEventTypes.includes(eventType)), + MetaEventTypes.filter( + eventType => !selectedEventTypes.includes(eventType), + ), [selectedEventTypes], ); @@ -1854,6 +1872,445 @@ function MetaEventsConfig(props: { ); } +const NewEventTypeFormSchema = z.object({ + name: z.string().min(1, 'Name is required'), + category: z.string().optional(), + description: z.string().optional(), +}); + +function EventTypesConfig(props: { + project: Project; + canManageProject: boolean; + hasAdvancedSubscriptions: boolean; + eventTypes: Array; +}) { + const { eventTypes, canManageProject, project , hasAdvancedSubscriptions} = props; + const [isCreating, setIsCreating] = useState(false); + const [isDeprecating, setIsDeprecating] = useState(false) + const [_eventTypes, set_eventTypes] = useState(eventTypes); + + const form = useForm>({ + resolver: zodResolver(NewEventTypeFormSchema), + defaultValues: { + name: '', + category: '', + description: '', + }, + mode: 'onTouched', + }); + + async function createNewEventType( + eventType: z.infer, + ) { + setIsCreating(true); + try { + const created = await projectsService.createEventType( + project.uid, + eventType, + ); + set_eventTypes(prev => prev.concat(created)); + // TODO should close the sheet here + } catch (error) { + console.error(error); + } finally { + setIsCreating(false); + } + } + + async function deprecateEventType(eventTypeUid:string) { + setIsDeprecating(true) + try { + await projectsService.deprecateEventType(eventTypeUid) + const {event_types} = await projectsService.getEventTypes(project.uid) + set_eventTypes(event_types) + + } catch (error) { + console.error(error) + } finally { + setIsDeprecating(false) + } + } + return ( +
      +
      +

      Event Types

      + + {/* This sheet is duplicated. Keep it single */} + + + + + + + New Event Type + + Add a new event type + + +
      +
      +
      + + void form.handleSubmit(createNewEventType)(...args) + } + > +
      + ( + +
      + + Name + +
      + + + + +
      + )} + /> + + ( + +
      + + Category + +
      + + + + +
      + )} + /> + + ( + +
      + + Description + +
      + + + + +
      + )} + /> +
      +
      + + + + + +
      +
      + +
      +
      +
      +
      + +
      + {/* TODO include empty state */} + {_eventTypes.length == 0 ? ( +
      + No event types created +

      + You currently do not have any event types +

      +

      + Event types will be listed here when available +

      + {/* This sheet is duplicated. Keep it single */} + + + + + + + New Event Type + + Add a new event type + + +
      +
      +
      + + void form.handleSubmit(createNewEventType)(...args) + } + > +
      + ( + +
      + + Name + +
      + + + + +
      + )} + /> + + ( + +
      + + Category + +
      + + + + +
      + )} + /> + + ( + +
      + + Description + +
      + + + + +
      + )} + /> +
      +
      + + + + + +
      +
      + +
      +
      +
      +
      + ) : ( +
      + + + {project.name} Event types + + + + + event type + + + category + + + description + + + + + + {_eventTypes.map(et => { + return ( + + + + {et.name} + + + {et.deprecated_at ? 'deprecated' : 'active'} + + + + {et.category || '-'} + + + {et.description || '-'} + + + + + + + ); + })} + +
      +
      + )} +
      +
      + ); +} + const tabs = [ { name: 'Project', @@ -1887,7 +2344,7 @@ const tabs = [ name: 'Event Types', value: 'event-types', icon: Home, - component: ProjectConfig, + component: EventTypesConfig, projectTypes: ['outgoing'], }, { @@ -1900,7 +2357,7 @@ const tabs = [ ]; function ProjectSettings() { - const { project, canManageProject } = Route.useLoaderData(); + const { project, canManageProject, eventTypes, hasAdvancedSubscriptions } = Route.useLoaderData(); return ( @@ -1933,6 +2390,8 @@ function ProjectSettings() { ))} diff --git a/web/ui/dashboard-react/src/models/project.model.ts b/web/ui/dashboard-react/src/models/project.model.ts index 63a2ed8468..f141d48d2f 100644 --- a/web/ui/dashboard-react/src/models/project.model.ts +++ b/web/ui/dashboard-react/src/models/project.model.ts @@ -35,7 +35,7 @@ export interface Project { ssl: Ssl; meta_event: { // FIXME I think `event_type` should be `event_types` or `types` - event_type: Array | null; + event_type: Array<(typeof MetaEventTypes)[number]> | null; is_enabled: boolean; secret: string; type: string; @@ -150,13 +150,21 @@ export interface Version { created_at: string; } -export const EventTypes = [ +export const MetaEventTypes = [ 'endpoint.created', 'endpoint.deleted', 'endpoint.updated', 'eventdelivery.success', 'eventdelivery.failed', 'project.updated', -] as const; +] as const; -export type EventType = typeof EventTypes[number] +export type MetaEventType = (typeof MetaEventTypes)[number]; + +export type EventType = { + uid: string; + name: string; + category: string; + description: string; + deprecated_at: null | string; +}; diff --git a/web/ui/dashboard-react/src/services/projects.service.ts b/web/ui/dashboard-react/src/services/projects.service.ts index b968cb2e8f..e03f78b1ce 100644 --- a/web/ui/dashboard-react/src/services/projects.service.ts +++ b/web/ui/dashboard-react/src/services/projects.service.ts @@ -1,6 +1,10 @@ import { request } from '@/services/http.service'; -import type { Project, CreateProjectResponse } from '@/models/project.model'; +import type { + Project, + CreateProjectResponse, + EventType, +} from '@/models/project.model'; type CreateProjectParams = { name: string; @@ -127,3 +131,48 @@ export async function deleteProject( return res.data; } + +export async function getEventTypes( + _uid: string, + deps: { httpReq: typeof request } = { httpReq: request }, +) { + const res = await deps.httpReq<{ event_types: Array }>({ + method: 'get', + url: '/event-types', + level: 'org_project', + }); + + return res.data; +} + +type CreateEventTypeParams = { + name: string; + category?: string; + description?: string; +}; + +export async function createEventType( + _uid: string, + eventType: CreateEventTypeParams, + deps: { httpReq: typeof request } = { httpReq: request }, +) { + const res = await deps.httpReq<{ event_type: EventType }>({ + url: `/event-types`, + method: 'post', + body: eventType, + level: 'org_project', + }); + + return res.data.event_type; +} + +export async function deprecateEventType(uid:string, deps: { httpReq: typeof request } = { httpReq: request },) { + const res = await deps.httpReq({ + url: `/event-types/${uid}/deprecate`, + method: 'post', + body: {}, + level: 'org_project' + }) + + return res; +} From 84479416e630ef5089ea2b06ba9f484fdfc53e99 Mon Sep 17 00:00:00 2001 From: Orim Dominic Adah Date: Wed, 19 Mar 2025 02:46:28 +0100 Subject: [PATCH 30/43] feat: project secrets config (#2272) --- .../src/app/projects_/$projectId/settings.tsx | 205 ++++++++++++++++-- .../src/services/http.service.ts | 2 +- .../src/services/projects.service.ts | 23 +- 3 files changed, 208 insertions(+), 22 deletions(-) diff --git a/web/ui/dashboard-react/src/app/projects_/$projectId/settings.tsx b/web/ui/dashboard-react/src/app/projects_/$projectId/settings.tsx index c21ea1dce4..40ca3be212 100644 --- a/web/ui/dashboard-react/src/app/projects_/$projectId/settings.tsx +++ b/web/ui/dashboard-react/src/app/projects_/$projectId/settings.tsx @@ -13,6 +13,8 @@ import { X, PencilLine, Trash2, + CopyIcon, + RefreshCcw, } from 'lucide-react'; import { @@ -85,12 +87,13 @@ import { useProjectStore } from '@/store/index'; import { ensureCanAccessPrivatePages } from '@/lib/auth'; import * as authService from '@/services/auth.service'; import * as projectsService from '@/services/projects.service'; -import * as licenseService from '@/services/licenses.service' +import * as licenseService from '@/services/licenses.service'; import { MetaEventTypes } from '@/models/project.model'; import type { KeyboardEvent } from 'react'; import type { EventType, MetaEventType, Project } from '@/models/project.model'; +import successAnimation from '../../../../assets/img/success.gif'; import warningAnimation from '../../../../assets/img/warning-animation.gif'; import eventTypesEmptyState from '../../../../assets/img/events-log-empty-state.png'; @@ -105,13 +108,18 @@ export const Route = createFileRoute('/projects_/$projectId/settings')({ 'Project Settings|MANAGE', ); - const licenses = await licenseService.getLicenses() + const licenses = await licenseService.getLicenses(); const { event_types } = await projectsService.getEventTypes( params.projectId, ); - return { project, canManageProject, eventTypes: event_types, hasAdvancedSubscriptions: licenses.includes('ADVANCED_SUBSCRIPTIONS') }; + return { + project, + canManageProject, + eventTypes: event_types, + hasAdvancedSubscriptions: licenses.includes('ADVANCED_SUBSCRIPTIONS'), + }; }, component: ProjectSettings, }); @@ -1884,9 +1892,10 @@ function EventTypesConfig(props: { hasAdvancedSubscriptions: boolean; eventTypes: Array; }) { - const { eventTypes, canManageProject, project , hasAdvancedSubscriptions} = props; + const { eventTypes, canManageProject, project, hasAdvancedSubscriptions } = + props; const [isCreating, setIsCreating] = useState(false); - const [isDeprecating, setIsDeprecating] = useState(false) + const [isDeprecating, setIsDeprecating] = useState(false); const [_eventTypes, set_eventTypes] = useState(eventTypes); const form = useForm>({ @@ -1917,17 +1926,16 @@ function EventTypesConfig(props: { } } - async function deprecateEventType(eventTypeUid:string) { - setIsDeprecating(true) + async function deprecateEventType(eventTypeUid: string) { + setIsDeprecating(true); try { - await projectsService.deprecateEventType(eventTypeUid) - const {event_types} = await projectsService.getEventTypes(project.uid) - set_eventTypes(event_types) - + await projectsService.deprecateEventType(eventTypeUid); + const { event_types } = await projectsService.getEventTypes(project.uid); + set_eventTypes(event_types); } catch (error) { - console.error(error) + console.error(error); } finally { - setIsDeprecating(false) + setIsDeprecating(false); } } return ( @@ -2282,16 +2290,22 @@ function EventTypesConfig(props: { +
      +
      + {/* [ ] shpuld propably use an here */} + + + + + {/* TODO: Maybe change the copy of this */} + + + Confirm Action + + + You are about to regenerate a new API Key for this project + + + + + + + + + + + + +
      +
      + + + + + + + warning + + Project Created Successfully + + +
      +
      + Your API Key has also been created. + Please copy this key and save it somewhere safe. +
      + +
      + + {apiKey} + + +
      +
      +
      + + + + + +
      +
      + + ); +} + const tabs = [ { name: 'Project', @@ -2351,13 +2519,14 @@ const tabs = [ name: 'Secrets', value: 'secrets', icon: Settings, - component: ProjectConfig, + component: SecretsConfig, projectTypes: ['incoming', 'outgoing'], }, ]; function ProjectSettings() { - const { project, canManageProject, eventTypes, hasAdvancedSubscriptions } = Route.useLoaderData(); + const { project, canManageProject, eventTypes, hasAdvancedSubscriptions } = + Route.useLoaderData(); return ( diff --git a/web/ui/dashboard-react/src/services/http.service.ts b/web/ui/dashboard-react/src/services/http.service.ts index 963f731686..2ebe39714d 100644 --- a/web/ui/dashboard-react/src/services/http.service.ts +++ b/web/ui/dashboard-react/src/services/http.service.ts @@ -176,7 +176,7 @@ export async function request( | undefined | null | Record - >; + > | null; method: 'get' | 'post' | 'delete' | 'put'; hideNotification?: boolean; query?: Record< diff --git a/web/ui/dashboard-react/src/services/projects.service.ts b/web/ui/dashboard-react/src/services/projects.service.ts index e03f78b1ce..ce91dae3c4 100644 --- a/web/ui/dashboard-react/src/services/projects.service.ts +++ b/web/ui/dashboard-react/src/services/projects.service.ts @@ -166,13 +166,30 @@ export async function createEventType( return res.data.event_type; } -export async function deprecateEventType(uid:string, deps: { httpReq: typeof request } = { httpReq: request },) { +export async function deprecateEventType( + uid: string, + deps: { httpReq: typeof request } = { httpReq: request }, +) { const res = await deps.httpReq({ url: `/event-types/${uid}/deprecate`, method: 'post', body: {}, - level: 'org_project' - }) + level: 'org_project', + }); return res; } + +export async function regenerateAPIKey( + _projectId: string, + deps: { httpReq: typeof request } = { httpReq: request }, +) { + const res = await deps.httpReq<{key:string, uid: string}>({ + url: `/security/keys/regenerate`, + method: 'put', + body: null, + level: 'org_project', + }); + + return res.data; +} From 54fece484b4e129c9dc98ac5484f8b1ddf40ada3 Mon Sep 17 00:00:00 2001 From: Orim Dominic Adah Date: Thu, 20 Mar 2025 22:01:20 +0100 Subject: [PATCH 31/43] feat: create endpoint (#2274) --- .../projects_/$projectId/endpoints/new.tsx | 907 ++++++++++++++++++ .../src/components/dashboard.tsx | 2 +- .../src/models/endpoint.model.ts | 77 ++ web/ui/dashboard-react/src/routes.gen.ts | 29 +- .../src/services/endpoints.service.ts | 71 ++ 5 files changed, 1084 insertions(+), 2 deletions(-) create mode 100644 web/ui/dashboard-react/src/app/projects_/$projectId/endpoints/new.tsx create mode 100644 web/ui/dashboard-react/src/models/endpoint.model.ts create mode 100644 web/ui/dashboard-react/src/services/endpoints.service.ts diff --git a/web/ui/dashboard-react/src/app/projects_/$projectId/endpoints/new.tsx b/web/ui/dashboard-react/src/app/projects_/$projectId/endpoints/new.tsx new file mode 100644 index 0000000000..0cd77d470d --- /dev/null +++ b/web/ui/dashboard-react/src/app/projects_/$projectId/endpoints/new.tsx @@ -0,0 +1,907 @@ +import { useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; +import { useParams, createFileRoute, Link } from '@tanstack/react-router'; +import { toast } from 'sonner'; +import { useNavigate } from '@tanstack/react-router'; + +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/tooltip'; +import { DashboardLayout } from '@/components/dashboard'; +import { + Form, + FormField, + FormItem, + FormLabel, + FormControl, + FormMessageWithErrorIcon, +} from '@/components/ui/form'; + +import * as authService from '@/services/auth.service'; +import { endpointsService } from '@/services/endpoints.service'; +import { useLicenseStore, useProjectStore } from '@/store/index'; +import type { EndpointFormValues } from '@/models/endpoint.model'; +import { ensureCanAccessPrivatePages } from '@/lib/auth'; +import { cn } from '@/lib/utils'; + +// Import back button icon +import modalCloseIcon from '../../../../../assets/svg/modal-close-icon.svg'; + +// Schema for form validation +const endpointSchema = z.object({ + name: z.string().min(1, { message: 'Please provide a name' }), + url: z.string().url({ message: 'Invalid endpoint URL' }), + support_email: z + .string() + .email({ message: 'Email is invalid' }) + .optional() + .or(z.literal('')), + slack_webhook_url: z + .string() + .url({ message: 'URL is invalid' }) + .optional() + .or(z.literal('')), + secret: z.string().nullable(), + http_timeout: z.number().nullable(), + description: z.string().nullable(), + owner_id: z.string().nullable(), + rate_limit: z.number().nullable(), + rate_limit_duration: z.number().nullable(), + authentication: z + .object({ + type: z.string(), + api_key: z.object({ + header_name: z.string(), + header_value: z.string(), + }), + }) + .optional(), + advanced_signatures: z.boolean().nullable(), +}); + +type ConfigItem = { + uid: string; + name: string; + show: boolean; + deleted: boolean; +}; + +export const Route = createFileRoute('/projects_/$projectId/endpoints/new')({ + beforeLoad({ context }) { + ensureCanAccessPrivatePages(context.auth?.getTokens().isLoggedIn); + }, + loader: async () => { + const perms = await authService.getUserPermissions(); + + return { + canManageEndpoints: perms.includes('Endpoints|MANAGE'), + }; + }, + component: CreateEndpointPage, +}); + +function CreateEndpointPage() { + // Router and navigation hooks + const navigate = useNavigate(); + const { canManageEndpoints } = Route.useLoaderData(); + const params = useParams({ from: '/projects_/$projectId/endpoints/new' }); + + // Global state from Zustand + const { licenses } = useLicenseStore(); + const { project } = useProjectStore(); + + // State + const [configurations, setConfigurations] = useState( + [ + { uid: 'http_timeout', name: 'Timeout', show: false, deleted: false }, + { uid: 'owner_id', name: 'Owner ID', show: false, deleted: false }, + { uid: 'rate_limit', name: 'Rate Limit', show: false, deleted: false }, + { uid: 'auth', name: 'Auth', show: false, deleted: false }, + { + uid: 'alert_config', + name: 'Notifications', + show: false, + deleted: false, + }, + ].concat( + project?.type === 'outgoing' + ? [ + { + uid: 'signature', + name: 'Signature Format', + show: false, + deleted: false, + }, + ] + : [], + ), + ); + const [isCreating, setIsCreating] = useState(false); + + // Check if user has required license + const hasAdvancedEndpointManagementLicense = + Array.isArray(licenses) && + licenses.includes('ADVANCED_ENDPOINT_MANAGEMENT'); + + // React Hook Form setup + const form = useForm({ + resolver: zodResolver(endpointSchema), + defaultValues: { + name: '', + url: '', + support_email: '', + slack_webhook_url: '', + secret: null, + http_timeout: null, + description: null, + owner_id: null, + rate_limit: null, + rate_limit_duration: null, + authentication: { + type: 'api_key', + api_key: { + header_name: '', + header_value: '', + }, + }, + advanced_signatures: null, + }, + }); + + const { register, setValue, watch, reset, control } = form; + + // Toggle configuration display + const toggleConfigForm = (configValue: string, deleted?: boolean) => { + setConfigurations(prev => + prev.map(config => + config.uid === configValue + ? { ...config, show: !config.show, deleted: deleted ?? false } + : config, + ), + ); + }; + + // Set configuration as deleted + const setConfigFormDeleted = (configValue: string, deleted: boolean) => { + setConfigurations(prev => + prev.map(config => + config.uid === configValue ? { ...config, deleted } : config, + ), + ); + }; + + // Check if configuration is shown + const showConfig = (configValue: string): boolean => { + return ( + configurations.find(config => config.uid === configValue)?.show || false + ); + }; + + // Check if configuration is marked as deleted + const configDeleted = (configValue: string): boolean => { + return ( + configurations.find(config => config.uid === configValue)?.deleted || + false + ); + }; + + // Form submission handler + const saveEndpoint = async (formData: EndpointFormValues) => { + // Handle rate limit deleted case + const rateLimitDeleted = + !showConfig('rate_limit') && configDeleted('rate_limit'); + if (rateLimitDeleted) { + formData.rate_limit = 0; + formData.rate_limit_duration = 0; + setConfigFormDeleted('rate_limit', false); + } + + setIsCreating(true); + + // Clone form data to avoid mutating the original + // Fix type conversion by first going through unknown + const endpointValue = structuredClone(formData) as unknown as Record< + string, + unknown + >; + + // Remove authentication if both fields are empty + if ( + formData.authentication && + !formData.authentication.api_key.header_name && + !formData.authentication.api_key.header_value + ) { + delete endpointValue.authentication; + } + + try { + // Create new endpoint + const response = await endpointsService.addEndpoint(endpointValue); + toast.success(response.message || 'Endpoint created successfully'); + + reset(); + + // Navigate back to endpoints list + navigate({ to: `/projects/${params.projectId}/endpoints` }); + } catch (error) { + toast.error('Failed to save endpoint'); + console.error(error); + } finally { + setIsCreating(false); + } + }; + + return ( + +
      +
      + + back to endpoints + +

      Create Endpoint

      +
      + +

      + An endpoint represents a destination for your webhook events. + Configure your endpoint details below. +

      + +
      +
      + +
      + ( + + + Endpoint Name + + + + + + + )} + /> + + ( + + + Enter URL + + + + + + + )} + /> +
      + +
      + + +
      + + {/* Owner ID Configuration */} + {showConfig('owner_id') && ( +
      +
      +
      + + + + + + + + A unique id for identifying a group of endpoints, + like a user id. Useful for fanning out an event to + multiple endpoints and creating portal link for + multiple endpoints. + + + +
      + +
      + +
      + +
      +
      + )} + + {/* Configuration wrapper to maintain width consistency */} +
      + {/* Rate Limit Configuration */} + {showConfig('rate_limit') && ( +
      +
      +

      + Rate Limit +

      + +
      + +
      + ( + + + Duration + + +
      + { + const value = + e.target.value === '' + ? null + : Number(e.target.value); + field.onChange(value); + }} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + placeholder="e.g 50" + className={cn( + 'mt-0 outline-none focus-visible:ring-0 border-neutral-4 shadow-none w-full h-auto transition-all duration-300 bg-white-100 py-3 px-4 text-neutral-11 !text-xs/5 rounded-[4px] placeholder:text-new.gray-300 placeholder:text-sm/5 font-normal disabled:text-neutral-6 disabled:border-new.primary-25', + fieldState.error + ? 'border-destructive focus-visible:ring-0 hover:border-destructive' + : 'hover:border-new.primary-100 focus:border-new.primary-300', + )} + /> +
      + sec +
      +
      +
      + +
      + )} + /> + + ( + + + Limit + + + { + const value = + e.target.value === '' + ? null + : Number(e.target.value); + field.onChange(value); + }} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + placeholder="e.g 10" + className={cn( + 'mt-0 outline-none focus-visible:ring-0 border-neutral-4 shadow-none w-full h-auto transition-all duration-300 bg-white-100 py-3 px-4 text-neutral-11 !text-xs/5 rounded-[4px] placeholder:text-new.gray-300 placeholder:text-sm/5 font-normal disabled:text-neutral-6 disabled:border-new.primary-25', + fieldState.error + ? 'border-destructive focus-visible:ring-0 hover:border-destructive' + : 'hover:border-new.primary-100 focus:border-new.primary-300', + )} + /> + + + + )} + /> +
      +
      + )} + + {/* Alert Configuration */} + {showConfig('alert_config') && ( +
      +
      +
      +

      + Alert Configuration +

      + {!hasAdvancedEndpointManagementLicense && ( + + + + + Business + + )} +
      + +
      + + {hasAdvancedEndpointManagementLicense && ( +
      + ( + + + Support Email + + + + + + + )} + /> + + ( + + + Slack webhook url + + + + + + + )} + /> +
      + )} +
      + )} + + {/* Authentication Configuration */} + {showConfig('auth') && ( +
      +
      +
      +

      + Endpoint Authentication +

      + + + + + + + You can set your provided endpoint authentication + if any is required + + + +
      + +
      + +
      + ( + + + API Key Name + + + + + + + )} + /> + + ( + + + API Key Value + + + + + + + )} + /> +
      +
      + )} + + {/* Signature Format Configuration */} + {showConfig('signature') && ( +
      +
      +
      +

      + Signature Format +

      + + + + + + + This specifies your signature format for your + project. + + + +
      + +
      + + + setValue('advanced_signatures', value === 'true') + } + > +
      + +
      +
      + +
      +
      +
      + )} + + {/* HTTP Timeout Configuration */} + {showConfig('http_timeout') && ( +
      +
      +
      +
      +

      + Endpoint Timeout +

      + + + + + + + How many seconds should Convoy wait for a + response from this endpoint before timing out? + + + +
      + {!hasAdvancedEndpointManagementLicense && ( + + + + + Business + + )} +
      + +
      + + ( + + + Timeout Value + + +
      + { + const value = + e.target.value === '' + ? null + : Number(e.target.value); + field.onChange(value); + }} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + placeholder="e.g 60" + readOnly={!hasAdvancedEndpointManagementLicense} + className={cn( + 'mt-0 outline-none focus-visible:ring-0 border-neutral-4 shadow-none w-full h-auto transition-all duration-300 bg-white-100 py-3 px-4 text-neutral-11 !text-xs/5 rounded-[4px] placeholder:text-new.gray-300 placeholder:text-sm/5 font-normal disabled:text-neutral-6 disabled:border-new.primary-25', + fieldState.error + ? 'border-destructive focus-visible:ring-0 hover:border-destructive' + : 'hover:border-new.primary-100 focus:border-new.primary-300', + )} + /> +
      + sec +
      +
      +
      + +
      + )} + /> +
      + )} +
      + + {/* Configuration Buttons */} +
      + {configurations.map( + config => + !config.show && ( + + ), + )} +
      + + {/* Submit Button */} +
      + +
      +
      + + + {isCreating && ( +
      +
      +
      + )} +
      +
      +
      + ); +} diff --git a/web/ui/dashboard-react/src/components/dashboard.tsx b/web/ui/dashboard-react/src/components/dashboard.tsx index 3dbbc29dc9..c36a8fe044 100644 --- a/web/ui/dashboard-react/src/components/dashboard.tsx +++ b/web/ui/dashboard-react/src/components/dashboard.tsx @@ -480,7 +480,7 @@ function ProjectLinks() { }, { name: 'Endpoints', - route: '/', + route: `/projects/${project?.uid}/endpoints`, }, { name: 'Events Log', diff --git a/web/ui/dashboard-react/src/models/endpoint.model.ts b/web/ui/dashboard-react/src/models/endpoint.model.ts new file mode 100644 index 0000000000..9b80449423 --- /dev/null +++ b/web/ui/dashboard-react/src/models/endpoint.model.ts @@ -0,0 +1,77 @@ +export interface ENDPOINT { + uid: string; + title: string; + advanced_signatures: boolean; + failure_rate: number; + authentication: { + api_key: { header_value: string; header_name: string }; + }; + created_at: string; + owner_id?: string; + description: string; + events?: unknown; + status: string; + secrets?: SECRET[]; + name?: string; + url: string; + target_url: string; + updated_at: string; + rate_limit: number; + rate_limit_duration: string; + http_timeout?: string; + support_email: string; + slack_webhook_url?: string; +} + +export interface SECRET { + created_at: string; + expires_at: string; + uid: string; + updated_at: string; + value: string; +} + +export interface PORTAL_LINK { + uid: string; + group_id: string; + endpoint_count: number; + endpoint: string[]; + endpoints_metadata: ENDPOINT[]; + can_manage_endpoint: boolean; + name: string; + owner_id: string; + url: string; + created_at: string; + updated_at: string; +} + +export interface API_KEY { + created_at: Date; + expires_at: Date; + key_type: string; + name: string; + role: { type: string; group: string; endpoint: string }; + uid: string; + updated_at: Date; +} + +export interface EndpointFormValues { + name: string; + url: string; + support_email: string; + slack_webhook_url: string; + secret: string | null; + http_timeout: number | null; + description: string | null; + owner_id: string | null; + rate_limit: number | null; + rate_limit_duration: number | null; + authentication?: { + type: string; + api_key: { + header_name: string; + header_value: string; + } + }; + advanced_signatures: boolean | null; +} diff --git a/web/ui/dashboard-react/src/routes.gen.ts b/web/ui/dashboard-react/src/routes.gen.ts index 4e54c2977c..4fc5f80c8d 100644 --- a/web/ui/dashboard-react/src/routes.gen.ts +++ b/web/ui/dashboard-react/src/routes.gen.ts @@ -22,6 +22,7 @@ import { Route as IndexImport } from './app/index' import { Route as ProjectsIndexImport } from './app/projects/index' import { Route as ProjectsNewImport } from './app/projects_/new' import { Route as ProjectsProjectIdSettingsImport } from './app/projects_/$projectId/settings' +import { Route as ProjectsProjectIdEndpointsNewImport } from './app/projects_/$projectId/endpoints/new' // Create/Update Routes @@ -91,6 +92,13 @@ const ProjectsProjectIdSettingsRoute = ProjectsProjectIdSettingsImport.update({ getParentRoute: () => rootRoute, } as any) +const ProjectsProjectIdEndpointsNewRoute = + ProjectsProjectIdEndpointsNewImport.update({ + id: '/projects_/$projectId/endpoints/new', + path: '/projects/$projectId/endpoints/new', + getParentRoute: () => rootRoute, + } as any) + // Populate the FileRoutesByPath interface declare module '@tanstack/react-router' { @@ -172,6 +180,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ProjectsProjectIdSettingsImport parentRoute: typeof rootRoute } + '/projects_/$projectId/endpoints/new': { + id: '/projects_/$projectId/endpoints/new' + path: '/projects/$projectId/endpoints/new' + fullPath: '/projects/$projectId/endpoints/new' + preLoaderRoute: typeof ProjectsProjectIdEndpointsNewImport + parentRoute: typeof rootRoute + } } } @@ -201,6 +216,7 @@ export interface FileRoutesByFullPath { '/projects/new': typeof ProjectsNewRoute '/projects/': typeof ProjectsIndexRoute '/projects/$projectId/settings': typeof ProjectsProjectIdSettingsRoute + '/projects/$projectId/endpoints/new': typeof ProjectsProjectIdEndpointsNewRoute } export interface FileRoutesByTo { @@ -214,6 +230,7 @@ export interface FileRoutesByTo { '/projects/new': typeof ProjectsNewRoute '/projects': typeof ProjectsIndexRoute '/projects/$projectId/settings': typeof ProjectsProjectIdSettingsRoute + '/projects/$projectId/endpoints/new': typeof ProjectsProjectIdEndpointsNewRoute } export interface FileRoutesById { @@ -229,6 +246,7 @@ export interface FileRoutesById { '/projects_/new': typeof ProjectsNewRoute '/projects/': typeof ProjectsIndexRoute '/projects_/$projectId/settings': typeof ProjectsProjectIdSettingsRoute + '/projects_/$projectId/endpoints/new': typeof ProjectsProjectIdEndpointsNewRoute } export interface FileRouteTypes { @@ -245,6 +263,7 @@ export interface FileRouteTypes { | '/projects/new' | '/projects/' | '/projects/$projectId/settings' + | '/projects/$projectId/endpoints/new' fileRoutesByTo: FileRoutesByTo to: | '/' @@ -257,6 +276,7 @@ export interface FileRouteTypes { | '/projects/new' | '/projects' | '/projects/$projectId/settings' + | '/projects/$projectId/endpoints/new' id: | '__root__' | '/' @@ -270,6 +290,7 @@ export interface FileRouteTypes { | '/projects_/new' | '/projects/' | '/projects_/$projectId/settings' + | '/projects_/$projectId/endpoints/new' fileRoutesById: FileRoutesById } @@ -284,6 +305,7 @@ export interface RootRouteChildren { UserSettingsRoute: typeof UserSettingsRoute ProjectsNewRoute: typeof ProjectsNewRoute ProjectsProjectIdSettingsRoute: typeof ProjectsProjectIdSettingsRoute + ProjectsProjectIdEndpointsNewRoute: typeof ProjectsProjectIdEndpointsNewRoute } const rootRouteChildren: RootRouteChildren = { @@ -297,6 +319,7 @@ const rootRouteChildren: RootRouteChildren = { UserSettingsRoute: UserSettingsRoute, ProjectsNewRoute: ProjectsNewRoute, ProjectsProjectIdSettingsRoute: ProjectsProjectIdSettingsRoute, + ProjectsProjectIdEndpointsNewRoute: ProjectsProjectIdEndpointsNewRoute, } export const routeTree = rootRoute @@ -318,7 +341,8 @@ export const routeTree = rootRoute "/signup", "/user-settings", "/projects_/new", - "/projects_/$projectId/settings" + "/projects_/$projectId/settings", + "/projects_/$projectId/endpoints/new" ] }, "/": { @@ -357,6 +381,9 @@ export const routeTree = rootRoute }, "/projects_/$projectId/settings": { "filePath": "projects_/$projectId/settings.tsx" + }, + "/projects_/$projectId/endpoints/new": { + "filePath": "projects_/$projectId/endpoints/new.tsx" } } } diff --git a/web/ui/dashboard-react/src/services/endpoints.service.ts b/web/ui/dashboard-react/src/services/endpoints.service.ts new file mode 100644 index 0000000000..79994ab090 --- /dev/null +++ b/web/ui/dashboard-react/src/services/endpoints.service.ts @@ -0,0 +1,71 @@ +import { request } from './http.service'; +import type { ENDPOINT } from '../models/endpoint.model'; +import type { HttpResponse } from '@/models/global.model'; +// TODO: type these data properly +type RequestBody = Record< + string, + string | number | object | Record | null | undefined +>; + +export async function addEndpoint( + body: Record, +): Promise> { + return request<{ data: ENDPOINT; message: string }>({ + url: '/endpoints', + method: 'post', + body: body as RequestBody, + level: 'org_project', + }); +} + +export async function updateEndpoint( + endpointId: string, + body: Record, +): Promise> { + return request<{ data: ENDPOINT; message: string }>({ + url: `/endpoints/${endpointId}`, + method: 'put', + body: body as RequestBody, + level: 'org_project', + }); +} + +export async function getEndpoint( + endpointId: string, +): Promise> { + return request<{ data: ENDPOINT; message: string }>({ + url: `/endpoints/${endpointId}`, + method: 'get', + level: 'org_project', + }); +} + +export async function getEndpoints( + params: Record = {}, +): Promise> { + return request<{ data: { content: ENDPOINT[] }; message: string }>({ + url: '/endpoints', + method: 'get', + query: params as unknown as Record>, + level: 'org_project', + }); +} + +export async function deleteEndpoint( + endpointId: string, +): Promise> { + return request<{ message: string }>({ + url: `/endpoints/${endpointId}`, + method: 'delete', + level: 'org_project', + }); +} + +// Export all functions as an object for compatibility +export const endpointsService = { + addEndpoint, + updateEndpoint, + getEndpoint, + getEndpoints, + deleteEndpoint +}; From 38fa2a6268bf15794fb0e5cf092ec342ca0a56fc Mon Sep 17 00:00:00 2001 From: Orim Dominic Adah Date: Sun, 23 Mar 2025 20:34:31 +0100 Subject: [PATCH 32/43] feat: list endpoints page (#2276) * feat: list endpoints page --- .../dashboard-react/assets/img/enter-icon.png | Bin 0 -> 549 bytes .../assets/svg/search-icon.svg | 10 + .../assets/svg/view-events-icon.svg | 3 + .../projects_/$projectId/endpoints/index.tsx | 815 ++++++++++++++++++ .../projects_/$projectId/endpoints/new.tsx | 8 +- .../src/app/projects_/$projectId/settings.tsx | 28 +- web/ui/dashboard-react/src/lib/pipes.ts | 24 + .../src/models/endpoint.model.ts | 2 +- .../src/models/global.model.ts | 2 +- web/ui/dashboard-react/src/routes.gen.ts | 29 +- .../src/services/endpoints.service.ts | 80 +- 11 files changed, 960 insertions(+), 41 deletions(-) create mode 100644 web/ui/dashboard-react/assets/img/enter-icon.png create mode 100644 web/ui/dashboard-react/assets/svg/search-icon.svg create mode 100644 web/ui/dashboard-react/assets/svg/view-events-icon.svg create mode 100644 web/ui/dashboard-react/src/app/projects_/$projectId/endpoints/index.tsx diff --git a/web/ui/dashboard-react/assets/img/enter-icon.png b/web/ui/dashboard-react/assets/img/enter-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..666035c45abe59520f35602e573d3b8b835f0e55 GIT binary patch literal 549 zcmV+=0^0qFP)q;qA5hLKntiT=B@$jl6!#Xp4k_YmPd}ZaCAL#kO)cDBS%#@o;`Am$FOgF;h7FV z7zCw}*iagY4W*IT5Q~um;J9cJ-(j;MI>*G)BhDy|#D***u9Y3YgICoPK%=s|2UZMu zRTl`Km7TRHo0t%lT^>}8gsANDplT#kWv9hmZirc^0zKeRL5mtpyLS<|mY7pu%93|Z zay%Kvsmzf@;7Z=V!EJ$9mOLkhan436=$Qc6m0VkfaZW}mz$JPnHYHadm^X~IHnM<@ ziCKojY19+%z^X+aYa{2WceGmH(!>wFGlqd0NPs(F57+@7mGX`;$Rfa?ev>IqMW(wGKT6gzRJ+Vk)tRZL|Kp6;P2NXi9ADI zI@n@Kp5s45(?&*@$ZNsm*kh!zm-S + + + + + + + + + diff --git a/web/ui/dashboard-react/assets/svg/view-events-icon.svg b/web/ui/dashboard-react/assets/svg/view-events-icon.svg new file mode 100644 index 0000000000..0b26dad9f2 --- /dev/null +++ b/web/ui/dashboard-react/assets/svg/view-events-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/ui/dashboard-react/src/app/projects_/$projectId/endpoints/index.tsx b/web/ui/dashboard-react/src/app/projects_/$projectId/endpoints/index.tsx new file mode 100644 index 0000000000..e8442ab125 --- /dev/null +++ b/web/ui/dashboard-react/src/app/projects_/$projectId/endpoints/index.tsx @@ -0,0 +1,815 @@ +import { createFileRoute, Link } from '@tanstack/react-router'; +import { z } from 'zod'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import React, { useEffect, useState } from 'react'; +import { + Copy, + MoreVertical, + PlayCircle, + PauseCircle, + Trash2, + Send, +} from 'lucide-react'; + +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogClose, + DialogDescription, +} from '@/components/ui/dialog'; +import { + Form, + FormField, + FormItem, +} from '@/components/ui/form'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { DashboardLayout } from '@/components/dashboard'; + +import { groupItemsByDate } from '@/lib/pipes'; +import { useLicenseStore, useProjectStore } from '@/store'; +import { endpointsService } from '@/services/endpoints.service'; + +import type { ENDPOINT } from '@/models/endpoint.model'; +import type { Pagination } from '@/models/global.model'; + +import viewEventsImg from '../../../../../assets/svg/view-events-icon.svg'; +import searchIcon from '../../../../../assets/svg/search-icon.svg'; + +export const Route = createFileRoute('/projects_/$projectId/endpoints/')({ + component: ListEndpointsPage, + +}); + +const ExpireSecretFormSchema = z.object({ + expiration: z + .enum([ + '0', + '3600', + '7200', + '14400', + '28800', + '43200', + '57600', + '72000', + '86400', + ]) + .pipe(z.coerce.number()) + .refine(v => v !== 0), +}); + +function EndpointsPageContent() { + const { projectId } = Route.useParams(); + const { project } = useProjectStore(); + const [isLoadingEndpoints, setIsLoadingEndpoints] = useState(false); + const [displayedEndpoints, setDisplayedEndpoints] = useState([]); + const [pagination, setPagination] = useState(null); + const [showExpireSecretSelectOptions, setShowExpireSecretSelectOptions] = + useState(false); + const [selectedEndpoint, setSelectedEndpoint] = useState( + null, + ); + const [isSendingTestEvent, setIsSendingTestEvent] = useState(false); + const [searchString, setSearchString] = useState(''); + const [isTogglingEndpoint, setIsTogglingEndpoint] = useState(false); + const [isDeletingEndpoint, setIsDeletingEndpoint] = useState(false); + const [showSecretModal, setShowSecretModal] = useState(false); + const [showDeleteModal, setShowDeleteModal] = useState(false); + const { licenses } = useLicenseStore(); + + const expireSecretForm = useForm>({ + resolver: zodResolver(ExpireSecretFormSchema), + defaultValues: { + expiration: 0, + }, + mode: 'onTouched', + }); + + useEffect(() => { + getEndpoints(); + }, []); + + // Function to get endpoints from API + const getEndpoints = async (params: Record = {}) => { + setIsLoadingEndpoints(true); + try { + const response = await endpointsService.getEndpoints(params); + // The response contains a flat array of ENDPOINT objects + setDisplayedEndpoints(response.data?.content || []); + setPagination(response.data?.pagination); + } catch (error) { + console.error('Error fetching endpoints:', error); + } finally { + setIsLoadingEndpoints(false); + } + }; + + // Function to handle search form submission + const handleSearch = (e: React.FormEvent) => { + e.preventDefault(); + getEndpoints({ q: searchString }); + }; + + // Function to handle pagination + const handlePagination = (params: Record) => { + getEndpoints({ + ...params, + ...(searchString ? { search: searchString } : {}), + }); + }; + + // Function to toggle endpoint status (pause/unpause) + const toggleEndpoint = async (endpointId: string) => { + setIsTogglingEndpoint(true); + try { + await endpointsService.toggleEndpoint(endpointId); + await getEndpoints(); + } catch (error) { + console.error('Error toggling endpoint:', error); + } finally { + setIsTogglingEndpoint(false); + } + }; + + // Function to delete an endpoint + const deleteEndpoint = async () => { + if (!selectedEndpoint) return; + + setIsDeletingEndpoint(true); + try { + await endpointsService.deleteEndpoint(selectedEndpoint.uid); + setShowDeleteModal(false); + await getEndpoints(); + } catch (error) { + console.error('Error deleting endpoint:', error); + } finally { + setIsDeletingEndpoint(false); + } + }; + + async function sendTestEvent() { + const testEvent = { + data: { data: 'test event from Convoy', convoy: 'https://getconvoy.io', amount: 1000 }, + endpoint_id: selectedEndpoint?.uid, + event_type: 'test.convoy' + }; + + setIsSendingTestEvent(true); + try { + const response = await endpointsService.sendEvent({ body: testEvent }); + // TODO: Add toast notification + } catch (error){ + console.error(error) + } finally { + setIsSendingTestEvent(false); + } + } + + + // Function to copy text to clipboard + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text); + // TODO: Add toast notification + }; + + // Get appropriate status color for badges + const getStatusColor = (status: 'active' | 'paused' | 'inactive') => { + switch (status.toLowerCase()) { + case 'active': + return 'bg-new.success-50 text-new.success-700'; + case 'paused': + return 'bg-neutral-a3 text-neutral-11'; + default: + return 'border border-neutral-10'; + } + }; + + // Loading state + if (isLoadingEndpoints) { + return ( +
      +
      +
      + ); + } + + // Empty state + if (!searchString && displayedEndpoints.length === 0) { + return ( +
      + Empty state +

      + {searchString + ? `${searchString} endpoint does not exist` + : 'You currently do not have any endpoints'} +

      +

      + Endpoints will be listed here when available +

      + +
      + ); + } + + // Main content with endpoints table + return ( +
      +
      +
      +

      + Endpoints +

      +
      +
      + +
      +
      +
      + search icon + setSearchString(e.target.value)} + /> + {searchString && ( + + )} +
      +
      + + {displayedEndpoints.length > 0 && ( + + )} +
      + +
      +
      + {/* TODO: make content of table scrollable without scrolling the page and the header*/} + + + + + Name + + + Status + + + Url + + + ID + + {licenses.includes('CIRCUIT_BREAKING') ? ( + + Failure Rate + + ) : null} + + + + + + {Array.from(groupItemsByDate(displayedEndpoints)).map( + ([dateKey, endpoints]) => { + return [ + + + {dateKey} + + + + + + {licenses.includes('CIRCUIT_BREAKING') ? ( + + ) : null} + + , + ].concat( + endpoints.map(ep => ( + + +
      + {ep.name || ep.title} +
      +
      + + {ep.status && ( + + {ep.status} + + )} + + +
      + + {ep.url || ep.target_url} + + +
      +
      + +
      + + {ep.uid} + + +
      +
      + {licenses.includes('CIRCUIT_BREAKING') ? ( + + {ep.failure_rate}% + + ) : null} + + +
      + + + + e.stopPropagation()} + > + + + + { + e.stopPropagation(); + setSelectedEndpoint(ep); + setShowSecretModal(true); + }} + > + + + + + View Secret + + + + + ({ + ...prev, + endpointId: ep.uid, + })} + > + + + + + View Subscriptions + + + + + { + e.stopPropagation(); + toggleEndpoint(ep.uid); + }} + disabled={isTogglingEndpoint} + > + {ep.status === 'paused' ? ( + + ) : ( + + )} + + {ep.status === 'paused' + ? 'Unpause' + : 'Pause'} + + + + {ep.status === 'inactive' && ( + { + e.stopPropagation(); + setIsTogglingEndpoint(true); + try { + await endpointsService.activateEndpoint( + ep.uid, + ); + await getEndpoints(); + } catch (error) { + console.error( + 'Error activating endpoint:', + error, + ); + } finally { + setIsTogglingEndpoint(false); + } + }} + disabled={isTogglingEndpoint} + > + + + Activate Endpoint + + + )} + + + + + + + + Edit + + + + + { + e.stopPropagation(); + setSelectedEndpoint(ep); + setShowDeleteModal(true); + }} + > + + + Delete + + + + {project?.type === 'outgoing' ? ( + { + e.stopPropagation(); + sendTestEvent(); + }} + > + + + Send Test Event + + + ) : null} + + +
      +
      +
      + )), + ); + }, + )} +
      +
      +
      + + {/* Pagination */} + {/* TODO: Add pagination */} +
      + + {/* Secret Modal */} + { + setShowSecretModal(!showSecretModal); + setShowExpireSecretSelectOptions(false); + }} + > + + + Endpoint Secret + + Endpoint Secret + + +
      + {selectedEndpoint && selectedEndpoint.secrets?.length && ( +
      + {selectedEndpoint.secrets?.map((secret, index, arr) => { + if (index != arr.length - 1) return null; + return ( +
      +

      Secret

      +
      +

      + {secret.value} +

      + +
      +
      + +
      + ( + + + + )} + > +
      + +
      + + + + + +
      +
      + +
      + ); + })} +
      + )} +
      +
      +
      + + {/* Delete Modal */} + + + + Delete Endpoint + +
      +

      + Are you sure you want to delete " + {selectedEndpoint?.name || selectedEndpoint?.title}"? +

      +

      + This action cannot be undone. +

      +
      + + +
      +
      +
      +
      +
      + ); +} + +function ListEndpointsPage() { + return ( + + + + ); +} diff --git a/web/ui/dashboard-react/src/app/projects_/$projectId/endpoints/new.tsx b/web/ui/dashboard-react/src/app/projects_/$projectId/endpoints/new.tsx index 0cd77d470d..ad2e702dcd 100644 --- a/web/ui/dashboard-react/src/app/projects_/$projectId/endpoints/new.tsx +++ b/web/ui/dashboard-react/src/app/projects_/$projectId/endpoints/new.tsx @@ -3,7 +3,6 @@ import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { useParams, createFileRoute, Link } from '@tanstack/react-router'; -import { toast } from 'sonner'; import { useNavigate } from '@tanstack/react-router'; import { Button } from '@/components/ui/button'; @@ -29,10 +28,11 @@ import { import * as authService from '@/services/auth.service'; import { endpointsService } from '@/services/endpoints.service'; import { useLicenseStore, useProjectStore } from '@/store/index'; -import type { EndpointFormValues } from '@/models/endpoint.model'; import { ensureCanAccessPrivatePages } from '@/lib/auth'; import { cn } from '@/lib/utils'; +import type { EndpointFormValues } from '@/models/endpoint.model'; + // Import back button icon import modalCloseIcon from '../../../../../assets/svg/modal-close-icon.svg'; @@ -226,14 +226,14 @@ function CreateEndpointPage() { try { // Create new endpoint const response = await endpointsService.addEndpoint(endpointValue); - toast.success(response.message || 'Endpoint created successfully'); + // toast.success(response.message || 'Endpoint created successfully'); reset(); // Navigate back to endpoints list navigate({ to: `/projects/${params.projectId}/endpoints` }); } catch (error) { - toast.error('Failed to save endpoint'); + // toast.error('Failed to save endpoint'); console.error(error); } finally { setIsCreating(false); diff --git a/web/ui/dashboard-react/src/app/projects_/$projectId/settings.tsx b/web/ui/dashboard-react/src/app/projects_/$projectId/settings.tsx index 40ca3be212..ca26403afe 100644 --- a/web/ui/dashboard-react/src/app/projects_/$projectId/settings.tsx +++ b/web/ui/dashboard-react/src/app/projects_/$projectId/settings.tsx @@ -82,7 +82,7 @@ import { DashboardLayout } from '@/components/dashboard'; import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; import { cn } from '@/lib/utils'; -import { toMMMDDYYYY } from '@/lib/pipes'; +import { groupItemsByDate } from '@/lib/pipes'; import { useProjectStore } from '@/store/index'; import { ensureCanAccessPrivatePages } from '@/lib/auth'; import * as authService from '@/services/auth.service'; @@ -1120,30 +1120,6 @@ function ProjectConfig(props: { project: Project; canManageProject: boolean }) { ); } -function groupItemsByDate( - items: Array, - sortOrder: 'desc' | 'asc' = 'desc', -) { - const groupsObj = Object.groupBy(items, ({ created_at }) => - toMMMDDYYYY(created_at), - ); - - const sortedGroup = new Map(); - - Object.keys(groupsObj) - .sort((dateA, dateB) => { - if (sortOrder == 'desc') { - return Number(new Date(dateB)) - Number(new Date(dateA)); - } - return Number(new Date(dateA)) - Number(new Date(dateB)); - }) - .reduce((acc, dateKey) => { - return acc.set(dateKey, groupsObj[dateKey] as typeof items); - }, sortedGroup); - - return sortedGroup; -} - const NewSignatureFormSchema = z.object({ encoding: z .enum(['hex', 'base64', '']) @@ -1391,7 +1367,7 @@ function SignatureHistoryConfig(props: { key={dateKey} className="border-new.primary-25 border-t border-b-0 hover:bg-transparent" > - + {dateKey} diff --git a/web/ui/dashboard-react/src/lib/pipes.ts b/web/ui/dashboard-react/src/lib/pipes.ts index 32cc07d3ff..f2452023a7 100644 --- a/web/ui/dashboard-react/src/lib/pipes.ts +++ b/web/ui/dashboard-react/src/lib/pipes.ts @@ -24,3 +24,27 @@ export function toMMMDDYYYY(date: string) { new Date(date), ); } + +export function groupItemsByDate( + items: Array, + sortOrder: 'desc' | 'asc' = 'desc', +) { + const groupsObj = Object.groupBy(items, ({ created_at }) => + toMMMDDYYYY(created_at), + ); + + const sortedGroup = new Map(); + + Object.keys(groupsObj) + .sort((dateA, dateB) => { + if (sortOrder == 'desc') { + return Number(new Date(dateB)) - Number(new Date(dateA)); + } + return Number(new Date(dateA)) - Number(new Date(dateB)); + }) + .reduce((acc, dateKey) => { + return acc.set(dateKey, groupsObj[dateKey] as typeof items); + }, sortedGroup); + + return sortedGroup; +} diff --git a/web/ui/dashboard-react/src/models/endpoint.model.ts b/web/ui/dashboard-react/src/models/endpoint.model.ts index 9b80449423..a4f71beb09 100644 --- a/web/ui/dashboard-react/src/models/endpoint.model.ts +++ b/web/ui/dashboard-react/src/models/endpoint.model.ts @@ -10,7 +10,7 @@ export interface ENDPOINT { owner_id?: string; description: string; events?: unknown; - status: string; + status: 'paused' | 'active' | 'inactive'; secrets?: SECRET[]; name?: string; url: string; diff --git a/web/ui/dashboard-react/src/models/global.model.ts b/web/ui/dashboard-react/src/models/global.model.ts index 6f48973ee2..e1c772864b 100644 --- a/web/ui/dashboard-react/src/models/global.model.ts +++ b/web/ui/dashboard-react/src/models/global.model.ts @@ -5,7 +5,7 @@ export interface HttpResponse { status: boolean; } -type Pagination = { +export type Pagination = { per_page: number; has_next_page: boolean; has_prev_page: boolean; diff --git a/web/ui/dashboard-react/src/routes.gen.ts b/web/ui/dashboard-react/src/routes.gen.ts index 4fc5f80c8d..b6382cbf8f 100644 --- a/web/ui/dashboard-react/src/routes.gen.ts +++ b/web/ui/dashboard-react/src/routes.gen.ts @@ -22,6 +22,7 @@ import { Route as IndexImport } from './app/index' import { Route as ProjectsIndexImport } from './app/projects/index' import { Route as ProjectsNewImport } from './app/projects_/new' import { Route as ProjectsProjectIdSettingsImport } from './app/projects_/$projectId/settings' +import { Route as ProjectsProjectIdEndpointsIndexImport } from './app/projects_/$projectId/endpoints/index' import { Route as ProjectsProjectIdEndpointsNewImport } from './app/projects_/$projectId/endpoints/new' // Create/Update Routes @@ -92,6 +93,13 @@ const ProjectsProjectIdSettingsRoute = ProjectsProjectIdSettingsImport.update({ getParentRoute: () => rootRoute, } as any) +const ProjectsProjectIdEndpointsIndexRoute = + ProjectsProjectIdEndpointsIndexImport.update({ + id: '/projects_/$projectId/endpoints/', + path: '/projects/$projectId/endpoints/', + getParentRoute: () => rootRoute, + } as any) + const ProjectsProjectIdEndpointsNewRoute = ProjectsProjectIdEndpointsNewImport.update({ id: '/projects_/$projectId/endpoints/new', @@ -187,6 +195,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ProjectsProjectIdEndpointsNewImport parentRoute: typeof rootRoute } + '/projects_/$projectId/endpoints/': { + id: '/projects_/$projectId/endpoints/' + path: '/projects/$projectId/endpoints' + fullPath: '/projects/$projectId/endpoints' + preLoaderRoute: typeof ProjectsProjectIdEndpointsIndexImport + parentRoute: typeof rootRoute + } } } @@ -217,6 +232,7 @@ export interface FileRoutesByFullPath { '/projects/': typeof ProjectsIndexRoute '/projects/$projectId/settings': typeof ProjectsProjectIdSettingsRoute '/projects/$projectId/endpoints/new': typeof ProjectsProjectIdEndpointsNewRoute + '/projects/$projectId/endpoints': typeof ProjectsProjectIdEndpointsIndexRoute } export interface FileRoutesByTo { @@ -231,6 +247,7 @@ export interface FileRoutesByTo { '/projects': typeof ProjectsIndexRoute '/projects/$projectId/settings': typeof ProjectsProjectIdSettingsRoute '/projects/$projectId/endpoints/new': typeof ProjectsProjectIdEndpointsNewRoute + '/projects/$projectId/endpoints': typeof ProjectsProjectIdEndpointsIndexRoute } export interface FileRoutesById { @@ -247,6 +264,7 @@ export interface FileRoutesById { '/projects/': typeof ProjectsIndexRoute '/projects_/$projectId/settings': typeof ProjectsProjectIdSettingsRoute '/projects_/$projectId/endpoints/new': typeof ProjectsProjectIdEndpointsNewRoute + '/projects_/$projectId/endpoints/': typeof ProjectsProjectIdEndpointsIndexRoute } export interface FileRouteTypes { @@ -264,6 +282,7 @@ export interface FileRouteTypes { | '/projects/' | '/projects/$projectId/settings' | '/projects/$projectId/endpoints/new' + | '/projects/$projectId/endpoints' fileRoutesByTo: FileRoutesByTo to: | '/' @@ -277,6 +296,7 @@ export interface FileRouteTypes { | '/projects' | '/projects/$projectId/settings' | '/projects/$projectId/endpoints/new' + | '/projects/$projectId/endpoints' id: | '__root__' | '/' @@ -291,6 +311,7 @@ export interface FileRouteTypes { | '/projects/' | '/projects_/$projectId/settings' | '/projects_/$projectId/endpoints/new' + | '/projects_/$projectId/endpoints/' fileRoutesById: FileRoutesById } @@ -306,6 +327,7 @@ export interface RootRouteChildren { ProjectsNewRoute: typeof ProjectsNewRoute ProjectsProjectIdSettingsRoute: typeof ProjectsProjectIdSettingsRoute ProjectsProjectIdEndpointsNewRoute: typeof ProjectsProjectIdEndpointsNewRoute + ProjectsProjectIdEndpointsIndexRoute: typeof ProjectsProjectIdEndpointsIndexRoute } const rootRouteChildren: RootRouteChildren = { @@ -320,6 +342,7 @@ const rootRouteChildren: RootRouteChildren = { ProjectsNewRoute: ProjectsNewRoute, ProjectsProjectIdSettingsRoute: ProjectsProjectIdSettingsRoute, ProjectsProjectIdEndpointsNewRoute: ProjectsProjectIdEndpointsNewRoute, + ProjectsProjectIdEndpointsIndexRoute: ProjectsProjectIdEndpointsIndexRoute, } export const routeTree = rootRoute @@ -342,7 +365,8 @@ export const routeTree = rootRoute "/user-settings", "/projects_/new", "/projects_/$projectId/settings", - "/projects_/$projectId/endpoints/new" + "/projects_/$projectId/endpoints/new", + "/projects_/$projectId/endpoints/" ] }, "/": { @@ -384,6 +408,9 @@ export const routeTree = rootRoute }, "/projects_/$projectId/endpoints/new": { "filePath": "projects_/$projectId/endpoints/new.tsx" + }, + "/projects_/$projectId/endpoints/": { + "filePath": "projects_/$projectId/endpoints/index.tsx" } } } diff --git a/web/ui/dashboard-react/src/services/endpoints.service.ts b/web/ui/dashboard-react/src/services/endpoints.service.ts index 79994ab090..deb5dee2c5 100644 --- a/web/ui/dashboard-react/src/services/endpoints.service.ts +++ b/web/ui/dashboard-react/src/services/endpoints.service.ts @@ -1,10 +1,15 @@ import { request } from './http.service'; import type { ENDPOINT } from '../models/endpoint.model'; -import type { HttpResponse } from '@/models/global.model'; +import type { HttpResponse, PaginatedResult } from '@/models/global.model'; // TODO: type these data properly type RequestBody = Record< string, - string | number | object | Record | null | undefined + | string + | number + | object + | Record + | null + | undefined >; export async function addEndpoint( @@ -40,13 +45,14 @@ export async function getEndpoint( }); } -export async function getEndpoints( - params: Record = {}, -): Promise> { - return request<{ data: { content: ENDPOINT[] }; message: string }>({ +export async function getEndpoints(params: Record = {}) { + return request>({ url: '/endpoints', method: 'get', - query: params as unknown as Record>, + query: params as unknown as Record< + string, + Record + >, level: 'org_project', }); } @@ -61,11 +67,69 @@ export async function deleteEndpoint( }); } +/** + * Pause or unpause an endpoint + */ +export async function toggleEndpoint( + endpointId: string, +): Promise> { + return request<{ data: ENDPOINT; message: string }>({ + url: `/endpoints/${endpointId}/pause`, + method: 'put', + level: 'org_project', + }); +} + +export async function activateEndpoint( + endpointId: string, +): Promise> { + return request<{ data: ENDPOINT; message: string }>({ + url: `/endpoints/${endpointId}/activate`, + method: 'post', + level: 'org_project', + }); +} + +export async function expireSecret( + endpointId: string, + body: { expiration: number }, +): Promise> { + return request<{ data: ENDPOINT; message: string }>({ + url: `/endpoints/${endpointId}/expire_secret`, + method: 'put', + body: body as RequestBody, + level: 'org_project', + }); +} + +type SendEventPayload = { + data: { + data: string; + convoy: string; + amount: number; + }; + endpoint_id: string | undefined; + event_type: string; +}; + +async function sendEvent(requestDetails: { body: SendEventPayload }) { + return request<{ data: ENDPOINT; message: string }>({ + url: `/events`, + method: 'post', + body: requestDetails.body, + level: 'org_project', + }); +} + // Export all functions as an object for compatibility export const endpointsService = { addEndpoint, updateEndpoint, getEndpoint, getEndpoints, - deleteEndpoint + deleteEndpoint, + toggleEndpoint, + activateEndpoint, + expireSecret, + sendEvent }; From 74c92e64b79273dfb7f49ed2423488843d395d33 Mon Sep 17 00:00:00 2001 From: Orim Dominic Adah Date: Mon, 24 Mar 2025 06:56:50 +0100 Subject: [PATCH 33/43] feat: update endpoint (#2277) * fix: UI for list endpoints empty state * feat: update endpoint --- .../$projectId/endpoints/$endpointId.tsx | 909 ++++++++++++++++++ .../projects_/$projectId/endpoints/index.tsx | 47 +- .../projects_/$projectId/endpoints/new.tsx | 1 + web/ui/dashboard-react/src/routes.gen.ts | 28 + .../src/services/endpoints.service.ts | 8 +- 5 files changed, 962 insertions(+), 31 deletions(-) create mode 100644 web/ui/dashboard-react/src/app/projects_/$projectId/endpoints/$endpointId.tsx diff --git a/web/ui/dashboard-react/src/app/projects_/$projectId/endpoints/$endpointId.tsx b/web/ui/dashboard-react/src/app/projects_/$projectId/endpoints/$endpointId.tsx new file mode 100644 index 0000000000..fab98342ed --- /dev/null +++ b/web/ui/dashboard-react/src/app/projects_/$projectId/endpoints/$endpointId.tsx @@ -0,0 +1,909 @@ +import { z } from 'zod'; +import { useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { createFileRoute, Link } from '@tanstack/react-router'; + +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/tooltip'; +import { + Form, + FormField, + FormItem, + FormLabel, + FormControl, + FormMessageWithErrorIcon, +} from '@/components/ui/form'; +import { DashboardLayout } from '@/components/dashboard'; + +import { useLicenseStore, useProjectStore } from '@/store'; +import { endpointsService, getEndpoint } from '@/services/endpoints.service'; +import * as authService from '@/services/auth.service'; + +import type { EndpointFormValues } from '@/models/endpoint.model'; + +import modalCloseIcon from '../../../../../assets/svg/modal-close-icon.svg'; +import { cn } from '@/lib/utils'; + +type ConfigItem = { + uid: string; + name: string; + show: boolean; + deleted: boolean; +}; + +// FIXME Inconsistent state after updating endpoint +// Update an endpoint field, e.g owner_id, and save. +// Click the updated endpoint in the endpoints list to edit again. +// The owner_id field shown on the form is the previous value. + +export const Route = createFileRoute( + '/projects_/$projectId/endpoints/$endpointId', +)({ + component: UpdateEndpointPage, + loader: async ({ params }) => { + const { data: endpoint } = await getEndpoint(params.endpointId); + const perms = await authService.getUserPermissions(); + return { endpoint, canManageEndpoints: perms.includes('Endpoints|MANAGE') }; + }, +}); + +const endpointSchema = z.object({ + name: z.string().min(1, { message: 'Please provide a name' }), + url: z.string().url({ message: 'Invalid endpoint URL' }), + support_email: z + .string() + .email({ message: 'Email is invalid' }) + .optional() + .or(z.literal('')), + slack_webhook_url: z + .string() + .url({ message: 'URL is invalid' }) + .optional() + .or(z.literal('')), + secret: z.string().nullable(), + http_timeout: z.number().nullable(), + description: z.string().nullable(), + owner_id: z.string().nullable(), + rate_limit: z.number().nullable(), + rate_limit_duration: z.number().nullable(), + authentication: z + .object({ + type: z.string(), + api_key: z.object({ + header_name: z.string(), + header_value: z.string(), + }), + }) + .optional(), + advanced_signatures: z.boolean().nullable(), +}); + +function UpdateEndpointPage() { + const { endpoint, canManageEndpoints } = Route.useLoaderData(); + const params = Route.useParams(); + const navigate = Route.useNavigate(); + const { licenses } = useLicenseStore(); + // Check if user has required license + const hasAdvancedEndpointManagementLicense = + Array.isArray(licenses) && + licenses.includes('ADVANCED_ENDPOINT_MANAGEMENT'); + const { project } = useProjectStore(); + + // State + const [configurations, setConfigurations] = useState( + [ + { uid: 'http_timeout', name: 'Timeout', show: false, deleted: false }, + { uid: 'owner_id', name: 'Owner ID', show: false, deleted: false }, + { uid: 'rate_limit', name: 'Rate Limit', show: false, deleted: false }, + { uid: 'auth', name: 'Auth', show: false, deleted: false }, + { + uid: 'alert_config', + name: 'Notifications', + show: false, + deleted: false, + }, + ].concat( + project?.type === 'outgoing' + ? [ + { + uid: 'signature', + name: 'Signature Format', + show: false, + deleted: false, + }, + ] + : [], + ), + ); + const [isUpdating, setIsUpdating] = useState(false); + + // Toggle configuration display + const toggleConfigForm = (configValue: string, deleted?: boolean) => { + setConfigurations(prev => + prev.map(config => + config.uid === configValue + ? { ...config, show: !config.show, deleted: deleted ?? false } + : config, + ), + ); + }; + + // Set configuration as deleted + const setConfigFormDeleted = (configValue: string, deleted: boolean) => { + setConfigurations(prev => + prev.map(config => + config.uid === configValue ? { ...config, deleted } : config, + ), + ); + }; + + // Check if configuration is shown + const showConfig = (configValue: string): boolean => { + return ( + configurations.find(config => config.uid === configValue)?.show || false + ); + }; + + // Check if configuration is marked as deleted + const configDeleted = (configValue: string): boolean => { + return ( + configurations.find(config => config.uid === configValue)?.deleted || + false + ); + }; + + // Form submission handler + const updateEndpoint = async (formData: EndpointFormValues) => { + // Handle rate limit deleted case + const rateLimitDeleted = + !showConfig('rate_limit') && configDeleted('rate_limit'); + if (rateLimitDeleted) { + formData.rate_limit = 0; + formData.rate_limit_duration = 0; + setConfigFormDeleted('rate_limit', false); + } + + setIsUpdating(true); + + // Clone form data to avoid mutating the original + // Fix type conversion by first going through unknown + const endpointValue = structuredClone(formData) as unknown as Record< + string, + unknown + >; + + // Remove authentication if both fields are empty + if ( + formData.authentication && + !formData.authentication.api_key.header_name && + !formData.authentication.api_key.header_value + ) { + delete endpointValue.authentication; + } + + try { + // Create new endpoint + const response = await endpointsService.updateEndpoint(params.endpointId, endpointValue); + // toast.success(response.message || 'Endpoint updated successfully'); + + form.reset(); + + // Navigate back to endpoints list + navigate({ to: `/projects/${params.projectId}/endpoints` }); + } catch (error) { + // toast.error('Failed to save endpoint'); + console.error(error); + } finally { + setIsUpdating(false); + } + }; + + // React Hook Form setup + const form = useForm({ + resolver: zodResolver(endpointSchema), + defaultValues: { + name: endpoint.name, + url: endpoint.url, + support_email: endpoint.support_email, + slack_webhook_url: endpoint.slack_webhook_url, + secret: endpoint.secrets?.at(endpoint.secrets.length - 1)?.value ?? null, + http_timeout: endpoint.http_timeout + ? Number(endpoint.http_timeout) + : null, + description: endpoint.description, + owner_id: endpoint.owner_id, + rate_limit: endpoint.rate_limit, + rate_limit_duration: endpoint.rate_limit_duration + ? Number(endpoint.rate_limit_duration) + : null, + authentication: { + type: 'api_key', + api_key: { + header_name: endpoint.authentication?.api_key?.header_name ?? '', + header_value: endpoint.authentication?.api_key?.header_value ?? '', + }, + }, + advanced_signatures: endpoint.advanced_signatures , + }, + mode: 'onTouched' + }); + + return ( + +
      +
      + + back to endpoints + +

      Edit Endpoint

      +
      + +

      + An endpoint represents a destination for your webhook events. + Configure your endpoint details below. +

      + +
      +
      + +
      + ( + + + Endpoint Name + + + + + + + )} + /> + + ( + + + Enter URL + + + + + + + )} + /> +
      + +
      + + +
      + + {/* Owner ID Configuration */} + {showConfig('owner_id') && ( +
      +
      +
      + + + + + + + + A unique id for identifying a group of endpoints, + like a user id. Useful for fanning out an event to + multiple endpoints and creating portal link for + multiple endpoints. + + + +
      + +
      + +
      + +
      +
      + )} + + {/* Configuration wrapper to maintain width consistency */} +
      + {/* Rate Limit Configuration */} + {showConfig('rate_limit') && ( +
      +
      +

      + Rate Limit +

      + +
      + +
      + ( + + + Duration + + +
      + { + const value = + e.target.value === '' + ? null + : Number(e.target.value); + field.onChange(value); + }} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + placeholder="e.g 50" + className={cn( + 'mt-0 outline-none focus-visible:ring-0 border-neutral-4 shadow-none w-full h-auto transition-all duration-300 bg-white-100 py-3 px-4 text-neutral-11 !text-xs/5 rounded-[4px] placeholder:text-new.gray-300 placeholder:text-sm/5 font-normal disabled:text-neutral-6 disabled:border-new.primary-25', + fieldState.error + ? 'border-destructive focus-visible:ring-0 hover:border-destructive' + : 'hover:border-new.primary-100 focus:border-new.primary-300', + )} + /> +
      + sec +
      +
      +
      + +
      + )} + /> + + ( + + + Limit + + + { + const value = + e.target.value === '' + ? null + : Number(e.target.value); + field.onChange(value); + }} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + placeholder="e.g 10" + className={cn( + 'mt-0 outline-none focus-visible:ring-0 border-neutral-4 shadow-none w-full h-auto transition-all duration-300 bg-white-100 py-3 px-4 text-neutral-11 !text-xs/5 rounded-[4px] placeholder:text-new.gray-300 placeholder:text-sm/5 font-normal disabled:text-neutral-6 disabled:border-new.primary-25', + fieldState.error + ? 'border-destructive focus-visible:ring-0 hover:border-destructive' + : 'hover:border-new.primary-100 focus:border-new.primary-300', + )} + /> + + + + )} + /> +
      +
      + )} + + {/* Alert Configuration */} + {showConfig('alert_config') && ( +
      +
      +
      +

      + Alert Configuration +

      + {!hasAdvancedEndpointManagementLicense && ( + + + + + Business + + )} +
      + +
      + + {hasAdvancedEndpointManagementLicense && ( +
      + ( + + + Support Email + + + + + + + )} + /> + + ( + + + Slack webhook url + + + + + + + )} + /> +
      + )} +
      + )} + + {/* Authentication Configuration */} + {showConfig('auth') && ( +
      +
      +
      +

      + Endpoint Authentication +

      + + + + + + + You can set your provided endpoint + authentication if any is required + + + +
      + +
      + +
      + ( + + + API Key Name + + + + + + + )} + /> + + ( + + + API Key Value + + + + + + + )} + /> +
      +
      + )} + + {/* Signature Format Configuration */} + {showConfig('signature') && ( +
      +
      +
      +

      + Signature Format +

      + + + + + + + This specifies your signature format for your + project. + + + +
      + +
      + + + form.setValue('advanced_signatures', value === 'true') + } + > +
      + +
      +
      + +
      +
      +
      + )} + + {/* HTTP Timeout Configuration */} + {showConfig('http_timeout') && ( +
      +
      +
      +
      +

      + Endpoint Timeout +

      + + + + + + + How many seconds should Convoy wait for a + response from this endpoint before timing out? + + + +
      + {!hasAdvancedEndpointManagementLicense && ( + + + + + Business + + )} +
      + +
      + + ( + + + Timeout Value + + +
      + { + const value = + e.target.value === '' + ? null + : Number(e.target.value); + field.onChange(value); + }} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + placeholder="e.g 60" + readOnly={ + !hasAdvancedEndpointManagementLicense + } + className={cn( + 'mt-0 outline-none focus-visible:ring-0 border-neutral-4 shadow-none w-full h-auto transition-all duration-300 bg-white-100 py-3 px-4 text-neutral-11 !text-xs/5 rounded-[4px] placeholder:text-new.gray-300 placeholder:text-sm/5 font-normal disabled:text-neutral-6 disabled:border-new.primary-25', + fieldState.error + ? 'border-destructive focus-visible:ring-0 hover:border-destructive' + : 'hover:border-new.primary-100 focus:border-new.primary-300', + )} + /> +
      + sec +
      +
      +
      + +
      + )} + /> +
      + )} +
      + + {/* Configuration Buttons */} +
      + {configurations.map( + config => + !config.show && ( + + ), + )} +
      + + {/* Submit Button */} +
      + +
      +
      + + + {isUpdating && ( +
      +
      +
      + )} +
      +
      +
      + ); +} diff --git a/web/ui/dashboard-react/src/app/projects_/$projectId/endpoints/index.tsx b/web/ui/dashboard-react/src/app/projects_/$projectId/endpoints/index.tsx index e8442ab125..172537cc39 100644 --- a/web/ui/dashboard-react/src/app/projects_/$projectId/endpoints/index.tsx +++ b/web/ui/dashboard-react/src/app/projects_/$projectId/endpoints/index.tsx @@ -21,11 +21,7 @@ import { DialogClose, DialogDescription, } from '@/components/ui/dialog'; -import { - Form, - FormField, - FormItem, -} from '@/components/ui/form'; +import { Form, FormField, FormItem } from '@/components/ui/form'; import { Table, TableBody, @@ -58,10 +54,10 @@ import type { Pagination } from '@/models/global.model'; import viewEventsImg from '../../../../../assets/svg/view-events-icon.svg'; import searchIcon from '../../../../../assets/svg/search-icon.svg'; +import { ConvoyLoader } from '@/components/convoy-loader'; export const Route = createFileRoute('/projects_/$projectId/endpoints/')({ component: ListEndpointsPage, - }); const ExpireSecretFormSchema = z.object({ @@ -118,6 +114,7 @@ function EndpointsPageContent() { try { const response = await endpointsService.getEndpoints(params); // The response contains a flat array of ENDPOINT objects + setDisplayedEndpoints(response.data?.content || []); setPagination(response.data?.pagination); } catch (error) { @@ -172,23 +169,26 @@ function EndpointsPageContent() { async function sendTestEvent() { const testEvent = { - data: { data: 'test event from Convoy', convoy: 'https://getconvoy.io', amount: 1000 }, + data: { + data: 'test event from Convoy', + convoy: 'https://getconvoy.io', + amount: 1000, + }, endpoint_id: selectedEndpoint?.uid, - event_type: 'test.convoy' + event_type: 'test.convoy', }; setIsSendingTestEvent(true); try { const response = await endpointsService.sendEvent({ body: testEvent }); // TODO: Add toast notification - } catch (error){ - console.error(error) + } catch (error) { + console.error(error); } finally { setIsSendingTestEvent(false); } } - // Function to copy text to clipboard const copyToClipboard = (text: string) => { navigator.clipboard.writeText(text); @@ -209,11 +209,7 @@ function EndpointsPageContent() { // Loading state if (isLoadingEndpoints) { - return ( -
      -
      -
      - ); + return ; } // Empty state @@ -223,24 +219,23 @@ function EndpointsPageContent() { Empty state

      {searchString ? `${searchString} endpoint does not exist` : 'You currently do not have any endpoints'}

      -

      +

      Endpoints will be listed here when available

      - + -
      - - +
      + +
      + )} - {/* Owner ID Configuration */} - {showConfig('owner_id') && ( + {/* Configuration wrapper to maintain width consistency */} +
      + {/* Rate Limit Configuration */} + {showConfig('rate_limit') && (
      -
      - - - - - - - - A unique id for identifying a group of endpoints, - like a user id. Useful for fanning out an event to - multiple endpoints and creating portal link for - multiple endpoints. - - - -
      +

      + Rate Limit +

      -
      - -
      -
      - )} - - {/* Configuration wrapper to maintain width consistency */} -
      - {/* Rate Limit Configuration */} - {showConfig('rate_limit') && ( -
      -
      -

      - Rate Limit -

      - -
      - -
      - ( - - - Duration - - -
      - { - const value = - e.target.value === '' - ? null - : Number(e.target.value); - field.onChange(value); - }} - onBlur={field.onBlur} - name={field.name} - ref={field.ref} - placeholder="e.g 50" - className={cn( - 'mt-0 outline-none focus-visible:ring-0 border-neutral-4 shadow-none w-full h-auto transition-all duration-300 bg-white-100 py-3 px-4 text-neutral-11 !text-xs/5 rounded-[4px] placeholder:text-new.gray-300 placeholder:text-sm/5 font-normal disabled:text-neutral-6 disabled:border-new.primary-25', - fieldState.error - ? 'border-destructive focus-visible:ring-0 hover:border-destructive' - : 'hover:border-new.primary-100 focus:border-new.primary-300', - )} - /> -
      - sec -
      -
      -
      - -
      - )} - /> - - ( - - - Limit - - +
      + ( + + + Duration + + +
      { const value = @@ -476,7 +436,7 @@ function UpdateEndpointPage() { onBlur={field.onBlur} name={field.name} ref={field.ref} - placeholder="e.g 10" + placeholder="e.g 50" className={cn( 'mt-0 outline-none focus-visible:ring-0 border-neutral-4 shadow-none w-full h-auto transition-all duration-300 bg-white-100 py-3 px-4 text-neutral-11 !text-xs/5 rounded-[4px] placeholder:text-new.gray-300 placeholder:text-sm/5 font-normal disabled:text-neutral-6 disabled:border-new.primary-25', fieldState.error @@ -484,157 +444,111 @@ function UpdateEndpointPage() { : 'hover:border-new.primary-100 focus:border-new.primary-300', )} /> - - - - )} - /> -
      -
      - )} - - {/* Alert Configuration */} - {showConfig('alert_config') && ( -
      -
      -
      -

      - Alert Configuration -

      - {!hasAdvancedEndpointManagementLicense && ( - - - - - Business - - )} -
      - -
      - - {hasAdvancedEndpointManagementLicense && ( -
      - ( - - - Support Email - - - - - - - )} - /> +
      + sec +
      +
      + + + + )} + /> - ( - - - Slack webhook url - - - - - - - )} - /> -
      - )} + ( + + + Limit + + + { + const value = + e.target.value === '' + ? null + : Number(e.target.value); + field.onChange(value); + }} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + placeholder="e.g 10" + className={cn( + 'mt-0 outline-none focus-visible:ring-0 border-neutral-4 shadow-none w-full h-auto transition-all duration-300 bg-white-100 py-3 px-4 text-neutral-11 !text-xs/5 rounded-[4px] placeholder:text-new.gray-300 placeholder:text-sm/5 font-normal disabled:text-neutral-6 disabled:border-new.primary-25', + fieldState.error + ? 'border-destructive focus-visible:ring-0 hover:border-destructive' + : 'hover:border-new.primary-100 focus:border-new.primary-300', + )} + /> + + + + )} + />
      - )} +
      + )} - {/* Authentication Configuration */} - {showConfig('auth') && ( -
      -
      -
      -

      - Endpoint Authentication -

      - - - - - - - You can set your provided endpoint - authentication if any is required - - - -
      - + {/* Alert Configuration */} + {showConfig('alert_config') && ( +
      +
      +
      +

      + Alert Configuration +

      + {!hasAdvancedEndpointManagementLicense && ( + + + + + Business + + )}
      + +
      + {hasAdvancedEndpointManagementLicense && (
      ( - API Key Name + Support Email ( - API Key Value + Slack webhook url
      -
      - )} + )} +
      + )} - {/* Signature Format Configuration */} - {showConfig('signature') && ( -
      -
      -
      -

      - Signature Format -

      - - - - - - - This specifies your signature format for your - project. - - - -
      - + {/* Authentication Configuration */} + {showConfig('auth') && ( +
      +
      +
      +

      + Endpoint Authentication +

      + + + + + + + You can set your provided endpoint authentication + if any is required + + +
      - - - form.setValue('advanced_signatures', value === 'true') - } +
      - )} - {/* HTTP Timeout Configuration */} - {showConfig('http_timeout') && ( -
      -
      -
      -
      -

      - Endpoint Timeout -

      - - - - - - - How many seconds should Convoy wait for a - response from this endpoint before timing out? - - - -
      - {!hasAdvancedEndpointManagementLicense && ( - - - - - Business - - )} -
      - -
      +
      + ( + + + API Key Name + + + + + + + )} + /> ( - Timeout Value + API Key Value -
      - { - const value = - e.target.value === '' - ? null - : Number(e.target.value); - field.onChange(value); - }} - onBlur={field.onBlur} - name={field.name} - ref={field.ref} - placeholder="e.g 60" - readOnly={ - !hasAdvancedEndpointManagementLicense - } - className={cn( - 'mt-0 outline-none focus-visible:ring-0 border-neutral-4 shadow-none w-full h-auto transition-all duration-300 bg-white-100 py-3 px-4 text-neutral-11 !text-xs/5 rounded-[4px] placeholder:text-new.gray-300 placeholder:text-sm/5 font-normal disabled:text-neutral-6 disabled:border-new.primary-25', - fieldState.error - ? 'border-destructive focus-visible:ring-0 hover:border-destructive' - : 'hover:border-new.primary-100 focus:border-new.primary-300', - )} - /> -
      - sec -
      -
      +
      )} />
      - )} -
      +
      + )} + + {/* Signature Format Configuration */} + {showConfig('signature') && ( +
      +
      +
      +

      + Signature Format +

      + + + + + + + This specifies your signature format for your + project. + + + +
      + +
      - {/* Configuration Buttons */} -
      - {configurations.map( - config => - !config.show && ( - - ), - )} -
      + +
      +

      + Simple +

      +
      + +
      +
      + +
      + +
      + )} - {/* Submit Button */} -
      - -
      - - + {/* HTTP Timeout Configuration */} + {showConfig('http_timeout') && ( +
      +
      +
      +
      +

      + Endpoint Timeout +

      + + + + + + + How many seconds should Convoy wait for a + response from this endpoint before timing out? + + + +
      + {!hasAdvancedEndpointManagementLicense && ( + + + + + Business + + )} +
      + +
      + + ( + + + Timeout Value + + +
      + { + const value = + e.target.value === '' + ? null + : Number(e.target.value); + field.onChange(value); + }} + onBlur={field.onBlur} + name={field.name} + ref={field.ref} + placeholder="e.g 60" + readOnly={!hasAdvancedEndpointManagementLicense} + className={cn( + 'mt-0 outline-none focus-visible:ring-0 border-neutral-4 shadow-none w-full h-auto transition-all duration-300 bg-white-100 py-3 px-4 text-neutral-11 !text-xs/5 rounded-[4px] placeholder:text-new.gray-300 placeholder:text-sm/5 font-normal disabled:text-neutral-6 disabled:border-new.primary-25', + fieldState.error + ? 'border-destructive focus-visible:ring-0 hover:border-destructive' + : 'hover:border-new.primary-100 focus:border-new.primary-300', + )} + /> +
      + sec +
      +
      +
      + +
      + )} + /> +
      + )} +
      + + {/* Configuration Buttons */} +
      + {configurations.map( + config => + !config.show && ( + + ), + )} +
      - {isUpdating && ( -
      -
      + {/* Submit Button */} +
      +
      - )} -
      - + + + + {isUpdating && ( +
      +
      +
      + )} +
      + ); } diff --git a/web/ui/dashboard-react/src/app/projects_/$projectId/endpoints/index.tsx b/web/ui/dashboard-react/src/app/projects_/$projectId/endpoints/index.tsx index 172537cc39..e8eabba357 100644 --- a/web/ui/dashboard-react/src/app/projects_/$projectId/endpoints/index.tsx +++ b/web/ui/dashboard-react/src/app/projects_/$projectId/endpoints/index.tsx @@ -52,12 +52,16 @@ import { endpointsService } from '@/services/endpoints.service'; import type { ENDPOINT } from '@/models/endpoint.model'; import type { Pagination } from '@/models/global.model'; +import { ensureCanAccessPrivatePages } from '@/lib/auth'; import viewEventsImg from '../../../../../assets/svg/view-events-icon.svg'; import searchIcon from '../../../../../assets/svg/search-icon.svg'; import { ConvoyLoader } from '@/components/convoy-loader'; export const Route = createFileRoute('/projects_/$projectId/endpoints/')({ component: ListEndpointsPage, + beforeLoad({ context }) { + ensureCanAccessPrivatePages(context.auth?.getTokens().isLoggedIn); + }, }); const ExpireSecretFormSchema = z.object({ @@ -653,7 +657,7 @@ function EndpointsPageContent() { if (index != arr.length - 1) return null; return (
      -

      Secret

      +

      Secret

      {secret.value} diff --git a/web/ui/dashboard-react/src/app/projects_/$projectId/sources/index.tsx b/web/ui/dashboard-react/src/app/projects_/$projectId/sources/index.tsx new file mode 100644 index 0000000000..363b2852e5 --- /dev/null +++ b/web/ui/dashboard-react/src/app/projects_/$projectId/sources/index.tsx @@ -0,0 +1,79 @@ +import { createFileRoute, Link } from '@tanstack/react-router'; + +import { Button } from '@/components/ui/button'; +import { DashboardLayout } from '@/components/dashboard'; + +import { ensureCanAccessPrivatePages } from '@/lib/auth'; +import { getUserPermissions } from '@/services/auth.service'; + +import sourcesEmptyState from "../../../../../assets/img/sources-empty-state.png" + +export const Route = createFileRoute('/projects_/$projectId/sources/')({ + component: RouteComponent, + beforeLoad({ context }) { + ensureCanAccessPrivatePages(context.auth?.getTokens().isLoggedIn); + }, + loader: async () => { + const perms = await getUserPermissions(); + + return { + canManageSources: perms.includes('Sources|MANAGE'), + sources: { content: [] }, + }; + }, +}); + +function RouteComponent() { + const { canManageSources, sources } = Route.useLoaderData(); + const { projectId } = Route.useParams(); + + if (sources.content.length === 0) { + return ( + +

      +
      + No subscriptions created +

      + Create your first source +

      + +

      + Sources are how your webhook events are routed into the Convoy. +

      + + +
      +
      + + ); + } + + return ( + +
      List sources
      +
      + ); +} diff --git a/web/ui/dashboard-react/src/app/projects_/$projectId/sources/new.tsx b/web/ui/dashboard-react/src/app/projects_/$projectId/sources/new.tsx new file mode 100644 index 0000000000..69fd22c89c --- /dev/null +++ b/web/ui/dashboard-react/src/app/projects_/$projectId/sources/new.tsx @@ -0,0 +1,3444 @@ +import { z } from 'zod'; +import { useState } from 'react'; +import { useForm, type RegisterOptions } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { createFileRoute, Link, useNavigate } from '@tanstack/react-router'; +import { DiffEditor, Editor } from '@monaco-editor/react'; + +import { ChevronRight, Info, CopyIcon, SaveIcon } from 'lucide-react'; + +import { + Form, + FormField, + FormItem, + FormLabel, + FormControl, + FormMessageWithErrorIcon, +} from '@/components/ui/form'; +import { InputTags } from '@/components/ui/input-tags'; +import { Textarea } from '@/components/ui/textarea'; +import { ConvoyCheckbox } from '@/components/convoy-checkbox'; +import { ToggleGroupItem } from '@/components/ui/toggle-group'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/tooltip'; +import { + Dialog, + DialogClose, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, + DialogDescription, +} from '@/components/ui/dialog'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { ToggleGroup } from '@radix-ui/react-toggle-group'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { DashboardLayout } from '@/components/dashboard'; + +import { cn } from '@/lib/utils'; +import { useLicenseStore, useProjectStore } from '@/store'; +import * as authService from '@/services/auth.service'; +import * as sourcesService from '@/services/sources.service'; + +import type { DragEvent, ChangeEvent } from 'react'; + +import uploadIcon from '../../../../../assets/img/upload.png'; +import githubIcon from '../../../../../assets/img/github.png'; +import shopifyIcon from '../../../../../assets/img/shopify.png'; +import twitterIcon from '../../../../../assets/img/twitter.png'; +import docIcon from '../../../../../assets/img/doc-icon-primary.svg'; +import modalCloseIcon from '../../../../../assets/svg/modal-close-icon.svg'; + +type FuncOutput = { + previous: Record | null | string; + current: Record | null | string; +}; + +const editorOptions = { + formatOnType: true, + formatOnPaste: true, + minimap: { enabled: false }, + scrollBeyondLastLine: false, + fontSize: 12, +}; + +export const Route = createFileRoute('/projects_/$projectId/sources/new')({ + component: RouteComponent, + async loader() { + const perms = await authService.getUserPermissions(); + return { + canManageSources: perms.includes('Sources|MANAGE'), + }; + }, +}); + +const pubSubTypes = [ + { uid: 'google', name: 'Google Pub/Sub' }, + { uid: 'kafka', name: 'Kafka' }, + { uid: 'sqs', name: 'AWS SQS' }, + { uid: 'amqp', name: 'AMQP / RabbitMQ' }, +] as const; + +const sourceVerifications = [ + { uid: 'noop', name: 'None' }, + { uid: 'hmac', name: 'HMAC' }, + { uid: 'basic_auth', name: 'Basic Auth' }, + { uid: 'api_key', name: 'API Key' }, + { uid: 'github', name: 'Github' }, + { uid: 'shopify', name: 'Shopify' }, + { uid: 'twitter', name: 'Twitter' }, +] as const; + +const AWSRegions = [ + { uid: 'us-east-2', name: 'US East (Ohio)' }, + { uid: 'us-east-1', name: 'US East (N. Virginia)' }, + { uid: 'us-west-1', name: 'US West (N. California)' }, + { uid: 'us-west-2', name: 'US West (Oregon)' }, + { uid: 'af-south-1', name: 'Africa (Cape Town)' }, + { uid: 'ap-east-1', name: 'Asia Pacific (Hong Kong)' }, + { uid: 'ap-south-2', name: 'Asia Pacific (Hyderabad)' }, + { uid: 'ap-southeast-3', name: 'Asia Pacific (Jakarta)' }, + { uid: 'ap-southeast-4', name: 'Asia Pacific (Melbourne)' }, + { uid: 'ap-south-1', name: 'Asia Pacific (Mumbai)' }, + { uid: 'ap-northeast-3', name: 'Asia Pacific (Osaka)' }, + { uid: 'ap-northeast-2', name: 'Asia Pacific (Seoul)' }, + { uid: 'ap-southeast-1', name: 'Asia Pacific (Singapore)' }, + { uid: 'ap-southeast-2', name: 'Asia Pacific (Sydney)' }, + { uid: 'ap-northeast-1', name: 'Asia Pacific (Tokyo)' }, + { uid: 'ca-central-1', name: 'Canada (Central)' }, + { uid: 'eu-central-1', name: 'Europe (Frankfurt)' }, + { uid: 'eu-west-1', name: 'Europe (Ireland)' }, + { uid: 'eu-west-2', name: 'Europe (London)' }, + { uid: 'eu-south-1', name: 'Europe (Milan)' }, + { uid: 'eu-west-3', name: 'Europe (Paris)' }, + { uid: 'eu-south-2', name: 'Europe (Spain)' }, + { uid: 'eu-north-1', name: 'Europe (Stockholm)' }, + { uid: 'eu-central-2', name: 'Europe (Zurich)' }, + { uid: 'me-south-1', name: 'Middle East (Bahrain)' }, + { uid: 'me-central-1', name: 'Middle East (UAE)' }, + { uid: 'sa-east-1', name: 'South America (São Paulo)' }, + { uid: 'us-gov-east-1', name: 'AWS GovCloud (US-East)' }, + { uid: 'us-gov-west-1', name: 'AWS GovCloud (US-West)' }, +] as const; + +const IncomingSourceFormSchema = z + .object({ + name: z.string().min(1, 'Enter new source name'), + type: z.enum([ + sourceVerifications[0].uid, + ...sourceVerifications.slice(1).map(t => t.uid), + ]), + is_disabled: z.boolean().optional(), + config: z.object({ + encoding: z.enum(['base64', 'hex', '']).optional(), + hash: z.enum(['SHA256', 'SHA512', '']).optional(), + header: z.string().optional(), + secret: z.string().optional(), + username: z.string().optional(), + password: z.string().optional(), + header_name: z.string().optional(), + header_value: z.string().optional(), + }), // z.record(z.union([z.string(), z.boolean()])).optional(), + custom_response: z + .object({ + content_type: z.string().optional(), + body: z.string().optional(), + }) + .optional(), + idempotency_keys: z.array(z.string()).optional(), + showHmac: z.boolean(), + showBasicAuth: z.boolean(), + showAPIKey: z.boolean(), + showGithub: z.boolean(), + showTwitter: z.boolean(), + showShopify: z.boolean(), + showCustomResponse: z.boolean(), + showIdempotency: z.boolean(), + }) + .refine( + v => { + if ( + v.showCustomResponse && + (!v.custom_response?.content_type || !v.custom_response.body) + ) { + return false; + } + return true; + }, + ({ custom_response }) => { + if (!custom_response?.content_type) + return { + message: 'Enter content type', + path: ['custom_response.content_type'], + }; + + if (!custom_response?.body) + return { + message: 'Enter response content', + path: ['custom_response.body'], + }; + + return { message: '', path: [] }; + }, + ) + .refine( + ({ showIdempotency, idempotency_keys }) => { + if (showIdempotency && idempotency_keys?.length == 0) return false; + return true; + }, + () => { + return { + message: + 'Add at least one idempotency key if idempotency configuration is enabled', + path: ['idempotency_keys'], + }; + }, + ) + .refine( + ({ type, config }) => { + const { encoding, hash, header, secret } = config; + const hasInvalidValue = !encoding || !hash || !header || !secret; + if (type == 'hmac' && hasInvalidValue) return false; + return true; + }, + ({ config }) => { + const { encoding, hash, header, secret } = config; + if (!encoding) + return { + message: 'Enter encoding value', + path: ['config.encoding'], + }; + + if (!hash) + return { + message: 'Enter hash value', + path: ['config.hash'], + }; + + if (!header) + return { + message: 'Enter header key', + path: ['config.header'], + }; + + if (!secret) + return { + message: 'Enter webhook signing secret', + path: ['config.secret'], + }; + + return { message: '', path: [] }; + }, + ) + .refine( + ({ type, config }) => { + const { secret } = config; + const isPreconfigured = ['github', 'shopify', 'twitter'].includes(type); + if (isPreconfigured && !secret) return false; + return true; + }, + () => ({ + message: 'Enter webhook signing secret', + path: ['config.secret'], + }), + ) + .refine( + ({ type, config }) => { + const { username, password } = config; + const hasInvalidValue = !username || !password; + if (type == 'basic_auth' && hasInvalidValue) return false; + return true; + }, + ({ config }) => { + const { username, password } = config; + if (!username) + return { + message: 'Enter username', + path: ['config.username'], + }; + + if (!password) + return { + message: 'Enter password', + path: ['config.password'], + }; + + return { message: '', path: [] }; + }, + ) + .refine( + ({ type, config }) => { + const { header_name, header_value } = config; + const hasInvalidValue = !header_name || !header_value; + if (type == 'api_key' && hasInvalidValue) return false; + return true; + }, + ({ config }) => { + const { header_name, header_value } = config; + if (!header_name) + return { + message: 'Enter API header key', + path: ['config.header_name'], + }; + + if (!header_value) + return { + message: 'Enter API header value', + path: ['config.header_value'], + }; + + return { message: '', path: [] }; + }, + ); + +const OutgoingSourceFormSchema = z + .object({ + name: z + .string({ required_error: 'Enter new source name' }) + .min(1, 'Enter new source name'), + type: z.literal('pub_sub'), + is_disabled: z.boolean(), + pub_sub: z.object({ + type: z + .enum(['', ...pubSubTypes.map(t => t.uid)]) + .refine(v => (v == '' ? false : true), { + message: 'Select source type', + path: ['type'], + }), + workers: z + .string({ required_error: 'Enter number of workers' }) + .min(1, 'Enter number of workers') + .refine(v => v !== null) + .transform(Number), + google: z + .object({ + project_id: z.string(), + service_account: z.string(), + subscription_id: z.string(), + }) + .optional(), + kafka: z + .object({ + brokers: z.array(z.string()).optional(), + consumer_group_id: z.string().optional(), + topic_name: z.string(), + auth: z + .object({ + type: z.enum(['plain', 'scram', '']), + tls: z.enum(['enabled', 'disabled', '']), + username: z.string().optional(), + password: z.string().optional(), + hash: z.enum(['SHA256', 'SHA512', '']).optional(), + }) + .optional(), + }) + .optional(), + sqs: z + .object({ + queue_name: z.string(), + access_key_id: z.string(), + secret_key: z.string(), + default_region: z + .enum(['', ...AWSRegions.map(reg => reg.uid)]) + .optional(), + }) + .optional(), + amqp: z + .object({ + schema: z.string(), + host: z.string(), + port: z.string().transform(Number), + queue: z.string(), + deadLetterExchange: z.string().optional(), + vhost: z.string().optional(), + auth: z + .object({ + user: z.string(), + password: z.string(), + }) + .optional(), + bindExchange: z + .object({ + exchange: z.string(), + routingKey: z.string(), + }) + .optional(), + }) + .optional(), + }), + showKafkaAuth: z.boolean().optional(), + showAMQPAuth: z.boolean().optional(), + showAMQPBindExhange: z.boolean().optional(), + showTransform: z.boolean().optional(), + }) + .refine( + ({ pub_sub }) => { + if (pub_sub.type !== 'google') return true; + + if ( + !pub_sub.google?.project_id || + !pub_sub.google?.subscription_id || + !pub_sub.google?.service_account + ) { + return false; + } + + return true; + }, + ({ pub_sub }) => { + if (!pub_sub.google?.project_id) { + return { + message: 'Project ID is required', + path: ['pub_sub.google.project_id'], + }; + } + + if (!pub_sub.google?.subscription_id) { + return { + message: 'Subscription ID is required', + path: ['pub_sub.google.subscription_id'], + }; + } + if (!pub_sub.google?.service_account) { + return { + message: 'Service account is required', + path: ['pub_sub.google.service_account'], + }; + } + + return { message: '', path: [] }; + }, + ) + .refine( + ({ showKafkaAuth, pub_sub }) => { + if (pub_sub.type !== 'kafka') return true; + if (!pub_sub.kafka?.brokers?.length) return false; + if (!pub_sub.kafka.topic_name) return false; + if (!showKafkaAuth) return true; + + let hasInvalidValue = + !pub_sub.kafka?.auth?.type || !pub_sub.kafka?.auth?.tls; + if (hasInvalidValue) return false; + + hasInvalidValue = + !pub_sub.kafka?.auth?.username || !pub_sub.kafka?.auth?.password; + if (hasInvalidValue) return false; + + hasInvalidValue = + pub_sub.kafka?.auth?.type == 'scram' && !pub_sub.kafka?.auth?.hash; + if (hasInvalidValue) return false; + + return true; + }, + ({ pub_sub }) => { + if (!pub_sub.kafka?.topic_name) { + return { + message: 'Topic name is required', + path: ['pub_sub.kafka.topic_name'], + }; + } + + if (!pub_sub.kafka?.brokers?.length) { + return { + message: 'A minimum of one broker address is required', + path: ['pub_sub.kafka.brokers'], + }; + } + + if (!pub_sub.kafka?.auth?.type) { + return { + message: 'Please select authentication type', + path: ['pub_sub.kafka.auth.type'], + }; + } + + if (!pub_sub.kafka?.auth?.tls) { + return { + message: 'Enable or disable TLS', + path: ['pub_sub.kafka.auth.tls'], + }; + } + + if (!pub_sub.kafka?.auth?.hash) { + return { + message: 'has is required when authentication type is scram', + path: ['pub_sub.kafka.auth.hash'], + }; + } + + if (!pub_sub.kafka?.auth?.username) { + return { + message: 'Username is required', + path: ['pub_sub.kafka.auth.username'], + }; + } + + if (!pub_sub.kafka?.auth?.password) { + return { + message: 'Password is required', + path: ['pub_sub.kafka.auth.password'], + }; + } + + return { message: '', path: [''] }; + }, + ) + .refine( + ({ pub_sub }) => { + if (pub_sub.type !== 'sqs') return true; + + if ( + !pub_sub.sqs?.default_region || + !pub_sub.sqs?.queue_name || + !pub_sub.sqs?.access_key_id || + !pub_sub.sqs?.secret_key + ) + return false; + return true; + }, + { + message: 'Select AWS default region', + path: ['pub_sub.sqs.default_region'], + }, + ) + .refine( + ({ pub_sub, showAMQPAuth, showAMQPBindExhange }) => { + if (pub_sub.type != 'amqp') return true; + + if ( + !pub_sub.amqp?.schema || + !pub_sub.amqp?.host || + !pub_sub.amqp?.port || + !pub_sub.amqp?.queue + ) { + return false; + } + + if (!showAMQPAuth) return true; + + if (!pub_sub.amqp?.auth?.user || !pub_sub.amqp?.auth?.password) { + return false; + } + + if (!showAMQPBindExhange) return true; + + if ( + !pub_sub.amqp?.bindExchange?.exchange || + !pub_sub.amqp?.bindExchange?.routingKey + ) { + return false; + } + + return true; + }, + ({ pub_sub, showAMQPAuth, showAMQPBindExhange }) => { + if (!pub_sub.amqp?.schema) + return { + message: 'Schema is required', + path: ['pub_sub.amqp.schema'], + }; + + if (!pub_sub.amqp?.host) + return { + message: 'Host is required', + path: ['pub_sub.amqp.host'], + }; + + if (!pub_sub.amqp?.port) + return { + message: 'Port is required', + path: ['pub_sub.amqp.port'], + }; + + if (!pub_sub.amqp?.queue) + return { + message: 'Queue is required', + path: ['pub_sub.amqp.queue'], + }; + + if (showAMQPAuth && !pub_sub.amqp?.auth?.user) + return { + message: 'User is required when authentication is enabled', + path: ['pub_sub.amqp.auth.user'], + }; + + if (showAMQPAuth && !pub_sub.amqp?.auth?.password) + return { + message: 'Password is required when authentication is enabled', + path: ['pub_sub.amqp.auth.password'], + }; + + if (showAMQPBindExhange && !pub_sub.amqp?.bindExchange?.exchange) + return { + message: 'Exchange is required when binding exchange is enabled', + path: ['pub_sub.amqp.bindExchange.exchange'], + }; + + if (showAMQPBindExhange && !pub_sub.amqp?.bindExchange?.routingKey) + return { + message: 'Routing key is required when binding exchange is enabled', + path: ['pub_sub.amqp.bindExchange.routingKey'], + }; + + return { message: '', path: [] }; + }, + ); + +const defaultBody = { + id: 'Sample-1', + name: 'Sample 1', + description: 'This is sample data #1', +}; + +const defaultOutput = { previous: '', current: '' }; + +function RouteComponent() { + const navigate = useNavigate(); + const { project } = useProjectStore(); + const { licenses } = useLicenseStore(); + const { projectId } = Route.useParams(); + const [sourceUrl, setSourceUrl] = useState(''); + const [isCreating, setIsCreating] = useState(false); + const [hasCreatedIncomingSource, setHasCreatedIncomingSource] = + useState(false); + const [selectedFile, setSelectedFile] = useState(null); + const [showTransformFunctionDialog, setShowTransformFunctionDialog] = + useState(false); + + // Transform function state variables + // TODO use a reducer hook + const [isTestingFunction, setIsTestingFunction] = useState(false); + const [isTransformPassed, setIsTransformPassed] = useState(false); + const [showConsole, setShowConsole] = useState(true); + const [transformBodyPayload, setTransformBodyPayload] = + useState>(defaultBody); + const [headerPayload, setHeaderPayload] = + useState>(defaultBody); + const [transformFnBody, setTransformFnBody] = useState( + `/* 1. While you can write multiple functions, the main + function called for your transformation is the transform function. + +2. The only argument acceptable in the transform function is the + payload data. + +3. The transform method must return a value. + +4. Console logs lust be written like this: +console.log('%j', logged_item) to get printed in the log below. + +5. The output payload from the function should be in this format + { + "owner_id": "string, optional", + "event_type": "string, required", + "data": "object, required", + "custom_headers": "object, optional", + "idempotency_key": "string, optional" + "endpoint_id": "string, depends", + } + +6. The endpoint_id field is only required when sending event to +a single endpoint. */ + +function transform(payload) { + // Transform function here + return { + "endpoint_id": "", + "owner_id": "", + "event_type": "sample", + "data": payload, + "custom_headers": { + "sample-header": "sample-value" + }, + "idempotency_key": "" + } +}`, + ); + const [transformFnHeader, setTransformFnHeader] = useState( + `/* 1. While you can write multiple functions, the main function +called for your transformation is the transform function. + +2. The only argument acceptable in the transform function is +the payload data. + +3. The transform method must return a value. + +4. Console logs lust be written like this +console.log('%j', logged_item) to get printed in the log below. */ + +function transform(payload) { +// Transform function here +return payload; +}`, + ); + const [bodyOutput, setBodyOutput] = useState(defaultOutput); + const [headerOutput, setHeaderOutput] = useState(defaultOutput); + const [bodyLogs, setBodyLogs] = useState([]); + const [headerLogs, setHeaderLogs] = useState([]); + const [transformFn, setTransformFn] = useState(); + const [headerTransformFn, setHeaderTransformFn] = useState(); + const [hasSavedFn, setHasSavedFn] = useState(false); + + type SourceType = (typeof sourceVerifications)[number]['uid']; + + const incomingForm = useForm>({ + resolver: zodResolver(IncomingSourceFormSchema), + defaultValues: { + name: '', + type: 'noop', + is_disabled: true, + config: { + hash: '', + encoding: '', + header: '', + secret: '', + username: '', + password: '', + header_name: '', + header_value: '', + }, + custom_response: { + content_type: '', + body: '', + }, + idempotency_keys: [], + showHmac: false, + showBasicAuth: false, + showAPIKey: false, + showGithub: false, + showTwitter: false, + showShopify: false, + showCustomResponse: false, + showIdempotency: false, + }, + mode: 'onTouched', + }); + + function getVerifierType( + type: SourceType, + config: z.infer['config'], + ) { + const obj: Record = {}; + + if (type == 'hmac') { + return { + type: 'hmac', + hmac: Object.entries(config).reduce((acc, record: [string, string]) => { + const [key, val] = record; + if (['encoding', 'hash', 'header', 'secret'].includes(key)) { + acc[key] = val; + return acc; + } + return acc; + }, obj), + }; + } + + if (type == 'basic_auth') { + return { + type: 'basic_auth', + basic_auth: Object.entries(config).reduce( + (acc, record: [string, string]) => { + const [key, val] = record; + if (['password', 'username'].includes(key)) { + acc[key] = val; + return acc; + } + return acc; + }, + obj, + ), + }; + } + + if (type == 'api_key') { + return { + type: 'api_key', + api_key: Object.entries(config).reduce( + // @ts-expect-error types match + (acc, record: [string, string]) => { + const [key, val] = record; + if (['header_name', 'header_value'].includes(key)) { + return (acc[key] = val); + } + return acc; + }, + obj, + ), + }; + } + + if (['github', 'shopify', 'twitter'].includes(type)) { + return { + type: 'hmac', + hmac: { + encoding: type == 'github' ? 'hex' : 'base64', + hash: 'SHA256', + header: `X-${type == 'github' ? 'Hub' : type == 'shopify' ? 'Shopify-Hmac' : 'Twitter-Webhooks'}-Signature-256`, + secret: config.secret, + }, + }; + } + + return { + type: 'noop', + noop: obj, + }; + } + + function transformIncomingSource( + v: z.infer, + ) { + return { + name: v.name, + is_disabled: v.is_disabled, + type: 'http', + custom_response: { + body: v.custom_response?.body || '', + content_type: v.custom_response?.content_type || '', + }, + idempotency_keys: v.idempotency_keys?.length ? v.idempotency_keys : null, + verifier: getVerifierType(v.type, v.config), + provider: ['github', 'twitter', 'shopify'].includes(v.type) ? v.type : '', + }; + } + + async function createIncomingSource( + raw: z.infer, + ) { + const payload = transformIncomingSource(raw); + setIsCreating(true); + try { + const response = await sourcesService.createSource(payload); + setSourceUrl(response.url); + setHasCreatedIncomingSource(true); + } catch (error) { + console.error(error); + } finally { + setIsCreating(false); + } + } + + function onFileInputChange( + e: ChangeEvent, + field: RegisterOptions, + ) { + if (e.target?.files?.length) { + const file = e.target.files[0]; + // ensure 5kb limit + if (file.size > 5 * 1024) { + setSelectedFile(null); + field.onChange?.(''); + // TODO: Show error toast/message + console.error('File size exceeds 5kb limit'); + return; + } + setSelectedFile(file); + // Handle the file here + const reader = new FileReader(); + reader.onload = event => { + try { + JSON.parse(event.target?.result as string); + // Parse JSON to the form to check if it's valid + if (reader.result) { + field.onChange?.(btoa(reader.result.toString())); + } + } catch (error) { + console.error('Invalid JSON file'); + } + }; + reader.readAsText(file, 'UTF-8'); + } + } + + function onFileInputDrop( + e: DragEvent, + field: RegisterOptions, + ) { + e.preventDefault(); + e.stopPropagation(); + if (e.dataTransfer.files && e.dataTransfer.files[0]) { + const file = e.dataTransfer.files[0]; + if (file.size > 5 * 1024) { + // TODO: Show error toast/message + setSelectedFile(null); + console.error('File size exceeds 5kb limit'); + return; + } + setSelectedFile(file); + const reader = new FileReader(); + reader.onload = event => { + try { + // Parse JSON to the form to check if it's valid + JSON.parse(event.target?.result as string); + if (reader.result) { + field.onChange?.(btoa(reader.result.toString())); + } + } catch (error) { + console.error('Invalid JSON file'); + } + }; + reader.readAsText(file); + } + } + + const outgoingForm = useForm>({ + resolver: zodResolver(OutgoingSourceFormSchema), + defaultValues: { + name: '', + type: 'pub_sub', + is_disabled: true, + body_function: {}, + header_function: {}, + pub_sub: { + type: '', + // @ts-expect-error the transform works + workers: '', + google: { + project_id: '', + subscription_id: '', + service_account: '', + }, + kafka: { + brokers: [], + consumer_group_id: '', + topic_name: '', + auth: { + type: '', + tls: '', + username: '', + password: '', + hash: '', + }, + }, + sqs: { + queue_name: '', + access_key_id: '', + secret_key: '', + default_region: '', + }, + amqp: { + schema: '', + host: '', + // @ts-expect-error the transform fixes this + port: '', + queue: '', + deadLetterExchange: '', + vhost: '', + auth: { + password: '', + user: '', + }, + bindExchange: { + exchange: '', + routingKey: '""', + }, + }, + }, + showKafkaAuth: false, + showAMQPAuth: false, + showAMQPBindExhange: false, + showTransform: false, + }, + mode: 'onTouched', + }); + + function transformOutgoingSource( + raw: z.infer, + ) { + const payload = { + name: raw.name, + type: raw.type, + is_disabled: true, + pub_sub: { + workers: raw.pub_sub.workers, + type: raw.pub_sub.type, + }, + body_function: raw.showTransform && transformFn ? transformFn : null, + header_function: + raw.showTransform && headerTransformFn ? headerTransformFn : null, + }; + + if (raw.pub_sub.type == 'google') { + return { + ...payload, + pub_sub: { + ...payload.pub_sub, + google: raw.pub_sub.google, + }, + }; + } + + if (raw.pub_sub.type == 'kafka') { + const kafka = raw.pub_sub.kafka; + return { + ...payload, + pub_sub: { + ...payload.pub_sub, + kafka: { + ...kafka, + auth: { + ...kafka?.auth, + tls: kafka?.auth?.tls == 'enabled' ? true : false, + }, + }, + }, + }; + } + + if (raw.pub_sub.type == 'sqs') { + const sqs = raw.pub_sub.sqs; + return { + ...payload, + pub_sub: { + ...payload.pub_sub, + sqs, + }, + }; + } + + if (raw.pub_sub.type == 'amqp') { + const amqp = raw.pub_sub.amqp; + return { + ...payload, + pub_sub: { + ...payload.pub_sub, + amqp, + }, + }; + } + } + + // Function to test transform + async function testTransformFunction(type: 'body' | 'header') { + setIsTransformPassed(false); + setIsTestingFunction(true); + + const payload = type === 'body' ? transformBodyPayload : headerPayload; + const transformFunc = type === 'body' ? transformFnBody : transformFnHeader; + + try { + // Call the sources service to test the transform function + const response = await sourcesService.testTransformFunction({ + payload, + function: transformFunc, + type, + }); + + // In a real implementation, this would return payload and logs + if (type === 'body') { + setBodyOutput(prev => ({ + current: response.payload, + previous: prev.current, + })); + setBodyLogs( + response.log.toReversed() || [ + 'Transform function executed successfully', + ], + ); + } else { + setHeaderOutput(prev => ({ + current: response.payload, + previous: prev.current, + })); + setHeaderLogs( + response.log.toReversed() || [ + 'Transform function executed successfully', + ], + ); + } + + setIsTransformPassed(true); + setIsTestingFunction(false); + setShowConsole(bodyLogs.length || headerLogs.length ? true : false); + + if (type === 'body') { + setTransformFn(transformFunc); + } else { + setHeaderTransformFn(transformFunc); + } + } catch (error) { + console.error(error); + setIsTestingFunction(false); + if (type === 'body') { + setBodyLogs(['Error executing transform function']); + } else { + setHeaderLogs(['Error executing transform function']); + } + } + } + + async function createOutgoingSource( + raw: z.infer, + ) { + const payload = transformOutgoingSource(raw); + console.log(payload); + // setIsCreating(true); + // try { + // /* const res = */ await sourcesService.createSource(payload); + // TODO notify UI + // } catch (error) { + // console.error(error); + // } finally { + // setIsCreating(false); + // } + } + + return ( + +
      +
      + + back to endpoints + +

      Create Source

      +
      + + {project?.type == 'incoming' && ( +
      + +
      +
      +

      + Pre-configured Sources +

      +
      + { + incomingForm.setValue('type', v); + incomingForm.setValue( + 'name', + `${v.charAt(0).toUpperCase()}${v.slice(1)} Source`, + ); + }} + > + + github preconfigured source + + + shopify preconfigured source + + + twitter preconfigured source + + +
      +
      +
      + ( + + + Source Name + + + + + + + )} + /> +
      +
      + ( + + + Source Verification + + + + )} + /> +
      + + {/* When source verification is HMAC */} + {incomingForm.watch('type') == 'hmac' && ( +
      +

      + Configure HMAC +

      + + ( + + + Encoding + + + + )} + /> + + ( + + + Hash Algorithm + + + + )} + /> + + ( + + + Header Value + + + + + + + )} + /> + + ( + + + Webhook signing secret + + + + + + + )} + /> +
      + )} + + {/* When source verification is basic auth */} + {incomingForm.watch('type') == 'basic_auth' && ( +
      +

      + Configure Basic Auth +

      + + ( + + + Username + + + + + + + )} + /> + + ( + + + Password + + + + + + + )} + /> +
      + )} + + {/* When source verification is API Key */} + {incomingForm.watch('type') == 'api_key' && ( +
      +

      + Configure API Key +

      + + ( + + + Header Name + + + + + + + )} + /> + + ( + + + Header Value + + + + + + + )} + /> +
      + )} + + {/* When source verification is github, twitter or shopify */} + {['github', 'shopify', 'twitter'].includes( + incomingForm.watch('type'), + ) && ( +
      +

      + {incomingForm.watch('type')} Credentials +

      + + ( + + + Webhook Signing Secret + + + + + + + )} + /> +
      + )} + +
      +
      +
      + + {/* Checkboxes for custom response and idempotency */} +
      + ( + + + + + + )} + /> + + ( + + + + + + )} + /> +
      + + {/* Custom Response */} + {incomingForm.watch('showCustomResponse') && ( +
      +

      + Custom Response +

      + + ( + + + Response Content Type + + + + + + + )} + /> + + ( + + + Response Content + + +