From f12d9e7c0ef7e3ffab4c372306a578a4ba6fb1ad Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Mon, 10 Jun 2024 21:34:14 +0530 Subject: [PATCH 001/582] classifier-93 adding jira integration endpoint and validate signature --- DSL/DMapper/js/webhookValidation.js | 50 +++++++++++++++++++ .../integration/jira/cloud/accept.yml | 43 ++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 DSL/DMapper/js/webhookValidation.js create mode 100644 DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml diff --git a/DSL/DMapper/js/webhookValidation.js b/DSL/DMapper/js/webhookValidation.js new file mode 100644 index 00000000..82328c3e --- /dev/null +++ b/DSL/DMapper/js/webhookValidation.js @@ -0,0 +1,50 @@ +import crypto from "crypto-js"; + +//test code,code need to revamp + +const webhookValidation = async (payload, signature) => { + if (!signature) { + return res.status(400).json({ + status: "error", + code: 400, + message: "Missing signature", + data: null + }); + } + + try { + if (verifySignature(payload, signature)) { + // Process the webhook payload + res.status(200).json({ + status: "success", + code: 200, + message: "Signature verified", + data: null + }); + } else { + res.status(401).json({ + status: "error", + code: 401, + message: "Invalid signature", + data: null + }); + } + } catch (error) { + console.error('Error processing signature:', error); + res.status(500).json({ + status: "error", + code: 500, + message: "Internal Server Error", + data: null + }); + } +}; + +function verifySignature(payload, signature) { + const hmac = crypto.createHmac('sha256', SECRET); + hmac.update(payload); + const computedSignature = hmac.digest('hex'); + return crypto.timingSafeEqual(Buffer.from(computedSignature), Buffer.from(signature)); +} + +export default webhookValidation; diff --git a/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml b/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml new file mode 100644 index 00000000..87000253 --- /dev/null +++ b/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml @@ -0,0 +1,43 @@ +declaration: + call: declare + version: 0.1 + description: "Decription placeholder for 'ACEEPT'" + method: post + accepts: json + returns: json + namespace: classifier + +jira_webhook_data: + assign: + signature: ${incoming.headers.X-Hub-Signature} + payload: ${incoming.body} + issue_info: ${incoming.body.issue} + next: get_jira_signature + +get_jira_signature: + call: http.post + args: + url: "[#CLASSIFIER_DMAPPER]/js/encryption/urlValidator" + body: + signature: ${signature} + payload: ${payload} + result: res + +validateUrlSignature: + switch: + - condition: "${res.response.status==200 && res.response.body.isValid}" + next: get_jira_issue_info + next: return_error_found + +return_error_found: + return: ${res.response.code} + status: ${res.response.message} + next: end + +get_jira_issue_info: + assign: + issue_info: ${issue_info} + +#payload format data mapper +#sent to private ruuter endpoint + From 1e273c4979a3a4faedbcdb13d59f10ee2777f396 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Tue, 11 Jun 2024 19:18:43 +0530 Subject: [PATCH 002/582] initial ui setup --- GUI/.eslintrc.cjs | 18 + GUI/.gitignore | 24 + GUI/README.md | 30 + GUI/i18n.ts | 26 + GUI/index.html | 13 + GUI/locales/en/translation.json | 4 + GUI/locales/et/translation.json | 4 + GUI/package-lock.json | 6156 +++++++++++++++++ GUI/package.json | 49 + GUI/postcss.config.js | 6 + GUI/public/vite.svg | 1 + GUI/src/App.css | 8 + GUI/src/App.tsx | 18 + GUI/src/assets/react.svg | 1 + GUI/src/components/Layout/Layout.scss | 20 + GUI/src/components/Layout/index.tsx | 22 + GUI/src/components/atoms/Button/Button.scss | 150 + GUI/src/components/atoms/Button/index.tsx | 41 + GUI/src/components/atoms/CheckBox/index.tsx | 32 + .../components/atoms/RadioButton/index.tsx | 34 + GUI/src/components/atoms/Toast/Toast.scss | 73 + GUI/src/components/atoms/Toast/index.tsx | 51 + GUI/src/components/atoms/Tooltip/Tooltip.scss | 21 + GUI/src/components/atoms/Tooltip/index.tsx | 28 + GUI/src/components/index.tsx | 9 + .../components/molecules/Header/Header.scss | 8 + GUI/src/components/molecules/Header/index.tsx | 12 + .../components/molecules/SideBar/SideBar.scss | 30 + .../components/molecules/SideBar/index.tsx | 64 + GUI/src/config/menuConfig.json | 34 + GUI/src/context/ToastContext.tsx | 58 + GUI/src/hooks/useToast.tsx | 5 + GUI/src/index.css | 26 + GUI/src/main.tsx | 14 + GUI/src/pages/Home.tsx | 57 + GUI/src/styles/components/_vertical-tabs.scss | 119 + GUI/src/styles/generic/_base.scss | 78 + GUI/src/styles/generic/_fonts.scss | 15 + GUI/src/styles/generic/_reset.scss | 145 + GUI/src/styles/main.scss | 21 + GUI/src/styles/settings/_mixins.scss | 23 + GUI/src/styles/settings/_utility-classes.scss | 3 + .../settings/variables/_breakpoints.scss | 9 + .../styles/settings/variables/_colors.scss | 155 + GUI/src/styles/settings/variables/_grid.scss | 3 + GUI/src/styles/settings/variables/_other.scss | 16 + .../styles/settings/variables/_spacing.scss | 21 + .../settings/variables/_typography.scss | 22 + GUI/src/styles/tools/_color.scss | 4 + GUI/src/styles/tools/_spacing.scss | 4 + GUI/src/vite-env.d.ts | 1 + GUI/tailwind.config.js | 12 + GUI/tsconfig.json | 25 + GUI/tsconfig.node.json | 11 + GUI/vite.config.ts | 7 + 55 files changed, 7841 insertions(+) create mode 100644 GUI/.eslintrc.cjs create mode 100644 GUI/.gitignore create mode 100644 GUI/README.md create mode 100644 GUI/i18n.ts create mode 100644 GUI/index.html create mode 100644 GUI/locales/en/translation.json create mode 100644 GUI/locales/et/translation.json create mode 100644 GUI/package-lock.json create mode 100644 GUI/package.json create mode 100644 GUI/postcss.config.js create mode 100644 GUI/public/vite.svg create mode 100644 GUI/src/App.css create mode 100644 GUI/src/App.tsx create mode 100644 GUI/src/assets/react.svg create mode 100644 GUI/src/components/Layout/Layout.scss create mode 100644 GUI/src/components/Layout/index.tsx create mode 100644 GUI/src/components/atoms/Button/Button.scss create mode 100644 GUI/src/components/atoms/Button/index.tsx create mode 100644 GUI/src/components/atoms/CheckBox/index.tsx create mode 100644 GUI/src/components/atoms/RadioButton/index.tsx create mode 100644 GUI/src/components/atoms/Toast/Toast.scss create mode 100644 GUI/src/components/atoms/Toast/index.tsx create mode 100644 GUI/src/components/atoms/Tooltip/Tooltip.scss create mode 100644 GUI/src/components/atoms/Tooltip/index.tsx create mode 100644 GUI/src/components/index.tsx create mode 100644 GUI/src/components/molecules/Header/Header.scss create mode 100644 GUI/src/components/molecules/Header/index.tsx create mode 100644 GUI/src/components/molecules/SideBar/SideBar.scss create mode 100644 GUI/src/components/molecules/SideBar/index.tsx create mode 100644 GUI/src/config/menuConfig.json create mode 100644 GUI/src/context/ToastContext.tsx create mode 100644 GUI/src/hooks/useToast.tsx create mode 100644 GUI/src/index.css create mode 100644 GUI/src/main.tsx create mode 100644 GUI/src/pages/Home.tsx create mode 100644 GUI/src/styles/components/_vertical-tabs.scss create mode 100644 GUI/src/styles/generic/_base.scss create mode 100644 GUI/src/styles/generic/_fonts.scss create mode 100644 GUI/src/styles/generic/_reset.scss create mode 100644 GUI/src/styles/main.scss create mode 100644 GUI/src/styles/settings/_mixins.scss create mode 100644 GUI/src/styles/settings/_utility-classes.scss create mode 100644 GUI/src/styles/settings/variables/_breakpoints.scss create mode 100644 GUI/src/styles/settings/variables/_colors.scss create mode 100644 GUI/src/styles/settings/variables/_grid.scss create mode 100644 GUI/src/styles/settings/variables/_other.scss create mode 100644 GUI/src/styles/settings/variables/_spacing.scss create mode 100644 GUI/src/styles/settings/variables/_typography.scss create mode 100644 GUI/src/styles/tools/_color.scss create mode 100644 GUI/src/styles/tools/_spacing.scss create mode 100644 GUI/src/vite-env.d.ts create mode 100644 GUI/tailwind.config.js create mode 100644 GUI/tsconfig.json create mode 100644 GUI/tsconfig.node.json create mode 100644 GUI/vite.config.ts diff --git a/GUI/.eslintrc.cjs b/GUI/.eslintrc.cjs new file mode 100644 index 00000000..d6c95379 --- /dev/null +++ b/GUI/.eslintrc.cjs @@ -0,0 +1,18 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', + ], + ignorePatterns: ['dist', '.eslintrc.cjs'], + parser: '@typescript-eslint/parser', + plugins: ['react-refresh'], + rules: { + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, +} diff --git a/GUI/.gitignore b/GUI/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/GUI/.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/GUI/README.md b/GUI/README.md new file mode 100644 index 00000000..0d6babed --- /dev/null +++ b/GUI/README.md @@ -0,0 +1,30 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: + +- Configure the top-level `parserOptions` property like this: + +```js +export default { + // other rules... + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + project: ['./tsconfig.json', './tsconfig.node.json'], + tsconfigRootDir: __dirname, + }, +} +``` + +- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` +- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` +- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list diff --git a/GUI/i18n.ts b/GUI/i18n.ts new file mode 100644 index 00000000..e16cd3f2 --- /dev/null +++ b/GUI/i18n.ts @@ -0,0 +1,26 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; + +import commonEN from './locales/en/translation.json'; +import commonET from './locales/et/translation.json'; + +i18n + .use(LanguageDetector) + .use(initReactI18next) + .init({ + debug: import.meta.env.NODE_ENV === 'development', + fallbackLng: 'et', + supportedLngs: ['et','en'], + resources: { + en: { + common: commonEN, + }, + et: { + common: commonET, + }, + }, + defaultNS: 'common', + }); + +export default i18n; diff --git a/GUI/index.html b/GUI/index.html new file mode 100644 index 00000000..e4b78eae --- /dev/null +++ b/GUI/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + TS + + +
+ + + diff --git a/GUI/locales/en/translation.json b/GUI/locales/en/translation.json new file mode 100644 index 00000000..8bde3754 --- /dev/null +++ b/GUI/locales/en/translation.json @@ -0,0 +1,4 @@ +{ + "Title":"En Title", + "Description":"En Description" +} \ No newline at end of file diff --git a/GUI/locales/et/translation.json b/GUI/locales/et/translation.json new file mode 100644 index 00000000..b48e8799 --- /dev/null +++ b/GUI/locales/et/translation.json @@ -0,0 +1,4 @@ +{ + "Title":"Et Title", + "Description":"Et Description" +} \ No newline at end of file diff --git a/GUI/package-lock.json b/GUI/package-lock.json new file mode 100644 index 00000000..575d4d50 --- /dev/null +++ b/GUI/package-lock.json @@ -0,0 +1,6156 @@ +{ + "name": "est-gov-classifier", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "est-gov-classifier", + "version": "0.0.0", + "dependencies": { + "@radix-ui/react-accessible-icon": "^1.0.1", + "@radix-ui/react-collapsible": "^1.0.1", + "@radix-ui/react-dialog": "^1.0.2", + "@radix-ui/react-popover": "^1.0.2", + "@radix-ui/react-select": "^1.1.2", + "@radix-ui/react-switch": "^1.0.1", + "@radix-ui/react-tabs": "^1.0.1", + "@radix-ui/react-toast": "^1.1.2", + "@radix-ui/themes": "^3.0.5", + "@tanstack/react-query": "^4.20.4", + "clsx": "^2.1.1", + "i18next": "^23.11.5", + "i18next-browser-languagedetector": "^8.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-i18next": "^14.1.2", + "react-icons": "^4.10.1", + "react-router-dom": "^6.23.1" + }, + "devDependencies": { + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22", + "@types/react-i18next": "^8.1.0", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/parser": "^7.2.0", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.19", + "eslint": "^8.57.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.6", + "postcss": "^8.4.38", + "sass": "^1.77.4", + "tailwindcss": "^3.4.4", + "typescript": "^5.2.2", + "vite": "^5.2.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "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.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", + "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", + "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helpers": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7", + "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/core/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, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", + "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", + "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/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, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", + "dev": true, + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", + "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", + "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", + "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", + "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", + "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.7.tgz", + "integrity": "sha512-fOPQYbGSgH0HUp4UJO4sMBFjY6DuWq+2i8rixyUMb3CdGixs/gccURvYOAhajBdKDoGajFr3mUq5rH3phtkGzw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.7.tgz", + "integrity": "sha512-J2z+MWzZHVOemyLweMqngXrgGC42jQ//R0KdxqkIz/OrbVIIlhFI3WigZ5fO+nwFvBlncr4MGapd8vTyc7RPNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", + "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", + "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", + "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.1.tgz", + "integrity": "sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.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": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/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, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/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, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.2.tgz", + "integrity": "sha512-+2XpQV9LLZeanU4ZevzRnGFg2neDeKHgFLjP6YLW+tly0IvrhqT4u8enLGjLH3qeh85g19xY5rsAusfwTdn5lg==", + "dependencies": { + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.5.tgz", + "integrity": "sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw==", + "dependencies": { + "@floating-ui/core": "^1.0.0", + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.0.tgz", + "integrity": "sha512-lNzj5EQmEKn5FFKc04+zasr09h/uX8RtJRNj5gUXsSQIXHVWTVh+hVAg1vOMCexkX8EgvemMvIFpQfkosnVNyA==", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.2.tgz", + "integrity": "sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==" + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/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, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/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, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "dev": true + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "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, + "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, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "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, + "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, + "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, + "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, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@radix-ui/colors": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-3.0.0.tgz", + "integrity": "sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==" + }, + "node_modules/@radix-ui/number": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.1.tgz", + "integrity": "sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", + "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/@radix-ui/react-accessible-icon": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.0.3.tgz", + "integrity": "sha512-duVGKeWPSUILr/MdlPxV+GeULTc2rS1aihGdQ3N2qCUPMgxYLxvAsHJM3mCVLF8d5eK+ympmB22mb1F3a5biNw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-visually-hidden": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.0.5.tgz", + "integrity": "sha512-OrVIOcZL0tl6xibeuGt5/+UxoT2N27KCFOPjFyfXMnchxSHZ/OW7cCX2nGlIYJrbHK/fczPcFzAwvNBB6XBNMA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dialog": "1.0.5", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz", + "integrity": "sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.0.3.tgz", + "integrity": "sha512-fXR5kbMan9oQqMuacfzlGG/SQMcmMlZ4wrvpckv8SgUulD0MMpspxJrxg/Gp/ISV3JfV1AeSWTYK9GvxA4ySwA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.0.4.tgz", + "integrity": "sha512-kVK2K7ZD3wwj3qhle0ElXhOjbezIgyl2hVvgwfIdexL3rN6zJmy5AqqIf+D31lxVppdzV8CjAfZ6PklkmInZLw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.0.4.tgz", + "integrity": "sha512-CBuGQa52aAYnADZVt/KBQzXrwx6TqnlwtcIPGtVt5JkkzQwMOLJjPukimhfKEr4GQNd43C+djUh5Ikopj8pSLg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-previous": "1.0.1", + "@radix-ui/react-use-size": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.0.3.tgz", + "integrity": "sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz", + "integrity": "sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", + "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", + "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.1.5.tgz", + "integrity": "sha512-R5XaDj06Xul1KGb+WP8qiOh7tKJNz2durpLBXAGZjSVtctcRFCuEvy2gtMwRJGePwQQE5nV77gs4FwRi8T+r2g==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-menu": "2.0.6", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz", + "integrity": "sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-focus-guards": "1.0.1", + "@radix-ui/react-focus-scope": "1.0.4", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-controllable-state": "1.0.1", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.1.tgz", + "integrity": "sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz", + "integrity": "sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-escape-keydown": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.6.tgz", + "integrity": "sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-menu": "2.0.6", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz", + "integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz", + "integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-form": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.0.3.tgz", + "integrity": "sha512-kgE+Z/haV6fxE5WqIXj05KkaXa3OkZASoTDy25yX2EIp/x0c54rOH/vFr5nOZTg7n7T1z8bSyXmiVIFP9bbhPQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-label": "2.0.2", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.0.7.tgz", + "integrity": "sha512-OcUN2FU0YpmajD/qkph3XzMcK/NmSk9hGWnjV68p6QiZMgILugusgQwnLSDs3oFSJYGKf3Y49zgFedhGh04k9A==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-popper": "1.1.3", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", + "integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.0.2.tgz", + "integrity": "sha512-N5ehvlM7qoTLx7nWPodsPYPgMzA5WM8zZChQg8nyFJKnDO5WHdba1vv5/H6IO5LtJMfD2Q3wh1qHFGNtK0w3bQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.0.6.tgz", + "integrity": "sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-focus-guards": "1.0.1", + "@radix-ui/react-focus-scope": "1.0.4", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-popper": "1.1.3", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-roving-focus": "1.0.4", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-callback-ref": "1.0.1", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.1.4.tgz", + "integrity": "sha512-Cc+seCS3PmWmjI51ufGG7zp1cAAIRqHVw7C9LOA2TZ+R4hG6rDvHcTqIsEEFLmZO3zNVH72jOOE7kKNy8W+RtA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1", + "@radix-ui/react-use-previous": "1.0.1", + "@radix-ui/react-visually-hidden": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.0.7.tgz", + "integrity": "sha512-shtvVnlsxT6faMnK/a7n0wptwBD23xc1Z5mdrtKLwVEfsEMXodS0r5s0/g5P0hX//EKYZS2sxUjqfzlg52ZSnQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-focus-guards": "1.0.1", + "@radix-ui/react-focus-scope": "1.0.4", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-popper": "1.1.3", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-controllable-state": "1.0.1", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.3.tgz", + "integrity": "sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1", + "@radix-ui/react-use-rect": "1.0.1", + "@radix-ui/react-use-size": "1.0.1", + "@radix-ui/rect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz", + "integrity": "sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz", + "integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", + "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.0.3.tgz", + "integrity": "sha512-5G6Om/tYSxjSeEdrb1VfKkfZfn/1IlPWd731h2RfPuSbIfNUgfqAwbKfJCg/PP6nuUCTrYzalwHSpSinoWoCag==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.1.3.tgz", + "integrity": "sha512-x+yELayyefNeKeTx4fjK6j99Fs6c4qKm3aY38G3swQVTN6xMpsrbigC0uHs2L//g8q4qR7qOcww8430jJmi2ag==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-roving-focus": "1.0.4", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-previous": "1.0.1", + "@radix-ui/react-use-size": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz", + "integrity": "sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.0.5.tgz", + "integrity": "sha512-b6PAgH4GQf9QEn8zbT2XUHpW5z8BzqEc7Kl11TwDrvuTrxlkcjTD5qa/bxgKr+nmuXKu4L/W5UZ4mlP/VG/5Gw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/number": "1.0.1", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-1.2.2.tgz", + "integrity": "sha512-zI7McXr8fNaSrUY9mZe4x/HC0jTLY9fWNhO1oLWYMQGDXuV4UCivIGTxwioSzO0ZCYX9iSLyWmAh/1TOmX3Cnw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/number": "1.0.1", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.4", + "@radix-ui/react-focus-guards": "1.0.1", + "@radix-ui/react-focus-scope": "1.0.3", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-popper": "1.1.2", + "@radix-ui/react-portal": "1.0.3", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1", + "@radix-ui/react-use-previous": "1.0.1", + "@radix-ui/react-visually-hidden": "1.0.3", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.4.tgz", + "integrity": "sha512-7UpBa/RKMoHJYjie1gkF1DlK8l1fdU/VKDpoS3rCCo8YBJR294GwcEHyxHw72yvphJ7ld0AXEcSLAzY2F/WyCg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-escape-keydown": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-scope": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.3.tgz", + "integrity": "sha512-upXdPfqI4islj2CslyfUBNlaJCPybbqRHAi1KER7Isel9Q2AtSJ0zRBZv8mWQiFXD2nyAJ4BhC3yXgZ6kMBSrQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-popper": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.2.tgz", + "integrity": "sha512-1CnGGfFi/bbqtJZZ0P/NQY20xdG3E0LALJaLUEoKwPLwl6PPPfbeiCqMVQnhoFRAxjJj4RpBRJzDmUgsex2tSg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1", + "@radix-ui/react-use-rect": "1.0.1", + "@radix-ui/react-use-size": "1.0.1", + "@radix-ui/rect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-portal": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.3.tgz", + "integrity": "sha512-xLYZeHrWoPmA5mEKEfZZevoVRK/Q43GfzRXkWV6qawIWWK8t6ifIiLQdd7rmQ4Vk1bmI21XhqF9BN3jWf+phpA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.1.2.tgz", + "integrity": "sha512-NKs15MJylfzVsCagVSWKhGGLNR1W9qWs+HtgbmjjVUB3B9+lb3PYoXxVju3kOrpf0VKyVCtZp+iTwVoqpa1Chw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/number": "1.0.1", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1", + "@radix-ui/react-use-previous": "1.0.1", + "@radix-ui/react-use-size": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", + "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.0.3.tgz", + "integrity": "sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-previous": "1.0.1", + "@radix-ui/react-use-size": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.0.4.tgz", + "integrity": "sha512-egZfYY/+wRNCflXNHx+dePvnz9FbmssDTJBtgRfDY7e8SE5oIo3Py2eCB1ckAbh1Q7cQ/6yJZThJ++sgbxibog==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-roving-focus": "1.0.4", + "@radix-ui/react-use-controllable-state": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.1.5.tgz", + "integrity": "sha512-fRLn227WHIBRSzuRzGJ8W+5YALxofH23y0MlPLddaIpLpCDqdE0NZlS2NRQDRiptfxDeeCjgFIpexB1/zkxDlw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1", + "@radix-ui/react-visually-hidden": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.0.3.tgz", + "integrity": "sha512-Pkqg3+Bc98ftZGsl60CLANXQBBQ4W3mTFS9EJvNxKMZ7magklKV69/id1mlAlOFDDfHvlCms0fx8fA4CMKDJHg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.0.4.tgz", + "integrity": "sha512-Uaj/M/cMyiyT9Bx6fOZO0SAG4Cls0GptBWiBmBxofmDbNVnYYoyRWj/2M/6VCi/7qcXFWnHhRUfdfZFvvkuu8A==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-roving-focus": "1.0.4", + "@radix-ui/react-toggle": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz", + "integrity": "sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-popper": "1.1.3", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-visually-hidden": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", + "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", + "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz", + "integrity": "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", + "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.0.1.tgz", + "integrity": "sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz", + "integrity": "sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/rect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz", + "integrity": "sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.3.tgz", + "integrity": "sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.0.1.tgz", + "integrity": "sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/@radix-ui/themes": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/themes/-/themes-3.0.5.tgz", + "integrity": "sha512-fqIxdxer2tVHtrmuT/Wx793sMKIaJBSZQjFSrkorz6BgOCrijjK6pRRi1qa3Sq78vyjixVXR7kRlieBomUzzzQ==", + "dependencies": { + "@radix-ui/colors": "^3.0.0", + "@radix-ui/primitive": "^1.0.1", + "@radix-ui/react-accessible-icon": "^1.0.3", + "@radix-ui/react-alert-dialog": "^1.0.5", + "@radix-ui/react-aspect-ratio": "^1.0.3", + "@radix-ui/react-avatar": "^1.0.4", + "@radix-ui/react-checkbox": "^1.0.4", + "@radix-ui/react-compose-refs": "^1.0.1", + "@radix-ui/react-context": "^1.0.1", + "@radix-ui/react-context-menu": "^2.1.5", + "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-direction": "^1.0.1", + "@radix-ui/react-dropdown-menu": "^2.0.6", + "@radix-ui/react-form": "^0.0.3", + "@radix-ui/react-hover-card": "^1.0.7", + "@radix-ui/react-navigation-menu": "^1.1.4", + "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-portal": "^1.0.4", + "@radix-ui/react-primitive": "^1.0.3", + "@radix-ui/react-progress": "^1.0.3", + "@radix-ui/react-radio-group": "^1.1.3", + "@radix-ui/react-roving-focus": "^1.0.4", + "@radix-ui/react-scroll-area": "^1.0.5", + "@radix-ui/react-select": "^2.0.0", + "@radix-ui/react-slider": "^1.1.2", + "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-switch": "^1.0.3", + "@radix-ui/react-tabs": "^1.0.4", + "@radix-ui/react-toggle-group": "^1.0.4", + "@radix-ui/react-tooltip": "^1.0.7", + "@radix-ui/react-use-callback-ref": "^1.0.1", + "@radix-ui/react-use-controllable-state": "^1.0.1", + "@radix-ui/react-visually-hidden": "^1.0.3", + "classnames": "^2.3.2", + "react-remove-scroll-bar": "2.3.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/themes/node_modules/@radix-ui/react-select": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.0.0.tgz", + "integrity": "sha512-RH5b7af4oHtkcHS7pG6Sgv5rk5Wxa7XI8W5gvB1N/yiuDGZxko1ynvOiVhFM7Cis2A8zxF9bTOUVbRDzPepe6w==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/number": "1.0.1", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-focus-guards": "1.0.1", + "@radix-ui/react-focus-scope": "1.0.4", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-popper": "1.1.3", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1", + "@radix-ui/react-use-previous": "1.0.1", + "@radix-ui/react-visually-hidden": "1.0.3", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@remix-run/router": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz", + "integrity": "sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", + "integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz", + "integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz", + "integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz", + "integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz", + "integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz", + "integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz", + "integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz", + "integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz", + "integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz", + "integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz", + "integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", + "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz", + "integrity": "sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz", + "integrity": "sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz", + "integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz", + "integrity": "sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tanstack/query-core": { + "version": "4.36.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.36.1.tgz", + "integrity": "sha512-DJSilV5+ytBP1FbFcEJovv4rnnm/CokuVvrBEtW/Va9DvuJ3HksbXUJEpI0aV1KtuL4ZoO9AVE6PyNLzF7tLeA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "4.36.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.36.1.tgz", + "integrity": "sha512-y7ySVHFyyQblPl3J3eQBWpXZkliroki3ARnBKsdJchlgt7yJLRDUcf4B8soufgiYt3pEQIkBWBx1N9/ZPIeUWw==", + "dependencies": { + "@tanstack/query-core": "4.36.1", + "use-sync-external-store": "^1.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-native": "*" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "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, + "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, + "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, + "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, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@types/prop-types": { + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", + "devOptional": true + }, + "node_modules/@types/react": { + "version": "18.3.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", + "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "devOptional": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "devOptional": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-i18next": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@types/react-i18next/-/react-i18next-8.1.0.tgz", + "integrity": "sha512-d4xhcjX5b3roNMObRNMfb1HinHQlQLPo8xlDj60dnHeeAw2bBymR2cy/l1giJpHzo/ZFgSvgVUvIWr4kCrenCg==", + "deprecated": "This is a stub types definition. react-i18next provides its own type definitions, so you do not need this installed.", + "dev": true, + "dependencies": { + "react-i18next": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.12.0.tgz", + "integrity": "sha512-7F91fcbuDf/d3S8o21+r3ZncGIke/+eWk0EpO21LXhDfLahriZF9CGj4fbAetEjlaBdjdSm9a6VeXbpbT6Z40Q==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.12.0", + "@typescript-eslint/type-utils": "7.12.0", + "@typescript-eslint/utils": "7.12.0", + "@typescript-eslint/visitor-keys": "7.12.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.12.0.tgz", + "integrity": "sha512-dm/J2UDY3oV3TKius2OUZIFHsomQmpHtsV0FTh1WO8EKgHLQ1QCADUqscPgTpU+ih1e21FQSRjXckHn3txn6kQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "7.12.0", + "@typescript-eslint/types": "7.12.0", + "@typescript-eslint/typescript-estree": "7.12.0", + "@typescript-eslint/visitor-keys": "7.12.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.12.0.tgz", + "integrity": "sha512-itF1pTnN6F3unPak+kutH9raIkL3lhH1YRPGgt7QQOh43DQKVJXmWkpb+vpc/TiDHs6RSd9CTbDsc/Y+Ygq7kg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.12.0", + "@typescript-eslint/visitor-keys": "7.12.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.12.0.tgz", + "integrity": "sha512-lib96tyRtMhLxwauDWUp/uW3FMhLA6D0rJ8T7HmH7x23Gk1Gwwu8UZ94NMXBvOELn6flSPiBrCKlehkiXyaqwA==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "7.12.0", + "@typescript-eslint/utils": "7.12.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.12.0.tgz", + "integrity": "sha512-o+0Te6eWp2ppKY3mLCU+YA9pVJxhUJE15FV7kxuD9jgwIAa+w/ycGJBMrYDTpVGUM/tgpa9SeMOugSabWFq7bg==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.12.0.tgz", + "integrity": "sha512-5bwqLsWBULv1h6pn7cMW5dXX/Y2amRqLaKqsASVwbBHMZSnHqE/HN4vT4fE0aFsiwxYvr98kqOWh1a8ZKXalCQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.12.0", + "@typescript-eslint/visitor-keys": "7.12.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.12.0.tgz", + "integrity": "sha512-Y6hhwxwDx41HNpjuYswYp6gDbkiZ8Hin9Bf5aJQn1bpTs3afYY4GX+MPYxma8jtoIV2GRwTM/UJm/2uGCVv+DQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.12.0", + "@typescript-eslint/types": "7.12.0", + "@typescript-eslint/typescript-estree": "7.12.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.12.0.tgz", + "integrity": "sha512-uZk7DevrQLL3vSnfFl5bj4sL75qC9D6EdjemIdbtkuUmIheWpuiiylSY01JxJE7+zGrOWDZrp1WxOuDntvKrHQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.12.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.0.tgz", + "integrity": "sha512-KcEbMsn4Dpk+LIbHMj7gDPRKaTMStxxWRkRmxsg/jVdFdJCZWt1SchZcf0M4t8lIKdwwMsEyzhrcOXRrDPtOBw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.24.5", + "@babel/plugin-transform-react-jsx-self": "^7.24.5", + "@babel/plugin-transform-react-jsx-source": "^7.24.1", + "@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" + } + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "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, + "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, + "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-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "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 + }, + "node_modules/aria-hidden": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", + "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.19", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", + "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-lite": "^1.0.30001599", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "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 + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "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, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "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" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "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, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001629", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001629.tgz", + "integrity": "sha512-c3dl911slnQhmxUIT4HhYzT7wnBK/XYpGnYLOj4nJBaRiw52Ibe7YxlDaAeRECvA786zCuExhxIUJ2K7nHMrBw==", + "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" + } + ] + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/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, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "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 + }, + "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 + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "devOptional": true + }, + "node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "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 + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.4.791", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.791.tgz", + "integrity": "sha512-6FlqP0NSWvxFf1v+gHu+LCn5wjr1pmkj5nPr7BsxPnj41EDR4EWhK/KmQN0ytHUqgTR1lkpHRYxvHBLZFQtkKw==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.7.tgz", + "integrity": "sha512-yrj+KInFmwuQS2UQcg1SF83ha1tuHC1jMQbRNyuWtlEzzKRDgAl7L4Yp4NlDUZTZNlWvHEzOtJhMi40R7JxcSw==", + "dev": true, + "peerDependencies": { + "eslint": ">=7" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "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, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/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, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/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, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/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, + "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/eslint/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, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/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 + }, + "node_modules/eslint/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, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/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, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/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, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/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, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "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, + "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, + "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, + "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 + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "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.4" + }, + "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, + "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 + }, + "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 + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.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, + "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, + "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": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "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, + "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, + "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, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "engines": { + "node": ">=6" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/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, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/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, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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, + "engines": { + "node": ">=4" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/i18next": { + "version": "23.11.5", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.11.5.tgz", + "integrity": "sha512-41pvpVbW9rhZPk5xjCX2TPJi2861LEig/YRhUkY+1FQ2IQPS0bKUDYnEqY8XPPbB48h1uIwLnP9iiEfuSl20CA==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.0.tgz", + "integrity": "sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/immutable": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz", + "integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==", + "dev": true + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "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, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/jackspeak": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.0.tgz", + "integrity": "sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.6", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", + "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "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==" + }, + "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, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "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 + }, + "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 + }, + "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 + }, + "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, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "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, + "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, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "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, + "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 + }, + "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==", + "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, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "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, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "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 + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "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/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, + "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, + "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, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "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 + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "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" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", + "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/postcss-nested": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.11" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz", + "integrity": "sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "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, + "engines": { + "node": ">= 0.8.0" + } + }, + "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, + "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" + } + ] + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-i18next": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.1.2.tgz", + "integrity": "sha512-FSIcJy6oauJbGEXfhUgVeLzvWBhIBIS+/9c6Lj4niwKZyGaGb4V4vUbATXSlsHJDXXB+ociNxqFNiFuV1gmoqg==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 23.2.3", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/react-icons": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.12.0.tgz", + "integrity": "sha512-IBaDuHiShdZqmfc/TwHu6+d6k2ltNCf3AszxNmjJc1KUfXdEeRJOKyNvLmAHaarhzGmTSVygNdyu8/opXv2gaw==", + "peerDependencies": { + "react": "*" + } + }, + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", + "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==", + "dependencies": { + "react-remove-scroll-bar": "^2.3.3", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz", + "integrity": "sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==", + "dependencies": { + "react-style-singleton": "^2.2.1", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-router": { + "version": "6.23.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.23.1.tgz", + "integrity": "sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ==", + "dependencies": { + "@remix-run/router": "1.16.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.23.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.23.1.tgz", + "integrity": "sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ==", + "dependencies": { + "@remix-run/router": "1.16.1", + "react-router": "6.23.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", + "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==", + "dependencies": { + "get-nonce": "^1.0.0", + "invariant": "^2.2.4", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "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, + "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, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz", + "integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.18.0", + "@rollup/rollup-android-arm64": "4.18.0", + "@rollup/rollup-darwin-arm64": "4.18.0", + "@rollup/rollup-darwin-x64": "4.18.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.18.0", + "@rollup/rollup-linux-arm-musleabihf": "4.18.0", + "@rollup/rollup-linux-arm64-gnu": "4.18.0", + "@rollup/rollup-linux-arm64-musl": "4.18.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.18.0", + "@rollup/rollup-linux-riscv64-gnu": "4.18.0", + "@rollup/rollup-linux-s390x-gnu": "4.18.0", + "@rollup/rollup-linux-x64-gnu": "4.18.0", + "@rollup/rollup-linux-x64-musl": "4.18.0", + "@rollup/rollup-win32-arm64-msvc": "4.18.0", + "@rollup/rollup-win32-ia32-msvc": "4.18.0", + "@rollup/rollup-win32-x64-msvc": "4.18.0", + "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" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/sass": { + "version": "1.77.4", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.4.tgz", + "integrity": "sha512-vcF3Ckow6g939GMA4PeU7b2K/9FALXk2KF9J87txdHzXbUF9XRQRwSxcAs/fGaTnJeBFd7UoV22j3lzMLdM0Pw==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", + "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "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, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz", + "integrity": "sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.0", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "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, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + }, + "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, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", + "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "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" + } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.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, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz", + "integrity": "sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", + "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", + "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/vite": { + "version": "5.2.12", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.12.tgz", + "integrity": "sha512-/gC8GxzxMK5ntBwb48pR32GGhENnjtY30G4A0jemunsBkiEZFw60s8InGpN8gkhHEkjnRK1aSAxeQgwvFhUHAA==", + "dev": true, + "dependencies": { + "esbuild": "^0.20.1", + "postcss": "^8.4.38", + "rollup": "^4.13.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.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", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/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, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/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, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/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 + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yaml": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz", + "integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "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, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/GUI/package.json b/GUI/package.json new file mode 100644 index 00000000..a8a4a7c6 --- /dev/null +++ b/GUI/package.json @@ -0,0 +1,49 @@ +{ + "name": "est-gov-classifier", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "@radix-ui/react-accessible-icon": "^1.0.1", + "@radix-ui/react-collapsible": "^1.0.1", + "@radix-ui/react-dialog": "^1.0.2", + "@radix-ui/react-popover": "^1.0.2", + "@radix-ui/react-select": "^1.1.2", + "@radix-ui/react-switch": "^1.0.1", + "@radix-ui/react-tabs": "^1.0.1", + "@radix-ui/react-toast": "^1.1.2", + "@radix-ui/themes": "^3.0.5", + "@tanstack/react-query": "^4.20.4", + "clsx": "^2.1.1", + "i18next": "^23.11.5", + "i18next-browser-languagedetector": "^8.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-i18next": "^14.1.2", + "react-icons": "^4.10.1", + "react-router-dom": "^6.23.1" + }, + "devDependencies": { + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22", + "@types/react-i18next": "^8.1.0", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/parser": "^7.2.0", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.19", + "eslint": "^8.57.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.6", + "postcss": "^8.4.38", + "sass": "^1.77.4", + "tailwindcss": "^3.4.4", + "typescript": "^5.2.2", + "vite": "^5.2.0" + } +} diff --git a/GUI/postcss.config.js b/GUI/postcss.config.js new file mode 100644 index 00000000..2e7af2b7 --- /dev/null +++ b/GUI/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/GUI/public/vite.svg b/GUI/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/GUI/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/GUI/src/App.css b/GUI/src/App.css new file mode 100644 index 00000000..c15efd46 --- /dev/null +++ b/GUI/src/App.css @@ -0,0 +1,8 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + + diff --git a/GUI/src/App.tsx b/GUI/src/App.tsx new file mode 100644 index 00000000..4157d1f1 --- /dev/null +++ b/GUI/src/App.tsx @@ -0,0 +1,18 @@ +// src/App.js + +import { Navigate, Route, Routes } from "react-router-dom"; +import Layout from "./components/Layout"; +import Home from "./pages/Home"; + +const App = () => { + return ( + + }> + } /> + } /> + + + ); +}; + +export default App; diff --git a/GUI/src/assets/react.svg b/GUI/src/assets/react.svg new file mode 100644 index 00000000..6c87de9b --- /dev/null +++ b/GUI/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/GUI/src/components/Layout/Layout.scss b/GUI/src/components/Layout/Layout.scss new file mode 100644 index 00000000..d4d47f3b --- /dev/null +++ b/GUI/src/components/Layout/Layout.scss @@ -0,0 +1,20 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; + +.layout{ + display: 'flex'; + flex-direction: 'column'; + height: '100vh' +} + +.body{ + margin-top: '60px' +} + +.main{ + flex-grow: 1; + padding: '20px'; + background: '#f0f0f0'; + margin-left: '60px' +} + diff --git a/GUI/src/components/Layout/index.tsx b/GUI/src/components/Layout/index.tsx new file mode 100644 index 00000000..58e08d4f --- /dev/null +++ b/GUI/src/components/Layout/index.tsx @@ -0,0 +1,22 @@ +import { FC } from 'react'; +import { Outlet } from 'react-router-dom'; +import './Layout.scss'; +import Sidebar from '../molecules/SideBar'; +import Header from '../molecules/Header'; + +const Layout: FC = () => { + return ( +
+ +
+
+
+ +
+
+
+ + ); +}; + +export default Layout; diff --git a/GUI/src/components/atoms/Button/Button.scss b/GUI/src/components/atoms/Button/Button.scss new file mode 100644 index 00000000..fa0b5b11 --- /dev/null +++ b/GUI/src/components/atoms/Button/Button.scss @@ -0,0 +1,150 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/other'; +@import 'src/styles/settings/variables/typography'; + +.btn { + $self: &; + appearance: none; + display: inline-flex; + align-items: center; + background: none; + border: 0; + color: get-color(black-coral-0); + cursor: pointer; + font: inherit; + gap: get-spacing(rapla); + overflow: visible; + padding: 8px 40px; + text-decoration: none; + font-size: $veera-font-size-100; + line-height: 24px; + border-radius: 20px; + white-space: nowrap; + + &:focus { + outline: none; + } + + &--disabled { + cursor: not-allowed; + } + + &--primary { + background-color: get-color(sapphire-blue-10); + + &:hover, + &:active { + background-color: get-color(sapphire-blue-13); + } + + &:focus { + box-shadow: inset 0 0 0 2px get-color(sapphire-blue-3) + } + + &#{$self}--disabled { + background-color: get-color(black-coral-2); + color: get-color(white); + } + } + + &--secondary { + background-color: get-color(white); + box-shadow: inset 0 0 0 2px get-color(black-coral-10); + color: get-color(black-coral-15); + + &:hover, + &:active { + box-shadow: inset 0 0 0 2px get-color(black-coral-2); + } + + &:focus { + box-shadow: inset 0 0 0 2px get-color(sapphire-blue-10); + } + + &#{$self}--disabled { + background-color: get-color(black-coral-2); + color: get-color(black-coral-6); + box-shadow: inset 0 0 0 2px get-color(black-coral-2); + } + } + + &--text { + padding: 0; + background: none; + color: get-color(sapphire-blue-10); + gap: 4px; + border-radius: 0; + + &:hover, + &:active { + text-decoration: underline; + } + + &:focus { + box-shadow: inset 0 0 0 2px get-color(sapphire-blue-10); + } + + &#{$self}--disabled { + color: get-color(black-coral-6); + } + } + + &--icon { + width: 36px; + height: 36px; + padding: 0; + justify-content: center; + color: get-color(black-coral-10); + font-size: 24px; + + &:hover, + &:active { + color: get-color(sapphire-blue-10); + } + + &:focus { + color: get-color(sapphire-blue-10); + box-shadow: inset 0 0 0 2px get-color(sapphire-blue-10); + } + } + + &--error { + background-color: get-color(jasper-10); + + &:hover, + &:active { + background-color: get-color(jasper-12); + } + + &:focus { + box-shadow: inset 0 0 0 2px get-color(jasper-13); + } + + &#{$self}--disabled { + background-color: get-color(black-coral-2); + } + } + + &--success { + background-color: get-color(sea-green-10); + + &:hover, + &:active { + background-color: get-color(sea-green-12); + } + + &:focus { + background-color: get-color(sea-green-10); + box-shadow: inset 0 0 0 2px get-color(sea-green-12); + } + + &#{$self}--disabled { + background-color: get-color(black-coral-2); + } + } + + &--s { + padding: 4.5px 24px; + } +} diff --git a/GUI/src/components/atoms/Button/index.tsx b/GUI/src/components/atoms/Button/index.tsx new file mode 100644 index 00000000..7b520d83 --- /dev/null +++ b/GUI/src/components/atoms/Button/index.tsx @@ -0,0 +1,41 @@ +import { ButtonHTMLAttributes, FC, PropsWithChildren, useRef } from 'react'; +import clsx from 'clsx'; + +import './Button.scss'; + +type ButtonProps = ButtonHTMLAttributes & { + appearance?: 'primary' | 'secondary' | 'text' | 'icon' | 'error' | 'success'; + size?: 'm' | 's'; + disabledWithoutStyle?: boolean; +}; + +const Button: FC> = ({ + appearance = 'primary', + size = 'm', + disabled, + disabledWithoutStyle = false, + children, + ...rest +}) => { + const ref = useRef(null); + + const buttonClasses = clsx( + 'btn', + `btn--${appearance}`, + `btn--${size}`, + disabled && 'btn--disabled' + ); + + return ( + + ); +}; + +export default Button; diff --git a/GUI/src/components/atoms/CheckBox/index.tsx b/GUI/src/components/atoms/CheckBox/index.tsx new file mode 100644 index 00000000..fbdb54ed --- /dev/null +++ b/GUI/src/components/atoms/CheckBox/index.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import * as RadixCheckbox from "@radix-ui/react-checkbox"; +import { MdCheck } from "react-icons/md"; + +interface CheckboxProps { + checked: boolean; + onCheckedChange: (checked: boolean) => void; + label: string; +} + +const Checkbox: React.FC = ({ + checked, + onCheckedChange, + label, +}) => { + return ( + + ); +}; + +export default Checkbox; diff --git a/GUI/src/components/atoms/RadioButton/index.tsx b/GUI/src/components/atoms/RadioButton/index.tsx new file mode 100644 index 00000000..b5629c65 --- /dev/null +++ b/GUI/src/components/atoms/RadioButton/index.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import * as RadixRadioGroup from '@radix-ui/react-radio-group'; + +interface RadioButtonProps { + options: { label: string, value: string }[]; + value: string; + onValueChange: (value: string) => void; + name: string; +} + +const RadioButton: React.FC = ({ options, value, onValueChange, name }) => { + return ( + + {options.map((option, index) => ( + + ))} + + ); +}; + +export default RadioButton; diff --git a/GUI/src/components/atoms/Toast/Toast.scss b/GUI/src/components/atoms/Toast/Toast.scss new file mode 100644 index 00000000..fd340916 --- /dev/null +++ b/GUI/src/components/atoms/Toast/Toast.scss @@ -0,0 +1,73 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/other'; +@import 'src/styles/settings/variables/typography'; + +.toast { + padding: 16px; + border-radius: 5px; + border: 1px solid; + display: flex; + flex-direction: column; + gap: 8px; + position: relative; + transition: opacity 0.25s ease-out; + + &__title { + display: flex; + align-items: center; + gap: 8px; + padding-right: 25px; + } + + &__list { + position: fixed; + bottom: 0; + right: 0; + display: flex; + flex-direction: column; + gap: 16px; + padding: 8px; + width: 408px; + max-width: 100vw; + z-index: 9999; + list-style: none; + } + + &__content { + font-size: $veera-font-size-80; + + a { + display: inline; + color: get-color(sapphire-blue-10); + text-decoration: underline; + } + } + + &__close { + position: absolute; + top: 16px; + right: 16px; + font-size: 20px; + } + + &--success { + border-color: get-color(sea-green-10); + background-color: get-color(sea-green-0); + } + + &--info { + border-color: get-color(sapphire-blue-10); + background-color: get-color(sapphire-blue-1); + } + + &--error { + border-color: get-color(jasper-10); + background-color: #FCEEEE; + } + + &--warning { + border-color: get-color(dark-tangerine-10); + background-color: get-color(dark-tangerine-1); + } +} diff --git a/GUI/src/components/atoms/Toast/index.tsx b/GUI/src/components/atoms/Toast/index.tsx new file mode 100644 index 00000000..4f6b5237 --- /dev/null +++ b/GUI/src/components/atoms/Toast/index.tsx @@ -0,0 +1,51 @@ +import { FC, useState } from 'react'; +import * as RadixToast from '@radix-ui/react-toast'; +import { + MdOutlineClose, + MdOutlineInfo, + MdCheckCircleOutline, + MdOutlineWarningAmber, + MdErrorOutline, +} from 'react-icons/md'; +import clsx from 'clsx'; + +import type { ToastType } from 'context/ToastContext'; +import './Toast.scss'; + +type ToastProps = { + toast: ToastType; + close: () => void; +}; + +const toastIcons = { + info: , + success: , + warning: , + error: , +}; + +const Toast: FC = ({ toast, close }) => { + const [open, setOpen] = useState(true); + + const toastClasses = clsx('toast', `toast--${toast.type}`); + + return ( + + + {toast.title} + + + {toast.message} + + + + + ); +}; + +export default Toast; diff --git a/GUI/src/components/atoms/Tooltip/Tooltip.scss b/GUI/src/components/atoms/Tooltip/Tooltip.scss new file mode 100644 index 00000000..03f15dbb --- /dev/null +++ b/GUI/src/components/atoms/Tooltip/Tooltip.scss @@ -0,0 +1,21 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/typography'; + +.tooltip { + background-color: get-color(rgb(0, 0, 0)); + padding: 4px; + border-radius: 4px; + filter: drop-shadow(0px 0px 20px rgba(0, 0, 0, 0.25)); + font-size: $veera-font-size-80; + max-width: 50vw; + + &__arrow { + fill: get-color(rgb(192, 65, 65)); + } +} + +.test{ + color: get-color(black-coral-12); + font-size: --cvi-font-size-50; +} diff --git a/GUI/src/components/atoms/Tooltip/index.tsx b/GUI/src/components/atoms/Tooltip/index.tsx new file mode 100644 index 00000000..ee004c4a --- /dev/null +++ b/GUI/src/components/atoms/Tooltip/index.tsx @@ -0,0 +1,28 @@ +import { FC, PropsWithChildren, ReactNode } from 'react'; +import * as RadixTooltip from '@radix-ui/react-tooltip'; + +import './Tooltip.scss'; + +type TooltipProps = { + content: ReactNode; +} + +const Tooltip: FC> = ({ content, children }) => { + return ( + + + + {children} + + + + {content} + + + + + + ); +}; + +export default Tooltip; diff --git a/GUI/src/components/index.tsx b/GUI/src/components/index.tsx new file mode 100644 index 00000000..9af30ad3 --- /dev/null +++ b/GUI/src/components/index.tsx @@ -0,0 +1,9 @@ +import Button from './atoms/Button'; + +import Tooltip from './atoms/Tooltip'; + + +export { + Button, + Tooltip, +}; diff --git a/GUI/src/components/molecules/Header/Header.scss b/GUI/src/components/molecules/Header/Header.scss new file mode 100644 index 00000000..81e1b518 --- /dev/null +++ b/GUI/src/components/molecules/Header/Header.scss @@ -0,0 +1,8 @@ +.header{ + height: '60px'; + background: '#282c34'; + color: 'white'; + display: 'flex'; + align-items: 'center'; + padding: '0 20px'; + } \ No newline at end of file diff --git a/GUI/src/components/molecules/Header/index.tsx b/GUI/src/components/molecules/Header/index.tsx new file mode 100644 index 00000000..8ca6bbe1 --- /dev/null +++ b/GUI/src/components/molecules/Header/index.tsx @@ -0,0 +1,12 @@ +// src/components/Header.tsx +import React from 'react'; + +const Header: React.FC = () => { + return ( +
+

Placeholder for Header

+
+ ); +}; + +export default Header; diff --git a/GUI/src/components/molecules/SideBar/SideBar.scss b/GUI/src/components/molecules/SideBar/SideBar.scss new file mode 100644 index 00000000..da6c4b69 --- /dev/null +++ b/GUI/src/components/molecules/SideBar/SideBar.scss @@ -0,0 +1,30 @@ +.sidebar_collapsed { + width: "60px"; + height: "100vh"; + background: "#333"; + color: "white"; + transition: "width 0.3s"; + overflow: "hidden"; +} + +.sidebar_expanded { + width: "200px"; + height: "100vh"; + background: "#333"; + color: "white"; + transition: "width 0.3s"; + overflow: "hidden"; + } + +.toggle_button{ + background: 'none'; + border: 'none'; + color: 'white'; + cursor: 'pointer'; + padding: '10px' +} + +.menu{ + list-style: 'none'; + padding: '0' +} \ No newline at end of file diff --git a/GUI/src/components/molecules/SideBar/index.tsx b/GUI/src/components/molecules/SideBar/index.tsx new file mode 100644 index 00000000..bd71cc13 --- /dev/null +++ b/GUI/src/components/molecules/SideBar/index.tsx @@ -0,0 +1,64 @@ +import React, { useState } from 'react'; +import { IconType } from 'react-icons'; +import * as MdIcons from 'react-icons/md'; +import menuConfig from '../../../config/menuConfig.json'; + +interface MenuItem { + title: string; + icon: string; + submenu?: MenuItem[]; +} + +const Sidebar: React.FC = () => { + const [collapsed, setCollapsed] = useState(false); + const [expandedMenus, setExpandedMenus] = useState<{ [key: number]: boolean }>({}); + + const toggleSidebar = () => { + setCollapsed(!collapsed); + }; + + const toggleMenu = (index: number) => { + setExpandedMenus(prevState => ({ + ...prevState, + [index]: !prevState[index] + })); + }; + + const renderIcon = (iconName: string): JSX.Element => { + const Icon: IconType = (MdIcons as any)[iconName]; + return ; + }; + + return ( +
+ +
    + {menuConfig.map((item: MenuItem, index: number) => ( +
  • +
    toggleMenu(index)} + > + {renderIcon(item.icon)} + {!collapsed && {item.title}} +
    + {expandedMenus[index] && item.submenu && ( +
      + {item.submenu.map((subItem, subIndex) => ( +
    • + {renderIcon(subItem.icon)} + {!collapsed && {subItem.title}} +
    • + ))} +
    + )} +
  • + ))} +
+
+ ); +}; + +export default Sidebar; diff --git a/GUI/src/config/menuConfig.json b/GUI/src/config/menuConfig.json new file mode 100644 index 00000000..e13da714 --- /dev/null +++ b/GUI/src/config/menuConfig.json @@ -0,0 +1,34 @@ +[ + { + "title": "User Management", + "icon": "MdPeople" + }, + { + "title": "Integration", + "icon": "MdSettings" + }, + { + "title": "Dataset", + "icon": "MdDashboard" + }, + { + "title": "Data Models", + "icon": "MdDashboard" + }, + { + "title": "Classes", + "icon": "MdSettings", + "submenu": [ + { "title": "Classes", "icon": "MdPersonAdd" }, + { "title": "Stop Words", "icon": "MdList" } + ] + }, + { + "title": "Incoming Texts", + "icon": "MdTextFields" + }, + { + "title": "Test Model", + "icon": "MdDashboard" + } +] diff --git a/GUI/src/context/ToastContext.tsx b/GUI/src/context/ToastContext.tsx new file mode 100644 index 00000000..5c07ef4a --- /dev/null +++ b/GUI/src/context/ToastContext.tsx @@ -0,0 +1,58 @@ +import { + createContext, + FC, + PropsWithChildren, + ReactNode, + useMemo, + useState, +} from 'react'; +import { useTranslation } from 'react-i18next'; +import * as RadixToast from '@radix-ui/react-toast'; + +import { Toast } from 'components'; +import { generateUEID } from 'utils/generateUEID'; + +export type ToastType = { + type: 'info' | 'success' | 'error' | 'warning'; + title: string; + message: ReactNode; +}; + +type ToastTypeWithId = ToastType & { id: string }; + +type ToastContextType = { + open: (toast: ToastType) => void; +}; + +export const ToastContext = createContext(null!); + +export const ToastProvider: FC = ({ children }) => { + const { t } = useTranslation(); + const [toasts, setToasts] = useState([]); + const open = (content: ToastType) => { + setToasts((prevState) => [ + ...prevState, + { id: generateUEID(), ...content }, + ]); + }; + const close = (id: string) => { + setToasts((prevState) => prevState.filter((toast) => toast.id === id)); + }; + + const contextValue = useMemo(() => ({ open }), []); + + return ( + + + {children} + {toasts.map((toast) => ( + close(toast.id)} /> + ))} + + + + ); +}; diff --git a/GUI/src/hooks/useToast.tsx b/GUI/src/hooks/useToast.tsx new file mode 100644 index 00000000..3c9a430c --- /dev/null +++ b/GUI/src/hooks/useToast.tsx @@ -0,0 +1,5 @@ +import { useContext } from 'react'; + +import { ToastContext } from '../context/ToastContext'; + +export const useToast = () => useContext(ToastContext); diff --git a/GUI/src/index.css b/GUI/src/index.css new file mode 100644 index 00000000..5378bd75 --- /dev/null +++ b/GUI/src/index.css @@ -0,0 +1,26 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +: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; +} + +body { + margin: 0; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + diff --git a/GUI/src/main.tsx b/GUI/src/main.tsx new file mode 100644 index 00000000..af402ab1 --- /dev/null +++ b/GUI/src/main.tsx @@ -0,0 +1,14 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App.tsx"; +import "./index.css"; +import "../i18n.ts"; +import { BrowserRouter } from "react-router-dom"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + + + +); diff --git a/GUI/src/pages/Home.tsx b/GUI/src/pages/Home.tsx new file mode 100644 index 00000000..6ffdd84e --- /dev/null +++ b/GUI/src/pages/Home.tsx @@ -0,0 +1,57 @@ +import { FC, useState } from "react"; +import { Button, Tooltip } from "../components"; +import Checkbox from "../components/atoms/CheckBox"; +import RadioButton from "../components/atoms/RadioButton"; + +const Home: FC = () => { + //check box + const [isChecked, setIsChecked] = useState(false); + + const handleCheckboxChange = (checked: boolean) => { + setIsChecked(checked); + }; + + //radio button + const [selectedValue, setSelectedValue] = useState("option1"); + + const handleValueChange = (value: string) => { + setSelectedValue(value); + }; + + const options = [ + { label: "Option 1", value: "option1" }, + { label: "Option 2", value: "option2" }, + ]; + + return ( +
+
This is list of components
+
+ + Tooltip + +
+
+ {" "} +
+ +
+ {" "} +
+
+ +
+
+ ); +}; + +export default Home; diff --git a/GUI/src/styles/components/_vertical-tabs.scss b/GUI/src/styles/components/_vertical-tabs.scss new file mode 100644 index 00000000..dd48d096 --- /dev/null +++ b/GUI/src/styles/components/_vertical-tabs.scss @@ -0,0 +1,119 @@ +.vertical-tabs { + display: flex; + border-radius: 4px; + border: 1px solid get-color(black-coral-2); + background-color: get-color(white); + + &__list { + display: flex; + flex-direction: column; + width: 288px; + flex-shrink: 0; + background-color: get-color(black-coral-0); + border-radius: 4px 0 0 4px; + border-right: 1px solid get-color(black-coral-7); + } + + &__list-search { + padding: get-spacing(haapsalu); + } + + &__body { + flex: 1; + display: flex; + flex-direction: column; + + &[data-state="inactive"] { + display: none; + } + } + + &__body-placeholder { + flex: 1; + position: relative; + + p { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } + } + + &__content-header { + padding: get-spacing(haapsalu); + background-color: get-color(extra-light); + border-bottom: 1px solid get-color(black-coral-2); + border-top-right-radius: 4px; + } + + &__content-footer { + display: flex; + justify-content: flex-end; + padding: get-spacing(haapsalu); + border-top: 1px solid get-color(black-coral-2); + border-bottom-right-radius: 4px; + } + + &__content { + flex: 1; + padding: get-spacing(haapsalu); + } + + &__trigger { + cursor: pointer; + padding: get-spacing(haapsalu); + font-size: $veera-font-size-100; + line-height: 24px; + color: get-color(sapphire-blue-20); + + &.active { + background-color: #FCEEEE; + box-shadow: inset 0 0 0 1px get-color(jasper-10); + animation: blink-animation 0.5s steps(100, start) 3; + -webkit-animation: blink-animation 0.5s steps(100, start) 3; + @keyframes blink-animation { + to { + background: get-color(jasper-8); + } + } + @-webkit-keyframes blink-animation { + to { + background: #FCEEEE; + } + } + } + + &[aria-selected=true] { + color: get-color(sapphire-blue-10); + background-color: get-color(white); + + &.active { + background-color: get-color(white); + box-shadow: none; + } + } + + &:first-child { + border-top-left-radius: 4px; + } + } + + &__group-header { + text-transform: uppercase; + font-weight: 700; + font-size: $veera-font-size-100; + line-height: 24px; + padding: 16px 16px 4px; + color: get-color(black-coral-20); + border-bottom: 1px solid get-color(black-coral-2); + } + + &__sub-group-header { + font-weight: 700; + line-height: 24px; + padding: 16px 16px 4px; + color: get-color(black-coral-20); + border-bottom: 1px solid get-color(black-coral-2); + } +} diff --git a/GUI/src/styles/generic/_base.scss b/GUI/src/styles/generic/_base.scss new file mode 100644 index 00000000..dfe6ac06 --- /dev/null +++ b/GUI/src/styles/generic/_base.scss @@ -0,0 +1,78 @@ +html, body, #root, #overlay-root { + height: 100%; + overflow: hidden; +} + +html { + scroll-behavior: smooth; + @media screen and (prefers-reduced-motion: reduce) { + & { + scroll-behavior: auto; + } + } +} + +body { + font-family: $font-family-base; + font-size: $veera-font-size-100; + line-height: $veera-line-height-500; + background-color: get-color(black-coral-0); + + @include veera-breakpoint-down(sm) { + background-color: get-color(black-coral-0); + } + +} + +a, +input, +select, +textarea, +button { + font-family: inherit; + transition: background-color 0.25s, color 0.25s, border-color 0.25s, box-shadow 0.25s; +} + +a { + color: get-color(black-coral-12); + text-decoration: none; + + &:hover { + text-decoration: underline; + } +} + +img { + max-width: 100%; + height: auto; +} + +button { + font-family: inherit; +} + +h1, .h1 { + font-size: $veera-font-size-350; + font-weight: $veera-font-weight-alpha; +} + +h2, .h2 { + font-size: $veera-font-size-300; +} + +h3, .h3 { + font-size: $veera-font-size-250; +} + +h4, .h4 { + font-size: $veera-font-size-220; +} + +h5, .h5 { + font-size: $veera-font-size-200; +} + +h6, .h6 { + font-size: $veera-font-size-100; + font-weight: $veera-font-weight-delta; +} diff --git a/GUI/src/styles/generic/_fonts.scss b/GUI/src/styles/generic/_fonts.scss new file mode 100644 index 00000000..3602e250 --- /dev/null +++ b/GUI/src/styles/generic/_fonts.scss @@ -0,0 +1,15 @@ +@use '@fontsource/roboto/scss/mixins' as Roboto; + +$subsets: (latin, latin-ext); + +@include Roboto.fontFace($weight: 300, $unicodeMap: $subsets); + +@include Roboto.fontFace($weight: 400, $unicodeMap: $subsets); + +@include Roboto.fontFace($weight: 400, $style: italic, $unicodeMap: $subsets); + +@include Roboto.fontFace($weight: 500, $unicodeMap: $subsets); + +@include Roboto.fontFace($weight: 700, $unicodeMap: $subsets); + +@include Roboto.fontFace($weight: 700, $style: italic, $unicodeMap: $subsets); diff --git a/GUI/src/styles/generic/_reset.scss b/GUI/src/styles/generic/_reset.scss new file mode 100644 index 00000000..def28896 --- /dev/null +++ b/GUI/src/styles/generic/_reset.scss @@ -0,0 +1,145 @@ +html, +body, +div, +span, +object, +iframe, +h1, +h2, +h3, +h4, +h5, +h6, +p, +blockquote, +pre, +abbr, +code, +em, +img, +small, +strong, +sub, +sup, +ol, +ul, +li, +fieldset, +form, +label, +legend, +table, +tbody, +tfoot, +thead, +tr, +th, +td, +article, +aside, +footer, +header, +nav, +section, +time, +audio, +video { + padding: 0; + border: 0; + margin: 0; + background: transparent; + font-size: 100%; + font-weight: inherit; + vertical-align: baseline; +} + +article, +aside, +figure, +footer, +header, +nav, +section { + display: block; +} + +html { + box-sizing: border-box; + overflow-y: scroll; +} + +*, +*::before, +*::after { + box-sizing: inherit; +} + +img, +object { + max-width: 100%; +} + +ul { + list-style: none; +} + +table { + border-collapse: collapse; + border-spacing: 0; +} + +th { + font-weight: bold; + vertical-align: bottom; +} + +td { + font-weight: normal; + vertical-align: top; +} + +input, +select { + vertical-align: middle; +} + +input[type='radio'] { + vertical-align: text-bottom; +} + +input[type='checkbox'] { + vertical-align: bottom; +} + +strong { + font-weight: bold; +} + +label, +input[type='file'] { + cursor: pointer; +} + +input, +select, +textarea { + border: 0; + margin: 0; +} + +button, +input[type='button'], +input[type='submit'] { + padding: 0; + border: 0; + border-radius: 0; + margin: 0; + background: transparent; + cursor: pointer; + text-align: left; +} + +button::-moz-focus-inner { + padding: 0; + border: 0; +} diff --git a/GUI/src/styles/main.scss b/GUI/src/styles/main.scss new file mode 100644 index 00000000..dbc5c667 --- /dev/null +++ b/GUI/src/styles/main.scss @@ -0,0 +1,21 @@ +// Settings - Sass variables and mixins +@import 'settings/variables/colors'; +@import 'settings/variables/typography'; +@import 'settings/variables/breakpoints'; +@import 'settings/variables/grid'; +@import 'settings/variables/spacing'; +@import 'settings/variables/other'; +@import 'settings/mixins'; +@import 'settings/utility-classes'; + +// Tools - Sass functions +@import 'tools/color'; +@import 'tools/spacing'; + +// Generic - global CSS styling and CSS at-rules +@import 'generic/reset'; +@import 'generic/fonts'; +@import 'generic/base'; + +// Components +@import 'components/vertical-tabs'; diff --git a/GUI/src/styles/settings/_mixins.scss b/GUI/src/styles/settings/_mixins.scss new file mode 100644 index 00000000..d58dea85 --- /dev/null +++ b/GUI/src/styles/settings/_mixins.scss @@ -0,0 +1,23 @@ +@mixin veera-screenreader-text { + position: absolute; + overflow: hidden; + clip: rect(0 0 0 0); + height: 1px; + width: 1px; + margin: -1px; + padding: 0; + border: none; + white-space: nowrap; +} + +@mixin veera-breakpoint-down($breakpoint) { + @media (max-width: #{map-get($veera-grid-breakpoints, $breakpoint)}) { + @content; + } +} + +@mixin veera-breakpoint-up($breakpoint) { + @media (min-width: #{map-get($veera-grid-breakpoints, $breakpoint) + 1px}) { + @content; + } +} diff --git a/GUI/src/styles/settings/_utility-classes.scss b/GUI/src/styles/settings/_utility-classes.scss new file mode 100644 index 00000000..e0cc532d --- /dev/null +++ b/GUI/src/styles/settings/_utility-classes.scss @@ -0,0 +1,3 @@ +.veera-screenreader-text { + @include veera-screenreader-text; +} diff --git a/GUI/src/styles/settings/variables/_breakpoints.scss b/GUI/src/styles/settings/variables/_breakpoints.scss new file mode 100644 index 00000000..bea2970d --- /dev/null +++ b/GUI/src/styles/settings/variables/_breakpoints.scss @@ -0,0 +1,9 @@ +// Grid breakpoints +$veera-grid-breakpoints: ( + xs: 0, + sm: 601px, + md: 801px, + lg: 1025px, + xl: 1281px, + xxl: 1601px, +); diff --git a/GUI/src/styles/settings/variables/_colors.scss b/GUI/src/styles/settings/variables/_colors.scss new file mode 100644 index 00000000..0b7797cc --- /dev/null +++ b/GUI/src/styles/settings/variables/_colors.scss @@ -0,0 +1,155 @@ +// Color codes +$veera-colors: ( + // Black coral + black-coral-0: #f0f0f2, + black-coral-1: #e1e2e5, + black-coral-2: #d2d3d8, + black-coral-3: #c4c5cb, + black-coral-4: #b5b6be, + black-coral-5: #a6a8b1, + black-coral-6: #9799a4, + black-coral-7: #898b97, + black-coral-8: #7a7c8a, + black-coral-9: #6b6e7d, + black-coral-10: #5d6071, + black-coral-11: #555867, + black-coral-12: #4d4f5d, + black-coral-13: #444653, + black-coral-14: #3c3e48, + black-coral-15: #33353e, + black-coral-16: #2b2c34, + black-coral-17: #22232a, + black-coral-18: #1a1b1f, + black-coral-19: #111215, + black-coral-20: #09090b, + + // Dark tangerine + dark-tangerine-0: #fff8e9, + dark-tangerine-1: #fff1d3, + dark-tangerine-2: #ffeabe, + dark-tangerine-3: #ffe4a8, + dark-tangerine-4: #ffdd92, + dark-tangerine-5: #ffd67d, + dark-tangerine-6: #ffcf67, + dark-tangerine-7: #ffc951, + dark-tangerine-8: #ffc23c, + dark-tangerine-9: #ffbb26, + dark-tangerine-10: #ffb511, + dark-tangerine-11: #e8a510, + dark-tangerine-12: #d1950e, + dark-tangerine-13: #ba840d, + dark-tangerine-14: #a3740b, + dark-tangerine-15: #8c630a, + dark-tangerine-16: #745308, + dark-tangerine-17: #5d4207, + dark-tangerine-18: #463205, + dark-tangerine-19: #2f2104, + dark-tangerine-20: #181102, + + // Jasper + jasper-0: #fbeded, + jasper-1: #f7dbdb, + jasper-2: #f4caca, + jasper-3: #f0b8b8, + jasper-4: #eca7a7, + jasper-5: #e99595, + jasper-6: #e58484, + jasper-7: #e17272, + jasper-8: #de6161, + jasper-9: #da4f4f, + jasper-10: #d73e3e, + jasper-11: #c43939, + jasper-12: #b03333, + jasper-13: #9d2e2e, + jasper-14: #892828, + jasper-15: #762222, + jasper-16: #621d1d, + jasper-17: #4f1717, + jasper-18: #3b1111, + jasper-19: #280c0c, + jasper-20: #140606, + + // Orange + orange-0: #fff3e7, + orange-1: #ffe7d0, + orange-2: #ffdcb9, + orange-3: #ffd0a2, + orange-4: #ffc58b, + orange-5: #ffb973, + orange-6: #ffae5c, + orange-7: #ffa245, + orange-8: #ff972e, + orange-9: #ff8b17, + orange-10: #ff8000, + orange-11: #e87500, + orange-12: #d16900, + orange-13: #ba5e00, + orange-14: #a35200, + orange-15: #8c4600, + orange-16: #743b00, + orange-17: #5d2f00, + orange-18: #462300, + orange-19: #2f1800, + orange-20: #180c00, + + // Sapphire blue + sapphire-blue-0: #e7f0f6, + sapphire-blue-1: #d0e1ee, + sapphire-blue-2: #B9D2E5, + sapphire-blue-3: #a2c3dd, + sapphire-blue-4: #8bb4d5, + sapphire-blue-5: #73a5cc, + sapphire-blue-6: #5c96c4, + sapphire-blue-7: #4587bc, + sapphire-blue-8: #2e78b3, + sapphire-blue-9: #1769ab, + sapphire-blue-10: #005aa3, + sapphire-blue-11: #005295, + sapphire-blue-12: #004a86, + sapphire-blue-13: #004277, + sapphire-blue-14: #003a68, + sapphire-blue-15: #003259, + sapphire-blue-16: #00294b, + sapphire-blue-17: #00213c, + sapphire-blue-18: #00192d, + sapphire-blue-19: #00111e, + sapphire-blue-20: #00090f, + + // Sea green + sea-green-0: #ecf4ef, + sea-green-1: #d9e9df, + sea-green-2: #c6ded0, + sea-green-3: #b3d3c0, + sea-green-4: #a0c8b0, + sea-green-5: #8ebda1, + sea-green-6: #7bb291, + sea-green-7: #68a781, + sea-green-8: #559c72, + sea-green-9: #429162, + sea-green-10: #308653, + sea-green-11: #2c7a4c, + sea-green-12: #286e44, + sea-green-13: #23623d, + sea-green-14: #1f5635, + sea-green-15: #1b4a2e, + sea-green-16: #163d26, + sea-green-17: #12311f, + sea-green-18: #0e2517, + sea-green-19: #091910, + sea-green-20: #050d08, + + // Other + white: #ffffff, + black: #000000, + extra-light: #f9f9f9 +); + +// CSS variables +:root { + @each $name, $color in $veera-colors { + --veera-color-#{'' + $name}: #{$color}; + } + @each $name, $color in $veera-colors { + --veera-color-rgb-#{'' + $name}: #{red($color) green($color) blue($color)}; + } +} diff --git a/GUI/src/styles/settings/variables/_grid.scss b/GUI/src/styles/settings/variables/_grid.scss new file mode 100644 index 00000000..b52bf253 --- /dev/null +++ b/GUI/src/styles/settings/variables/_grid.scss @@ -0,0 +1,3 @@ +// Grid settings +$veera-grid-columns: 12; +$veera-grid-gutter-width: 16px; diff --git a/GUI/src/styles/settings/variables/_other.scss b/GUI/src/styles/settings/variables/_other.scss new file mode 100644 index 00000000..8d5cea3c --- /dev/null +++ b/GUI/src/styles/settings/variables/_other.scss @@ -0,0 +1,16 @@ +// Border radii +$veera-radius-pill: 999px; +$veera-radius-l: 8px; +$veera-radius-m: 6px; +$veera-radius-s: 4px; +$veera-radius-xs: 2px; + +// Border widths +$veera-border-width: 2px; + +// Shadows +$veera-shadow-beta-blur: 15px; + +$veera-shadow-alpha: 0 1px 5px 0 rgba(var(--veera-color-rgb-black) / .15); +$veera-shadow-beta: 0 4px $veera-shadow-beta-blur 0 rgba(var(--veera-color-rgb-black) / .15); +$veera-shadow-gamma: 0 0 20px 0 rgba(var(--veera-color-rgb-sapphire-blue-16) / .1); diff --git a/GUI/src/styles/settings/variables/_spacing.scss b/GUI/src/styles/settings/variables/_spacing.scss new file mode 100644 index 00000000..42e1c7ee --- /dev/null +++ b/GUI/src/styles/settings/variables/_spacing.scss @@ -0,0 +1,21 @@ +$veera-spacing-unit: 4px; + +$veera-spacing-patterns: ( + 'loksa': $veera-spacing-unit, + 'paldiski': 8px, + 'rapla': 10px, + 'elva': 12px, + 'haapsalu': 16px, + 'valga': 20px, + 'kuressaare': 24px, + 'viljandi': 32px, + 'parnu': 48px, + 'narva': 60px, +); + +:root { + --veera-spacing-unit: #{$veera-spacing-unit}; + @each $name, $size in $veera-spacing-patterns { + --veera-spacing-#{'' + $name}: #{$size}; + } +} diff --git a/GUI/src/styles/settings/variables/_typography.scss b/GUI/src/styles/settings/variables/_typography.scss new file mode 100644 index 00000000..a4f9eb5c --- /dev/null +++ b/GUI/src/styles/settings/variables/_typography.scss @@ -0,0 +1,22 @@ +$font-family-base: Roboto, Arial, Helvetica, sans-serif; + +// Font sizes +$veera-font-size-50: 10px; +$veera-font-size-70: 12px; +$veera-font-size-80: 14px; +$veera-font-size-100: 16px; +$veera-font-size-200: 18px; +$veera-font-size-220: 20px; +$veera-font-size-250: 24px; +$veera-font-size-300: 28px; +$veera-font-size-350: 32px; +$veera-font-size-400: 36px; +$veera-font-size-500: 48px; + +$veera-line-height-100: 1; +$veera-line-height-500: 1.5; + +$veera-font-weight-alpha: 300; +$veera-font-weight-beta: 400; +$veera-font-weight-gamma: 500; +$veera-font-weight-delta: 700; diff --git a/GUI/src/styles/tools/_color.scss b/GUI/src/styles/tools/_color.scss new file mode 100644 index 00000000..d1f89647 --- /dev/null +++ b/GUI/src/styles/tools/_color.scss @@ -0,0 +1,4 @@ +// Returns variable as a CSS variable +@function get-color($color-name) { + @return var(--veera-color-#{'' + $color-name}); +} diff --git a/GUI/src/styles/tools/_spacing.scss b/GUI/src/styles/tools/_spacing.scss new file mode 100644 index 00000000..20ffb38e --- /dev/null +++ b/GUI/src/styles/tools/_spacing.scss @@ -0,0 +1,4 @@ + // Returns variable as a CSS variable +@function get-spacing($spacing-name) { + @return var(--veera-spacing-#{$spacing-name}); +} diff --git a/GUI/src/vite-env.d.ts b/GUI/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/GUI/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/GUI/tailwind.config.js b/GUI/tailwind.config.js new file mode 100644 index 00000000..d37737fc --- /dev/null +++ b/GUI/tailwind.config.js @@ -0,0 +1,12 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./index.html", + "./src/**/*.{js,ts,jsx,tsx}", + ], + theme: { + extend: {}, + }, + plugins: [], +} + diff --git a/GUI/tsconfig.json b/GUI/tsconfig.json new file mode 100644 index 00000000..a7fc6fbf --- /dev/null +++ b/GUI/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/GUI/tsconfig.node.json b/GUI/tsconfig.node.json new file mode 100644 index 00000000..97ede7ee --- /dev/null +++ b/GUI/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true + }, + "include": ["vite.config.ts"] +} diff --git a/GUI/vite.config.ts b/GUI/vite.config.ts new file mode 100644 index 00000000..5a33944a --- /dev/null +++ b/GUI/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], +}) From e7848f4d437cc9255856aeb02ecbd676ae6cabaa Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Wed, 12 Jun 2024 21:05:25 +0530 Subject: [PATCH 003/582] classifier-93 implement rest api inorder to communicate data through ruuter and data-mapper --- .../hbs/return_jira_issue_info.handlebars | 5 ++ DSL/DMapper/hbs/verify_signature.handlebars | 3 ++ DSL/DMapper/js/webhookValidation.js | 50 ------------------- DSL/DMapper/lib/helpers.js | 13 +++++ DSL/DMapper/lib/requestLoggerMiddleware.js | 30 +++++++++++ .../integration/jira/cloud/accept.yml | 47 +++++++++-------- constants.ini | 4 ++ docker-compose.yml | 43 ++++++++++++++++ 8 files changed, 125 insertions(+), 70 deletions(-) create mode 100644 DSL/DMapper/hbs/return_jira_issue_info.handlebars create mode 100644 DSL/DMapper/hbs/verify_signature.handlebars delete mode 100644 DSL/DMapper/js/webhookValidation.js create mode 100644 DSL/DMapper/lib/helpers.js create mode 100644 DSL/DMapper/lib/requestLoggerMiddleware.js create mode 100644 constants.ini create mode 100644 docker-compose.yml diff --git a/DSL/DMapper/hbs/return_jira_issue_info.handlebars b/DSL/DMapper/hbs/return_jira_issue_info.handlebars new file mode 100644 index 00000000..3c645326 --- /dev/null +++ b/DSL/DMapper/hbs/return_jira_issue_info.handlebars @@ -0,0 +1,5 @@ +{ + "jiraId": "{{data.key}}", + "summary": "{{data.fields.summary}}", + "projectId": "{{data.fields.project.key}}" +} diff --git a/DSL/DMapper/hbs/verify_signature.handlebars b/DSL/DMapper/hbs/verify_signature.handlebars new file mode 100644 index 00000000..4d5434e4 --- /dev/null +++ b/DSL/DMapper/hbs/verify_signature.handlebars @@ -0,0 +1,3 @@ +{ + "valid": "{{verifySignature payload headers}}" +} diff --git a/DSL/DMapper/js/webhookValidation.js b/DSL/DMapper/js/webhookValidation.js deleted file mode 100644 index 82328c3e..00000000 --- a/DSL/DMapper/js/webhookValidation.js +++ /dev/null @@ -1,50 +0,0 @@ -import crypto from "crypto-js"; - -//test code,code need to revamp - -const webhookValidation = async (payload, signature) => { - if (!signature) { - return res.status(400).json({ - status: "error", - code: 400, - message: "Missing signature", - data: null - }); - } - - try { - if (verifySignature(payload, signature)) { - // Process the webhook payload - res.status(200).json({ - status: "success", - code: 200, - message: "Signature verified", - data: null - }); - } else { - res.status(401).json({ - status: "error", - code: 401, - message: "Invalid signature", - data: null - }); - } - } catch (error) { - console.error('Error processing signature:', error); - res.status(500).json({ - status: "error", - code: 500, - message: "Internal Server Error", - data: null - }); - } -}; - -function verifySignature(payload, signature) { - const hmac = crypto.createHmac('sha256', SECRET); - hmac.update(payload); - const computedSignature = hmac.digest('hex'); - return crypto.timingSafeEqual(Buffer.from(computedSignature), Buffer.from(signature)); -} - -export default webhookValidation; diff --git a/DSL/DMapper/lib/helpers.js b/DSL/DMapper/lib/helpers.js new file mode 100644 index 00000000..1c241f0e --- /dev/null +++ b/DSL/DMapper/lib/helpers.js @@ -0,0 +1,13 @@ +import { createHmac,timingSafeEqual } from "crypto" + +export function verifySignature(payload, headers) { + const signature = headers['x-hub-signature']; + const SHARED_SECRET = 'wNc6HjKGu3RZXYNXqMTh'; + const hmac = createHmac('sha256', Buffer.from(SHARED_SECRET, 'utf8')); + const payloadString = JSON.stringify(payload); + hmac.update(Buffer.from(payloadString, 'utf8')); + const computedSignature = hmac.digest('hex'); + const computedSignaturePrefixed = "sha256=" + computedSignature; + const isValid = timingSafeEqual(Buffer.from(computedSignaturePrefixed, 'utf8'), Buffer.from(signature, 'utf8')); + return isValid; +} diff --git a/DSL/DMapper/lib/requestLoggerMiddleware.js b/DSL/DMapper/lib/requestLoggerMiddleware.js new file mode 100644 index 00000000..727a36fa --- /dev/null +++ b/DSL/DMapper/lib/requestLoggerMiddleware.js @@ -0,0 +1,30 @@ +/** + * @param res Original Response Object + * @param send Original UNMODIFIED res.send function + * @return A patched res.send which takes the send content, binds it to contentBody on + * the res and then calls the original res.send after restoring it + */ +const resDotSendInterceptor = (res, send) => (content) => { + res.contentBody = content; + res.send = send; + res.send(content); +}; + +export const requestLoggerMiddleware = + ({ logger }) => + (req, res, next) => { + logger( + `Request: {method: ${req.method}, url: ${ + req.url + }, params: ${JSON.stringify(req.params)}, query: ${JSON.stringify( + req.query + )}, body: ${JSON.stringify(req.body)}` + ); + res.send = resDotSendInterceptor(res, res.send); + res.on("finish", () => { + logger( + `Response: {statusCode: ${res.statusCode}, responseData: ${res.contentBody}}` + ); + }); + next(); + }; diff --git a/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml b/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml index 87000253..5b9a9e97 100644 --- a/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml +++ b/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml @@ -9,35 +9,42 @@ declaration: jira_webhook_data: assign: - signature: ${incoming.headers.X-Hub-Signature} + headers: ${incoming.headers} payload: ${incoming.body} issue_info: ${incoming.body.issue} - next: get_jira_signature + next: get_jira_issue_info -get_jira_signature: +verify_jira_signature: call: http.post args: - url: "[#CLASSIFIER_DMAPPER]/js/encryption/urlValidator" + url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/verify_signature" + headers: + type: json body: - signature: ${signature} payload: ${payload} - result: res + headers: ${headers} + result: valid_data -validateUrlSignature: - switch: - - condition: "${res.response.status==200 && res.response.body.isValid}" - next: get_jira_issue_info - next: return_error_found +get_jira_issue_info: + call: http.post + args: + url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/return_jira_issue_info" + headers: + type: json + body: + data: ${issue_info} + result: extract_info + next: send_issue_data -return_error_found: - return: ${res.response.code} - status: ${res.response.message} +send_issue_data: + call: http.post + args: + url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/return_jira_issue_info"# need correct url + headers: + type: json + body: + data: ${extract_info} + result: response next: end -get_jira_issue_info: - assign: - issue_info: ${issue_info} - -#payload format data mapper -#sent to private ruuter endpoint diff --git a/constants.ini b/constants.ini new file mode 100644 index 00000000..6bb36f3a --- /dev/null +++ b/constants.ini @@ -0,0 +1,4 @@ +[DSL] + +CLASSIFIER_RUUTER_PUBLIC=http://ruuter-public:8086 +CLASSIFIER_DMAPPER=http://data-mapper:3000 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..abfa12fd --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,43 @@ +services: + ruuter-public: + container_name: ruuter-public + image: ruuter + environment: + - application.cors.allowedOrigins=http://localhost:3001 + - application.httpCodesAllowList=200,201,202,400,401,403,500 + - application.internalRequests.allowedIPs=127.0.0.1 + - application.logging.displayRequestContent=true + - application.logging.displayResponseContent=true + - server.port=8086 + volumes: + - ./DSL/Ruuter.public/DSL:/DSL + - ./constants.ini:/app/constants.ini + ports: + - 8086:8086 + networks: + - bykstack + cpus: "0.5" + mem_limit: "512M" + + data-mapper: + container_name: data-mapper + image: data-mapper + environment: + - PORT=3000 + - CONTENT_FOLDER=/data + volumes: + - ./DSL:/data + - ./DSL/DMapper/hbs:/workspace/app/views/classifier + - ./DSL/DMapper/js:/workspace/app/js/classifier + - ./DSL/DMapper/lib:/workspace/app/lib + ports: + - 3000:3000 + networks: + - bykstack + +networks: + bykstack: + name: bykstack + driver: bridge + driver_opts: + com.docker.network.driver.mtu: 1400 From 6d8b8f0b29e0d5ac34912ed7bfdc8c9c1922eded Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Wed, 12 Jun 2024 22:45:51 +0530 Subject: [PATCH 004/582] feat: adding user management page initial components --- GUI/.gitignore | 1 + GUI/package-lock.json | 141 +++++++++- GUI/package.json | 5 + GUI/src/App.tsx | 3 + GUI/src/components/atoms/Button/Button.scss | 1 + GUI/src/components/atoms/Icon/Icon.scss | 17 ++ GUI/src/components/atoms/Icon/index.tsx | 26 ++ GUI/src/components/atoms/InputField/index.tsx | 27 ++ GUI/src/components/atoms/Track/index.tsx | 57 ++++ GUI/src/components/index.tsx | 5 + .../molecules/DataTable/CloseIcon.tsx | 22 ++ .../molecules/DataTable/DataTable.scss | 189 +++++++++++++ .../molecules/DataTable/DeboucedInput.scss | 11 + .../molecules/DataTable/DebouncedInput.tsx | 54 ++++ .../components/molecules/DataTable/Filter.tsx | 121 ++++++++ .../components/molecules/DataTable/index.tsx | 258 ++++++++++++++++++ GUI/src/config/users.json | 119 ++++++++ GUI/src/pages/Home.tsx | 26 ++ GUI/src/pages/UserManagement.tsx | 114 ++++++++ GUI/src/types/authorities.ts | 8 + GUI/src/types/chat.ts | 108 ++++++++ GUI/src/types/emergencyNotice.ts | 6 + GUI/src/types/establishment.ts | 4 + GUI/src/types/mainNavigation.ts | 11 + GUI/src/types/message.ts | 64 +++++ GUI/src/types/organizationWorkingTime.ts | 22 ++ GUI/src/types/router.ts | 4 + GUI/src/types/service.ts | 6 + GUI/src/types/session.ts | 7 + GUI/src/types/user.ts | 17 ++ GUI/src/types/userInfo.ts | 16 ++ GUI/src/types/userProfileSettings.ts | 10 + GUI/src/types/widgetConfig.ts | 8 + 33 files changed, 1482 insertions(+), 6 deletions(-) create mode 100644 GUI/src/components/atoms/Icon/Icon.scss create mode 100644 GUI/src/components/atoms/Icon/index.tsx create mode 100644 GUI/src/components/atoms/InputField/index.tsx create mode 100644 GUI/src/components/atoms/Track/index.tsx create mode 100644 GUI/src/components/molecules/DataTable/CloseIcon.tsx create mode 100644 GUI/src/components/molecules/DataTable/DataTable.scss create mode 100644 GUI/src/components/molecules/DataTable/DeboucedInput.scss create mode 100644 GUI/src/components/molecules/DataTable/DebouncedInput.tsx create mode 100644 GUI/src/components/molecules/DataTable/Filter.tsx create mode 100644 GUI/src/components/molecules/DataTable/index.tsx create mode 100644 GUI/src/config/users.json create mode 100644 GUI/src/pages/UserManagement.tsx create mode 100644 GUI/src/types/authorities.ts create mode 100644 GUI/src/types/chat.ts create mode 100644 GUI/src/types/emergencyNotice.ts create mode 100644 GUI/src/types/establishment.ts create mode 100644 GUI/src/types/mainNavigation.ts create mode 100644 GUI/src/types/message.ts create mode 100644 GUI/src/types/organizationWorkingTime.ts create mode 100644 GUI/src/types/router.ts create mode 100644 GUI/src/types/service.ts create mode 100644 GUI/src/types/session.ts create mode 100644 GUI/src/types/user.ts create mode 100644 GUI/src/types/userInfo.ts create mode 100644 GUI/src/types/userProfileSettings.ts create mode 100644 GUI/src/types/widgetConfig.ts diff --git a/GUI/.gitignore b/GUI/.gitignore index a547bf36..2107e24e 100644 --- a/GUI/.gitignore +++ b/GUI/.gitignore @@ -8,6 +8,7 @@ pnpm-debug.log* lerna-debug.log* node_modules +package-lock.json dist dist-ssr *.local diff --git a/GUI/package-lock.json b/GUI/package-lock.json index 575d4d50..049ad2be 100644 --- a/GUI/package-lock.json +++ b/GUI/package-lock.json @@ -8,6 +8,9 @@ "name": "est-gov-classifier", "version": "0.0.0", "dependencies": { + "@buerokratt-ria/header": "^0.1.6", + "@buerokratt-ria/menu": "^0.1.15", + "@buerokratt-ria/styles": "^0.0.1", "@radix-ui/react-accessible-icon": "^1.0.1", "@radix-ui/react-collapsible": "^1.0.1", "@radix-ui/react-dialog": "^1.0.2", @@ -17,7 +20,9 @@ "@radix-ui/react-tabs": "^1.0.1", "@radix-ui/react-toast": "^1.1.2", "@radix-ui/themes": "^3.0.5", + "@tanstack/match-sorter-utils": "^8.7.2", "@tanstack/react-query": "^4.20.4", + "@tanstack/react-table": "^8.7.4", "clsx": "^2.1.1", "i18next": "^23.11.5", "i18next-browser-languagedetector": "^8.0.0", @@ -431,6 +436,82 @@ "node": ">=6.9.0" } }, + "node_modules/@buerokratt-ria/header": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@buerokratt-ria/header/-/header-0.1.6.tgz", + "integrity": "sha512-sPynHp0LQvBdjqNma6KmAGHzfP3qnpFl4WdZDpHBRJS/f09EPlEvSfV6N6D693i9M7oXD9WhykIvthcugoeCbQ==", + "dependencies": { + "@buerokratt-ria/styles": "^0.0.1", + "@types/react": "^18.2.21", + "react": "^18.2.0" + }, + "peerDependencies": { + "@fontsource/roboto": "^4.5.8", + "@formkit/auto-animate": "^0.7.0", + "@radix-ui/react-accessible-icon": "^1.0.3", + "@radix-ui/react-dialog": "^1.0.4", + "@radix-ui/react-switch": "^1.0.3", + "@radix-ui/react-toast": "^1.1.4", + "@tanstack/react-query": "^4.32.1", + "axios": "^1.4.0", + "clsx": "^1.2.1", + "i18next": "^23.2.3", + "i18next-browser-languagedetector": "^7.1.0", + "path": "^0.12.7", + "react": "^18.2.0", + "react-cookie": "^4.1.1", + "react-dom": "^18.2.0", + "react-hook-form": "^7.45.4", + "react-i18next": "^12.1.1", + "react-icons": "^4.10.1", + "react-idle-timer": "^5.7.2", + "react-router-dom": "^6.14.2", + "rxjs": "^7.8.1", + "tslib": "^2.3.0", + "vite-plugin-dts": "^3.5.2", + "vite-plugin-svgr": "^3.2.0", + "zustand": "^4.4.0" + } + }, + "node_modules/@buerokratt-ria/menu": { + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@buerokratt-ria/menu/-/menu-0.1.16.tgz", + "integrity": "sha512-k6G9I1Y7y98E7Re8QI+vm5EjYEVpGwTP9n8aStt/ScdiWmwrw12hFd/yRLj3kO8phMvWUPUfBPuE3UajlfoesA==", + "dependencies": { + "@buerokratt-ria/styles": "^0.0.1", + "@types/react": "^18.2.21", + "react": "^18.2.0" + }, + "peerDependencies": { + "@radix-ui/react-accessible-icon": "^1.0.3", + "@radix-ui/react-dialog": "^1.0.4", + "@radix-ui/react-switch": "^1.0.3", + "@radix-ui/react-toast": "^1.1.4", + "@tanstack/react-query": "^4.32.1", + "clsx": "^1.2.1", + "i18next": "^23.2.3", + "i18next-browser-languagedetector": "^7.1.0", + "path": "^0.12.7", + "react": "^18.2.0", + "react-cookie": "^4.1.1", + "react-dom": "^18.2.0", + "react-hook-form": "^7.45.4", + "react-i18next": "^12.1.1", + "react-icons": "^4.10.1", + "react-idle-timer": "^5.7.2", + "react-router-dom": "^6.14.2", + "rxjs": "^7.8.1", + "tslib": "^2.3.0", + "vite-plugin-dts": "^3.5.2", + "vite-plugin-svgr": "^3.2.0", + "zustand": "^4.4.0" + } + }, + "node_modules/@buerokratt-ria/styles": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@buerokratt-ria/styles/-/styles-0.0.1.tgz", + "integrity": "sha512-bSj7WsdQO4P/43mRgsa5sDEwBuOebXcl3+Peur8NwToqczqsTMbXSO5P6xyXHoTnHWt082PhT8ht7OAgtFSzfw==" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", @@ -2845,6 +2926,21 @@ "win32" ] }, + "node_modules/@tanstack/match-sorter-utils": { + "version": "8.15.1", + "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.15.1.tgz", + "integrity": "sha512-PnVV3d2poenUM31ZbZi/yXkBu3J7kd5k2u51CGwwNojag451AjTH9N6n41yjXz2fpLeewleyLBmNS6+HcGDlXw==", + "dependencies": { + "remove-accents": "0.5.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tanstack/query-core": { "version": "4.36.1", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.36.1.tgz", @@ -2880,6 +2976,37 @@ } } }, + "node_modules/@tanstack/react-table": { + "version": "8.17.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.17.3.tgz", + "integrity": "sha512-5gwg5SvPD3lNAXPuJJz1fOCEZYk9/GeBFH3w/hCgnfyszOIzwkwgp5I7Q4MJtn0WECp84b5STQUDdmvGi8m3nA==", + "dependencies": { + "@tanstack/table-core": "8.17.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.17.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.17.3.tgz", + "integrity": "sha512-mPBodDGVL+fl6d90wUREepHa/7lhsghg2A3vFpakEhrhtbIlgNAZiMr7ccTgak5qbHqF14Fwy+W1yFWQt+WmYQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2930,14 +3057,12 @@ "node_modules/@types/prop-types": { "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", - "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", - "devOptional": true + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" }, "node_modules/@types/react": { "version": "18.3.3", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", - "devOptional": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -2947,7 +3072,7 @@ "version": "18.3.0", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", - "devOptional": true, + "dev": true, "dependencies": { "@types/react": "*" } @@ -3555,8 +3680,7 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "devOptional": true + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/debug": { "version": "4.3.5", @@ -5295,6 +5419,11 @@ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, + "node_modules/remove-accents": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz", + "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", diff --git a/GUI/package.json b/GUI/package.json index a8a4a7c6..a2c5598a 100644 --- a/GUI/package.json +++ b/GUI/package.json @@ -10,6 +10,9 @@ "preview": "vite preview" }, "dependencies": { + "@buerokratt-ria/header": "^0.1.6", + "@buerokratt-ria/menu": "^0.1.15", + "@buerokratt-ria/styles": "^0.0.1", "@radix-ui/react-accessible-icon": "^1.0.1", "@radix-ui/react-collapsible": "^1.0.1", "@radix-ui/react-dialog": "^1.0.2", @@ -20,6 +23,8 @@ "@radix-ui/react-toast": "^1.1.2", "@radix-ui/themes": "^3.0.5", "@tanstack/react-query": "^4.20.4", + "@tanstack/react-table": "^8.7.4", + "@tanstack/match-sorter-utils": "^8.7.2", "clsx": "^2.1.1", "i18next": "^23.11.5", "i18next-browser-languagedetector": "^8.0.0", diff --git a/GUI/src/App.tsx b/GUI/src/App.tsx index 4157d1f1..2c61bcfd 100644 --- a/GUI/src/App.tsx +++ b/GUI/src/App.tsx @@ -3,6 +3,7 @@ import { Navigate, Route, Routes } from "react-router-dom"; import Layout from "./components/Layout"; import Home from "./pages/Home"; +import UserManagement from "./pages/UserManagement"; const App = () => { return ( @@ -10,6 +11,8 @@ const App = () => { }> } /> } /> + } /> + ); diff --git a/GUI/src/components/atoms/Button/Button.scss b/GUI/src/components/atoms/Button/Button.scss index fa0b5b11..25c7a631 100644 --- a/GUI/src/components/atoms/Button/Button.scss +++ b/GUI/src/components/atoms/Button/Button.scss @@ -1,6 +1,7 @@ @import 'src/styles/tools/spacing'; @import 'src/styles/tools/color'; @import 'src/styles/settings/variables/other'; +@import 'src/styles/settings/variables/colors'; @import 'src/styles/settings/variables/typography'; .btn { diff --git a/GUI/src/components/atoms/Icon/Icon.scss b/GUI/src/components/atoms/Icon/Icon.scss new file mode 100644 index 00000000..ce570acf --- /dev/null +++ b/GUI/src/components/atoms/Icon/Icon.scss @@ -0,0 +1,17 @@ +@import 'src/styles/tools/spacing'; + +.icon { + display: inline-flex; + align-items: center; + justify-content: center; + + &--small { + width: get-spacing(haapsalu); + height: get-spacing(haapsalu); + } + + &--medium { + width: get-spacing(kuressaare); + height: get-spacing(kuressaare); + } +} diff --git a/GUI/src/components/atoms/Icon/index.tsx b/GUI/src/components/atoms/Icon/index.tsx new file mode 100644 index 00000000..d9ab3988 --- /dev/null +++ b/GUI/src/components/atoms/Icon/index.tsx @@ -0,0 +1,26 @@ +import { CSSProperties, forwardRef, ReactNode, StyleHTMLAttributes } from 'react'; +import * as AccessibleIcon from '@radix-ui/react-accessible-icon'; +import clsx from 'clsx'; + +import './Icon.scss'; + +type IconProps = StyleHTMLAttributes & { + label?: string | null; + icon: ReactNode; + size?: 'small' | 'medium'; +}; + +const Icon = forwardRef(({ label, icon, size = 'small', ...rest }, ref) => { + const iconClasses = clsx( + 'icon', + `icon--${size}`, + ); + + return ( + + {icon} + + ); +}); + +export default Icon; diff --git a/GUI/src/components/atoms/InputField/index.tsx b/GUI/src/components/atoms/InputField/index.tsx new file mode 100644 index 00000000..cb6d8560 --- /dev/null +++ b/GUI/src/components/atoms/InputField/index.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import * as RadixLabel from '@radix-ui/react-label'; + +interface InputFieldProps { + label: string; + value: string; + onChange: (e: React.ChangeEvent) => void; + placeholder?: string; + error?: string; +} + +const InputField: React.FC = ({ label, value, onChange, placeholder, error }) => { + return ( +
+ {label} + + {error && {error}} +
+ ); +}; + +export default InputField; diff --git a/GUI/src/components/atoms/Track/index.tsx b/GUI/src/components/atoms/Track/index.tsx new file mode 100644 index 00000000..2b66b6e7 --- /dev/null +++ b/GUI/src/components/atoms/Track/index.tsx @@ -0,0 +1,57 @@ +import { FC, HTMLAttributes, PropsWithChildren } from 'react'; + +type TrackProps = HTMLAttributes & { + gap?: number; + align?: 'left' | 'center' | 'right' | 'stretch'; + justify?: 'start' | 'between' | 'center' | 'around' | 'end'; + direction?: 'horizontal' | 'vertical'; + isMultiline?: boolean; +} + +const alignMap = { + left: 'flex-start', + center: 'center', + right: 'flex-end', + stretch: 'stretch', +}; + +const justifyMap = { + start: 'flex-start', + between: 'space-between', + center: 'center', + around: 'space-around', + end: 'flex-end', +}; + +const Track: FC> = ( + { + gap = 0, + align = 'center', + justify = 'start', + direction = 'horizontal', + isMultiline = false, + children, + style, + ...rest + }, +) => { + return ( +
+ {children} +
+ ); +}; + +export default Track; diff --git a/GUI/src/components/index.tsx b/GUI/src/components/index.tsx index 9af30ad3..edbf281f 100644 --- a/GUI/src/components/index.tsx +++ b/GUI/src/components/index.tsx @@ -1,9 +1,14 @@ import Button from './atoms/Button'; import Tooltip from './atoms/Tooltip'; +import Track from './atoms/Track'; +import Icon from './atoms/Icon'; + export { Button, Tooltip, + Track, + Icon }; diff --git a/GUI/src/components/molecules/DataTable/CloseIcon.tsx b/GUI/src/components/molecules/DataTable/CloseIcon.tsx new file mode 100644 index 00000000..85de2dbb --- /dev/null +++ b/GUI/src/components/molecules/DataTable/CloseIcon.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import './DeboucedInput.scss'; + +const CloseIcon: React.FC = () => ( + + + + +); + +export default CloseIcon; diff --git a/GUI/src/components/molecules/DataTable/DataTable.scss b/GUI/src/components/molecules/DataTable/DataTable.scss new file mode 100644 index 00000000..cf6b16ac --- /dev/null +++ b/GUI/src/components/molecules/DataTable/DataTable.scss @@ -0,0 +1,189 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/typography'; + +.data-table { + width: 100%; + color: get-color(black-coral-20); + text-align: left; + margin-bottom: 0; + display: table; + + &__scrollWrapper { + height: 100%; + overflow-x: auto; + white-space: nowrap; + display: block; + } + + thead, tbody { + width: 100%; + } + + th { + padding: 12px 14.5px; + color: get-color(black-coral-12); + border-bottom: 1px solid get-color(black-coral-10); + font-weight: $veera-font-weight-beta; + vertical-align: middle; + position: relative; + } + + td { + padding: 12px 24px 12px 16px; + border-bottom: 1px solid get-color(black-coral-2); + vertical-align: middle; + max-width: fit-content; + + p{ + white-space: break-spaces; + } + + .entity { + display: inline-flex; + align-items: center; + padding-left: 4px; + background-color: get-color(sapphire-blue-2); + border-radius: 4px; + + span { + display: inline-flex; + font-size: $veera-font-size-80; + background-color: get-color(white); + padding: 0 4px; + border-radius: 4px; + margin: 2px 2px 2px 4px; + } + } + } + + tbody { + tr { + &:last-child { + td { + border-bottom: 0; + } + } + } + } + + &__filter { + position: absolute; + top: 100%; + left: 0; + right: 0; + padding: get-spacing(paldiski); + background-color: get-color(white); + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.14); + border-radius: 0 0 4px 4px; + border: 1px solid get-color(black-coral-2); + + input { + width: 100%; + display: block; + appearance: none; + background-color: get-color(white); + border: 1px solid get-color(black-coral-6); + border-radius: 5px; + color: var(--color-black); + font-size: $veera-font-size-100; + height: 32px; + line-height: 24px; + padding: get-spacing(paldiski); + + &::placeholder { + color: get-color(black-coral-6); + } + + &:focus { + outline: none; + border-color: get-color(sapphire-blue-10); + } + } + } + + &__pagination-wrapper { + display: flex; + box-shadow: 0 -1px 0 get-color(black-coral-10); + padding: 6px 16px; + } + + &__pagination { + display: flex; + align-items: center; + gap: 15px; + margin: 0 auto; + + + .data-table__page-size { + margin-left: 0; + } + + .next, + .previous { + display: flex; + color: get-color(sapphire-blue-10); + + &[disabled] { + color: get-color(black-coral-11); + cursor: initial; + } + } + + .links { + display: flex; + align-items: center; + gap: 5px; + font-size: $veera-font-size-80; + color: get-color(black-coral-10); + + li { + display: block; + + a, span { + display: flex; + align-items: center; + justify-content: center; + width: 25px; + height: 25px; + border-radius: 50%; + + &:hover { + text-decoration: none; + } + } + + &.active { + a, span { + color: get-color(white); + background-color: get-color(sapphire-blue-10); + } + } + } + } + } + + &__page-size { + display: flex; + align-items: center; + gap: 8px; + font-size: $veera-font-size-80; + line-height: 16px; + color: get-color(black-coral-11); + margin-left: auto; + + select { + appearance: none; + font-size: $veera-font-size-70; + line-height: 16px; + height: 30px; + min-width: 50px; + padding: 6px 10px; + border: 1px solid #8F91A8; + border-radius: 2px; + background-color: get-color(white); + background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iNiIgdmlld0JveD0iMCAwIDEwIDYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNS4zMTMwNiA1LjgwODIyQzUuMTU2ODUgNS45NjQ0MyA0LjkwMzU4IDUuOTY0NDMgNC43NDczNyA1LjgwODIyTDAuMjgyNzMgMS4zNDM1OEMwLjEyNjUyIDEuMTg3MzcgMC4xMjY1MiAwLjkzNDEwMiAwLjI4MjczIDAuNzc3ODkzTDAuNzc3NzA0IDAuMjgyOTE4QzAuOTMzOTE0IDAuMTI2NzA4IDEuMTg3MTggMC4xMjY3MDggMS4zNDMzOSAwLjI4MjkxN0w1LjAzMDIyIDMuOTY5NzRMOC43MTcwNCAwLjI4MjkxN0M4Ljg3MzI1IDAuMTI2NzA4IDkuMTI2NTIgMC4xMjY3MDggOS4yODI3MyAwLjI4MjkxN0w5Ljc3NzcgMC43Nzc4OTJDOS45MzM5MSAwLjkzNDEwMiA5LjkzMzkxIDEuMTg3MzcgOS43Nzc3IDEuMzQzNThMNS4zMTMwNiA1LjgwODIyWiIgZmlsbD0iIzU1NTg2NyIvPgo8L3N2Zz4K'); + background-repeat: no-repeat; + background-position: top 11px right 10px; + } + } +} diff --git a/GUI/src/components/molecules/DataTable/DeboucedInput.scss b/GUI/src/components/molecules/DataTable/DeboucedInput.scss new file mode 100644 index 00000000..753f1ad0 --- /dev/null +++ b/GUI/src/components/molecules/DataTable/DeboucedInput.scss @@ -0,0 +1,11 @@ +.input-container { + position: relative; +} + +.search-icon { + position: absolute; + top: 50%; + right: 10px; + margin-left: 10px; + transform: translateY(-50%); +} diff --git a/GUI/src/components/molecules/DataTable/DebouncedInput.tsx b/GUI/src/components/molecules/DataTable/DebouncedInput.tsx new file mode 100644 index 00000000..1ad1f52f --- /dev/null +++ b/GUI/src/components/molecules/DataTable/DebouncedInput.tsx @@ -0,0 +1,54 @@ +import { FC, InputHTMLAttributes, useEffect, useState } from 'react'; +import './DeboucedInput.scss'; +import CloseIcon from './CloseIcon'; + +type DebouncedInputProps = Omit< + InputHTMLAttributes, + 'onChange' +> & { + value: string | number | string[]; + onChange: (value: string | number | string[]) => void; + debounce?: number; +}; + +const DebouncedInput: FC = ({ + value: initialValue, + onChange, + debounce = 500, + ...props +}) => { + const [value, setValue] = useState(initialValue); + + useEffect(() => { + setValue(initialValue); + }, [initialValue]); + + useEffect(() => { + const timeout = setTimeout(() => { + onChange(value); + }, debounce); + + return () => clearTimeout(timeout); + }, [value]); + + return ( +
+ setValue(e.target.value)} + /> + {value && ( + + )} +
+ ); +}; + +export default DebouncedInput; diff --git a/GUI/src/components/molecules/DataTable/Filter.tsx b/GUI/src/components/molecules/DataTable/Filter.tsx new file mode 100644 index 00000000..6622c7aa --- /dev/null +++ b/GUI/src/components/molecules/DataTable/Filter.tsx @@ -0,0 +1,121 @@ +import React, { FC, useState, MouseEvent } from 'react'; +import { Column, Table } from '@tanstack/react-table'; +import { useTranslation } from 'react-i18next'; +import { MdOutlineSearch } from 'react-icons/md'; + +import { Icon } from 'components'; +import useDocumentEscapeListener from 'hooks/useDocumentEscapeListener'; +import DebouncedInput from './DebouncedInput'; + +type FilterProps = { + column: Column; + table: Table; +}; + +const Filter: FC = ({ column, table }) => { + const { t } = useTranslation(); + const [filterOpen, setFilterOpen] = useState(false); + const firstValue = table + .getPreFilteredRowModel() + .flatRows[0]?.getValue(column.id); + + const columnFilterValue = column.getFilterValue(); + + useDocumentEscapeListener(() => setFilterOpen(false)); + + const handleFilterToggle = (e: MouseEvent) => { + e.stopPropagation(); + setFilterOpen(!filterOpen); + }; + + return ( + <> + + {filterOpen && ( +
+ {typeof firstValue === 'number' ? ( + + column.setFilterValue((old: [number, number]) => [ + value, + old?.[1], + ]) + } + /> + ) : ( + column.setFilterValue(value)} + placeholder={t('global.search') + '...'} + // list={column.id + 'list'} + /> + )} +
+ )} + + ); + + // return typeof firstValue === 'number' ? ( + //
+ //
+ // + // column.setFilterValue((old: [number, number]) => [value, old?.[1]]) + // } + // placeholder={`Min ${ + // column.getFacetedMinMaxValues()?.[0] + // ? `(${column.getFacetedMinMaxValues()?.[0]})` + // : '' + // }`} + // className='w-24 border shadow rounded' + // /> + // + // column.setFilterValue((old: [number, number]) => [old?.[0], value]) + // } + // placeholder={`Max ${ + // column.getFacetedMinMaxValues()?.[1] + // ? `(${column.getFacetedMinMaxValues()?.[1]})` + // : '' + // }`} + // className='w-24 border shadow rounded' + // /> + //
+ //
+ //
+ // ) : ( + // <> + // + // {sortedUniqueValues.slice(0, 5000).map((value: any) => ( + // + // column.setFilterValue(value)} + // placeholder={`Search... (${column.getFacetedUniqueValues().size})`} + // className='w-36 border shadow rounded' + // list={column.id + 'list'} + // /> + //
+ // + // ); +}; + +export default Filter; diff --git a/GUI/src/components/molecules/DataTable/index.tsx b/GUI/src/components/molecules/DataTable/index.tsx new file mode 100644 index 00000000..2d855144 --- /dev/null +++ b/GUI/src/components/molecules/DataTable/index.tsx @@ -0,0 +1,258 @@ +import React, { CSSProperties, FC, ReactNode, useId } from 'react'; +import { + ColumnDef, + useReactTable, + getCoreRowModel, + flexRender, + getSortedRowModel, + SortingState, + FilterFn, + getFilteredRowModel, + VisibilityState, + getPaginationRowModel, + PaginationState, + TableMeta, + Row, + RowData, ColumnFiltersState, +} from '@tanstack/react-table'; +import { + RankingInfo, + rankItem, +} from '@tanstack/match-sorter-utils'; +import { + MdUnfoldMore, + MdExpandMore, + MdExpandLess, + MdOutlineEast, + MdOutlineWest, +} from 'react-icons/md'; +import clsx from 'clsx'; +import { Link } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; + +import { Icon, Track } from '../../../components'; +import Filter from './Filter'; +import './DataTable.scss'; + +type DataTableProps = { + data: any; + columns: ColumnDef[]; + tableBodyPrefix?: ReactNode; + isClientSide?: boolean; + sortable?: boolean; + filterable?: boolean; + pagination?: PaginationState; + sorting?: SortingState; + setPagination?: (state: PaginationState) => void; + setSorting?: (state: SortingState) => void; + globalFilter?: string; + setGlobalFilter?: React.Dispatch>; + columnVisibility?: VisibilityState; + setColumnVisibility?: React.Dispatch>; + disableHead?: boolean; + pagesCount?: number; + meta?: TableMeta; +}; + +type ColumnMeta = { + meta: { + size: number | string; + } +} + +type CustomColumnDef = ColumnDef & ColumnMeta; + +declare module '@tanstack/table-core' { + interface FilterFns { + fuzzy: FilterFn; + } + + interface FilterMeta { + itemRank: RankingInfo; + } +} + +declare module '@tanstack/react-table' { + interface TableMeta { + getRowStyles: (row: Row) => CSSProperties; + } + class Column { + columnDef: CustomColumnDef; + } +} + +const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { + const itemRank = rankItem(row.getValue(columnId), value); + addMeta({ + itemRank, + }); + return itemRank.passed; +}; + +const DataTable: FC = ( + { + data, + columns, + isClientSide = true, + tableBodyPrefix, + sortable, + filterable, + pagination, + sorting, + setPagination, + setSorting, + globalFilter, + setGlobalFilter, + columnVisibility, + setColumnVisibility, + disableHead, + pagesCount, + meta, + }, +) => { + const id = useId(); + const { t } = useTranslation(); + const [columnFilters, setColumnFilters] = React.useState([]); + const table = useReactTable({ + data, + columns, + filterFns: { + fuzzy: fuzzyFilter, + }, + state: { + sorting, + columnFilters, + globalFilter, + columnVisibility, + ...{ pagination }, + }, + meta, + onColumnFiltersChange: setColumnFilters, + onGlobalFilterChange: setGlobalFilter, + onColumnVisibilityChange: setColumnVisibility, + globalFilterFn: fuzzyFilter, + onSortingChange: (updater) => { + if (typeof updater !== 'function') return; + setSorting?.(updater(table.getState().sorting)); + }, + onPaginationChange: (updater) => { + if (typeof updater !== 'function') return; + setPagination?.(updater(table.getState().pagination)); + }, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + ...(pagination && { getPaginationRowModel: getPaginationRowModel() }), + ...(sortable && { getSortedRowModel: getSortedRowModel() }), + manualPagination: isClientSide ? undefined : true, + manualSorting: isClientSide ? undefined : true, + pageCount: isClientSide ? undefined : pagesCount, + }); + + return ( +
+ + {!disableHead && ( + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + )} + + {tableBodyPrefix} + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + ))} + +
+ {header.isPlaceholder ? null : ( + + {sortable && header.column.getCanSort() && ( + + )} + {flexRender(header.column.columnDef.header, header.getContext())} + {/* {filterable && header.column.getCanFilter() && ( + + )} */} + + )} +
{flexRender(cell.column.columnDef.cell, cell.getContext())}
+ {pagination && ( +
+ {(table.getPageCount() * table.getState().pagination.pageSize) > table.getState().pagination.pageSize && ( +
+ + + +
+ )} +
+ + +
+
+ )} +
+ ); +}; + +export default DataTable; diff --git a/GUI/src/config/users.json b/GUI/src/config/users.json new file mode 100644 index 00000000..301e82fd --- /dev/null +++ b/GUI/src/config/users.json @@ -0,0 +1,119 @@ +[ + { + "login": "EE40404049985", + "firstName": "admin", + "lastName": "admin", + "idCode": "EE40404049985", + "displayName": "admin", + "csaTitle": "admin", + "csaEmail": "admin@admin.ee", + "authorities": [ + "ROLE_ADMINISTRATOR" + ], + "customerSupportStatus": "offline", + "totalPages": 1 + }, + { + "login": "EE38807130279", + "firstName": "Jaanus", + "lastName": "Kääp", + "idCode": "EE38807130279", + "displayName": "Jaanus", + "csaTitle": "tester", + "csaEmail": "jaanus@clarifiedsecurity.com", + "authorities": [ + "ROLE_SERVICE_MANAGER", + "ROLE_CUSTOMER_SUPPORT_AGENT", + "ROLE_CHATBOT_TRAINER", + "ROLE_ANALYST", + "ROLE_ADMINISTRATOR" + ], + "customerSupportStatus": "offline", + "totalPages": 1 + }, + { + "login": "EE30303039816", + "firstName": "kolmas", + "lastName": "admin", + "idCode": "EE30303039816", + "displayName": "kolmas", + "csaTitle": "kolmas", + "csaEmail": "kolmas@admin.ee", + "authorities": [ + "ROLE_ADMINISTRATOR" + ], + "customerSupportStatus": "offline", + "totalPages": 1 + }, + { + "login": "EE30303039914", + "firstName": "Kustuta", + "lastName": "Kasutaja", + "idCode": "EE30303039914", + "displayName": "Kustutamiseks", + "csaTitle": "", + "csaEmail": "kustutamind@mail.ee", + "authorities": [ + "ROLE_ADMINISTRATOR" + ], + "customerSupportStatus": "offline", + "totalPages": 1 + }, + { + "login": "EE50001029996", + "firstName": "Nipi", + "lastName": "Tiri", + "idCode": "EE50001029996", + "displayName": "Nipi", + "csaTitle": "Dr", + "csaEmail": "nipi@tiri.ee", + "authorities": [ + "ROLE_CUSTOMER_SUPPORT_AGENT" + ], + "customerSupportStatus": "offline", + "totalPages": 1 + }, + { + "login": "EE40404049996", + "firstName": "teine", + "lastName": "admin", + "idCode": "EE40404049996", + "displayName": "teine admin", + "csaTitle": "teine admin", + "csaEmail": "Teine@admin.ee", + "authorities": [ + "ROLE_ADMINISTRATOR" + ], + "customerSupportStatus": "offline", + "totalPages": 1 + }, + { + "login": "EE50701019992", + "firstName": "Valter", + "lastName": "Aro", + "idCode": "EE50701019992", + "displayName": "Valter", + "csaTitle": "Mister", + "csaEmail": "valter.aro@ria.ee", + "authorities": [ + "ROLE_ANALYST", + "ROLE_CHATBOT_TRAINER" + ], + "customerSupportStatus": "offline", + "totalPages": 1 + }, + { + "login": "EE38104266023", + "firstName": "Varmo", + "lastName": "", + "idCode": "EE38104266023", + "displayName": "Varmo", + "csaTitle": "MISTER", + "csaEmail": "mail@mail.ee", + "authorities": [ + "ROLE_ADMINISTRATOR" + ], + "customerSupportStatus": "online", + "totalPages": 1 + } +] \ No newline at end of file diff --git a/GUI/src/pages/Home.tsx b/GUI/src/pages/Home.tsx index 6ffdd84e..0a0712dd 100644 --- a/GUI/src/pages/Home.tsx +++ b/GUI/src/pages/Home.tsx @@ -2,6 +2,7 @@ import { FC, useState } from "react"; import { Button, Tooltip } from "../components"; import Checkbox from "../components/atoms/CheckBox"; import RadioButton from "../components/atoms/RadioButton"; +import InputField from "../components/atoms/InputField"; const Home: FC = () => { //check box @@ -23,6 +24,22 @@ const Home: FC = () => { { label: "Option 2", value: "option2" }, ]; + //input field + const [inputValue, setInputValue] = useState(''); + const [error, setError] = useState(''); + + const handleInputChange = (e: React.ChangeEvent) => { + const value = e.target.value; + setInputValue(value); + + // Example validation + if (value.length < 5) { + setError('Input must be at least 5 characters long'); + } else { + setError(''); + } + }; + return (
This is list of components
@@ -50,6 +67,15 @@ const Home: FC = () => {
+
+ +
); }; diff --git a/GUI/src/pages/UserManagement.tsx b/GUI/src/pages/UserManagement.tsx new file mode 100644 index 00000000..5b745bd6 --- /dev/null +++ b/GUI/src/pages/UserManagement.tsx @@ -0,0 +1,114 @@ +import { FC, useMemo, useState } from "react"; +import { Button, Icon } from "../components"; +import DataTable from "../components/molecules/DataTable"; +import users from "../config/users.json"; +import { Row, createColumnHelper } from "@tanstack/react-table"; +import { User } from "../types/user"; +import { MdOutlineDeleteOutline, MdOutlineEdit } from "react-icons/md"; + +const UserManagement: FC = () => { + const [data, setData] = useState([]); + const columnHelper = createColumnHelper(); + const [editableRow, setEditableRow] = useState(null); + const [deletableRow, setDeletableRow] = useState( + null + ); + const editView = (props: any) => ( + + ); + + const deleteView = (props: any) => ( + + ); + + const usersColumns = useMemo( + () => [ + columnHelper.accessor( + (row) => `${row.firstName ?? ''} ${row.lastName ?? ''}`, + { + id: `name`, + header: 'name', + } + ), + columnHelper.accessor('idCode', { + header: 'idCode', + }), + columnHelper.accessor( + (data: { authorities:any}) => { + const output: string[] = []; + data.authorities?.map?.((role) => { + return output.push('role'); + }); + return output; + }, + { + header:'role' ?? '', + cell: (props) => props.getValue().join(', '), + filterFn: (row: Row, _, filterValue) => { + const rowAuthorities: string[] = []; + row.original.authorities.map((role) => { + return rowAuthorities.push('role'); + }); + const filteredArray = rowAuthorities.filter((word) => + word.toLowerCase().includes(filterValue.toLowerCase()) + ); + return filteredArray.length > 0; + }, + } + ), + columnHelper.accessor('displayName', { + header: 'displayName' ?? '', + }), + columnHelper.accessor('csaTitle', { + header: 'csaTitle' ?? '', + }), + columnHelper.accessor('csaEmail', { + header: 'csaEmail' ?? '', + }), + columnHelper.display({ + id: 'edit', + cell: editView, + meta: { + size: '1%', + }, + }), + columnHelper.display({ + id: 'delete', + cell: deleteView, + meta: { + size: '1%', + }, + }), + ], + [] + ); + + + return ( + <> +
+
User Management
+ +
+ <> + + + + ); +}; + +export default UserManagement; diff --git a/GUI/src/types/authorities.ts b/GUI/src/types/authorities.ts new file mode 100644 index 00000000..50afc258 --- /dev/null +++ b/GUI/src/types/authorities.ts @@ -0,0 +1,8 @@ +export enum AUTHORITY { + ADMINISTRATOR = 'ROLE_ADMINISTRATOR', + SERVICE_MANAGER = 'ROLE_SERVICE_MANAGER', + CUSTOMER_SUPPORT_AGENT = 'ROLE_CUSTOMER_SUPPORT_AGENT', + CHATBOT_TRAINER = 'ROLE_CHATBOT_TRAINER', + ANALYST = 'ROLE_ANALYST', + UNAUTHENTICATED = 'ROLE_UNAUTHENTICATED', +} diff --git a/GUI/src/types/chat.ts b/GUI/src/types/chat.ts new file mode 100644 index 00000000..adb7ce0d --- /dev/null +++ b/GUI/src/types/chat.ts @@ -0,0 +1,108 @@ +export enum CHAT_STATUS { + ENDED = 'ENDED', + OPEN = 'OPEN', + REDIRECTED = 'REDIRECTED', + IDLE = 'IDLE', +} + +export enum CHAT_EVENTS { + ANSWERED = 'answered', + TERMINATED = 'terminated', + CHAT_SENT_TO_CSA_EMAIL = 'chat_sent_to_csa_email', + CLIENT_LEFT = 'client-left', + CLIENT_LEFT_WITH_ACCEPTED = 'client_left_with_accepted', + CLIENT_LEFT_WITH_NO_RESOLUTION = 'client_left_with_no_resolution', + CLIENT_LEFT_FOR_UNKNOWN_REASONS = 'client_left_for_unknown_reasons', + ACCEPTED = 'accepted', + HATE_SPEECH = 'hate_speech', + OTHER = 'other', + RESPONSE_SENT_TO_CLIENT_EMAIL = 'response_sent_to_client_email', + GREETING = 'greeting', + REQUESTED_AUTHENTICATION = 'requested-authentication', + AUTHENTICATION_SUCCESSFUL = 'authentication_successful', + AUTHENTICATION_FAILED = 'authentication_failed', + ASK_PERMISSION = 'ask-permission', + ASK_PERMISSION_ACCEPTED = 'ask-permission-accepted', + ASK_PERMISSION_REJECTED = 'ask-permission-rejected', + ASK_PERMISSION_IGNORED = 'ask-permission-ignored', + RATING = 'rating', + REDIRECTED = 'redirected', + CONTACT_INFORMATION = 'contact-information', + CONTACT_INFORMATION_REJECTED = 'contact-information-rejected', + CONTACT_INFORMATION_FULFILLED = 'contact-information-fulfilled', + UNAVAILABLE_CONTACT_INFORMATION_FULFILLED = 'unavailable-contact-information-fulfilled', + CONTACT_INFORMATION_SKIPPED = 'contact-information-skipped', + REQUESTED_CHAT_FORWARD = 'requested-chat-forward', + REQUESTED_CHAT_FORWARD_ACCEPTED = 'requested-chat-forward-accepted', + REQUESTED_CHAT_FORWARD_REJECTED = 'requested-chat-forward-rejected', + UNAVAILABLE_ORGANIZATION = 'unavailable_organization', + UNAVAILABLE_CSAS = 'unavailable_csas', + UNAVAILABLE_HOLIDAY = 'unavailable_holiday', + ASSIGN_PENDING_CHAT_CSA = 'pending-assigned', + PENDING_USER_REACHED = 'user-reached', + PENDING_USER_NOT_REACHED = 'user-not-reached', + USER_AUTHENTICATED = 'user-authenticated', + READ = 'message-read', +} + +export interface Chat { + id: string; + csaTitle?: string | null; + customerSupportId?: string; + customerSupportDisplayName?: string; + endUserId?: string; + endUserEmail?: string; + endUserPhone?: string; + endUserFirstName?: string; + endUserLastName?: string; + contactsMessage?: string; + status: CHAT_STATUS; + created: string; + updated: string; + ended: string; + lastMessage: string; + endUserUrl?: string; + endUserOs?: string; + lastMessageTimestamp?: string; + lastMessageEvent?: CHAT_EVENTS | null; + forwardedToName?: string; + forwardedByUser?: string; + forwardedFromCsa?: string; + forwardedToCsa?: string; + receivedFrom?: string; + comment?: string; + labels: string; + feedbackText?: string; + feedbackRating?: number; + nps?: number; +} +export interface GroupedChat { + myChats: Chat[]; + otherChats: { + groupId: string; + name: string; + chats: Chat[]; + }[]; +} + +export interface GroupedPendingChat { + newChats: Chat[]; + inProcessChats: Chat[]; + myChats: Chat[]; + otherChats: { + groupId: string; + name: string; + chats: Chat[]; + }[]; +} + +export enum MessageSseEvent { + READ = 'message-read', + DELIVERED = 'message-delivered', + PREVIEW = 'message-preview', +} + +export type MessageStatus = { + messageId: string | null; + readTime: any; +} diff --git a/GUI/src/types/emergencyNotice.ts b/GUI/src/types/emergencyNotice.ts new file mode 100644 index 00000000..958fe71d --- /dev/null +++ b/GUI/src/types/emergencyNotice.ts @@ -0,0 +1,6 @@ +export interface EmergencyNotice { + emergencyNoticeText: string; + emergencyNoticeStartISO: Date | string; + emergencyNoticeEndISO: Date | string; + isEmergencyNoticeVisible: boolean; +} diff --git a/GUI/src/types/establishment.ts b/GUI/src/types/establishment.ts new file mode 100644 index 00000000..35b0aa8d --- /dev/null +++ b/GUI/src/types/establishment.ts @@ -0,0 +1,4 @@ +export interface Establishment { + readonly id: number; + name: string; +} diff --git a/GUI/src/types/mainNavigation.ts b/GUI/src/types/mainNavigation.ts new file mode 100644 index 00000000..32ec0be6 --- /dev/null +++ b/GUI/src/types/mainNavigation.ts @@ -0,0 +1,11 @@ +export interface MenuItem { + id?: string; + label: string; + path: string | null; + target?: '_blank' | '_self'; + children?: MenuItem[]; +} + +export interface MainNavigation { + data: MenuItem[]; +} diff --git a/GUI/src/types/message.ts b/GUI/src/types/message.ts new file mode 100644 index 00000000..1980e9e4 --- /dev/null +++ b/GUI/src/types/message.ts @@ -0,0 +1,64 @@ +export interface UseSendAttachment { + successCb?: (data: any) => void; + errorCb?: (error: any) => void; + data: { + chatId: string, + name: string, + type: string, + size: string, + base64: string, + } +} + +export interface Attachment { + chatId: string; + name: string; + type: AttachmentTypes; + size: number; + base64: string; +} + +export interface Message { + id?: string; + chatId: string; + content?: string; + event?: string; + authorId?: string; + authorTimestamp: string; + authorFirstName: string; + authorLastName?: string; + authorRole: string; + forwardedByUser: string; + forwardedFromCsa: string; + forwardedToCsa: string; + rating?: string; + created?: string; + preview?: string; + updated?: string; +} + +export interface MessagePreviewSseResponse { + data: Message; + origin: string; + type: string; +} + +export enum AttachmentTypes { + PDF = 'application/pdf', + PNG = 'image/png', + JPEG = 'image/jpeg', + TXT = 'text/plain', + DOCX = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + ODT = 'application/vnd.oasis.opendocument.text', + XLSX = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + ODS = 'ods', + CDOC = 'application/x-cdoc', + ASICE = 'application/vnd.etsi.asic-e+zip', + MP3 = 'audio/mpeg', + WAV = 'audio/wav', + M4A = 'audio/x-m4a', + MP4 = 'video/mp4', + WEBM = 'video/webm', + OGG = 'video/ogg', + MOV = 'video/quicktime', +} diff --git a/GUI/src/types/organizationWorkingTime.ts b/GUI/src/types/organizationWorkingTime.ts new file mode 100644 index 00000000..ec11db73 --- /dev/null +++ b/GUI/src/types/organizationWorkingTime.ts @@ -0,0 +1,22 @@ +export interface OrganizationWorkingTime { + organizationMondayWorkingTimeStartISO: Date | string; + organizationMondayWorkingTimeEndISO: Date | string; + organizationTuesdayWorkingTimeStartISO: Date | string; + organizationTuesdayWorkingTimeEndISO: Date | string; + organizationWednesdayWorkingTimeStartISO: Date | string; + organizationWednesdayWorkingTimeEndISO: Date | string; + organizationThursdayWorkingTimeStartISO: Date | string; + organizationThursdayWorkingTimeEndISO: Date | string; + organizationFridayWorkingTimeStartISO: Date | string; + organizationFridayWorkingTimeEndISO: Date | string; + organizationSaturdayWorkingTimeStartISO: Date | string; + organizationSaturdayWorkingTimeEndISO: Date | string; + organizationSundayWorkingTimeStartISO: Date | string; + organizationSundayWorkingTimeEndISO: Date | string; + organizationAllWeekdaysTimeStartISO: Date | string; + organizationAllWeekdaysTimeEndISO: Date | string; + organizationWorkingTimeWeekdays: string[]; + organizationWorkingTimeNationalHolidays: boolean; + organizationClosedOnWeekEnds: boolean; + organizationTheSameOnAllWorkingDays: boolean; +} diff --git a/GUI/src/types/router.ts b/GUI/src/types/router.ts new file mode 100644 index 00000000..17fb31f5 --- /dev/null +++ b/GUI/src/types/router.ts @@ -0,0 +1,4 @@ +export interface RouterResponse { + data: Record | null; + error: string | null; +} diff --git a/GUI/src/types/service.ts b/GUI/src/types/service.ts new file mode 100644 index 00000000..2aa95172 --- /dev/null +++ b/GUI/src/types/service.ts @@ -0,0 +1,6 @@ +export interface Service { + id: string; + name: string; + type: 'POST' | 'GET'; + state?: 'active' | 'inactive' | 'draft'; +} diff --git a/GUI/src/types/session.ts b/GUI/src/types/session.ts new file mode 100644 index 00000000..b9af48f4 --- /dev/null +++ b/GUI/src/types/session.ts @@ -0,0 +1,7 @@ +export interface Session { + readonly id: number; + key: string; + value: string; + deleted: boolean; + created: Date | string; +} diff --git a/GUI/src/types/user.ts b/GUI/src/types/user.ts new file mode 100644 index 00000000..60ddbaad --- /dev/null +++ b/GUI/src/types/user.ts @@ -0,0 +1,17 @@ +import { ROLES } from 'utils/constants'; + +export interface User { + login?: string; + fullName?: string; + firstName: string; + lastName: string; + idCode: string; + displayName: string; + csaTitle: string; + csaEmail: string; + authorities: ROLES[]; + customerSupportStatus: 'online' | 'idle' | 'offline'; +} + +export interface UserDTO extends Pick { +} diff --git a/GUI/src/types/userInfo.ts b/GUI/src/types/userInfo.ts new file mode 100644 index 00000000..42a4be81 --- /dev/null +++ b/GUI/src/types/userInfo.ts @@ -0,0 +1,16 @@ +export interface UserInfo { + JWTCreated: string; + JWTExpirationTimestamp: string; + firstName: string; + lastName: string; + loggedInDate: string; + loginExpireDate: string; + authMethod: string; + fullName: string; + authorities: string[]; + displayName: string; + idCode: string; + email: string; + csaEmail: string; + csaTitle: string; +} diff --git a/GUI/src/types/userProfileSettings.ts b/GUI/src/types/userProfileSettings.ts new file mode 100644 index 00000000..05086624 --- /dev/null +++ b/GUI/src/types/userProfileSettings.ts @@ -0,0 +1,10 @@ +export interface UserProfileSettings { + userId: number; + forwardedChatPopupNotifications: boolean; + forwardedChatSoundNotifications: boolean; + forwardedChatEmailNotifications: boolean; + newChatPopupNotifications: boolean; + newChatSoundNotifications: boolean; + newChatEmailNotifications: boolean; + useAutocorrect: boolean; +} diff --git a/GUI/src/types/widgetConfig.ts b/GUI/src/types/widgetConfig.ts new file mode 100644 index 00000000..1570add3 --- /dev/null +++ b/GUI/src/types/widgetConfig.ts @@ -0,0 +1,8 @@ +export interface WidgetConfig { + widgetProactiveSeconds: number; + widgetDisplayBubbleMessageSeconds: number; + widgetBubbleMessageText: string; + widgetColor: string; + widgetAnimation: string; + isWidgetActive: boolean; +} From e60700573ca2d2fb39c82764c7989d58b3693471 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Thu, 13 Jun 2024 21:56:18 +0530 Subject: [PATCH 005/582] classifier-93 implement initial phase of jira webhook subscribe and unsubscribe rest api(DSL files) --- .../integration/jira/cloud/accept.yml | 32 ++++++++++++++++--- .../integration/jira/cloud/connect.yml | 22 +++++++++++++ .../integration/jira/cloud/token.yml | 26 +++++++++++++++ 3 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/connect.yml create mode 100644 DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/token.yml diff --git a/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml b/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml index 5b9a9e97..0a445999 100644 --- a/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml +++ b/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml @@ -7,12 +7,12 @@ declaration: returns: json namespace: classifier -jira_webhook_data: +get_webhook_data: assign: headers: ${incoming.headers} payload: ${incoming.body} issue_info: ${incoming.body.issue} - next: get_jira_issue_info + next: verify_jira_signature verify_jira_signature: call: http.post @@ -24,6 +24,18 @@ verify_jira_signature: payload: ${payload} headers: ${headers} result: valid_data + next: assign_verification + +assign_verification: + assign: + is_valid: ${valid_data.response.body.valid} + next: validate_url_signature + +validate_url_signature: + switch: + - condition: ${is_valid === "true"} + next: get_jira_issue_info + next: return_error_found get_jira_issue_info: call: http.post @@ -37,14 +49,26 @@ get_jira_issue_info: next: send_issue_data send_issue_data: - call: http.post + call: reflect.mock args: url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/return_jira_issue_info"# need correct url headers: type: json body: - data: ${extract_info} + info: ${extract_info} + response: + val: "send to mock url" result: response + next: output_val + +output_val: + return: ${response} + +return_error_found: + return: ${res.response.message} + status: 400 next: end + + diff --git a/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/connect.yml b/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/connect.yml new file mode 100644 index 00000000..b581031e --- /dev/null +++ b/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/connect.yml @@ -0,0 +1,22 @@ +declaration: + call: declare + version: 0.1 + description: "Decription placeholder for 'CONNECT'" + method: post + accepts: json + returns: json + namespace: classifier + +get_access_token: + call: http.post + args: + url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/verify_signature" + headers: + type: json + body: + payload: ${payload} + headers: ${headers} + result: valid_data + next: assign_verification + + diff --git a/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/token.yml b/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/token.yml new file mode 100644 index 00000000..2500954d --- /dev/null +++ b/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/token.yml @@ -0,0 +1,26 @@ +declaration: + call: declare + version: 0.1 + description: "Decription placeholder for 'CONNECT'" + method: post + accepts: json + returns: json + namespace: classifier + +get_access_token: + call: http.post + args: + url: "https://auth.atlassian.com/authorize" + headers: + type: json + query: + audience: "api.atlassian.com" + client_id: "client_id" + scope: "write:jira-work" + redirect_uri: "redirect_uri" + state: "state" + response_type: "code" + prompt: "consent" + result: valid_data + + From 8e540f4d9ffecc4c4c5e3f9d89009d3d7bff3ee6 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Thu, 13 Jun 2024 23:16:08 +0530 Subject: [PATCH 006/582] added modal component --- .../components/molecules/DataTable/Filter.tsx | 4 +- .../components/molecules/DataTable/index.tsx | 4 +- .../components/molecules/Dialog/Dialog.scss | 63 ++++++++++++++++ GUI/src/components/molecules/Dialog/index.tsx | 45 ++++++++++++ GUI/src/hooks/useAudio.tsx | 35 +++++++++ GUI/src/hooks/useDocumentEscapeListener.tsx | 17 +++++ GUI/src/hooks/useToast.tsx | 2 +- GUI/src/main.tsx | 1 + GUI/src/pages/UserManagement.tsx | 73 +++++++++++++------ 9 files changed, 215 insertions(+), 29 deletions(-) create mode 100644 GUI/src/components/molecules/Dialog/Dialog.scss create mode 100644 GUI/src/components/molecules/Dialog/index.tsx create mode 100644 GUI/src/hooks/useAudio.tsx create mode 100644 GUI/src/hooks/useDocumentEscapeListener.tsx diff --git a/GUI/src/components/molecules/DataTable/Filter.tsx b/GUI/src/components/molecules/DataTable/Filter.tsx index 6622c7aa..52dc42dc 100644 --- a/GUI/src/components/molecules/DataTable/Filter.tsx +++ b/GUI/src/components/molecules/DataTable/Filter.tsx @@ -3,8 +3,8 @@ import { Column, Table } from '@tanstack/react-table'; import { useTranslation } from 'react-i18next'; import { MdOutlineSearch } from 'react-icons/md'; -import { Icon } from 'components'; -import useDocumentEscapeListener from 'hooks/useDocumentEscapeListener'; +import { Icon } from '../../../components'; +import useDocumentEscapeListener from '../../../hooks/useDocumentEscapeListener'; import DebouncedInput from './DebouncedInput'; type FilterProps = { diff --git a/GUI/src/components/molecules/DataTable/index.tsx b/GUI/src/components/molecules/DataTable/index.tsx index 2d855144..8b17ad5e 100644 --- a/GUI/src/components/molecules/DataTable/index.tsx +++ b/GUI/src/components/molecules/DataTable/index.tsx @@ -170,9 +170,9 @@ const DataTable: FC = ( )} {flexRender(header.column.columnDef.header, header.getContext())} - {/* {filterable && header.column.getCanFilter() && ( + {filterable && header.column.getCanFilter() && ( - )} */} + )} )} diff --git a/GUI/src/components/molecules/Dialog/Dialog.scss b/GUI/src/components/molecules/Dialog/Dialog.scss new file mode 100644 index 00000000..7af6a64b --- /dev/null +++ b/GUI/src/components/molecules/Dialog/Dialog.scss @@ -0,0 +1,63 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/other'; +@import 'src/styles/settings/variables/typography'; + +.dialog { + background-color: get-color(white); + box-shadow: 0 0 20px rgba(0, 0, 0, 0.25); + border-radius: 4px; + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 100%; + max-width: 600px; + z-index: 101; + max-height: 90vh; + + &--large { + max-width: 800px; + } + + &__overlay { + position: fixed; + inset: 0; + background-color: rgba(0, 0, 0, 0.54); + z-index: 100; + } + + &__header, + &__body, + &__footer { + padding: get-spacing(haapsalu); + } + + &__header { + display: flex; + align-items: center; + gap: get-spacing(haapsalu); + background-color: get-color(black-coral-0); + border-bottom: 1px solid get-color(black-coral-2); + } + + &__title { + flex: 1; + } + + &__close { + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + } + + &__body { + overflow: auto; + max-height: calc(90vh - 70px); + } + + &__footer { + border-top: 1px solid get-color(black-coral-2); + } +} diff --git a/GUI/src/components/molecules/Dialog/index.tsx b/GUI/src/components/molecules/Dialog/index.tsx new file mode 100644 index 00000000..fbe609e3 --- /dev/null +++ b/GUI/src/components/molecules/Dialog/index.tsx @@ -0,0 +1,45 @@ +import { FC, PropsWithChildren, ReactNode } from 'react'; +import * as RadixDialog from '@radix-ui/react-dialog'; +import { MdOutlineClose } from 'react-icons/md'; +import clsx from 'clsx'; + +import { Icon, Track } from '../../../components'; +import './Dialog.scss'; + +type DialogProps = { + title?: string | null; + footer?: ReactNode; + onClose: () => void; + size?: 'default' | 'large'; + isOpen?: boolean; +} + +const Dialog: FC> = ({ title, footer, onClose, size = 'default', children,isOpen }) => { + return ( + + + + + { + title &&
+ {title} + + + +
+ } +
+ {children} +
+ {footer && ( + {footer} + )} +
+
+
+ ); +}; + +export default Dialog; diff --git a/GUI/src/hooks/useAudio.tsx b/GUI/src/hooks/useAudio.tsx new file mode 100644 index 00000000..55631e2c --- /dev/null +++ b/GUI/src/hooks/useAudio.tsx @@ -0,0 +1,35 @@ +import { useEffect, useState } from 'react'; +import { Howl } from 'howler'; +import ding from '../assets/ding.mp3'; +import newMessageSound from '../assets/newMessageSound.mp3'; + +export const useAudio = (audiosrc: string) => { + const [audio, setAudio] = useState(null); + + useEffect(() => { + const howl = new Howl({ + src: audiosrc, + onloaderror: (soundId, error) => console.error(soundId, error), + onplayerror: (soundId, error) => { + console.error(soundId, error); + howl.once('unlock', () => howl.play()); + }, + }); + + setAudio(howl); + + return () => { + howl.unload(); + } + }, []); + + return [audio] as const; +} + +export const useDing = () => { + return useAudio(ding); +} + +export const useNewMessageSound = () => { + return useAudio(newMessageSound); +} diff --git a/GUI/src/hooks/useDocumentEscapeListener.tsx b/GUI/src/hooks/useDocumentEscapeListener.tsx new file mode 100644 index 00000000..8f7b3b6b --- /dev/null +++ b/GUI/src/hooks/useDocumentEscapeListener.tsx @@ -0,0 +1,17 @@ +import { useLayoutEffect } from 'react'; + +const useDocumentEscapeListener = (callback: () => void) => { + useLayoutEffect(() => { + const handleKeyUp = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + callback(); + } + }; + + document.addEventListener('keyup', handleKeyUp); + + return () => document.removeEventListener('keyup', handleKeyUp); + }, [callback]); +}; + +export default useDocumentEscapeListener; diff --git a/GUI/src/hooks/useToast.tsx b/GUI/src/hooks/useToast.tsx index 3c9a430c..51715549 100644 --- a/GUI/src/hooks/useToast.tsx +++ b/GUI/src/hooks/useToast.tsx @@ -1,5 +1,5 @@ import { useContext } from 'react'; -import { ToastContext } from '../context/ToastContext'; +import { ToastContext } from 'context/ToastContext'; export const useToast = () => useContext(ToastContext); diff --git a/GUI/src/main.tsx b/GUI/src/main.tsx index af402ab1..5f71987a 100644 --- a/GUI/src/main.tsx +++ b/GUI/src/main.tsx @@ -3,6 +3,7 @@ import ReactDOM from "react-dom/client"; import App from "./App.tsx"; import "./index.css"; import "../i18n.ts"; +import './styles/main.scss'; import { BrowserRouter } from "react-router-dom"; ReactDOM.createRoot(document.getElementById("root")!).render( diff --git a/GUI/src/pages/UserManagement.tsx b/GUI/src/pages/UserManagement.tsx index 5b745bd6..6a475b10 100644 --- a/GUI/src/pages/UserManagement.tsx +++ b/GUI/src/pages/UserManagement.tsx @@ -5,6 +5,7 @@ import users from "../config/users.json"; import { Row, createColumnHelper } from "@tanstack/react-table"; import { User } from "../types/user"; import { MdOutlineDeleteOutline, MdOutlineEdit } from "react-icons/md"; +import Dialog from "../components/molecules/Dialog"; const UserManagement: FC = () => { const [data, setData] = useState([]); @@ -13,13 +14,15 @@ const UserManagement: FC = () => { const [deletableRow, setDeletableRow] = useState( null ); + const [showAddUserModal, setShowAddUserModal] = useState(false); + const editView = (props: any) => ( ); @@ -29,37 +32,37 @@ const UserManagement: FC = () => { onClick={() => setDeletableRow(props.row.original.idCode)} > } /> - {'Delete'} + {"Delete"} ); const usersColumns = useMemo( () => [ columnHelper.accessor( - (row) => `${row.firstName ?? ''} ${row.lastName ?? ''}`, + (row) => `${row.firstName ?? ""} ${row.lastName ?? ""}`, { id: `name`, - header: 'name', + header: "name", } ), - columnHelper.accessor('idCode', { - header: 'idCode', + columnHelper.accessor("idCode", { + header: "idCode", }), columnHelper.accessor( - (data: { authorities:any}) => { + (data: { authorities: any }) => { const output: string[] = []; data.authorities?.map?.((role) => { - return output.push('role'); + return output.push("role"); }); return output; }, { - header:'role' ?? '', - cell: (props) => props.getValue().join(', '), + header: "role" ?? "", + cell: (props) => props.getValue().join(", "), filterFn: (row: Row, _, filterValue) => { const rowAuthorities: string[] = []; row.original.authorities.map((role) => { - return rowAuthorities.push('role'); + return rowAuthorities.push("role"); }); const filteredArray = rowAuthorities.filter((word) => word.toLowerCase().includes(filterValue.toLowerCase()) @@ -68,44 +71,66 @@ const UserManagement: FC = () => { }, } ), - columnHelper.accessor('displayName', { - header: 'displayName' ?? '', + columnHelper.accessor("displayName", { + header: "displayName" ?? "", }), - columnHelper.accessor('csaTitle', { - header: 'csaTitle' ?? '', + columnHelper.accessor("csaTitle", { + header: "csaTitle" ?? "", }), - columnHelper.accessor('csaEmail', { - header: 'csaEmail' ?? '', + columnHelper.accessor("csaEmail", { + header: "csaEmail" ?? "", }), columnHelper.display({ - id: 'edit', + id: "edit", cell: editView, meta: { - size: '1%', + size: "1%", }, }), columnHelper.display({ - id: 'delete', + id: "delete", cell: deleteView, meta: { - size: '1%', + size: "1%", }, }), ], [] ); - return ( <>
User Management
-
<> - + + setShowAddUserModal(false)} + title={"Add User"} + isOpen={showAddUserModal} + footer={ + <> + + + + } + > + + body + ); From 8684f0037037eaf2da4f44b821f065bd2a4845d7 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Mon, 17 Jun 2024 23:37:32 +0530 Subject: [PATCH 007/582] classifier-93 implement basic auth flow using API token for jira cloud --- DSL/DMapper/hbs/get_auth_header.handlebars | 4 ++ DSL/DMapper/lib/helpers.js | 9 +++- .../integration/jira/cloud/accept.yml | 13 ++++- .../integration/jira/cloud/connect.yml | 51 +++++++++++++++++-- .../integration/jira/cloud/token.yml | 26 ---------- constants.ini | 4 ++ 6 files changed, 76 insertions(+), 31 deletions(-) create mode 100644 DSL/DMapper/hbs/get_auth_header.handlebars delete mode 100644 DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/token.yml diff --git a/DSL/DMapper/hbs/get_auth_header.handlebars b/DSL/DMapper/hbs/get_auth_header.handlebars new file mode 100644 index 00000000..94acdff6 --- /dev/null +++ b/DSL/DMapper/hbs/get_auth_header.handlebars @@ -0,0 +1,4 @@ +{ + "val": "{{getAuthHeader username token}}" +} + diff --git a/DSL/DMapper/lib/helpers.js b/DSL/DMapper/lib/helpers.js index 1c241f0e..d906c33c 100644 --- a/DSL/DMapper/lib/helpers.js +++ b/DSL/DMapper/lib/helpers.js @@ -1,4 +1,4 @@ -import { createHmac,timingSafeEqual } from "crypto" +import { createHmac,timingSafeEqual } from "crypto"; export function verifySignature(payload, headers) { const signature = headers['x-hub-signature']; @@ -11,3 +11,10 @@ export function verifySignature(payload, headers) { const isValid = timingSafeEqual(Buffer.from(computedSignaturePrefixed, 'utf8'), Buffer.from(signature, 'utf8')); return isValid; } + +export function getAuthHeader(username, token) { + const auth = `${username}:${token}`; + const encodedAuth = Buffer.from(auth).toString("base64"); + console.log("encodedAuth: " + encodedAuth); + return `Basic ${encodedAuth}`; +} diff --git a/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml b/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml index 0a445999..b48df027 100644 --- a/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml +++ b/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml @@ -1,11 +1,22 @@ declaration: call: declare version: 0.1 - description: "Decription placeholder for 'ACEEPT'" + description: "Description placeholder for 'ACEEPT'" method: post accepts: json returns: json namespace: classifier + allowlist: + body: + - field: headers + type: object + description: "Body field 'headers'" + - field: payload + type: object + description: "Body field 'payload'" + - field: issue_info + type: string + description: "Body field 'issue_info'" get_webhook_data: assign: diff --git a/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/connect.yml b/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/connect.yml index b581031e..5b02e858 100644 --- a/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/connect.yml +++ b/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/connect.yml @@ -1,13 +1,52 @@ declaration: call: declare version: 0.1 - description: "Decription placeholder for 'CONNECT'" + description: "Description placeholder for 'CONNECT'" method: post accepts: json returns: json namespace: classifier + allowlist: + params: + - field: is_connect + type: boolean + description: "Parameter 'isConnect'" -get_access_token: +assign_integration: + assign: + is_connect: ${incoming.params.isConnect} + next: get_auth_header + +get_auth_header: + call: http.post + args: + url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/get_auth_header" + headers: + type: json + body: + username: "[#JIRA_USERNAME]" + token: "[#JIRA_API_TOKEN]" + result: auth_header + next: validate_integration + +validate_integration: + switch: + - condition: ${is_connect === "true"} + next: connect_jira + next: disconnect_jira + +connect_jira: + call: http.get + args: + url: "[#JIRA_CLOUD_DOMAIN]/rest/webhooks/1.0/webhook" + headers: + Accept: "application/json" + Content-Type: "application/json" + Authorization: ${auth_header.val} + result: valid_data + next: output_val + +disconnect_jira: call: http.post args: url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/verify_signature" @@ -17,6 +56,12 @@ get_access_token: payload: ${payload} headers: ${headers} result: valid_data - next: assign_verification + next: output_val + +output_val: + return: ${valid_data} + + + diff --git a/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/token.yml b/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/token.yml deleted file mode 100644 index 2500954d..00000000 --- a/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/token.yml +++ /dev/null @@ -1,26 +0,0 @@ -declaration: - call: declare - version: 0.1 - description: "Decription placeholder for 'CONNECT'" - method: post - accepts: json - returns: json - namespace: classifier - -get_access_token: - call: http.post - args: - url: "https://auth.atlassian.com/authorize" - headers: - type: json - query: - audience: "api.atlassian.com" - client_id: "client_id" - scope: "write:jira-work" - redirect_uri: "redirect_uri" - state: "state" - response_type: "code" - prompt: "consent" - result: valid_data - - diff --git a/constants.ini b/constants.ini index 6bb36f3a..91016a4a 100644 --- a/constants.ini +++ b/constants.ini @@ -2,3 +2,7 @@ CLASSIFIER_RUUTER_PUBLIC=http://ruuter-public:8086 CLASSIFIER_DMAPPER=http://data-mapper:3000 +JIRA_API_TOKEN=value +JIRA_USERNAME=value +JIRA_CLOUD_DOMAIN=value + From 9e4c5077fb39b6853aa32acb01f6ec5f41ca2299 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Tue, 18 Jun 2024 01:33:37 +0530 Subject: [PATCH 008/582] feat: user management modal handling --- GUI/.dockerignore | 7 + GUI/.env.development | 16 + GUI/.env.example | 1 + GUI/.eslintrc.cjs | 18 - GUI/.eslintrc.json | 3 + GUI/.gitignore | 7 +- GUI/.prettierignore | 1 + GUI/.prettierrc | 6 + GUI/Dockerfile | 23 + GUI/Dockerfile.dev | 14 + GUI/Dockerfile.test | 17 + GUI/README.md | 30 - GUI/docker-compose.yml | 10 + GUI/entrypoint.sh | 7 + GUI/i18n.ts | 4 +- GUI/index.html | 7 +- GUI/locales/en/translation.json | 4 - GUI/locales/et/translation.json | 4 - GUI/nginx/http-nginx.conf | 21 + GUI/nginx/https-nginx.conf | 26 + GUI/nginx/nginx.conf | 11 + GUI/nginx/scripts/env.sh | 12 + GUI/package-lock.json | 17244 ++++++++++++---- GUI/package.json | 108 +- GUI/postcss.config.js | 6 - GUI/public/favicon.ico | Bin 0 -> 15406 bytes GUI/public/mockServiceWorker.js | 303 + GUI/public/vite.svg | 1 - GUI/rebuild.sh | 12 + GUI/src/App.css | 8 - GUI/src/App.tsx | 30 +- GUI/src/assets/ding.mp3 | Bin 0 -> 26266 bytes GUI/src/assets/logo-white.svg | 29 + GUI/src/assets/logo.svg | 31 + GUI/src/assets/newMessageSound.mp3 | Bin 0 -> 20942 bytes GUI/src/assets/react.svg | 1 - GUI/src/components/Box/Box.scss | 56 + GUI/src/components/Box/index.tsx | 16 + .../components/{atoms => }/Button/Button.scss | 1 - .../components/{atoms => }/Button/index.tsx | 0 GUI/src/components/Card/Card.scss | 60 + GUI/src/components/Card/index.tsx | 37 + .../components/Collapsible/Collapsible.scss | 35 + GUI/src/components/Collapsible/index.tsx | 31 + .../{molecules => }/DataTable/CloseIcon.tsx | 0 .../{molecules => }/DataTable/DataTable.scss | 0 .../DataTable/DeboucedInput.scss | 0 .../DataTable/DebouncedInput.tsx | 0 .../{molecules => }/DataTable/Filter.tsx | 4 +- .../{molecules => }/DataTable/index.tsx | 2 +- .../{molecules => }/Dialog/Dialog.scss | 0 .../{molecules => }/Dialog/index.tsx | 4 +- GUI/src/components/Drawer/Drawer.scss | 40 + GUI/src/components/Drawer/index.tsx | 42 + .../FormCheckbox/FormCheckbox.scss | 57 + .../FormElements/FormCheckbox/index.tsx | 39 + .../FormCheckboxes/FormCheckboxes.scss | 63 + .../FormElements/FormCheckboxes/index.tsx | 44 + .../FormDatepicker/FormDatepicker.scss | 154 + .../FormElements/FormDatepicker/index.tsx | 98 + .../FormElements/FormInput/FormInput.scss | 90 + .../FormElements/FormInput/index.tsx | 46 + .../FormElements/FormRadios/FormRadios.scss | 72 + .../FormElements/FormRadios/index.tsx | 36 + .../FormSelect/FormMultiselect.tsx | 124 + .../FormElements/FormSelect/FormSelect.scss | 121 + .../FormElements/FormSelect/index.tsx | 94 + .../FormTextarea/FormTextarea.scss | 109 + .../FormElements/FormTextarea/index.tsx | 72 + .../FormElements/Switch/Switch.scss | 69 + .../components/FormElements/Switch/index.tsx | 65 + .../FormElements/SwitchBox/SwitchBox.scss | 45 + .../FormElements/SwitchBox/index.tsx | 44 + GUI/src/components/FormElements/index.tsx | 23 + GUI/src/components/Header/index.tsx | 523 + GUI/src/components/{atoms => }/Icon/Icon.scss | 0 GUI/src/components/{atoms => }/Icon/index.tsx | 0 GUI/src/components/Label/Label.scss | 76 + GUI/src/components/Label/index.tsx | 40 + GUI/src/components/Layout/Layout.scss | 36 +- GUI/src/components/Layout/index.tsx | 26 +- GUI/src/components/MainNavigation/index.tsx | 282 + GUI/src/components/Popover/Popover.scss | 15 + GUI/src/components/Popover/index.tsx | 27 + GUI/src/components/Section/Section.scss | 11 + GUI/src/components/Section/index.tsx | 13 + .../components/{atoms => }/Toast/Toast.scss | 0 .../components/{atoms => }/Toast/index.tsx | 3 + .../{atoms => }/Tooltip/Tooltip.scss | 9 +- .../components/{atoms => }/Tooltip/index.tsx | 4 +- .../components/{atoms => }/Track/index.tsx | 0 GUI/src/components/atoms/CheckBox/index.tsx | 32 - GUI/src/components/atoms/InputField/index.tsx | 27 - .../components/atoms/RadioButton/index.tsx | 34 - GUI/src/components/index.tsx | 59 +- .../components/molecules/Header/Header.scss | 8 - GUI/src/components/molecules/Header/index.tsx | 12 - .../components/molecules/SideBar/SideBar.scss | 30 - .../components/molecules/SideBar/index.tsx | 64 - GUI/src/config/rolesConfig.json | 1 + GUI/src/constants/config.ts | 5 + GUI/src/constants/menuIcons.tsx | 24 + GUI/src/hoc/with-authorization.tsx | 28 + GUI/src/index.css | 26 - GUI/src/locale/et_EE.ts | 31 + GUI/src/main.tsx | 57 +- GUI/src/mocks/handlers.ts | 21 + GUI/src/mocks/healthzStatus.ts | 18 + GUI/src/mocks/users.ts | 54 + GUI/src/model/ruuter-response-model.ts | 11 + GUI/src/modules/attachment/api.ts | 21 + GUI/src/modules/attachment/hooks.ts | 25 + GUI/src/pages/Home.tsx | 83 - GUI/src/pages/UserManagement.tsx | 139 - .../pages/UserManagement/UserManagement.scss | 29 + GUI/src/pages/UserManagement/index.tsx | 213 + GUI/src/services/api-dev.ts | 38 + GUI/src/services/api.ts | 38 + GUI/src/services/sse-service.ts | 30 + GUI/src/services/users.ts | 46 + GUI/src/static/icons/link-external-blue.svg | 8 + GUI/src/static/icons/link-external-white.svg | 1 + GUI/src/store/index.ts | 253 + GUI/src/utils/constants.ts | 19 + GUI/src/utils/format-bytes.ts | 8 + GUI/src/utils/generateUEID.ts | 8 + GUI/src/utils/local-storage-utils.ts | 17 + GUI/src/utils/state-management-utils.ts | 11 + GUI/src/vite-env.d.ts | 1 + GUI/tailwind.config.js | 12 - GUI/translations/en/common.json | 359 + GUI/translations/et/common.json | 359 + GUI/tsconfig.json | 41 +- GUI/tsconfig.node.json | 6 +- GUI/vite.config.ts | 44 +- GUI/vitePlugin.js | 25 + 136 files changed, 18225 insertions(+), 5031 deletions(-) create mode 100644 GUI/.dockerignore create mode 100644 GUI/.env.development create mode 100644 GUI/.env.example delete mode 100644 GUI/.eslintrc.cjs create mode 100644 GUI/.eslintrc.json create mode 100644 GUI/.prettierignore create mode 100644 GUI/.prettierrc create mode 100644 GUI/Dockerfile create mode 100644 GUI/Dockerfile.dev create mode 100644 GUI/Dockerfile.test delete mode 100644 GUI/README.md create mode 100644 GUI/docker-compose.yml create mode 100644 GUI/entrypoint.sh delete mode 100644 GUI/locales/en/translation.json delete mode 100644 GUI/locales/et/translation.json create mode 100644 GUI/nginx/http-nginx.conf create mode 100644 GUI/nginx/https-nginx.conf create mode 100644 GUI/nginx/nginx.conf create mode 100644 GUI/nginx/scripts/env.sh delete mode 100644 GUI/postcss.config.js create mode 100644 GUI/public/favicon.ico create mode 100644 GUI/public/mockServiceWorker.js delete mode 100644 GUI/public/vite.svg create mode 100644 GUI/rebuild.sh delete mode 100644 GUI/src/App.css create mode 100644 GUI/src/assets/ding.mp3 create mode 100644 GUI/src/assets/logo-white.svg create mode 100644 GUI/src/assets/logo.svg create mode 100644 GUI/src/assets/newMessageSound.mp3 delete mode 100644 GUI/src/assets/react.svg create mode 100644 GUI/src/components/Box/Box.scss create mode 100644 GUI/src/components/Box/index.tsx rename GUI/src/components/{atoms => }/Button/Button.scss (98%) rename GUI/src/components/{atoms => }/Button/index.tsx (100%) create mode 100644 GUI/src/components/Card/Card.scss create mode 100644 GUI/src/components/Card/index.tsx create mode 100644 GUI/src/components/Collapsible/Collapsible.scss create mode 100644 GUI/src/components/Collapsible/index.tsx rename GUI/src/components/{molecules => }/DataTable/CloseIcon.tsx (100%) rename GUI/src/components/{molecules => }/DataTable/DataTable.scss (100%) rename GUI/src/components/{molecules => }/DataTable/DeboucedInput.scss (100%) rename GUI/src/components/{molecules => }/DataTable/DebouncedInput.tsx (100%) rename GUI/src/components/{molecules => }/DataTable/Filter.tsx (96%) rename GUI/src/components/{molecules => }/DataTable/index.tsx (99%) rename GUI/src/components/{molecules => }/Dialog/Dialog.scss (100%) rename GUI/src/components/{molecules => }/Dialog/index.tsx (95%) create mode 100644 GUI/src/components/Drawer/Drawer.scss create mode 100644 GUI/src/components/Drawer/index.tsx create mode 100644 GUI/src/components/FormElements/FormCheckbox/FormCheckbox.scss create mode 100644 GUI/src/components/FormElements/FormCheckbox/index.tsx create mode 100644 GUI/src/components/FormElements/FormCheckboxes/FormCheckboxes.scss create mode 100644 GUI/src/components/FormElements/FormCheckboxes/index.tsx create mode 100644 GUI/src/components/FormElements/FormDatepicker/FormDatepicker.scss create mode 100644 GUI/src/components/FormElements/FormDatepicker/index.tsx create mode 100644 GUI/src/components/FormElements/FormInput/FormInput.scss create mode 100644 GUI/src/components/FormElements/FormInput/index.tsx create mode 100644 GUI/src/components/FormElements/FormRadios/FormRadios.scss create mode 100644 GUI/src/components/FormElements/FormRadios/index.tsx create mode 100644 GUI/src/components/FormElements/FormSelect/FormMultiselect.tsx create mode 100644 GUI/src/components/FormElements/FormSelect/FormSelect.scss create mode 100644 GUI/src/components/FormElements/FormSelect/index.tsx create mode 100644 GUI/src/components/FormElements/FormTextarea/FormTextarea.scss create mode 100644 GUI/src/components/FormElements/FormTextarea/index.tsx create mode 100644 GUI/src/components/FormElements/Switch/Switch.scss create mode 100644 GUI/src/components/FormElements/Switch/index.tsx create mode 100644 GUI/src/components/FormElements/SwitchBox/SwitchBox.scss create mode 100644 GUI/src/components/FormElements/SwitchBox/index.tsx create mode 100644 GUI/src/components/FormElements/index.tsx create mode 100644 GUI/src/components/Header/index.tsx rename GUI/src/components/{atoms => }/Icon/Icon.scss (100%) rename GUI/src/components/{atoms => }/Icon/index.tsx (100%) create mode 100644 GUI/src/components/Label/Label.scss create mode 100644 GUI/src/components/Label/index.tsx create mode 100644 GUI/src/components/MainNavigation/index.tsx create mode 100644 GUI/src/components/Popover/Popover.scss create mode 100644 GUI/src/components/Popover/index.tsx create mode 100644 GUI/src/components/Section/Section.scss create mode 100644 GUI/src/components/Section/index.tsx rename GUI/src/components/{atoms => }/Toast/Toast.scss (100%) rename GUI/src/components/{atoms => }/Toast/index.tsx (89%) rename GUI/src/components/{atoms => }/Tooltip/Tooltip.scss (64%) rename GUI/src/components/{atoms => }/Tooltip/index.tsx (83%) rename GUI/src/components/{atoms => }/Track/index.tsx (100%) delete mode 100644 GUI/src/components/atoms/CheckBox/index.tsx delete mode 100644 GUI/src/components/atoms/InputField/index.tsx delete mode 100644 GUI/src/components/atoms/RadioButton/index.tsx delete mode 100644 GUI/src/components/molecules/Header/Header.scss delete mode 100644 GUI/src/components/molecules/Header/index.tsx delete mode 100644 GUI/src/components/molecules/SideBar/SideBar.scss delete mode 100644 GUI/src/components/molecules/SideBar/index.tsx create mode 100644 GUI/src/config/rolesConfig.json create mode 100644 GUI/src/constants/config.ts create mode 100644 GUI/src/constants/menuIcons.tsx create mode 100644 GUI/src/hoc/with-authorization.tsx delete mode 100644 GUI/src/index.css create mode 100644 GUI/src/locale/et_EE.ts create mode 100644 GUI/src/mocks/handlers.ts create mode 100644 GUI/src/mocks/healthzStatus.ts create mode 100644 GUI/src/mocks/users.ts create mode 100644 GUI/src/model/ruuter-response-model.ts create mode 100644 GUI/src/modules/attachment/api.ts create mode 100644 GUI/src/modules/attachment/hooks.ts delete mode 100644 GUI/src/pages/Home.tsx delete mode 100644 GUI/src/pages/UserManagement.tsx create mode 100644 GUI/src/pages/UserManagement/UserManagement.scss create mode 100644 GUI/src/pages/UserManagement/index.tsx create mode 100644 GUI/src/services/api-dev.ts create mode 100644 GUI/src/services/api.ts create mode 100644 GUI/src/services/sse-service.ts create mode 100644 GUI/src/services/users.ts create mode 100644 GUI/src/static/icons/link-external-blue.svg create mode 100644 GUI/src/static/icons/link-external-white.svg create mode 100644 GUI/src/store/index.ts create mode 100644 GUI/src/utils/constants.ts create mode 100644 GUI/src/utils/format-bytes.ts create mode 100644 GUI/src/utils/generateUEID.ts create mode 100644 GUI/src/utils/local-storage-utils.ts create mode 100644 GUI/src/utils/state-management-utils.ts delete mode 100644 GUI/tailwind.config.js create mode 100644 GUI/translations/en/common.json create mode 100644 GUI/translations/et/common.json create mode 100644 GUI/vitePlugin.js diff --git a/GUI/.dockerignore b/GUI/.dockerignore new file mode 100644 index 00000000..ab4f96a1 --- /dev/null +++ b/GUI/.dockerignore @@ -0,0 +1,7 @@ +node_modules +npm-debug.log +build +.git +*.md +.gitignore +.env.development diff --git a/GUI/.env.development b/GUI/.env.development new file mode 100644 index 00000000..526b0e9b --- /dev/null +++ b/GUI/.env.development @@ -0,0 +1,16 @@ +REACT_APP_RUUTER_API_URL=http://localhost:8086 +REACT_APP_RUUTER_PRIVATE_API_URL=http://localhost:8088 +REACT_APP_BUEROKRATT_CHATBOT_URL=http://buerokratt-chat:8086 +REACT_APP_MENU_URL=https://admin.dev.buerokratt.ee +REACT_APP_MENU_PATH=/chat/menu.json +REACT_APP_CUSTOMER_SERVICE_LOGIN=http://localhost:3004/et/dev-auth +REACT_APP_CONVERSATIONS_BASE_URL=http://localhost:8080/chat +REACT_APP_TRAINING_BASE_URL=http://localhost:8080/training +REACT_APP_ANALYTICS_BASE_URL=http://localhost:8080/analytics +REACT_APP_SERVICES_BASE_URL=http://localhost:8080/services +REACT_APP_SETTINGS_BASE_URL=http://localhost:8080/settings +REACT_APP_MONITORING_BASE_URL=http://localhost:8080/monitoring +REACT_APP_SERVICE_ID=conversations,settings,monitoring +REACT_APP_NOTIFICATION_NODE_URL=http://localhost:4040 +REACT_APP_CSP=upgrade-insecure-requests; default-src 'self'; font-src 'self' data:; img-src 'self' data:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self' http://localhost:8086 http://localhost:8088 http://localhost:8085 http://localhost:4040 https://admin.dev.buerokratt.ee/chat/menu.json; +REACT_APP_ENABLE_HIDDEN_FEATURES=TRUE diff --git a/GUI/.env.example b/GUI/.env.example new file mode 100644 index 00000000..9ff59d7b --- /dev/null +++ b/GUI/.env.example @@ -0,0 +1 @@ +REACT_APP_RUUTER_API_URL= diff --git a/GUI/.eslintrc.cjs b/GUI/.eslintrc.cjs deleted file mode 100644 index d6c95379..00000000 --- a/GUI/.eslintrc.cjs +++ /dev/null @@ -1,18 +0,0 @@ -module.exports = { - root: true, - env: { browser: true, es2020: true }, - extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:react-hooks/recommended', - ], - ignorePatterns: ['dist', '.eslintrc.cjs'], - parser: '@typescript-eslint/parser', - plugins: ['react-refresh'], - rules: { - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, - ], - }, -} diff --git a/GUI/.eslintrc.json b/GUI/.eslintrc.json new file mode 100644 index 00000000..5e603ecd --- /dev/null +++ b/GUI/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "react-app" +} diff --git a/GUI/.gitignore b/GUI/.gitignore index 2107e24e..d79b5ca1 100644 --- a/GUI/.gitignore +++ b/GUI/.gitignore @@ -8,11 +8,16 @@ pnpm-debug.log* lerna-debug.log* node_modules -package-lock.json dist dist-ssr *.local +# testing +/coverage + +# production +/build + # Editor directories and files .vscode/* !.vscode/extensions.json diff --git a/GUI/.prettierignore b/GUI/.prettierignore new file mode 100644 index 00000000..3c3629e6 --- /dev/null +++ b/GUI/.prettierignore @@ -0,0 +1 @@ +node_modules diff --git a/GUI/.prettierrc b/GUI/.prettierrc new file mode 100644 index 00000000..0a725205 --- /dev/null +++ b/GUI/.prettierrc @@ -0,0 +1,6 @@ +{ + "trailingComma": "es5", + "tabWidth": 2, + "semi": true, + "singleQuote": true +} diff --git a/GUI/Dockerfile b/GUI/Dockerfile new file mode 100644 index 00000000..1fe950be --- /dev/null +++ b/GUI/Dockerfile @@ -0,0 +1,23 @@ +ARG nginx_version=nginx:1.25.4-alpine + +FROM node:22.0.0-alpine AS image +WORKDIR /usr/buerokratt-chatbot +COPY ./package*.json ./ + +FROM image AS build +RUN npm install --legacy-peer-deps --mode=development +COPY . . +RUN ./node_modules/.bin/vite build --mode=development +VOLUME /usr/buerokratt-chatbot + +FROM $nginx_version AS web +COPY ./nginx/http-nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build ./usr/buerokratt-chatbot/build/assets /usr/share/nginx/html/buerokratt-chatbot/chat/assets +COPY --from=build ./usr/buerokratt-chatbot/build/index.html /usr/share/nginx/html/buerokratt-chatbot +COPY --from=build ./usr/buerokratt-chatbot/build/favicon.ico /usr/share/nginx/html/buerokratt-chatbot +RUN apk add --no-cache bash +COPY ./nginx/scripts/env.sh /docker-entrypoint.d/env.sh +RUN chmod +x /docker-entrypoint.d/env.sh +EXPOSE 3001 + +ENTRYPOINT [ "bash", "/docker-entrypoint.d/env.sh" ] diff --git a/GUI/Dockerfile.dev b/GUI/Dockerfile.dev new file mode 100644 index 00000000..48b7890e --- /dev/null +++ b/GUI/Dockerfile.dev @@ -0,0 +1,14 @@ +FROM node:22.0.0-alpine AS image +WORKDIR /app +COPY ./package.json . + +FROM image AS build +RUN npm install --legacy-peer-deps --mode=development +COPY . . +RUN ./node_modules/.bin/vite build --mode=development + +EXPOSE 3001 + +ENV REACT_APP_ENABLE_HIDDEN_FEATURES TRUE + +CMD ["npm", "run", "dev"] diff --git a/GUI/Dockerfile.test b/GUI/Dockerfile.test new file mode 100644 index 00000000..cb8261f2 --- /dev/null +++ b/GUI/Dockerfile.test @@ -0,0 +1,17 @@ +FROM node:lts AS build +WORKDIR /app +COPY ./package.json . +RUN npm install --legacy-peer-deps --mode=development +COPY . . +RUN ./node_modules/.bin/vite build --mode=development + +FROM nginx:1.25.4-alpine +COPY --from=build /app /usr/share/nginx/html +#COPY ./nginx/nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 3001 +EXPOSE 80 + +ENV REACT_APP_ENABLE_HIDDEN_FEATURES TRUE + +CMD ["nginx", "-g", "daemon off;"] diff --git a/GUI/README.md b/GUI/README.md deleted file mode 100644 index 0d6babed..00000000 --- a/GUI/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# React + TypeScript + Vite - -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: - -- Configure the top-level `parserOptions` property like this: - -```js -export default { - // other rules... - parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - project: ['./tsconfig.json', './tsconfig.node.json'], - tsconfigRootDir: __dirname, - }, -} -``` - -- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` -- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` -- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list diff --git a/GUI/docker-compose.yml b/GUI/docker-compose.yml new file mode 100644 index 00000000..3b37f8c3 --- /dev/null +++ b/GUI/docker-compose.yml @@ -0,0 +1,10 @@ +version: "3.9" +services: + buerokratt_chatbot: + container_name: buerokratt_chatbot + build: + context: . + target: web + entrypoint: "/opt/buerokratt-chatbot/rebuild.sh" + ports: + - '3001:3001' diff --git a/GUI/entrypoint.sh b/GUI/entrypoint.sh new file mode 100644 index 00000000..636848f7 --- /dev/null +++ b/GUI/entrypoint.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +# Replace environment variables in the Nginx configuration template +envsubst '$BASE_URL $REACT_APP_RUUTER_API_URL $REACT_APP_RUUTER_V1_PRIVATE_API_URL $REACT_APP_RUUTER_V2_PRIVATE_API_URL $REACT_APP_CUSTOMER_SERVICE_LOGIN $CHOKIDAR_USEPOLLING $PORT' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf + +# Start the Nginx server +nginx -g "daemon off;" diff --git a/GUI/i18n.ts b/GUI/i18n.ts index e16cd3f2..6a4593d0 100644 --- a/GUI/i18n.ts +++ b/GUI/i18n.ts @@ -2,8 +2,8 @@ import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; import LanguageDetector from 'i18next-browser-languagedetector'; -import commonEN from './locales/en/translation.json'; -import commonET from './locales/et/translation.json'; +import commonEN from './translations/en/common.json'; +import commonET from './translations/et/common.json'; i18n .use(LanguageDetector) diff --git a/GUI/index.html b/GUI/index.html index e4b78eae..047cff35 100644 --- a/GUI/index.html +++ b/GUI/index.html @@ -1,13 +1,14 @@ - + - + - Vite + React + TS + Bürokratt
+
diff --git a/GUI/locales/en/translation.json b/GUI/locales/en/translation.json deleted file mode 100644 index 8bde3754..00000000 --- a/GUI/locales/en/translation.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "Title":"En Title", - "Description":"En Description" -} \ No newline at end of file diff --git a/GUI/locales/et/translation.json b/GUI/locales/et/translation.json deleted file mode 100644 index b48e8799..00000000 --- a/GUI/locales/et/translation.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "Title":"Et Title", - "Description":"Et Description" -} \ No newline at end of file diff --git a/GUI/nginx/http-nginx.conf b/GUI/nginx/http-nginx.conf new file mode 100644 index 00000000..c246c74b --- /dev/null +++ b/GUI/nginx/http-nginx.conf @@ -0,0 +1,21 @@ +server { + server_name localhost; + listen 3001; # replace port depends on deploy environment + + server_tokens off; + add_header Access-Control-Allow-Origin *; + + location / { + root /usr/share/nginx/html/buerokratt-chatbot; + try_files $uri /index.html; + } + + location /status { + access_log off; + default_type text/plain; + add_header Content-Type text/plain; + return 200 "alive"; + } + + rewrite ^/$ http://$host:$server_port/chat permanent; # replace port depends on deploy environment +} diff --git a/GUI/nginx/https-nginx.conf b/GUI/nginx/https-nginx.conf new file mode 100644 index 00000000..efd2b2ca --- /dev/null +++ b/GUI/nginx/https-nginx.conf @@ -0,0 +1,26 @@ +server { + server_name localhost; + listen 443 ssl; + ssl_certificate /etc/tls/tls.crt; + ssl_certificate_key /etc/tls/tls.key; + + server_tokens off; + + location / { + root /usr/share/nginx/html/buerokratt-chatbot; + try_files $uri /index.html; + } + + location /status { + access_log off; + default_type text/plain; + add_header Content-Type text/plain; + return 200 "alive"; + } +} + +server { + listen 3001; + server_name localhost; + return 301 https://$host$request_uri; +} diff --git a/GUI/nginx/nginx.conf b/GUI/nginx/nginx.conf new file mode 100644 index 00000000..d28f7e8d --- /dev/null +++ b/GUI/nginx/nginx.conf @@ -0,0 +1,11 @@ +server { + listen 80; + server_name localhost; + + location / { + root /usr/share/nginx/html; + index index.html; + try_files $uri $uri/ /index.html; + } + +} diff --git a/GUI/nginx/scripts/env.sh b/GUI/nginx/scripts/env.sh new file mode 100644 index 00000000..de80ffdc --- /dev/null +++ b/GUI/nginx/scripts/env.sh @@ -0,0 +1,12 @@ +#!/bin/sh +for envrow in $(printenv); +do + IFS='=' read -r key value <<< "${envrow}" + if [[ $key == "REACT_APP_"* ]]; then + for file in $(find /usr/share/nginx/html -type f \( -name '*.js' -o -name '*.css' \)) + do + sed -i "s|{}.${key}|\"${value}\"|g" $file; + done + fi +done +[ -z "$@" ] && nginx -g 'daemon off;' || $@ diff --git a/GUI/package-lock.json b/GUI/package-lock.json index 049ad2be..c29181eb 100644 --- a/GUI/package-lock.json +++ b/GUI/package-lock.json @@ -1,16 +1,19 @@ { - "name": "est-gov-classifier", + "name": "byk-training-module-gui", "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "est-gov-classifier", + "name": "byk-training-module-gui", "version": "0.0.0", "dependencies": { "@buerokratt-ria/header": "^0.1.6", - "@buerokratt-ria/menu": "^0.1.15", + "@buerokratt-ria/menu": "^0.1.11", "@buerokratt-ria/styles": "^0.0.1", + "@fontsource/roboto": "^4.5.8", + "@formkit/auto-animate": "^1.0.0-beta.5", + "@fortaine/fetch-event-source": "^3.0.6", "@radix-ui/react-accessible-icon": "^1.0.1", "@radix-ui/react-collapsible": "^1.0.1", "@radix-ui/react-dialog": "^1.0.2", @@ -19,47 +22,70 @@ "@radix-ui/react-switch": "^1.0.1", "@radix-ui/react-tabs": "^1.0.1", "@radix-ui/react-toast": "^1.1.2", - "@radix-ui/themes": "^3.0.5", + "@radix-ui/react-tooltip": "^1.0.2", "@tanstack/match-sorter-utils": "^8.7.2", "@tanstack/react-query": "^4.20.4", "@tanstack/react-table": "^8.7.4", - "clsx": "^2.1.1", - "i18next": "^23.11.5", - "i18next-browser-languagedetector": "^8.0.0", + "axios": "^1.2.1", + "clsx": "^1.2.1", + "date-fns": "^2.29.3", + "downshift": "^7.0.5", + "esbuild": "^0.19.5", + "framer-motion": "^8.5.5", + "howler": "^2.2.4", + "i18next": "^22.4.5", + "i18next-browser-languagedetector": "^7.0.1", + "linkify-react": "^4.1.1", + "linkifyjs": "^4.1.1", + "lodash": "^4.17.21", "react": "^18.2.0", + "react-color": "^2.19.3", + "react-cookie": "^4.1.1", + "react-datepicker": "^4.8.0", "react-dom": "^18.2.0", - "react-i18next": "^14.1.2", + "react-hook-form": "^7.41.5", + "react-i18next": "^12.1.1", "react-icons": "^4.10.1", - "react-router-dom": "^6.23.1" + "react-idle-timer": "^5.5.2", + "react-modal": "^3.16.1", + "react-redux": "^8.1.1", + "react-router-dom": "^6.5.0", + "react-select": "^5.7.4", + "react-text-selection-popover": "^2.0.2", + "react-textarea-autosize": "^8.4.0", + "reactflow": "^11.4.0", + "regexify-string": "^1.0.19", + "rxjs": "^7.8.1", + "timeago.js": "^4.0.2", + "usehooks-ts": "^2.9.1", + "uuid": "^9.0.0", + "zustand": "^4.4.4" }, "devDependencies": { - "@types/react": "^18.2.66", - "@types/react-dom": "^18.2.22", - "@types/react-i18next": "^8.1.0", - "@typescript-eslint/eslint-plugin": "^7.2.0", - "@typescript-eslint/parser": "^7.2.0", - "@vitejs/plugin-react": "^4.2.1", - "autoprefixer": "^10.4.19", - "eslint": "^8.57.0", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.6", - "postcss": "^8.4.38", - "sass": "^1.77.4", - "tailwindcss": "^3.4.4", - "typescript": "^5.2.2", - "vite": "^5.2.0" - } - }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "@types/howler": "^2.2.11", + "@types/lodash": "^4.14.191", + "@types/lodash.debounce": "^4.0.7", + "@types/node": "^18.11.17", + "@types/react": "^18.0.26", + "@types/react-color": "^3.0.6", + "@types/react-datepicker": "^4.8.0", + "@types/react-dom": "^18.0.9", + "@types/uuid": "^9.0.2", + "@vitejs/plugin-react": "^3.0.0", + "eslint": "^8.30.0", + "eslint-config-react-app": "^7.0.1", + "eslint-plugin-react": "^7.31.11", + "eslint-plugin-typescript": "^0.14.0", + "mocksse": "^1.0.4", + "msw": "^0.49.2", + "prettier": "^2.8.1", + "sass": "^1.57.0", + "typescript": "^4.9.3", + "vite": "^4.0.0", + "vite-plugin-env-compatible": "^1.1.1", + "vite-plugin-svgr": "^2.4.0", + "vite-plugin-transform": "^2.0.1", + "vite-tsconfig-paths": "^4.0.3" } }, "node_modules/@ampproject/remapping": { @@ -79,7 +105,6 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", - "dev": true, "dependencies": { "@babel/highlight": "^7.24.7", "picocolors": "^1.0.0" @@ -127,20 +152,37 @@ "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "node_modules/@babel/eslint-parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.24.7.tgz", + "integrity": "sha512-SO5E3bVxDuxyNxM5agFv480YA2HO6ohZbGxbazZdIk3KQOPOGVNw6q78I9/lbviIf95eq6tPozeYnJLbjnC8IA==", "dev": true, - "bin": { - "semver": "bin/semver.js" + "dependencies": { + "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", + "eslint-visitor-keys": "^2.1.0", + "semver": "^6.3.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || >=14.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0", + "eslint": "^7.5.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/@babel/eslint-parser/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" } }, "node_modules/@babel/generator": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", - "dev": true, "dependencies": { "@babel/types": "^7.24.7", "@jridgewell/gen-mapping": "^0.3.5", @@ -151,6 +193,31 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.7.tgz", + "integrity": "sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-compilation-targets": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", @@ -167,20 +234,66 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.7.tgz", + "integrity": "sha512-kTkaDl7c9vO80zeX1rJxnuRpEsD5tA81yh11X1gQo+PhSti3JS+7qeZo9U4RHobKRiFPKaGK3svUAeb8D0Q7eg==", "dev": true, - "bin": { - "semver": "bin/semver.js" + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.7", + "@babel/helper-optimise-call-expression": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.24.7.tgz", + "integrity": "sha512-03TCmXy2FtXJEZfbXDTSqq1fRJArk7lX9DOFC/47VthYcxyIOx+eXQmdo6DOQvrbpIix+KfXwvuXdFDZHxt+rA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz", + "integrity": "sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/@babel/helper-environment-visitor": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", - "dev": true, "dependencies": { "@babel/types": "^7.24.7" }, @@ -192,7 +305,6 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", - "dev": true, "dependencies": { "@babel/template": "^7.24.7", "@babel/types": "^7.24.7" @@ -205,8 +317,20 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.7.tgz", + "integrity": "sha512-LGeMaf5JN4hAT471eJdBs/GK1DoYIJ5GCtZN/EsL6KUiiDZOvO/eKE11AMZJa2zP4zk4qe9V2O/hxAmkRc8p6w==", "dev": true, "dependencies": { + "@babel/traverse": "^7.24.7", "@babel/types": "^7.24.7" }, "engines": { @@ -217,7 +341,6 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", - "dev": true, "dependencies": { "@babel/traverse": "^7.24.7", "@babel/types": "^7.24.7" @@ -245,6 +368,18 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz", + "integrity": "sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-plugin-utils": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", @@ -254,6 +389,40 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.24.7.tgz", + "integrity": "sha512-9pKLcTlZ92hNZMQfGCHImUpDOlAgkkpqalWEeftW5FBya75k8Li2ilerxkM/uBEj01iBZXcCIB/bwvDYgWyibA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-wrap-function": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.7.tgz", + "integrity": "sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.7", + "@babel/helper-optimise-call-expression": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/helper-simple-access": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", @@ -267,11 +436,23 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz", + "integrity": "sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-split-export-declaration": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", - "dev": true, "dependencies": { "@babel/types": "^7.24.7" }, @@ -283,7 +464,6 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -292,7 +472,6 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -306,6 +485,21 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.24.7.tgz", + "integrity": "sha512-N9JIYk3TD+1vq/wn77YnJOqMtfWhNewNE+DJV4puD2X7Ew9J4JvrzrFDfTfyv5EgEXVy9/Wt8QiOErzEmv5Ifw==", + "dev": true, + "dependencies": { + "@babel/helper-function-name": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helpers": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", @@ -323,7 +517,6 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", - "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.24.7", "chalk": "^2.4.2", @@ -338,7 +531,6 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", - "dev": true, "bin": { "parser": "bin/babel-parser.js" }, @@ -346,25 +538,26 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/plugin-transform-react-jsx-self": { + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.7.tgz", - "integrity": "sha512-fOPQYbGSgH0HUp4UJO4sMBFjY6DuWq+2i8rixyUMb3CdGixs/gccURvYOAhajBdKDoGajFr3mUq5rH3phtkGzw==", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.7.tgz", + "integrity": "sha512-TiT1ss81W80eQsN+722OaeQMY/G4yTb4G9JrqeiDADs3N8lbPMGldWi9x8tyqCW5NLx1Jh2AvkE6r6QvEltMMQ==", "dev": true, "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-transform-react-jsx-source": { + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.7.tgz", - "integrity": "sha512-J2z+MWzZHVOemyLweMqngXrgGC42jQ//R0KdxqkIz/OrbVIIlhFI3WigZ5fO+nwFvBlncr4MGapd8vTyc7RPNQ==", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.7.tgz", + "integrity": "sha512-unaQgZ/iRu/By6tsjMZzpeBZjChYfLYry6HrEXPoz3KmfF0sVBQ1l8zKMQ4xRGLWVsjuvB8nQfjNP/DcfEOCsg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.24.7" @@ -373,3562 +566,11477 @@ "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0" } }, - "node_modules/@babel/runtime": { + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", - "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz", + "integrity": "sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==", + "dev": true, "dependencies": { - "regenerator-runtime": "^0.14.0" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.7" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" } }, - "node_modules/@babel/template": { + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", - "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.7.tgz", + "integrity": "sha512-utA4HuR6F4Vvcr+o4DnjL8fCOlgRFGbeeBEGNg3ZTrLFw6VWG5XmUrvcQ0FjIYMU2ST4XcR2Wsp7t9qOAPnxMg==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@babel/traverse": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", - "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", + "node_modules/@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.", "dev": true, "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.7", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-hoist-variables": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/types": { + "node_modules/@babel/plugin-proposal-decorators": { "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", - "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.24.7.tgz", + "integrity": "sha512-RL9GR0pUG5Kc8BUWLNDm2T5OpYwSX15r98I0IkgmRQTXuELq/OynH8xtMTMvTJFjXbMWFVTKtYkTaYQsuAwQlQ==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7", - "to-fast-properties": "^2.0.0" + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-decorators": "^7.24.7" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@buerokratt-ria/header": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@buerokratt-ria/header/-/header-0.1.6.tgz", - "integrity": "sha512-sPynHp0LQvBdjqNma6KmAGHzfP3qnpFl4WdZDpHBRJS/f09EPlEvSfV6N6D693i9M7oXD9WhykIvthcugoeCbQ==", + "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", + "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead.", + "dev": true, "dependencies": { - "@buerokratt-ria/styles": "^0.0.1", - "@types/react": "^18.2.21", - "react": "^18.2.0" + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { - "@fontsource/roboto": "^4.5.8", - "@formkit/auto-animate": "^0.7.0", - "@radix-ui/react-accessible-icon": "^1.0.3", - "@radix-ui/react-dialog": "^1.0.4", - "@radix-ui/react-switch": "^1.0.3", - "@radix-ui/react-toast": "^1.1.4", - "@tanstack/react-query": "^4.32.1", - "axios": "^1.4.0", - "clsx": "^1.2.1", - "i18next": "^23.2.3", - "i18next-browser-languagedetector": "^7.1.0", - "path": "^0.12.7", - "react": "^18.2.0", - "react-cookie": "^4.1.1", - "react-dom": "^18.2.0", - "react-hook-form": "^7.45.4", - "react-i18next": "^12.1.1", - "react-icons": "^4.10.1", - "react-idle-timer": "^5.7.2", - "react-router-dom": "^6.14.2", - "rxjs": "^7.8.1", - "tslib": "^2.3.0", - "vite-plugin-dts": "^3.5.2", - "vite-plugin-svgr": "^3.2.0", - "zustand": "^4.4.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/@buerokratt-ria/menu": { - "version": "0.1.16", - "resolved": "https://registry.npmjs.org/@buerokratt-ria/menu/-/menu-0.1.16.tgz", - "integrity": "sha512-k6G9I1Y7y98E7Re8QI+vm5EjYEVpGwTP9n8aStt/ScdiWmwrw12hFd/yRLj3kO8phMvWUPUfBPuE3UajlfoesA==", + "node_modules/@babel/plugin-proposal-numeric-separator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", + "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead.", + "dev": true, "dependencies": { - "@buerokratt-ria/styles": "^0.0.1", - "@types/react": "^18.2.21", - "react": "^18.2.0" + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { - "@radix-ui/react-accessible-icon": "^1.0.3", - "@radix-ui/react-dialog": "^1.0.4", - "@radix-ui/react-switch": "^1.0.3", - "@radix-ui/react-toast": "^1.1.4", - "@tanstack/react-query": "^4.32.1", - "clsx": "^1.2.1", - "i18next": "^23.2.3", - "i18next-browser-languagedetector": "^7.1.0", - "path": "^0.12.7", - "react": "^18.2.0", - "react-cookie": "^4.1.1", - "react-dom": "^18.2.0", - "react-hook-form": "^7.45.4", - "react-i18next": "^12.1.1", - "react-icons": "^4.10.1", - "react-idle-timer": "^5.7.2", - "react-router-dom": "^6.14.2", - "rxjs": "^7.8.1", - "tslib": "^2.3.0", - "vite-plugin-dts": "^3.5.2", - "vite-plugin-svgr": "^3.2.0", - "zustand": "^4.4.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/@buerokratt-ria/styles": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@buerokratt-ria/styles/-/styles-0.0.1.tgz", - "integrity": "sha512-bSj7WsdQO4P/43mRgsa5sDEwBuOebXcl3+Peur8NwToqczqsTMbXSO5P6xyXHoTnHWt082PhT8ht7OAgtFSzfw==" - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", - "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", - "cpu": [ - "ppc64" - ], + "node_modules/@babel/plugin-proposal-optional-chaining": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", + "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead.", "dev": true, - "optional": true, - "os": [ - "aix" - ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", - "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", - "cpu": [ - "arm" - ], + "node_modules/@babel/plugin-proposal-private-methods": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", + "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead.", "dev": true, - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", - "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", "dev": true, - "optional": true, - "os": [ - "android" - ], "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", - "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", - "cpu": [ - "x64" - ], + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", - "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", - "cpu": [ - "x64" - ], + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", "dev": true, - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", - "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.24.7.tgz", + "integrity": "sha512-Ui4uLJJrRV1lb38zg1yYTmRKmiZLiftDEvZN2iq3kd9kUFU+PttmzTbAFC2ucRk/XJmtek6G23gPsuZbhrT8fQ==", "dev": true, - "optional": true, - "os": [ - "freebsd" - ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", - "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", - "cpu": [ - "x64" - ], + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", - "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", - "cpu": [ - "arm" - ], + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", - "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/plugin-syntax-flow": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.24.7.tgz", + "integrity": "sha512-9G8GYT/dxn/D1IIKOUBmGX0mnmj46mGH9NnZyJLwtCpgh5f7D2VbuKodb+2s9m1Yavh1s7ASQN8lf0eqrb1LTw==", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", - "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", - "cpu": [ - "ia32" - ], + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.7.tgz", + "integrity": "sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg==", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", - "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", - "cpu": [ - "loong64" - ], + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz", + "integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", - "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", - "cpu": [ - "mips64el" - ], + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", - "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", - "cpu": [ - "ppc64" - ], + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", - "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", - "cpu": [ - "riscv64" - ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", + "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", - "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", - "cpu": [ - "s390x" - ], + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", - "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", - "cpu": [ - "x64" - ], + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", - "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", - "cpu": [ - "x64" - ], + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", - "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", - "cpu": [ - "x64" - ], + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", - "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", - "cpu": [ - "x64" - ], + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", - "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", - "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", - "cpu": [ - "ia32" - ], + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", "dev": true, - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", - "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", - "cpu": [ - "x64" - ], + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", "dev": true, - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz", + "integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==", "dev": true, "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=6.9.0" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.1.tgz", - "integrity": "sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==", + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.7.tgz", + "integrity": "sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ==", "dev": true, "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.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" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=6.9.0" }, - "funding": { - "url": "https://opencollective.com/eslint" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@eslint/eslintrc/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==", + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.24.7.tgz", + "integrity": "sha512-o+iF77e3u7ZS4AoAuJvapz9Fm001PuD2V3Lp6OSE4FYQke+cSewYtnek+THqGRWyQloRCyvWL1OkyfNEl9vr/g==", "dev": true, "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-remap-async-to-generator": "^7.24.7", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.7.tgz", + "integrity": "sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA==", "dev": true, "dependencies": { - "type-fest": "^0.20.2" + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-remap-async-to-generator": "^7.24.7" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.7.tgz", + "integrity": "sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==", "dev": true, "dependencies": { - "brace-expansion": "^1.1.7" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { - "node": "*" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.7.tgz", + "integrity": "sha512-Nd5CvgMbWc+oWzBsuaMcbwjJWAcp5qzrbg69SZdHSP7AMY0AbWFqFO0WTFCA1jxhMCwodRwvRec8k0QUbZk7RQ==", "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@floating-ui/core": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.2.tgz", - "integrity": "sha512-+2XpQV9LLZeanU4ZevzRnGFg2neDeKHgFLjP6YLW+tly0IvrhqT4u8enLGjLH3qeh85g19xY5rsAusfwTdn5lg==", + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.7.tgz", + "integrity": "sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w==", + "dev": true, "dependencies": { - "@floating-ui/utils": "^0.2.0" + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@floating-ui/dom": { - "version": "1.6.5", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.5.tgz", - "integrity": "sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw==", + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.7.tgz", + "integrity": "sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ==", + "dev": true, "dependencies": { - "@floating-ui/core": "^1.0.0", - "@floating-ui/utils": "^0.2.0" + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" } }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.0.tgz", - "integrity": "sha512-lNzj5EQmEKn5FFKc04+zasr09h/uX8RtJRNj5gUXsSQIXHVWTVh+hVAg1vOMCexkX8EgvemMvIFpQfkosnVNyA==", + "node_modules/@babel/plugin-transform-classes": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.7.tgz", + "integrity": "sha512-CFbbBigp8ln4FU6Bpy6g7sE8B/WmCmzvivzUC6xDAdWVsjYTXijpuuGJmYkAaoWAzcItGKT3IOAbxRItZ5HTjw==", + "dev": true, "dependencies": { - "@floating-ui/dom": "^1.0.0" + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/@floating-ui/utils": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.2.tgz", - "integrity": "sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==" - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz", + "integrity": "sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/template": "^7.24.7" }, "engines": { - "node": ">=10.10.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@humanwhocodes/config-array/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==", + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.7.tgz", + "integrity": "sha512-19eJO/8kdCQ9zISOf+SEUJM/bAUIsvY3YDnXZTupUCQ8LgrWnsG/gFB9dvXqdXnRXMAM8fvt7b0CBKQHNGy1mw==", "dev": true, "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz", + "integrity": "sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==", "dev": true, "dependencies": { - "brace-expansion": "^1.1.7" + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { - "node": "*" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "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==", + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz", + "integrity": "sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==", "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, "engines": { - "node": ">=12.22" + "node": ">=6.9.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "dev": true - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.7.tgz", + "integrity": "sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg==", "dev": true, "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.7.tgz", + "integrity": "sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ==", "dev": true, + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.7.tgz", + "integrity": "sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA==", "dev": true, "dependencies": { - "ansi-regex": "^6.0.1" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" }, "engines": { - "node": ">=12" + "node": ">=6.9.0" }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "node_modules/@babel/plugin-transform-flow-strip-types": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.24.7.tgz", + "integrity": "sha512-cjRKJ7FobOH2eakx7Ja+KpJRj8+y+/SiB3ooYm/n2UJfxu0oEaOoxOinitkJcPqv9KxS0kxTGPUaR7L2XcXDXA==", "dev": true, "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-flow": "^7.24.7" }, "engines": { - "node": ">=6.0.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.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==", + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz", + "integrity": "sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g==", "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" + }, "engines": { - "node": ">=6.0.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.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==", + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.7.tgz", + "integrity": "sha512-U9FcnA821YoILngSmYkW6FjyQe2TyZD5pHt4EVIhmcTkrJw/3KqcrRSxuOo5tFZJi7TE19iDyI1u+weTI7bn2w==", "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, "engines": { - "node": ">=6.0.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true - }, - "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==", + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.7.tgz", + "integrity": "sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw==", "dev": true, "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "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==", + "node_modules/@babel/plugin-transform-literals": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.7.tgz", + "integrity": "sha512-vcwCbb4HDH+hWi8Pqenwnjy+UiklO4Kt1vfspcQYFhJdpthSnW8XvWGyDZWKNVrVbVViI/S7K9PDJZiUmP2fYQ==", "dev": true, "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { - "node": ">= 8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "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==", + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.7.tgz", + "integrity": "sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw==", "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, "engines": { - "node": ">= 8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "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==", + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.7.tgz", + "integrity": "sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw==", "dev": true, "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { - "node": ">= 8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz", + "integrity": "sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==", "dev": true, - "optional": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, "engines": { - "node": ">=14" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@radix-ui/colors": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-3.0.0.tgz", - "integrity": "sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==" - }, - "node_modules/@radix-ui/number": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.1.tgz", - "integrity": "sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==", + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.7.tgz", + "integrity": "sha512-iFI8GDxtevHJ/Z22J5xQpVqFLlMNstcLXh994xifFwxxGslr2ZXXLWgtBeLctOD63UFDArdvN6Tg8RFw+aEmjQ==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10" + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@radix-ui/primitive": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", - "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==", + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.7.tgz", + "integrity": "sha512-GYQE0tW7YoaN13qFh3O1NCY4MPkUiAH3fiF7UcV/I3ajmDKEdG3l+UOcbAm4zUE3gnvUU+Eni7XrVKo9eO9auw==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10" + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@radix-ui/react-accessible-icon": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.0.3.tgz", - "integrity": "sha512-duVGKeWPSUILr/MdlPxV+GeULTc2rS1aihGdQ3N2qCUPMgxYLxvAsHJM3mCVLF8d5eK+ympmB22mb1F3a5biNw==", + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz", + "integrity": "sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-visually-hidden": "1.0.3" + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">=6.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@radix-ui/react-alert-dialog": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.0.5.tgz", - "integrity": "sha512-OrVIOcZL0tl6xibeuGt5/+UxoT2N27KCFOPjFyfXMnchxSHZ/OW7cCX2nGlIYJrbHK/fczPcFzAwvNBB6XBNMA==", + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.7.tgz", + "integrity": "sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-dialog": "1.0.5", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2" + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">=6.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@radix-ui/react-arrow": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz", - "integrity": "sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==", + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz", + "integrity": "sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" + "@babel/helper-plugin-utils": "^7.24.7" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">=6.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@radix-ui/react-aspect-ratio": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.0.3.tgz", - "integrity": "sha512-fXR5kbMan9oQqMuacfzlGG/SQMcmMlZ4wrvpckv8SgUulD0MMpspxJrxg/Gp/ISV3JfV1AeSWTYK9GvxA4ySwA==", + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.7.tgz", + "integrity": "sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">=6.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@radix-ui/react-avatar": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.0.4.tgz", - "integrity": "sha512-kVK2K7ZD3wwj3qhle0ElXhOjbezIgyl2hVvgwfIdexL3rN6zJmy5AqqIf+D31lxVppdzV8CjAfZ6PklkmInZLw==", + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz", + "integrity": "sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">=6.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@radix-ui/react-checkbox": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.0.4.tgz", - "integrity": "sha512-CBuGQa52aAYnADZVt/KBQzXrwx6TqnlwtcIPGtVt5JkkzQwMOLJjPukimhfKEr4GQNd43C+djUh5Ikopj8pSLg==", + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz", + "integrity": "sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-controllable-state": "1.0.1", - "@radix-ui/react-use-previous": "1.0.1", - "@radix-ui/react-use-size": "1.0.1" + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.24.7" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">=6.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@radix-ui/react-collapsible": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.0.3.tgz", - "integrity": "sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==", + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.7.tgz", + "integrity": "sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-controllable-state": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">=6.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@radix-ui/react-collection": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz", - "integrity": "sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==", + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.7.tgz", + "integrity": "sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">=6.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", - "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.7.tgz", + "integrity": "sha512-tK+0N9yd4j+x/4hxF3F0e0fu/VdcxU18y5SevtyM/PCFlQvXbR0Zmlo2eBrKtVipGNFzpq56o8WsIIKcJFUCRQ==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">=6.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@radix-ui/react-context": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", - "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.7.tgz", + "integrity": "sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10" + "@babel/helper-plugin-utils": "^7.24.7" }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">=6.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@radix-ui/react-context-menu": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.1.5.tgz", - "integrity": "sha512-R5XaDj06Xul1KGb+WP8qiOh7tKJNz2durpLBXAGZjSVtctcRFCuEvy2gtMwRJGePwQQE5nV77gs4FwRi8T+r2g==", + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.7.tgz", + "integrity": "sha512-COTCOkG2hn4JKGEKBADkA8WNb35TGkkRbI5iT845dB+NyqgO8Hn+ajPbSnIQznneJTa3d30scb6iz/DhH8GsJQ==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-menu": "2.0.6", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-controllable-state": "1.0.1" + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "@babel/core": "^7.0.0-0" } }, - "node_modules/@radix-ui/react-dialog": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz", - "integrity": "sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==", + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.7.tgz", + "integrity": "sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.5", - "@radix-ui/react-focus-guards": "1.0.1", - "@radix-ui/react-focus-scope": "1.0.4", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-portal": "1.0.4", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2", - "@radix-ui/react-use-controllable-state": "1.0.1", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.5" + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">=6.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@radix-ui/react-direction": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.1.tgz", - "integrity": "sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==", + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.7.tgz", + "integrity": "sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10" + "@babel/helper-plugin-utils": "^7.24.7" }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">=6.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz", - "integrity": "sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==", + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.24.7.tgz", + "integrity": "sha512-H/Snz9PFxKsS1JLI4dJLtnJgCJRoo0AUm3chP6NYr+9En1JMKloheEiLIhlp5MDVznWo+H3AAC1Mc8lmUEpsgg==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-escape-keydown": "1.0.3" + "@babel/helper-plugin-utils": "^7.24.7" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">=6.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.6.tgz", - "integrity": "sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA==", + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.24.7.tgz", + "integrity": "sha512-+Dj06GDZEFRYvclU6k4bme55GKBEWUmByM/eoKuqg4zTNQHiApWRhQph5fxQB2wAEFvRzL1tOEj1RJ19wJrhoA==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-menu": "2.0.6", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-controllable-state": "1.0.1" + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-jsx": "^7.24.7", + "@babel/types": "^7.24.7" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">=6.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@radix-ui/react-focus-guards": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz", - "integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==", + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.24.7.tgz", + "integrity": "sha512-QG9EnzoGn+Qar7rxuW+ZOsbWOt56FvvI93xInqsZDC5fsekx1AlIO4KIJ5M+D0p0SqSH156EpmZyXq630B8OlQ==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10" + "@babel/plugin-transform-react-jsx": "^7.24.7" }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">=6.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@radix-ui/react-focus-scope": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz", - "integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==", + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.7.tgz", + "integrity": "sha512-fOPQYbGSgH0HUp4UJO4sMBFjY6DuWq+2i8rixyUMb3CdGixs/gccURvYOAhajBdKDoGajFr3mUq5rH3phtkGzw==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1" + "@babel/helper-plugin-utils": "^7.24.7" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">=6.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@radix-ui/react-form": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.0.3.tgz", - "integrity": "sha512-kgE+Z/haV6fxE5WqIXj05KkaXa3OkZASoTDy25yX2EIp/x0c54rOH/vFr5nOZTg7n7T1z8bSyXmiVIFP9bbhPQ==", + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.7.tgz", + "integrity": "sha512-J2z+MWzZHVOemyLweMqngXrgGC42jQ//R0KdxqkIz/OrbVIIlhFI3WigZ5fO+nwFvBlncr4MGapd8vTyc7RPNQ==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-label": "2.0.2", - "@radix-ui/react-primitive": "1.0.3" + "@babel/helper-plugin-utils": "^7.24.7" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">=6.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@radix-ui/react-hover-card": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.0.7.tgz", - "integrity": "sha512-OcUN2FU0YpmajD/qkph3XzMcK/NmSk9hGWnjV68p6QiZMgILugusgQwnLSDs3oFSJYGKf3Y49zgFedhGh04k9A==", + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.24.7.tgz", + "integrity": "sha512-PLgBVk3fzbmEjBJ/u8kFzOqS9tUeDjiaWud/rRym/yjCo/M9cASPlnrd2ZmmZpQT40fOOrvR8jh+n8jikrOhNA==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.5", - "@radix-ui/react-popper": "1.1.3", - "@radix-ui/react-portal": "1.0.4", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-controllable-state": "1.0.1" + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">=6.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@radix-ui/react-id": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", - "integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==", + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.7.tgz", + "integrity": "sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-layout-effect": "1.0.1" + "@babel/helper-plugin-utils": "^7.24.7", + "regenerator-transform": "^0.15.2" }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">=6.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@radix-ui/react-label": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.0.2.tgz", - "integrity": "sha512-N5ehvlM7qoTLx7nWPodsPYPgMzA5WM8zZChQg8nyFJKnDO5WHdba1vv5/H6IO5LtJMfD2Q3wh1qHFGNtK0w3bQ==", + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz", + "integrity": "sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" + "@babel/helper-plugin-utils": "^7.24.7" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">=6.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@radix-ui/react-menu": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.0.6.tgz", - "integrity": "sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA==", + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.7.tgz", + "integrity": "sha512-YqXjrk4C+a1kZjewqt+Mmu2UuV1s07y8kqcUf4qYLnoqemhR4gRQikhdAhSVJioMjVTu6Mo6pAbaypEA3jY6fw==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-collection": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.5", - "@radix-ui/react-focus-guards": "1.0.1", - "@radix-ui/react-focus-scope": "1.0.4", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-popper": "1.1.3", - "@radix-ui/react-portal": "1.0.4", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-roving-focus": "1.0.4", - "@radix-ui/react-slot": "1.0.2", - "@radix-ui/react-use-callback-ref": "1.0.1", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.5" + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.1", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "semver": "^6.3.1" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">=6.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@radix-ui/react-navigation-menu": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.1.4.tgz", - "integrity": "sha512-Cc+seCS3PmWmjI51ufGG7zp1cAAIRqHVw7C9LOA2TZ+R4hG6rDvHcTqIsEEFLmZO3zNVH72jOOE7kKNy8W+RtA==", + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.7.tgz", + "integrity": "sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-collection": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.5", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-controllable-state": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1", - "@radix-ui/react-use-previous": "1.0.1", - "@radix-ui/react-visually-hidden": "1.0.3" + "@babel/helper-plugin-utils": "^7.24.7" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">=6.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@radix-ui/react-popover": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.0.7.tgz", - "integrity": "sha512-shtvVnlsxT6faMnK/a7n0wptwBD23xc1Z5mdrtKLwVEfsEMXodS0r5s0/g5P0hX//EKYZS2sxUjqfzlg52ZSnQ==", + "node_modules/@babel/plugin-transform-spread": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz", + "integrity": "sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.5", - "@radix-ui/react-focus-guards": "1.0.1", - "@radix-ui/react-focus-scope": "1.0.4", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-popper": "1.1.3", - "@radix-ui/react-portal": "1.0.4", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2", - "@radix-ui/react-use-controllable-state": "1.0.1", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.5" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">=6.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@radix-ui/react-popper": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.3.tgz", - "integrity": "sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==", + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz", + "integrity": "sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1", - "@radix-ui/react-use-rect": "1.0.1", - "@radix-ui/react-use-size": "1.0.1", - "@radix-ui/rect": "1.0.1" + "@babel/helper-plugin-utils": "^7.24.7" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">=6.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@radix-ui/react-portal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz", - "integrity": "sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==", + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz", + "integrity": "sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" + "@babel/helper-plugin-utils": "^7.24.7" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">=6.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@radix-ui/react-presence": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz", - "integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==", + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.7.tgz", + "integrity": "sha512-VtR8hDy7YLB7+Pet9IarXjg/zgCMSF+1mNS/EQEiEaUPoFXCVsHG64SIxcaaI2zJgRiv+YmgaQESUfWAdbjzgg==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1" + "@babel/helper-plugin-utils": "^7.24.7" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">=6.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@radix-ui/react-primitive": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", - "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.7.tgz", + "integrity": "sha512-iLD3UNkgx2n/HrjBesVbYX6j0yqn/sJktvbtKKgcaLIQ4bTTQ8obAypc1VpyHPD2y4Phh9zHOaAt8e/L14wCpw==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-slot": "1.0.2" + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-typescript": "^7.24.7" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">=6.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@radix-ui/react-progress": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.0.3.tgz", - "integrity": "sha512-5G6Om/tYSxjSeEdrb1VfKkfZfn/1IlPWd731h2RfPuSbIfNUgfqAwbKfJCg/PP6nuUCTrYzalwHSpSinoWoCag==", + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz", + "integrity": "sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-primitive": "1.0.3" + "@babel/helper-plugin-utils": "^7.24.7" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">=6.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@radix-ui/react-radio-group": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.1.3.tgz", - "integrity": "sha512-x+yELayyefNeKeTx4fjK6j99Fs6c4qKm3aY38G3swQVTN6xMpsrbigC0uHs2L//g8q4qR7qOcww8430jJmi2ag==", + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.7.tgz", + "integrity": "sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-roving-focus": "1.0.4", - "@radix-ui/react-use-controllable-state": "1.0.1", - "@radix-ui/react-use-previous": "1.0.1", - "@radix-ui/react-use-size": "1.0.1" + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">=6.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@radix-ui/react-roving-focus": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz", - "integrity": "sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==", + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz", + "integrity": "sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-collection": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-controllable-state": "1.0.1" + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">=6.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@radix-ui/react-scroll-area": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.0.5.tgz", - "integrity": "sha512-b6PAgH4GQf9QEn8zbT2XUHpW5z8BzqEc7Kl11TwDrvuTrxlkcjTD5qa/bxgKr+nmuXKu4L/W5UZ4mlP/VG/5Gw==", + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.7.tgz", + "integrity": "sha512-2G8aAvF4wy1w/AGZkemprdGMRg5o6zPNhbHVImRz3lss55TYCBd6xStN19rt8XJHq20sqV0JbyWjOWwQRwV/wg==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/number": "1.0.1", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1" + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">=6.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@radix-ui/react-select": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-1.2.2.tgz", - "integrity": "sha512-zI7McXr8fNaSrUY9mZe4x/HC0jTLY9fWNhO1oLWYMQGDXuV4UCivIGTxwioSzO0ZCYX9iSLyWmAh/1TOmX3Cnw==", + "node_modules/@babel/preset-env": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.7.tgz", + "integrity": "sha512-1YZNsc+y6cTvWlDHidMBsQZrZfEFjRIo/BZCT906PMdzOyXtSLTgqGdrpcuTDCXyd11Am5uQULtDIcCfnTc8fQ==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/number": "1.0.1", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-collection": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.4", - "@radix-ui/react-focus-guards": "1.0.1", - "@radix-ui/react-focus-scope": "1.0.3", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-popper": "1.1.2", - "@radix-ui/react-portal": "1.0.3", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-controllable-state": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1", - "@radix-ui/react-use-previous": "1.0.1", - "@radix-ui/react-visually-hidden": "1.0.3", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.5" + "@babel/compat-data": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.24.7", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.24.7", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.24.7", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.24.7", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.24.7", + "@babel/plugin-transform-async-generator-functions": "^7.24.7", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoped-functions": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.24.7", + "@babel/plugin-transform-class-properties": "^7.24.7", + "@babel/plugin-transform-class-static-block": "^7.24.7", + "@babel/plugin-transform-classes": "^7.24.7", + "@babel/plugin-transform-computed-properties": "^7.24.7", + "@babel/plugin-transform-destructuring": "^7.24.7", + "@babel/plugin-transform-dotall-regex": "^7.24.7", + "@babel/plugin-transform-duplicate-keys": "^7.24.7", + "@babel/plugin-transform-dynamic-import": "^7.24.7", + "@babel/plugin-transform-exponentiation-operator": "^7.24.7", + "@babel/plugin-transform-export-namespace-from": "^7.24.7", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-function-name": "^7.24.7", + "@babel/plugin-transform-json-strings": "^7.24.7", + "@babel/plugin-transform-literals": "^7.24.7", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", + "@babel/plugin-transform-member-expression-literals": "^7.24.7", + "@babel/plugin-transform-modules-amd": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.7", + "@babel/plugin-transform-modules-systemjs": "^7.24.7", + "@babel/plugin-transform-modules-umd": "^7.24.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-new-target": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-numeric-separator": "^7.24.7", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-object-super": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.7", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-property-literals": "^7.24.7", + "@babel/plugin-transform-regenerator": "^7.24.7", + "@babel/plugin-transform-reserved-words": "^7.24.7", + "@babel/plugin-transform-shorthand-properties": "^7.24.7", + "@babel/plugin-transform-spread": "^7.24.7", + "@babel/plugin-transform-sticky-regex": "^7.24.7", + "@babel/plugin-transform-template-literals": "^7.24.7", + "@babel/plugin-transform-typeof-symbol": "^7.24.7", + "@babel/plugin-transform-unicode-escapes": "^7.24.7", + "@babel/plugin-transform-unicode-property-regex": "^7.24.7", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.24.7", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.4", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.31.0", + "semver": "^6.3.1" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">=6.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.4.tgz", - "integrity": "sha512-7UpBa/RKMoHJYjie1gkF1DlK8l1fdU/VKDpoS3rCCo8YBJR294GwcEHyxHw72yvphJ7ld0AXEcSLAzY2F/WyCg==", + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-escape-keydown": "1.0.3" + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" }, "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-scope": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.3.tgz", - "integrity": "sha512-upXdPfqI4islj2CslyfUBNlaJCPybbqRHAi1KER7Isel9Q2AtSJ0zRBZv8mWQiFXD2nyAJ4BhC3yXgZ6kMBSrQ==", + "node_modules/@babel/preset-react": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.24.7.tgz", + "integrity": "sha512-AAH4lEkpmzFWrGVlHaxJB7RLH21uPQ9+He+eFLWHmF9IuFQVugz8eAsamaW0DXRrTfco5zj1wWtpdcXJUOfsag==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "@babel/plugin-transform-react-display-name": "^7.24.7", + "@babel/plugin-transform-react-jsx": "^7.24.7", + "@babel/plugin-transform-react-jsx-development": "^7.24.7", + "@babel/plugin-transform-react-pure-annotations": "^7.24.7" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">=6.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-popper": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.2.tgz", - "integrity": "sha512-1CnGGfFi/bbqtJZZ0P/NQY20xdG3E0LALJaLUEoKwPLwl6PPPfbeiCqMVQnhoFRAxjJj4RpBRJzDmUgsex2tSg==", + "node_modules/@babel/preset-typescript": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.24.7.tgz", + "integrity": "sha512-SyXRe3OdWwIwalxDg5UtJnJQO+YPcTfwiIY2B0Xlddh9o7jpWLvv8X1RthIeDOxQ+O1ML5BLPCONToObyVQVuQ==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1", - "@radix-ui/react-use-rect": "1.0.1", - "@radix-ui/react-use-size": "1.0.1", - "@radix-ui/rect": "1.0.1" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "@babel/plugin-syntax-jsx": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.7", + "@babel/plugin-transform-typescript": "^7.24.7" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">=6.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-portal": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.3.tgz", - "integrity": "sha512-xLYZeHrWoPmA5mEKEfZZevoVRK/Q43GfzRXkWV6qawIWWK8t6ifIiLQdd7rmQ4Vk1bmI21XhqF9BN3jWf+phpA==", + "node_modules/@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "dev": true + }, + "node_modules/@babel/runtime": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", + "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "regenerator-runtime": "^0.14.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@radix-ui/react-slider": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.1.2.tgz", - "integrity": "sha512-NKs15MJylfzVsCagVSWKhGGLNR1W9qWs+HtgbmjjVUB3B9+lb3PYoXxVju3kOrpf0VKyVCtZp+iTwVoqpa1Chw==", + "node_modules/@babel/template": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/number": "1.0.1", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-collection": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-controllable-state": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1", - "@radix-ui/react-use-previous": "1.0.1", - "@radix-ui/react-use-size": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@radix-ui/react-slot": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", - "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "node_modules/@babel/traverse": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", + "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1" + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7", + "debug": "^4.3.1", + "globals": "^11.1.0" }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", + "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", + "dependencies": { + "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@radix-ui/react-switch": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.0.3.tgz", - "integrity": "sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==", + "node_modules/@buerokratt-ria/header": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@buerokratt-ria/header/-/header-0.1.6.tgz", + "integrity": "sha512-sPynHp0LQvBdjqNma6KmAGHzfP3qnpFl4WdZDpHBRJS/f09EPlEvSfV6N6D693i9M7oXD9WhykIvthcugoeCbQ==", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-controllable-state": "1.0.1", - "@radix-ui/react-use-previous": "1.0.1", - "@radix-ui/react-use-size": "1.0.1" + "@buerokratt-ria/styles": "^0.0.1", + "@types/react": "^18.2.21", + "react": "^18.2.0" }, "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "@fontsource/roboto": "^4.5.8", + "@formkit/auto-animate": "^0.7.0", + "@radix-ui/react-accessible-icon": "^1.0.3", + "@radix-ui/react-dialog": "^1.0.4", + "@radix-ui/react-switch": "^1.0.3", + "@radix-ui/react-toast": "^1.1.4", + "@tanstack/react-query": "^4.32.1", + "axios": "^1.4.0", + "clsx": "^1.2.1", + "i18next": "^23.2.3", + "i18next-browser-languagedetector": "^7.1.0", + "path": "^0.12.7", + "react": "^18.2.0", + "react-cookie": "^4.1.1", + "react-dom": "^18.2.0", + "react-hook-form": "^7.45.4", + "react-i18next": "^12.1.1", + "react-icons": "^4.10.1", + "react-idle-timer": "^5.7.2", + "react-router-dom": "^6.14.2", + "rxjs": "^7.8.1", + "tslib": "^2.3.0", + "vite-plugin-dts": "^3.5.2", + "vite-plugin-svgr": "^3.2.0", + "zustand": "^4.4.0" } }, - "node_modules/@radix-ui/react-tabs": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.0.4.tgz", - "integrity": "sha512-egZfYY/+wRNCflXNHx+dePvnz9FbmssDTJBtgRfDY7e8SE5oIo3Py2eCB1ckAbh1Q7cQ/6yJZThJ++sgbxibog==", + "node_modules/@buerokratt-ria/menu": { + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@buerokratt-ria/menu/-/menu-0.1.16.tgz", + "integrity": "sha512-k6G9I1Y7y98E7Re8QI+vm5EjYEVpGwTP9n8aStt/ScdiWmwrw12hFd/yRLj3kO8phMvWUPUfBPuE3UajlfoesA==", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-roving-focus": "1.0.4", - "@radix-ui/react-use-controllable-state": "1.0.1" + "@buerokratt-ria/styles": "^0.0.1", + "@types/react": "^18.2.21", + "react": "^18.2.0" }, "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "@radix-ui/react-accessible-icon": "^1.0.3", + "@radix-ui/react-dialog": "^1.0.4", + "@radix-ui/react-switch": "^1.0.3", + "@radix-ui/react-toast": "^1.1.4", + "@tanstack/react-query": "^4.32.1", + "clsx": "^1.2.1", + "i18next": "^23.2.3", + "i18next-browser-languagedetector": "^7.1.0", + "path": "^0.12.7", + "react": "^18.2.0", + "react-cookie": "^4.1.1", + "react-dom": "^18.2.0", + "react-hook-form": "^7.45.4", + "react-i18next": "^12.1.1", + "react-icons": "^4.10.1", + "react-idle-timer": "^5.7.2", + "react-router-dom": "^6.14.2", + "rxjs": "^7.8.1", + "tslib": "^2.3.0", + "vite-plugin-dts": "^3.5.2", + "vite-plugin-svgr": "^3.2.0", + "zustand": "^4.4.0" + } + }, + "node_modules/@buerokratt-ria/styles": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@buerokratt-ria/styles/-/styles-0.0.1.tgz", + "integrity": "sha512-bSj7WsdQO4P/43mRgsa5sDEwBuOebXcl3+Peur8NwToqczqsTMbXSO5P6xyXHoTnHWt082PhT8ht7OAgtFSzfw==" + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", + "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/serialize": "^1.1.2", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + }, + "node_modules/@emotion/babel-plugin/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==", + "engines": { + "node": ">=10" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@radix-ui/react-toast": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.1.5.tgz", - "integrity": "sha512-fRLn227WHIBRSzuRzGJ8W+5YALxofH23y0MlPLddaIpLpCDqdE0NZlS2NRQDRiptfxDeeCjgFIpexB1/zkxDlw==", + "node_modules/@emotion/cache": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", + "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-collection": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.5", - "@radix-ui/react-portal": "1.0.4", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-controllable-state": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1", - "@radix-ui/react-visually-hidden": "1.0.3" + "@emotion/memoize": "^0.8.1", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache/node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + }, + "node_modules/@emotion/hash": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", + "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", + "optional": true, + "dependencies": { + "@emotion/memoize": "0.7.4" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", + "optional": true + }, + "node_modules/@emotion/react": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.4.tgz", + "integrity": "sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/cache": "^11.11.0", + "@emotion/serialize": "^1.1.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "hoist-non-react-statics": "^3.3.1" }, "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "react": ">=16.8.0" }, "peerDependenciesMeta": { "@types/react": { "optional": true - }, - "@types/react-dom": { - "optional": true } } }, - "node_modules/@radix-ui/react-toggle": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.0.3.tgz", - "integrity": "sha512-Pkqg3+Bc98ftZGsl60CLANXQBBQ4W3mTFS9EJvNxKMZ7magklKV69/id1mlAlOFDDfHvlCms0fx8fA4CMKDJHg==", + "node_modules/@emotion/serialize": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.4.tgz", + "integrity": "sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ==", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-controllable-state": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/unitless": "^0.8.1", + "@emotion/utils": "^1.2.1", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/serialize/node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + }, + "node_modules/@emotion/sheet": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", + "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" + }, + "node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", + "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", + "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.1.tgz", + "integrity": "sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.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": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.2.tgz", + "integrity": "sha512-+2XpQV9LLZeanU4ZevzRnGFg2neDeKHgFLjP6YLW+tly0IvrhqT4u8enLGjLH3qeh85g19xY5rsAusfwTdn5lg==", + "dependencies": { + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.5.tgz", + "integrity": "sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw==", + "dependencies": { + "@floating-ui/core": "^1.0.0", + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.0.tgz", + "integrity": "sha512-lNzj5EQmEKn5FFKc04+zasr09h/uX8RtJRNj5gUXsSQIXHVWTVh+hVAg1vOMCexkX8EgvemMvIFpQfkosnVNyA==", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.2.tgz", + "integrity": "sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==" + }, + "node_modules/@fontsource/roboto": { + "version": "4.5.8", + "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-4.5.8.tgz", + "integrity": "sha512-CnD7zLItIzt86q4Sj3kZUiLcBk1dSk81qcqgMGaZe7SQ1P8hFNxhMl5AZthK1zrDM5m74VVhaOpuMGIL4gagaA==" + }, + "node_modules/@formkit/auto-animate": { + "version": "1.0.0-pre-alpha.3", + "resolved": "https://registry.npmjs.org/@formkit/auto-animate/-/auto-animate-1.0.0-pre-alpha.3.tgz", + "integrity": "sha512-lMVZ3LFUIu0RIxCEwmV8nUUJQ46M2bv2NDU3hrhZivViuR1EheC8Mj5sx/ACqK5QLK8XB8z7GDIZBUGdU/9OZQ==", + "peerDependencies": { + "react": "^16.8.0", + "vue": "^3.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/@fortaine/fetch-event-source": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@fortaine/fetch-event-source/-/fetch-event-source-3.0.6.tgz", + "integrity": "sha512-621GAuLMvKtyZQ3IA6nlDWhV1V/7PGOTNIGLUifxt0KzM+dZIweJ6F3XvQF3QnqeNfS1N7WQ0Kil1Di/lhChEw==", + "engines": { + "node": ">=16.15" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "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, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true + }, + "node_modules/@icons/material": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz", + "integrity": "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==", + "peerDependencies": { + "react": "*" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "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==", + "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==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "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==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lezer/common": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.1.tgz", + "integrity": "sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==" + }, + "node_modules/@lezer/lr": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.1.tgz", + "integrity": "sha512-CHsKq8DMKBf9b3yXPDIU4DbH+ZJd/sJdYOW2llbW/HudP5u0VS6Bfq1hLYfgU7uAYGFIyGGQIsSOXGPEErZiJw==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lmdb/lmdb-darwin-arm64": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-2.8.5.tgz", + "integrity": "sha512-KPDeVScZgA1oq0CiPBcOa3kHIqU+pTOwRFDIhxvmf8CTNvqdZQYp5cCKW0bUk69VygB2PuTiINFWbY78aR2pQw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-darwin-x64": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-2.8.5.tgz", + "integrity": "sha512-w/sLhN4T7MW1nB3R/U8WK5BgQLz904wh+/SmA2jD8NnF7BLLoUgflCNxOeSPOWp8geP6nP/+VjWzZVip7rZ1ug==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-2.8.5.tgz", + "integrity": "sha512-c0TGMbm2M55pwTDIfkDLB6BpIsgxV4PjYck2HiOX+cy/JWiBXz32lYbarPqejKs9Flm7YVAKSILUducU9g2RVg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm64": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-2.8.5.tgz", + "integrity": "sha512-vtbZRHH5UDlL01TT5jB576Zox3+hdyogvpcbvVJlmU5PdL3c5V7cj1EODdh1CHPksRl+cws/58ugEHi8bcj4Ww==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-x64": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-2.8.5.tgz", + "integrity": "sha512-Xkc8IUx9aEhP0zvgeKy7IQ3ReX2N8N1L0WPcQwnZweWmOuKfwpS3GRIYqLtK5za/w3E60zhFfNdS+3pBZPytqQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-win32-x64": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-2.8.5.tgz", + "integrity": "sha512-4wvrf5BgnR8RpogHhtpCPJMKBmvyZPhhUtEwMJbXh0ni2BucpfF07jlmyM11zRqQ2XIq6PbC2j7W7UCCcm1rRQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@mischnic/json-sourcemap": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@mischnic/json-sourcemap/-/json-sourcemap-0.1.1.tgz", + "integrity": "sha512-iA7+tyVqfrATAIsIRWQG+a7ZLLD0VaOCKV2Wd/v4mqIU3J9c4jx9p7S0nw1XH3gJCKNBOOwACOPYYSUu9pgT+w==", + "dependencies": { + "@lezer/common": "^1.0.0", + "@lezer/lr": "^1.0.0", + "json5": "^2.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@motionone/animation": { + "version": "10.18.0", + "resolved": "https://registry.npmjs.org/@motionone/animation/-/animation-10.18.0.tgz", + "integrity": "sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw==", + "dependencies": { + "@motionone/easing": "^10.18.0", + "@motionone/types": "^10.17.1", + "@motionone/utils": "^10.18.0", + "tslib": "^2.3.1" + } + }, + "node_modules/@motionone/dom": { + "version": "10.18.0", + "resolved": "https://registry.npmjs.org/@motionone/dom/-/dom-10.18.0.tgz", + "integrity": "sha512-bKLP7E0eyO4B2UaHBBN55tnppwRnaE3KFfh3Ps9HhnAkar3Cb69kUCJY9as8LrccVYKgHA+JY5dOQqJLOPhF5A==", + "dependencies": { + "@motionone/animation": "^10.18.0", + "@motionone/generators": "^10.18.0", + "@motionone/types": "^10.17.1", + "@motionone/utils": "^10.18.0", + "hey-listen": "^1.0.8", + "tslib": "^2.3.1" + } + }, + "node_modules/@motionone/easing": { + "version": "10.18.0", + "resolved": "https://registry.npmjs.org/@motionone/easing/-/easing-10.18.0.tgz", + "integrity": "sha512-VcjByo7XpdLS4o9T8t99JtgxkdMcNWD3yHU/n6CLEz3bkmKDRZyYQ/wmSf6daum8ZXqfUAgFeCZSpJZIMxaCzg==", + "dependencies": { + "@motionone/utils": "^10.18.0", + "tslib": "^2.3.1" + } + }, + "node_modules/@motionone/generators": { + "version": "10.18.0", + "resolved": "https://registry.npmjs.org/@motionone/generators/-/generators-10.18.0.tgz", + "integrity": "sha512-+qfkC2DtkDj4tHPu+AFKVfR/C30O1vYdvsGYaR13W/1cczPrrcjdvYCj0VLFuRMN+lP1xvpNZHCRNM4fBzn1jg==", + "dependencies": { + "@motionone/types": "^10.17.1", + "@motionone/utils": "^10.18.0", + "tslib": "^2.3.1" + } + }, + "node_modules/@motionone/types": { + "version": "10.17.1", + "resolved": "https://registry.npmjs.org/@motionone/types/-/types-10.17.1.tgz", + "integrity": "sha512-KaC4kgiODDz8hswCrS0btrVrzyU2CSQKO7Ps90ibBVSQmjkrt2teqta6/sOG59v7+dPnKMAg13jyqtMKV2yJ7A==" + }, + "node_modules/@motionone/utils": { + "version": "10.18.0", + "resolved": "https://registry.npmjs.org/@motionone/utils/-/utils-10.18.0.tgz", + "integrity": "sha512-3XVF7sgyTSI2KWvTf6uLlBJ5iAgRgmvp3bpuOiQJvInd4nZ19ET8lX5unn30SlmRH7hXbBbH+Gxd0m0klJ3Xtw==", + "dependencies": { + "@motionone/types": "^10.17.1", + "hey-listen": "^1.0.8", + "tslib": "^2.3.1" + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@mswjs/cookies": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@mswjs/cookies/-/cookies-0.2.2.tgz", + "integrity": "sha512-mlN83YSrcFgk7Dm1Mys40DLssI1KdJji2CMKN8eOlBqsTADYzj2+jWzsANsUTFbxDMWPD5e9bfA1RGqBpS3O1g==", + "dev": true, + "dependencies": { + "@types/set-cookie-parser": "^2.4.0", + "set-cookie-parser": "^2.4.6" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@mswjs/interceptors": { + "version": "0.17.10", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.17.10.tgz", + "integrity": "sha512-N8x7eSLGcmUFNWZRxT1vsHvypzIRgQYdG0rJey/rZCy6zT/30qDt8Joj7FxzGNLSwXbeZqJOMqDurp7ra4hgbw==", + "dev": true, + "dependencies": { + "@open-draft/until": "^1.0.3", + "@types/debug": "^4.1.7", + "@xmldom/xmldom": "^0.8.3", + "debug": "^4.3.3", + "headers-polyfill": "3.2.5", + "outvariant": "^1.2.1", + "strict-event-emitter": "^0.2.4", + "web-encoding": "^1.1.5" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@mswjs/interceptors/node_modules/headers-polyfill": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-3.2.5.tgz", + "integrity": "sha512-tUCGvt191vNSQgttSyJoibR+VO+I6+iCHIUdhzEMJKE+EAL8BwCN7fUOZlY4ofOelNHsK+gEjxB/B+9N3EWtdA==", + "dev": true + }, + "node_modules/@mswjs/interceptors/node_modules/strict-event-emitter": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.2.8.tgz", + "integrity": "sha512-KDf/ujU8Zud3YaLtMCcTI4xkZlZVIYxTLr+XIULexP+77EEVWixeXroLUXQXiVtH4XH2W7jr/3PT1v3zBuvc3A==", + "dev": true, + "dependencies": { + "events": "^3.3.0" + } + }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { + "version": "5.1.1-v1", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", + "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", + "dev": true, + "dependencies": { + "eslint-scope": "5.1.1" + } + }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "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, + "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, + "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, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@open-draft/until": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-1.0.3.tgz", + "integrity": "sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q==", + "dev": true + }, + "node_modules/@parcel/bundler-default": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/bundler-default/-/bundler-default-2.12.0.tgz", + "integrity": "sha512-3ybN74oYNMKyjD6V20c9Gerdbh7teeNvVMwIoHIQMzuIFT6IGX53PyOLlOKRLbjxMc0TMimQQxIt2eQqxR5LsA==", + "dependencies": { + "@parcel/diagnostic": "2.12.0", + "@parcel/graph": "3.2.0", + "@parcel/plugin": "2.12.0", + "@parcel/rust": "2.12.0", + "@parcel/utils": "2.12.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/cache": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/cache/-/cache-2.12.0.tgz", + "integrity": "sha512-FX5ZpTEkxvq/yvWklRHDESVRz+c7sLTXgFuzz6uEnBcXV38j6dMSikflNpHA6q/L4GKkCqRywm9R6XQwhwIMyw==", + "dependencies": { + "@parcel/fs": "2.12.0", + "@parcel/logger": "2.12.0", + "@parcel/utils": "2.12.0", + "lmdb": "2.8.5" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.12.0" + } + }, + "node_modules/@parcel/codeframe": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/codeframe/-/codeframe-2.12.0.tgz", + "integrity": "sha512-v2VmneILFiHZJTxPiR7GEF1wey1/IXPdZMcUlNXBiPZyWDfcuNgGGVQkx/xW561rULLIvDPharOMdxz5oHOKQg==", + "dependencies": { + "chalk": "^4.1.0" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/codeframe/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==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@parcel/codeframe/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "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/@parcel/codeframe/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==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@parcel/codeframe/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==" + }, + "node_modules/@parcel/codeframe/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==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@parcel/codeframe/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==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@parcel/compressor-raw": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/compressor-raw/-/compressor-raw-2.12.0.tgz", + "integrity": "sha512-h41Q3X7ZAQ9wbQ2csP8QGrwepasLZdXiuEdpUryDce6rF9ZiHoJ97MRpdLxOhOPyASTw/xDgE1xyaPQr0Q3f5A==", + "dependencies": { + "@parcel/plugin": "2.12.0" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/config-default": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/config-default/-/config-default-2.12.0.tgz", + "integrity": "sha512-dPNe2n9eEsKRc1soWIY0yToMUPirPIa2QhxcCB3Z5RjpDGIXm0pds+BaiqY6uGLEEzsjhRO0ujd4v2Rmm0vuFg==", + "dependencies": { + "@parcel/bundler-default": "2.12.0", + "@parcel/compressor-raw": "2.12.0", + "@parcel/namer-default": "2.12.0", + "@parcel/optimizer-css": "2.12.0", + "@parcel/optimizer-htmlnano": "2.12.0", + "@parcel/optimizer-image": "2.12.0", + "@parcel/optimizer-svgo": "2.12.0", + "@parcel/optimizer-swc": "2.12.0", + "@parcel/packager-css": "2.12.0", + "@parcel/packager-html": "2.12.0", + "@parcel/packager-js": "2.12.0", + "@parcel/packager-raw": "2.12.0", + "@parcel/packager-svg": "2.12.0", + "@parcel/packager-wasm": "2.12.0", + "@parcel/reporter-dev-server": "2.12.0", + "@parcel/resolver-default": "2.12.0", + "@parcel/runtime-browser-hmr": "2.12.0", + "@parcel/runtime-js": "2.12.0", + "@parcel/runtime-react-refresh": "2.12.0", + "@parcel/runtime-service-worker": "2.12.0", + "@parcel/transformer-babel": "2.12.0", + "@parcel/transformer-css": "2.12.0", + "@parcel/transformer-html": "2.12.0", + "@parcel/transformer-image": "2.12.0", + "@parcel/transformer-js": "2.12.0", + "@parcel/transformer-json": "2.12.0", + "@parcel/transformer-postcss": "2.12.0", + "@parcel/transformer-posthtml": "2.12.0", + "@parcel/transformer-raw": "2.12.0", + "@parcel/transformer-react-refresh-wrap": "2.12.0", + "@parcel/transformer-svg": "2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.12.0" + } + }, + "node_modules/@parcel/core": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/core/-/core-2.12.0.tgz", + "integrity": "sha512-s+6pwEj+GfKf7vqGUzN9iSEPueUssCCQrCBUlcAfKrJe0a22hTUCjewpB0I7lNrCIULt8dkndD+sMdOrXsRl6Q==", + "dependencies": { + "@mischnic/json-sourcemap": "^0.1.0", + "@parcel/cache": "2.12.0", + "@parcel/diagnostic": "2.12.0", + "@parcel/events": "2.12.0", + "@parcel/fs": "2.12.0", + "@parcel/graph": "3.2.0", + "@parcel/logger": "2.12.0", + "@parcel/package-manager": "2.12.0", + "@parcel/plugin": "2.12.0", + "@parcel/profiler": "2.12.0", + "@parcel/rust": "2.12.0", + "@parcel/source-map": "^2.1.1", + "@parcel/types": "2.12.0", + "@parcel/utils": "2.12.0", + "@parcel/workers": "2.12.0", + "abortcontroller-polyfill": "^1.1.9", + "base-x": "^3.0.8", + "browserslist": "^4.6.6", + "clone": "^2.1.1", + "dotenv": "^7.0.0", + "dotenv-expand": "^5.1.0", + "json5": "^2.2.0", + "msgpackr": "^1.9.9", + "nullthrows": "^1.1.1", + "semver": "^7.5.2" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/core/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@parcel/diagnostic": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/diagnostic/-/diagnostic-2.12.0.tgz", + "integrity": "sha512-8f1NOsSFK+F4AwFCKynyIu9Kr/uWHC+SywAv4oS6Bv3Acig0gtwUjugk0C9UaB8ztBZiW5TQZhw+uPZn9T/lJA==", + "dependencies": { + "@mischnic/json-sourcemap": "^0.1.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/events": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/events/-/events-2.12.0.tgz", + "integrity": "sha512-nmAAEIKLjW1kB2cUbCYSmZOGbnGj8wCzhqnK727zCCWaA25ogzAtt657GPOeFyqW77KyosU728Tl63Fc8hphIA==", + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/fs": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/fs/-/fs-2.12.0.tgz", + "integrity": "sha512-NnFkuvou1YBtPOhTdZr44WN7I60cGyly2wpHzqRl62yhObyi1KvW0SjwOMa0QGNcBOIzp4G0CapoZ93hD0RG5Q==", + "dependencies": { + "@parcel/rust": "2.12.0", + "@parcel/types": "2.12.0", + "@parcel/utils": "2.12.0", + "@parcel/watcher": "^2.0.7", + "@parcel/workers": "2.12.0" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.12.0" + } + }, + "node_modules/@parcel/graph": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@parcel/graph/-/graph-3.2.0.tgz", + "integrity": "sha512-xlrmCPqy58D4Fg5umV7bpwDx5Vyt7MlnQPxW68vae5+BA4GSWetfZt+Cs5dtotMG2oCHzZxhIPt7YZ7NRyQzLA==", + "dependencies": { + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/logger": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/logger/-/logger-2.12.0.tgz", + "integrity": "sha512-cJ7Paqa7/9VJ7C+KwgJlwMqTQBOjjn71FbKk0G07hydUEBISU2aDfmc/52o60ErL9l+vXB26zTrIBanbxS8rVg==", + "dependencies": { + "@parcel/diagnostic": "2.12.0", + "@parcel/events": "2.12.0" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/markdown-ansi": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/markdown-ansi/-/markdown-ansi-2.12.0.tgz", + "integrity": "sha512-WZz3rzL8k0H3WR4qTHX6Ic8DlEs17keO9gtD4MNGyMNQbqQEvQ61lWJaIH0nAtgEetu0SOITiVqdZrb8zx/M7w==", + "dependencies": { + "chalk": "^4.1.0" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/markdown-ansi/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==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@parcel/markdown-ansi/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "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/@parcel/markdown-ansi/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==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@parcel/markdown-ansi/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==" + }, + "node_modules/@parcel/markdown-ansi/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==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@parcel/markdown-ansi/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==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@parcel/namer-default": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/namer-default/-/namer-default-2.12.0.tgz", + "integrity": "sha512-9DNKPDHWgMnMtqqZIMiEj/R9PNWW16lpnlHjwK3ciRlMPgjPJ8+UNc255teZODhX0T17GOzPdGbU/O/xbxVPzA==", + "dependencies": { + "@parcel/diagnostic": "2.12.0", + "@parcel/plugin": "2.12.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/node-resolver-core": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@parcel/node-resolver-core/-/node-resolver-core-3.3.0.tgz", + "integrity": "sha512-rhPW9DYPEIqQBSlYzz3S0AjXxjN6Ub2yS6tzzsW/4S3Gpsgk/uEq4ZfxPvoPf/6TgZndVxmKwpmxaKtGMmf3cA==", + "dependencies": { + "@mischnic/json-sourcemap": "^0.1.0", + "@parcel/diagnostic": "2.12.0", + "@parcel/fs": "2.12.0", + "@parcel/rust": "2.12.0", + "@parcel/utils": "2.12.0", + "nullthrows": "^1.1.1", + "semver": "^7.5.2" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/node-resolver-core/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@parcel/optimizer-css": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/optimizer-css/-/optimizer-css-2.12.0.tgz", + "integrity": "sha512-ifbcC97fRzpruTjaa8axIFeX4MjjSIlQfem3EJug3L2AVqQUXnM1XO8L0NaXGNLTW2qnh1ZjIJ7vXT/QhsphsA==", + "dependencies": { + "@parcel/diagnostic": "2.12.0", + "@parcel/plugin": "2.12.0", + "@parcel/source-map": "^2.1.1", + "@parcel/utils": "2.12.0", + "browserslist": "^4.6.6", + "lightningcss": "^1.22.1", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/optimizer-htmlnano": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/optimizer-htmlnano/-/optimizer-htmlnano-2.12.0.tgz", + "integrity": "sha512-MfPMeCrT8FYiOrpFHVR+NcZQlXAptK2r4nGJjfT+ndPBhEEZp4yyL7n1y7HfX9geg5altc4WTb4Gug7rCoW8VQ==", + "dependencies": { + "@parcel/plugin": "2.12.0", + "htmlnano": "^2.0.0", + "nullthrows": "^1.1.1", + "posthtml": "^0.16.5", + "svgo": "^2.4.0" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/optimizer-image": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/optimizer-image/-/optimizer-image-2.12.0.tgz", + "integrity": "sha512-bo1O7raeAIbRU5nmNVtx8divLW9Xqn0c57GVNGeAK4mygnQoqHqRZ0mR9uboh64pxv6ijXZHPhKvU9HEpjPjBQ==", + "dependencies": { + "@parcel/diagnostic": "2.12.0", + "@parcel/plugin": "2.12.0", + "@parcel/rust": "2.12.0", + "@parcel/utils": "2.12.0", + "@parcel/workers": "2.12.0" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.12.0" + } + }, + "node_modules/@parcel/optimizer-svgo": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/optimizer-svgo/-/optimizer-svgo-2.12.0.tgz", + "integrity": "sha512-Kyli+ZZXnoonnbeRQdoWwee9Bk2jm/49xvnfb+2OO8NN0d41lblBoRhOyFiScRnJrw7eVl1Xrz7NTkXCIO7XFQ==", + "dependencies": { + "@parcel/diagnostic": "2.12.0", + "@parcel/plugin": "2.12.0", + "@parcel/utils": "2.12.0", + "svgo": "^2.4.0" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/optimizer-swc": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/optimizer-swc/-/optimizer-swc-2.12.0.tgz", + "integrity": "sha512-iBi6LZB3lm6WmbXfzi8J3DCVPmn4FN2lw7DGXxUXu7MouDPVWfTsM6U/5TkSHJRNRogZ2gqy5q9g34NPxHbJcw==", + "dependencies": { + "@parcel/diagnostic": "2.12.0", + "@parcel/plugin": "2.12.0", + "@parcel/source-map": "^2.1.1", + "@parcel/utils": "2.12.0", + "@swc/core": "^1.3.36", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/package-manager": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/package-manager/-/package-manager-2.12.0.tgz", + "integrity": "sha512-0nvAezcjPx9FT+hIL+LS1jb0aohwLZXct7jAh7i0MLMtehOi0z1Sau+QpgMlA9rfEZZ1LIeFdnZZwqSy7Ccspw==", + "dependencies": { + "@parcel/diagnostic": "2.12.0", + "@parcel/fs": "2.12.0", + "@parcel/logger": "2.12.0", + "@parcel/node-resolver-core": "3.3.0", + "@parcel/types": "2.12.0", + "@parcel/utils": "2.12.0", + "@parcel/workers": "2.12.0", + "@swc/core": "^1.3.36", + "semver": "^7.5.2" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.12.0" + } + }, + "node_modules/@parcel/package-manager/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@parcel/packager-css": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/packager-css/-/packager-css-2.12.0.tgz", + "integrity": "sha512-j3a/ODciaNKD19IYdWJT+TP+tnhhn5koBGBWWtrKSu0UxWpnezIGZetit3eE+Y9+NTePalMkvpIlit2eDhvfJA==", + "dependencies": { + "@parcel/diagnostic": "2.12.0", + "@parcel/plugin": "2.12.0", + "@parcel/source-map": "^2.1.1", + "@parcel/utils": "2.12.0", + "lightningcss": "^1.22.1", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/packager-html": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/packager-html/-/packager-html-2.12.0.tgz", + "integrity": "sha512-PpvGB9hFFe+19NXGz2ApvPrkA9GwEqaDAninT+3pJD57OVBaxB8U+HN4a5LICKxjUppPPqmrLb6YPbD65IX4RA==", + "dependencies": { + "@parcel/plugin": "2.12.0", + "@parcel/types": "2.12.0", + "@parcel/utils": "2.12.0", + "nullthrows": "^1.1.1", + "posthtml": "^0.16.5" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/packager-js": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/packager-js/-/packager-js-2.12.0.tgz", + "integrity": "sha512-viMF+FszITRRr8+2iJyk+4ruGiL27Y6AF7hQ3xbJfzqnmbOhGFtLTQwuwhOLqN/mWR2VKdgbLpZSarWaO3yAMg==", + "dependencies": { + "@parcel/diagnostic": "2.12.0", + "@parcel/plugin": "2.12.0", + "@parcel/rust": "2.12.0", + "@parcel/source-map": "^2.1.1", + "@parcel/types": "2.12.0", + "@parcel/utils": "2.12.0", + "globals": "^13.2.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/packager-js/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@parcel/packager-js/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@parcel/packager-raw": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/packager-raw/-/packager-raw-2.12.0.tgz", + "integrity": "sha512-tJZqFbHqP24aq1F+OojFbQIc09P/u8HAW5xfndCrFnXpW4wTgM3p03P0xfw3gnNq+TtxHJ8c3UFE5LnXNNKhYA==", + "dependencies": { + "@parcel/plugin": "2.12.0" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/packager-svg": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/packager-svg/-/packager-svg-2.12.0.tgz", + "integrity": "sha512-ldaGiacGb2lLqcXas97k8JiZRbAnNREmcvoY2W2dvW4loVuDT9B9fU777mbV6zODpcgcHWsLL3lYbJ5Lt3y9cg==", + "dependencies": { + "@parcel/plugin": "2.12.0", + "@parcel/types": "2.12.0", + "@parcel/utils": "2.12.0", + "posthtml": "^0.16.4" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/packager-wasm": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/packager-wasm/-/packager-wasm-2.12.0.tgz", + "integrity": "sha512-fYqZzIqO9fGYveeImzF8ll6KRo2LrOXfD+2Y5U3BiX/wp9wv17dz50QLDQm9hmTcKGWxK4yWqKQh+Evp/fae7A==", + "dependencies": { + "@parcel/plugin": "2.12.0" + }, + "engines": { + "node": ">=12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/plugin": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/plugin/-/plugin-2.12.0.tgz", + "integrity": "sha512-nc/uRA8DiMoe4neBbzV6kDndh/58a4wQuGKw5oEoIwBCHUvE2W8ZFSu7ollSXUGRzfacTt4NdY8TwS73ScWZ+g==", + "dependencies": { + "@parcel/types": "2.12.0" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/profiler": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/profiler/-/profiler-2.12.0.tgz", + "integrity": "sha512-q53fvl5LDcFYzMUtSusUBZSjQrKjMlLEBgKeQHFwkimwR1mgoseaDBDuNz0XvmzDzF1UelJ02TUKCGacU8W2qA==", + "dependencies": { + "@parcel/diagnostic": "2.12.0", + "@parcel/events": "2.12.0", + "chrome-trace-event": "^1.0.2" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/reporter-cli": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/reporter-cli/-/reporter-cli-2.12.0.tgz", + "integrity": "sha512-TqKsH4GVOLPSCanZ6tcTPj+rdVHERnt5y4bwTM82cajM21bCX1Ruwp8xOKU+03091oV2pv5ieB18pJyRF7IpIw==", + "dependencies": { + "@parcel/plugin": "2.12.0", + "@parcel/types": "2.12.0", + "@parcel/utils": "2.12.0", + "chalk": "^4.1.0", + "term-size": "^2.2.1" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/reporter-cli/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==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@parcel/reporter-cli/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "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/@parcel/reporter-cli/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==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@parcel/reporter-cli/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==" + }, + "node_modules/@parcel/reporter-cli/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==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@parcel/reporter-cli/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==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@parcel/reporter-dev-server": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/reporter-dev-server/-/reporter-dev-server-2.12.0.tgz", + "integrity": "sha512-tIcDqRvAPAttRlTV28dHcbWT5K2r/MBFks7nM4nrEDHWtnrCwimkDmZTc1kD8QOCCjGVwRHcQybpHvxfwol6GA==", + "dependencies": { + "@parcel/plugin": "2.12.0", + "@parcel/utils": "2.12.0" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/reporter-tracer": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/reporter-tracer/-/reporter-tracer-2.12.0.tgz", + "integrity": "sha512-g8rlu9GxB8Ut/F8WGx4zidIPQ4pcYFjU9bZO+fyRIPrSUFH2bKijCnbZcr4ntqzDGx74hwD6cCG4DBoleq2UlQ==", + "dependencies": { + "@parcel/plugin": "2.12.0", + "@parcel/utils": "2.12.0", + "chrome-trace-event": "^1.0.3", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/resolver-default": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/resolver-default/-/resolver-default-2.12.0.tgz", + "integrity": "sha512-uuhbajTax37TwCxu7V98JtRLiT6hzE4VYSu5B7Qkauy14/WFt2dz6GOUXPgVsED569/hkxebPx3KCMtZW6cHHA==", + "dependencies": { + "@parcel/node-resolver-core": "3.3.0", + "@parcel/plugin": "2.12.0" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/runtime-browser-hmr": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/runtime-browser-hmr/-/runtime-browser-hmr-2.12.0.tgz", + "integrity": "sha512-4ZLp2FWyD32r0GlTulO3+jxgsA3oO1P1b5oO2IWuWilfhcJH5LTiazpL5YdusUjtNn9PGN6QLAWfxmzRIfM+Ow==", + "dependencies": { + "@parcel/plugin": "2.12.0", + "@parcel/utils": "2.12.0" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/runtime-js": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/runtime-js/-/runtime-js-2.12.0.tgz", + "integrity": "sha512-sBerP32Z1crX5PfLNGDSXSdqzlllM++GVnVQVeM7DgMKS8JIFG3VLi28YkX+dYYGtPypm01JoIHCkvwiZEcQJg==", + "dependencies": { + "@parcel/diagnostic": "2.12.0", + "@parcel/plugin": "2.12.0", + "@parcel/utils": "2.12.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/runtime-react-refresh": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/runtime-react-refresh/-/runtime-react-refresh-2.12.0.tgz", + "integrity": "sha512-SCHkcczJIDFTFdLTzrHTkQ0aTrX3xH6jrA4UsCBL6ji61+w+ohy4jEEe9qCgJVXhnJfGLE43HNXek+0MStX+Mw==", + "dependencies": { + "@parcel/plugin": "2.12.0", + "@parcel/utils": "2.12.0", + "react-error-overlay": "6.0.9", + "react-refresh": "^0.9.0" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/runtime-react-refresh/node_modules/react-refresh": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.9.0.tgz", + "integrity": "sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@parcel/runtime-service-worker": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/runtime-service-worker/-/runtime-service-worker-2.12.0.tgz", + "integrity": "sha512-BXuMBsfiwpIEnssn+jqfC3jkgbS8oxeo3C7xhSQsuSv+AF2FwY3O3AO1c1RBskEW3XrBLNINOJujroNw80VTKA==", + "dependencies": { + "@parcel/plugin": "2.12.0", + "@parcel/utils": "2.12.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/rust": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/rust/-/rust-2.12.0.tgz", + "integrity": "sha512-005cldMdFZFDPOjbDVEXcINQ3wT4vrxvSavRWI3Az0e3E18exO/x/mW9f648KtXugOXMAqCEqhFHcXECL9nmMw==", + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/source-map": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@parcel/source-map/-/source-map-2.1.1.tgz", + "integrity": "sha512-Ejx1P/mj+kMjQb8/y5XxDUn4reGdr+WyKYloBljpppUy8gs42T+BNoEOuRYqDVdgPc6NxduzIDoJS9pOFfV5Ew==", + "dependencies": { + "detect-libc": "^1.0.3" + }, + "engines": { + "node": "^12.18.3 || >=14" + } + }, + "node_modules/@parcel/transformer-babel": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/transformer-babel/-/transformer-babel-2.12.0.tgz", + "integrity": "sha512-zQaBfOnf/l8rPxYGnsk/ufh/0EuqvmnxafjBIpKZ//j6rGylw5JCqXSb1QvvAqRYruKeccxGv7+HrxpqKU6V4A==", + "dependencies": { + "@parcel/diagnostic": "2.12.0", + "@parcel/plugin": "2.12.0", + "@parcel/source-map": "^2.1.1", + "@parcel/utils": "2.12.0", + "browserslist": "^4.6.6", + "json5": "^2.2.0", + "nullthrows": "^1.1.1", + "semver": "^7.5.2" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-babel/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@parcel/transformer-css": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/transformer-css/-/transformer-css-2.12.0.tgz", + "integrity": "sha512-vXhOqoAlQGATYyQ433Z1DXKmiKmzOAUmKysbYH3FD+LKEKLMEl/pA14goqp00TW+A/EjtSKKyeMyHlMIIUqj4Q==", + "dependencies": { + "@parcel/diagnostic": "2.12.0", + "@parcel/plugin": "2.12.0", + "@parcel/source-map": "^2.1.1", + "@parcel/utils": "2.12.0", + "browserslist": "^4.6.6", + "lightningcss": "^1.22.1", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-html": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/transformer-html/-/transformer-html-2.12.0.tgz", + "integrity": "sha512-5jW4dFFBlYBvIQk4nrH62rfA/G/KzVzEDa6S+Nne0xXhglLjkm64Ci9b/d4tKZfuGWUbpm2ASAq8skti/nfpXw==", + "dependencies": { + "@parcel/diagnostic": "2.12.0", + "@parcel/plugin": "2.12.0", + "@parcel/rust": "2.12.0", + "nullthrows": "^1.1.1", + "posthtml": "^0.16.5", + "posthtml-parser": "^0.10.1", + "posthtml-render": "^3.0.0", + "semver": "^7.5.2", + "srcset": "4" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-html/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@parcel/transformer-image": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/transformer-image/-/transformer-image-2.12.0.tgz", + "integrity": "sha512-8hXrGm2IRII49R7lZ0RpmNk27EhcsH+uNKsvxuMpXPuEnWgC/ha/IrjaI29xCng1uGur74bJF43NUSQhR4aTdw==", + "dependencies": { + "@parcel/plugin": "2.12.0", + "@parcel/utils": "2.12.0", + "@parcel/workers": "2.12.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "peerDependencies": { + "@parcel/core": "^2.12.0" + } + }, + "node_modules/@parcel/transformer-js": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/transformer-js/-/transformer-js-2.12.0.tgz", + "integrity": "sha512-OSZpOu+FGDbC/xivu24v092D9w6EGytB3vidwbdiJ2FaPgfV7rxS0WIUjH4I0OcvHAcitArRXL0a3+HrNTdQQw==", + "dependencies": { + "@parcel/diagnostic": "2.12.0", + "@parcel/plugin": "2.12.0", + "@parcel/rust": "2.12.0", + "@parcel/source-map": "^2.1.1", + "@parcel/utils": "2.12.0", + "@parcel/workers": "2.12.0", + "@swc/helpers": "^0.5.0", + "browserslist": "^4.6.6", + "nullthrows": "^1.1.1", + "regenerator-runtime": "^0.13.7", + "semver": "^7.5.2" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.12.0" + } + }, + "node_modules/@parcel/transformer-js/node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + }, + "node_modules/@parcel/transformer-js/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@parcel/transformer-json": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/transformer-json/-/transformer-json-2.12.0.tgz", + "integrity": "sha512-Utv64GLRCQILK5r0KFs4o7I41ixMPllwOLOhkdjJKvf1hZmN6WqfOmB1YLbWS/y5Zb/iB52DU2pWZm96vLFQZQ==", + "dependencies": { + "@parcel/plugin": "2.12.0", + "json5": "^2.2.0" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-postcss": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/transformer-postcss/-/transformer-postcss-2.12.0.tgz", + "integrity": "sha512-FZqn+oUtiLfPOn67EZxPpBkfdFiTnF4iwiXPqvst3XI8H+iC+yNgzmtJkunOOuylpYY6NOU5jT8d7saqWSDv2Q==", + "dependencies": { + "@parcel/diagnostic": "2.12.0", + "@parcel/plugin": "2.12.0", + "@parcel/rust": "2.12.0", + "@parcel/utils": "2.12.0", + "clone": "^2.1.1", + "nullthrows": "^1.1.1", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.2" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-postcss/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@parcel/transformer-posthtml": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/transformer-posthtml/-/transformer-posthtml-2.12.0.tgz", + "integrity": "sha512-z6Z7rav/pcaWdeD+2sDUcd0mmNZRUvtHaUGa50Y2mr+poxrKilpsnFMSiWBT+oOqPt7j71jzDvrdnAF4XkCljg==", + "dependencies": { + "@parcel/plugin": "2.12.0", + "@parcel/utils": "2.12.0", + "nullthrows": "^1.1.1", + "posthtml": "^0.16.5", + "posthtml-parser": "^0.10.1", + "posthtml-render": "^3.0.0", + "semver": "^7.5.2" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-posthtml/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@parcel/transformer-raw": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/transformer-raw/-/transformer-raw-2.12.0.tgz", + "integrity": "sha512-Ht1fQvXxix0NncdnmnXZsa6hra20RXYh1VqhBYZLsDfkvGGFnXIgO03Jqn4Z8MkKoa0tiNbDhpKIeTjyclbBxQ==", + "dependencies": { + "@parcel/plugin": "2.12.0" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-react-refresh-wrap": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/transformer-react-refresh-wrap/-/transformer-react-refresh-wrap-2.12.0.tgz", + "integrity": "sha512-GE8gmP2AZtkpBIV5vSCVhewgOFRhqwdM5Q9jNPOY5PKcM3/Ff0qCqDiTzzGLhk0/VMBrdjssrfZkVx6S/lHdJw==", + "dependencies": { + "@parcel/plugin": "2.12.0", + "@parcel/utils": "2.12.0", + "react-refresh": "^0.9.0" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-react-refresh-wrap/node_modules/react-refresh": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.9.0.tgz", + "integrity": "sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@parcel/transformer-svg": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/transformer-svg/-/transformer-svg-2.12.0.tgz", + "integrity": "sha512-cZJqGRJ4JNdYcb+vj94J7PdOuTnwyy45dM9xqbIMH+HSiiIkfrMsdEwYft0GTyFTdsnf+hdHn3tau7Qa5hhX+A==", + "dependencies": { + "@parcel/diagnostic": "2.12.0", + "@parcel/plugin": "2.12.0", + "@parcel/rust": "2.12.0", + "nullthrows": "^1.1.1", + "posthtml": "^0.16.5", + "posthtml-parser": "^0.10.1", + "posthtml-render": "^3.0.0", + "semver": "^7.5.2" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-svg/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@parcel/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/types/-/types-2.12.0.tgz", + "integrity": "sha512-8zAFiYNCwNTQcglIObyNwKfRYQK5ELlL13GuBOrSMxueUiI5ylgsGbTS1N7J3dAGZixHO8KhHGv5a71FILn9rQ==", + "dependencies": { + "@parcel/cache": "2.12.0", + "@parcel/diagnostic": "2.12.0", + "@parcel/fs": "2.12.0", + "@parcel/package-manager": "2.12.0", + "@parcel/source-map": "^2.1.1", + "@parcel/workers": "2.12.0", + "utility-types": "^3.10.0" + } + }, + "node_modules/@parcel/utils": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/utils/-/utils-2.12.0.tgz", + "integrity": "sha512-z1JhLuZ8QmDaYoEIuUCVZlhcFrS7LMfHrb2OCRui5SQFntRWBH2fNM6H/fXXUkT9SkxcuFP2DUA6/m4+Gkz72g==", + "dependencies": { + "@parcel/codeframe": "2.12.0", + "@parcel/diagnostic": "2.12.0", + "@parcel/logger": "2.12.0", + "@parcel/markdown-ansi": "2.12.0", + "@parcel/rust": "2.12.0", + "@parcel/source-map": "^2.1.1", + "chalk": "^4.1.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/utils/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==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@parcel/utils/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "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/@parcel/utils/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==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@parcel/utils/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==" + }, + "node_modules/@parcel/utils/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==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@parcel/utils/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==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.1.tgz", + "integrity": "sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==", + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.4.1", + "@parcel/watcher-darwin-arm64": "2.4.1", + "@parcel/watcher-darwin-x64": "2.4.1", + "@parcel/watcher-freebsd-x64": "2.4.1", + "@parcel/watcher-linux-arm-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-musl": "2.4.1", + "@parcel/watcher-linux-x64-glibc": "2.4.1", + "@parcel/watcher-linux-x64-musl": "2.4.1", + "@parcel/watcher-win32-arm64": "2.4.1", + "@parcel/watcher-win32-ia32": "2.4.1", + "@parcel/watcher-win32-x64": "2.4.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz", + "integrity": "sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz", + "integrity": "sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz", + "integrity": "sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz", + "integrity": "sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz", + "integrity": "sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz", + "integrity": "sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz", + "integrity": "sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz", + "integrity": "sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz", + "integrity": "sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz", + "integrity": "sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz", + "integrity": "sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz", + "integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/workers": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/workers/-/workers-2.12.0.tgz", + "integrity": "sha512-zv5We5Jmb+ZWXlU6A+AufyjY4oZckkxsZ8J4dvyWL0W8IQvGO1JB4FGeryyttzQv3RM3OxcN/BpTGPiDG6keBw==", + "dependencies": { + "@parcel/diagnostic": "2.12.0", + "@parcel/logger": "2.12.0", + "@parcel/profiler": "2.12.0", + "@parcel/types": "2.12.0", + "@parcel/utils": "2.12.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.12.0" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.1.tgz", + "integrity": "sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", + "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/@radix-ui/react-accessible-icon": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.0.3.tgz", + "integrity": "sha512-duVGKeWPSUILr/MdlPxV+GeULTc2rS1aihGdQ3N2qCUPMgxYLxvAsHJM3mCVLF8d5eK+ympmB22mb1F3a5biNw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-visually-hidden": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz", + "integrity": "sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.0.3.tgz", + "integrity": "sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz", + "integrity": "sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", + "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", + "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz", + "integrity": "sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-focus-guards": "1.0.1", + "@radix-ui/react-focus-scope": "1.0.4", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-controllable-state": "1.0.1", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.1.tgz", + "integrity": "sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz", + "integrity": "sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-escape-keydown": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz", + "integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz", + "integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", + "integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.0.7.tgz", + "integrity": "sha512-shtvVnlsxT6faMnK/a7n0wptwBD23xc1Z5mdrtKLwVEfsEMXodS0r5s0/g5P0hX//EKYZS2sxUjqfzlg52ZSnQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-focus-guards": "1.0.1", + "@radix-ui/react-focus-scope": "1.0.4", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-popper": "1.1.3", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-controllable-state": "1.0.1", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.3.tgz", + "integrity": "sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1", + "@radix-ui/react-use-rect": "1.0.1", + "@radix-ui/react-use-size": "1.0.1", + "@radix-ui/rect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz", + "integrity": "sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz", + "integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", + "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz", + "integrity": "sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-1.2.2.tgz", + "integrity": "sha512-zI7McXr8fNaSrUY9mZe4x/HC0jTLY9fWNhO1oLWYMQGDXuV4UCivIGTxwioSzO0ZCYX9iSLyWmAh/1TOmX3Cnw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/number": "1.0.1", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.4", + "@radix-ui/react-focus-guards": "1.0.1", + "@radix-ui/react-focus-scope": "1.0.3", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-popper": "1.1.2", + "@radix-ui/react-portal": "1.0.3", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1", + "@radix-ui/react-use-previous": "1.0.1", + "@radix-ui/react-visually-hidden": "1.0.3", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.4.tgz", + "integrity": "sha512-7UpBa/RKMoHJYjie1gkF1DlK8l1fdU/VKDpoS3rCCo8YBJR294GwcEHyxHw72yvphJ7ld0AXEcSLAzY2F/WyCg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-escape-keydown": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-scope": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.3.tgz", + "integrity": "sha512-upXdPfqI4islj2CslyfUBNlaJCPybbqRHAi1KER7Isel9Q2AtSJ0zRBZv8mWQiFXD2nyAJ4BhC3yXgZ6kMBSrQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-popper": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.2.tgz", + "integrity": "sha512-1CnGGfFi/bbqtJZZ0P/NQY20xdG3E0LALJaLUEoKwPLwl6PPPfbeiCqMVQnhoFRAxjJj4RpBRJzDmUgsex2tSg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1", + "@radix-ui/react-use-rect": "1.0.1", + "@radix-ui/react-use-size": "1.0.1", + "@radix-ui/rect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-portal": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.3.tgz", + "integrity": "sha512-xLYZeHrWoPmA5mEKEfZZevoVRK/Q43GfzRXkWV6qawIWWK8t6ifIiLQdd7rmQ4Vk1bmI21XhqF9BN3jWf+phpA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", + "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.0.3.tgz", + "integrity": "sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-previous": "1.0.1", + "@radix-ui/react-use-size": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.0.4.tgz", + "integrity": "sha512-egZfYY/+wRNCflXNHx+dePvnz9FbmssDTJBtgRfDY7e8SE5oIo3Py2eCB1ckAbh1Q7cQ/6yJZThJ++sgbxibog==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-roving-focus": "1.0.4", + "@radix-ui/react-use-controllable-state": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.1.5.tgz", + "integrity": "sha512-fRLn227WHIBRSzuRzGJ8W+5YALxofH23y0MlPLddaIpLpCDqdE0NZlS2NRQDRiptfxDeeCjgFIpexB1/zkxDlw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1", + "@radix-ui/react-visually-hidden": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz", + "integrity": "sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-popper": "1.1.3", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-visually-hidden": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", + "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", + "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz", + "integrity": "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", + "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.0.1.tgz", + "integrity": "sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz", + "integrity": "sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/rect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz", + "integrity": "sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.3.tgz", + "integrity": "sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.0.1.tgz", + "integrity": "sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/@reactflow/background": { + "version": "11.3.13", + "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.13.tgz", + "integrity": "sha512-hkvpVEhgvfTDyCvdlitw4ioKCYLaaiRXnuEG+1QM3Np+7N1DiWF1XOv5I8AFyNoJL07yXEkbECUTsHvkBvcG5A==", + "dependencies": { + "@reactflow/core": "11.11.3", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/controls": { + "version": "11.2.13", + "resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.13.tgz", + "integrity": "sha512-3xgEg6ALIVkAQCS4NiBjb7ad8Cb3D8CtA7Vvl4Hf5Ar2PIVs6FOaeft9s2iDZGtsWP35ECDYId1rIFVhQL8r+A==", + "dependencies": { + "@reactflow/core": "11.11.3", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/core": { + "version": "11.11.3", + "resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.11.3.tgz", + "integrity": "sha512-+adHdUa7fJSEM93fWfjQwyWXeI92a1eLKwWbIstoCakHpL8UjzwhEh6sn+mN2h/59MlVI7Ehr1iGTt3MsfcIFA==", + "dependencies": { + "@types/d3": "^7.4.0", + "@types/d3-drag": "^3.0.1", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", + "classcat": "^5.0.3", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/minimap": { + "version": "11.7.13", + "resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.13.tgz", + "integrity": "sha512-m2MvdiGSyOu44LEcERDEl1Aj6x//UQRWo3HEAejNU4HQTlJnYrSN8tgrYF8TxC1+c/9UdyzQY5VYgrTwW4QWdg==", + "dependencies": { + "@reactflow/core": "11.11.3", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", + "classcat": "^5.0.3", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/node-resizer": { + "version": "2.2.13", + "resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.13.tgz", + "integrity": "sha512-X7ceQ2s3jFLgbkg03n2RYr4hm3jTVrzkW2W/8ANv/SZfuVmF8XJxlERuD8Eka5voKqLda0ywIZGAbw9GoHLfUQ==", + "dependencies": { + "@reactflow/core": "11.11.3", + "classcat": "^5.0.4", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/node-toolbar": { + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.13.tgz", + "integrity": "sha512-aknvNICO10uWdthFSpgD6ctY/CTBeJUMV9co8T9Ilugr08Nb89IQ4uD0dPmr031ewMQxixtYIkw+sSDDzd2aaQ==", + "dependencies": { + "@reactflow/core": "11.11.3", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@remix-run/router": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz", + "integrity": "sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", + "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.3.tgz", + "integrity": "sha512-qC/xYId4NMebE6w/V33Fh9gWxLgURiNYgVNObbJl2LZv0GUUItCcCqC5axQSwRaAgaxl2mELq1rMzlswaQ0Zxg==", + "dev": true + }, + "node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-6.5.1.tgz", + "integrity": "sha512-9PYGcXrAxitycIjRmZB+Q0JaN07GZIWaTBIGQzfaZv+qr1n8X1XUEJ5rZ/vx6OVD9RRYlrNnXWExQXcmZeD/BQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", + "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-6.5.1.tgz", + "integrity": "sha512-8DPaVVE3fd5JKuIC29dqyMB54sA6mfgki2H2+swh+zNJoynC8pMPzOkidqHOSc6Wj032fhl8Z0TVn1GiPpAiJg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-6.5.1.tgz", + "integrity": "sha512-FwOEi0Il72iAzlkaHrlemVurgSQRDFbk0OC8dSvD5fSBPHltNh7JtLsxmZUhjYBZo2PpcU/RJvvi6Q0l7O7ogw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-6.5.1.tgz", + "integrity": "sha512-gWGsiwjb4tw+ITOJ86ndY/DZZ6cuXMNE/SjcDRg+HLuCmwpcjOktwRF9WgAiycTqJD/QXqL2f8IzE2Rzh7aVXA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-6.5.1.tgz", + "integrity": "sha512-2jT3nTayyYP7kI6aGutkyfJ7UMGtuguD72OjeGLwVNyfPRBD8zQthlvL+fAbAKk5n9ZNcvFkp/b1lZ7VsYqVJg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-6.5.1.tgz", + "integrity": "sha512-a1p6LF5Jt33O3rZoVRBqdxL350oge54iZWHNI6LJB5tQ7EelvD/Mb1mfBiZNAan0dt4i3VArkFRjA4iObuNykQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-preset": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-6.5.1.tgz", + "integrity": "sha512-6127fvO/FF2oi5EzSQOAjo1LE3OtNVh11R+/8FXa+mHx1ptAaS4cknIjnUA7e6j6fwGGJ17NzaTJFUwOV2zwCw==", + "dev": true, + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "^6.5.1", + "@svgr/babel-plugin-remove-jsx-attribute": "*", + "@svgr/babel-plugin-remove-jsx-empty-expression": "*", + "@svgr/babel-plugin-replace-jsx-attribute-value": "^6.5.1", + "@svgr/babel-plugin-svg-dynamic-title": "^6.5.1", + "@svgr/babel-plugin-svg-em-dimensions": "^6.5.1", + "@svgr/babel-plugin-transform-react-native-svg": "^6.5.1", + "@svgr/babel-plugin-transform-svg-component": "^6.5.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/core": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-6.5.1.tgz", + "integrity": "sha512-/xdLSWxK5QkqG524ONSjvg3V/FkNyCv538OIBdQqPNaAta3AsXj/Bd2FbvR87yMbXO2hFSWiAe/Q6IkVPDw+mw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.19.6", + "@svgr/babel-preset": "^6.5.1", + "@svgr/plugin-jsx": "^6.5.1", + "camelcase": "^6.2.0", + "cosmiconfig": "^7.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/hast-util-to-babel-ast": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-6.5.1.tgz", + "integrity": "sha512-1hnUxxjd83EAxbL4a0JDJoD3Dao3hmjvyvyEV8PzWmLK3B9m9NPlW7GKjFyoWE8nM7HnXzPcmmSyOW8yOddSXw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.0", + "entities": "^4.4.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/hast-util-to-babel-ast/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@svgr/plugin-jsx": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-6.5.1.tgz", + "integrity": "sha512-+UdQxI3jgtSjCykNSlEMuy1jSRQlGC7pqBCPvkG/2dATdWo082zHTTK3uhnAju2/6XpE6B5mZ3z4Z8Ns01S8Gw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.19.6", + "@svgr/babel-preset": "^6.5.1", + "@svgr/hast-util-to-babel-ast": "^6.5.1", + "svg-parser": "^2.0.4" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "^6.0.0" + } + }, + "node_modules/@swc/core": { + "version": "1.5.28", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.5.28.tgz", + "integrity": "sha512-muCdNIqOTURUgYeyyOLYE3ShL8SZO6dw6bhRm6dCvxWzCZOncPc5fB0kjcPXTML+9KJoHL7ks5xg+vsQK+v6ig==", + "hasInstallScript": true, + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.8" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.5.28", + "@swc/core-darwin-x64": "1.5.28", + "@swc/core-linux-arm-gnueabihf": "1.5.28", + "@swc/core-linux-arm64-gnu": "1.5.28", + "@swc/core-linux-arm64-musl": "1.5.28", + "@swc/core-linux-x64-gnu": "1.5.28", + "@swc/core-linux-x64-musl": "1.5.28", + "@swc/core-win32-arm64-msvc": "1.5.28", + "@swc/core-win32-ia32-msvc": "1.5.28", + "@swc/core-win32-x64-msvc": "1.5.28" + }, + "peerDependencies": { + "@swc/helpers": "*" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.5.28", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.5.28.tgz", + "integrity": "sha512-sP6g63ybzIdOWNDbn51tyHN8EMt7Mb4RMeHQEsXB7wQfDvzhpWB+AbfK6Gs3Q8fwP/pmWIrWW9csKOc1K2Mmkg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.5.28", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.5.28.tgz", + "integrity": "sha512-Bd/agp/g7QocQG5AuorOzSC78t8OzeN+pCN/QvJj1CvPhvppjJw6e1vAbOR8vO2vvGi2pvtf3polrYQStJtSiA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.5.28", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.5.28.tgz", + "integrity": "sha512-Wr3TwPGIveS9/OBWm0r9VAL8wkCR0zQn46J8K01uYCmVhUNK3Muxjs0vQBZaOrGu94mqbj9OXY+gB3W7aDvGdA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.5.28", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.5.28.tgz", + "integrity": "sha512-8G1ZwVTuLgTAVTMPD+M97eU6WeiRIlGHwKZ5fiJHPBcz1xqIC7jQcEh7XBkobkYoU5OILotls3gzjRt8CMNyDQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.5.28", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.5.28.tgz", + "integrity": "sha512-0Ajdzb5Fzvz+XUbN5ESeHAz9aHHSYiQcm+vmsDi0TtPHmsalfnqEPZmnK0zPALPJPLQP2dDo4hELeDg3/c3xgA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.5.28", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.5.28.tgz", + "integrity": "sha512-ueQ9VejnQUM2Pt+vT0IAKoF4vYBWUP6n1KHGdILpoGe3LuafQrqu7RoyQ15C7/AYii7hAeNhTFdf6gLbg8cjFg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.5.28", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.5.28.tgz", + "integrity": "sha512-G5th8Mg0az8CbY4GQt9/m5hg2Y0kGIwvQBeVACuLQB6q2Y4txzdiTpjmFqUUhEvvl7Klyx1IHvNhfXs3zpt7PA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.5.28", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.5.28.tgz", + "integrity": "sha512-JezwCGavZ7CkNXx4yInI4kpb71L0zxzxA9BFlmnsGKEEjVQcKc3hFpmIzfFVs+eotlBUwDNb0+Yo9m6Cb7lllA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.5.28", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.5.28.tgz", + "integrity": "sha512-q8tW5J4RkOkl7vYShnWS//VAb2Ngolfm9WOMaF2GRJUr2Y/Xeb/+cNjdsNOqea2BzW049D5vdP7XPmir3/zUZw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.5.28", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.5.28.tgz", + "integrity": "sha512-jap6EiB3wG1YE1hyhNr9KLPpH4PGm+5tVMfN0l7fgKtV0ikgpcEN/YF94tru+z5m2HovqYW009+Evq9dcVGmpg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==" + }, + "node_modules/@swc/helpers": { + "version": "0.5.11", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.11.tgz", + "integrity": "sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@swc/types": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.8.tgz", + "integrity": "sha512-RNFA3+7OJFNYY78x0FYwi1Ow+iF1eF5WvmfY1nXPOEH4R2p/D4Cr1vzje7dNAI2aLFqpv8Wyz4oKSWqIZArpQA==", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@tanstack/match-sorter-utils": { + "version": "8.15.1", + "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.15.1.tgz", + "integrity": "sha512-PnVV3d2poenUM31ZbZi/yXkBu3J7kd5k2u51CGwwNojag451AjTH9N6n41yjXz2fpLeewleyLBmNS6+HcGDlXw==", + "dependencies": { + "remove-accents": "0.5.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-core": { + "version": "4.36.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.36.1.tgz", + "integrity": "sha512-DJSilV5+ytBP1FbFcEJovv4rnnm/CokuVvrBEtW/Va9DvuJ3HksbXUJEpI0aV1KtuL4ZoO9AVE6PyNLzF7tLeA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "4.36.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.36.1.tgz", + "integrity": "sha512-y7ySVHFyyQblPl3J3eQBWpXZkliroki3ARnBKsdJchlgt7yJLRDUcf4B8soufgiYt3pEQIkBWBx1N9/ZPIeUWw==", + "dependencies": { + "@tanstack/query-core": "4.36.1", + "use-sync-external-store": "^1.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-native": "*" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/@tanstack/react-table": { + "version": "8.17.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.17.3.tgz", + "integrity": "sha512-5gwg5SvPD3lNAXPuJJz1fOCEZYk9/GeBFH3w/hCgnfyszOIzwkwgp5I7Q4MJtn0WECp84b5STQUDdmvGi8m3nA==", + "dependencies": { + "@tanstack/table-core": "8.17.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.17.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.17.3.tgz", + "integrity": "sha512-mPBodDGVL+fl6d90wUREepHa/7lhsghg2A3vFpakEhrhtbIlgNAZiMr7ccTgak5qbHqF14Fwy+W1yFWQt+WmYQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "dev": true + }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz", + "integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.9.tgz", + "integrity": "sha512-IKtvyFdb4Q0LWna6ymywQsEYjK/94SGhPrMfEr1TIc5OBeziTi+1jcCvttts8e0UWZIxpasjnQk9MNk/3iS+kA==" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", + "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz", + "integrity": "sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.10.tgz", + "integrity": "sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg==" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz", + "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", + "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.8.tgz", + "integrity": "sha512-ew63aJfQ/ms7QQ4X7pk5NxQ9fZH/z+i24ZfJ6tJSfqxJMrYLiK01EAs2/Rtw/JreGUsS3pLPNV644qXFGnoZNQ==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dev": true, + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@types/geojson": { + "version": "7946.0.14", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", + "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==" + }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", + "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, + "node_modules/@types/howler": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/@types/howler/-/howler-2.2.11.tgz", + "integrity": "sha512-7aBoUL6RbSIrqKnpEgfa1wSNUBK06mn08siP2QI0zYk7MXfEJAaORc4tohamQYqCqVESoDyRWSdQn2BOKWj2Qw==", + "dev": true + }, + "node_modules/@types/js-levenshtein": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/js-levenshtein/-/js-levenshtein-1.1.3.tgz", + "integrity": "sha512-jd+Q+sD20Qfu9e2aEXogiO3vpOC1PYJOUdyN9gvs4Qrvkg4wF43L5OhqrPeokdv8TL0/mXoYfpkcoGZMNN2pkQ==", + "dev": true + }, + "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 + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "node_modules/@types/lodash": { + "version": "4.17.5", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.5.tgz", + "integrity": "sha512-MBIOHVZqVqgfro1euRDWX7OO0fBVUUMrN6Pwm8LQsz8cWhEpihlvR70ENj3f40j58TNxZaWv2ndSkInykNBBJw==", + "dev": true + }, + "node_modules/@types/lodash.debounce": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.9.tgz", + "integrity": "sha512-Ma5JcgTREwpLRwMM+XwBR7DaWe96nC38uCBDFKZWbNKD+osjVzdpnUSwBcqCptrp16sSOLBAUb50Car5I0TCsQ==", + "dev": true, + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", + "dev": true + }, + "node_modules/@types/node": { + "version": "18.19.34", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.34.tgz", + "integrity": "sha512-eXF4pfBNV5DAMKGbI02NnDtWrQ40hAN558/2vvS4gMpMIxaf6JmD7YjnZbq0Q9TDSSkKBamime8ewRoomHdt4g==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" + }, + "node_modules/@types/prop-types": { + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" + }, + "node_modules/@types/react": { + "version": "18.3.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", + "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-color": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@types/react-color/-/react-color-3.0.12.tgz", + "integrity": "sha512-pr3uKE3lSvf7GFo1Rn2K3QktiZQFFrSgSGJ/3iMvSOYWt2pPAJ97rVdVfhWxYJZ8prAEXzoP2XX//3qGSQgu7Q==", + "dev": true, + "dependencies": { + "@types/react": "*", + "@types/reactcss": "*" + } + }, + "node_modules/@types/react-datepicker": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/react-datepicker/-/react-datepicker-4.19.6.tgz", + "integrity": "sha512-uH5fzxt9eXxnc+hDCy/iRSFqU2+9lR/q2lAmaG4WILMai1o3IOdpcV+VSypzBFJLTEC2jrfeDXcdol0CJVMq4g==", + "dev": true, + "dependencies": { + "@popperjs/core": "^2.9.2", + "@types/react": "*", + "date-fns": "^2.0.1", + "react-popper": "^2.2.5" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", + "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/reactcss": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@types/reactcss/-/reactcss-1.2.12.tgz", + "integrity": "sha512-BrXUQ86/wbbFiZv8h/Q1/Q1XOsaHneYmCb/tHe9+M8XBAAUc2EHfdY0DY22ZZjVSaXr5ix7j+zsqO2eGZub8lQ==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/semver": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "dev": true + }, + "node_modules/@types/set-cookie-parser": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/@types/set-cookie-parser/-/set-cookie-parser-2.4.9.tgz", + "integrity": "sha512-bCorlULvl0xTdjj4BPUHX4cqs9I+go2TfW/7Do1nnFYWS0CPP429Qr1AY42kiFhCwLpvAkWFr1XIBHd8j6/MCQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", + "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/experimental-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.62.0.tgz", + "integrity": "sha512-RTXpeB3eMkpoclG3ZHft6vG/Z30azNHuqY6wKPBHlVMZFuEvrtlEDe8gMqDb+SO+9hjC/pLekeSCryf9vMZlCw==", + "dev": true, + "dependencies": { + "@typescript-eslint/utils": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/@vitejs/plugin-react": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-3.1.0.tgz", + "integrity": "sha512-AfgcRL8ZBhAlc3BFdigClmTUMISmmzHn7sB2h9U1odvc5U/MjWXsAaz18b/WoppUTDBzxOJwo2VdClfUcItu9g==", + "dev": true, + "dependencies": { + "@babel/core": "^7.20.12", + "@babel/plugin-transform-react-jsx-self": "^7.18.6", + "@babel/plugin-transform-react-jsx-source": "^7.19.6", + "magic-string": "^0.27.0", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.1.0-beta.0" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@zxing/text-encoding": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz", + "integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==", + "dev": true, + "optional": true + }, + "node_modules/abortcontroller-polyfill": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.5.tgz", + "integrity": "sha512-JMJ5soJWP18htbbxJjG7bG6yuI6pRhgJ0scHHTfkUjf6wjP912xZWvM+A4sJK3gqd9E8fcPbDnOefbA9Th/FIQ==" + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "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, + "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, + "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-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "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==" + }, + "node_modules/aria-hidden": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", + "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "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, + "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-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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, + "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.findlastindex": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "dev": true, + "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.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.toreversed": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz", + "integrity": "sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + } + }, + "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, + "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.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "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, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz", + "integrity": "sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/axios": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axobject-query": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", + "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", + "integrity": "sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.6.2", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz", + "integrity": "sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.1", + "core-js-compat": "^3.36.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz", + "integrity": "sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-transform-react-remove-prop-types": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz", + "integrity": "sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA==", + "dev": true + }, + "node_modules/babel-preset-react-app": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-react-app/-/babel-preset-react-app-10.0.1.tgz", + "integrity": "sha512-b0D9IZ1WhhCWkrTXyFuIIgqGzSkRIH5D5AmB0bXbzYAB1OBAwHcUeyWW2LorutLWF5btNo/N7r/cIdmvvKJlYg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.16.0", + "@babel/plugin-proposal-class-properties": "^7.16.0", + "@babel/plugin-proposal-decorators": "^7.16.4", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.0", + "@babel/plugin-proposal-numeric-separator": "^7.16.0", + "@babel/plugin-proposal-optional-chaining": "^7.16.0", + "@babel/plugin-proposal-private-methods": "^7.16.0", + "@babel/plugin-transform-flow-strip-types": "^7.16.0", + "@babel/plugin-transform-react-display-name": "^7.16.0", + "@babel/plugin-transform-runtime": "^7.16.4", + "@babel/preset-env": "^7.16.4", + "@babel/preset-react": "^7.16.0", + "@babel/preset-typescript": "^7.16.0", + "@babel/runtime": "^7.16.3", + "babel-plugin-macros": "^3.1.0", + "babel-plugin-transform-react-remove-prop-types": "^0.4.24" + } + }, + "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 + }, + "node_modules/base-x": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz", + "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "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" + } + ] + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, + "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, + "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==", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", + "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", + "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" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001629", + "electron-to-chromium": "^1.4.796", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.16" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "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" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "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==", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001632", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001632.tgz", + "integrity": "sha512-udx3o7yHJfUxMLkGohMlVHCvFvWmirKh9JAH/d7WOLPetlH+LTL5cocMZ0t7oZx/mdlOWXti97xLZWc8uURRHg==", + "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" + } + ] + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/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, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==" + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/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, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/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, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/cliui/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 + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/compute-scroll-into-view": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-2.0.4.tgz", + "integrity": "sha512-y/ZA3BGnxoM/QHHQ2Uy49CLtnWPbt4tTPpEEZiEmmiWBFKjej7nEyH8Ryz54jH0MLXflUYA3Er2zUxPSJu5R+g==" + }, + "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 + }, + "node_modules/confusing-browser-globals": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", + "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", + "dev": true + }, + "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 + }, + "node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/core-js-compat": { + "version": "3.37.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz", + "integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/css-tree/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/csso": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", + "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", + "dependencies": { + "css-tree": "^1.1.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true + }, + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "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 + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defaults/node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "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, + "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, + "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/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dotenv": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-7.0.0.tgz", + "integrity": "sha512-M3NhsLbV1i6HuGzBUH8vXrtxOk+tWmzWKDMbAVSUp3Zsjm7ywFeuwrUXhmhQyRK1q5B5GGy7hcXPbj3bnfZg2g==", + "engines": { + "node": ">=6" + } + }, + "node_modules/dotenv-expand": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", + "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==" + }, + "node_modules/downshift": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/downshift/-/downshift-7.6.2.tgz", + "integrity": "sha512-iOv+E1Hyt3JDdL9yYcOgW7nZ7GQ2Uz6YbggwXvKUSleetYhU2nXD482Rz6CzvM4lvI1At34BYruKAL4swRGxaA==", + "dependencies": { + "@babel/runtime": "^7.14.8", + "compute-scroll-into-view": "^2.0.4", + "prop-types": "^15.7.2", + "react-is": "^17.0.2", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "react": ">=16.12.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.799", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.799.tgz", + "integrity": "sha512-3D3DwWkRTzrdEpntY0hMLYwj7SeBk1138CkPE8sBDSj3WzrzOiG2rHm3luw8jucpf+WiyLBCZyU9lMHyQI9M9Q==" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/entities": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.3", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.15" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "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, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz", + "integrity": "sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "globalthis": "^1.0.3", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.7", + "iterator.prototype": "^1.1.2", + "safe-array-concat": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" + }, + "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, + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-react-app": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz", + "integrity": "sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.16.0", + "@babel/eslint-parser": "^7.16.3", + "@rushstack/eslint-patch": "^1.1.0", + "@typescript-eslint/eslint-plugin": "^5.5.0", + "@typescript-eslint/parser": "^5.5.0", + "babel-preset-react-app": "^10.0.1", + "confusing-browser-globals": "^1.0.11", + "eslint-plugin-flowtype": "^8.0.3", + "eslint-plugin-import": "^2.25.3", + "eslint-plugin-jest": "^25.3.0", + "eslint-plugin-jsx-a11y": "^6.5.1", + "eslint-plugin-react": "^7.27.1", + "eslint-plugin-react-hooks": "^4.3.0", + "eslint-plugin-testing-library": "^5.0.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "eslint": "^8.0.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz", + "integrity": "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==", + "dev": true, + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-flowtype": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-8.0.3.tgz", + "integrity": "sha512-dX8l6qUL6O+fYPtpNRideCFSpmWOUVx5QcaGLVqe/vlDiBSe4vYljDWDETwnyFzpl7By/WVIu6rcrniCgH9BqQ==", + "dev": true, + "dependencies": { + "lodash": "^4.17.21", + "string-natural-compare": "^3.0.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@babel/plugin-syntax-flow": "^7.14.5", + "@babel/plugin-transform-react-jsx": "^7.14.9", + "eslint": "^8.1.0" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", + "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.7", + "array.prototype.findlastindex": "^1.2.3", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.8.0", + "hasown": "^2.0.0", + "is-core-module": "^2.13.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.7", + "object.groupby": "^1.0.1", + "object.values": "^1.1.7", + "semver": "^6.3.1", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-jest": { + "version": "25.7.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-25.7.0.tgz", + "integrity": "sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/experimental-utils": "^5.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^4.0.0 || ^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz", + "integrity": "sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.23.2", + "aria-query": "^5.3.0", + "array-includes": "^3.1.7", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "=4.7.0", + "axobject-query": "^3.2.1", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "es-iterator-helpers": "^1.0.15", + "hasown": "^2.0.0", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.entries": "^1.1.7", + "object.fromentries": "^2.0.7" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.34.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.2.tgz", + "integrity": "sha512-2HCmrU+/JNigDN6tg55cRDKCQWicYAPB38JGSFDQt95jDm8rrvSUo7YPkOIm5l6ts1j1zCvysNcasvfTMQzUOw==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.2", + "array.prototype.toreversed": "^1.1.2", + "array.prototype.tosorted": "^1.1.3", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.0.19", + "estraverse": "^5.3.0", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.hasown": "^1.1.4", + "object.values": "^1.2.0", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.11" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/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, + "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/eslint-plugin-testing-library": { + "version": "5.11.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.11.1.tgz", + "integrity": "sha512-5eX9e1Kc2PqVRed3taaLnAAqPZGEX75C+M/rXzUAI3wIg/ZxzUm1OVAwfe/O+vE+6YXOLetSe9g5GKD2ecXipw==", + "dev": true, + "dependencies": { + "@typescript-eslint/utils": "^5.58.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0", + "npm": ">=6" + }, + "peerDependencies": { + "eslint": "^7.5.0 || ^8.0.0" + } + }, + "node_modules/eslint-plugin-typescript": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-typescript/-/eslint-plugin-typescript-0.14.0.tgz", + "integrity": "sha512-2u1WnnDF2mkWWgU1lFQ2RjypUlmRoBEvQN02y9u+IL12mjWlkKFGEBnVsjs9Y8190bfPQCvWly1c2rYYUSOxWw==", + "deprecated": "Deprecated: Use @typescript-eslint/eslint-plugin instead", + "dev": true, + "dependencies": { + "requireindex": "~1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "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, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/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, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/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, + "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/eslint/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, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/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 + }, + "node_modules/eslint/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, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/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, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/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, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "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, + "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, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/exenv": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", + "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==" + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "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 + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "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.4" + }, + "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, + "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 + }, + "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 + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.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==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, + "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, + "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": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/framer-motion": { + "version": "8.5.5", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-8.5.5.tgz", + "integrity": "sha512-5IDx5bxkjWHWUF3CVJoSyUVOtrbAxtzYBBowRE2uYI/6VYhkEBD+rbTHEGuUmbGHRj6YqqSfoG7Aa1cLyWCrBA==", + "dependencies": { + "@motionone/dom": "^10.15.3", + "hey-listen": "^1.0.8", + "tslib": "^2.4.0" + }, + "optionalDependencies": { + "@emotion/is-prop-valid": "^0.8.2" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "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, + "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==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "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, + "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, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-port": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-4.2.0.tgz", + "integrity": "sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "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==", + "engines": { + "node": ">=4" + } + }, + "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, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "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 + }, + "node_modules/graphql": { + "version": "16.8.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.2.tgz", + "integrity": "sha512-cvVIBILwuoSyD54U4cF/UXDh5yAobhNV/tPygI4lZhgOIJQE/WLWC4waBRb4I6bDVYb3OVx3lfHbaQOEoUD5sg==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "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, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "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, + "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==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/headers-polyfill": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-3.3.0.tgz", + "integrity": "sha512-5e57etwBpNcDc0b6KCVWEh/Ro063OxPvzVimUdM0/tsYM/T7Hfy3kknIGj78SFTOhNd8AZY41U8mOHoO4LzmIQ==", + "dev": true + }, + "node_modules/hey-listen": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz", + "integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==" + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/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==" + }, + "node_modules/howler": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/howler/-/howler-2.2.4.tgz", + "integrity": "sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w==" + }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/htmlnano": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/htmlnano/-/htmlnano-2.1.1.tgz", + "integrity": "sha512-kAERyg/LuNZYmdqgCdYvugyLWNFAm8MWXpQMz1pLpetmCbFwoMxvkSoaAMlFrOC4OKTWI4KlZGT/RsNxg4ghOw==", + "dependencies": { + "cosmiconfig": "^9.0.0", + "posthtml": "^0.16.5", + "timsort": "^0.3.0" + }, + "peerDependencies": { + "cssnano": "^7.0.0", + "postcss": "^8.3.11", + "purgecss": "^6.0.0", + "relateurl": "^0.2.7", + "srcset": "5.0.1", + "svgo": "^3.0.2", + "terser": "^5.10.0", + "uncss": "^0.17.3" + }, + "peerDependenciesMeta": { + "cssnano": { + "optional": true + }, + "postcss": { + "optional": true + }, + "purgecss": { + "optional": true + }, + "relateurl": { + "optional": true + }, + "srcset": { + "optional": true + }, + "svgo": { + "optional": true + }, + "terser": { + "optional": true + }, + "uncss": { + "optional": true + } + } + }, + "node_modules/htmlnano/node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/htmlparser2": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-7.2.0.tgz", + "integrity": "sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.2", + "domutils": "^2.8.0", + "entities": "^3.0.1" + } + }, + "node_modules/i18next": { + "version": "22.5.1", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-22.5.1.tgz", + "integrity": "sha512-8TGPgM3pAD+VRsMtUMNknRz3kzqwp/gPALrWMsDnmC1mKqJwpWyooQRLMcbTwq8z8YwSmuj+ZYvc+xCuEpkssA==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "dependencies": { + "@babel/runtime": "^7.20.6" + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.2.1.tgz", + "integrity": "sha512-h/pM34bcH6tbz8WgGXcmWauNpQupCGr25XPp9cZwZInR9XHSjIFDYp1SIok7zSPsTOMxdvuLyu86V+g2Kycnfw==", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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" + } + ] + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/immutable": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz", + "integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==", + "dev": true + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "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, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/inquirer": { + "version": "8.2.6", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", + "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^6.0.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/inquirer/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, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/inquirer/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, + "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/inquirer/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, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/inquirer/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 + }, + "node_modules/inquirer/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, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/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, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/@radix-ui/react-toggle-group": { + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "node_modules/is-async-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", + "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.0.4.tgz", - "integrity": "sha512-Uaj/M/cMyiyT9Bx6fOZO0SAG4Cls0GptBWiBmBxofmDbNVnYYoyRWj/2M/6VCi/7qcXFWnHhRUfdfZFvvkuu8A==", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-roving-focus": "1.0.4", - "@radix-ui/react-toggle": "1.0.3", - "@radix-ui/react-use-controllable-state": "1.0.1" + "has-bigints": "^1.0.1" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": ">=8" } }, - "node_modules/@radix-ui/react-tooltip": { + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "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, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, + "dependencies": { + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "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==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", + "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.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==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-json": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-json/-/is-json-2.0.1.tgz", + "integrity": "sha512-6BEnpVn1rcf3ngfmViLM6vjUjGErbdrL4rwlv+u1NO1XO8kqT4YGL8+19Q+Z/bas8tY90BTWMk2+fW1g6hQjbA==" + }, + "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, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true + }, + "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==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz", - "integrity": "sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw==", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.5", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-popper": "1.1.3", - "@radix-ui/react-portal": "1.0.4", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2", - "@radix-ui/react-use-controllable-state": "1.0.1", - "@radix-ui/react-visually-hidden": "1.0.3" + "has-tostringtag": "^1.0.0" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "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, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", - "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", + "node_modules/is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10" + "call-bind": "^1.0.7" }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", - "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "1.0.1" + "has-tostringtag": "^1.0.0" }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz", - "integrity": "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==", + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "1.0.1" + "has-symbols": "^1.0.2" }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", - "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", + "node_modules/is-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10" + "which-typed-array": "^1.1.14" }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/@radix-ui/react-use-previous": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.0.1.tgz", - "integrity": "sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@radix-ui/react-use-rect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz", - "integrity": "sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/rect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "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, + "engines": { + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/@radix-ui/react-use-size": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz", - "integrity": "sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==", + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-layout-effect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "call-bind": "^1.0.2" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.3.tgz", - "integrity": "sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==", + "node_modules/is-weakset": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", + "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/@radix-ui/rect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.0.1.tgz", - "integrity": "sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==", + "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 + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/iterator.prototype": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", + "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.13.10" + "define-properties": "^1.2.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "reflect.getprototypeof": "^1.0.4", + "set-function-name": "^2.0.1" + } + }, + "node_modules/js-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "dev": true, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/@radix-ui/themes": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@radix-ui/themes/-/themes-3.0.5.tgz", - "integrity": "sha512-fqIxdxer2tVHtrmuT/Wx793sMKIaJBSZQjFSrkorz6BgOCrijjK6pRRi1qa3Sq78vyjixVXR7kRlieBomUzzzQ==", + "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==" + }, + "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==", "dependencies": { - "@radix-ui/colors": "^3.0.0", - "@radix-ui/primitive": "^1.0.1", - "@radix-ui/react-accessible-icon": "^1.0.3", - "@radix-ui/react-alert-dialog": "^1.0.5", - "@radix-ui/react-aspect-ratio": "^1.0.3", - "@radix-ui/react-avatar": "^1.0.4", - "@radix-ui/react-checkbox": "^1.0.4", - "@radix-ui/react-compose-refs": "^1.0.1", - "@radix-ui/react-context": "^1.0.1", - "@radix-ui/react-context-menu": "^2.1.5", - "@radix-ui/react-dialog": "^1.0.5", - "@radix-ui/react-direction": "^1.0.1", - "@radix-ui/react-dropdown-menu": "^2.0.6", - "@radix-ui/react-form": "^0.0.3", - "@radix-ui/react-hover-card": "^1.0.7", - "@radix-ui/react-navigation-menu": "^1.1.4", - "@radix-ui/react-popover": "^1.0.7", - "@radix-ui/react-portal": "^1.0.4", - "@radix-ui/react-primitive": "^1.0.3", - "@radix-ui/react-progress": "^1.0.3", - "@radix-ui/react-radio-group": "^1.1.3", - "@radix-ui/react-roving-focus": "^1.0.4", - "@radix-ui/react-scroll-area": "^1.0.5", - "@radix-ui/react-select": "^2.0.0", - "@radix-ui/react-slider": "^1.1.2", - "@radix-ui/react-slot": "^1.0.2", - "@radix-ui/react-switch": "^1.0.3", - "@radix-ui/react-tabs": "^1.0.4", - "@radix-ui/react-toggle-group": "^1.0.4", - "@radix-ui/react-tooltip": "^1.0.7", - "@radix-ui/react-use-callback-ref": "^1.0.1", - "@radix-ui/react-use-controllable-state": "^1.0.1", - "@radix-ui/react-visually-hidden": "^1.0.3", - "classnames": "^2.3.2", - "react-remove-scroll-bar": "2.3.4" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "argparse": "^2.0.1" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/@radix-ui/themes/node_modules/@radix-ui/react-select": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.0.0.tgz", - "integrity": "sha512-RH5b7af4oHtkcHS7pG6Sgv5rk5Wxa7XI8W5gvB1N/yiuDGZxko1ynvOiVhFM7Cis2A8zxF9bTOUVbRDzPepe6w==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/number": "1.0.1", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-collection": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.5", - "@radix-ui/react-focus-guards": "1.0.1", - "@radix-ui/react-focus-scope": "1.0.4", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-popper": "1.1.3", - "@radix-ui/react-portal": "1.0.4", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-controllable-state": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1", - "@radix-ui/react-use-previous": "1.0.1", - "@radix-ui/react-visually-hidden": "1.0.3", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.5" + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "bin": { + "jsesc": "bin/jsesc" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "engines": { + "node": ">=4" + } + }, + "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 + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "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 + }, + "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 + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": ">=6" } }, - "node_modules/@remix-run/router": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz", - "integrity": "sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==", + "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, + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, "engines": { - "node": ">=14.0.0" + "node": ">=4.0" } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", - "integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==", - "cpu": [ - "arm" - ], + "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, - "optional": true, - "os": [ - "android" - ] + "dependencies": { + "json-buffer": "3.0.1" + } }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz", - "integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==", - "cpu": [ - "arm64" - ], + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", "dev": true, - "optional": true, - "os": [ - "android" - ] + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "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, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz", - "integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==", + "node_modules/lightningcss": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.25.1.tgz", + "integrity": "sha512-V0RMVZzK1+rCHpymRv4URK2lNhIRyO8g7U7zOFwVAhJuat74HtkjIQpQRKNCwFEYkRGpafOpmXXLoaoBcyVtBg==", + "dependencies": { + "detect-libc": "^1.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.25.1", + "lightningcss-darwin-x64": "1.25.1", + "lightningcss-freebsd-x64": "1.25.1", + "lightningcss-linux-arm-gnueabihf": "1.25.1", + "lightningcss-linux-arm64-gnu": "1.25.1", + "lightningcss-linux-arm64-musl": "1.25.1", + "lightningcss-linux-x64-gnu": "1.25.1", + "lightningcss-linux-x64-musl": "1.25.1", + "lightningcss-win32-x64-msvc": "1.25.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.25.1.tgz", + "integrity": "sha512-G4Dcvv85bs5NLENcu/s1f7ehzE3D5ThnlWSDwE190tWXRQCQaqwcuHe+MGSVI/slm0XrxnaayXY+cNl3cSricw==", "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz", - "integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==", + "node_modules/lightningcss-darwin-x64": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.25.1.tgz", + "integrity": "sha512-dYWuCzzfqRueDSmto6YU5SoGHvZTMU1Em9xvhcdROpmtOQLorurUZz8+xFxZ51lCO2LnYbfdjZ/gCqWEkwixNg==", "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz", - "integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==", + "node_modules/lightningcss-freebsd-x64": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.25.1.tgz", + "integrity": "sha512-hXoy2s9A3KVNAIoKz+Fp6bNeY+h9c3tkcx1J3+pS48CqAt+5bI/R/YY4hxGL57fWAIquRjGKW50arltD6iRt/w==", "cpu": [ - "arm" + "x64" ], - "dev": true, "optional": true, "os": [ - "linux" - ] + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz", - "integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==", + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.25.1.tgz", + "integrity": "sha512-tWyMgHFlHlp1e5iW3EpqvH5MvsgoN7ZkylBbG2R2LWxnvH3FuWCJOhtGcYx9Ks0Kv0eZOBud789odkYLhyf1ng==", "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz", - "integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==", - "cpu": [ - "arm64" ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz", - "integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==", + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.25.1.tgz", + "integrity": "sha512-Xjxsx286OT9/XSnVLIsFEDyDipqe4BcLeB4pXQ/FEA5+2uWCCuAEarUNQumRucnj7k6ftkAHUEph5r821KBccQ==", "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz", - "integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==", - "cpu": [ - "ppc64" ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz", - "integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==", + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.25.1.tgz", + "integrity": "sha512-IhxVFJoTW8wq6yLvxdPvyHv4NjzcpN1B7gjxrY3uaykQNXPHNIpChLB52+wfH+yS58zm1PL4LemUp8u9Cfp6Bw==", "cpu": [ - "riscv64" + "arm64" ], - "dev": true, "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz", - "integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==", - "cpu": [ - "s390x" ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", - "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.25.1.tgz", + "integrity": "sha512-RXIaru79KrREPEd6WLXfKfIp4QzoppZvD3x7vuTKkDA64PwTzKJ2jaC43RZHRt8BmyIkRRlmywNhTRMbmkPYpA==", "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz", - "integrity": "sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==", + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.25.1.tgz", + "integrity": "sha512-TdcNqFsAENEEFr8fJWg0Y4fZ/nwuqTRsIr7W7t2wmDUlA8eSXVepeeONYcb+gtTj1RaXn/WgNLB45SFkz+XBZA==", "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz", - "integrity": "sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz", - "integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz", - "integrity": "sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==", - "cpu": [ - "x64" ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@tanstack/match-sorter-utils": { - "version": "8.15.1", - "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.15.1.tgz", - "integrity": "sha512-PnVV3d2poenUM31ZbZi/yXkBu3J7kd5k2u51CGwwNojag451AjTH9N6n41yjXz2fpLeewleyLBmNS6+HcGDlXw==", - "dependencies": { - "remove-accents": "0.5.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/query-core": { - "version": "4.36.1", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.36.1.tgz", - "integrity": "sha512-DJSilV5+ytBP1FbFcEJovv4rnnm/CokuVvrBEtW/Va9DvuJ3HksbXUJEpI0aV1KtuL4ZoO9AVE6PyNLzF7tLeA==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/react-query": { - "version": "4.36.1", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.36.1.tgz", - "integrity": "sha512-y7ySVHFyyQblPl3J3eQBWpXZkliroki3ARnBKsdJchlgt7yJLRDUcf4B8soufgiYt3pEQIkBWBx1N9/ZPIeUWw==", - "dependencies": { - "@tanstack/query-core": "4.36.1", - "use-sync-external-store": "^1.2.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-native": "*" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - } - } - }, - "node_modules/@tanstack/react-table": { - "version": "8.17.3", - "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.17.3.tgz", - "integrity": "sha512-5gwg5SvPD3lNAXPuJJz1fOCEZYk9/GeBFH3w/hCgnfyszOIzwkwgp5I7Q4MJtn0WECp84b5STQUDdmvGi8m3nA==", - "dependencies": { - "@tanstack/table-core": "8.17.3" - }, "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" + "node": ">= 12.0.0" }, - "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@tanstack/table-core": { - "version": "8.17.3", - "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.17.3.tgz", - "integrity": "sha512-mPBodDGVL+fl6d90wUREepHa/7lhsghg2A3vFpakEhrhtbIlgNAZiMr7ccTgak5qbHqF14Fwy+W1yFWQt+WmYQ==", + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.25.1.tgz", + "integrity": "sha512-9KZZkmmy9oGDSrnyHuxP6iMhbsgChUiu/NSgOx+U1I/wTngBStDf2i2aGRCHvFqj19HqqBEI4WuGVQBa2V6e0A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=12" + "node": ">= 12.0.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "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, - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, - "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, - "dependencies": { - "@babel/types": "^7.0.0" + "node_modules/linkify-react": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/linkify-react/-/linkify-react-4.1.3.tgz", + "integrity": "sha512-rhI3zM/fxn5BfRPHfi4r9N7zgac4vOIxub1wHIWXLA5ENTMs+BGaIaFO1D1PhmxgwhIKmJz3H7uCP0Dg5JwSlA==", + "peerDependencies": { + "linkifyjs": "^4.0.0", + "react": ">= 15.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, + "node_modules/linkifyjs": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.1.3.tgz", + "integrity": "sha512-auMesunaJ8yfkHvK4gfg1K0SaKX/6Wn9g2Aac/NwX+l5VdmFZzo/hdPGxEOETj+ryRa4/fiOPjeeKURSAJx1sg==" + }, + "node_modules/lmdb": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-2.8.5.tgz", + "integrity": "sha512-9bMdFfc80S+vSldBmG3HOuLVHnxRdNTlpzR6QDnzqCQtCzGUEAGTzBKYMeIM+I/sU4oZfgbcbS7X7F65/z/oxQ==", + "hasInstallScript": true, "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" + "msgpackr": "^1.9.5", + "node-addon-api": "^6.1.0", + "node-gyp-build-optional-packages": "5.1.1", + "ordered-binary": "^1.4.1", + "weak-lru-cache": "^1.2.2" + }, + "bin": { + "download-lmdb-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@lmdb/lmdb-darwin-arm64": "2.8.5", + "@lmdb/lmdb-darwin-x64": "2.8.5", + "@lmdb/lmdb-linux-arm": "2.8.5", + "@lmdb/lmdb-linux-arm64": "2.8.5", + "@lmdb/lmdb-linux-x64": "2.8.5", + "@lmdb/lmdb-win32-x64": "2.8.5" } }, - "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==", + "node_modules/lmdb/node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==" + }, + "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, "dependencies": { - "@babel/types": "^7.20.7" + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true - }, - "node_modules/@types/prop-types": { - "version": "15.7.12", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", - "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, - "node_modules/@types/react": { - "version": "18.3.3", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", - "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.0.2" - } + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" }, - "node_modules/@types/react-dom": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", - "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", - "dev": true, - "dependencies": { - "@types/react": "*" - } + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, - "node_modules/@types/react-i18next": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@types/react-i18next/-/react-i18next-8.1.0.tgz", - "integrity": "sha512-d4xhcjX5b3roNMObRNMfb1HinHQlQLPo8xlDj60dnHeeAw2bBymR2cy/l1giJpHzo/ZFgSvgVUvIWr4kCrenCg==", - "deprecated": "This is a stub types definition. react-i18next provides its own type definitions, so you do not need this installed.", - "dev": true, - "dependencies": { - "react-i18next": "*" - } + "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 }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.12.0.tgz", - "integrity": "sha512-7F91fcbuDf/d3S8o21+r3ZncGIke/+eWk0EpO21LXhDfLahriZF9CGj4fbAetEjlaBdjdSm9a6VeXbpbT6Z40Q==", + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "dev": true, "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.12.0", - "@typescript-eslint/type-utils": "7.12.0", - "@typescript-eslint/utils": "7.12.0", - "@typescript-eslint/visitor-keys": "7.12.0", - "graphemer": "^1.4.0", - "ignore": "^5.3.1", - "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": ">=10" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^7.0.0", - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@typescript-eslint/parser": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.12.0.tgz", - "integrity": "sha512-dm/J2UDY3oV3TKius2OUZIFHsomQmpHtsV0FTh1WO8EKgHLQ1QCADUqscPgTpU+ih1e21FQSRjXckHn3txn6kQ==", + "node_modules/log-symbols/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, "dependencies": { - "@typescript-eslint/scope-manager": "7.12.0", - "@typescript-eslint/types": "7.12.0", - "@typescript-eslint/typescript-estree": "7.12.0", - "@typescript-eslint/visitor-keys": "7.12.0", - "debug": "^4.3.4" + "color-convert": "^2.0.1" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": ">=8" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.12.0.tgz", - "integrity": "sha512-itF1pTnN6F3unPak+kutH9raIkL3lhH1YRPGgt7QQOh43DQKVJXmWkpb+vpc/TiDHs6RSd9CTbDsc/Y+Ygq7kg==", + "node_modules/log-symbols/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, "dependencies": { - "@typescript-eslint/types": "7.12.0", - "@typescript-eslint/visitor-keys": "7.12.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": ">=10" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.12.0.tgz", - "integrity": "sha512-lib96tyRtMhLxwauDWUp/uW3FMhLA6D0rJ8T7HmH7x23Gk1Gwwu8UZ94NMXBvOELn6flSPiBrCKlehkiXyaqwA==", + "node_modules/log-symbols/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, "dependencies": { - "@typescript-eslint/typescript-estree": "7.12.0", - "@typescript-eslint/utils": "7.12.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" + "color-name": "~1.1.4" }, "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "node": ">=7.0.0" } }, - "node_modules/@typescript-eslint/types": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.12.0.tgz", - "integrity": "sha512-o+0Te6eWp2ppKY3mLCU+YA9pVJxhUJE15FV7kxuD9jgwIAa+w/ycGJBMrYDTpVGUM/tgpa9SeMOugSabWFq7bg==", + "node_modules/log-symbols/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 + }, + "node_modules/log-symbols/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, "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">=8" } }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.12.0.tgz", - "integrity": "sha512-5bwqLsWBULv1h6pn7cMW5dXX/Y2amRqLaKqsASVwbBHMZSnHqE/HN4vT4fE0aFsiwxYvr98kqOWh1a8ZKXalCQ==", + "node_modules/log-symbols/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, "dependencies": { - "@typescript-eslint/types": "7.12.0", - "@typescript-eslint/visitor-keys": "7.12.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" + "has-flag": "^4.0.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "node": ">=8" } }, - "node_modules/@typescript-eslint/utils": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.12.0.tgz", - "integrity": "sha512-Y6hhwxwDx41HNpjuYswYp6gDbkiZ8Hin9Bf5aJQn1bpTs3afYY4GX+MPYxma8jtoIV2GRwTM/UJm/2uGCVv+DQ==", - "dev": true, + "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==", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.12.0", - "@typescript-eslint/types": "7.12.0", - "@typescript-eslint/typescript-estree": "7.12.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "js-tokens": "^3.0.0 || ^4.0.0" }, - "peerDependencies": { - "eslint": "^8.56.0" + "bin": { + "loose-envify": "cli.js" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.12.0.tgz", - "integrity": "sha512-uZk7DevrQLL3vSnfFl5bj4sL75qC9D6EdjemIdbtkuUmIheWpuiiylSY01JxJE7+zGrOWDZrp1WxOuDntvKrHQ==", + "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, "dependencies": { - "@typescript-eslint/types": "7.12.0", - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "yallist": "^3.0.2" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true - }, - "node_modules/@vitejs/plugin-react": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.0.tgz", - "integrity": "sha512-KcEbMsn4Dpk+LIbHMj7gDPRKaTMStxxWRkRmxsg/jVdFdJCZWt1SchZcf0M4t8lIKdwwMsEyzhrcOXRrDPtOBw==", + "node_modules/magic-string": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", + "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", "dev": true, "dependencies": { - "@babel/core": "^7.24.5", - "@babel/plugin-transform-react-jsx-self": "^7.24.5", - "@babel/plugin-transform-react-jsx-source": "^7.24.1", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.14.2" + "@jridgewell/sourcemap-codec": "^1.4.13" }, "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0" + "node": ">=12" } }, - "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } + "node_modules/material-colors": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", + "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==" }, - "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==", + "node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" + }, + "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, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "engines": { + "node": ">= 8" } }, - "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, + "node_modules/micromatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", "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" + "braces": "^3.0.3", + "picomatch": "^2.3.1" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "engines": { + "node": ">=8.6" } }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "engines": { - "node": ">=8" + "node": ">= 0.6" } }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dependencies": { - "color-convert": "^1.9.0" + "mime-db": "1.52.0" }, "engines": { - "node": ">=4" + "node": ">= 0.6" } }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "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, "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">= 8" + "node": "*" } }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "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==", + "node_modules/mocksse": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mocksse/-/mocksse-1.0.4.tgz", + "integrity": "sha512-W5DR/wwmx/EZUgjN1g+pvlhvFFtRJ3CqGRKqsK/B1hTxrjMb/t3JCbk6aomJD4WomrnueqMaTAhcAkIZJYd73w==", "dev": true }, - "node_modules/aria-hidden": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", - "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/msgpackr": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.10.2.tgz", + "integrity": "sha512-L60rsPynBvNE+8BWipKKZ9jHcSGbtyJYIwjRq0VrIvQ08cRjntGXJYW/tmciZ2IHWIY8WEW32Qa2xbh5+SKBZA==", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "hasInstallScript": true, + "optional": true, "dependencies": { - "tslib": "^2.0.0" + "node-gyp-build-optional-packages": "5.2.2" }, - "engines": { - "node": ">=10" + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" } }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, + "node_modules/msgpackr-extract/node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "optional": true, "engines": { "node": ">=8" } }, - "node_modules/autoprefixer": { - "version": "10.4.19", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", - "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", + "node_modules/msgpackr-extract/node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/msw": { + "version": "0.49.3", + "resolved": "https://registry.npmjs.org/msw/-/msw-0.49.3.tgz", + "integrity": "sha512-kRCbDNbNnRq5LC1H/NUceZlrPAvSrMH6Or0mirIuH69NY84xwDruPn/hkXTovIK1KwDwbk+ZdoSyJlpiekLxEA==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "hasInstallScript": true, "dependencies": { - "browserslist": "^4.23.0", - "caniuse-lite": "^1.0.30001599", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", - "postcss-value-parser": "^4.2.0" + "@mswjs/cookies": "^0.2.2", + "@mswjs/interceptors": "^0.17.5", + "@open-draft/until": "^1.0.3", + "@types/cookie": "^0.4.1", + "@types/js-levenshtein": "^1.1.1", + "chalk": "4.1.1", + "chokidar": "^3.4.2", + "cookie": "^0.4.2", + "graphql": "^15.0.0 || ^16.0.0", + "headers-polyfill": "^3.1.0", + "inquirer": "^8.2.0", + "is-node-process": "^1.0.1", + "js-levenshtein": "^1.1.6", + "node-fetch": "^2.6.7", + "outvariant": "^1.3.0", + "path-to-regexp": "^6.2.0", + "strict-event-emitter": "^0.4.3", + "type-fest": "^2.19.0", + "yargs": "^17.3.1" }, "bin": { - "autoprefixer": "bin/autoprefixer" + "msw": "cli/index.js" }, "engines": { - "node": "^10 || ^12 || >=14" + "node": ">=14" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mswjs" }, "peerDependencies": { - "postcss": "^8.1.0" + "typescript": ">= 4.4.x <= 4.9.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "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 - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "node_modules/msw/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, + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { "node": ">=8" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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, - "dependencies": { - "balanced-match": "^1.0.0" + "url": "https://github.com/chalk/ansi-styles?sponsor=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==", + "node_modules/msw/node_modules/chalk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", "dev": true, "dependencies": { - "fill-range": "^7.1.1" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "node_modules/msw/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, - "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" - } - ], "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" - }, - "bin": { - "browserslist": "cli.js" + "color-name": "~1.1.4" }, "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + "node": ">=7.0.0" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "node_modules/msw/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 + }, + "node_modules/msw/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, "engines": { - "node": ">=6" + "node": ">=8" } }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "node_modules/msw/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, + "dependencies": { + "has-flag": "^4.0.0" + }, "engines": { - "node": ">= 6" + "node": ">=8" } }, - "node_modules/caniuse-lite": { - "version": "1.0.30001629", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001629.tgz", - "integrity": "sha512-c3dl911slnQhmxUIT4HhYzT7wnBK/XYpGnYLOj4nJBaRiw52Ibe7YxlDaAeRECvA786zCuExhxIUJ2K7nHMrBw==", + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "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" } - ] - }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + ], + "bin": { + "nanoid": "bin/nanoid.cjs" }, "engines": { - "node": ">=4" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "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 + }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true + }, + "node_modules/node-addon-api": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.0.tgz", + "integrity": "sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==", + "engines": { + "node": "^16 || ^18 || >= 20" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "dev": true, "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "whatwg-url": "^5.0.0" }, "engines": { - "node": ">= 8.10.0" + "node": "4.x || >=6.0.0" }, - "funding": { - "url": "https://paulmillr.com/funding/" + "peerDependencies": { + "encoding": "^0.1.0" }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "peerDependenciesMeta": { + "encoding": { + "optional": true + } } }, - "node_modules/chokidar/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, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz", + "integrity": "sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==", "dependencies": { - "is-glob": "^4.0.1" + "detect-libc": "^2.0.1" }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/node-gyp-build-optional-packages/node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", "engines": { - "node": ">= 6" + "node": ">=8" } }, - "node_modules/classnames": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", - "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, "engines": { - "node": ">=6" + "node": ">=0.10.0" } }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", "dependencies": { - "color-name": "1.1.3" + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" } }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true + "node_modules/nullthrows": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", + "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==" }, - "node_modules/commander": { + "node_modules/object-assign": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "engines": { - "node": ">= 6" + "node": ">=0.10.0" } }, - "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 + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "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 + "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, + "engines": { + "node": ">= 0.4" + } }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "node_modules/object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", "dev": true, "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" }, "engines": { - "node": ">= 8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "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, - "bin": { - "cssesc": "bin/cssesc" + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "engines": { - "node": ">=4" + "node": ">= 0.4" } }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" - }, - "node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "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, "dependencies": { - "ms": "2.1.2" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" }, "engines": { - "node": ">=6.0" + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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 - }, - "node_modules/detect-node-es": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", "dev": true, "dependencies": { - "path-type": "^4.0.0" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" }, "engines": { - "node": ">=8" + "node": ">= 0.4" } }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "node_modules/object.hasown": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.4.tgz", + "integrity": "sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg==", "dev": true, "dependencies": { - "esutils": "^2.0.2" + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" }, "engines": { - "node": ">=6.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true - }, - "node_modules/electron-to-chromium": { - "version": "1.4.791", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.791.tgz", - "integrity": "sha512-6FlqP0NSWvxFf1v+gHu+LCn5wjr1pmkj5nPr7BsxPnj41EDR4EWhK/KmQN0ytHUqgTR1lkpHRYxvHBLZFQtkKw==", - "dev": true - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "node_modules/esbuild": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", - "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "node_modules/object.values": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "engines": { - "node": ">=12" + "node": ">= 0.4" }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.20.2", - "@esbuild/android-arm": "0.20.2", - "@esbuild/android-arm64": "0.20.2", - "@esbuild/android-x64": "0.20.2", - "@esbuild/darwin-arm64": "0.20.2", - "@esbuild/darwin-x64": "0.20.2", - "@esbuild/freebsd-arm64": "0.20.2", - "@esbuild/freebsd-x64": "0.20.2", - "@esbuild/linux-arm": "0.20.2", - "@esbuild/linux-arm64": "0.20.2", - "@esbuild/linux-ia32": "0.20.2", - "@esbuild/linux-loong64": "0.20.2", - "@esbuild/linux-mips64el": "0.20.2", - "@esbuild/linux-ppc64": "0.20.2", - "@esbuild/linux-riscv64": "0.20.2", - "@esbuild/linux-s390x": "0.20.2", - "@esbuild/linux-x64": "0.20.2", - "@esbuild/netbsd-x64": "0.20.2", - "@esbuild/openbsd-x64": "0.20.2", - "@esbuild/sunos-x64": "0.20.2", - "@esbuild/win32-arm64": "0.20.2", - "@esbuild/win32-ia32": "0.20.2", - "@esbuild/win32-x64": "0.20.2" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, "engines": { "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, + "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": ">= 0.8.0" } }, - "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", "dev": true, "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" }, - "bin": { - "eslint": "bin/eslint.js" + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/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, + "dependencies": { + "color-convert": "^2.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=8" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/eslint-plugin-react-hooks": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", - "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "node_modules/ora/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, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, "engines": { "node": ">=10" }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.7", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.7.tgz", - "integrity": "sha512-yrj+KInFmwuQS2UQcg1SF83ha1tuHC1jMQbRNyuWtlEzzKRDgAl7L4Yp4NlDUZTZNlWvHEzOtJhMi40R7JxcSw==", + "node_modules/ora/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, - "peerDependencies": { - "eslint": ">=7" + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "node_modules/ora/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 + }, + "node_modules/ora/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, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/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, "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" + "has-flag": "^4.0.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=8" + } + }, + "node_modules/ordered-binary": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.5.1.tgz", + "integrity": "sha512-5VyHfHY3cd0iza71JepYG50My+YUbrFtGoUz2ooEydPyPM7Aai/JW098juLr+RG6+rDJuzNNTsEQu2DZa1A41A==" + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/outvariant": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.2.tgz", + "integrity": "sha512-Ou3dJ6bA/UJ5GVHxah4LnqDwZRwAmWxrG3wtrHrbGnP4RnLCtA64A4F+ae7Y8ww660JaddSoArUR5HjipWSHAQ==", + "dev": true + }, + "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, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", + "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, + "dependencies": { + "p-limit": "^3.0.2" + }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=10" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/ansi-styles": { + "node_modules/parcel": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/parcel/-/parcel-2.12.0.tgz", + "integrity": "sha512-W+gxAq7aQ9dJIg/XLKGcRT0cvnStFAQHPaI0pvD0U2l6IVLueUAm3nwN7lkY62zZNmlvNx6jNtE4wlbS+CyqSg==", + "dependencies": { + "@parcel/config-default": "2.12.0", + "@parcel/core": "2.12.0", + "@parcel/diagnostic": "2.12.0", + "@parcel/events": "2.12.0", + "@parcel/fs": "2.12.0", + "@parcel/logger": "2.12.0", + "@parcel/package-manager": "2.12.0", + "@parcel/reporter-cli": "2.12.0", + "@parcel/reporter-dev-server": "2.12.0", + "@parcel/reporter-tracer": "2.12.0", + "@parcel/utils": "2.12.0", + "chalk": "^4.1.0", + "commander": "^7.0.0", + "get-port": "^4.2.0" + }, + "bin": { + "parcel": "lib/bin.js" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/parcel/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, "dependencies": { "color-convert": "^2.0.1" }, @@ -3939,21 +12047,10 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/eslint/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, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/chalk": { + "node_modules/parcel/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, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -3965,11 +12062,10 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/eslint/node_modules/color-convert": { + "node_modules/parcel/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, "dependencies": { "color-name": "~1.1.4" }, @@ -3977,31 +12073,50 @@ "node": ">=7.0.0" } }, - "node_modules/eslint/node_modules/color-name": { + "node_modules/parcel/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 + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, - "node_modules/eslint/node_modules/escape-string-regexp": { + "node_modules/parcel/node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "engines": { - "node": ">=10" + "node": ">=8" + } + }, + "node_modules/parcel/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==", + "dependencies": { + "has-flag": "^4.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=8" } }, - "node_modules/eslint/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, + "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==", "dependencies": { - "type-fest": "^0.20.2" + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" }, "engines": { "node": ">=8" @@ -4010,2080 +12125,2361 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/has-flag": { + "node_modules/path-exists": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, "engines": { "node": ">=8" } }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, "engines": { - "node": "*" + "node": ">=0.10.0" } }, - "node_modules/eslint/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==", + "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, - "dependencies": { - "has-flag": "^4.0.0" - }, "engines": { "node": ">=8" } }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "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==" + }, + "node_modules/path-to-regexp": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", + "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", "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" + } + ], "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/posthtml": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/posthtml/-/posthtml-0.16.6.tgz", + "integrity": "sha512-JcEmHlyLK/o0uGAlj65vgg+7LIms0xKXe60lcDOTU7oVX/3LuEuLwrQpW3VJ7de5TaFKiW4kWkaIpJL42FEgxQ==", + "dependencies": { + "posthtml-parser": "^0.11.0", + "posthtml-render": "^3.0.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=12.0.0" } }, - "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", - "dev": true, + "node_modules/posthtml-parser": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/posthtml-parser/-/posthtml-parser-0.10.2.tgz", + "integrity": "sha512-PId6zZ/2lyJi9LiKfe+i2xv57oEjJgWbsHGGANwos5AvdQp98i6AtamAl8gzSVFGfQ43Glb5D614cvZf012VKg==", "dependencies": { - "estraverse": "^5.1.0" + "htmlparser2": "^7.1.1" }, "engines": { - "node": ">=0.10" + "node": ">=12" } }, - "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, + "node_modules/posthtml-render": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/posthtml-render/-/posthtml-render-3.0.0.tgz", + "integrity": "sha512-z+16RoxK3fUPgwaIgH9NGnK1HKY9XIDpydky5eQGgAFVXTCSezalv9U2jQuNV+Z9qV1fDWNzldcw4eK0SSbqKA==", "dependencies": { - "estraverse": "^5.2.0" + "is-json": "^2.0.1" }, "engines": { - "node": ">=4.0" + "node": ">=12" } }, - "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, + "node_modules/posthtml/node_modules/posthtml-parser": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/posthtml-parser/-/posthtml-parser-0.11.0.tgz", + "integrity": "sha512-QecJtfLekJbWVo/dMAA+OSwY79wpRmbqS5TeXvXSX+f0c6pW4/SE6inzZ2qkU7oAMCPqIDkZDvd/bQsSFUnKyw==", + "dependencies": { + "htmlparser2": "^7.1.1" + }, "engines": { - "node": ">=4.0" + "node": ">=12" } }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "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, "engines": { - "node": ">=0.10.0" + "node": ">= 0.8.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 - }, - "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true, - "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.4" + "bin": { + "prettier": "bin-prettier.js" }, "engines": { - "node": ">=8.6.0" + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "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, + "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==", "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" } }, - "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 + "node_modules/prop-types/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==" }, - "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 + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, - "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "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, - "dependencies": { - "reusify": "^1.0.4" + "engines": { + "node": ">=6" } }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "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" + } + ] + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dependencies": { - "flat-cache": "^3.0.4" + "loose-envify": "^1.1.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=0.10.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, + "node_modules/react-color": { + "version": "2.19.3", + "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz", + "integrity": "sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==", "dependencies": { - "to-regex-range": "^5.0.1" + "@icons/material": "^0.2.4", + "lodash": "^4.17.15", + "lodash-es": "^4.17.15", + "material-colors": "^1.2.1", + "prop-types": "^15.5.10", + "reactcss": "^1.2.0", + "tinycolor2": "^1.4.1" }, - "engines": { - "node": ">=8" + "peerDependencies": { + "react": "*" } }, - "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, + "node_modules/react-cookie": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-4.1.1.tgz", + "integrity": "sha512-ffn7Y7G4bXiFbnE+dKhHhbP+b8I34mH9jqnm8Llhj89zF4nPxPutxHT1suUqMeCEhLDBI7InYwf1tpaSoK5w8A==", "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" + "@types/hoist-non-react-statics": "^3.0.1", + "hoist-non-react-statics": "^3.0.0", + "universal-cookie": "^4.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "react": ">= 16.3.0" } }, - "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, + "node_modules/react-datepicker": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.25.0.tgz", + "integrity": "sha512-zB7CSi44SJ0sqo8hUQ3BF1saE/knn7u25qEMTO1CQGofY1VAKahO8k9drZtp0cfW1DMfoYLR3uSY1/uMvbEzbg==", "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" + "@popperjs/core": "^2.11.8", + "classnames": "^2.2.6", + "date-fns": "^2.30.0", + "prop-types": "^15.7.2", + "react-onclickoutside": "^6.13.0", + "react-popper": "^2.3.0" }, - "engines": { - "node": "^10.12.0 || >=12.0.0" + "peerDependencies": { + "react": "^16.9.0 || ^17 || ^18", + "react-dom": "^16.9.0 || ^17 || ^18" } }, - "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true - }, - "node_modules/foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", - "dev": true, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "peerDependencies": { + "react": "^18.3.1" } }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "dev": true, + "node_modules/react-error-overlay": { + "version": "6.0.9", + "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz", + "integrity": "sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==" + }, + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==" + }, + "node_modules/react-hook-form": { + "version": "7.51.5", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.5.tgz", + "integrity": "sha512-J2ILT5gWx1XUIJRETiA7M19iXHlG74+6O3KApzvqB/w8S5NQR7AbU8HVZrMALdmDgWpRPYiZJl0zx8Z4L2mP6Q==", "engines": { - "node": "*" + "node": ">=12.22.0" }, "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "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, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node_modules/react-i18next": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-12.3.1.tgz", + "integrity": "sha512-5v8E2XjZDFzK7K87eSwC7AJcAkcLt5xYZ4+yTPDAW1i7C93oOY1dnr4BaQM7un4Hm+GmghuiPvevWwlca5PwDA==", + "dependencies": { + "@babel/runtime": "^7.20.6", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 19.0.0", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } } }, - "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, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node_modules/react-icons": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.12.0.tgz", + "integrity": "sha512-IBaDuHiShdZqmfc/TwHu6+d6k2ltNCf3AszxNmjJc1KUfXdEeRJOKyNvLmAHaarhzGmTSVygNdyu8/opXv2gaw==", + "peerDependencies": { + "react": "*" } }, - "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, - "engines": { - "node": ">=6.9.0" + "node_modules/react-idle-timer": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/react-idle-timer/-/react-idle-timer-5.7.2.tgz", + "integrity": "sha512-+BaPfc7XEUU5JFkwZCx6fO1bLVK+RBlFH+iY4X34urvIzZiZINP6v2orePx3E6pAztJGE7t4DzvL7if2SL/0GQ==", + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" } }, - "node_modules/get-nonce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", - "engines": { - "node": ">=6" - } + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "node_modules/react-modal": { + "version": "3.16.1", + "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.16.1.tgz", + "integrity": "sha512-VStHgI3BVcGo7OXczvnJN7yT2TWHJPDXZWyI/a0ssFNhGZWsPmB8cF0z33ewDXq4VfYMO1vXgiv/g8Nj9NDyWg==", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "exenv": "^1.2.0", + "prop-types": "^15.7.2", + "react-lifecycles-compat": "^3.0.0", + "warning": "^4.0.3" }, "engines": { - "node": "*" + "node": ">=8" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "peerDependencies": { + "react": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18", + "react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18" } }, - "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, - "dependencies": { - "is-glob": "^4.0.3" + "node_modules/react-onclickoutside": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.13.1.tgz", + "integrity": "sha512-LdrrxK/Yh9zbBQdFbMTXPp3dTSN9B+9YJQucdDu3JNKRrbdU+H+/TVONJoWtOwy4II8Sqf1y/DTI6w/vGPYW0w==", + "funding": { + "type": "individual", + "url": "https://github.com/Pomax/react-onclickoutside/blob/master/FUNDING.md" }, - "engines": { - "node": ">=10.13.0" + "peerDependencies": { + "react": "^15.5.x || ^16.x || ^17.x || ^18.x", + "react-dom": "^15.5.x || ^16.x || ^17.x || ^18.x" } }, - "node_modules/glob/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, + "node_modules/react-popper": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz", + "integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "react-fast-compare": "^3.0.1", + "warning": "^4.0.2" + }, + "peerDependencies": { + "@popperjs/core": "^2.0.0", + "react": "^16.8.0 || ^17 || ^18", + "react-dom": "^16.8.0 || ^17 || ^18" } }, - "node_modules/glob/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, + "node_modules/react-redux": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz", + "integrity": "sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==", "dependencies": { - "brace-expansion": "^1.1.7" + "@babel/runtime": "^7.12.1", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/use-sync-external-store": "^0.0.3", + "hoist-non-react-statics": "^3.3.2", + "react-is": "^18.0.0", + "use-sync-external-store": "^1.0.0" }, - "engines": { - "node": "*" + "peerDependencies": { + "@types/react": "^16.8 || ^17.0 || ^18.0", + "@types/react-dom": "^16.8 || ^17.0 || ^18.0", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0", + "react-native": ">=0.59", + "redux": "^4 || ^5.0.0-beta.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "redux": { + "optional": true + } } }, - "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==", + "node_modules/react-redux/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + }, + "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, "engines": { - "node": ">=4" + "node": ">=0.10.0" } }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, + "node_modules/react-remove-scroll": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", + "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==", "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" + "react-remove-scroll-bar": "^2.3.3", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" }, "engines": { "node": ">=10" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz", + "integrity": "sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==", "dependencies": { - "function-bind": "^1.1.2" + "react-style-singleton": "^2.2.1", + "tslib": "^2.0.0" }, "engines": { - "node": ">= 0.4" - } - }, - "node_modules/html-parse-stringify": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", - "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", - "dependencies": { - "void-elements": "3.1.0" - } - }, - "node_modules/i18next": { - "version": "23.11.5", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.11.5.tgz", - "integrity": "sha512-41pvpVbW9rhZPk5xjCX2TPJi2861LEig/YRhUkY+1FQ2IQPS0bKUDYnEqY8XPPbB48h1uIwLnP9iiEfuSl20CA==", - "funding": [ - { - "type": "individual", - "url": "https://locize.com" - }, - { - "type": "individual", - "url": "https://locize.com/i18next.html" - }, - { - "type": "individual", - "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true } - ], - "dependencies": { - "@babel/runtime": "^7.23.2" } }, - "node_modules/i18next-browser-languagedetector": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.0.tgz", - "integrity": "sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==", + "node_modules/react-router": { + "version": "6.23.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.23.1.tgz", + "integrity": "sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ==", "dependencies": { - "@babel/runtime": "^7.23.2" - } - }, - "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", - "dev": true, + "@remix-run/router": "1.16.1" + }, "engines": { - "node": ">= 4" + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" } }, - "node_modules/immutable": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz", - "integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==", - "dev": true - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, + "node_modules/react-router-dom": { + "version": "6.23.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.23.1.tgz", + "integrity": "sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ==", "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" + "@remix-run/router": "1.16.1", + "react-router": "6.23.1" }, "engines": { - "node": ">=6" + "node": ">=14.0.0" }, - "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, - "engines": { - "node": ">=0.8.19" + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, + "node_modules/react-select": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.0.tgz", + "integrity": "sha512-TfjLDo58XrhP6VG5M/Mi56Us0Yt8X7xD6cDybC7yoRMUNm7BGO7qk8J0TLQOua/prb8vUOtsfnXZwfm30HGsAA==", "dependencies": { - "once": "^1.3.0", - "wrappy": "1" + "@babel/runtime": "^7.12.0", + "@emotion/cache": "^11.4.0", + "@emotion/react": "^11.8.1", + "@floating-ui/dom": "^1.0.1", + "@types/react-transition-group": "^4.4.0", + "memoize-one": "^6.0.0", + "prop-types": "^15.6.0", + "react-transition-group": "^4.3.0", + "use-isomorphic-layout-effect": "^1.1.2" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "node_modules/invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "node_modules/react-style-singleton": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", + "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==", + "dependencies": { + "get-nonce": "^1.0.0", + "invariant": "^2.2.4", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-text-selection-popover": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/react-text-selection-popover/-/react-text-selection-popover-2.0.2.tgz", + "integrity": "sha512-VbQnJMHX6GrMRS5QGQnb8YuFL45JRcosraTJjdmjib4Xt9MOcTHXmuIyI12xbG2QZv2Tsa+aOZvYgTlo8I00dA==", "dependencies": { - "loose-envify": "^1.0.0" + "use-text-selection": "^1.1.3" + }, + "peerDependencies": { + "react": "^16.8.0,^17.x,^18.x", + "react-dom": "^16.8.0,^17.x,^18.x" } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, + "node_modules/react-textarea-autosize": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.3.tgz", + "integrity": "sha512-XT1024o2pqCuZSuBt9FwHlaDeNtVrtCXu0Rnz88t1jUGheCLa3PhjE1GH8Ctm2axEtvdCl5SUHYschyQ0L5QHQ==", "dependencies": { - "binary-extensions": "^2.0.0" + "@babel/runtime": "^7.20.13", + "use-composed-ref": "^1.3.0", + "use-latest": "^1.2.1" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, - "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", - "dev": true, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", "dependencies": { - "hasown": "^2.0.0" + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" } }, - "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, - "engines": { - "node": ">=0.10.0" + "node_modules/reactcss": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", + "integrity": "sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==", + "dependencies": { + "lodash": "^4.0.1" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" + "node_modules/reactflow": { + "version": "11.11.3", + "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.3.tgz", + "integrity": "sha512-wusd1Xpn1wgsSEv7UIa4NNraCwH9syBtubBy4xVNXg3b+CDKM+sFaF3hnMx0tr0et4km9urIDdNvwm34QiZong==", + "dependencies": { + "@reactflow/background": "11.3.13", + "@reactflow/controls": "11.2.13", + "@reactflow/core": "11.11.3", + "@reactflow/minimap": "11.7.13", + "@reactflow/node-resizer": "2.2.13", + "@reactflow/node-toolbar": "1.3.13" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" } }, - "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==", + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, "dependencies": { - "is-extglob": "^2.1.1" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">= 6" } }, - "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==", + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, "engines": { - "node": ">=0.12.0" + "node": ">=8.10.0" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "node_modules/reflect.getprototypeof": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", + "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==", "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.1", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "globalthis": "^1.0.3", + "which-builtin-type": "^1.1.3" + }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", "dev": true }, - "node_modules/jackspeak": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.0.tgz", - "integrity": "sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==", + "node_modules/regenerate-unicode-properties": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", + "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", "dev": true, "dependencies": { - "@isaacs/cliui": "^8.0.2" + "regenerate": "^1.4.2" }, "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" + "node": ">=4" } }, - "node_modules/jiti": { - "version": "1.21.6", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", - "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", "dev": true, - "bin": { - "jiti": "bin/jiti.js" + "dependencies": { + "@babel/runtime": "^7.8.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==" + "node_modules/regexify-string": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/regexify-string/-/regexify-string-1.0.19.tgz", + "integrity": "sha512-EREOggl31J6v2Hk3ksPuOof0DMq5QhFfVQ7iDaGQ6BeA1QcrV4rhGvwCES5a72ITMmLBDAOb6cOWbn8/Ja82Ig==" }, - "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==", + "node_modules/regexp.prototype.flags": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", "dev": true, "dependencies": { - "argparse": "^2.0.1" + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "node_modules/regexpu-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", "dev": true, - "bin": { - "jsesc": "bin/jsesc" + "dependencies": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" }, "engines": { "node": ">=4" } }, - "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 - }, - "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 - }, - "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 - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "node_modules/regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", "dev": true, - "bin": { - "json5": "lib/cli.js" + "dependencies": { + "jsesc": "~0.5.0" }, - "engines": { - "node": ">=6" + "bin": { + "regjsparser": "bin/parser" } }, - "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==", + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", "dev": true, - "dependencies": { - "json-buffer": "3.0.1" + "bin": { + "jsesc": "bin/jsesc" } }, - "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==", + "node_modules/remove-accents": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz", + "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, "engines": { - "node": ">= 0.8.0" + "node": ">=0.10.0" } }, - "node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "node_modules/requireindex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.1.0.tgz", + "integrity": "sha512-LBnkqsDE7BZKvqylbmn7lTIVdpx4K/QCduRATpO5R+wtPmky/a8pN1bO2D6wXppn1497AJF9mNjqAXr6bdl9jg==", "dev": true, "engines": { - "node": ">=10" + "node": ">=0.10.5" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true - }, - "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, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dependencies": { - "p-locate": "^5.0.0" + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" }, - "engines": { - "node": ">=10" + "bin": { + "resolve": "bin/resolve" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "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 - }, - "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==", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" + "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==", + "engines": { + "node": ">=4" } }, - "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==", + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", "dev": true, "dependencies": { - "yallist": "^3.0.2" - } - }, - "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, + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, "engines": { - "node": ">= 8" + "node": ">=8" } }, - "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "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, - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, "engines": { - "node": ">=8.6" + "iojs": ">=1.0.0", + "node": ">=0.10.0" } }, - "node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "dependencies": { - "brace-expansion": "^2.0.1" + "glob": "^7.1.3" }, - "engines": { - "node": ">=16 || 14 >=14.17" + "bin": { + "rimraf": "bin.js" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "node_modules/rollup": { + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" } }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", "dev": true, - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" + "engines": { + "node": ">=0.12.0" } }, - "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "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/ai" + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" } ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "dependencies": { + "queue-microtask": "^1.2.2" } }, - "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 - }, - "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", - "dev": true - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "engines": { - "node": ">=0.10.0" + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dependencies": { + "tslib": "^2.1.0" } }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "node_modules/safe-array-concat": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, "engines": { - "node": ">=0.10.0" + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-regex-test": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" + }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/sass": { + "version": "1.77.5", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.5.tgz", + "integrity": "sha512-oDfX1mukIlxacPdQqNb6mV2tVCrnE+P3nVYioy72V5tlk56CPNcO4TCuFcaCRKKfJ1M3lH95CleRS+dVKL2qMg==", "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, "engines": { - "node": ">= 6" + "node": ">=14.0.0" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "dependencies": { - "wrappy": "1" + "loose-envify": "^1.1.0" } }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "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, - "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" + "bin": { + "semver": "bin/semver.js" } }, - "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==", + "node_modules/set-cookie-parser": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", + "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==", + "dev": true + }, + "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, "dependencies": { - "yocto-queue": "^0.1.0" + "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": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.4" } }, - "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==", + "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, "dependencies": { - "p-limit": "^3.0.2" + "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": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.4" } }, - "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==", + "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, "dependencies": { - "callsites": "^3.0.0" + "shebang-regex": "^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, "engines": { "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "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==", + "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, "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 - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dev": true, "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" }, "engines": { - "node": ">=16 || 14 >=14.18" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", - "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", - "dev": true, - "engines": { - "node": "14 || >=16.14" - } + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, "engines": { "node": ">=8" } }, - "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", - "dev": true - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "node": ">=0.10.0" } }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", - "dev": true, - "engines": { - "node": ">= 6" - } + "node_modules/srcset": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/srcset/-/srcset-4.0.0.tgz", + "integrity": "sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", + "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility" }, - "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "node_modules/strict-event-emitter": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.4.6.tgz", + "integrity": "sha512-12KWeb+wixJohmnwNFerbyiBrAlq5qJLwIt38etRtKtmmHyDSoGlIqFE9wx+4IwG0aDjI7GV8tc8ZccjWZZtTg==", + "dev": true + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "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" - } - ], "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14" + "safe-buffer": "~5.2.0" } }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "node_modules/string-natural-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/string-natural-compare/-/string-natural-compare-3.0.1.tgz", + "integrity": "sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==", + "dev": true + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" + "node": ">=8" } }, - "node_modules/postcss-js": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", - "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "node_modules/string-width/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", + "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==", "dev": true, "dependencies": { - "camelcase-css": "^2.0.1" + "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", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.7", + "regexp.prototype.flags": "^1.5.2", + "set-function-name": "^2.0.2", + "side-channel": "^1.0.6" }, "engines": { - "node": "^12 || ^14 || >= 16" + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.4.21" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/postcss-load-config": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", - "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "node_modules/string.prototype.trim": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "dependencies": { - "lilconfig": "^3.0.0", - "yaml": "^2.3.4" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" }, "engines": { - "node": ">= 14" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/postcss-load-config/node_modules/lilconfig": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", - "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", + "node_modules/string.prototype.trimend": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", "dev": true, - "engines": { - "node": ">=14" + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "funding": { - "url": "https://github.com/sponsors/antonk52" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/postcss-nested": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", - "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "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, "dependencies": { - "postcss-selector-parser": "^6.0.11" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "engines": { - "node": ">=12.0" + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.2.14" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/postcss-selector-parser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz", - "integrity": "sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==", + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=4" + "node": ">=8" } }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "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==", + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true, "engines": { - "node": ">= 0.8.0" + "node": ">=4" } }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "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, "engines": { - "node": ">=6" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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" - } - ] + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" }, - "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dependencies": { - "loose-envify": "^1.1.0" + "has-flag": "^3.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=4" } }, - "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" + "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==", + "engines": { + "node": ">= 0.4" }, - "peerDependencies": { - "react": "^18.3.1" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/react-i18next": { - "version": "14.1.2", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.1.2.tgz", - "integrity": "sha512-FSIcJy6oauJbGEXfhUgVeLzvWBhIBIS+/9c6Lj4niwKZyGaGb4V4vUbATXSlsHJDXXB+ociNxqFNiFuV1gmoqg==", + "node_modules/svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", + "dev": true + }, + "node_modules/svgo": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", + "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", "dependencies": { - "@babel/runtime": "^7.23.9", - "html-parse-stringify": "^3.0.1" + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^4.1.3", + "css-tree": "^1.1.3", + "csso": "^4.2.0", + "picocolors": "^1.0.0", + "stable": "^0.1.8" }, - "peerDependencies": { - "i18next": ">= 23.2.3", - "react": ">= 16.8.0" + "bin": { + "svgo": "bin/svgo" }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - } + "engines": { + "node": ">=10.13.0" } }, - "node_modules/react-icons": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.12.0.tgz", - "integrity": "sha512-IBaDuHiShdZqmfc/TwHu6+d6k2ltNCf3AszxNmjJc1KUfXdEeRJOKyNvLmAHaarhzGmTSVygNdyu8/opXv2gaw==", - "peerDependencies": { - "react": "*" + "node_modules/term-size": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", + "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/timeago.js": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/timeago.js/-/timeago.js-4.0.2.tgz", + "integrity": "sha512-a7wPxPdVlQL7lqvitHGGRsofhdwtkoSXPGATFuSOA2i1ZNQEPLrGnj68vOp2sOJTCFAQVXPeNMX/GctBaO9L2w==" + }, + "node_modules/timsort": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", + "integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==" + }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==" + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, "engines": { - "node": ">=0.10.0" + "node": ">=0.6.0" } }, - "node_modules/react-remove-scroll": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", - "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==", + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" + } + }, + "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==", "dependencies": { - "react-remove-scroll-bar": "^2.3.3", - "react-style-singleton": "^2.2.1", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.0", - "use-sidecar": "^1.1.2" + "is-number": "^7.0.0" }, "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "node": ">=8.0" } }, - "node_modules/react-remove-scroll-bar": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz", - "integrity": "sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==", - "dependencies": { - "react-style-singleton": "^2.2.1", - "tslib": "^2.0.0" + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "node_modules/tsconfck": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.0.tgz", + "integrity": "sha512-CMjc5zMnyAjcS9sPLytrbFmj89st2g+JYtY/c02ug4Q+CZaAtCgbyviI0n1YvjZE/pzoc6FbNsINS13DOL1B9w==", + "dev": true, + "bin": { + "tsconfck": "bin/tsconfck.js" }, "engines": { - "node": ">=10" + "node": "^18 || >=20" }, "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "typescript": "^5.0.0" }, "peerDependenciesMeta": { - "@types/react": { + "typescript": { "optional": true } } }, - "node_modules/react-router": { - "version": "6.23.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.23.1.tgz", - "integrity": "sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ==", + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, "dependencies": { - "@remix-run/router": "1.16.1" - }, - "engines": { - "node": ">=14.0.0" + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" }, - "peerDependencies": { - "react": ">=16.8" + "bin": { + "json5": "lib/cli.js" } }, - "node_modules/react-router-dom": { - "version": "6.23.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.23.1.tgz", - "integrity": "sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ==", + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, "dependencies": { - "@remix-run/router": "1.16.1", - "react-router": "6.23.1" + "tslib": "^1.8.1" }, "engines": { - "node": ">=14.0.0" + "node": ">= 6" }, "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" } }, - "node_modules/react-style-singleton": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", - "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==", + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "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, "dependencies": { - "get-nonce": "^1.0.0", - "invariant": "^2.2.4", - "tslib": "^2.0.0" + "prelude-ls": "^1.2.1" }, "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "engines": { + "node": ">=12.20" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "node_modules/typed-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", "dev": true, "dependencies": { - "pify": "^2.3.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" } }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "node_modules/typed-array-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", "dev": true, "dependencies": { - "picomatch": "^2.2.1" + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" }, "engines": { - "node": ">=8.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" - }, - "node_modules/remove-accents": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz", - "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==" - }, - "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "node_modules/typed-array-byte-offset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", "dev": true, "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" }, - "bin": { - "resolve": "bin/resolve" + "engines": { + "node": ">= 0.4" }, "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==", + "node_modules/typed-array-length": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, "engines": { - "node": ">=4" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" + "node": ">=4.2.0" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", "dev": true, "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/rollup": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz", - "integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==", + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", "dev": true, "dependencies": { - "@types/estree": "1.0.5" - }, - "bin": { - "rollup": "dist/bin/rollup" + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.18.0", - "@rollup/rollup-android-arm64": "4.18.0", - "@rollup/rollup-darwin-arm64": "4.18.0", - "@rollup/rollup-darwin-x64": "4.18.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.18.0", - "@rollup/rollup-linux-arm-musleabihf": "4.18.0", - "@rollup/rollup-linux-arm64-gnu": "4.18.0", - "@rollup/rollup-linux-arm64-musl": "4.18.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.18.0", - "@rollup/rollup-linux-riscv64-gnu": "4.18.0", - "@rollup/rollup-linux-s390x-gnu": "4.18.0", - "@rollup/rollup-linux-x64-gnu": "4.18.0", - "@rollup/rollup-linux-x64-musl": "4.18.0", - "@rollup/rollup-win32-arm64-msvc": "4.18.0", - "@rollup/rollup-win32-ia32-msvc": "4.18.0", - "@rollup/rollup-win32-x64-msvc": "4.18.0", - "fsevents": "~2.3.2" + "node": ">=4" } }, - "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==", + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/universal-cookie": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-4.0.4.tgz", + "integrity": "sha512-lbRVHoOMtItjWbM7TwDLdl8wug7izB0tq3/YVKhT/ahB4VDvWMyvnADfnJI8y6fSvsjh51Ix7lTGC6Tn4rMPhw==", + "dependencies": { + "@types/cookie": "^0.3.3", + "cookie": "^0.4.0" + } + }, + "node_modules/universal-cookie/node_modules/@types/cookie": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz", + "integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==" + }, + "node_modules/update-browserslist-db": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", + "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", "funding": [ { - "type": "github", - "url": "https://github.com/sponsors/feross" + "type": "opencollective", + "url": "https://opencollective.com/browserslist" }, { - "type": "patreon", - "url": "https://www.patreon.com/feross" + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" }, { - "type": "consulting", - "url": "https://feross.org/support" + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.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, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz", + "integrity": "sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-composed-ref": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.3.0.tgz", + "integrity": "sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", + "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true } - ], - "dependencies": { - "queue-microtask": "^1.2.2" } }, - "node_modules/sass": { - "version": "1.77.4", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.4.tgz", - "integrity": "sha512-vcF3Ckow6g939GMA4PeU7b2K/9FALXk2KF9J87txdHzXbUF9XRQRwSxcAs/fGaTnJeBFd7UoV22j3lzMLdM0Pw==", - "dev": true, + "node_modules/use-latest": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.2.1.tgz", + "integrity": "sha512-xA+AVm/Wlg3e2P/JiItTziwS7FK92LWrDB0p+hgXloIMuVCeJJ8v6f0eeHyPZaJrM+usM1FkFfbNCrJGs8A/zw==", "dependencies": { - "chokidar": ">=3.0.0 <4.0.0", - "immutable": "^4.0.0", - "source-map-js": ">=0.6.2 <2.0.0" + "use-isomorphic-layout-effect": "^1.1.1" }, - "bin": { - "sass": "sass.js" + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" }, - "engines": { - "node": ">=14.0.0" + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "node_modules/use-sidecar": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", + "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==", "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", - "dev": true, - "bin": { - "semver": "bin/semver.js" + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" }, "engines": { "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "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, + "node_modules/use-sync-external-store": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", + "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/use-text-selection": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/use-text-selection/-/use-text-selection-1.1.5.tgz", + "integrity": "sha512-JOuQYG0vKHRj0dfax0dy/HxyF31MN0Q2UP1rl1LtFA0qnQ0Uw4XGh4BucHA9g8kxlnVFv+JTlJQ4B+TwXCGxOg==", "dependencies": { - "shebang-regex": "^3.0.0" + "parcel": "^2.0.0-beta.2" }, - "engines": { - "node": ">=8" + "peerDependencies": { + "react": "^17.0.1" } }, - "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, + "node_modules/usehooks-ts": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-2.16.0.tgz", + "integrity": "sha512-bez95WqYujxp6hFdM/CpRDiVPirZPxlMzOH2QB8yopoKQMXpscyZoxOjpEdaxvV+CAWUDSM62cWnqHE0E/MZ7w==", + "dependencies": { + "lodash.debounce": "^4.0.8" + }, "engines": { - "node": ">=8" + "node": ">=16.15.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" } }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" } }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", "engines": { - "node": ">=8" + "node": ">= 4" } }, - "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", - "dev": true, - "engines": { - "node": ">=0.10.0" + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" } }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "node_modules/vite": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", + "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", "dev": true, "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" }, "engines": { - "node": ">=12" + "node": "^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } } }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/vite-plugin-env-compatible": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/vite-plugin-env-compatible/-/vite-plugin-env-compatible-1.1.1.tgz", + "integrity": "sha512-4lqhBWhOzP+SaCPoCVdmpM5cXzjKQV5jgFauxea488oOeElXo/kw6bXkMIooZhrh9q7gclTl8en6N9NmnqUwRQ==", + "dev": true + }, + "node_modules/vite-plugin-svgr": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-2.4.0.tgz", + "integrity": "sha512-q+mJJol6ThvqkkJvvVFEndI4EaKIjSI0I3jNFgSoC9fXAz1M7kYTVUin8fhUsFojFDKZ9VHKtX6NXNaOLpbsHA==", "dev": true, "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "@rollup/pluginutils": "^5.0.2", + "@svgr/core": "^6.5.1" }, - "engines": { - "node": ">=8" + "peerDependencies": { + "vite": "^2.6.0 || 3 || 4" } }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "node_modules/vite-plugin-transform": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/vite-plugin-transform/-/vite-plugin-transform-2.0.1.tgz", + "integrity": "sha512-sI9SzcuFbCj04YHEmhw9C14kNnVq3QFLWq7eofjNnDWnw/p+i+6pnSvVZSx1GDVpW1ciZglrv794XEU/lGGvyA==", "dev": true }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "node_modules/vite-tsconfig-paths": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz", + "integrity": "sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==", "dev": true, - "engines": { - "node": ">=12" + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "peerDependencies": { + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } } }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, + "optional": true, + "os": [ + "android" + ], "engines": { "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=8" + "node": ">=12" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=8" + "node": ">=12" } }, - "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==", + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], "dev": true, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=12" } }, - "node_modules/sucrase": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", - "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "glob": "^10.3.10", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=12" } }, - "node_modules/sucrase/node_modules/glob": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", - "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=12" } }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=4" + "node": ">=12" } }, - "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==", + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], "dev": true, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=12" } }, - "node_modules/tailwindcss": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz", - "integrity": "sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==", + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], "dev": true, - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.5.3", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.0", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.0", - "lilconfig": "^2.1.0", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.23", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.1", - "postcss-nested": "^6.0.1", - "postcss-selector-parser": "^6.0.11", - "resolve": "^1.22.2", - "sucrase": "^3.32.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=14.0.0" + "node": ">=12" } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], "dev": true, - "dependencies": { - "any-promise": "^1.0.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], "dev": true, - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=0.8" + "node": ">=12" } }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], "dev": true, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=4" + "node": ">=12" } }, - "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==", + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8.0" + "node": ">=12" } }, - "node_modules/ts-api-utils": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", - "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], "dev": true, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=16" - }, - "peerDependencies": { - "typescript": ">=4.2.0" + "node": ">=12" } }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true - }, - "node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" - }, - "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==", + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 0.8.0" + "node": ">=12" } }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], "dev": true, + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=12" } }, - "node_modules/typescript": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=14.17" + "node": ">=12" } }, - "node_modules/update-browserslist-db": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", - "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], "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" - } + "optional": true, + "os": [ + "sunos" ], - "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" + "engines": { + "node": ">=12" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/use-callback-ref": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz", - "integrity": "sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" } }, - "node_modules/use-sidecar": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", - "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "node": ">=12" } }, - "node_modules/use-sync-external-store": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", - "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true - }, - "node_modules/vite": { - "version": "5.2.12", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.12.tgz", - "integrity": "sha512-/gC8GxzxMK5ntBwb48pR32GGhENnjtY30G4A0jemunsBkiEZFw60s8InGpN8gkhHEkjnRK1aSAxeQgwvFhUHAA==", + "node_modules/vite/node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", "dev": true, - "dependencies": { - "esbuild": "^0.20.1", - "postcss": "^8.4.38", - "rollup": "^4.13.0" - }, + "hasInstallScript": true, "bin": { - "vite": "bin/vite.js" + "esbuild": "bin/esbuild" }, "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" + "node": ">=12" }, "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" } }, "node_modules/void-elements": { @@ -6094,6 +14490,56 @@ "node": ">=0.10.0" } }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/weak-lru-cache": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", + "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==" + }, + "node_modules/web-encoding": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/web-encoding/-/web-encoding-1.1.5.tgz", + "integrity": "sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==", + "dev": true, + "dependencies": { + "util": "^0.12.3" + }, + "optionalDependencies": { + "@zxing/text-encoding": "0.9.0" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6109,37 +14555,98 @@ "node": ">= 8" } }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz", + "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==", + "dev": true, + "dependencies": { + "function.prototype.name": "^1.1.5", + "has-tostringtag": "^1.0.0", + "is-async-function": "^2.0.0", + "is-date-object": "^1.0.5", + "is-finalizationregistry": "^1.0.2", + "is-generator-function": "^1.0.10", + "is-regex": "^1.1.4", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "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, "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" }, "engines": { - "node": ">=12" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "node_modules/which-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "dev": true, "dependencies": { "ansi-styles": "^4.0.0", @@ -6147,13 +14654,10 @@ "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">=8" } }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "node_modules/wrap-ansi/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==", @@ -6168,7 +14672,7 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "node_modules/wrap-ansi/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==", @@ -6180,93 +14684,66 @@ "node": ">=7.0.0" } }, - "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "node_modules/wrap-ansi/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 }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, "engines": { - "node": ">=8" + "node": ">=10" } }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">= 6" } }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "dependencies": { - "ansi-regex": "^6.0.1" + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" }, "engines": { "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - }, - "node_modules/yaml": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz", - "integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==", + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, - "bin": { - "yaml": "bin.mjs" - }, "engines": { - "node": ">= 14" + "node": ">=12" } }, "node_modules/yocto-queue": { @@ -6280,6 +14757,41 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zustand": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.2.tgz", + "integrity": "sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==", + "dependencies": { + "use-sync-external-store": "1.2.0" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/zustand/node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } } } } diff --git a/GUI/package.json b/GUI/package.json index a2c5598a..99897b07 100644 --- a/GUI/package.json +++ b/GUI/package.json @@ -1,18 +1,22 @@ { - "name": "est-gov-classifier", + "name": "byk-training-module-gui", "private": true, "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite", + "dev": "vite --port 3001 --host", "build": "tsc && vite build", - "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", - "preview": "vite preview" + "preview": "vite preview", + "lint": "tsc --noEmit && eslint \"./src/**/*.{js,ts,tsx}\"", + "prettier": "prettier --write \"{,!(node_modules)/**/}*.{ts,tsx,js,json,css,less,scss}\"" }, "dependencies": { "@buerokratt-ria/header": "^0.1.6", - "@buerokratt-ria/menu": "^0.1.15", + "@buerokratt-ria/menu": "^0.1.11", "@buerokratt-ria/styles": "^0.0.1", + "@fontsource/roboto": "^4.5.8", + "@formkit/auto-animate": "^1.0.0-beta.5", + "@fortaine/fetch-event-source": "^3.0.6", "@radix-ui/react-accessible-icon": "^1.0.1", "@radix-ui/react-collapsible": "^1.0.1", "@radix-ui/react-dialog": "^1.0.2", @@ -21,34 +25,84 @@ "@radix-ui/react-switch": "^1.0.1", "@radix-ui/react-tabs": "^1.0.1", "@radix-ui/react-toast": "^1.1.2", - "@radix-ui/themes": "^3.0.5", + "@radix-ui/react-tooltip": "^1.0.2", + "@tanstack/match-sorter-utils": "^8.7.2", "@tanstack/react-query": "^4.20.4", "@tanstack/react-table": "^8.7.4", - "@tanstack/match-sorter-utils": "^8.7.2", - "clsx": "^2.1.1", - "i18next": "^23.11.5", - "i18next-browser-languagedetector": "^8.0.0", + "axios": "^1.2.1", + "clsx": "^1.2.1", + "date-fns": "^2.29.3", + "downshift": "^7.0.5", + "esbuild": "^0.19.5", + "framer-motion": "^8.5.5", + "howler": "^2.2.4", + "i18next": "^22.4.5", + "i18next-browser-languagedetector": "^7.0.1", + "linkify-react": "^4.1.1", + "linkifyjs": "^4.1.1", + "lodash": "^4.17.21", "react": "^18.2.0", + "react-color": "^2.19.3", + "react-cookie": "^4.1.1", + "react-datepicker": "^4.8.0", "react-dom": "^18.2.0", - "react-i18next": "^14.1.2", + "react-hook-form": "^7.41.5", + "react-i18next": "^12.1.1", "react-icons": "^4.10.1", - "react-router-dom": "^6.23.1" + "react-idle-timer": "^5.5.2", + "react-modal": "^3.16.1", + "react-redux": "^8.1.1", + "react-router-dom": "^6.5.0", + "react-select": "^5.7.4", + "react-text-selection-popover": "^2.0.2", + "react-textarea-autosize": "^8.4.0", + "reactflow": "^11.4.0", + "regexify-string": "^1.0.19", + "rxjs": "^7.8.1", + "timeago.js": "^4.0.2", + "usehooks-ts": "^2.9.1", + "uuid": "^9.0.0", + "zustand": "^4.4.4" }, "devDependencies": { - "@types/react": "^18.2.66", - "@types/react-dom": "^18.2.22", - "@types/react-i18next": "^8.1.0", - "@typescript-eslint/eslint-plugin": "^7.2.0", - "@typescript-eslint/parser": "^7.2.0", - "@vitejs/plugin-react": "^4.2.1", - "autoprefixer": "^10.4.19", - "eslint": "^8.57.0", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.6", - "postcss": "^8.4.38", - "sass": "^1.77.4", - "tailwindcss": "^3.4.4", - "typescript": "^5.2.2", - "vite": "^5.2.0" + "@types/howler": "^2.2.11", + "@types/lodash": "^4.14.191", + "@types/lodash.debounce": "^4.0.7", + "@types/node": "^18.11.17", + "@types/react": "^18.0.26", + "@types/react-color": "^3.0.6", + "@types/react-datepicker": "^4.8.0", + "@types/react-dom": "^18.0.9", + "@types/uuid": "^9.0.2", + "@vitejs/plugin-react": "^3.0.0", + "eslint": "^8.30.0", + "eslint-config-react-app": "^7.0.1", + "eslint-plugin-react": "^7.31.11", + "eslint-plugin-typescript": "^0.14.0", + "mocksse": "^1.0.4", + "msw": "^0.49.2", + "prettier": "^2.8.1", + "sass": "^1.57.0", + "typescript": "^4.9.3", + "vite": "^4.0.0", + "vite-plugin-env-compatible": "^1.1.1", + "vite-plugin-svgr": "^2.4.0", + "vite-plugin-transform": "^2.0.1", + "vite-tsconfig-paths": "^4.0.3" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "msw": { + "workerDirectory": "public" } } diff --git a/GUI/postcss.config.js b/GUI/postcss.config.js deleted file mode 100644 index 2e7af2b7..00000000 --- a/GUI/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} diff --git a/GUI/public/favicon.ico b/GUI/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..b9d127c23fc0da8cd48683ec6dc4ebef733d30ca GIT binary patch literal 15406 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 { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +self.addEventListener('fetch', function (event) { + const { request } = event + const accept = request.headers.get('accept') || '' + + // Bypass server-sent events. + if (accept.includes('text/event-stream')) { + return + } + + // Bypass navigation requests. + if (request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been deleted (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + // Generate unique request ID. + const requestId = Math.random().toString(16).slice(2) + + event.respondWith( + handleRequest(event, requestId).catch((error) => { + if (error.name === 'NetworkError') { + console.warn( + '[MSW] Successfully emulated a network error for the "%s %s" request.', + request.method, + request.url, + ) + return + } + + // At this point, any exception indicates an issue with the original request/response. + console.error( + `\ +[MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`, + request.method, + request.url, + `${error.name}: ${error.message}`, + ) + }), + ) +}) + +async function handleRequest(event, requestId) { + const client = await resolveMainClient(event) + const response = await getResponse(event, client, requestId) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + ;(async function () { + const clonedResponse = response.clone() + sendToClient(client, { + type: 'RESPONSE', + payload: { + requestId, + type: clonedResponse.type, + ok: clonedResponse.ok, + status: clonedResponse.status, + statusText: clonedResponse.statusText, + body: + clonedResponse.body === null ? null : await clonedResponse.text(), + headers: Object.fromEntries(clonedResponse.headers.entries()), + redirected: clonedResponse.redirected, + }, + }) + })() + } + + return response +} + +// Resolve the main client for the given event. +// Client that issues a request doesn't necessarily equal the client +// that registered the worker. It's with the latter the worker should +// communicate with during the response resolving phase. +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +async function getResponse(event, client, requestId) { + const { request } = event + const clonedRequest = request.clone() + + function passthrough() { + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const headers = Object.fromEntries(clonedRequest.headers.entries()) + + // Remove MSW-specific request headers so the bypassed requests + // comply with the server's CORS preflight check. + // Operate with the headers as an object because request "Headers" + // are immutable. + delete headers['x-msw-bypass'] + + return fetch(clonedRequest, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Bypass requests with the explicit bypass header. + // Such requests can be issued by "ctx.fetch()". + if (request.headers.get('x-msw-bypass') === 'true') { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const clientMessage = await sendToClient(client, { + type: 'REQUEST', + payload: { + id: requestId, + url: request.url, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + mode: request.mode, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: await request.text(), + bodyUsed: request.bodyUsed, + keepalive: request.keepalive, + }, + }) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'MOCK_NOT_FOUND': { + return passthrough() + } + + case 'NETWORK_ERROR': { + const { name, message } = clientMessage.data + const networkError = new Error(message) + networkError.name = name + + // Rejecting a "respondWith" promise emulates a network error. + throw networkError + } + } + + return passthrough() +} + +function sendToClient(client, message) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage(message, [channel.port2]) + }) +} + +function sleep(timeMs) { + return new Promise((resolve) => { + setTimeout(resolve, timeMs) + }) +} + +async function respondWithMock(response) { + await sleep(response.delay) + return new Response(response.body, response) +} diff --git a/GUI/public/vite.svg b/GUI/public/vite.svg deleted file mode 100644 index e7b8dfb1..00000000 --- a/GUI/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/GUI/rebuild.sh b/GUI/rebuild.sh new file mode 100644 index 00000000..3ce9c0fa --- /dev/null +++ b/GUI/rebuild.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +# Install dependencies +apk add nodejs + +# Rebuild the project +cd /opt/buerokratt-chatbot +./node_modules/.bin/vite build -l warn +cp -ru build/* /usr/share/nginx/html/buerokratt-chatbot + +# Start the Nginx server +nginx -g "daemon off;" diff --git a/GUI/src/App.css b/GUI/src/App.css deleted file mode 100644 index c15efd46..00000000 --- a/GUI/src/App.css +++ /dev/null @@ -1,8 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - - diff --git a/GUI/src/App.tsx b/GUI/src/App.tsx index 2c61bcfd..46fc7037 100644 --- a/GUI/src/App.tsx +++ b/GUI/src/App.tsx @@ -1,18 +1,30 @@ -// src/App.js +import { FC } from 'react'; +import { Navigate, Route, Routes } from 'react-router-dom'; +import { useQuery } from '@tanstack/react-query'; -import { Navigate, Route, Routes } from "react-router-dom"; -import Layout from "./components/Layout"; -import Home from "./pages/Home"; -import UserManagement from "./pages/UserManagement"; +import { Layout } from 'components'; +import useStore from 'store'; +import { UserInfo } from 'types/userInfo'; + +import './locale/et_EE'; +import UserManagement from 'pages/UserManagement'; + +const App: FC = () => { + useQuery<{ + data: { custom_jwt_userinfo: UserInfo }; + }>({ + queryKey: ['auth/jwt/userinfo', 'prod'], + onSuccess: (res: { response: UserInfo }) => { + localStorage.setItem('exp', res.response.JWTExpirationTimestamp); + return useStore.getState().setUserInfo(res.response); + }, + }); -const App = () => { return ( }> - } /> - } /> + } /> } /> - ); diff --git a/GUI/src/assets/ding.mp3 b/GUI/src/assets/ding.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..af75c65a52ac4e99c8d3e26b688552fc6169259a GIT binary patch literal 26266 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 Y29KM7-PLT(Y|~;`1COGYUren3KSG!=ga7~l literal 0 HcmV?d00001 diff --git a/GUI/src/assets/logo-white.svg b/GUI/src/assets/logo-white.svg new file mode 100644 index 00000000..20257361 --- /dev/null +++ b/GUI/src/assets/logo-white.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + diff --git a/GUI/src/assets/logo.svg b/GUI/src/assets/logo.svg new file mode 100644 index 00000000..6039e9b5 --- /dev/null +++ b/GUI/src/assets/logo.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/GUI/src/assets/newMessageSound.mp3 b/GUI/src/assets/newMessageSound.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..9400b22a4e90a798f02bf1eebccb9355536fbe67 GIT binary patch literal 20942 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 literal 0 HcmV?d00001 diff --git a/GUI/src/assets/react.svg b/GUI/src/assets/react.svg deleted file mode 100644 index 6c87de9b..00000000 --- a/GUI/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/GUI/src/components/Box/Box.scss b/GUI/src/components/Box/Box.scss new file mode 100644 index 00000000..8801c053 --- /dev/null +++ b/GUI/src/components/Box/Box.scss @@ -0,0 +1,56 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/other'; +@import 'src/styles/settings/variables/typography'; + +.box { + padding: get-spacing(paldiski); + border-radius: 4px; + border: 1px solid; + font-size: $veera-font-size-100; + line-height: $veera-line-height-500; + + &:hover { + cursor: grab; + } + + &--default { + background-color: get-color(black-coral-1); + border-color: get-color(black-coral-3); + } + + &--blue { + background-color: get-color(sapphire-blue-0); + border-color: get-color(sapphire-blue-2); + } + + &--yellow { + background-color: get-color(dark-tangerine-0); + border-color: get-color(dark-tangerine-4); + } + + &--green { + background-color: get-color(sea-green-1); + border-color: get-color(sea-green-3); + } + + &--red { + background-color: get-color(jasper-1); + border-color: get-color(jasper-3); + } + + &--gray { + background-color: get-color(black-coral-1); + border-color: get-color(black-coral-3); + } + + &--dark-blue { + background-color: get-color(sapphire-blue-3); + border-color: get-color(sapphire-blue-5); + } + + &--orange { + background-color: get-color(orange-3); + border-color: get-color(orange-5); + } +} diff --git a/GUI/src/components/Box/index.tsx b/GUI/src/components/Box/index.tsx new file mode 100644 index 00000000..df4d3992 --- /dev/null +++ b/GUI/src/components/Box/index.tsx @@ -0,0 +1,16 @@ +import { forwardRef, PropsWithChildren } from 'react'; +import clsx from 'clsx'; + +import './Box.scss'; + +type BoxProps = { + color?: 'default' | 'blue' | 'yellow' | 'green' | 'red' | 'gray' | 'dark-blue' | 'orange'; +} + +const Box = forwardRef>(({ color = 'default', children }, ref) => { + return ( +
{children}
+ ); +}); + +export default Box; diff --git a/GUI/src/components/atoms/Button/Button.scss b/GUI/src/components/Button/Button.scss similarity index 98% rename from GUI/src/components/atoms/Button/Button.scss rename to GUI/src/components/Button/Button.scss index 25c7a631..fa0b5b11 100644 --- a/GUI/src/components/atoms/Button/Button.scss +++ b/GUI/src/components/Button/Button.scss @@ -1,7 +1,6 @@ @import 'src/styles/tools/spacing'; @import 'src/styles/tools/color'; @import 'src/styles/settings/variables/other'; -@import 'src/styles/settings/variables/colors'; @import 'src/styles/settings/variables/typography'; .btn { diff --git a/GUI/src/components/atoms/Button/index.tsx b/GUI/src/components/Button/index.tsx similarity index 100% rename from GUI/src/components/atoms/Button/index.tsx rename to GUI/src/components/Button/index.tsx diff --git a/GUI/src/components/Card/Card.scss b/GUI/src/components/Card/Card.scss new file mode 100644 index 00000000..8dbba87e --- /dev/null +++ b/GUI/src/components/Card/Card.scss @@ -0,0 +1,60 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/other'; +@import 'src/styles/settings/variables/typography'; + +.card { + $self: &; + background-color: get-color(white); + border: 1px solid get-color(black-coral-2); + border-radius: $veera-radius-s; + + &--borderless { + border: 0; + border-radius: 0; + + #{$self}__header { + border-radius: 0; + } + } + + &__header, + &__body, + &__footer { + padding: get-spacing(haapsalu); + } + + &__header { + border-bottom: 1px solid get-color(black-coral-2); + background-color: get-color(extra-light); + border-radius: $veera-radius-s $veera-radius-s 0 0; + + &.white { + background-color: white + } + } + + &__body { + &.divided { + display: flex; + flex-direction: column; + padding-left: 0px; + padding-right: 0px; + + > :not(:last-child) { + margin-bottom: get-spacing(haapsalu); + border-bottom: 1px solid get-color(black-coral-2); + padding-bottom: get-spacing(haapsalu); + padding-left: get-spacing(haapsalu); + } + + > :is(:last-child) { + padding-left: get-spacing(haapsalu); + } + } + } + + &__footer { + border-top: 1px solid get-color(black-coral-2); + } +} diff --git a/GUI/src/components/Card/index.tsx b/GUI/src/components/Card/index.tsx new file mode 100644 index 00000000..128ba81f --- /dev/null +++ b/GUI/src/components/Card/index.tsx @@ -0,0 +1,37 @@ +import { FC, PropsWithChildren, ReactNode } from 'react'; +import clsx from 'clsx'; + +import './Card.scss'; + +type CardProps = { + header?: ReactNode; + footer?: ReactNode; + borderless?: boolean; + isHeaderLight?: boolean; + isBodyDivided?: boolean; +}; + +const Card: FC> = ({ + header, + footer, + borderless, + isHeaderLight, + isBodyDivided, + children, +}) => { + return ( +
+ {header && ( +
+ {header} +
+ )} +
+ {children} +
+ {footer &&
{footer}
} +
+ ); +}; + +export default Card; diff --git a/GUI/src/components/Collapsible/Collapsible.scss b/GUI/src/components/Collapsible/Collapsible.scss new file mode 100644 index 00000000..24328e65 --- /dev/null +++ b/GUI/src/components/Collapsible/Collapsible.scss @@ -0,0 +1,35 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/other'; +@import 'src/styles/settings/variables/typography'; + +.collapsible { + border: 1px solid get-color(black-coral-2); + border-radius: 4px; + + &__trigger { + width: 100%; + display: flex; + align-items: center; + gap: 4px; + padding: get-spacing(haapsalu); + background-color: get-color(extra-light); + border-radius: 4px; + + &[aria-expanded=true] { + border-bottom: 1px solid get-color(black-coral-2); + border-radius: 4px 4px 0 0; + } + + .icon { + font-size: 21px; + } + } + + &__content { + padding: get-spacing(haapsalu); + background-color: get-color(white); + border-radius: 0 0 4px 4px; + overflow: hidden; + } +} diff --git a/GUI/src/components/Collapsible/index.tsx b/GUI/src/components/Collapsible/index.tsx new file mode 100644 index 00000000..02a13bda --- /dev/null +++ b/GUI/src/components/Collapsible/index.tsx @@ -0,0 +1,31 @@ +import { FC, PropsWithChildren, useState } from 'react'; +import * as RadixCollapsible from '@radix-ui/react-collapsible'; +import { MdOutlineAddBox, MdOutlineIndeterminateCheckBox } from 'react-icons/md'; + +import { Icon } from 'components'; +import './Collapsible.scss'; + +type CollapsibleProps = { + title: string; + defaultOpen?: boolean; +} + +const Collapsible: FC> = ({ defaultOpen = false, title, children }) => { + const [open, setOpen] = useState(defaultOpen); + + return ( + + + + + + {children} + + + ); +}; + +export default Collapsible; diff --git a/GUI/src/components/molecules/DataTable/CloseIcon.tsx b/GUI/src/components/DataTable/CloseIcon.tsx similarity index 100% rename from GUI/src/components/molecules/DataTable/CloseIcon.tsx rename to GUI/src/components/DataTable/CloseIcon.tsx diff --git a/GUI/src/components/molecules/DataTable/DataTable.scss b/GUI/src/components/DataTable/DataTable.scss similarity index 100% rename from GUI/src/components/molecules/DataTable/DataTable.scss rename to GUI/src/components/DataTable/DataTable.scss diff --git a/GUI/src/components/molecules/DataTable/DeboucedInput.scss b/GUI/src/components/DataTable/DeboucedInput.scss similarity index 100% rename from GUI/src/components/molecules/DataTable/DeboucedInput.scss rename to GUI/src/components/DataTable/DeboucedInput.scss diff --git a/GUI/src/components/molecules/DataTable/DebouncedInput.tsx b/GUI/src/components/DataTable/DebouncedInput.tsx similarity index 100% rename from GUI/src/components/molecules/DataTable/DebouncedInput.tsx rename to GUI/src/components/DataTable/DebouncedInput.tsx diff --git a/GUI/src/components/molecules/DataTable/Filter.tsx b/GUI/src/components/DataTable/Filter.tsx similarity index 96% rename from GUI/src/components/molecules/DataTable/Filter.tsx rename to GUI/src/components/DataTable/Filter.tsx index 52dc42dc..6622c7aa 100644 --- a/GUI/src/components/molecules/DataTable/Filter.tsx +++ b/GUI/src/components/DataTable/Filter.tsx @@ -3,8 +3,8 @@ import { Column, Table } from '@tanstack/react-table'; import { useTranslation } from 'react-i18next'; import { MdOutlineSearch } from 'react-icons/md'; -import { Icon } from '../../../components'; -import useDocumentEscapeListener from '../../../hooks/useDocumentEscapeListener'; +import { Icon } from 'components'; +import useDocumentEscapeListener from 'hooks/useDocumentEscapeListener'; import DebouncedInput from './DebouncedInput'; type FilterProps = { diff --git a/GUI/src/components/molecules/DataTable/index.tsx b/GUI/src/components/DataTable/index.tsx similarity index 99% rename from GUI/src/components/molecules/DataTable/index.tsx rename to GUI/src/components/DataTable/index.tsx index 8b17ad5e..54727a7f 100644 --- a/GUI/src/components/molecules/DataTable/index.tsx +++ b/GUI/src/components/DataTable/index.tsx @@ -30,7 +30,7 @@ import clsx from 'clsx'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { Icon, Track } from '../../../components'; +import { Icon, Track } from 'components'; import Filter from './Filter'; import './DataTable.scss'; diff --git a/GUI/src/components/molecules/Dialog/Dialog.scss b/GUI/src/components/Dialog/Dialog.scss similarity index 100% rename from GUI/src/components/molecules/Dialog/Dialog.scss rename to GUI/src/components/Dialog/Dialog.scss diff --git a/GUI/src/components/molecules/Dialog/index.tsx b/GUI/src/components/Dialog/index.tsx similarity index 95% rename from GUI/src/components/molecules/Dialog/index.tsx rename to GUI/src/components/Dialog/index.tsx index fbe609e3..7b2848c1 100644 --- a/GUI/src/components/molecules/Dialog/index.tsx +++ b/GUI/src/components/Dialog/index.tsx @@ -2,9 +2,9 @@ import { FC, PropsWithChildren, ReactNode } from 'react'; import * as RadixDialog from '@radix-ui/react-dialog'; import { MdOutlineClose } from 'react-icons/md'; import clsx from 'clsx'; - -import { Icon, Track } from '../../../components'; import './Dialog.scss'; +import Icon from 'components/Icon'; +import Track from 'components/Track'; type DialogProps = { title?: string | null; diff --git a/GUI/src/components/Drawer/Drawer.scss b/GUI/src/components/Drawer/Drawer.scss new file mode 100644 index 00000000..df7bc711 --- /dev/null +++ b/GUI/src/components/Drawer/Drawer.scss @@ -0,0 +1,40 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/other'; +@import 'src/styles/settings/variables/typography'; + +.drawer { + position: fixed; + display: flex; + flex-direction: column; + top: 100px; + right: 0; + bottom: 0; + background-color: get-color(white); + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.14); + width: 50%; + transition: transform .25s ease-out; + overflow: hidden; + z-index: 98; + + &__header { + display: flex; + align-items: center; + gap: get-spacing(haapsalu); + padding: get-spacing(haapsalu); + border-bottom: 1px solid get-color(black-coral-2); + + .icon { + font-size: 20px; + } + } + + &__title, + &__body { + flex: 1; + } + + &__body { + overflow: auto; + } +} diff --git a/GUI/src/components/Drawer/index.tsx b/GUI/src/components/Drawer/index.tsx new file mode 100644 index 00000000..9b6f771f --- /dev/null +++ b/GUI/src/components/Drawer/index.tsx @@ -0,0 +1,42 @@ +import { CSSProperties, FC, PropsWithChildren, useEffect, useRef } from 'react'; +import { MdOutlineClose } from 'react-icons/md'; +import autoAnimate from '@formkit/auto-animate'; + +import { Icon } from 'components'; +import './Drawer.scss'; + +type DrawerProps = { + title: string; + onClose: () => void; + style?: CSSProperties; +} + +const Drawer: FC> = ({ title, onClose, children, style }) => { + const ref = useRef(null); + + useEffect(() => { + ref.current && autoAnimate(ref.current); + const handleKeyup = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose(); + }; + document.addEventListener('keyup', handleKeyup); + + return () => document.removeEventListener('keyup', handleKeyup); + }, [onClose]); + + return ( +
+
+

{title}

+ +
+
+ {children} +
+
+ ); +}; + +export default Drawer; diff --git a/GUI/src/components/FormElements/FormCheckbox/FormCheckbox.scss b/GUI/src/components/FormElements/FormCheckbox/FormCheckbox.scss new file mode 100644 index 00000000..33692e10 --- /dev/null +++ b/GUI/src/components/FormElements/FormCheckbox/FormCheckbox.scss @@ -0,0 +1,57 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/other'; +@import 'src/styles/settings/variables/typography'; + +.checkbox { + width: 100%; + display: flex; + align-items: flex-start; + gap: get-spacing(paldiski); + + &__label { + display: block; + flex: 0 0 185px; + font-size: $veera-font-size-100; + line-height: 24px; + } + + &__item { + input[type=checkbox] { + display: none; + + + label { + display: block; + padding-left: 32px; + position: relative; + font-size: $veera-font-size-100; + line-height: $veera-line-height-500; + + &::before { + content: ''; + display: block; + width: 16px; + height: 16px; + box-shadow: inset 0 0 0 1px get-color(black-coral-2); + border-radius: 2px; + position: absolute; + left: 4px; + top: 4px; + } + } + + &:checked { + + label { + &::before { + background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTEiIHZpZXdCb3g9IjAgMCAxNCAxMSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuNzQ5NzkgOC4xMjkwNkwxLjYyMjI5IDUuMDAxNTZMMC41NjEwMzUgNi4wNjI4MUw0Ljc0OTc5IDEwLjI1MTZMMTMuNzQ5OCAxLjI1MTU2TDEyLjY4ODUgMC4xOTAzMDhMNC43NDk3OSA4LjEyOTA2WiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg=='); + background-color: get-color(sapphire-blue-10); + background-repeat: no-repeat; + background-position: center; + background-size: 13px 10px; + box-shadow: inset 0 0 0 1px get-color(sapphire-blue-10); + } + } + } + } + } +} diff --git a/GUI/src/components/FormElements/FormCheckbox/index.tsx b/GUI/src/components/FormElements/FormCheckbox/index.tsx new file mode 100644 index 00000000..0ce1ba33 --- /dev/null +++ b/GUI/src/components/FormElements/FormCheckbox/index.tsx @@ -0,0 +1,39 @@ +import { forwardRef, InputHTMLAttributes, useId } from 'react'; + +import './FormCheckbox.scss'; + +type FormCheckboxType = InputHTMLAttributes & { + label: string; + name: string; + hideLabel?: boolean; + item: { + label: string; + value: string; + checked?: boolean; + }; +} + +const FormCheckbox = forwardRef(( + { + label, + name, + hideLabel, + item, + ...rest + }, + ref, +) => { + const uid = useId(); + + return ( +
+ {label && !hideLabel && } +
+ + +
+
+ ); +}); + +export default FormCheckbox; diff --git a/GUI/src/components/FormElements/FormCheckboxes/FormCheckboxes.scss b/GUI/src/components/FormElements/FormCheckboxes/FormCheckboxes.scss new file mode 100644 index 00000000..b312ad91 --- /dev/null +++ b/GUI/src/components/FormElements/FormCheckboxes/FormCheckboxes.scss @@ -0,0 +1,63 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/other'; +@import 'src/styles/settings/variables/typography'; + +.checkboxes { + width: 100%; + display: flex; + align-items: flex-start; + gap: get-spacing(paldiski); + + &__label { + display: block; + flex: 0 0 185px; + font-size: $veera-font-size-100; + line-height: 24px; + } + + &__wrapper { + display: flex; + flex-direction: column; + gap: 8px; + } + + &__item { + input[type=checkbox] { + display: none; + + + label { + display: block; + padding-left: 32px; + position: relative; + font-size: $veera-font-size-100; + line-height: $veera-line-height-500; + + &::before { + content: ''; + display: block; + width: 16px; + height: 16px; + box-shadow: inset 0 0 0 1px get-color(black-coral-2); + border-radius: 2px; + position: absolute; + left: 4px; + top: 4px; + } + } + + &:checked { + + label { + &::before { + background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTEiIHZpZXdCb3g9IjAgMCAxNCAxMSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuNzQ5NzkgOC4xMjkwNkwxLjYyMjI5IDUuMDAxNTZMMC41NjEwMzUgNi4wNjI4MUw0Ljc0OTc5IDEwLjI1MTZMMTMuNzQ5OCAxLjI1MTU2TDEyLjY4ODUgMC4xOTAzMDhMNC43NDk3OSA4LjEyOTA2WiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg=='); + background-color: get-color(sapphire-blue-10); + background-repeat: no-repeat; + background-position: center; + background-size: 13px 10px; + box-shadow: inset 0 0 0 1px get-color(sapphire-blue-10); + } + } + } + } + } +} diff --git a/GUI/src/components/FormElements/FormCheckboxes/index.tsx b/GUI/src/components/FormElements/FormCheckboxes/index.tsx new file mode 100644 index 00000000..e3270315 --- /dev/null +++ b/GUI/src/components/FormElements/FormCheckboxes/index.tsx @@ -0,0 +1,44 @@ +import { ChangeEvent, FC, useId, useState } from 'react'; + +import './FormCheckboxes.scss'; + +type FormCheckboxesType = { + label: string; + name: string; + hideLabel?: boolean; + onValuesChange?: (values: Record) => void; + items: { + label: string; + value: string; + }[]; +} + +const FormCheckboxes: FC = ({ label, name, hideLabel, onValuesChange, items }) => { + const id = useId(); + const [selectedValues, setSelectedValues] = useState>({}); + + const handleValuesChange = (e: ChangeEvent) => { + setSelectedValues((prevState) => ({ + ...prevState, + [e.target.name]: [e.target.value], + })); + if (onValuesChange) onValuesChange(selectedValues); + }; + + return ( +
+ {label && !hideLabel && } +
+ {items.map((item, index) => ( +
+ + +
+ ))} +
+
+ ); +}; + +export default FormCheckboxes; diff --git a/GUI/src/components/FormElements/FormDatepicker/FormDatepicker.scss b/GUI/src/components/FormElements/FormDatepicker/FormDatepicker.scss new file mode 100644 index 00000000..55ac0785 --- /dev/null +++ b/GUI/src/components/FormElements/FormDatepicker/FormDatepicker.scss @@ -0,0 +1,154 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/other'; +@import 'src/styles/settings/variables/typography'; + +.datepicker { + $self: &; + display: flex; + align-items: center; + gap: get-spacing(paldiski); + width: 100%; + + &__label { + flex: 0 0 185px; + font-size: $veera-font-size-100; + line-height: 24px; + } + + &__wrapper_column { + display: flex; + flex-direction: column; + gap: 7px; + position: relative; + width: 125px; + + .icon { + position: absolute; + right: 8px; + top: 8px; + pointer-events: none; + } + } + + &__wrapper_row { + display: flex; + flex-direction: row; + gap: 7px; + position: relative; + width: 125px; + + .icon { + position: absolute; + right: 8px; + top: 8px; + pointer-events: none; + } + } + + &__error { + width: 100%; + margin-right: 6px; + display: flex; + align-items: center; + gap: get-spacing(paldiski); + color: get-color(black-coral-20); + border-radius: $veera-radius-s; + background-color: get-color(jasper-3); + font-size: 13px; + line-height: 20px; + box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2); + + &::before { + content: ''; + display: block; + background-color: get-color(jasper-3); + border-left: 16px solid transparent; + border-right: 16px solid transparent; + border-bottom: 25px; + } + } + + input { + width: 100%; + display: block; + appearance: none; + background-color: get-color(white); + border: 1px solid get-color(black-coral-6); + border-radius: $veera-radius-s; + color: var(--color-black); + font-size: $veera-font-size-100; + height: 40px; + line-height: 24px; + padding: get-spacing(paldiski); + + &::placeholder { + color: get-color(black-coral-6); + } + + &:focus { + outline: none; + border-color: get-color(sapphire-blue-10); + } + } + + &--error { + input { + border-color: get-color(jasper-10); + } + } + + &--disabled & { + input { + background-color: get-color(black-coral-0); + } + } +} + +.react-datepicker { + font-family: inherit; + font-size: 14px; + border: 1px solid get-color(black-coral-6); + border-radius: 4px; + + &-popper[data-placement^=bottom] { + padding: 0; + } + + &-wrapper { + display: block; + } + + &__input-container { + display: block; + } + + &__triangle { + &::before, + &::after { + content: none !important; + } + } + + &__navigation { + width: 50px; + height: 50px; + top: 0; + + &:hover { + background-color: var(--color-bg); + } + + &--previous { + border-top-left-radius: 4px; + border-right: 1px solid var(--color-gray); + left: 0; + } + + &--next { + border-top-right-radius: 4px; + border-left: 1px solid var(--color-gray); + right: 0; + } + } +} diff --git a/GUI/src/components/FormElements/FormDatepicker/index.tsx b/GUI/src/components/FormElements/FormDatepicker/index.tsx new file mode 100644 index 00000000..1de8e635 --- /dev/null +++ b/GUI/src/components/FormElements/FormDatepicker/index.tsx @@ -0,0 +1,98 @@ +import { forwardRef, useId } from 'react'; +import ReactDatePicker, { registerLocale } from 'react-datepicker'; +import clsx from 'clsx'; +import { et } from 'date-fns/locale'; +import { ControllerRenderProps } from 'react-hook-form'; +import { + MdChevronRight, + MdChevronLeft, + MdOutlineToday, + MdOutlineSchedule, +} from 'react-icons/md'; + +import { Icon } from 'components'; +import 'react-datepicker/dist/react-datepicker.css'; +import './FormDatepicker.scss'; + +registerLocale('et-EE', et); + +type FormDatepickerProps = ControllerRenderProps & { + label: string; + name: string; + hideLabel?: boolean; + disabled?: boolean; + placeholder?: string; + timePicker?: boolean; + direction?: 'row' | 'column'; +}; + +const FormDatepicker = forwardRef( + ( + { + label, + name, + hideLabel, + disabled, + placeholder, + timePicker, + direction = 'column', + ...rest + }, + ref + ) => { + const id = useId(); + const { value, onChange } = rest; + + const datepickerClasses = clsx( + 'datepicker', + disabled && 'datepicker--disabled' + ); + + return ( +
+ {label && !hideLabel && ( + + )} +
+ } + nextMonthButtonLabel={} + aria-label={hideLabel ? label : undefined} + showTimeSelect={timePicker} + showTimeSelectOnly={timePicker} + timeIntervals={15} + timeFormat="HH:mm:ss" + timeInputLabel="" + portalId="overlay-root" + {...rest} + onChange={onChange} + /> + + ) : ( + + ) + } + size="medium" + /> +
+
+ ); + } +); + +export default FormDatepicker; diff --git a/GUI/src/components/FormElements/FormInput/FormInput.scss b/GUI/src/components/FormElements/FormInput/FormInput.scss new file mode 100644 index 00000000..1aab26f6 --- /dev/null +++ b/GUI/src/components/FormElements/FormInput/FormInput.scss @@ -0,0 +1,90 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/other'; +@import 'src/styles/settings/variables/typography'; + +.input { + $self: &; + display: flex; + align-items: center; + gap: get-spacing(paldiski); + width: 100%; + + &__label { + flex: 0 0 185px; + font-size: $veera-font-size-100; + line-height: 24px; + } + + &__wrapper { + flex: 1; + display: flex; + flex-direction: column; + gap: 7px; + position: relative; + + .icon { + position: absolute; + top: 10px; + right: 10px; + } + } + + &__error { + width: 100%; + margin-right: 6px; + display: flex; + align-items: center; + gap: get-spacing(paldiski); + color: get-color(black-coral-20); + border-radius: $veera-radius-s; + background-color: get-color(jasper-3); + font-size: 13px; + line-height: 20px; + box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2); + + &::before { + content: ''; + display: block; + background-color: get-color(jasper-3); + border-left: 16px solid transparent; + border-right: 16px solid transparent; + border-bottom: 25px; + } + } + + input { + width: 100%; + display: block; + appearance: none; + background-color: get-color(white); + border: 1px solid get-color(black-coral-6); + border-radius: $veera-radius-s; + color: var(--color-black); + font-size: $veera-font-size-100; + height: 40px; + line-height: 24px; + padding: get-spacing(paldiski); + + &::placeholder { + color: get-color(black-coral-6); + } + + &:focus { + outline: none; + border-color: get-color(sapphire-blue-10); + } + } + + &--error { + input { + border-color: get-color(jasper-10); + } + } + + &--disabled & { + input { + background-color: get-color(black-coral-0); + } + } +} diff --git a/GUI/src/components/FormElements/FormInput/index.tsx b/GUI/src/components/FormElements/FormInput/index.tsx new file mode 100644 index 00000000..dfa2dd04 --- /dev/null +++ b/GUI/src/components/FormElements/FormInput/index.tsx @@ -0,0 +1,46 @@ +import { forwardRef, InputHTMLAttributes, PropsWithChildren, useId } from 'react'; +import clsx from 'clsx'; +import './FormInput.scss'; +import { CHAT_INPUT_LENGTH } from 'constants/config'; + +type InputProps = PropsWithChildren> & { + label: string; + name: string; + hideLabel?: boolean; + maxLength?: number; +}; + +const FieldInput = forwardRef( + ( + { label, name, disabled, hideLabel, maxLength, children, ...rest }, + ref + ) => { + const id = useId(); + + const inputClasses = clsx('input', disabled && 'input--disabled'); + + return ( +
+ {label && !hideLabel && ( + + )} +
+ + {children} +
+
+ ); + } +); + +export default FieldInput; diff --git a/GUI/src/components/FormElements/FormRadios/FormRadios.scss b/GUI/src/components/FormElements/FormRadios/FormRadios.scss new file mode 100644 index 00000000..ac5f24c6 --- /dev/null +++ b/GUI/src/components/FormElements/FormRadios/FormRadios.scss @@ -0,0 +1,72 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/other'; +@import 'src/styles/settings/variables/typography'; + +.radios { + width: 100%; + display: flex; + align-items: flex-start; + gap: get-spacing(paldiski); + + &__label { + display: block; + flex: 0 0 185px; + font-size: $veera-font-size-100; + line-height: 24px; + } + + &__wrapper { + display: flex; + flex-direction: column; + gap: 8px; + } + + &__item { + input[type=radio] { + display: none; + + + label { + display: block; + padding-left: 32px; + position: relative; + font-size: $veera-font-size-100; + line-height: $veera-line-height-500; + + &::before { + content: ''; + display: block; + width: 16px; + height: 16px; + box-shadow: inset 0 0 0 1px get-color(black-coral-2); + border-radius: 50%; + position: absolute; + left: 4px; + top: 4px; + } + } + + &:checked { + + label { + &::before { + width: 20px; + height: 20px; + box-shadow: inset 0 0 0 1px #8F91A8; + } + + &::after { + content: ''; + display: block; + width: 10px; + height: 10px; + border-radius: 50%; + background-color: get-color(sapphire-blue-10); + position: absolute; + top: 9px; + left: 9px; + } + } + } + } + } +} diff --git a/GUI/src/components/FormElements/FormRadios/index.tsx b/GUI/src/components/FormElements/FormRadios/index.tsx new file mode 100644 index 00000000..e9357a78 --- /dev/null +++ b/GUI/src/components/FormElements/FormRadios/index.tsx @@ -0,0 +1,36 @@ +import { FC, useId } from 'react'; + +import './FormRadios.scss'; + +type FormRadiosType = { + label: string; + name: string; + hideLabel?: boolean; + items: { + label: string; + value: string; + }[]; + onChange: (selectedValue: string) => void; +} + +const FormRadios: FC = ({ label, name, hideLabel, items, onChange }) => { + const id = useId(); + + return ( +
+ {label && !hideLabel && } +
+ {items.map((item, index) => ( +
+ { + onChange(event.target.value); + }} /> + +
+ ))} +
+
+ ); +}; + +export default FormRadios; diff --git a/GUI/src/components/FormElements/FormSelect/FormMultiselect.tsx b/GUI/src/components/FormElements/FormSelect/FormMultiselect.tsx new file mode 100644 index 00000000..2eed1217 --- /dev/null +++ b/GUI/src/components/FormElements/FormSelect/FormMultiselect.tsx @@ -0,0 +1,124 @@ +import { FC, ReactNode, SelectHTMLAttributes, useId, useState } from 'react'; +import { useSelect } from 'downshift'; +import clsx from 'clsx'; +import { useTranslation } from 'react-i18next'; +import { MdArrowDropDown } from 'react-icons/md'; + +import { Icon } from 'components'; +import './FormSelect.scss'; + +type SelectOption = { label: string, value: string }; + +type FormMultiselectProps = SelectHTMLAttributes & { + label: ReactNode; + name: string; + placeholder?: string; + hideLabel?: boolean; + options: SelectOption[]; + selectedOptions?: SelectOption[]; + onSelectionChange?: (selection: SelectOption[] | null) => void; +}; + +const FormMultiselect: FC = ( + { + label, + hideLabel, + options, + disabled, + placeholder, + defaultValue, + selectedOptions, + onSelectionChange, + ...rest + }, +) => { + const id = useId(); + const { t } = useTranslation(); + const [selectedItems, setSelectedItems] = useState(selectedOptions ?? []); + const { + isOpen, + getToggleButtonProps, + getLabelProps, + getMenuProps, + highlightedIndex, + getItemProps, + } = useSelect({ + items: options, + stateReducer: (state, actionAndChanges) => { + const { changes, type } = actionAndChanges; + if (type === useSelect.stateChangeTypes.ItemClick) { + return { + ...changes, + isOpen: true, + highlightedIndex: state.highlightedIndex, + }; + } else { + return changes; + } + }, + selectedItem: null, + onSelectedItemChange: ({ selectedItem }) => { + if (!selectedItem) { + return; + } + const index = selectedItems.findIndex((item) => item.value === selectedItem.value); + const items = []; + if (index > 0) { + items.push( + ...selectedItems.slice(0, index), + ...selectedItems.slice(index + 1) + ); + } else if (index === 0) { + items.push(...selectedItems.slice(1)); + } else { + items.push(...selectedItems, selectedItem); + } + setSelectedItems(items); + if (onSelectionChange) onSelectionChange(items); + }, + }); + + const selectClasses = clsx( + 'select', + disabled && 'select--disabled', + ); + + const placeholderValue = placeholder || t('global.choose'); + + return ( +
+ {label && !hideLabel && } +
+
+ {selectedItems.length > 0 ? `${t('global.chosen')} (${selectedItems.length})` : placeholderValue} + } /> +
+ +
    + {isOpen && + options.map((item, index) => ( +
  • + s.value).includes(item.value)} + value={item.value} + onChange={() => null} + /> + {item.label} +
  • + ))} +
+
+
+ ); +}; + + +export default FormMultiselect; diff --git a/GUI/src/components/FormElements/FormSelect/FormSelect.scss b/GUI/src/components/FormElements/FormSelect/FormSelect.scss new file mode 100644 index 00000000..fcde774b --- /dev/null +++ b/GUI/src/components/FormElements/FormSelect/FormSelect.scss @@ -0,0 +1,121 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/other'; +@import 'src/styles/settings/variables/typography'; + +.select { + $self: &; + display: flex; + align-items: center; + gap: get-spacing(paldiski); + width: 100%; + + &__label { + flex: 0 0 185px; + font-size: $veera-font-size-100; + line-height: 24px; + } + + &__wrapper { + width: 100%; + position: relative; + } + + &__trigger { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + appearance: none; + background-color: get-color(white); + border: 1px solid get-color(black-coral-6); + border-radius: $veera-radius-s; + color: get-color(black); + font-size: $veera-font-size-100; + height: 40px; + line-height: 24px; + padding: get-spacing(paldiski); + + .icon { + font-size: $veera-font-size-250; + } + + &[aria-expanded=true] { + border-color: get-color(sapphire-blue-10); + border-radius: 3px; + + + #{$self}__menu { + display: block; + } + + +#{$self}__menu_up { + display: block; + } + + .icon { + transform: rotate(180deg); + } + } + } + + &__menu { + display: none; + position: absolute; + top: 100%; + left: 0; + right: 0; + background-color: get-color(white); + border-radius: 4px; + border: 1px solid get-color(black-coral-2); + border-top: 1; + z-index: 9998; + max-height: 320px; + overflow: auto; + margin-top: 3px; + } + + &__menu_up { + display: none; + position: absolute; + top: auto; + left: 0; + right: 0; + bottom: 100%; + background-color: get-color(white); + border-radius: 4px; + border: 1px solid get-color(black-coral-2); + border-top: 1; + z-index: 9998; + max-height: 320px; + overflow: auto; + margin-bottom: 3px; + } + + &__option { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 15px; + + span { + display: block; + } + + &[aria-selected=true] { + background-color: get-color(sapphire-blue-10); + color: get-color(white); + + &:hover, + &:focus { + background-color: get-color(sapphire-blue-10); + color: get-color(white); + } + } + + &:hover, + &:focus { + background-color: get-color(black-coral-0); + color: get-color(black-coral-20); + } + } +} diff --git a/GUI/src/components/FormElements/FormSelect/index.tsx b/GUI/src/components/FormElements/FormSelect/index.tsx new file mode 100644 index 00000000..0a5c2737 --- /dev/null +++ b/GUI/src/components/FormElements/FormSelect/index.tsx @@ -0,0 +1,94 @@ +import { forwardRef, ReactNode, SelectHTMLAttributes, useId, useState } from 'react'; +import { useSelect } from 'downshift'; +import clsx from 'clsx'; +import { useTranslation } from 'react-i18next'; +import { MdArrowDropDown } from 'react-icons/md'; + +import { Icon } from 'components'; +import './FormSelect.scss'; +import { ControllerRenderProps } from 'react-hook-form'; + +type FormSelectProps = Partial & SelectHTMLAttributes & { + label: ReactNode; + name: string; + hideLabel?: boolean; + direction?: 'down' | 'up'; + options: { + label: string; + value: string; + }[]; + onSelectionChange?: (selection: { label: string, value: string } | null) => void; +} + +const itemToString = (item: ({ label: string, value: string } | null)) => { + return item ? item.value : ''; +}; + +const FormSelect= forwardRef(( + { + label, + hideLabel, + direction = 'down', + options, + disabled, + placeholder, + defaultValue, + onSelectionChange, + ...rest + }, + ref +) => { + const id = useId(); + const { t } = useTranslation(); + const defaultSelected = options.find((o) => o.value === defaultValue) || null; + const [selectedItem, setSelectedItem] = useState<{ label: string, value: string } | null>(defaultSelected); + const { + isOpen, + getToggleButtonProps, + getLabelProps, + getMenuProps, + highlightedIndex, + getItemProps, + } = useSelect({ + id, + items: options, + itemToString, + selectedItem, + onSelectedItemChange: ({ selectedItem: newSelectedItem }) => { + setSelectedItem(newSelectedItem ?? null); + if (onSelectionChange) onSelectionChange(newSelectedItem ?? null); + }, + }); + + const selectClasses = clsx( + 'select', + disabled && 'select--disabled', + ); + + const placeholderValue = placeholder || t('global.choose'); + + return ( +
+ {label && !hideLabel && } +
+
+ {selectedItem?.label ?? placeholderValue} + } /> +
+
    + {isOpen && ( + options.map((item, index) => ( +
  • + {item.label} +
  • + )) + )} +
+
+
+ ); +}); + + +export default FormSelect; diff --git a/GUI/src/components/FormElements/FormTextarea/FormTextarea.scss b/GUI/src/components/FormElements/FormTextarea/FormTextarea.scss new file mode 100644 index 00000000..ff1971ac --- /dev/null +++ b/GUI/src/components/FormElements/FormTextarea/FormTextarea.scss @@ -0,0 +1,109 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/other'; +@import 'src/styles/settings/variables/typography'; + +.textarea { + $self: &; + display: flex; + align-items: center; + gap: get-spacing(paldiski); + width: 100%; + + &__label { + flex: 0 0 185px; + font-size: $veera-font-size-100; + line-height: 24px; + } + + &__wrapper { + flex: 1; + display: flex; + flex-direction: column; + gap: 7px; + position: relative; + } + + &__error { + width: 100%; + margin-right: 6px; + display: flex; + align-items: center; + gap: get-spacing(paldiski); + color: get-color(black-coral-20); + border-radius: $veera-radius-s; + background-color: get-color(jasper-3); + font-size: 13px; + line-height: 20px; + box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2); + + &::before { + content: ''; + display: block; + background-color: get-color(jasper-3); + border-left: 16px solid transparent; + border-right: 16px solid transparent; + border-bottom: 25px; + } + } + + &__max-length-top { + position: absolute; + top: 10px; + right: 8px; + font-size: $veera-font-size-80; + color: get-color(black-coral-12); + pointer-events: none; + } + + &__max-length-bottom { + position: absolute; + bottom: 10px; + right: 8px; + font-size: $veera-font-size-80; + color: get-color(black-coral-12); + pointer-events: none; + } + + textarea { + width: 100%; + display: block; + appearance: none; + background-color: get-color(white); + border: 1px solid get-color(black-coral-6); + border-radius: $veera-radius-s; + color: var(--color-black); + font-size: $veera-font-size-80; + line-height: $veera-line-height-500; + height: 40px; + min-height: 40px; + padding: get-spacing(paldiski); + + &::placeholder { + color: get-color(black-coral-6); + } + + &:focus { + outline: none; + border-color: get-color(sapphire-blue-10); + } + } + + &--error { + input { + border-color: get-color(jasper-10); + } + } + + &--disabled & { + input { + background-color: get-color(black-coral-0); + } + } + + &--maxlength-shown { + textarea { + padding-right: 70px; + } + } +} diff --git a/GUI/src/components/FormElements/FormTextarea/index.tsx b/GUI/src/components/FormElements/FormTextarea/index.tsx new file mode 100644 index 00000000..4190c782 --- /dev/null +++ b/GUI/src/components/FormElements/FormTextarea/index.tsx @@ -0,0 +1,72 @@ +import { ChangeEvent, forwardRef, useId, useState } from 'react'; +import TextareaAutosize, { TextareaAutosizeProps } from 'react-textarea-autosize'; +import clsx from 'clsx'; + +import './FormTextarea.scss'; + +type TextareaProps = TextareaAutosizeProps & { + label: string; + name: string; + hideLabel?: boolean; + showMaxLength?: boolean; + maxLengthBottom?: boolean; +}; + +const FormTextarea = forwardRef(( + { + label, + name, + maxLength = 2000, + minRows = 3, + maxRows = 3, + disabled, + hideLabel, + showMaxLength, + maxLengthBottom, + defaultValue, + onChange, + ...rest + }, + ref, +) => { + const id = useId(); + const [currentLength, setCurrentLength] = useState((typeof defaultValue === 'string' && defaultValue.length) || 0); + const textareaClasses = clsx( + 'textarea', + disabled && 'textarea--disabled', + showMaxLength && 'textarea--maxlength-shown', + ); + + const handleOnChange = (e: ChangeEvent) => { + if (showMaxLength) { + setCurrentLength(e.target.value.length); + } + }; + + return ( +
+ {label && !hideLabel && } +
+ { + if (onChange) onChange(e); + handleOnChange(e); + }} + {...rest} + /> + {showMaxLength && ( +
{currentLength}/{maxLength}
+ )} +
+
+ ); +}); + +export default FormTextarea; diff --git a/GUI/src/components/FormElements/Switch/Switch.scss b/GUI/src/components/FormElements/Switch/Switch.scss new file mode 100644 index 00000000..da3c14a2 --- /dev/null +++ b/GUI/src/components/FormElements/Switch/Switch.scss @@ -0,0 +1,69 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/other'; +@import 'src/styles/settings/variables/typography'; + +.switch { + $self: &; + display: flex; + align-items: center; + gap: get-spacing(paldiski); + width: 100%; + + &__label { + flex: 0 0 185px; + font-size: $veera-font-size-100; + line-height: 24px; + } + + &__button { + display: flex; + align-items: center; + gap: 4px; + height: 40px; + isolation: isolate; + padding: 4px; + border-radius: 20px; + background-color: get-color(black-coral-1); + font-size: $veera-font-size-80; + line-height: $veera-line-height-500; + color: get-color(black-coral-12); + position: relative; + transition: background-color .25s ease-out; + + &[aria-checked=true] { + background-color: var(--active-color, get-color(sapphire-blue-10)); + color: get-color(sapphire-blue-10); + + #{$self} { + &__off { + color: get-color(white); + background: none; + } + + &__on { + color: var(--active-color, get-color(sapphire-blue-10)); + background-color: get-color(white); + } + } + } + } + + &__thumb { + display: none; + } + + &__on, + &__off { + display: flex; + border-radius: 20px; + padding: 5.5px 10px; + font-weight: $veera-font-weight-delta; + transition: all .25s ease-out; + } + + &__off { + font-weight: $veera-font-weight-delta; + background-color: get-color(white); + } +} diff --git a/GUI/src/components/FormElements/Switch/index.tsx b/GUI/src/components/FormElements/Switch/index.tsx new file mode 100644 index 00000000..b8241974 --- /dev/null +++ b/GUI/src/components/FormElements/Switch/index.tsx @@ -0,0 +1,65 @@ +import { forwardRef, useId } from 'react'; +import * as RadixSwitch from '@radix-ui/react-switch'; +import { useTranslation } from 'react-i18next'; +import { ControllerRenderProps } from 'react-hook-form'; + +import './Switch.scss'; + +type SwitchProps = Partial & { + onLabel?: string; + offLabel?: string; + onColor?: string; + name?: string; + label: string; + checked?: boolean; + defaultChecked?: boolean; + hideLabel?: boolean; + onCheckedChange?: (checked: boolean) => void; +}; + +const Switch = forwardRef( + ( + { + onLabel, + offLabel, + onColor, + name, + label, + checked, + hideLabel, + onCheckedChange, + defaultChecked, + }, + ref + ) => { + const id = useId(); + const { t } = useTranslation(); + const onValueLabel = onLabel || t('global.on'); + const offValueLabel = offLabel || t('global.off'); + + return ( +
+ {label && !hideLabel && ( + + )} + + + {onValueLabel} + {offValueLabel} + +
+ ); + } +); + +export default Switch; diff --git a/GUI/src/components/FormElements/SwitchBox/SwitchBox.scss b/GUI/src/components/FormElements/SwitchBox/SwitchBox.scss new file mode 100644 index 00000000..2f7a0490 --- /dev/null +++ b/GUI/src/components/FormElements/SwitchBox/SwitchBox.scss @@ -0,0 +1,45 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/other'; +@import 'src/styles/settings/variables/typography'; + +.switchbox { + $self: &; + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + + &__button { + width: 48px; + height: 8px; + border-radius: 4px; + background-color: get-color(black-coral-6); + position: relative; + + &[aria-checked=true] { + background-color: get-color(sapphire-blue-4); + + #{$self} { + &__thumb { + transform: translate(24px, -50%); + background-color: get-color(sapphire-blue-10); + } + } + } + } + + &__thumb { + position: absolute; + width: 24px; + height: 24px; + border-radius: 50%; + background-color: get-color(white); + border: 1px solid get-color(black-coral-2); + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.14); + left: 0; + top: 50%; + transform: translateY(-50%); + transition: all .25s ease-out; + } +} diff --git a/GUI/src/components/FormElements/SwitchBox/index.tsx b/GUI/src/components/FormElements/SwitchBox/index.tsx new file mode 100644 index 00000000..1550576a --- /dev/null +++ b/GUI/src/components/FormElements/SwitchBox/index.tsx @@ -0,0 +1,44 @@ +import { forwardRef, useId } from 'react'; +import * as RadixSwitch from '@radix-ui/react-switch'; +import { ControllerRenderProps } from 'react-hook-form'; + +import './SwitchBox.scss'; + +type SwitchBoxProps = Partial & { + name?: string; + label: string; + checked?: boolean; + hideLabel?: boolean; + onCheckedChange?: (checked: boolean) => void; +} + +const SwitchBox = forwardRef(( + { + name, + label, + checked, + hideLabel, + onCheckedChange, + }, + ref, +) => { + const id = useId(); + + return ( +
+ {label && !hideLabel && } + + + +
+ ); +}); + +export default SwitchBox; diff --git a/GUI/src/components/FormElements/index.tsx b/GUI/src/components/FormElements/index.tsx new file mode 100644 index 00000000..ac295d55 --- /dev/null +++ b/GUI/src/components/FormElements/index.tsx @@ -0,0 +1,23 @@ +import FormInput from './FormInput'; +import FormTextarea from './FormTextarea'; +import FormSelect from './FormSelect'; +import FormMultiselect from './FormSelect/FormMultiselect'; +import Switch from './Switch'; +import FormCheckboxes from './FormCheckboxes'; +import FormRadios from './FormRadios'; +import FormCheckbox from './FormCheckbox'; +import FormDatepicker from './FormDatepicker'; +import SwitchBox from './SwitchBox'; + +export { + FormInput, + FormTextarea, + FormSelect, + FormMultiselect, + Switch, + FormCheckboxes, + FormRadios, + FormCheckbox, + FormDatepicker, + SwitchBox, +}; diff --git a/GUI/src/components/Header/index.tsx b/GUI/src/components/Header/index.tsx new file mode 100644 index 00000000..7298e8e9 --- /dev/null +++ b/GUI/src/components/Header/index.tsx @@ -0,0 +1,523 @@ +import { FC, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import { useIdleTimer } from 'react-idle-timer'; +import { MdOutlineExpandMore } from 'react-icons/md'; + +import { + Track, + Button, + Icon, + Drawer, + Section, + SwitchBox, + Switch, + Dialog, +} from 'components'; +import useStore from 'store'; +import { ReactComponent as BykLogo } from 'assets/logo.svg'; +import { UserProfileSettings } from 'types/userProfileSettings'; +import { Chat as ChatType } from 'types/chat'; +import { useToast } from 'hooks/useToast'; +import { USER_IDLE_STATUS_TIMEOUT } from 'constants/config'; +import apiDev from 'services/api-dev'; +import { interval } from 'rxjs'; +import { AUTHORITY } from 'types/authorities'; +import { useCookies } from 'react-cookie'; +import { useDing } from 'hooks/useAudio'; +import './Header.scss'; + +type CustomerSupportActivity = { + idCode: string; + active: true; + status: string; +}; + +type CustomerSupportActivityDTO = { + customerSupportActive: boolean; + customerSupportStatus: 'offline' | 'idle' | 'online'; + customerSupportId: string; +}; + +const statusColors: Record = { + idle: '#FFB511', + online: '#308653', + offline: '#D73E3E', +}; + +const Header: FC = () => { + const { t } = useTranslation(); + const userInfo = useStore((state) => state.userInfo); + const toast = useToast(); + let secondsUntilStatusPopup = 300; + const [statusPopupTimerHasStarted, setStatusPopupTimerHasStarted] = + useState(false); + const [showStatusConfirmationModal, setShowStatusConfirmationModal] = + useState(false); + + const queryClient = useQueryClient(); + const [userDrawerOpen, setUserDrawerOpen] = useState(false); + const [csaStatus, setCsaStatus] = useState<'idle' | 'offline' | 'online'>( + 'online' + ); + const [ding] = useDing(); + const chatCsaActive = useStore((state) => state.chatCsaActive); + const [userProfileSettings, setUserProfileSettings] = + useState({ + userId: 1, + forwardedChatPopupNotifications: true, + forwardedChatSoundNotifications: true, + forwardedChatEmailNotifications: false, + newChatPopupNotifications: false, + newChatSoundNotifications: true, + newChatEmailNotifications: false, + useAutocorrect: true, + }); + const customJwtCookieKey = 'customJwtCookie'; + + useEffect(() => { + const interval = setInterval(() => { + const expirationTimeStamp = localStorage.getItem('exp'); + if ( + expirationTimeStamp !== 'null' && + expirationTimeStamp !== null && + expirationTimeStamp !== undefined + ) { + const expirationDate = new Date(parseInt(expirationTimeStamp) ?? ''); + const currentDate = new Date(Date.now()); + if (expirationDate < currentDate) { + localStorage.removeItem('exp'); + window.location.href = + import.meta.env.REACT_APP_CUSTOMER_SERVICE_LOGIN; + } + } + }, 2000); + return () => clearInterval(interval); + }, [userInfo]); + + useEffect(() => { + getMessages(); + }, [userInfo?.idCode]); + + const getMessages = async () => { + const { data: res } = await apiDev.get('accounts/settings'); + + if (res.response && res.response != 'error: not found') + setUserProfileSettings(res.response[0]); + }; + // const { data: customerSupportActivity } = useQuery({ + // queryKey: ['accounts/customer-support-activity', 'prod'], + // onSuccess(res: any) { + // const activity = res.data.get_customer_support_activity[0]; + // setCsaStatus(activity.status); + // useStore.getState().setChatCsaActive(activity.active === 'true'); + // }, + // }); + + // useQuery({ + // queryKey: ['agents/chats/active', 'prod'], + // onSuccess(res: any) { + // useStore.getState().setActiveChats(res.data.get_all_active_chats); + // }, + // }); + + const [_, setCookie] = useCookies([customJwtCookieKey]); + const unansweredChatsLength = useStore((state) => + state.unansweredChatsLength() + ); + const forwardedChatsLength = useStore((state) => + state.forwordedChatsLength() + ); + + const handleNewMessage = () => { + if (unansweredChatsLength <= 0) { + return; + } + + if (userProfileSettings.newChatSoundNotifications) { + ding?.play(); + } + if (userProfileSettings.newChatEmailNotifications) { + // To be done: send email notification + } + if (userProfileSettings.newChatPopupNotifications) { + toast.open({ + type: 'info', + title: t('global.notification'), + message: t('settings.users.newUnansweredChat'), + }); + } + }; + + useEffect(() => { + handleNewMessage(); + + const subscription = interval(2 * 60 * 1000).subscribe(() => + handleNewMessage() + ); + return () => { + subscription?.unsubscribe(); + }; + }, [unansweredChatsLength, userProfileSettings]); + + const handleForwordMessage = () => { + if (forwardedChatsLength <= 0) { + return; + } + + if (userProfileSettings.forwardedChatSoundNotifications) { + ding?.play(); + } + if (userProfileSettings.forwardedChatEmailNotifications) { + // To be done: send email notification + } + if (userProfileSettings.forwardedChatPopupNotifications) { + toast.open({ + type: 'info', + title: t('global.notification'), + message: t('settings.users.newForwardedChat'), + }); + } + }; + + useEffect(() => { + handleForwordMessage(); + + const subscription = interval(2 * 60 * 1000).subscribe( + () => handleForwordMessage + ); + return () => { + subscription?.unsubscribe(); + }; + }, [forwardedChatsLength, userProfileSettings]); + + const userProfileSettingsMutation = useMutation({ + mutationFn: async (data: UserProfileSettings) => { + await apiDev.post('accounts/settings', { + forwardedChatPopupNotifications: data.forwardedChatPopupNotifications, + forwardedChatSoundNotifications: data.forwardedChatSoundNotifications, + forwardedChatEmailNotifications: data.newChatEmailNotifications, + newChatPopupNotifications: data.newChatPopupNotifications, + newChatSoundNotifications: data.newChatSoundNotifications, + newChatEmailNotifications: data.newChatEmailNotifications, + useAutocorrect: data.useAutocorrect, + }); + setUserProfileSettings(data); + }, + onError: async (error: AxiosError) => { + await queryClient.invalidateQueries(['accounts/settings']); + toast.open({ + type: 'error', + title: t('global.notificationError'), + message: error.message, + }); + }, + }); + + const unClaimAllAssignedChats = useMutation({ + mutationFn: async () => { + await apiDev.post('chats/assigned/unclaim'); + }, + }); + + const customerSupportActivityMutation = useMutation({ + mutationFn: (data: CustomerSupportActivityDTO) => + apiDev.post('accounts/customer-support-activity', { + customerSupportActive: data.customerSupportActive, + customerSupportStatus: data.customerSupportStatus, + }), + onSuccess: () => { + if (csaStatus === 'online') extendUserSessionMutation.mutate(); + }, + onError: async (error: AxiosError) => { + await queryClient.invalidateQueries([ + 'accounts/customer-support-activity', + 'prod', + ]); + toast.open({ + type: 'error', + title: t('global.notificationError'), + message: error.message, + }); + }, + }); + + const setNewCookie = (cookieValue: string) => { + const cookieOptions = { path: '/' }; + setCookie(customJwtCookieKey, cookieValue, cookieOptions); + }; + + const extendUserSessionMutation = useMutation({ + mutationFn: async () => { + const { + data: { data }, + } = await apiDev.post('extend', {}); + if (data.custom_jwt_extend === null) return; + setNewCookie(data.custom_jwt_extend); + }, + onError: (error: AxiosError) => {}, + }); + + const logoutMutation = useMutation({ + mutationFn: () => apiDev.get('accounts/logout'), + onSuccess(_: any) { + window.location.href = import.meta.env.REACT_APP_CUSTOMER_SERVICE_LOGIN; + }, + onError: async (error: AxiosError) => { + toast.open({ + type: 'error', + title: t('global.notificationError'), + message: error.message, + }); + }, + }); + + // const onIdle = () => { + // if (!customerSupportActivity) return; + // if (csaStatus === 'offline') return; + + // setCsaStatus('idle'); + // customerSupportActivityMutation.mutate({ + // customerSupportActive: chatCsaActive, + // customerSupportId: customerSupportActivity.idCode, + // customerSupportStatus: 'idle', + // }); + // }; + + // const onActive = () => { + // if (!customerSupportActivity) return; + // if (csaStatus === 'offline') { + // setShowStatusConfirmationModal((value) => !value); + // return; + // } + + // setCsaStatus('online'); + // customerSupportActivityMutation.mutate({ + // customerSupportActive: chatCsaActive, + // customerSupportId: customerSupportActivity.idCode, + // customerSupportStatus: 'online', + // }); + // }; + + // useIdleTimer({ + // onIdle, + // onActive, + // timeout: USER_IDLE_STATUS_TIMEOUT, + // throttle: 500, + // }); + + const handleUserProfileSettingsChange = (key: string, checked: boolean) => { + if (!userProfileSettings) return; + const newSettings = { + ...userProfileSettings, + [key]: checked, + }; + userProfileSettingsMutation.mutate(newSettings); + }; + + const handleCsaStatusChange = (checked: boolean) => { + if (checked === false) unClaimAllAssignedChats.mutate(); + + useStore.getState().setChatCsaActive(checked); + setCsaStatus(checked === true ? 'online' : 'offline'); + customerSupportActivityMutation.mutate({ + customerSupportActive: checked, + customerSupportStatus: checked === true ? 'online' : 'offline', + customerSupportId: '', + }); + + if (!checked) showStatusChangePopup(); + }; + + const showStatusChangePopup = () => { + if (statusPopupTimerHasStarted) return; + + setStatusPopupTimerHasStarted((value) => !value); + const timer = setInterval(() => { + let time = secondsUntilStatusPopup; + while (time > 0) { + time -= 1; + } + clearInterval(timer); + setShowStatusConfirmationModal((value) => !value); + setStatusPopupTimerHasStarted((value) => !value); + }, 1000); + }; + + return ( + <> +
+ + + {userInfo && ( + + + + + )} + +
+ + + {/* {userInfo && userProfileSettings && userDrawerOpen && ( + setUserDrawerOpen(false)} + style={{ width: 400 }} + > +
+ + {[ + { + label: t('settings.users.displayName'), + value: userInfo.displayName, + }, + { + label: t('settings.users.userRoles'), + value: userInfo.authorities + .map((r) => t(`roles.${r}`)) + .join(', '), + }, + { + label: t('settings.users.userTitle'), + value: userInfo.csaTitle?.replaceAll(' ', '\xa0'), + }, + { label: t('settings.users.email'), value: userInfo.csaEmail }, + ].map((meta, index) => ( + +

{meta.label}:

+

{meta.value}

+ + ))} + +
+ {[ + AUTHORITY.ADMINISTRATOR, + AUTHORITY.CUSTOMER_SUPPORT_AGENT, + AUTHORITY.SERVICE_MANAGER, + ].some((auth) => userInfo.authorities.includes(auth)) && ( + <> +
+ +

{t('settings.users.autoCorrector')}

+ + handleUserProfileSettingsChange('useAutocorrect', checked) + } + /> + +
+
+ +

{t('settings.users.emailNotifications')}

+ + handleUserProfileSettingsChange( + 'forwardedChatEmailNotifications', + checked + ) + } + /> + + handleUserProfileSettingsChange( + 'newChatEmailNotifications', + checked + ) + } + /> + +
+
+ +

{t('settings.users.soundNotifications')}

+ + handleUserProfileSettingsChange( + 'forwardedChatSoundNotifications', + checked + ) + } + /> + + handleUserProfileSettingsChange( + 'newChatSoundNotifications', + checked + ) + } + /> + +
+
+ +

{t('settings.users.popupNotifications')}

+ + handleUserProfileSettingsChange( + 'forwardedChatPopupNotifications', + checked + ) + } + /> + + handleUserProfileSettingsChange( + 'newChatPopupNotifications', + checked + ) + } + /> + +
+ + )} +
+ )} */} + + ); +}; + +export default Header; diff --git a/GUI/src/components/atoms/Icon/Icon.scss b/GUI/src/components/Icon/Icon.scss similarity index 100% rename from GUI/src/components/atoms/Icon/Icon.scss rename to GUI/src/components/Icon/Icon.scss diff --git a/GUI/src/components/atoms/Icon/index.tsx b/GUI/src/components/Icon/index.tsx similarity index 100% rename from GUI/src/components/atoms/Icon/index.tsx rename to GUI/src/components/Icon/index.tsx diff --git a/GUI/src/components/Label/Label.scss b/GUI/src/components/Label/Label.scss new file mode 100644 index 00000000..89db8ed5 --- /dev/null +++ b/GUI/src/components/Label/Label.scss @@ -0,0 +1,76 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/other'; +@import 'src/styles/settings/variables/typography'; + +.label { + $self: &; + display: flex; + padding: 1.5px 16px; + font-size: $veera-font-size-80; + font-weight: $veera-font-weight-delta; + border: 2px solid; + background-color: get-color(white); + border-radius: $veera-radius-s; + position: relative; + + &--info { + color: get-color(sapphire-blue-10); + border-color: get-color(sapphire-blue-10); + + #{$self} { + &__icon { + border-color: get-color(sapphire-blue-10); + } + } + } + + &--warning { + color: get-color(dark-tangerine-10); + border-color: get-color(dark-tangerine-10); + + #{$self} { + &__icon { + border-color: get-color(dark-tangerine-10); + } + } + } + + &--error { + color: get-color(jasper-10); + border-color: get-color(jasper-10); + + #{$self} { + &__icon { + border-color: get-color(jasper-10); + } + } + } + + &--success { + color: get-color(sea-green-10); + border-color: get-color(sea-green-10); + + #{$self} { + &__icon { + border-color: get-color(sea-green-10); + } + } + } + + &__icon { + display: flex; + align-items: center; + justify-content: center; + position: absolute; + font-size: 13px; + line-height: 15px; + right: -8px; + top: 4px; + width: 16px; + height: 16px; + border-radius: 50%; + border: 2px solid; + background-color: get-color(white); + } +} diff --git a/GUI/src/components/Label/index.tsx b/GUI/src/components/Label/index.tsx new file mode 100644 index 00000000..0b450c24 --- /dev/null +++ b/GUI/src/components/Label/index.tsx @@ -0,0 +1,40 @@ +import { forwardRef, PropsWithChildren, ReactNode } from 'react'; +import clsx from 'clsx'; +import { MdOutlineCheck } from 'react-icons/md'; + +import { Tooltip } from 'components'; +import './Label.scss'; + +type LabelProps = { + type?: 'warning' | 'error' | 'info' | 'success'; + tooltip?: ReactNode; +} + +const Label = forwardRef>(( + { + type = 'info', + tooltip, + children, + }, ref, +) => { + const labelClasses = clsx( + 'label', + `label--${type}`, + tooltip && 'label--tooltip', + ); + + return ( + + {children} + {tooltip && ( + + + {type === 'success' ? : 'i'} + + + )} + + ); +}); + +export default Label; diff --git a/GUI/src/components/Layout/Layout.scss b/GUI/src/components/Layout/Layout.scss index d4d47f3b..116641c0 100644 --- a/GUI/src/components/Layout/Layout.scss +++ b/GUI/src/components/Layout/Layout.scss @@ -1,20 +1,28 @@ @import 'src/styles/tools/spacing'; @import 'src/styles/tools/color'; -.layout{ - display: 'flex'; - flex-direction: 'column'; - height: '100vh' -} +.layout { + height: 100%; + display: flex; -.body{ - margin-top: '60px' -} + &__wrapper { + flex: 1; + display: flex; + flex-direction: column; + position: relative; + } -.main{ - flex-grow: 1; - padding: '20px'; - background: '#f0f0f0'; - margin-left: '60px' + &__main { + flex: 1; + display: flex; + flex-direction: column; + gap: get-spacing(haapsalu); + overflow: auto; + padding: get-spacing(haapsalu); + position: absolute; + top: 100px; + left: 0; + right: 0; + bottom: 0; + } } - diff --git a/GUI/src/components/Layout/index.tsx b/GUI/src/components/Layout/index.tsx index 58e08d4f..8c9a864c 100644 --- a/GUI/src/components/Layout/index.tsx +++ b/GUI/src/components/Layout/index.tsx @@ -1,21 +1,25 @@ import { FC } from 'react'; import { Outlet } from 'react-router-dom'; +import useStore from 'store'; +import { MainNavigation } from '@buerokratt-ria/menu'; +import { Header } from '@buerokratt-ria/header'; import './Layout.scss'; -import Sidebar from '../molecules/SideBar'; -import Header from '../molecules/Header'; +import { useToast } from '../../hooks/useToast'; const Layout: FC = () => { return ( -
- -
-
-
- -
+
+ +
+
+
+ +
+
-
- ); }; diff --git a/GUI/src/components/MainNavigation/index.tsx b/GUI/src/components/MainNavigation/index.tsx new file mode 100644 index 00000000..a067baa1 --- /dev/null +++ b/GUI/src/components/MainNavigation/index.tsx @@ -0,0 +1,282 @@ +import { FC, MouseEvent, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { NavLink, useLocation } from 'react-router-dom'; +import { MdClose, MdKeyboardArrowDown } from 'react-icons/md'; +import { useQuery } from '@tanstack/react-query'; +import clsx from 'clsx'; +import { Icon } from 'components'; +import type { MenuItem } from 'types/mainNavigation'; +import { menuIcons } from 'constants/menuIcons'; +// import './MainNavigation.scss'; + +const MainNavigation: FC = () => { + const { t } = useTranslation(); + const [menuItems, setMenuItems] = useState([]); + + const items = [ + { + id: 'conversations', + label: t('menu.conversations'), + path: '/chat', + children: [ + { + label: t('menu.unanswered'), + path: '/unanswered', + }, + { + label: t('menu.active'), + path: '/active', + }, + { + label: t('menu.history'), + path: '/history', + }, + ], + }, + { + id: 'training', + label: t('menu.training'), + path: '#', + children: [ + { + label: t('menu.training'), + path: '#', + children: [ + { + label: t('menu.themes'), + path: '#', + }, + { + label: t('menu.answers'), + path: '#', + }, + { + label: t('menu.userStories'), + path: '#', + }, + { + label: t('menu.configuration'), + path: '#', + }, + { + label: t('menu.forms'), + path: '#', + }, + { + label: t('menu.slots'), + path: '#', + }, + ], + }, + { + label: t('menu.historicalConversations'), + path: '#', + children: [ + { + label: t('menu.history'), + path: '#', + }, + { + label: t('menu.appeals'), + path: '#', + }, + ], + }, + { + label: t('menu.modelBankAndAnalytics'), + path: '#', + children: [ + { + label: t('menu.overviewOfTopics'), + path: '#', + }, + { + label: t('menu.comparisonOfModels'), + path: '#', + }, + { + label: t('menu.testTracks'), + path: '#', + }, + ], + }, + { + label: t('menu.trainNewModel'), + path: '#', + }, + ], + }, + { + id: 'analytics', + label: t('menu.analytics'), + path: '/analytics', + children: [ + { + label: t('menu.overview'), + path: '#', + }, + { + label: t('menu.chats'), + path: '#', + }, + { + label: t('menu.burokratt'), + path: '#', + }, + { + label: t('menu.feedback'), + path: '#', + }, + { + label: t('menu.advisors'), + path: '#', + }, + { + label: t('menu.reports'), + path: '#', + }, + ], + }, + { + id: 'settings', + label: t('menu.administration'), + path: '/settings', + children: [ + { + label: t('menu.users'), + path: '/settings/users', + }, + { + label: t('menu.chatbot'), + path: '/settings/chatbot', + children: [ + { + label: t('menu.settings'), + path: '/settings/chatbot/settings', + }, + { + label: t('menu.welcomeMessage'), + path: '/settings/chatbot/welcome-message', + }, + { + label: t('menu.appearanceAndBehavior'), + path: '/settings/chatbot/appearance', + }, + { + label: t('menu.emergencyNotices'), + path: '/settings/chatbot/emergency-notices', + }, + ], + }, + { + label: t('menu.officeOpeningHours'), + path: '/settings/working-time', + }, + { + label: t('menu.sessionLength'), + path: '/settings/session-length', + }, + ], + }, + { + id: 'monitoring', + label: t('menu.monitoring'), + path: '/monitoring', + children: [ + { + label: t('menu.workingHours'), + path: '/monitoring/uptime', + }, + ], + }, + ]; + + useQuery({ + queryKey: ['/accounts/user-role', 'prod'], + onSuccess: (res: any) => { + const filteredItems = + items.filter((item) => { + const role = res.data.get_user[0].authorities[0]; + switch (role) { + case 'ROLE_ADMINISTRATOR': + return item.id; + case 'ROLE_SERVICE_MANAGER': + return item.id != 'settings' && item.id != 'training'; + case 'ROLE_CUSTOMER_SUPPORT_AGENT': + return item.id != 'settings' && item.id != 'analytics'; + case 'ROLE_CHATBOT_TRAINER': + return item.id != 'settings' && item.id != 'conversations'; + case 'ROLE_ANALYST': + return item.id == 'analytics' || item.id == 'monitoring'; + case 'ROLE_UNAUTHENTICATED': + return; + } + }) ?? []; + setMenuItems(filteredItems); + }, + }); + + const location = useLocation(); + const [navCollapsed, setNavCollapsed] = useState(false); + + const handleNavToggle = (event: MouseEvent) => { + const isExpanded = + event.currentTarget.getAttribute('aria-expanded') === 'true'; + event.currentTarget.setAttribute( + 'aria-expanded', + isExpanded ? 'false' : 'true' + ); + }; + + const renderMenuTree = (menuItems: MenuItem[]) => { + return menuItems.map((menuItem) => ( +
  • + {menuItem.children ? ( + <> + +
      + {renderMenuTree(menuItem.children)} +
    + + ) : ( + {menuItem.label} + )} +
  • + )); + }; + + if (!menuItems) return null; + + return ( + + ); +}; + +export default MainNavigation; diff --git a/GUI/src/components/Popover/Popover.scss b/GUI/src/components/Popover/Popover.scss new file mode 100644 index 00000000..9278c909 --- /dev/null +++ b/GUI/src/components/Popover/Popover.scss @@ -0,0 +1,15 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/typography'; + +.popover { + background-color: get-color(white); + padding: 4px; + border-radius: 4px; + filter: drop-shadow(0px 0px 20px rgba(0, 0, 0, 0.25)); + font-size: $veera-font-size-80; + + &__arrow { + fill: get-color(white); + } +} diff --git a/GUI/src/components/Popover/index.tsx b/GUI/src/components/Popover/index.tsx new file mode 100644 index 00000000..929015bd --- /dev/null +++ b/GUI/src/components/Popover/index.tsx @@ -0,0 +1,27 @@ +import { FC, PropsWithChildren, ReactNode } from 'react'; +import * as RadixPopover from '@radix-ui/react-popover'; + +import './Popover.scss'; + +type PopoverProps = { + content: ReactNode; + defaultOpen?: boolean; +} + +const Popover: FC> = ({ children, content, defaultOpen = false }) => { + return ( + + + {children} + + + + {content} + + + + + ); +}; + +export default Popover; diff --git a/GUI/src/components/Section/Section.scss b/GUI/src/components/Section/Section.scss new file mode 100644 index 00000000..cdbb136e --- /dev/null +++ b/GUI/src/components/Section/Section.scss @@ -0,0 +1,11 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/typography'; + +.section { + padding: get-spacing(haapsalu); + + &:not(:last-child) { + border-bottom: 1px solid get-color(black-coral-2); + } +} diff --git a/GUI/src/components/Section/index.tsx b/GUI/src/components/Section/index.tsx new file mode 100644 index 00000000..7ecd131d --- /dev/null +++ b/GUI/src/components/Section/index.tsx @@ -0,0 +1,13 @@ +import { forwardRef, PropsWithChildren } from 'react'; + +import './Section.scss'; + +const Section = forwardRef(({ children }, ref) => { + return ( +
    + {children} +
    + ); +}); + +export default Section; diff --git a/GUI/src/components/atoms/Toast/Toast.scss b/GUI/src/components/Toast/Toast.scss similarity index 100% rename from GUI/src/components/atoms/Toast/Toast.scss rename to GUI/src/components/Toast/Toast.scss diff --git a/GUI/src/components/atoms/Toast/index.tsx b/GUI/src/components/Toast/index.tsx similarity index 89% rename from GUI/src/components/atoms/Toast/index.tsx rename to GUI/src/components/Toast/index.tsx index 4f6b5237..ffa29f61 100644 --- a/GUI/src/components/atoms/Toast/index.tsx +++ b/GUI/src/components/Toast/index.tsx @@ -9,6 +9,7 @@ import { } from 'react-icons/md'; import clsx from 'clsx'; +import { Icon } from 'components'; import type { ToastType } from 'context/ToastContext'; import './Toast.scss'; @@ -37,12 +38,14 @@ const Toast: FC = ({ toast, close }) => { onOpenChange={setOpen} > + {toast.title} {toast.message} + } size="medium" /> ); diff --git a/GUI/src/components/atoms/Tooltip/Tooltip.scss b/GUI/src/components/Tooltip/Tooltip.scss similarity index 64% rename from GUI/src/components/atoms/Tooltip/Tooltip.scss rename to GUI/src/components/Tooltip/Tooltip.scss index 03f15dbb..bd062f75 100644 --- a/GUI/src/components/atoms/Tooltip/Tooltip.scss +++ b/GUI/src/components/Tooltip/Tooltip.scss @@ -3,7 +3,7 @@ @import 'src/styles/settings/variables/typography'; .tooltip { - background-color: get-color(rgb(0, 0, 0)); + background-color: get-color(white); padding: 4px; border-radius: 4px; filter: drop-shadow(0px 0px 20px rgba(0, 0, 0, 0.25)); @@ -11,11 +11,6 @@ max-width: 50vw; &__arrow { - fill: get-color(rgb(192, 65, 65)); + fill: get-color(white); } } - -.test{ - color: get-color(black-coral-12); - font-size: --cvi-font-size-50; -} diff --git a/GUI/src/components/atoms/Tooltip/index.tsx b/GUI/src/components/Tooltip/index.tsx similarity index 83% rename from GUI/src/components/atoms/Tooltip/index.tsx rename to GUI/src/components/Tooltip/index.tsx index ee004c4a..3cd41ac2 100644 --- a/GUI/src/components/atoms/Tooltip/index.tsx +++ b/GUI/src/components/Tooltip/index.tsx @@ -11,11 +11,11 @@ const Tooltip: FC> = ({ content, children }) => return ( - + {children} - + {content} diff --git a/GUI/src/components/atoms/Track/index.tsx b/GUI/src/components/Track/index.tsx similarity index 100% rename from GUI/src/components/atoms/Track/index.tsx rename to GUI/src/components/Track/index.tsx diff --git a/GUI/src/components/atoms/CheckBox/index.tsx b/GUI/src/components/atoms/CheckBox/index.tsx deleted file mode 100644 index fbdb54ed..00000000 --- a/GUI/src/components/atoms/CheckBox/index.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from "react"; -import * as RadixCheckbox from "@radix-ui/react-checkbox"; -import { MdCheck } from "react-icons/md"; - -interface CheckboxProps { - checked: boolean; - onCheckedChange: (checked: boolean) => void; - label: string; -} - -const Checkbox: React.FC = ({ - checked, - onCheckedChange, - label, -}) => { - return ( - - ); -}; - -export default Checkbox; diff --git a/GUI/src/components/atoms/InputField/index.tsx b/GUI/src/components/atoms/InputField/index.tsx deleted file mode 100644 index cb6d8560..00000000 --- a/GUI/src/components/atoms/InputField/index.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react'; -import * as RadixLabel from '@radix-ui/react-label'; - -interface InputFieldProps { - label: string; - value: string; - onChange: (e: React.ChangeEvent) => void; - placeholder?: string; - error?: string; -} - -const InputField: React.FC = ({ label, value, onChange, placeholder, error }) => { - return ( -
    - {label} - - {error && {error}} -
    - ); -}; - -export default InputField; diff --git a/GUI/src/components/atoms/RadioButton/index.tsx b/GUI/src/components/atoms/RadioButton/index.tsx deleted file mode 100644 index b5629c65..00000000 --- a/GUI/src/components/atoms/RadioButton/index.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import * as RadixRadioGroup from '@radix-ui/react-radio-group'; - -interface RadioButtonProps { - options: { label: string, value: string }[]; - value: string; - onValueChange: (value: string) => void; - name: string; -} - -const RadioButton: React.FC = ({ options, value, onValueChange, name }) => { - return ( - - {options.map((option, index) => ( - - ))} - - ); -}; - -export default RadioButton; diff --git a/GUI/src/components/index.tsx b/GUI/src/components/index.tsx index edbf281f..5bb3b36f 100644 --- a/GUI/src/components/index.tsx +++ b/GUI/src/components/index.tsx @@ -1,14 +1,55 @@ -import Button from './atoms/Button'; - -import Tooltip from './atoms/Tooltip'; -import Track from './atoms/Track'; -import Icon from './atoms/Icon'; - - +import Layout from './Layout'; +import Button from './Button'; +import Icon from './Icon'; +import Track from './Track'; +import { + FormInput, + FormTextarea, + FormSelect, + FormMultiselect, + Switch, + FormCheckboxes, + FormRadios, + FormCheckbox, + FormDatepicker, + SwitchBox, +} from './FormElements'; +import DataTable from './DataTable'; +import Tooltip from './Tooltip'; +import Card from './Card'; +import Label from './Label'; +import Toast from './Toast'; +import Popover from './Popover'; +import Collapsible from './Collapsible'; +import Box from './Box'; +import Drawer from './Drawer'; +import Dialog from './Dialog'; +import Section from './Section'; export { + Layout, Button, - Tooltip, + Icon, Track, - Icon + Tooltip, + DataTable, + FormInput, + FormTextarea, + FormSelect, + FormMultiselect, + FormDatepicker, + Switch, + SwitchBox, + Card, + Label, + Toast, + FormCheckboxes, + FormRadios, + FormCheckbox, + Popover, + Collapsible, + Box, + Drawer, + Dialog, + Section, }; diff --git a/GUI/src/components/molecules/Header/Header.scss b/GUI/src/components/molecules/Header/Header.scss deleted file mode 100644 index 81e1b518..00000000 --- a/GUI/src/components/molecules/Header/Header.scss +++ /dev/null @@ -1,8 +0,0 @@ -.header{ - height: '60px'; - background: '#282c34'; - color: 'white'; - display: 'flex'; - align-items: 'center'; - padding: '0 20px'; - } \ No newline at end of file diff --git a/GUI/src/components/molecules/Header/index.tsx b/GUI/src/components/molecules/Header/index.tsx deleted file mode 100644 index 8ca6bbe1..00000000 --- a/GUI/src/components/molecules/Header/index.tsx +++ /dev/null @@ -1,12 +0,0 @@ -// src/components/Header.tsx -import React from 'react'; - -const Header: React.FC = () => { - return ( -
    -

    Placeholder for Header

    -
    - ); -}; - -export default Header; diff --git a/GUI/src/components/molecules/SideBar/SideBar.scss b/GUI/src/components/molecules/SideBar/SideBar.scss deleted file mode 100644 index da6c4b69..00000000 --- a/GUI/src/components/molecules/SideBar/SideBar.scss +++ /dev/null @@ -1,30 +0,0 @@ -.sidebar_collapsed { - width: "60px"; - height: "100vh"; - background: "#333"; - color: "white"; - transition: "width 0.3s"; - overflow: "hidden"; -} - -.sidebar_expanded { - width: "200px"; - height: "100vh"; - background: "#333"; - color: "white"; - transition: "width 0.3s"; - overflow: "hidden"; - } - -.toggle_button{ - background: 'none'; - border: 'none'; - color: 'white'; - cursor: 'pointer'; - padding: '10px' -} - -.menu{ - list-style: 'none'; - padding: '0' -} \ No newline at end of file diff --git a/GUI/src/components/molecules/SideBar/index.tsx b/GUI/src/components/molecules/SideBar/index.tsx deleted file mode 100644 index bd71cc13..00000000 --- a/GUI/src/components/molecules/SideBar/index.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import React, { useState } from 'react'; -import { IconType } from 'react-icons'; -import * as MdIcons from 'react-icons/md'; -import menuConfig from '../../../config/menuConfig.json'; - -interface MenuItem { - title: string; - icon: string; - submenu?: MenuItem[]; -} - -const Sidebar: React.FC = () => { - const [collapsed, setCollapsed] = useState(false); - const [expandedMenus, setExpandedMenus] = useState<{ [key: number]: boolean }>({}); - - const toggleSidebar = () => { - setCollapsed(!collapsed); - }; - - const toggleMenu = (index: number) => { - setExpandedMenus(prevState => ({ - ...prevState, - [index]: !prevState[index] - })); - }; - - const renderIcon = (iconName: string): JSX.Element => { - const Icon: IconType = (MdIcons as any)[iconName]; - return ; - }; - - return ( -
    - -
      - {menuConfig.map((item: MenuItem, index: number) => ( -
    • -
      toggleMenu(index)} - > - {renderIcon(item.icon)} - {!collapsed && {item.title}} -
      - {expandedMenus[index] && item.submenu && ( -
        - {item.submenu.map((subItem, subIndex) => ( -
      • - {renderIcon(subItem.icon)} - {!collapsed && {subItem.title}} -
      • - ))} -
      - )} -
    • - ))} -
    -
    - ); -}; - -export default Sidebar; diff --git a/GUI/src/config/rolesConfig.json b/GUI/src/config/rolesConfig.json new file mode 100644 index 00000000..b92c748a --- /dev/null +++ b/GUI/src/config/rolesConfig.json @@ -0,0 +1 @@ +[{ "label": "Admin", "value": "Admin" },{ "label": "Service Manager", "value": "Service_Manager" }] diff --git a/GUI/src/constants/config.ts b/GUI/src/constants/config.ts new file mode 100644 index 00000000..5c0855f4 --- /dev/null +++ b/GUI/src/constants/config.ts @@ -0,0 +1,5 @@ +export const EMERGENCY_NOTICE_LENGTH = 250; +export const WELCOME_MESSAGE_LENGTH = 250; +export const USER_IDLE_STATUS_TIMEOUT = 300000; // milliseconds +export const CHAT_INPUT_LENGTH = 500; +export const CHAT_HISTORY_PREFERENCES_KEY = 'chat-history-preferences'; diff --git a/GUI/src/constants/menuIcons.tsx b/GUI/src/constants/menuIcons.tsx new file mode 100644 index 00000000..06ac2b01 --- /dev/null +++ b/GUI/src/constants/menuIcons.tsx @@ -0,0 +1,24 @@ +import { MdOutlineForum, MdOutlineAdb, MdOutlineEqualizer, MdSettings, MdOutlineMonitorWeight } from 'react-icons/md'; + +export const menuIcons = [ + { + id: 'conversations', + icon: , + }, + { + id: 'training', + icon: , + }, + { + id: 'analytics', + icon: , + }, + { + id: 'settings', + icon: , + }, + { + id: 'monitoring', + icon: , + }, +]; diff --git a/GUI/src/hoc/with-authorization.tsx b/GUI/src/hoc/with-authorization.tsx new file mode 100644 index 00000000..a36e021f --- /dev/null +++ b/GUI/src/hoc/with-authorization.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import useStore from 'store'; +import { ROLES } from 'utils/constants'; + +function withAuthorization

    ( + WrappedComponent: React.ComponentType

    , + allowedRoles: ROLES[] = [], +): React.FC

    { + const CheckRoles: React.FC

    = ({ ...props }: P) => { + + const userInfo = useStore(x => x.userInfo); + const allowed = allowedRoles?.some(x => userInfo?.authorities.includes(x)); + + if(!userInfo) { + return Loading... + } + + if(!allowed) { + return Unauthorized Access + } + + return ; + }; + + return CheckRoles; +}; + +export default withAuthorization; diff --git a/GUI/src/index.css b/GUI/src/index.css deleted file mode 100644 index 5378bd75..00000000 --- a/GUI/src/index.css +++ /dev/null @@ -1,26 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -: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; -} - -body { - margin: 0; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - diff --git a/GUI/src/locale/et_EE.ts b/GUI/src/locale/et_EE.ts new file mode 100644 index 00000000..26f69578 --- /dev/null +++ b/GUI/src/locale/et_EE.ts @@ -0,0 +1,31 @@ +import * as timeago from 'timeago.js'; + +/* To Add Weeks Support Please add the following in index 8 & 9 + ['nädal tagasi', 'nädala pärast'], + ['%s nädalat tagasi', '%s nädala pärast'], +*/ + +function locale(number: number, index: number, totalSec: number | undefined) { + const days = Math.round(Math.round(totalSec ?? 0) / (3600 * 24)); + const monthRemainingDays = days - (number * 30); + const isDaysPlural = monthRemainingDays != 1 ? 'a' : ''; + const isDaysAvailable = monthRemainingDays != 0 ? `${monthRemainingDays} päev${isDaysPlural}` : ''; + return [ + ['just nüüd', 'praegu'], + ['%s sekundit tagasi', '%s sekundi pärast'], + ['minut tagasi', 'minuti pärast'], + ['%s minutit tagasi', '%s minuti pärast'], + ['tund tagasi', 'tunni pärast'], + ['%s tundi tagasi', '%s tunni pärast'], + ['päev tagasi', 'päeva pärast'], + ['%s päeva tagasi', '%s päeva pärast'], + [`${days} päev tagasi`, 'nädala pärast'], + [`${days} päev tagasi`, '%s nädala pärast'], + [`%s kuu ja ${isDaysAvailable}`, 'kuu pärast'], + [`%s kuud ja ${isDaysAvailable}`, '%s kuu pärast'], + ['aasta tagasi', 'aasta pärast'], + ['%s aastat tagasi', '%s aasta pärast'], + ][index] as [string, string]; +} + +timeago.register('et_EE', locale); diff --git a/GUI/src/main.tsx b/GUI/src/main.tsx index 5f71987a..e07f8060 100644 --- a/GUI/src/main.tsx +++ b/GUI/src/main.tsx @@ -1,15 +1,48 @@ -import React from "react"; -import ReactDOM from "react-dom/client"; -import App from "./App.tsx"; -import "./index.css"; -import "../i18n.ts"; -import './styles/main.scss'; -import { BrowserRouter } from "react-router-dom"; - -ReactDOM.createRoot(document.getElementById("root")!).render( +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; +import { + QueryClient, + QueryClientProvider, + QueryFunction, +} from '@tanstack/react-query'; + +import App from './App'; +import api from 'services/api'; +import apiDev from 'services/api-dev'; +import { ToastProvider } from 'context/ToastContext'; +import 'styles/main.scss'; +import '../i18n'; +import { CookiesProvider } from 'react-cookie'; + +const defaultQueryFn: QueryFunction | undefined = async ({ queryKey }) => { + if (queryKey.includes('prod')) { + const { data } = await apiDev.get(queryKey[0] as string); + return data; + } + + const { data } = await api.get(queryKey[0] as string); + return data; +}; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + queryFn: defaultQueryFn, + }, + }, +}); + +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( - - - + + + + + + + + + ); diff --git a/GUI/src/mocks/handlers.ts b/GUI/src/mocks/handlers.ts new file mode 100644 index 00000000..7fc29c5d --- /dev/null +++ b/GUI/src/mocks/handlers.ts @@ -0,0 +1,21 @@ +import { rest } from 'msw'; + +import { usersData } from './users'; +import { healthzStatusData } from './healthzStatus'; + +const BASE_URL = import.meta.env.BASE_URL; + +export const handlers = [ + rest.get(BASE_URL + 'accounts/admins', (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json(usersData), + ); + }), + rest.get(BASE_URL + 'health/components-status', (req, res, ctx) => { + return res(ctx.json(healthzStatusData)); + }), + rest.post(BASE_URL + 'attachments/add', (req, res, ctx) => { + return res(ctx.status(200)); + }) +]; diff --git a/GUI/src/mocks/healthzStatus.ts b/GUI/src/mocks/healthzStatus.ts new file mode 100644 index 00000000..b3122851 --- /dev/null +++ b/GUI/src/mocks/healthzStatus.ts @@ -0,0 +1,18 @@ +export const healthzStatusData = [ + { + name: 'TIM', + version: '{{{escape_special_chars tim.version}}}', + }, + { + name: 'RUUTER', + version: '{{{escape_special_chars ruuter.version}}}', + }, + { + name: 'DMAPPER', + version: '{{{escape_special_chars dmapper.version}}}', + }, + { + name: 'RESQL', + version: '{{{escape_special_chars resql.version}}}', + }, +]; diff --git a/GUI/src/mocks/users.ts b/GUI/src/mocks/users.ts new file mode 100644 index 00000000..997a6cf1 --- /dev/null +++ b/GUI/src/mocks/users.ts @@ -0,0 +1,54 @@ +export const usersData = [ + { + login: 'Admin', + firstName: null, + lastName: null, + idCode: 'EE60001019906', + displayName: 'Admin', + authorities: [ + 'ROLE_ADMINISTRATOR', + 'ROLE_SERVICE_MANAGER', + 'ROLE_CUSTOMER_SUPPORT_AGENT', + 'ROLE_CHATBOT_TRAINER', + 'ROLE_ANALYST', + ], + }, + { + login: 'Anne Clowd', + firstName: null, + lastName: null, + idCode: 'EE49002124277', + displayName: 'Anne Clowd', + authorities: [ + 'ROLE_ADMINISTRATOR', + 'ROLE_SERVICE_MANAGER', + 'ROLE_CUSTOMER_SUPPORT_AGENT', + 'ROLE_CHATBOT_TRAINER', + 'ROLE_ANALYST', + ], + }, + { + login: 'Viva Windler', + firstName: null, + lastName: null, + idCode: 'EE38001085718', + displayName: 'Viva Windler', + authorities: [ + 'ROLE_ADMINISTRATOR', + 'ROLE_SERVICE_MANAGER', + 'ROLE_CUSTOMER_SUPPORT_AGENT', + 'ROLE_CHATBOT_TRAINER', + 'ROLE_ANALYST', + ], + }, + { + login: 'Janina', + firstName: null, + lastName: null, + idCode: 'EE49209110848', + displayName: 'Janina', + authorities: [ + 'ROLE_ADMINISTRATOR', + ], + }, +]; diff --git a/GUI/src/model/ruuter-response-model.ts b/GUI/src/model/ruuter-response-model.ts new file mode 100644 index 00000000..07cafc1c --- /dev/null +++ b/GUI/src/model/ruuter-response-model.ts @@ -0,0 +1,11 @@ +export interface RuuterResponse { + data: Record | null; + error: string | null; +} + +export interface CustomJwtExtendResponse { + data: { + custom_jwt_extend: string; + }; + error: null; +} diff --git a/GUI/src/modules/attachment/api.ts b/GUI/src/modules/attachment/api.ts new file mode 100644 index 00000000..6e3792d0 --- /dev/null +++ b/GUI/src/modules/attachment/api.ts @@ -0,0 +1,21 @@ +import instance from "services/api"; +import { RUUTER_ENDPOINTS } from "utils/constants"; + +const sendAttachment = async (data:any) => { + const body = { + chatId: data.chatId, + name: data.name, + type: data.type, + size: data.size, + base64: data.base64, + }; + return instance({ + url: RUUTER_ENDPOINTS.SEND_ATTACHMENT, + method: "POST", + data: body, + }).then(({ data }) => { + return data; + }); + }; + + export default sendAttachment; diff --git a/GUI/src/modules/attachment/hooks.ts b/GUI/src/modules/attachment/hooks.ts new file mode 100644 index 00000000..04985b8b --- /dev/null +++ b/GUI/src/modules/attachment/hooks.ts @@ -0,0 +1,25 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import sendAttachment from './api'; + +const useSendAttachment = () => { + const queryClient = useQueryClient(); + queryClient.setMutationDefaults(['send-attachment'], { + mutationFn: (data) => sendAttachment(data), + onMutate: async (variables) => { + const { successCb, errorCb } = variables; + return { successCb, errorCb }; + }, + onSuccess: (result, variables, context) => { + if (context.successCb) { + context.successCb(result); + } + }, + onError: (error, variables, context) => { + if (context.errorCb) { + context.errorCb(error); + } + }, + }); + return useMutation(['send-attachment']); +}; +export default useSendAttachment; diff --git a/GUI/src/pages/Home.tsx b/GUI/src/pages/Home.tsx deleted file mode 100644 index 0a0712dd..00000000 --- a/GUI/src/pages/Home.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { FC, useState } from "react"; -import { Button, Tooltip } from "../components"; -import Checkbox from "../components/atoms/CheckBox"; -import RadioButton from "../components/atoms/RadioButton"; -import InputField from "../components/atoms/InputField"; - -const Home: FC = () => { - //check box - const [isChecked, setIsChecked] = useState(false); - - const handleCheckboxChange = (checked: boolean) => { - setIsChecked(checked); - }; - - //radio button - const [selectedValue, setSelectedValue] = useState("option1"); - - const handleValueChange = (value: string) => { - setSelectedValue(value); - }; - - const options = [ - { label: "Option 1", value: "option1" }, - { label: "Option 2", value: "option2" }, - ]; - - //input field - const [inputValue, setInputValue] = useState(''); - const [error, setError] = useState(''); - - const handleInputChange = (e: React.ChangeEvent) => { - const value = e.target.value; - setInputValue(value); - - // Example validation - if (value.length < 5) { - setError('Input must be at least 5 characters long'); - } else { - setError(''); - } - }; - - return ( -

    -
    This is list of components
    -
    - - Tooltip - -
    -
    - {" "} -
    - -
    - {" "} -
    -
    - -
    -
    - -
    -
    - ); -}; - -export default Home; diff --git a/GUI/src/pages/UserManagement.tsx b/GUI/src/pages/UserManagement.tsx deleted file mode 100644 index 6a475b10..00000000 --- a/GUI/src/pages/UserManagement.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { FC, useMemo, useState } from "react"; -import { Button, Icon } from "../components"; -import DataTable from "../components/molecules/DataTable"; -import users from "../config/users.json"; -import { Row, createColumnHelper } from "@tanstack/react-table"; -import { User } from "../types/user"; -import { MdOutlineDeleteOutline, MdOutlineEdit } from "react-icons/md"; -import Dialog from "../components/molecules/Dialog"; - -const UserManagement: FC = () => { - const [data, setData] = useState([]); - const columnHelper = createColumnHelper(); - const [editableRow, setEditableRow] = useState(null); - const [deletableRow, setDeletableRow] = useState( - null - ); - const [showAddUserModal, setShowAddUserModal] = useState(false); - - const editView = (props: any) => ( - - ); - - const deleteView = (props: any) => ( - - ); - - const usersColumns = useMemo( - () => [ - columnHelper.accessor( - (row) => `${row.firstName ?? ""} ${row.lastName ?? ""}`, - { - id: `name`, - header: "name", - } - ), - columnHelper.accessor("idCode", { - header: "idCode", - }), - columnHelper.accessor( - (data: { authorities: any }) => { - const output: string[] = []; - data.authorities?.map?.((role) => { - return output.push("role"); - }); - return output; - }, - { - header: "role" ?? "", - cell: (props) => props.getValue().join(", "), - filterFn: (row: Row, _, filterValue) => { - const rowAuthorities: string[] = []; - row.original.authorities.map((role) => { - return rowAuthorities.push("role"); - }); - const filteredArray = rowAuthorities.filter((word) => - word.toLowerCase().includes(filterValue.toLowerCase()) - ); - return filteredArray.length > 0; - }, - } - ), - columnHelper.accessor("displayName", { - header: "displayName" ?? "", - }), - columnHelper.accessor("csaTitle", { - header: "csaTitle" ?? "", - }), - columnHelper.accessor("csaEmail", { - header: "csaEmail" ?? "", - }), - columnHelper.display({ - id: "edit", - cell: editView, - meta: { - size: "1%", - }, - }), - columnHelper.display({ - id: "delete", - cell: deleteView, - meta: { - size: "1%", - }, - }), - ], - [] - ); - - return ( - <> -
    -
    User Management
    - -
    - <> - - setShowAddUserModal(false)} - title={"Add User"} - isOpen={showAddUserModal} - footer={ - <> - - - - } - > - - body - - - - ); -}; - -export default UserManagement; diff --git a/GUI/src/pages/UserManagement/UserManagement.scss b/GUI/src/pages/UserManagement/UserManagement.scss new file mode 100644 index 00000000..94b402a6 --- /dev/null +++ b/GUI/src/pages/UserManagement/UserManagement.scss @@ -0,0 +1,29 @@ +.container { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 48px; + } + + .title { + font-size: 1.5rem; + color: #000; + font-weight: 300; + } + + .button { + background-color: #007bff; + padding: 10px 20px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 1rem; + } + + .button:hover { + background-color: #0056b3; + } + + .form-group { + margin-bottom: 20px; + } \ No newline at end of file diff --git a/GUI/src/pages/UserManagement/index.tsx b/GUI/src/pages/UserManagement/index.tsx new file mode 100644 index 00000000..538ffdb6 --- /dev/null +++ b/GUI/src/pages/UserManagement/index.tsx @@ -0,0 +1,213 @@ +import { FC, useMemo, useState } from 'react'; +import { + Button, + DataTable, + Dialog, + FormInput, + FormSelect, + Icon, +} from '../../components'; +import users from '../../config/users.json'; +import { Row, createColumnHelper } from '@tanstack/react-table'; +import { User } from '../../types/user'; +import { MdOutlineDeleteOutline, MdOutlineEdit } from 'react-icons/md'; +import './UserManagement.scss'; +import roles from '../../config/rolesConfig.json'; +import { useTranslation } from 'react-i18next'; +import { ROLES } from 'utils/constants'; + +const UserManagement: FC = () => { + const columnHelper = createColumnHelper(); + const [newUserModal, setNewUserModal] = useState(false); + const [editableRow, setEditableRow] = useState(null); + const [deletableRow, setDeletableRow] = useState( + null + ); + const { t } = useTranslation(); + + const editView = (props: any) => ( + + ); + + const deleteView = (props: any) => ( + + ); + + const usersColumns = useMemo( + () => [ + columnHelper.accessor( + (row) => `${row.firstName ?? ''} ${row.lastName ?? ''}`, + { + id: `name`, + header: t('settings.users.name') ?? '', + } + ), + columnHelper.accessor('idCode', { + header: t('settings.users.idCode') ?? '', + }), + columnHelper.accessor( + (data: { authorities: ROLES[] }) => { + const output: string[] = []; + data.authorities?.map?.((role) => { + return output.push(t(`roles.${role}`)); + }); + return output; + }, + { + header: t('settings.users.role') ?? '', + cell: (props) => props.getValue().join(', '), + filterFn: (row: Row, _, filterValue) => { + const rowAuthorities: string[] = []; + row.original.authorities.map((role) => { + return rowAuthorities.push(t(`roles.${role}`)); + }); + const filteredArray = rowAuthorities.filter((word) => + word.toLowerCase().includes(filterValue.toLowerCase()) + ); + return filteredArray.length > 0; + }, + } + ), + columnHelper.accessor('csaEmail', { + header: t('settings.users.email') ?? '', + }), + columnHelper.display({ + id: 'edit', + cell: editView, + meta: { + size: '1%', + }, + }), + columnHelper.display({ + id: 'delete', + cell: deleteView, + meta: { + size: '1%', + }, + }), + ], + [] + ); + + return ( + <> +
    +
    User Management
    + +
    +
    + + + {deletableRow !== null && ( + setDeletableRow(null)} + isOpen={true} + footer={ + <> + + + + } + > +

    {t('global.removeValidation')}

    +
    + )} + {(newUserModal || editableRow) && ( + { + setNewUserModal(false); + setEditableRow(null); + }} + title={newUserModal ? 'Add User' : 'Edit User'} + isOpen={newUserModal || editableRow !== null} + footer={ + <> + + + + } + > + <> +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    + +
    + )} +
    + + ); +}; + +export default UserManagement; diff --git a/GUI/src/services/api-dev.ts b/GUI/src/services/api-dev.ts new file mode 100644 index 00000000..83623d26 --- /dev/null +++ b/GUI/src/services/api-dev.ts @@ -0,0 +1,38 @@ +import axios, { AxiosError } from 'axios'; + +const instance = axios.create({ + baseURL: import.meta.env.REACT_APP_RUUTER_PRIVATE_API_URL, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + withCredentials: true, +}); + +instance.interceptors.response.use( + (axiosResponse) => { + return axiosResponse; + }, + (error: AxiosError) => { + console.log(error); + return Promise.reject(new Error(error.message)); + } +); + +instance.interceptors.request.use( + (axiosRequest) => { + return axiosRequest; + }, + (error: AxiosError) => { + console.log(error); + if (error.response?.status === 401) { + // To be added: handle unauthorized requests + } + if (error.response?.status === 403) { + // To be added: handle unauthorized requests + } + return Promise.reject(new Error(error.message)); + } +); + +export default instance; diff --git a/GUI/src/services/api.ts b/GUI/src/services/api.ts new file mode 100644 index 00000000..885d36dd --- /dev/null +++ b/GUI/src/services/api.ts @@ -0,0 +1,38 @@ +import axios, { AxiosError } from 'axios'; + +const instance = axios.create({ + baseURL: import.meta.env.BASE_URL, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + withCredentials: true, +}); + +instance.interceptors.response.use( + (axiosResponse) => { + return axiosResponse; + }, + (error: AxiosError) => { + console.log(error); + return Promise.reject(new Error(error.message)); + } +); + +instance.interceptors.request.use( + (axiosRequest) => { + return axiosRequest; + }, + (error: AxiosError) => { + console.log(error); + if (error.response?.status === 401) { + // To be added: handle unauthorized requests + } + if (error.response?.status === 403) { + // To be added: handle unauthorized requests + } + return Promise.reject(new Error(error.message)); + } +); + +export default instance; diff --git a/GUI/src/services/sse-service.ts b/GUI/src/services/sse-service.ts new file mode 100644 index 00000000..ee5389c8 --- /dev/null +++ b/GUI/src/services/sse-service.ts @@ -0,0 +1,30 @@ +const notificationNodeUrl = import.meta.env.REACT_APP_NOTIFICATION_NODE_URL; + +const sse = (url: string, onMessage: (data: T) => void): EventSource => { + if (!notificationNodeUrl) { + console.error('Notification node url is not defined'); + throw new Error('Notification node url is not defined'); + } + const eventSource = new EventSource( + `${notificationNodeUrl}/sse/notifications${url}` + ); + + eventSource.onmessage = (event: MessageEvent) => { + if (event.data != undefined && event.data != 'undefined') { + const response = JSON.parse(event.data); + if (response != undefined) { + onMessage(Object.values(response)[0] as T); + } + } + }; + + eventSource.onopen = () => { + console.log('SSE connection Opened'); + }; + + eventSource.onerror = () => {}; + + return eventSource; +}; + +export default sse; diff --git a/GUI/src/services/users.ts b/GUI/src/services/users.ts new file mode 100644 index 00000000..044efe8e --- /dev/null +++ b/GUI/src/services/users.ts @@ -0,0 +1,46 @@ +import apiDev from './api-dev'; +import { User, UserDTO } from 'types/user'; + +export async function createUser(userData: UserDTO) { + const authorities = userData.authorities.map((e) => (e as any).value).filter(item => item); + const fullName = userData.fullName?.trim(); + const { data } = await apiDev.post('accounts/add', { + "firstName": fullName?.split(' ').slice(0, 1).join(' ') ?? '', + "lastName": fullName?.split(' ').slice(1, 2).join(' ') ?? '', + "userIdCode": userData.idCode, + "displayName": userData.displayName, + "csaTitle": userData.csaTitle, + "csa_email": userData.csaEmail, + "roles": authorities.length === 0 ? Object.values(userData.authorities) : authorities + }); + return data; +} + +export async function checkIfUserExists(userData: UserDTO) { + const { data } = await apiDev.post('accounts/exists', { + "userIdCode": userData.idCode + }); + return data; +} + +export async function editUser(id: string | number, userData: UserDTO) { + const authorities = userData.authorities.map((e: any) => e.value).filter(item => item); + const fullName = userData.fullName?.trim(); + const { data } = await apiDev.post('accounts/edit', { + "firstName": fullName?.split(' ').slice(0, 1).join(' ') ?? '', + "lastName": fullName?.split(' ').slice(1, 2).join(' ') ?? '', + "userIdCode": id, + "displayName": userData.displayName, + "csaTitle": userData.csaTitle, + "csa_email": userData.csaEmail, + "roles": authorities.length === 0 ? Object.values(userData.authorities) : authorities + }); + return data; +} + +export async function deleteUser(id: string | number) { + const { data } = await apiDev.post('accounts/delete', { + "userIdCode": id, + }); + return data; +} diff --git a/GUI/src/static/icons/link-external-blue.svg b/GUI/src/static/icons/link-external-blue.svg new file mode 100644 index 00000000..9bd1d1fb --- /dev/null +++ b/GUI/src/static/icons/link-external-blue.svg @@ -0,0 +1,8 @@ + diff --git a/GUI/src/static/icons/link-external-white.svg b/GUI/src/static/icons/link-external-white.svg new file mode 100644 index 00000000..a391216d --- /dev/null +++ b/GUI/src/static/icons/link-external-white.svg @@ -0,0 +1 @@ + diff --git a/GUI/src/store/index.ts b/GUI/src/store/index.ts new file mode 100644 index 00000000..4261a966 --- /dev/null +++ b/GUI/src/store/index.ts @@ -0,0 +1,253 @@ +import { create } from 'zustand'; +import { UserInfo } from 'types/userInfo'; +import { + CHAT_STATUS, + Chat as ChatType, + GroupedChat, + GroupedPendingChat, +} from 'types/chat'; +import apiDev from 'services/api-dev'; + +interface StoreState { + userInfo: UserInfo | null; + userId: string; + activeChats: ChatType[]; + pendingChats: ChatType[]; + selectedChatId: string | null; + chatCsaActive: boolean; + setActiveChats: (chats: ChatType[]) => void; + setPendingChats: (chats: ChatType[]) => void; + setUserInfo: (info: UserInfo) => void; + setSelectedChatId: (id: string | null) => void; + setChatCsaActive: (active: boolean) => void; + selectedChat: () => ChatType | null | undefined; + selectedPendingChat: () => ChatType | null | undefined; + unansweredChats: () => ChatType[]; + forwordedChats: () => ChatType[]; + unansweredChatsLength: () => number; + forwordedChatsLength: () => number; + loadActiveChats: () => Promise; + getGroupedActiveChats: () => GroupedChat; + getGroupedUnansweredChats: () => GroupedChat; + loadPendingChats: () => Promise; + getGroupedPendingChats: () => GroupedPendingChat; +} + +const useStore = create((set, get, store) => ({ + userInfo: null, + userId: '', + activeChats: [], + pendingChats: [], + selectedChatId: null, + chatCsaActive: false, + setActiveChats: (chats) => set({ activeChats: chats }), + setPendingChats: (chats) => set({ pendingChats: chats }), + setUserInfo: (data) => set({ userInfo: data, userId: data?.idCode || '' }), + setSelectedChatId: (id) => set({ selectedChatId: id }), + setChatCsaActive: (active) => { + set({ + chatCsaActive: active, + }); + get().loadActiveChats(); + get().loadPendingChats(); + }, + selectedChat: () => { + const selectedChatId = get().selectedChatId; + return get().activeChats.find((c) => c.id === selectedChatId); + }, + selectedPendingChat: () => { + const selectedChatId = get().selectedChatId; + return get().pendingChats.find((c) => c.id === selectedChatId); + }, + unansweredChats: () => { + return get().activeChats?.filter?.((c) => c.customerSupportId === '') ?? []; + }, + forwordedChats: () => { + const userId = get().userId; + return ( + get().activeChats?.filter( + (c) => + c.status === CHAT_STATUS.REDIRECTED && c.customerSupportId === userId + ) || [] + ); + }, + unansweredChatsLength: () => get().unansweredChats().length, + forwordedChatsLength: () => get().forwordedChats().length, + + loadActiveChats: async () => { + const res = await apiDev.get('agents/chats/active'); + const chats: ChatType[] = res.data.response ?? []; + const selectedChatId = get().selectedChatId; + const isChatStillExists = chats?.filter( + (e: any) => e.id === selectedChatId + ); + if (isChatStillExists.length === 0 && get().activeChats.length > 0) { + setTimeout(() => get().setActiveChats(chats), 3000); + } else { + get().setActiveChats(chats); + } + }, + loadPendingChats: async () => { + const res = await apiDev.get('agents/chats/pending'); + const chats: ChatType[] = res.data.response ?? []; + const selectedChatId = get().selectedChatId; + const isChatStillExists = chats?.filter( + (e: any) => e.id === selectedChatId + ); + if (isChatStillExists.length === 0 && get().pendingChats.length > 0) { + setTimeout(() => get().setPendingChats(chats), 3000); + } else { + get().setPendingChats(chats); + } + }, + getGroupedActiveChats: () => { + const activeChats = get().activeChats; + const userInfo = get().userInfo; + const chatCsaActive = get().chatCsaActive; + + const grouped: GroupedChat = { + myChats: [], + otherChats: [], + }; + + if (!activeChats) return grouped; + + if ( + chatCsaActive === false && + !userInfo?.authorities.includes('ROLE_ADMINISTRATOR') + ) { + if (get().selectedChatId !== null) { + get().setSelectedChatId(null); + } + return grouped; + } + + activeChats.forEach((c) => { + if (c.customerSupportId === userInfo?.idCode) { + grouped.myChats.push(c); + return; + } + + const groupIndex = grouped.otherChats.findIndex( + (x) => x.groupId === c.customerSupportId + ); + + if (c.customerSupportId !== '') { + if (groupIndex === -1) { + grouped.otherChats.push({ + groupId: c.customerSupportId ?? '', + name: c.customerSupportDisplayName ?? '', + chats: [c], + }); + } else { + grouped.otherChats[groupIndex].chats.push(c); + } + } + }); + + grouped.otherChats.sort((a, b) => a.name.localeCompare(b.name)); + return grouped; + }, + + getGroupedUnansweredChats: () => { + const activeChats = get().activeChats; + const userInfo = get().userInfo; + const chatCsaActive = get().chatCsaActive; + + const grouped: GroupedChat = { + myChats: [], + otherChats: [], + }; + + if (!activeChats) return grouped; + + if (chatCsaActive === true) { + activeChats.forEach((c) => { + if (c.customerSupportId === '') { + grouped.myChats.push(c); + } + }); + } else { + activeChats.forEach((c) => { + if ( + c.customerSupportId === userInfo?.idCode || + c.customerSupportId === '' + ) { + grouped.myChats.push(c); + return; + } + + grouped.myChats.sort((a, b) => a.created.localeCompare(b.created)); + const groupIndex = grouped.otherChats.findIndex( + (x) => x.groupId === c.customerSupportId + ); + if (c.customerSupportId !== '') { + if (groupIndex === -1) { + grouped.otherChats.push({ + groupId: c.customerSupportId ?? '', + name: c.customerSupportDisplayName ?? '', + chats: [c], + }); + } else { + grouped.otherChats[groupIndex].chats.push(c); + } + } + }); + + grouped.otherChats.sort((a, b) => a.name.localeCompare(b.name)); + } + + return grouped; + }, + getGroupedPendingChats: () => { + const pendingChats = get().pendingChats; + const userInfo = get().userInfo; + const chatCsaActive = get().chatCsaActive; + + const grouped: GroupedPendingChat = { + newChats: [], + inProcessChats: [], + myChats: [], + otherChats: [], + }; + + if (!pendingChats) return grouped; + + if (chatCsaActive) { + pendingChats.forEach((c) => { + if (c.customerSupportId === 'chatbot') { + grouped.newChats.push(c); + } else { + grouped.inProcessChats.push(c); + } + }); + + grouped.inProcessChats.forEach((c) => { + if (c.customerSupportId === userInfo?.idCode) { + grouped.myChats.push(c); + return; + } + + grouped.myChats.sort((a, b) => a.created.localeCompare(b.created)); + const groupIndex = grouped.otherChats.findIndex( + (x) => x.groupId === c.customerSupportId + ); + if (c.customerSupportId !== '') { + if (groupIndex === -1) { + grouped.otherChats.push({ + groupId: c.customerSupportId ?? '', + name: c.customerSupportDisplayName ?? '', + chats: [c], + }); + } else { + grouped.otherChats[groupIndex].chats.push(c); + } + } + grouped.otherChats.sort((a, b) => a.name.localeCompare(b.name)); + }); + } + return grouped; + }, +})); + +export default useStore; diff --git a/GUI/src/utils/constants.ts b/GUI/src/utils/constants.ts new file mode 100644 index 00000000..bc9be5bc --- /dev/null +++ b/GUI/src/utils/constants.ts @@ -0,0 +1,19 @@ +export const MESSAGE_FILE_SIZE_LIMIT = 10_000_000; + +export enum ROLES { + ROLE_ADMINISTRATOR = 'ROLE_ADMINISTRATOR', + ROLE_SERVICE_MANAGER = 'ROLE_SERVICE_MANAGER', + ROLE_CUSTOMER_SUPPORT_AGENT = 'ROLE_CUSTOMER_SUPPORT_AGENT', + ROLE_CHATBOT_TRAINER = 'ROLE_CHATBOT_TRAINER', + ROLE_ANALYST = 'ROLE_ANALYST', + ROLE_UNAUTHENTICATED = 'ROLE_UNAUTHENTICATED', +} + +export enum RUUTER_ENDPOINTS { + SEND_ATTACHMENT= '/attachments/add' +} + +export enum AUTHOR_ROLES { + END_USER = 'end-user', + BACKOFFICE_USER = 'backoffice-user', +} diff --git a/GUI/src/utils/format-bytes.ts b/GUI/src/utils/format-bytes.ts new file mode 100644 index 00000000..4f0e5abf --- /dev/null +++ b/GUI/src/utils/format-bytes.ts @@ -0,0 +1,8 @@ +export default function formatBytes(bytes: number, decimals = 0) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'kB', 'MB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; +} diff --git a/GUI/src/utils/generateUEID.ts b/GUI/src/utils/generateUEID.ts new file mode 100644 index 00000000..8e7a1fc0 --- /dev/null +++ b/GUI/src/utils/generateUEID.ts @@ -0,0 +1,8 @@ +export const generateUEID = () => { + let first: string | number = (Math.random() * 46656) | 0; + let second: string | number = (Math.random() * 46656) | 0; + first = ('000' + first.toString(36)).slice(-3); + second = ('000' + second.toString(36)).slice(-3); + + return first + second; +}; diff --git a/GUI/src/utils/local-storage-utils.ts b/GUI/src/utils/local-storage-utils.ts new file mode 100644 index 00000000..c4183f51 --- /dev/null +++ b/GUI/src/utils/local-storage-utils.ts @@ -0,0 +1,17 @@ +export const getFromLocalStorage = ( + key: string, + initialValue: any = null +): any => { + try { + const item = localStorage.getItem(key); + return item ? JSON.parse(item) : initialValue; + } catch { + return initialValue; + } +}; + +export const setToLocalStorage = (key: string, value: any): void => { + try { + localStorage.setItem(key, JSON.stringify(value)); + } catch {} +}; diff --git a/GUI/src/utils/state-management-utils.ts b/GUI/src/utils/state-management-utils.ts new file mode 100644 index 00000000..adad0899 --- /dev/null +++ b/GUI/src/utils/state-management-utils.ts @@ -0,0 +1,11 @@ +import { CHAT_EVENTS } from "types/chat"; +import { Message } from "types/message"; + +export const isStateChangingEventMessage = (msg: Message): boolean => + msg.event === CHAT_EVENTS.GREETING || + msg.event === CHAT_EVENTS.ASK_PERMISSION_IGNORED || + (msg.event === CHAT_EVENTS.CONTACT_INFORMATION && msg.content?.length === 0) || + msg.event === CHAT_EVENTS.ANSWERED || + msg.event === CHAT_EVENTS.READ || + msg.event === CHAT_EVENTS.RATING || + msg.event === CHAT_EVENTS.TERMINATED; diff --git a/GUI/src/vite-env.d.ts b/GUI/src/vite-env.d.ts index 11f02fe2..b1f45c78 100644 --- a/GUI/src/vite-env.d.ts +++ b/GUI/src/vite-env.d.ts @@ -1 +1,2 @@ /// +/// diff --git a/GUI/tailwind.config.js b/GUI/tailwind.config.js deleted file mode 100644 index d37737fc..00000000 --- a/GUI/tailwind.config.js +++ /dev/null @@ -1,12 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -export default { - content: [ - "./index.html", - "./src/**/*.{js,ts,jsx,tsx}", - ], - theme: { - extend: {}, - }, - plugins: [], -} - diff --git a/GUI/translations/en/common.json b/GUI/translations/en/common.json new file mode 100644 index 00000000..a1b8da72 --- /dev/null +++ b/GUI/translations/en/common.json @@ -0,0 +1,359 @@ +{ + "global": { + "save": "Save", + "add": "Add", + "edit": "Edit", + "delete": "Delete", + "cancel": "Cancel", + "modifiedAt": "Last modified at", + "addNew": "Add new", + "dependencies": "Dependencies", + "language": "Language", + "choose": "Choose", + "search": "Search", + "notification": "Notification", + "notificationError": "Error", + "active": "Active", + "activate": "Activate", + "deactivate": "Deactivate", + "on": "On", + "off": "Off", + "back": "Back", + "from": "From", + "to": "To", + "view": "View", + "resultCount": "Result count", + "paginationNavigation": "Pagination navigation", + "gotoPage": "Goto page", + "name": "Name", + "idCode": "ID code", + "status": "Status", + "statusChangeQuestion": "Would you like to change your status to \"present\"?", + "yes": "Yes", + "no": "No", + "removeValidation": "Are you sure?", + "startDate": "Start date", + "endDate": "End date", + "preview": "Preview", + "logout": "Logout", + "anonymous": "Anonymous", + "csaStatus": "Customer support status", + "present": "Present", + "away": "Away", + "today": "Today", + "forward": "Forward", + "chosen": "Chosen", + "read": "Read" + }, + "mainMenu": { + "menuLabel": "Main navigation", + "closeMenu": "Close menu", + "openIcon": "Open menu icon", + "closeIcon": "Close menu icon" + }, + "menu": { + "conversations": "Conversations", + "unanswered": "Unanswered", + "active": "Active", + "history": "History", + "training": "Training", + "themes": "Themes", + "answers": "Answers", + "userStories": "User Stories", + "configuration": "Configuration", + "forms": "Forms", + "slots": "Slots", + "historicalConversations": "Historical Conversations", + "modelBankAndAnalytics": "Model Bank And Analytics", + "overviewOfTopics": "Overview Of Topics", + "comparisonOfModels": "Comparison Of Models", + "appeals": "Appeals", + "testTracks": "Test Tracks", + "trainNewModel": "Train New Model", + "analytics": "Analytics", + "settings": "Settings", + "overview": "Overview", + "chats": "Chats", + "burokratt": "Bürokratt", + "feedback": "Feedback", + "advisors": "Advisors", + "reports": "Open Data", + "users": "Users", + "administration": "Administration", + "chatbot": "Chatbot", + "welcomeMessage": "Welcome Message", + "appearanceAndBehavior": "Appearance And Behavior", + "emergencyNotices": "Emergency Notices", + "officeOpeningHours": "Office Opening Hours", + "sessionLength": "Session Length", + "monitoring": "Monitoring", + "workingHours": "Working hours" + }, + "chat": { + "reply": "Reply", + "unansweredChats": "Unanswered chats", + "unanswered": "Unanswered", + "forwarded": "Forwarded", + "endUser": "End user name", + "endUserId": "End user id", + "csaName": "Client support name", + "endUserEmail": "End User Email", + "endUserPhoneNumber": "End User Phone", + "startedAt": "Chat started at", + "device": "Device", + "location": "Location", + "redirectedMessageByOwner": "{{from}} forwarded chat to {{to}} {{date}}", + "redirectedMessageClaimed": "{{to}} took over chat from {{from}} {{date}}", + "redirectedMessage": "{{user}} forwarded chat from {{from}} to {{to}} {{date}}", + "new": "New", + "inProcess": "In Process", + "status": { + "active": "Active", + "ended": "Unspecified" + }, + "chatStatus": "Chat status", + "changeStatus": "Change status", + "active": { + "list": "Active chat list", + "myChats": "My chats", + "newChats": "New Chats", + "chooseChat": "Choose a chat to begin", + "endChat": "End chat", + "takeOver": "Take Over", + "askAuthentication": "Ask for authentication", + "askForContact": "Ask for contact", + "askPermission": "Ask permission", + "forwardToColleague": "Forward to colleague", + "forwardToOrganization": "Forward to organization", + "startedAt": "Chat started at {{date}}", + "forwardChat": "Who to forward the chat?", + "searchByName": "Search by name", + "onlyActiveAgents": "Show only active client support agents", + "establishment": "Establishment", + "searchByEstablishmentName": "Search by establishment name", + "sendToEmail": "Send to email", + "chooseChatStatus": "Choose chat status", + "statusChange": "Chat status change", + "startService": "Start a service", + "selectService": "Select a service", + "start": "Start", + "service": "Service", + "ContactedUser": "Contacted User", + "couldNotReachUser": "Could Not Reach User" + }, + "history": { + "title": "History", + "searchChats": "Search chats", + "startTime": "Start time", + "endTime": "End time", + "csaName": "Customer support name", + "contact": "Contact", + "comment": "Comment", + "label": "Label", + "nps": "NPS", + "forwarded": "Forwarded", + "addACommentToTheConversation": "Add a comment to the conversation", + "rating": "Rating", + "feedback": "Feedback" + }, + "plainEvents": { + "answered": "Answered", + "terminated": "Unspecified", + "sent_to_csa_email": "Chat sent to CSA email", + "client-left": "Client left", + "client_left_with_accepted": "Client left with accepted response", + "client_left_with_no_resolution": "Client left with no resolution", + "client_left_for_unknown_reasons": "Client left for unknown reason", + "accepted": "Accepted response", + "hate_speech": "Hate speech", + "other": "Other reasons", + "response_sent_to_client_email": "Response was sent to client email", + "greeting": "Greetings", + "requested-authentication": "Requested authentication", + "authentication_successful": "Authentication successful", + "authentication_failed": "Authentication failed", + "ask-permission": "Asked permission", + "ask-permission-accepted": "Permission accepted", + "ask-permission-rejected": "Permission rejected", + "ask-permission-ignored": "Permission ignored", + "rating": "Rating", + "contact-information": "Requested contact information", + "contact-information-rejected": "Contact information rejected", + "contact-information-fulfilled": "Contact information fulfilled", + "requested-chat-forward": "Requested chat forward", + "requested-chat-forward-accepted": "Requested chat forward accepted", + "requested-chat-forward-rejected": "Requested chat forward rejected", + "inactive-chat-ended": "Ended cue to inactivity", + "contact-information-skipped": "Contact Information Skipped", + "unavailable-contact-information-fulfilled": "Contact information provided", + "unavailable_organization": "Organization Unavailable", + "unavailable_csas": "CSA's Unavailable", + "unavailable_holiday": "Holiday", + "message-read": "Read", + "user-reached": "User contacted", + "user-not-reached": "User could not be reached" + }, + "events": { + "answered": "Answered {{date}}", + "terminated": "Unspecified {{date}}", + "sent_to_csa_email": "Chat sent to CSA email {{date}}", + "client-left": "Client left {{date}}", + "client_left_with_accepted": "Client left with accepted response {{date}}", + "client_left_with_no_resolution": "Client left with no resolution {{date}}", + "client_left_for_unknown_reasons": "Client left for unknown reason {{date}}", + "accepted": "Accepted response {{date}}", + "hate_speech": "Hate speech {{date}}", + "other": "Other reasons {{date}}", + "response_sent_to_client_email": "Response was sent to client email {{date}}", + "greeting": "Greetings {{date}}", + "requested-authentication": "Requested authentication {{date}}", + "authentication_successful": "Authentication successful {{date}}", + "authentication_failed": "Authentication failed {{date}}", + "ask-permission": "Asked permission {{date}}", + "ask-permission-accepted": "Permission accepted {{date}}", + "ask-permission-rejected": "Permission rejected {{date}}", + "ask-permission-ignored": "Permission ignored {{date}}", + "rating": "Rating {{date}}", + "contact-information": "Requested contact information {{date}}", + "contact-information-rejected": "Contact information rejected {{date}}", + "contact-information-fulfilled": "Contact information fulfilled {{date}}", + "requested-chat-forward": "Requested chat forward {{date}}", + "requested-chat-forward-accepted": "Requested chat forward accepted {{date}}", + "requested-chat-forward-rejected": "Requested chat forward rejected {{date}}", + "message-read": "Read", + "contact-information-skipped": "Contact Information Skipped", + "unavailable-contact-information-fulfilled": "Contact information provided", + "unavailable_organization": "Organization Unavailable", + "unavailable_csas": "CSA's Unavailable", + "unavailable_holiday": "Holiday", + "pending-assigned": "{{name}} assigned to contact user", + "user-reached": "{{name}} contacted the user", + "user-not-reached": "{{name}} could not reach the user", + "user-authenticated": "{{name}} is authenticated {{date}}" + } + }, + "roles": { + "ROLE_ADMINISTRATOR": "Administrator", + "ROLE_SERVICE_MANAGER": "Service manager", + "ROLE_CUSTOMER_SUPPORT_AGENT": "Customer support", + "ROLE_CHATBOT_TRAINER": "Chatbot trainer", + "ROLE_ANALYST": "Analyst", + "ROLE_UNAUTHENTICATED": "Unauthenticated" + }, + "settings": { + "title": "Settings", + "users": { + "title": "Users", + "name": "Name", + "idCode": "ID code", + "role": "Role", + "displayName": "Display Name", + "userTitle": "User title", + "email": "E-mail", + "addUser": "Add user", + "editUser": "Edit user", + "deleteUser": "Delete user", + "fullName": "First- and lastname", + "userRoles": "User role(s)", + "autoCorrector": "Auto corrector", + "emailNotifications": "Notifications to e-mail", + "soundNotifications": "Sound notifications", + "popupNotifications": "Popup notifications", + "newUnansweredChat": "New unanswered chat", + "newForwardedChat": "New forwarded chat", + "useAutocorrect": "Text auto corrector", + "required": "Required", + "invalidemail": "Invalid email", + "invalidIdCode": "Invalid Id Code", + "idCodePlaceholder": "The personal identification number must start with a country prefix, (eg.EE12345678910)", + "choose": "Choose", + "userExists": "User Exists" + }, + "chat": { + "chatActive": "Chatbot active", + "showSupportName": "Show support name", + "showSupportTitle": "Show support title" + }, + "emergencyNotices": { + "title": "Emergency notices", + "noticeActive": "Notice active", + "notice": "Notice", + "displayPeriod": "Display period", + "noticeChanged": "Emergency notice settings changed successfully" + }, + "appearance": { + "title": "Appearance and behavior", + "widgetBubbleMessageText": "Widget bubble message text", + "widgetProactiveSeconds": "Widget proactive seconds", + "widgetDisplayBubbleMessageSeconds": "Widget bubble message seconds", + "widgetColor": "Widget color", + "widgetAnimation": "Widget animation" + }, + "workingTime": { + "title": "Organization working time", + "description": "After the end of the working hours, if the chatbot cannot answer the customer's questions by itself, it will ask for the customer's contact details.", + "openFrom": "Open from", + "openUntil": "Open until", + "publicHolidays": "Consider public holidays", + "consider": "Consider", + "dontConsider": "Don't Consider", + "closedOnWeekends": "Closed on weekends", + "theSameOnAllWorkingDays": "The same on all working days", + "open": "Open", + "closed": "Closed", + "until": "Until", + "allWeekdaysExceptWeekend": "M-F", + "allWeekdays": "M-S" + }, + "userSession": { + "title": "User session", + "sessionLength": "Session length", + "description": "User session length, after which inactive users are logged out.", + "rule": "Session length is allowed between 30 min - 480 min (8h)", + "minutes": "Minutes", + "sessionChanged": "Session length changed successfully", + "emptySession": "Session length can't be empty", + "invalidSession": "Session length must be between 30 and 480 minuites" + }, + "greeting": { + "title": "Greeting message" + }, + "weekdays": { + "label": "Weekdays", + "monday": "Monday", + "tuesday": "Tuesday", + "wednesday": "Wednesday", + "thursday": "Thursday", + "friday": "Friday", + "saturday": "Saturday", + "sunday": "Sunday" + }, + "nationalHolidays": "National holidays", + "welcomeMessage": { + "welcomeMessage": "Welcome Message", + "description": "The bureaucrat's automatic welcome message that is displayed as the first message when opening a chat window.", + "greetingActive": "Greeting Active", + "messageChanged": "Welcome message changed successfully", + "emptyMessage": "Welcome message can't be empty" + } + }, + "monitoring": { + "uptime": { + "title": "Uptime", + "daysAgo": "{{days}} ago", + "uptimePercent": "{{percent}}% uptime" + } + }, + "toast": { + "success": { + "updateSuccess": "Updated Successfully", + "messageToUserEmail": "Message sent to user email", + "chatStatusChanged": "Chat status changed", + "chatCommentChanged": "Chat comment changed", + "copied": "Copied", + "userDeleted": "User deleted", + "newUserAdded": "New user added", + "userUpdated": "User updated" + } + } +} diff --git a/GUI/translations/et/common.json b/GUI/translations/et/common.json new file mode 100644 index 00000000..7d70424b --- /dev/null +++ b/GUI/translations/et/common.json @@ -0,0 +1,359 @@ +{ + "global": { + "save": "Salvesta", + "add": "Lisa", + "edit": "Muuda", + "delete": "Kustuta", + "cancel": "Tühista", + "modifiedAt": "Viimati muudetud", + "addNew": "Lisa uus", + "dependencies": "Sõltuvused", + "language": "Keel", + "choose": "Vali", + "search": "Otsi", + "notification": "Teade", + "notificationError": "Veateade", + "active": "Aktiivne", + "activate": "Aktiveeri", + "deactivate": "Deaktiveeri", + "on": "Sees", + "off": "Väljas", + "back": "Tagasi", + "from": "Alates", + "to": "Kuni", + "view": "Vaata", + "resultCount": "Kuvan korraga", + "paginationNavigation": "Lehtedel navigeerimine", + "gotoPage": "Mine lehele", + "name": "Nimi", + "idCode": "Isikukood", + "status": "Staatus", + "statusChangeQuestion": "Kas soovid muuta staatuseks \"kohal\"?", + "yes": "Jah", + "no": "Ei", + "removeValidation": "Oled sa kindel?", + "startDate": "Algusaeg", + "endDate": "Lõppaeg", + "preview": "Eelvaade", + "logout": "Logi välja", + "anonymous": "Anonüümne", + "csaStatus": "Nõustaja", + "present": "Kohal", + "away": "Eemal", + "today": "Täna", + "forward": "Suuna", + "chosen": "Valitud", + "read": "Loetud" + }, + "mainMenu": { + "menuLabel": "Põhinavigatsioon", + "closeMenu": "Kitsendan menüü", + "openMenuIcon": "Ava menüü ikoon", + "closeMenuIcon": "Sulge menüü ikoon" + }, + "menu": { + "conversations": "Vestlused", + "unanswered": "Vastamata", + "active": "Aktiivsed", + "history": "Ajalugu", + "training": "Treening", + "themes": "Teemad", + "answers": "Vastused", + "userStories": "Vestlusvood", + "configuration": "Seaded", + "forms": "Vormid", + "slots": "Pilud", + "historicalConversations": "Ajaloolised vestlused", + "modelBankAndAnalytics": "Mudelipank ja analüütika", + "overviewOfTopics": "Teemade ülevaade", + "comparisonOfModels": "Mudelite võrdlus", + "appeals": "Pöördumised", + "testTracks": "Testlood", + "trainNewModel": "Treeni uus mudel", + "analytics": "Analüütika", + "settings": "Seaded", + "overview": "Ülevaade", + "chats": "Vestlused", + "burokratt": "Bürokratt", + "feedback": "Tagasiside", + "advisors": "Nõustajad", + "reports": "Avaandmed", + "users": "Kasutajad", + "administration": "Haldus", + "chatbot": "Vestlusbot", + "welcomeMessage": "Tervitussõnum", + "appearanceAndBehavior": "Välimus ja käitumine", + "emergencyNotices": "Erakorralised teated", + "officeOpeningHours": "Asutuse tööaeg", + "sessionLength": "Sessiooni pikkus", + "monitoring": "Seire", + "workingHours": "Tööaeg" + }, + "chat": { + "reply": "Vasta", + "unansweredChats": "Vastamata vestlused", + "unanswered": "Vastamata", + "forwarded": "Suunatud", + "endUser": "Vestleja nimi", + "endUserId": "Vestleja isikukood", + "csaName": "Nõustaja nimi", + "endUserEmail": "Vestleja e-post", + "endUserPhoneNumber": "Vestleja telefoninumber", + "startedAt": "Vestlus alustatud", + "device": "Seade", + "location": "Lähtekoht", + "redirectedMessageByOwner": "{{from}} suunas vestluse kasutajale {{to}} {{date}}", + "redirectedMessageClaimed": "{{to}} võttis vestluse üle kasutajalt {{from}} {{date}}", + "redirectedMessage": "{{user}} suunas vestluse kasutajalt {{from}} kasutajale {{to}} {{date}}", + "new": "uued", + "inProcess": "töös", + "status": { + "active": "Aktiivne", + "ended": "Määramata" + }, + "chatStatus": "Vestluse staatus", + "changeStatus": "Muuda staatust", + "active": { + "list": "Aktiivsed vestlused", + "myChats": "Minu vestlused", + "newChats": "Uued vestlused", + "chooseChat": "Alustamiseks vali vestlus", + "endChat": "Lõpeta vestlus", + "takeOver": "Võta üle", + "askAuthentication": "Küsi autentimist", + "askForContact": "Küsi kontaktandmeid", + "askPermission": "Küsi nõusolekut", + "forwardToColleague": "Suuna kolleegile", + "forwardToOrganization": "Suuna asutusele", + "startedAt": "Vestlus alustatud {{date}}", + "forwardChat": "Kellele vestlus suunata?", + "searchByName": "Otsi nime või tiitli järgi", + "onlyActiveAgents": "Näita ainult kohal olevaid nõustajaid", + "establishment": "Asutus", + "searchByEstablishmentName": "Otsi asutuse nime järgi", + "sendToEmail": "Saada e-posti", + "chooseChatStatus": "Vali vestluse staatus", + "statusChange": "Vestluse staatus", + "startService": "Alusta teenust", + "selectService": "Vali teenus", + "start": "Alusta", + "service": "Teenus", + "ContactedUser": "Kasutajaga ühendust võetud", + "couldNotReachUser": "Kasutajaga ei õnnestunud ühendust saada" + }, + "history": { + "title": "Ajalugu", + "searchChats": "Otsi üle vestluste", + "startTime": "Algusaeg", + "endTime": "Lõppaeg", + "csaName": "Nõustaja nimi", + "contact": "Kontaktandmed", + "comment": "Kommentaar", + "label": "Märksõna", + "nps": "Soovitusindeks", + "forwarded": "Suunatud", + "addACommentToTheConversation": "Lisa vestlusele kommentaar", + "rating": "Hinnang", + "feedback": "Tagasiside" + }, + "plainEvents": { + "answered": "Vastatud", + "terminated": "Määramata", + "sent_to_csa_email": "Vestlus saadetud nõustaja e-posti", + "client-left": "Klient lahkus", + "client_left_with_accepted": "Klient lahkus aktsepteeritud vastusega", + "client_left_with_no_resolution": "Klient lahkus vastuseta", + "client_left_for_unknown_reasons": "Klient lahkus määramata põhjustel", + "accepted": "Aktsepteeritud", + "hate_speech": "Vihakõne", + "other": "Muud põhjused", + "response_sent_to_client_email": "Kliendile vastati tema jäetud kontaktile", + "greeting": "Tervitus", + "requested-authentication": "Autentimine algatatud", + "authentication_successful": "Autentimine õnnestus", + "authentication_failed": "Autentimine ebaõnnestus", + "ask-permission": "Küsiti nõusolekut", + "ask-permission-accepted": "Nõusolek antud", + "ask-permission-rejected": "Nõusolekust keelduti", + "ask-permission-ignored": "Nõusolek ignoreeritud", + "rating": "Hinnang", + "contact-information": "Küsiti kontaktandmeid", + "contact-information-rejected": "Kontaktandmetest keeldutud", + "contact-information-fulfilled": "Kontaktandmed saadetud", + "requested-chat-forward": "Küsiti vestluse suunamist", + "requested-chat-forward-accepted": "Vestluse suunamine aktsepteeritud", + "requested-chat-forward-rejected": "Vestluse suunamine tagasi lükatud", + "inactive-chat-ended": "Lõpetatud tegevusetuse tõttu", + "message-read": "Loetud", + "contact-information-skipped": "Kontaktandmeid ei saadetud", + "unavailable-contact-information-fulfilled": "Kontaktandmed on antud", + "unavailable_organization": "Organisatsioon pole saadaval", + "unavailable_csas": "Nõustajad pole saadaval", + "unavailable_holiday": "Puhkus", + "user-reached": "Kasutajaga võeti ühendust", + "user-not-reached": "Kasutajaga ei õnnestunud ühendust saada" + }, + "events": { + "answered": "Vastatud {{date}}", + "terminated": "Määramata {{date}}", + "sent_to_csa_email": "Vestlus saadetud nõustaja e-posti {{date}}", + "client-left": "Klient lahkus {{date}}", + "client_left_with_accepted": "Klient lahkus aktsepteeritud vastusega {{date}}", + "client_left_with_no_resolution": "Klient lahkus vastuseta {{date}}", + "client_left_for_unknown_reasons": "Klient lahkus määramata põhjustel {{date}}", + "accepted": "Aktsepteeritud {{date}}", + "hate_speech": "Vihakõne {{date}}", + "other": "Muud põhjused {{date}}", + "response_sent_to_client_email": "Kliendile vastati tema jäetud kontaktile {{date}}", + "greeting": "Tervitus", + "requested-authentication": "Küsiti autentimist", + "authentication_successful": "Autentimine õnnestus {{date}}", + "authentication_failed": "Autentimine ebaõnnestus {{date}}", + "ask-permission": "Küsiti nõusolekut", + "ask-permission-accepted": "Nõusolek antud {{date}}", + "ask-permission-rejected": "Nõusolekust keeldutud {{date}}", + "ask-permission-ignored": "Nõusolek ignoreeritud {{date}}", + "rating": "Hinnang {{date}}", + "contact-information": "Küsiti kontaktandmeid {{date}}", + "contact-information-rejected": "Kontaktandmetest keeldutud {{date}}", + "contact-information-fulfilled": "Kontaktandmed saadetud {{date}}", + "requested-chat-forward": "Küsiti vestluse suunamist", + "requested-chat-forward-accepted": "Vestluse suunamine aktsepteeritud {{date}}", + "requested-chat-forward-rejected": "Vestluse suunamine tagasi lükatud {{date}}", + "message-read": "Loetud", + "contact-information-skipped": "Kontaktandmeid pole esitatud", + "unavailable-contact-information-fulfilled": "Kontaktandmed on antud", + "unavailable_organization": "Organisatsioon pole saadaval", + "unavailable_csas": "Nõustajad pole saadaval", + "unavailable_holiday": "Puhkus", + "pending-assigned": "{{name}} määratud kontaktkasutajale", + "user-reached": "{{name}} võttis kasutajaga ühendust", + "user-not-reached": "{{name}} ei saanud kasutajaga ühendust", + "user-authenticated": "{{name}} on autenditud {{date}}" + } + }, + "roles": { + "ROLE_ADMINISTRATOR": "Administraator", + "ROLE_SERVICE_MANAGER": "Teenuse haldur", + "ROLE_CUSTOMER_SUPPORT_AGENT": "Nõustaja", + "ROLE_CHATBOT_TRAINER": "Vestlusroboti treener", + "ROLE_ANALYST": "Analüütik", + "ROLE_UNAUTHENTICATED": "Autentimata" + }, + "settings": { + "title": "Seaded", + "users": { + "title": "Kasutajad", + "name": "Nimi", + "idCode": "Isikukood", + "role": "Roll", + "displayName": "Kuvatav nimi", + "userTitle": "Tiitel", + "email": "E-post", + "addUser": "Lisa kasutaja", + "editUser": "Muuda kasutajat", + "deleteUser": "Kustuta kasutaja", + "fullName": "Ees- ja perekonnanimi", + "userRoles": "Kasutaja roll(id)", + "autoCorrector": "Autokorrektor", + "emailNotifications": "Märguanded e-postile", + "soundNotifications": "Häälmärguanded", + "popupNotifications": "Pop-up märguanded", + "newUnansweredChat": "Uus vastamata vestlus", + "newForwardedChat": "Uus suunatud vestlus", + "useAutocorrect": "Teksti autokorrektor", + "required": "Nõutud", + "invalidemail": "Kehtetu e-posti", + "invalidIdCode": "Kehtetu isikukood", + "idCodePlaceholder": "Isikukood peab algama riigi eesliitega, (eg.EE12345678910)", + "choose": "Vali", + "userExists": "Kasutaja on olemas" + }, + "chat": { + "chatActive": "Vestlusrobot aktiivne", + "showSupportName": "Kuva nõustaja nimi", + "showSupportTitle": "Kuva nõustaja tiitel" + }, + "emergencyNotices": { + "title": "Erakorralised teated", + "noticeActive": "Teade aktiivne", + "notice": "Teade", + "displayPeriod": "Kuvamisperiood", + "noticeChanged": "Teate seadeid muudeti edukalt" + }, + "appearance": { + "title": "Välimus ja käitumine", + "widgetBubbleMessageText": "Märguandesõnum", + "widgetProactiveSeconds": "Animatsiooni algus sekundites", + "widgetDisplayBubbleMessageSeconds": "Märguandesõnumi aeg sekundites", + "widgetColor": "Põhivärv", + "widgetAnimation": "Animatsioon" + }, + "workingTime": { + "title": "Asutuse tööaeg", + "description": "Peale tööaja lõppemist, juhul kui juturobot ise kliendi küsimustele vastata ei oska, küsib ta kliendi kontaktandmeid.", + "openFrom": "Avatud alates", + "openUntil": "Avatud kuni", + "publicHolidays": "Arvesta riigipühadega", + "consider": "Arvesta", + "dontConsider": "Ära arvesta", + "closedOnWeekends": "Nädalavahetustel suletud", + "theSameOnAllWorkingDays": "Kõigil tööpäevadel sama", + "open": "Avatud", + "closed": "Suletud", + "until": "kuni", + "allWeekdaysExceptWeekend": "E-R", + "allWeekdays": "E-P" + }, + "userSession": { + "title": "Kasutaja sessioon", + "sessionLength": "Sessiooni pikkus", + "description": "Kasutaja sessiooni pikkus, mille möödumisel logitakse mitteaktiivsed kasutajad välja", + "rule": "Sessiooni pikkus on lubatud vahemikus 30 min - 480 min (8h)", + "minutes": "minutit", + "sessionChanged": "Seansi pikkuse muutmine õnnestus", + "emptySession": "Seansi pikkuse väli ei tohi olla tühi", + "invalidSession": "Seansi pikkus peab olema vahemikus 30 - 480 minutit" + }, + "greeting": { + "title": "Tervitussõnum" + }, + "weekdays": { + "label": "Nädalapäevad", + "monday": "Esmaspäev", + "tuesday": "Teisipäev", + "wednesday": "Kolmapäev", + "thursday": "Neljapäev", + "friday": "Reede", + "saturday": "Laupäev", + "sunday": "Pühapäev" + }, + "nationalHolidays": "Riiklikud pühad", + "welcomeMessage": { + "welcomeMessage": "Tervitussõnum", + "description": "Bürokrati automaatne tervitussõnum, mida kuvatakse esimese sõnumina vestlusakna avamisel", + "greetingActive": "Tervitus aktiivne", + "messageChanged": "Tervitust muudeti edukalt", + "emptyMessage": "Tervitussõnumi väli ei tohi olla tühi" + } + }, + "monitoring": { + "uptime": { + "title": "Tööaeg", + "daysAgo": "{{days}} päeva tagasi", + "uptimePercent": "{{percent}}% tööaeg" + } + }, + "toast": { + "success": { + "updateSuccess": "Värskendamine õnnestus", + "messageToUserEmail": "Sõnum saadeti kasutaja e-posti", + "chatStatusChanged": "Vestluse staatus muudetud", + "chatCommentChanged": "Vestluse kommentaar muudetud", + "copied": "Kopeeritud", + "userDeleted": "Kasutaja kustutatud", + "newUserAdded": "Uus kasutaja lisatud", + "userUpdated": "Kasutaja uuendatud" + } + } +} diff --git a/GUI/tsconfig.json b/GUI/tsconfig.json index a7fc6fbf..3dd4695d 100644 --- a/GUI/tsconfig.json +++ b/GUI/tsconfig.json @@ -1,25 +1,36 @@ { "compilerOptions": { - "target": "ES2020", + "target": "ESNext", "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", + "lib": [ + "DOM", + "DOM.Iterable", + "ESNext" + ], + "allowJs": false, "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true + "baseUrl": "src", + "types": [ + "vite/client", + "node" + ] }, - "include": ["src"], - "references": [{ "path": "./tsconfig.node.json" }] + "include": [ + "src" + ], + "references": [ + { + "path": "./tsconfig.node.json" + } + ] } diff --git a/GUI/tsconfig.node.json b/GUI/tsconfig.node.json index 97ede7ee..9d31e2ae 100644 --- a/GUI/tsconfig.node.json +++ b/GUI/tsconfig.node.json @@ -1,11 +1,9 @@ { "compilerOptions": { "composite": true, - "skipLibCheck": true, "module": "ESNext", - "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true, - "strict": true + "moduleResolution": "Node", + "allowSyntheticDefaultImports": true }, "include": ["vite.config.ts"] } diff --git a/GUI/vite.config.ts b/GUI/vite.config.ts index 5a33944a..c1c65994 100644 --- a/GUI/vite.config.ts +++ b/GUI/vite.config.ts @@ -1,7 +1,43 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import tsconfigPaths from 'vite-tsconfig-paths'; +import svgr from 'vite-plugin-svgr'; +import path from 'path'; +import { removeHiddenMenuItems } from './vitePlugin'; // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react()], -}) + envPrefix: 'REACT_APP_', + plugins: [ + react(), + tsconfigPaths(), + svgr(), + { + name: 'removeHiddenMenuItemsPlugin', + transform: (str, id) => { + if(!id.endsWith('/menu-structure.json')) + return str; + return removeHiddenMenuItems(str); + }, + }, + ], + base: 'chat', + build: { + outDir: './build', + target: 'es2015', + emptyOutDir: true, + }, + server: { + headers: { + ...(process.env.REACT_APP_CSP && { + 'Content-Security-Policy': process.env.REACT_APP_CSP, + }), + }, + }, + resolve: { + alias: { + '~@fontsource': path.resolve(__dirname, 'node_modules/@fontsource'), + '@': `${path.resolve(__dirname, './src')}`, + }, + }, +}); diff --git a/GUI/vitePlugin.js b/GUI/vitePlugin.js new file mode 100644 index 00000000..80cf7a0e --- /dev/null +++ b/GUI/vitePlugin.js @@ -0,0 +1,25 @@ +export function removeHiddenMenuItems(str) { + const badJson = str.replace('export default [', '[').replace('];', ']'); + const correctJson = badJson.replace(/(['"])?([a-z0-9A-Z_]+)(['"])?:/g, '"$2": '); + + const isHiddenFeaturesEnabled = + process.env.REACT_APP_ENABLE_HIDDEN_FEATURES?.toLowerCase().trim() == 'true' || + process.env.REACT_APP_ENABLE_HIDDEN_FEATURES?.toLowerCase().trim() == '1'; + + const json = removeHidden(JSON.parse(correctJson), isHiddenFeaturesEnabled); + + const updatedJson = JSON.stringify(json); + + return 'export default ' + updatedJson + ';' +} + +function removeHidden(menuItems, isHiddenFeaturesEnabled) { + if(!menuItems) return menuItems; + const arr = menuItems + ?.filter(x => !x.hidden) + ?.filter(x => isHiddenFeaturesEnabled || x.hiddenMode !== "production"); + for (const a of arr) { + a.children = removeHidden(a.children, isHiddenFeaturesEnabled); + } + return arr; +} From d2b1d7450a84311cf8433bf2fc87e91265e3be26 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Tue, 18 Jun 2024 22:52:17 +0530 Subject: [PATCH 009/582] classifier-93 fix encode issue and implement Put API for subscription --- DSL/DMapper/hbs/get_auth_header.handlebars | 2 +- DSL/DMapper/lib/helpers.js | 1 - docker-compose.yml | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DSL/DMapper/hbs/get_auth_header.handlebars b/DSL/DMapper/hbs/get_auth_header.handlebars index 94acdff6..e94d1d22 100644 --- a/DSL/DMapper/hbs/get_auth_header.handlebars +++ b/DSL/DMapper/hbs/get_auth_header.handlebars @@ -1,4 +1,4 @@ { - "val": "{{getAuthHeader username token}}" + "val": "{{{getAuthHeader username token}}}" } diff --git a/DSL/DMapper/lib/helpers.js b/DSL/DMapper/lib/helpers.js index d906c33c..003bc074 100644 --- a/DSL/DMapper/lib/helpers.js +++ b/DSL/DMapper/lib/helpers.js @@ -15,6 +15,5 @@ export function verifySignature(payload, headers) { export function getAuthHeader(username, token) { const auth = `${username}:${token}`; const encodedAuth = Buffer.from(auth).toString("base64"); - console.log("encodedAuth: " + encodedAuth); return `Basic ${encodedAuth}`; } diff --git a/docker-compose.yml b/docker-compose.yml index abfa12fd..863aaaac 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,6 +7,7 @@ services: - application.httpCodesAllowList=200,201,202,400,401,403,500 - application.internalRequests.allowedIPs=127.0.0.1 - application.logging.displayRequestContent=true + - application.incomingRequests.allowedMethodTypes=POST,GET,PUT - application.logging.displayResponseContent=true - server.port=8086 volumes: From 8255274b4da0366af3e1d1e3de11bb39925f2870 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Tue, 18 Jun 2024 22:53:04 +0530 Subject: [PATCH 010/582] classifier-93 fix encode issue and implement Put API for subscription --- .../integration/jira/cloud/connect.yml | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/connect.yml b/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/connect.yml index 5b02e858..d731fe6b 100644 --- a/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/connect.yml +++ b/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/connect.yml @@ -36,32 +36,26 @@ validate_integration: next: disconnect_jira connect_jira: - call: http.get + call: http.put args: - url: "[#JIRA_CLOUD_DOMAIN]/rest/webhooks/1.0/webhook" + url: "[#JIRA_CLOUD_DOMAIN]/rest/webhooks/1.0/webhook/1" headers: - Accept: "application/json" - Content-Type: "application/json" - Authorization: ${auth_header.val} + Authorization: ${auth_header.response.body.val} + body: + enabled: true result: valid_data next: output_val disconnect_jira: - call: http.post + call: http.put args: - url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/verify_signature" + url: "[#JIRA_CLOUD_DOMAIN]/rest/webhooks/1.0/webhook/1" headers: - type: json + Authorization: ${auth_header.response.body.val} body: - payload: ${payload} - headers: ${headers} + enabled: false result: valid_data next: output_val output_val: - return: ${valid_data} - - - - - + return: ${valid_data} \ No newline at end of file From d14272298473fda4abe31564045719fd44490e31 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Wed, 19 Jun 2024 02:04:14 +0530 Subject: [PATCH 011/582] feat: integration UIs --- GUI/src/App.tsx | 3 + GUI/src/assets/Jira.tsx | 55 +++++++++++++ GUI/src/assets/Outlook.tsx | 25 ++++++ GUI/src/assets/Pinal.tsx | 42 ++++++++++ GUI/src/components/Card/Card.scss | 7 +- GUI/src/components/Card/index.tsx | 4 +- .../IntegrationCard/IntegrationCard.scss | 78 +++++++++++++++++++ .../molecules/IntegrationCard/index.tsx | 62 +++++++++++++++ GUI/src/config/rolesConfig.json | 11 ++- GUI/src/pages/Integrations/Integrations.scss | 17 ++++ GUI/src/pages/Integrations/index.tsx | 48 ++++++++++++ .../pages/UserManagement/UserManagement.scss | 56 ++++++------- GUI/src/pages/UserManagement/index.tsx | 27 ++++++- 13 files changed, 401 insertions(+), 34 deletions(-) create mode 100644 GUI/src/assets/Jira.tsx create mode 100644 GUI/src/assets/Outlook.tsx create mode 100644 GUI/src/assets/Pinal.tsx create mode 100644 GUI/src/components/molecules/IntegrationCard/IntegrationCard.scss create mode 100644 GUI/src/components/molecules/IntegrationCard/index.tsx create mode 100644 GUI/src/pages/Integrations/Integrations.scss create mode 100644 GUI/src/pages/Integrations/index.tsx diff --git a/GUI/src/App.tsx b/GUI/src/App.tsx index 46fc7037..893211d6 100644 --- a/GUI/src/App.tsx +++ b/GUI/src/App.tsx @@ -8,6 +8,7 @@ import { UserInfo } from 'types/userInfo'; import './locale/et_EE'; import UserManagement from 'pages/UserManagement'; +import Integrations from 'pages/Integrations'; const App: FC = () => { useQuery<{ @@ -25,6 +26,8 @@ const App: FC = () => { }> } /> } /> + } /> + ); diff --git a/GUI/src/assets/Jira.tsx b/GUI/src/assets/Jira.tsx new file mode 100644 index 00000000..457aab56 --- /dev/null +++ b/GUI/src/assets/Jira.tsx @@ -0,0 +1,55 @@ +const Jira = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + ); +}; +export default Jira; diff --git a/GUI/src/assets/Outlook.tsx b/GUI/src/assets/Outlook.tsx new file mode 100644 index 00000000..5eb0ebbc --- /dev/null +++ b/GUI/src/assets/Outlook.tsx @@ -0,0 +1,25 @@ +const Outlook = () => { + return ( + + + + + + ); +}; +export default Outlook; diff --git a/GUI/src/assets/Pinal.tsx b/GUI/src/assets/Pinal.tsx new file mode 100644 index 00000000..ac74d104 --- /dev/null +++ b/GUI/src/assets/Pinal.tsx @@ -0,0 +1,42 @@ +const Pinal = () => { + return ( + + + + + + + + + + + + + + ); +}; + +export default Pinal; diff --git a/GUI/src/components/Card/Card.scss b/GUI/src/components/Card/Card.scss index 8dbba87e..88b94c73 100644 --- a/GUI/src/components/Card/Card.scss +++ b/GUI/src/components/Card/Card.scss @@ -8,6 +8,7 @@ background-color: get-color(white); border: 1px solid get-color(black-coral-2); border-radius: $veera-radius-s; + margin-bottom: 10px; &--borderless { border: 0; @@ -18,6 +19,10 @@ } } + &--fullWidth { + width: 100%; + } + &__header, &__body, &__footer { @@ -26,7 +31,7 @@ &__header { border-bottom: 1px solid get-color(black-coral-2); - background-color: get-color(extra-light); + background-color: white; border-radius: $veera-radius-s $veera-radius-s 0 0; &.white { diff --git a/GUI/src/components/Card/index.tsx b/GUI/src/components/Card/index.tsx index 128ba81f..6cab8644 100644 --- a/GUI/src/components/Card/index.tsx +++ b/GUI/src/components/Card/index.tsx @@ -9,6 +9,7 @@ type CardProps = { borderless?: boolean; isHeaderLight?: boolean; isBodyDivided?: boolean; + isfullwidth?: boolean; }; const Card: FC> = ({ @@ -18,9 +19,10 @@ const Card: FC> = ({ isHeaderLight, isBodyDivided, children, + isfullwidth, }) => { return ( -
    +
    {header && (
    {header} diff --git a/GUI/src/components/molecules/IntegrationCard/IntegrationCard.scss b/GUI/src/components/molecules/IntegrationCard/IntegrationCard.scss new file mode 100644 index 00000000..ab493fed --- /dev/null +++ b/GUI/src/components/molecules/IntegrationCard/IntegrationCard.scss @@ -0,0 +1,78 @@ +.card_header { + display: flex; + align-items: center; + margin-bottom: 16px; + padding: 0px 10px; +} + +.logo { + width: 120px; + height: 40px; + margin-right: 8px; +} + + +.title h2 { + font-size: 18px; + margin-bottom: 4px; +} + +.title p { + font-size: 14px; + color: #666; +} + +.toggle-switch { + position: relative; + margin-left: auto; +} + +.footer_container { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0px 10px; +} + +.status-indicators { + display: flex; + align-items: center; +} + +.status { + display: flex; + align-items: center; + margin-right: 16px; + font-size: 14px; + color: #333; +} + +.dot { + width: 10px; + height: 10px; + border-radius: 50%; + margin-right: 8px; +} + +.green { + background-color: green; +} + +.grey { + background-color: grey; +} + +.actions { + display: flex; + gap: 10px; + align-items: center; +} + +.connect { + background-color: #007bff; +} + +.disconnect { + background-color: #dc3545; +} \ No newline at end of file diff --git a/GUI/src/components/molecules/IntegrationCard/index.tsx b/GUI/src/components/molecules/IntegrationCard/index.tsx new file mode 100644 index 00000000..41ec2175 --- /dev/null +++ b/GUI/src/components/molecules/IntegrationCard/index.tsx @@ -0,0 +1,62 @@ +import { FC, PropsWithChildren, ReactNode } from 'react'; +import './IntegrationCard.scss'; +import { useTranslation } from 'react-i18next'; +import { Button, Card, Switch } from 'components'; + +type IntegrationCardProps = { + logo?: ReactNode; + channel?: string; + channelDescription?: string; + user?: string; + isActive?:boolean; + status?:string; + }; + +const IntegrationCard: FC> = ({logo,channel,channelDescription,user,isActive,status}) => { + const { t } = useTranslation(); + + return ( + +
    +
    + + connected - Outlook + + + Disconnected - Pinal + +
    +
    + + +
    +
    + + } + > + <> +
    +
    + {logo} +
    +
    +

    {channel}

    +

    + {channelDescription} +

    +

    {user}

    +
    +
    + +
    +
    + +
    + + ); +}; + +export default IntegrationCard; diff --git a/GUI/src/config/rolesConfig.json b/GUI/src/config/rolesConfig.json index b92c748a..35ece24c 100644 --- a/GUI/src/config/rolesConfig.json +++ b/GUI/src/config/rolesConfig.json @@ -1 +1,10 @@ -[{ "label": "Admin", "value": "Admin" },{ "label": "Service Manager", "value": "Service_Manager" }] +[ + { "label": "ROLE_SERVICE_MANAGER", "value": "ROLE_SERVICE_MANAGER" }, + { + "label": "ROLE_CUSTOMER_SUPPORT_AGENT", + "value": "ROLE_CUSTOMER_SUPPORT_AGENT" + }, + { "label": "ROLE_CHATBOT_TRAINER", "value": "ROLE_CHATBOT_TRAINER" }, + { "label": "ROLE_ANALYST", "value": "ROLE_ANALYST" }, + { "label": "ROLE_ADMINISTRATOR", "value": "ROLE_ADMINISTRATOR" } +] diff --git a/GUI/src/pages/Integrations/Integrations.scss b/GUI/src/pages/Integrations/Integrations.scss new file mode 100644 index 00000000..f2e77d05 --- /dev/null +++ b/GUI/src/pages/Integrations/Integrations.scss @@ -0,0 +1,17 @@ +.container { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0px 48px; +} + +.title { + font-size: 1.5rem; + color: #000; + font-weight: 300; +} + +.integration_container{ + padding: 10px 48px; +} + diff --git a/GUI/src/pages/Integrations/index.tsx b/GUI/src/pages/Integrations/index.tsx new file mode 100644 index 00000000..402219aa --- /dev/null +++ b/GUI/src/pages/Integrations/index.tsx @@ -0,0 +1,48 @@ +import { FC } from 'react'; +import './Integrations.scss'; +import { useTranslation } from 'react-i18next'; +import { Button, Card, Switch } from 'components'; +import IntegrationCard from 'components/molecules/IntegrationCard'; +import Jira from 'assets/jira'; +import Outlook from 'assets/Outlook'; +import Pinal from 'assets/Pinal'; + +const Integrations: FC = () => { + const { t } = useTranslation(); + + return ( + <> +
    +
    Integration
    +
    +
    + } + channel={"Jira"} + channelDescription={"Atlassian vea jälgimise ja projektijuhtimise tarkvara"} + user={"Rickey Walker - Admin"} + isActive={true} + status={"Connected"} + /> + } + channel={"Outlook"} + channelDescription={"Atlassian vea jälgimise ja projektijuhtimise tarkvara"} + user={"Rickey Walker - Admin"} + isActive={true} + status={"Connected"} + /> + } + channel={"Outlook+Pinal"} + channelDescription={"Atlassian vea jälgimise ja projektijuhtimise tarkvara"} + user={"Rickey Walker - Admin"} + isActive={true} + status={"Connected"} + /> +
    + + ); +}; + +export default Integrations; diff --git a/GUI/src/pages/UserManagement/UserManagement.scss b/GUI/src/pages/UserManagement/UserManagement.scss index 94b402a6..c5fef484 100644 --- a/GUI/src/pages/UserManagement/UserManagement.scss +++ b/GUI/src/pages/UserManagement/UserManagement.scss @@ -1,29 +1,29 @@ .container { - display: flex; - justify-content: space-between; - align-items: center; - padding: 20px 48px; - } - - .title { - font-size: 1.5rem; - color: #000; - font-weight: 300; - } - - .button { - background-color: #007bff; - padding: 10px 20px; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 1rem; - } - - .button:hover { - background-color: #0056b3; - } - - .form-group { - margin-bottom: 20px; - } \ No newline at end of file + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 48px; +} + +.title { + font-size: 1.5rem; + color: #000; + font-weight: 300; +} + +.button { + background-color: #007bff; + padding: 10px 20px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 1rem; +} + +.button:hover { + background-color: #0056b3; +} + +.form-group { + margin-bottom: 20px; +} diff --git a/GUI/src/pages/UserManagement/index.tsx b/GUI/src/pages/UserManagement/index.tsx index 538ffdb6..234b988d 100644 --- a/GUI/src/pages/UserManagement/index.tsx +++ b/GUI/src/pages/UserManagement/index.tsx @@ -4,7 +4,7 @@ import { DataTable, Dialog, FormInput, - FormSelect, + FormMultiselect, Icon, } from '../../components'; import users from '../../config/users.json'; @@ -172,17 +172,36 @@ const UserManagement: FC = () => { label="First and last name" placeholder="Enter name" name="fullName" - value={editableRow?.fullName} + value={editableRow?.firstName} />
    - + ({ + value: role, + label: role + .replace('ROLE_', '') + .split('_') + .map( + (word) => + word.charAt(0) + word.slice(1).toLowerCase() + ) + .join(' '), + }) + )} + />
    @@ -190,6 +209,7 @@ const UserManagement: FC = () => { label="Title" placeholder="Enter title" name="title" + value={editableRow?.csaTitle} />
    @@ -198,6 +218,7 @@ const UserManagement: FC = () => { placeholder="Enter email" name="email" type="email" + value={editableRow?.csaEmail} />
    From b3a315cdc944b54202d09edeea6516cdb595b058 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Wed, 19 Jun 2024 12:07:17 +0530 Subject: [PATCH 012/582] classifier-93 rest api grooming(attributes ,method names and steps) --- .../integration/jira/cloud/accept.yml | 23 +++--- .../integration/jira/cloud/connect.yml | 61 -------------- .../jira/cloud/toggle-subscription.yml | 79 +++++++++++++++++++ 3 files changed, 93 insertions(+), 70 deletions(-) delete mode 100644 DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/connect.yml create mode 100644 DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/toggle-subscription.yml diff --git a/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml b/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml index b48df027..80d060f2 100644 --- a/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml +++ b/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml @@ -60,24 +60,29 @@ get_jira_issue_info: next: send_issue_data send_issue_data: - call: reflect.mock + call: http.post args: url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/return_jira_issue_info"# need correct url headers: type: json body: info: ${extract_info} - response: - val: "send to mock url" - result: response - next: output_val + result: res + +check_response: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: return_ok + next: return_bad_request -output_val: - return: ${response} +return_ok: + status: 200 + return: "Jira data send successfully" + next: end -return_error_found: - return: ${res.response.message} +return_bad_request: status: 400 + return: "Bad Request" next: end diff --git a/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/connect.yml b/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/connect.yml deleted file mode 100644 index d731fe6b..00000000 --- a/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/connect.yml +++ /dev/null @@ -1,61 +0,0 @@ -declaration: - call: declare - version: 0.1 - description: "Description placeholder for 'CONNECT'" - method: post - accepts: json - returns: json - namespace: classifier - allowlist: - params: - - field: is_connect - type: boolean - description: "Parameter 'isConnect'" - -assign_integration: - assign: - is_connect: ${incoming.params.isConnect} - next: get_auth_header - -get_auth_header: - call: http.post - args: - url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/get_auth_header" - headers: - type: json - body: - username: "[#JIRA_USERNAME]" - token: "[#JIRA_API_TOKEN]" - result: auth_header - next: validate_integration - -validate_integration: - switch: - - condition: ${is_connect === "true"} - next: connect_jira - next: disconnect_jira - -connect_jira: - call: http.put - args: - url: "[#JIRA_CLOUD_DOMAIN]/rest/webhooks/1.0/webhook/1" - headers: - Authorization: ${auth_header.response.body.val} - body: - enabled: true - result: valid_data - next: output_val - -disconnect_jira: - call: http.put - args: - url: "[#JIRA_CLOUD_DOMAIN]/rest/webhooks/1.0/webhook/1" - headers: - Authorization: ${auth_header.response.body.val} - body: - enabled: false - result: valid_data - next: output_val - -output_val: - return: ${valid_data} \ No newline at end of file diff --git a/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/toggle-subscription.yml b/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/toggle-subscription.yml new file mode 100644 index 00000000..fc1adb6b --- /dev/null +++ b/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/toggle-subscription.yml @@ -0,0 +1,79 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'TOGGLE-SUBSCRIPTION'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: is_connect + type: boolean + description: "Body field 'isConnect'" + +extract_request_data: + assign: + is_connect: ${incoming.body.isConnect} + next: get_auth_header + +get_auth_header: + call: http.post + args: + url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/get_auth_header" + headers: + type: json + body: + username: "[#JIRA_USERNAME]" + token: "[#JIRA_API_TOKEN]" + result: auth_header + next: check_integration_type + +check_integration_type: + switch: + - condition: ${is_connect === true} + next: subscribe_jira + next: unsubscribe_jira + +subscribe_jira: + call: http.put + args: + url: "[#JIRA_CLOUD_DOMAIN]/rest/webhooks/1.0/webhook/[#JIRA_WEBHOOK_ID]" + headers: + Authorization: ${auth_header.response.body.val} + body: + enabled: true + result: res + next: assign_jira_webhook_status + +unsubscribe_jira: + call: http.put + args: + url: "[#JIRA_CLOUD_DOMAIN]/rest/webhooks/1.0/webhook/1" + headers: + Authorization: ${auth_header.response.body.val} + body: + enabled: false + result: res + next: assign_jira_webhook_status + +assign_jira_webhook_status: + assign: + status: ${res.response.body.enabled} + next: check_jira_webhook_response + +check_jira_webhook_response: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: return_ok + next: return_bad_request + +return_ok: + status: 200 + return: "webhook service successfully ${status}" + next: end + +return_bad_request: + status: 400 + return: "Bad Request" + next: end \ No newline at end of file From ad16081484ffc2d1960416bbcb56a0b654ddc7fd Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Wed, 19 Jun 2024 12:17:54 +0530 Subject: [PATCH 013/582] classifier-93 code refactoring on file structure level --- .../DSL/POST/classifier/integration/jira/cloud/accept.yml | 0 .../integration/jira/cloud/toggle-subscription.yml | 0 constants.ini | 3 ++- docker-compose.yml | 8 ++++---- 4 files changed, 6 insertions(+), 5 deletions(-) rename DSL/{Ruuter.public => Ruuter.private}/DSL/POST/classifier/integration/jira/cloud/accept.yml (100%) rename DSL/{Ruuter.public => Ruuter.private}/DSL/POST/classifier/integration/jira/cloud/toggle-subscription.yml (100%) diff --git a/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml b/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/accept.yml similarity index 100% rename from DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml rename to DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/accept.yml diff --git a/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/toggle-subscription.yml b/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/toggle-subscription.yml similarity index 100% rename from DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/toggle-subscription.yml rename to DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/toggle-subscription.yml diff --git a/constants.ini b/constants.ini index 91016a4a..d5c06401 100644 --- a/constants.ini +++ b/constants.ini @@ -1,8 +1,9 @@ [DSL] CLASSIFIER_RUUTER_PUBLIC=http://ruuter-public:8086 +CLASSIFIER_RUUTER_PRIVATE=http://ruuter-public:8088 CLASSIFIER_DMAPPER=http://data-mapper:3000 JIRA_API_TOKEN=value JIRA_USERNAME=value JIRA_CLOUD_DOMAIN=value - +JIRA_WEBHOOK_ID=1 diff --git a/docker-compose.yml b/docker-compose.yml index 863aaaac..8287f230 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: ruuter-public: - container_name: ruuter-public + container_name: ruuter-private image: ruuter environment: - application.cors.allowedOrigins=http://localhost:3001 @@ -9,12 +9,12 @@ services: - application.logging.displayRequestContent=true - application.incomingRequests.allowedMethodTypes=POST,GET,PUT - application.logging.displayResponseContent=true - - server.port=8086 + - server.port=8088 volumes: - - ./DSL/Ruuter.public/DSL:/DSL + - ./DSL/Ruuter.private/DSL:/DSL - ./constants.ini:/app/constants.ini ports: - - 8086:8086 + - 8088:8088 networks: - bykstack cpus: "0.5" From a9929962eccbcca6682f565577823b8700440d02 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Thu, 20 Jun 2024 00:28:26 +0530 Subject: [PATCH 014/582] classifier-93 implement update jira labels when recieve response from model: done --- .../hbs/return_label_field_array.handlebars | 9 ++ DSL/DMapper/lib/helpers.js | 34 +++++--- .../integration/jira/cloud/update.yml | 86 +++++++++++++++++++ docker-compose.yml | 2 +- 4 files changed, 120 insertions(+), 11 deletions(-) create mode 100644 DSL/DMapper/hbs/return_label_field_array.handlebars create mode 100644 DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/update.yml diff --git a/DSL/DMapper/hbs/return_label_field_array.handlebars b/DSL/DMapper/hbs/return_label_field_array.handlebars new file mode 100644 index 00000000..9f6b7701 --- /dev/null +++ b/DSL/DMapper/hbs/return_label_field_array.handlebars @@ -0,0 +1,9 @@ +{{#with (mergeLabelData labels existing_labels) as |merged|}} +{ + "labels": [ + {{#each merged.labels}} + "{{this}}"{{#unless @last}},{{/unless}} + {{/each}} + ] +} +{{/with}} diff --git a/DSL/DMapper/lib/helpers.js b/DSL/DMapper/lib/helpers.js index 003bc074..5adbf2fb 100644 --- a/DSL/DMapper/lib/helpers.js +++ b/DSL/DMapper/lib/helpers.js @@ -1,15 +1,18 @@ -import { createHmac,timingSafeEqual } from "crypto"; +import { createHmac, timingSafeEqual } from "crypto"; export function verifySignature(payload, headers) { - const signature = headers['x-hub-signature']; - const SHARED_SECRET = 'wNc6HjKGu3RZXYNXqMTh'; - const hmac = createHmac('sha256', Buffer.from(SHARED_SECRET, 'utf8')); - const payloadString = JSON.stringify(payload); - hmac.update(Buffer.from(payloadString, 'utf8')); - const computedSignature = hmac.digest('hex'); - const computedSignaturePrefixed = "sha256=" + computedSignature; - const isValid = timingSafeEqual(Buffer.from(computedSignaturePrefixed, 'utf8'), Buffer.from(signature, 'utf8')); - return isValid; + const signature = headers["x-hub-signature"]; + const SHARED_SECRET = "wNc6HjKGu3RZXYNXqMTh"; + const hmac = createHmac("sha256", Buffer.from(SHARED_SECRET, "utf8")); + const payloadString = JSON.stringify(payload); + hmac.update(Buffer.from(payloadString, "utf8")); + const computedSignature = hmac.digest("hex"); + const computedSignaturePrefixed = "sha256=" + computedSignature; + const isValid = timingSafeEqual( + Buffer.from(computedSignaturePrefixed, "utf8"), + Buffer.from(signature, "utf8") + ); + return isValid; } export function getAuthHeader(username, token) { @@ -17,3 +20,14 @@ export function getAuthHeader(username, token) { const encodedAuth = Buffer.from(auth).toString("base64"); return `Basic ${encodedAuth}`; } + +export function mergeLabelData(labels, existing_labels) { + // Merge the arrays + let mergedArray = [...labels, ...existing_labels]; + + // Remove duplicates + let uniqueArray = [...new Set(mergedArray)]; + + // Return as JSON object + return { labels: uniqueArray }; +} diff --git a/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/update.yml b/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/update.yml new file mode 100644 index 00000000..c6dbb257 --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/update.yml @@ -0,0 +1,86 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'UPDATE'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: issueKey + type: string + description: "Body field 'issueId'" + - field: labels + type: array + description: "Body field 'labels'" + +extract_request_data: + assign: + issue_key: ${incoming.body.issueKey} + label_list: ${incoming.body.labels} + next: check_for_request_data + +check_for_request_data: + switch: + - condition: ${issue_key == null || label_list == null} + next: return_incorrect_request + next: get_auth_header + +get_auth_header: + call: http.post + args: + url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/get_auth_header" + headers: + type: json + body: + username: "[#JIRA_USERNAME]" + token: "[#JIRA_API_TOKEN]" + result: auth_header + next: get_jira_issue_info + +get_jira_issue_info: + call: http.get + args: + url: "[#JIRA_CLOUD_DOMAIN]/rest/api/3/issue/${issue_key}" + headers: + Authorization: ${auth_header.response.body.val} + result: jira_issue_info + next: assign_existing_labels + +assign_existing_labels: + assign: + existing_label_list: ${jira_issue_info.response.body.fields.labels} + next: merge_labels + +merge_labels: + call: http.post + args: + url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/return_label_field_array" + headers: + type: json + body: + labels: ${label_list} + existing_labels: ${existing_label_list} + result: res + next: set_data + +set_data: + assign: + all_labels: ${res.response.body} + next: update_jira_issue + +update_jira_issue: + call: http.put + args: + url: "[#JIRA_CLOUD_DOMAIN]/rest/api/3/issue/${issue_key}" + headers: + Authorization: ${auth_header.response.body.val} + body: + fields: ${all_labels} + result: value + +return_incorrect_request: + status: 400 + return: 'missing labels' + next: end \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 8287f230..0e81a0d2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ services: image: ruuter environment: - application.cors.allowedOrigins=http://localhost:3001 - - application.httpCodesAllowList=200,201,202,400,401,403,500 + - application.httpCodesAllowList=200,201,202,204,400,401,403,500 - application.internalRequests.allowedIPs=127.0.0.1 - application.logging.displayRequestContent=true - application.incomingRequests.allowedMethodTypes=POST,GET,PUT From 7d10096cfdd7dbd4a533c9df79fbf6d82c24c1e6 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Thu, 20 Jun 2024 00:51:43 +0530 Subject: [PATCH 015/582] feat: added integartion modals --- .../molecules/IntegrationCard/index.tsx | 197 ++++++++++++++---- 1 file changed, 153 insertions(+), 44 deletions(-) diff --git a/GUI/src/components/molecules/IntegrationCard/index.tsx b/GUI/src/components/molecules/IntegrationCard/index.tsx index 41ec2175..dfe8a78b 100644 --- a/GUI/src/components/molecules/IntegrationCard/index.tsx +++ b/GUI/src/components/molecules/IntegrationCard/index.tsx @@ -1,61 +1,170 @@ -import { FC, PropsWithChildren, ReactNode } from 'react'; +import { FC, PropsWithChildren, ReactNode, useState } from 'react'; import './IntegrationCard.scss'; import { useTranslation } from 'react-i18next'; -import { Button, Card, Switch } from 'components'; +import { Button, Card, Dialog, FormInput, Switch } from 'components'; type IntegrationCardProps = { - logo?: ReactNode; - channel?: string; - channelDescription?: string; - user?: string; - isActive?:boolean; - status?:string; - }; + logo?: ReactNode; + channel?: string; + channelDescription?: string; + user?: string; + isActive?: boolean; + status?: string; +}; -const IntegrationCard: FC> = ({logo,channel,channelDescription,user,isActive,status}) => { +const IntegrationCard: FC> = ({ + logo, + channel, + channelDescription, + user, + isActive, + status, +}) => { const { t } = useTranslation(); + const [isModalOpen, setIsModalOpen] = useState(false); + const [modalType, setModalType] = useState('JIRA_INTEGRATION'); return ( - + +
    +
    + + connected - Outlook + + + Disconnected - Pinal + +
    +
    + + +
    +
    + + } + > + <> +
    +
    {logo}
    +
    +

    {channel}

    +

    {channelDescription}

    +

    {user}

    +
    +
    + +
    +
    + +
    + {modalType === 'JIRA_INTEGRATION' && ( + setIsModalOpen(false)} + isOpen={isModalOpen} + title={'Integration with Jira'} footer={ <> -
    -
    - - connected - Outlook - - - Disconnected - Pinal - -
    -
    - - -
    -
    + <> + + + } > - <> -
    -
    - {logo} -
    -
    -

    {channel}

    -

    - {channelDescription} -

    -

    {user}

    +
    +
    +
    +
    -
    - -
    -
    - - - + +
    +
    + )} + {modalType === 'SUCCESS' && ( + setIsModalOpen(false)} + isOpen={isModalOpen} + title={'Integration Successful'} + > +
    + You have successfully connected with Jira! Your integration is now + complete, and you can start working with Jira seamlessly. +
    +
    + )} + {modalType === 'ERROR' && ( + setIsModalOpen(false)} + isOpen={isModalOpen} + title={'Integration Error'} + > +
    + Failed to connect with Jira. Please check your settings and try again. If the problem persists, contact support for assistance. +
    +
    + )} + {modalType === 'DISCONNECT' && ( + setIsModalOpen(false)} + isOpen={isModalOpen} + title={'Are you sure?'} + footer={ + <> + <> + + + + + } + > +
    + Are you sure you want to disconnect the Jira integration? This action cannot be undone and may affect your workflow and linked issues. +
    +
    + )} + ); }; From a01b53bd9b3730d85c7f416b916b9df56bee79d0 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Thu, 20 Jun 2024 08:10:33 +0530 Subject: [PATCH 016/582] classifier-93 update error and success status --- .../integration/jira/cloud/update.yml | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/update.yml b/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/update.yml index c6dbb257..0ea5cc74 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/update.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/update.yml @@ -78,7 +78,24 @@ update_jira_issue: Authorization: ${auth_header.response.body.val} body: fields: ${all_labels} - result: value + result: res_jira + next: check_status + +check_status: + switch: + - condition: ${200 <= res_jira.response.statusCodeValue && res_jira.response.statusCodeValue < 300} + next: return_ok + next: return_bad_request + +return_ok: + status: 200 + return: "Jira Ticket Updated" + next: end + +return_bad_request: + status: 400 + return: "Bad Request" + next: end return_incorrect_request: status: 400 From 5d86202073531723dd4fef147dbdba977df72305 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Fri, 21 Jun 2024 00:40:42 +0530 Subject: [PATCH 017/582] adding docker-compose to root --- GUI/src/App.tsx | 45 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/GUI/src/App.tsx b/GUI/src/App.tsx index 893211d6..84eeb763 100644 --- a/GUI/src/App.tsx +++ b/GUI/src/App.tsx @@ -1,4 +1,4 @@ -import { FC } from 'react'; +import { FC, useEffect } from 'react'; import { Navigate, Route, Routes } from 'react-router-dom'; import { useQuery } from '@tanstack/react-query'; @@ -11,15 +11,40 @@ import UserManagement from 'pages/UserManagement'; import Integrations from 'pages/Integrations'; const App: FC = () => { - useQuery<{ - data: { custom_jwt_userinfo: UserInfo }; - }>({ - queryKey: ['auth/jwt/userinfo', 'prod'], - onSuccess: (res: { response: UserInfo }) => { - localStorage.setItem('exp', res.response.JWTExpirationTimestamp); - return useStore.getState().setUserInfo(res.response); - }, - }); + + const res={ + response: { + firstName: "Kustuta", + lastName: "Kasutaja", + idCode: "EE30303039914", + displayName: "Kustutamiseks", + JWTCreated: "1.71886644E12" , + fullName: "OK TESTNUMBER", + login: "EE30303039914", + authMethod: "smartid", + csaEmail: "kustutamind@mail.ee", + authorities: [ + "ROLE_ADMINISTRATOR" + ], + csaTitle: "", + JWTExpirationTimestamp: "1.71887364E12" + } +}; + // useQuery<{ + // data: { response: UserInfo }; + // }>({ + // queryKey: ['auth/jwt/userinfo', 'prod'], + // onSuccess: (res: { response: UserInfo }) => { + // localStorage.setItem('exp', res.response.JWTExpirationTimestamp); + // return useStore.getState().setUserInfo(res.response); + // }, + // }); + + useEffect(()=>{ + localStorage.setItem('exp', res.response.JWTExpirationTimestamp); + return useStore.getState().setUserInfo(res.response); + + },[]) return ( From 828c6583cef82db2c51a29937db3f48e21abd768 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Fri, 21 Jun 2024 00:41:01 +0530 Subject: [PATCH 018/582] adding docker-compose to root --- docker-compose.yml | 252 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..5c1bea50 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,252 @@ +version: "3.9" + +services: + ruuter-public: + container_name: ruuter-public + image: ruuter + environment: + - application.cors.allowedOrigins=http://localhost:8086,http://localhost:3001,http://localhost:3003,http://localhost:3004,http://localhost:8080 + - application.httpCodesAllowList=200,201,202,400,401,403,500 + - application.internalRequests.allowedIPs=127.0.0.1 + - application.logging.displayRequestContent=true + - application.logging.displayResponseContent=true + - server.port=8086 + volumes: + - ./DSL/Ruuter.public/DSL:/DSL + - ./constants.ini:/app/constants.ini + ports: + - 8086:8086 + networks: + - bykstack + cpus: "0.5" + mem_limit: "512M" + + ruuter-private: + container_name: ruuter-private + image: ruuter + environment: + - application.cors.allowedOrigins=http://localhost:8082,http://localhost:3001,http://localhost:3003 + - application.httpCodesAllowList=200,201,202,400,401,403,500 + - application.internalRequests.allowedIPs=127.0.0.1 + - application.logging.displayRequestContent=true + - application.logging.displayResponseContent=true + - server.port=8088 + volumes: + - ./DSL/Ruuter.private/DSL:/DSL + - ./constants.ini:/app/constants.ini + ports: + - 8088:8088 + networks: + - bykstack + cpus: "0.5" + mem_limit: "512M" + + tim: + container_name: tim + image: tim + depends_on: + - tim-postgresql + environment: + - SECURITY_ALLOWLIST_JWT=ruuter-private,ruuter-public,data-mapper,resql,tim,tim-postgresql,chat-widget,authentication-layer,127.0.0.1,::1 + ports: + - 8085:8085 + networks: + - bykstack + extra_hosts: + - "host.docker.internal:host-gateway" + cpus: "0.5" + mem_limit: "512M" + + tim-postgresql: + container_name: tim-postgresql + image: postgres:14.1 + environment: + - POSTGRES_USER=tim + - POSTGRES_PASSWORD=123 + - POSTGRES_DB=tim + - POSTGRES_HOST_AUTH_METHOD=trust + volumes: + - ./tim-db:/var/lib/postgresql/data + ports: + - 9876:5432 + networks: + - bykstack + + # data-mapper: + # container_name: data-mapper + # image: data-mapper + # environment: + # - PORT=3000 + # - CONTENT_FOLDER=/data + # volumes: + # - ./DSL:/data + # - ./DSL/DMapper/hbs:/workspace/app/views/chat-bot + # - ./DSL/DMapper/js:/workspace/app/js/chat-bot + # - ./DSL/DMapper/lib:/workspace/app/lib + # ports: + # - 3000:3000 + # networks: + # - bykstack + + # resql: + # container_name: resql + # image: resql + # depends_on: + # - users_db + # environment: + # - sqlms.datasources.[0].name=byk + # - sqlms.datasources.[0].jdbcUrl=jdbc:postgresql://171.22.247.13:5433/byk?sslmode=require + # - sqlms.datasources.[0].username=byk + # - sqlms.datasources.[0].password=2nH09n6Gly + # - logging.level.org.springframework.boot=INFO + # ports: + # - 8082:8082 + # volumes: + # - ./DSL/Resql:/workspace/app/templates/byk + # networks: + # - bykstack + + users_db: + container_name: users_db + image: postgres:14.1 + environment: + - POSTGRES_USER=byk + - POSTGRES_PASSWORD=01234 + - POSTGRES_DB=byk + ports: + - 5433:5432 + volumes: + - ./data:/var/lib/postgresql/data + networks: + - bykstack + + # resql_training: + # container_name: resql-training + # image: resql + # environment: + # - sqlms.datasources.[0].name=training + # - sqlms.datasources.[0].jdbcUrl=jdbc:postgresql://171.22.247.13:5434/train_db + # - sqlms.datasources.[0].username=byk + # - sqlms.datasources.[0].password=01234 + # - logging.level.org.springframework.boot=INFO + # - server.port=8083 + # ports: + # - 8083:8083 + # volumes: + # - ./DSL/Resql.training:/workspace/app/templates/training + # networks: + # - bykstack + + gui: + container_name: gui + environment: + - NODE_ENV=development + - BASE_URL=http://localhost:8080 + - REACT_APP_RUUTER_API_URL=http://localhost:8086 + - REACT_APP_RUUTER_PRIVATE_API_URL=http://localhost:8088 + - REACT_APP_CUSTOMER_SERVICE_LOGIN=http://localhost:3004/et/dev-auth + - REACT_APP_NOTIFICATION_NODE_URL=http://localhost:4040 + - REACT_APP_CSP=upgrade-insecure-requests; default-src 'self'; font-src 'self' data:; img-src 'self' data:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self' http://localhost:8086 http://localhost:8088 http://localhost:8085 http://localhost:4040 https://admin.dev.buerokratt.ee/chat/menu.json; + - DEBUG_ENABLED=true + - CHOKIDAR_USEPOLLING=true + - PORT=3001 + - REACT_APP_BUEROKRATT_CHATBOT_URL=http://buerokratt-chat:8086 + - REACT_APP_MENU_URL=https://admin.dev.buerokratt.ee + - REACT_APP_MENU_PATH=/chat/menu.json + - REACT_APP_CONVERSATIONS_BASE_URL=http://localhost:8080/chat + - REACT_APP_TRAINING_BASE_URL=http://localhost:8080/training + - REACT_APP_ANALYTICS_BASE_URL=http://localhost:8080/analytics + - REACT_APP_SERVICES_BASE_URL=http://localhost:8080/services + - REACT_APP_SETTINGS_BASE_URL=http://localhost:8080/settings + - REACT_APP_MONITORING_BASE_URL=http://localhost:8080/monitoring + - REACT_APP_SERVICE_ID=conversations,settings,monitoring + - REACT_APP_ENABLE_HIDDEN_FEATURES=TRUE + build: + context: ./GUI + dockerfile: Dockerfile.dev + ports: + - 3001:3001 + volumes: + - /app/node_modules + - ./GUI:/app + networks: + - bykstack + cpus: "0.5" + mem_limit: "1G" + + # chat-widget: + # container_name: chat-widget + # image: chat-widget + # ports: + # - 3003:3003 + # networks: + # - bykstack + + authentication-layer: + container_name: authentication-layer + image: authentication-layer + ports: + - 3004:3004 + networks: + - bykstack + + # opensearch-node: + # image: opensearchproject/opensearch:2.11.1 + # container_name: opensearch-node + # environment: + # - node.name=opensearch-node + # - discovery.seed_hosts=opensearch + # - discovery.type=single-node + # - bootstrap.memory_lock=true + # - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m" + # - plugins.security.disabled=true + # ulimits: + # memlock: + # soft: -1 + # hard: -1 + # nofile: + # soft: 65536 + # hard: 65536 + # volumes: + # - opensearch-data:/usr/share/opensearch/data + # ports: + # - 9200:9200 + # - 9600:9600 + # networks: + # - bykstack + + # notifications-node: + # container_name: notifications-node + # build: + # context: ./notification-server + # dockerfile: Dockerfile + # ports: + # - 4040:4040 + # depends_on: + # - opensearch-node + # environment: + # OPENSEARCH_PROTOCOL: http + # OPENSEARCH_HOST: opensearch-node + # OPENSEARCH_PORT: 9200 + # OPENSEARCH_USERNAME: admin + # OPENSEARCH_PASSWORD: admin + # PORT: 4040 + # REFRESH_INTERVAL: 1000 + # QUEUE_REFRESH_INTERVAL: 4000 + # CORS_WHITELIST_ORIGINS: http://localhost:3001,http://localhost:3003,http://localhost:3004,http://localhost:8080 + # RUUTER_URL: http://ruuter-public:8086 + # CHAT_TERMINATION_DELAY: 5000 + # volumes: + # - /app/node_modules + # - ./notification-server:/app + # networks: + # - bykstack + +# volumes: +# opensearch-data: +networks: + bykstack: + name: bykstack + driver: bridge + driver_opts: + com.docker.network.driver.mtu: 1400 From 714326a8689b123580144410f099310135c05982 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Mon, 24 Jun 2024 09:29:37 +0530 Subject: [PATCH 019/582] classifier-93 refactor --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 0e81a0d2..4019972a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,5 @@ services: - ruuter-public: + ruuter-private: container_name: ruuter-private image: ruuter environment: From 0cfde3e00038db7c63a71efc216f083d7c23d7a2 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Tue, 25 Jun 2024 23:31:12 +0530 Subject: [PATCH 020/582] update gui Dockerfile --- GUI/Dockerfile | 10 +++++----- GUI/src/components/Layout/index.tsx | 2 -- GUI/vite.config.ts | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/GUI/Dockerfile b/GUI/Dockerfile index 1fe950be..355f3c30 100644 --- a/GUI/Dockerfile +++ b/GUI/Dockerfile @@ -1,20 +1,20 @@ ARG nginx_version=nginx:1.25.4-alpine FROM node:22.0.0-alpine AS image -WORKDIR /usr/buerokratt-chatbot +WORKDIR /usr/buerokratt-classifier COPY ./package*.json ./ FROM image AS build RUN npm install --legacy-peer-deps --mode=development COPY . . RUN ./node_modules/.bin/vite build --mode=development -VOLUME /usr/buerokratt-chatbot +VOLUME /usr/buerokratt-classifier FROM $nginx_version AS web COPY ./nginx/http-nginx.conf /etc/nginx/conf.d/default.conf -COPY --from=build ./usr/buerokratt-chatbot/build/assets /usr/share/nginx/html/buerokratt-chatbot/chat/assets -COPY --from=build ./usr/buerokratt-chatbot/build/index.html /usr/share/nginx/html/buerokratt-chatbot -COPY --from=build ./usr/buerokratt-chatbot/build/favicon.ico /usr/share/nginx/html/buerokratt-chatbot +COPY --from=build ./usr/buerokratt-classifier/build/assets /usr/share/nginx/html/buerokratt-classifier/chat/assets +COPY --from=build ./usr/buerokratt-classifier/build/index.html /usr/share/nginx/html/buerokratt-classifier +COPY --from=build ./usr/buerokratt-classifier/build/favicon.ico /usr/share/nginx/html/buerokratt-classifier RUN apk add --no-cache bash COPY ./nginx/scripts/env.sh /docker-entrypoint.d/env.sh RUN chmod +x /docker-entrypoint.d/env.sh diff --git a/GUI/src/components/Layout/index.tsx b/GUI/src/components/Layout/index.tsx index 8c9a864c..549a6f9f 100644 --- a/GUI/src/components/Layout/index.tsx +++ b/GUI/src/components/Layout/index.tsx @@ -10,8 +10,6 @@ const Layout: FC = () => { return (
    diff --git a/GUI/vite.config.ts b/GUI/vite.config.ts index c1c65994..3d4993d8 100644 --- a/GUI/vite.config.ts +++ b/GUI/vite.config.ts @@ -21,7 +21,7 @@ export default defineConfig({ }, }, ], - base: 'chat', + base: 'classifier', build: { outDir: './build', target: 'es2015', From 6d34c1ab879d17e5d49878cee3efb68f206d8cb9 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Wed, 26 Jun 2024 00:26:18 +0530 Subject: [PATCH 021/582] classifier-97 implement outlook integration including authorization and webhook process flows --- .../classifier/integration/outlook/token.yml | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 DSL/Ruuter.private/DSL/GET/classifier/integration/outlook/token.yml diff --git a/DSL/Ruuter.private/DSL/GET/classifier/integration/outlook/token.yml b/DSL/Ruuter.private/DSL/GET/classifier/integration/outlook/token.yml new file mode 100644 index 00000000..4a747fad --- /dev/null +++ b/DSL/Ruuter.private/DSL/GET/classifier/integration/outlook/token.yml @@ -0,0 +1,32 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'TOKEN'" + method: get + accepts: json + returns: json + namespace: classifier + +set_refresh_token: + assign: + refresh_token: "xxxxxx" + next: get_access_token + +get_access_token: + call: http.post + args: + url: "https://login.microsoftonline.com/[#TENANT]/oauth2/v2.0/token" + contentType: formdata + headers: + type: json + body: + client_id: "33f0e28a-b6ff-4b8c-ad7c-14f32d05e51f" + scope: "User.Read Mail.Read Mail.ReadWrite" + refresh_token: ${refresh_token} + grant_type: "refresh_token" + client_secret: "xxxxxxxxxxxx" + result: res + next: return_result + +return_result: + return: ${res.response.body} \ No newline at end of file From 9b691525db653b8027846d93345ff2ef8495c9df Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Wed, 26 Jun 2024 00:28:49 +0530 Subject: [PATCH 022/582] classifier-97 implement outlook integration including authorization and webhook process flows --- .../DSL/GET/classifier/integration/outlook/token.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/DSL/Ruuter.private/DSL/GET/classifier/integration/outlook/token.yml b/DSL/Ruuter.private/DSL/GET/classifier/integration/outlook/token.yml index 4a747fad..cc907153 100644 --- a/DSL/Ruuter.private/DSL/GET/classifier/integration/outlook/token.yml +++ b/DSL/Ruuter.private/DSL/GET/classifier/integration/outlook/token.yml @@ -9,7 +9,7 @@ declaration: set_refresh_token: assign: - refresh_token: "xxxxxx" + refresh_token: "[#OUTLOOK_REFRESH_KEY]" next: get_access_token get_access_token: @@ -20,11 +20,11 @@ get_access_token: headers: type: json body: - client_id: "33f0e28a-b6ff-4b8c-ad7c-14f32d05e51f" + client_id: "[#OUTLOOK_CLIENT_ID]" scope: "User.Read Mail.Read Mail.ReadWrite" refresh_token: ${refresh_token} grant_type: "refresh_token" - client_secret: "xxxxxxxxxxxx" + client_secret: "[#OUTLOOK_SECRET_KEY]" result: res next: return_result From 10ec10a0b7ccfd8ac3a0bd344baeeb848e3ecf44 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Wed, 26 Jun 2024 00:30:54 +0530 Subject: [PATCH 023/582] classifier-97 implement outlook integration including authorization and webhook process flows --- .../outlook/toggle-subscription.yml | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/toggle-subscription.yml diff --git a/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/toggle-subscription.yml b/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/toggle-subscription.yml new file mode 100644 index 00000000..6322fd11 --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/toggle-subscription.yml @@ -0,0 +1,124 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'TOGGLE-SUBSCRIPTION'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: is_connect + type: boolean + description: "Body field 'isConnect'" + +extract_request_data: + assign: + is_connect: ${incoming.body.isConnect} + next: get_token_info + +get_token_info: + call: http.get + args: + url: "[#CLASSIFIER_RUUTER_PRIVATE]/classifier/integration/outlook/token" + result: res + next: assign_access_token + +assign_access_token: + assign: + access_token: ${res.response.body.response.access_token} + next: check_integration_type + +check_integration_type: + switch: + - condition: ${is_connect === true} + next: subscribe_outlook + next: get_subscription_id + +subscribe_outlook: + call: http.post + args: + url: "https://graph.microsoft.com/v1.0/subscriptions" + headers: + Authorization: ${'Bearer ' + access_token} + body: + changeType: "created" + notificationUrl: "[#CLASSIFIER_RUUTER_PRIVATE]/classifier/integration/outlook/accept" + resource: "me/mailFolders('inbox')/messages" + expirationDateTime: "2024-06-28T21:10:45.9356913Z" + clientState: "state" + result: res_subscribe + next: check_subscribe_response + +get_subscription_id: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-outlook-subscription-data" + result: res_data + next: check_subscription_id + +check_subscription_id: + switch: + - condition: ${res_data.response.subscriptionId !== null} + next: unsubscribe_outlook + next: return_not_found + +unsubscribe_outlook: + call: http.delete + args: + url: "https://graph.microsoft.com/v1.0/subscriptions/${res_data.response.subscriptionId}" + headers: + Authorization: ${'Bearer ' + access_token} + result: res_unsubscribe + next: check_unsubscribe_response + +check_subscribe_response: + switch: + - condition: ${200 <= res_subscribe.response.statusCodeValue && res_subscribe.response.statusCodeValue < 300} + next: set_subscription_data + next: return_bad_request + +check_unsubscribe_response: + switch: + - condition: ${200 <= res_unsubscribe.response.statusCodeValue && res_unsubscribe.response.statusCodeValue < 300} + next: remove_subscription_data + next: return_bad_request + +set_subscription_data: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/set-outlook-subscription-data" + body: + subscriptionId: ${res_subscribe.response.id} + result: set_status_res + next: check_db_status + +remove_subscription_data: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/delete-outlook-subscription-data" + body: + subscriptionId: ${subscription-id} + result: set_status_res + next: check_db_status + +check_db_status: + switch: + - condition: ${200 <= set_status_res.response.statusCodeValue && set_status_res.response.statusCodeValue < 300} + next: return_ok + next: return_bad_request + +return_ok: + status: 200 + return: "service successful" + next: end + +return_not_found: + status: 404 + return: "Subscription not found" + next: end + +return_bad_request: + status: 400 + return: "Bad Request" + next: end \ No newline at end of file From 52a4b752ab7838c943ef1cba251e0cba1ab04e52 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Wed, 26 Jun 2024 00:39:52 +0530 Subject: [PATCH 024/582] classifier-97 implement outlook integration including authorization and webhook process flows --- .../classifier/integration/outlook/accept.yml | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/accept.yml diff --git a/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/accept.yml b/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/accept.yml new file mode 100644 index 00000000..212a7fc8 --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/accept.yml @@ -0,0 +1,39 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'ACCEPT'" + method: post + accepts: json + returns: json + namespace: classifier + +extract_request_data: + assign: + payload: ${incoming.body} + next: check_validation_token + +check_validation_token: + switch: + - condition: ${payload !== null && payload.validationToken !==null} + next: assign_validation_token + next: return_error_found + +assign_validation_token: + assign: + validation_token: ${payload.validationToken} + next: set_response_data + +set_response_data: + call: http.post + args: + url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/return_outlook_validation_token" + headers: + type: json + body: + validationToken: ${validation_token} + result: response + next: return_validation_response + +return_validation_response: + return: ${response} + status: 200 \ No newline at end of file From 27659fde14558971f1be99a5a5cc00181ce96742 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Wed, 26 Jun 2024 13:14:18 +0530 Subject: [PATCH 025/582] classifier-97 implement outlook integration validation and remaining issue send flows done --- .../classifier/integration/outlook/accept.yml | 71 +++++++++++++++++-- .../outlook/toggle-subscription.yml | 5 +- 2 files changed, 70 insertions(+), 6 deletions(-) diff --git a/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/accept.yml b/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/accept.yml index 212a7fc8..05ca5402 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/accept.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/accept.yml @@ -2,20 +2,35 @@ declaration: call: declare version: 0.1 description: "Description placeholder for 'ACCEPT'" - method: post + method: get accepts: json returns: json namespace: classifier + allowlist: + params: + - field: validationToken + type: boolean + description: "parameter 'validationToken'" + body: + - field: payload + type: json + description: "body field is 'payload'" extract_request_data: assign: + validation_token: ${incoming.params.validationToken} payload: ${incoming.body} - next: check_validation_token + next: check_process_flow + +new_val: + return: ${validation_token} -check_validation_token: +check_process_flow: switch: - - condition: ${payload !== null && payload.validationToken !==null} + - condition: ${validation_token !==null} next: assign_validation_token + - condition: ${payload !==null} + next: assign_outlook_mail_info next: return_error_found assign_validation_token: @@ -36,4 +51,50 @@ set_response_data: return_validation_response: return: ${response} - status: 200 \ No newline at end of file + status: 200 + +assign_outlook_mail_info: + assign: + mail_info: ${payload} + next: extract_data_from_payload + +extract_data_from_payload: + call: http.post + args: + url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/return_outlook_mail_info" + headers: + type: json + body: + data: ${mail_info} + result: extract_info + next: send_issue_data + +#check the mail id is an existing id and check categories from mail and db are same +#if different or new send to AI model + +send_issue_data: + call: http.post + args: + url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/return_jira_issue_info"# need correct url + headers: + type: json + body: + info: ${extract_info} + result: res + next: check_response + +check_response: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: return_ok + next: return_bad_request + +return_ok: + status: 200 + return: "Outlook data send successfully" + next: end + +return_bad_request: + status: 400 + return: "Bad Request" + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/toggle-subscription.yml b/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/toggle-subscription.yml index 6322fd11..ec9637dc 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/toggle-subscription.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/toggle-subscription.yml @@ -43,7 +43,7 @@ subscribe_outlook: Authorization: ${'Bearer ' + access_token} body: changeType: "created" - notificationUrl: "[#CLASSIFIER_RUUTER_PRIVATE]/classifier/integration/outlook/accept" + notificationUrl: "https://f249-2402-4000-2081-3ac5-90b3-3a49-b826-3da.ngrok-free.app/classifier/integration/outlook/accept" resource: "me/mailFolders('inbox')/messages" expirationDateTime: "2024-06-28T21:10:45.9356913Z" clientState: "state" @@ -93,6 +93,9 @@ set_subscription_data: result: set_status_res next: check_db_status +output_val: + return: ${res_subscribe} + remove_subscription_data: call: http.post args: From 7f867f7cb6af1c633d884e39fa71c53d698d2e5d Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Wed, 26 Jun 2024 15:11:35 +0530 Subject: [PATCH 026/582] classifier-97 rest api to update mail groups according to the labels predicted --- .../classifier/integration/outlook/group.yml | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/group.yml diff --git a/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/group.yml b/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/group.yml new file mode 100644 index 00000000..be875312 --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/group.yml @@ -0,0 +1,103 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'Group'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: mailId + type: string + description: "Body field 'issueId'" + - field: labels + type: array + description: "Body field 'labels'" + +extract_request_data: + assign: + mail_id: ${incoming.body.mailId} + label_list: ${incoming.body.labels} + next: check_for_request_data + +check_for_request_data: + switch: + - condition: ${mail_id !== null || label_list @== null} + next: get_token_info + next: return_incorrect_request + +get_token_info: + call: http.get + args: + url: "[#CLASSIFIER_RUUTER_PRIVATE]/classifier/integration/outlook/token" + result: res + next: assign_access_token + +assign_access_token: + assign: + access_token: ${res.response.body.response.access_token} + next: get_mail_issue_info + +get_mail_issue_info: + call: http.get + args: + url: "https://graph.microsoft.com/v1.0/me/messages//${mail_id}" + headers: + Authorization: ${'Bearer ' + access_token} + result: mail_info + next: assign_existing_labels + +assign_existing_labels: + assign: + existing_label_list: ${mail_info.response.body.fields.categories} + next: merge_labels + +merge_labels: + call: http.post + args: + url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/return_label_field_array" + headers: + type: json + body: + labels: ${label_list} + existing_labels: ${existing_label_list} + result: res + next: set_data + +set_data: + assign: + all_labels: ${res.response.body} + next: update_mail + +update_mail: + call: http.put + args: + url: "https://graph.microsoft.com/v1.0/me/messages//${mail_id}" + headers: + Authorization: ${'Bearer ' + access_token} + body: + fields: ${all_labels} #catergories need to change mapper cll + result: res_mail + next: check_status + +check_status: + switch: + - condition: ${200 <= res_mail.response.statusCodeValue && res_mail.response.statusCodeValue < 300} + next: return_ok + next: return_bad_request + +return_ok: + status: 200 + return: "JOutlook email Updated" + next: end + +return_bad_request: + status: 400 + return: "Bad Request" + next: end + +return_incorrect_request: + status: 400 + return: 'missing labels' + next: end \ No newline at end of file From c6c1af433aa822826b2554f73fc83ad314489908 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Wed, 26 Jun 2024 15:11:55 +0530 Subject: [PATCH 027/582] classifier-97 rest api to update mail groups according to the labels predicted --- .../DSL/POST/classifier/integration/outlook/group.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/group.yml b/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/group.yml index be875312..a3eecf31 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/group.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/group.yml @@ -89,7 +89,7 @@ check_status: return_ok: status: 200 - return: "JOutlook email Updated" + return: "Outlook email Updated" next: end return_bad_request: From 60cb887d4bc730e0f61b8a32a0b59bdb56235a47 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Thu, 27 Jun 2024 17:00:53 +0530 Subject: [PATCH 028/582] data_enrichment module --- src/data_enrichment/data_enrichment.py | 48 ++++++++++++++++++++++++++ src/data_enrichment/paraphraser.py | 35 +++++++++++++++++++ src/data_enrichment/translator.py | 47 +++++++++++++++++++++++++ 3 files changed, 130 insertions(+) create mode 100644 src/data_enrichment/data_enrichment.py create mode 100644 src/data_enrichment/paraphraser.py create mode 100644 src/data_enrichment/translator.py diff --git a/src/data_enrichment/data_enrichment.py b/src/data_enrichment/data_enrichment.py new file mode 100644 index 00000000..0dd02468 --- /dev/null +++ b/src/data_enrichment/data_enrichment.py @@ -0,0 +1,48 @@ +from translator import Translator +from paraphraser import Paraphraser + +class DataEnrichment: + def __init__(self): + self.translator = Translator() + self.paraphraser = Paraphraser() + + def enrich_data(self, text, lang): + supported_languages = ['en', 'et', 'ru', 'pl', 'fi'] + + if lang not in supported_languages: + print(f"Unsupported language: {lang}") + return None + + if lang == 'en': + paraphrases = self.paraphraser.generate_paraphrases(text) + else: + english_text = self.translator.translate(text, lang, 'en') + paraphrases = self.paraphraser.generate_paraphrases(english_text) + translated_paraphrases = [] + for paraphrase in paraphrases: + translated_paraphrase = self.translator.translate(paraphrase, 'en', lang) + translated_paraphrases.append(translated_paraphrase) + return translated_paraphrases + + return paraphrases + +if __name__ == "__main__": + enricher = DataEnrichment() + + sentences = [ + ("Hello, I hope this email finds you well. I am writing to inquire about the status of my application. I submitted all required documents last month but have not received any update since then. Could you please provide me with the current status?", 'en'), + ("Tere, ma loodan, et see e-kiri leiab teid hästi. Ma kirjutan päringu staatuse minu taotluse. Ma esitasin kõik nõutavad dokumendid möödunud kuu, kuid ma pole saanud ühtegi värskendust sellest ajast. Kas saaksite palun mulle hetkeseisu kohta teavet anda?", 'et'), # Estonian + ("Привет, надеюсь, что это письмо находит вас в порядке. Я пишу с запросом о статусе моего заявления. Я подал все необходимые документы в прошлом месяце, но с тех пор не получил никаких обновлений. Не могли бы вы, пожалуйста, предоставить мне текущий статус?", 'ru'), # Russian + ("Cześć, mam nadzieję, że to e-mail znajduje cię w dobrym zdrowiu. Piszę z zapytaniem o status mojej aplikacji. Złożyłem wszystkie wymagane dokumenty w zeszłym miesiącu, ale od tego czasu nie otrzymałem żadnej aktualizacji. Czy moglibyście podać mi bieżący status?", 'pl'), # Polish + ("Hei, toivottavasti tämä sähköposti tavoittaa teidät hyvin. Kirjoitan tiedustelun tilastani hakemukseni suhteen. Lähetin kaikki tarvittavat asiakirjat viime kuussa, mutta en ole sen jälkeen saanut päivityksiä. Voisitteko antaa minulle ajantasaisen tilanteen?", 'fi') # Finnish + ] + + + for sentence, lang in sentences: + print(f"Original sentence ({lang}): {sentence}") + enriched_data = enricher.enrich_data(sentence, lang) + if enriched_data: + print("Paraphrases:") + for i, paraphrase in enumerate(enriched_data, 1): + print(f"Paraphrase {i}: {paraphrase}") + print("-" * 50) diff --git a/src/data_enrichment/paraphraser.py b/src/data_enrichment/paraphraser.py new file mode 100644 index 00000000..c81e5ddd --- /dev/null +++ b/src/data_enrichment/paraphraser.py @@ -0,0 +1,35 @@ +from transformers import AutoTokenizer, AutoModelForSeq2SeqLM + +class Paraphraser: + def __init__(self): + self.model_name = "humarin/chatgpt_paraphraser_on_T5_base" + self.tokenizer = AutoTokenizer.from_pretrained(self.model_name) + self.model = AutoModelForSeq2SeqLM.from_pretrained(self.model_name) + + def generate_paraphrases(self, question, + num_beams=5, + num_beam_groups=5, + num_return_sequences=3, + repetition_penalty=10.0, + diversity_penalty=3.0, + no_repeat_ngram_size=2, + temperature=0.7, + max_length=128 + ): + input_ids = self.tokenizer( + f'paraphrase: {question}', + return_tensors="pt", padding="longest", + max_length=max_length, + truncation=True, + ).input_ids.to('cpu') + + outputs = self.model.generate( + input_ids, temperature=temperature, repetition_penalty=repetition_penalty, + num_return_sequences=num_return_sequences, no_repeat_ngram_size=no_repeat_ngram_size, + num_beams=num_beams, num_beam_groups=num_beam_groups, + max_length=max_length, diversity_penalty=diversity_penalty + ) + + res = self.tokenizer.batch_decode(outputs, skip_special_tokens=True) + + return res \ No newline at end of file diff --git a/src/data_enrichment/translator.py b/src/data_enrichment/translator.py new file mode 100644 index 00000000..38685cfd --- /dev/null +++ b/src/data_enrichment/translator.py @@ -0,0 +1,47 @@ +from transformers import MarianMTModel, MarianTokenizer + +class Translator: + def __init__(self): + self.models = { + 'et-en': ('Helsinki-NLP/opus-mt-et-en', 'Helsinki-NLP/opus-mt-en-et'), + 'ru-en': ('Helsinki-NLP/opus-mt-ru-en', 'Helsinki-NLP/opus-mt-en-ru'), + 'pl-en': ('Helsinki-NLP/opus-mt-pl-en', 'Helsinki-NLP/opus-mt-en-pl'), + 'fi-en': ('Helsinki-NLP/opus-mt-fi-en', 'Helsinki-NLP/opus-mt-en-fi'), + 'en-fr': ('Helsinki-NLP/opus-mt-en-fr', 'Helsinki-NLP/opus-mt-fr-en'), + 'fr-pl': ('Helsinki-NLP/opus-mt-fr-pl', 'Helsinki-NLP/opus-mt-pl-fr'), + } + self.tokenizers = {} + self.models_instances = {} + + for key, (model_name, reverse_model_name) in self.models.items(): + self.tokenizers[key] = MarianTokenizer.from_pretrained(model_name) + self.models_instances[key] = MarianMTModel.from_pretrained(model_name) + reverse_key = f"{key.split('-')[1]}-{key.split('-')[0]}" + if reverse_model_name != 'Helsinki-NLP/opus-mt-en-pl': + self.tokenizers[reverse_key] = MarianTokenizer.from_pretrained(reverse_model_name) + self.models_instances[reverse_key] = MarianMTModel.from_pretrained(reverse_model_name) + + def translate(self, text, src_lang, tgt_lang): + if src_lang == 'en' and tgt_lang == 'pl': + intermediate_text = self._translate(text, 'en', 'fr') + translated_text = self._translate(intermediate_text, 'fr', 'pl') + else: + translated_text = self._translate(text, src_lang, tgt_lang) + + return translated_text + + def _translate(self, text, src_lang, tgt_lang): + key = f'{src_lang}-{tgt_lang}' + if key not in self.models_instances: + raise ValueError(f"Translation from {src_lang} to {tgt_lang} is not supported.") + + tokenizer = self.tokenizers[key] + model = self.models_instances[key] + + tokens = tokenizer(text, return_tensors="pt", padding=True) + + translated_tokens = model.generate(**tokens) + translated_text = tokenizer.decode(translated_tokens[0], skip_special_tokens=True) + + return translated_text + From ff14637afb2f6d04f3679b54d0699e468b30b7b4 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Thu, 27 Jun 2024 21:51:04 +0530 Subject: [PATCH 029/582] classifier-93 jira webhook secret key pass from config value --- DSL/DMapper/hbs/verify_signature.handlebars | 2 +- DSL/DMapper/lib/helpers.js | 4 ++-- .../DSL/POST/classifier/integration/jira/cloud/accept.yml | 1 + constants.ini | 1 + 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/DSL/DMapper/hbs/verify_signature.handlebars b/DSL/DMapper/hbs/verify_signature.handlebars index 4d5434e4..a39e32d2 100644 --- a/DSL/DMapper/hbs/verify_signature.handlebars +++ b/DSL/DMapper/hbs/verify_signature.handlebars @@ -1,3 +1,3 @@ { - "valid": "{{verifySignature payload headers}}" + "valid": "{{verifySignature payload headers secret}}" } diff --git a/DSL/DMapper/lib/helpers.js b/DSL/DMapper/lib/helpers.js index 5adbf2fb..c6728751 100644 --- a/DSL/DMapper/lib/helpers.js +++ b/DSL/DMapper/lib/helpers.js @@ -1,8 +1,8 @@ import { createHmac, timingSafeEqual } from "crypto"; -export function verifySignature(payload, headers) { +export function verifySignature(payload, headers, secret) { const signature = headers["x-hub-signature"]; - const SHARED_SECRET = "wNc6HjKGu3RZXYNXqMTh"; + const SHARED_SECRET = secret; const hmac = createHmac("sha256", Buffer.from(SHARED_SECRET, "utf8")); const payloadString = JSON.stringify(payload); hmac.update(Buffer.from(payloadString, "utf8")); diff --git a/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/accept.yml b/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/accept.yml index 80d060f2..2ef5a6ad 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/accept.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/accept.yml @@ -34,6 +34,7 @@ verify_jira_signature: body: payload: ${payload} headers: ${headers} + secret: "[#JIRA_WEBHOOK_SECRET]" result: valid_data next: assign_verification diff --git a/constants.ini b/constants.ini index d5c06401..1025f1ce 100644 --- a/constants.ini +++ b/constants.ini @@ -7,3 +7,4 @@ JIRA_API_TOKEN=value JIRA_USERNAME=value JIRA_CLOUD_DOMAIN=value JIRA_WEBHOOK_ID=1 +JIRA_WEBHOOK_SECRET=value \ No newline at end of file From 34fdcd07450fc833afa3d2c6e3fa3d9efce524ec Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Thu, 27 Jun 2024 21:57:38 +0530 Subject: [PATCH 030/582] classifier-93 typo --- .../DSL/POST/classifier/integration/jira/cloud/accept.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/accept.yml b/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/accept.yml index 2ef5a6ad..c4aa6fa3 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/accept.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/accept.yml @@ -1,7 +1,7 @@ declaration: call: declare version: 0.1 - description: "Description placeholder for 'ACEEPT'" + description: "Description placeholder for 'ACCEPT'" method: post accepts: json returns: json From 8a11ed9c0162cb1d9b5dc9c7019b81c24bc2d78e Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Thu, 27 Jun 2024 23:58:02 +0530 Subject: [PATCH 031/582] classifier-97 setup resql and local users_db ,implement liquibase flow and migration script, add toggle-subscription.yml resql flows and token.yml resql flows(postgresql) --- .../changelog/create-initial-tables.sql | 21 +++++ DSL/Liquibase/liquibase.properties | 6 ++ DSL/Liquibase/master.yml | 4 + DSL/Resql/connect-platform.sql | 3 + DSL/Resql/disconnect-platform.sql | 3 + DSL/Resql/get-integration-status.sql | 1 + DSL/Resql/get-platform-integration-status.sql | 3 + DSL/Resql/get-token.sql | 3 + .../classifier/integration/outlook/token.yml | 25 +++++- .../classifier/integration/outlook/group.yml | 2 + .../outlook/toggle-subscription.yml | 81 +++++++++++-------- create-migration.sh | 9 +++ migrate.sh | 2 + 13 files changed, 126 insertions(+), 37 deletions(-) create mode 100644 DSL/Liquibase/changelog/create-initial-tables.sql create mode 100644 DSL/Liquibase/liquibase.properties create mode 100644 DSL/Liquibase/master.yml create mode 100644 DSL/Resql/connect-platform.sql create mode 100644 DSL/Resql/disconnect-platform.sql create mode 100644 DSL/Resql/get-integration-status.sql create mode 100644 DSL/Resql/get-platform-integration-status.sql create mode 100644 DSL/Resql/get-token.sql create mode 100644 create-migration.sh create mode 100644 migrate.sh diff --git a/DSL/Liquibase/changelog/create-initial-tables.sql b/DSL/Liquibase/changelog/create-initial-tables.sql new file mode 100644 index 00000000..79f84361 --- /dev/null +++ b/DSL/Liquibase/changelog/create-initial-tables.sql @@ -0,0 +1,21 @@ +-- liquibase formatted sql + +-- changeset kalsara Magamage:classifier-ddl-script-v1-changeset1 +CREATE TYPE platform AS ENUM ('JIRA', 'OUTLOOK', 'PINAL'); + +-- changeset kalsara Magamage:classifier-ddl-script-v1-changeset2 +CREATE TABLE public."integration_status" ( + id int8 NOT NULL GENERATED BY DEFAULT AS IDENTITY, + platform platform, + is_connect BOOLEAN NOT NULL DEFAULT FALSE, + subscription_id VARCHAR(50) DEFAULT NULL, + token TEXT DEFAULT NULL, + CONSTRAINT integration_status_pkey PRIMARY KEY (id) +); + +-- changeset kalsara Magamage:classifier-ddl-script-v1-changeset3 +INSERT INTO public."integration_status" (platform, is_connect, subscription_id, token) +VALUES + ('JIRA', FALSE, NULL, NULL), + ('OUTLOOK', FALSE, NULL, NULL), + ('PINAL', FALSE, NULL, NULL); \ No newline at end of file diff --git a/DSL/Liquibase/liquibase.properties b/DSL/Liquibase/liquibase.properties new file mode 100644 index 00000000..03d0c917 --- /dev/null +++ b/DSL/Liquibase/liquibase.properties @@ -0,0 +1,6 @@ +changelogFile: /changelog/master.yml +url: jdbc:postgresql://localhost:5433/classifier +username: root +password: root +secureParsing: false +liquibase.hub.mode=off diff --git a/DSL/Liquibase/master.yml b/DSL/Liquibase/master.yml new file mode 100644 index 00000000..8e0d64de --- /dev/null +++ b/DSL/Liquibase/master.yml @@ -0,0 +1,4 @@ +databaseChangeLog: + - includeAll: + path: changelog/ + errorIfMissingOrEmpty: true diff --git a/DSL/Resql/connect-platform.sql b/DSL/Resql/connect-platform.sql new file mode 100644 index 00000000..84f86043 --- /dev/null +++ b/DSL/Resql/connect-platform.sql @@ -0,0 +1,3 @@ +UPDATE integration_status +SET subscription_id = :id , is_connect = TRUE +WHERE platform =:platform::platform; \ No newline at end of file diff --git a/DSL/Resql/disconnect-platform.sql b/DSL/Resql/disconnect-platform.sql new file mode 100644 index 00000000..0db31995 --- /dev/null +++ b/DSL/Resql/disconnect-platform.sql @@ -0,0 +1,3 @@ +UPDATE integration_status +SET subscription_id = NULL, is_connect = FALSE +WHERE platform =:platform::platform; \ No newline at end of file diff --git a/DSL/Resql/get-integration-status.sql b/DSL/Resql/get-integration-status.sql new file mode 100644 index 00000000..06d857a9 --- /dev/null +++ b/DSL/Resql/get-integration-status.sql @@ -0,0 +1 @@ +SELECT * FROM integration_status \ No newline at end of file diff --git a/DSL/Resql/get-platform-integration-status.sql b/DSL/Resql/get-platform-integration-status.sql new file mode 100644 index 00000000..3a35eee3 --- /dev/null +++ b/DSL/Resql/get-platform-integration-status.sql @@ -0,0 +1,3 @@ +SELECT is_connect +FROM integration_status +WHERE platform=:platform::platform; \ No newline at end of file diff --git a/DSL/Resql/get-token.sql b/DSL/Resql/get-token.sql new file mode 100644 index 00000000..ba4c16d0 --- /dev/null +++ b/DSL/Resql/get-token.sql @@ -0,0 +1,3 @@ +SELECT token +FROM integration_status +WHERE platform=:platform::platform; diff --git a/DSL/Ruuter.private/DSL/GET/classifier/integration/outlook/token.yml b/DSL/Ruuter.private/DSL/GET/classifier/integration/outlook/token.yml index cc907153..eb7e610f 100644 --- a/DSL/Ruuter.private/DSL/GET/classifier/integration/outlook/token.yml +++ b/DSL/Ruuter.private/DSL/GET/classifier/integration/outlook/token.yml @@ -7,10 +7,24 @@ declaration: returns: json namespace: classifier +get_refresh_token: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-token" + body: + platform: 'OUTLOOK' + result: res + set_refresh_token: assign: - refresh_token: "[#OUTLOOK_REFRESH_KEY]" - next: get_access_token + refresh_token: ${res.response.body.token} + next: check_refresh_token + +check_refresh_token: + switch: + - condition: ${refresh_token !== null} + next: get_access_token + next: return_not_found get_access_token: call: http.post @@ -29,4 +43,9 @@ get_access_token: next: return_result return_result: - return: ${res.response.body} \ No newline at end of file + return: ${res.response.body} + +return_not_found: + status: 404 + return: "refresh token not found" + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/group.yml b/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/group.yml index a3eecf31..7943fa89 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/group.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/group.yml @@ -87,6 +87,8 @@ check_status: next: return_ok next: return_bad_request +#update table on success of mail update + return_ok: status: 200 return: "Outlook email Updated" diff --git a/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/toggle-subscription.yml b/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/toggle-subscription.yml index ec9637dc..9c52ce4c 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/toggle-subscription.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/toggle-subscription.yml @@ -15,7 +15,28 @@ declaration: extract_request_data: assign: is_connect: ${incoming.body.isConnect} - next: get_token_info + next: get_platform_integration_status + +get_platform_integration_status: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-platform-integration-status" + body: + platform: 'OUTLOOK' + result: res + next: assign_db_platform_integration_data + +assign_db_platform_integration_data: + assign: + db_platform_status: ${res.response.body.is_connect} + subscription_id: ${res.response.body.subscription_id} + next: validate_request + +validate_request: + switch: + - condition: ${db_platform_status !== is_connect} + next: get_token_info + next: return_already_request get_token_info: call: http.get @@ -31,9 +52,11 @@ assign_access_token: check_integration_type: switch: - - condition: ${is_connect === true} + - condition: ${is_connect === true && subscription_id == null} next: subscribe_outlook - next: get_subscription_id + - condition: ${is_connect === false && subscription_id !== null} + next: unsubscribe_outlook + next: return_bad_request subscribe_outlook: call: http.post @@ -43,25 +66,28 @@ subscribe_outlook: Authorization: ${'Bearer ' + access_token} body: changeType: "created" - notificationUrl: "https://f249-2402-4000-2081-3ac5-90b3-3a49-b826-3da.ngrok-free.app/classifier/integration/outlook/accept" + notificationUrl: "ngrok/classifier/integration/outlook/accept" resource: "me/mailFolders('inbox')/messages" expirationDateTime: "2024-06-28T21:10:45.9356913Z" clientState: "state" result: res_subscribe next: check_subscribe_response -get_subscription_id: +check_subscribe_response: + switch: + - condition: ${200 <= res_subscribe.response.statusCodeValue && res_subscribe.response.statusCodeValue < 300} + next: set_subscription_data + next: return_bad_request + +set_subscription_data: call: http.post args: - url: "[#CLASSIFIER_RESQL]/get-outlook-subscription-data" - result: res_data - next: check_subscription_id - -check_subscription_id: - switch: - - condition: ${res_data.response.subscriptionId !== null} - next: unsubscribe_outlook - next: return_not_found + url: "[#CLASSIFIER_RESQL]/connect-platform" + body: + id: ${res_subscribe.response.id} + platform: 'OUTLOOK' + result: set_status_res + next: check_db_status unsubscribe_outlook: call: http.delete @@ -72,36 +98,18 @@ unsubscribe_outlook: result: res_unsubscribe next: check_unsubscribe_response -check_subscribe_response: - switch: - - condition: ${200 <= res_subscribe.response.statusCodeValue && res_subscribe.response.statusCodeValue < 300} - next: set_subscription_data - next: return_bad_request - check_unsubscribe_response: switch: - condition: ${200 <= res_unsubscribe.response.statusCodeValue && res_unsubscribe.response.statusCodeValue < 300} next: remove_subscription_data next: return_bad_request -set_subscription_data: - call: http.post - args: - url: "[#CLASSIFIER_RESQL]/set-outlook-subscription-data" - body: - subscriptionId: ${res_subscribe.response.id} - result: set_status_res - next: check_db_status - -output_val: - return: ${res_subscribe} - remove_subscription_data: call: http.post args: - url: "[#CLASSIFIER_RESQL]/delete-outlook-subscription-data" + url: "[#CLASSIFIER_RESQL]/disconnect-platform" body: - subscriptionId: ${subscription-id} + platform: 'OUTLOOK' result: set_status_res next: check_db_status @@ -121,6 +129,11 @@ return_not_found: return: "Subscription not found" next: end +return_already_request: + status: 400 + return: "Already Requested-Bad Request" + next: end + return_bad_request: status: 400 return: "Bad Request" diff --git a/create-migration.sh b/create-migration.sh new file mode 100644 index 00000000..f91fe720 --- /dev/null +++ b/create-migration.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +if [[ $# -eq 0 ]] ; then + echo 'specify descriptive name for migration file' + exit 0 +fi + +echo "-- liquibase formatted sql" > "DSL/Liquibase/changelog/`date '+%s'`-$1.sql" +echo "-- changeset $(git config user.name):`date '+%s'`" >> "DSL/Liquibase/changelog/`date '+%s'`-$1.sql" diff --git a/migrate.sh b/migrate.sh new file mode 100644 index 00000000..9b13c63f --- /dev/null +++ b/migrate.sh @@ -0,0 +1,2 @@ +#!/bin/bash +docker run --rm --network bykstack -v `pwd`/DSL/Liquibase/changelog:/liquibase/changelog -v `pwd`/DSL/Liquibase/master.yml:/liquibase/master.yml -v `pwd`/DSL/Liquibase/data:/liquibase/data liquibase/liquibase --defaultsFile=/liquibase/changelog/liquibase.properties --changelog-file=master.yml --url=jdbc:postgresql://users_db:5432/classifier?user=root --password=root update From 3abdf7413fe9dcb9ab44c28c3402eb30b4377b8e Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 28 Jun 2024 07:41:17 +0530 Subject: [PATCH 032/582] test cases for data enrichment --- src/data_enrichment/data_enrichment.py | 20 ------ src/data_enrichment/test_data_enrichment.py | 68 +++++++++++++++++++++ 2 files changed, 68 insertions(+), 20 deletions(-) create mode 100644 src/data_enrichment/test_data_enrichment.py diff --git a/src/data_enrichment/data_enrichment.py b/src/data_enrichment/data_enrichment.py index 0dd02468..962617a3 100644 --- a/src/data_enrichment/data_enrichment.py +++ b/src/data_enrichment/data_enrichment.py @@ -26,23 +26,3 @@ def enrich_data(self, text, lang): return paraphrases -if __name__ == "__main__": - enricher = DataEnrichment() - - sentences = [ - ("Hello, I hope this email finds you well. I am writing to inquire about the status of my application. I submitted all required documents last month but have not received any update since then. Could you please provide me with the current status?", 'en'), - ("Tere, ma loodan, et see e-kiri leiab teid hästi. Ma kirjutan päringu staatuse minu taotluse. Ma esitasin kõik nõutavad dokumendid möödunud kuu, kuid ma pole saanud ühtegi värskendust sellest ajast. Kas saaksite palun mulle hetkeseisu kohta teavet anda?", 'et'), # Estonian - ("Привет, надеюсь, что это письмо находит вас в порядке. Я пишу с запросом о статусе моего заявления. Я подал все необходимые документы в прошлом месяце, но с тех пор не получил никаких обновлений. Не могли бы вы, пожалуйста, предоставить мне текущий статус?", 'ru'), # Russian - ("Cześć, mam nadzieję, że to e-mail znajduje cię w dobrym zdrowiu. Piszę z zapytaniem o status mojej aplikacji. Złożyłem wszystkie wymagane dokumenty w zeszłym miesiącu, ale od tego czasu nie otrzymałem żadnej aktualizacji. Czy moglibyście podać mi bieżący status?", 'pl'), # Polish - ("Hei, toivottavasti tämä sähköposti tavoittaa teidät hyvin. Kirjoitan tiedustelun tilastani hakemukseni suhteen. Lähetin kaikki tarvittavat asiakirjat viime kuussa, mutta en ole sen jälkeen saanut päivityksiä. Voisitteko antaa minulle ajantasaisen tilanteen?", 'fi') # Finnish - ] - - - for sentence, lang in sentences: - print(f"Original sentence ({lang}): {sentence}") - enriched_data = enricher.enrich_data(sentence, lang) - if enriched_data: - print("Paraphrases:") - for i, paraphrase in enumerate(enriched_data, 1): - print(f"Paraphrase {i}: {paraphrase}") - print("-" * 50) diff --git a/src/data_enrichment/test_data_enrichment.py b/src/data_enrichment/test_data_enrichment.py new file mode 100644 index 00000000..f1a97936 --- /dev/null +++ b/src/data_enrichment/test_data_enrichment.py @@ -0,0 +1,68 @@ +from unittest import TestCase +import unittest +from data_enrichment import DataEnrichment + +class TestDataEnrichment(TestCase): + + def setUp(self): + self.enricher = DataEnrichment() + + def test_enrich_data_with_supported_language(self): + original_sentence = "Hello, I hope this email finds you well. I am writing to inquire about the status of my application." + language = 'en' + + enriched_data = self.enricher.enrich_data(original_sentence, language) + self.assertIsNotNone(enriched_data) + + for i, paraphrase in enumerate(enriched_data, 1): + print(f"Paraphrase {i}: {paraphrase}") + + def test_enrich_data_with_unsupported_language(self): + original_sentence = "Hello, I hope this email finds you well. I am writing to inquire about the status of my application." + language = 'fr' + + enriched_data = self.enricher.enrich_data(original_sentence, language) + self.assertIsNone(enriched_data) + + def test_enrich_data_with_estonian_sentence(self): + original_sentence = "Tere, ma loodan, et see e-kiri leiab teid hästi. Ma kirjutan päringu staatuse minu taotluse. Ma esitasin kõik nõutavad dokumendid möödunud kuu, kuid ma pole saanud ühtegi värskendust sellest ajast. Kas saaksite palun mulle hetkeseisu kohta teavet anda?" + language = 'et' + + enriched_data = self.enricher.enrich_data(original_sentence, language) + self.assertIsNotNone(enriched_data) + + for i, paraphrase in enumerate(enriched_data, 1): + print(f"Paraphrase {i}: {paraphrase}") + + def test_enrich_data_with_russian_sentence(self): + original_sentence = "Привет, надеюсь, что это письмо находит вас в порядке. Я пишу с запросом о статусе моего заявления. Я подал все необходимые документы в прошлом месяце, но с тех пор не получил никаких обновлений. Не могли бы вы, пожалуйста, предоставить мне текущий статус?" + language = 'ru' + + enriched_data = self.enricher.enrich_data(original_sentence, language) + self.assertIsNotNone(enriched_data) + + for i, paraphrase in enumerate(enriched_data, 1): + print(f"Paraphrase {i}: {paraphrase}") + + def test_enrich_data_with_polish_sentence(self): + original_sentence = "Cześć, mam nadzieję, że to e-mail znajduje cię w dobrym zdrowiu. Piszę z zapytaniem o status mojej aplikacji. Złożyłem wszystkie wymagane dokumenty w zeszłym miesiącu, ale od tego czasu nie otrzymałem żadnej aktualizacji. Czy moglibyście podać mi bieżący status?" + language = 'pl' + + enriched_data = self.enricher.enrich_data(original_sentence, language) + self.assertIsNotNone(enriched_data) + + for i, paraphrase in enumerate(enriched_data, 1): + print(f"Paraphrase {i}: {paraphrase}") + + def test_enrich_data_with_finnish_sentence(self): + original_sentence = "Hei, toivottavasti tämä sähköposti tavoittaa teidät hyvin. Kirjoitan tiedustelun tilastani hakemukseni suhteen. Lähetin kaikki tarvittavat asiakirjat viime kuussa, mutta en ole sen jälkeen saanut päivityksiä. Voisitteko antaa minulle ajantasaisen tilanteen?" + language = 'fi' + + enriched_data = self.enricher.enrich_data(original_sentence, language) + self.assertIsNotNone(enriched_data) + + for i, paraphrase in enumerate(enriched_data, 1): + print(f"Paraphrase {i}: {paraphrase}") + +if __name__ == "__main__": + unittest.main() From ad19a71962e21b139f336cd7d777f0152134f0b3 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 28 Jun 2024 08:10:53 +0530 Subject: [PATCH 033/582] support language bug fix --- src/data_enrichment/data_enrichment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data_enrichment/data_enrichment.py b/src/data_enrichment/data_enrichment.py index 962617a3..03d0d9f9 100644 --- a/src/data_enrichment/data_enrichment.py +++ b/src/data_enrichment/data_enrichment.py @@ -7,7 +7,7 @@ def __init__(self): self.paraphraser = Paraphraser() def enrich_data(self, text, lang): - supported_languages = ['en', 'et', 'ru', 'pl', 'fi'] + supported_languages = ['en', 'et', 'ru', 'pl', 'fi', 'fr'] if lang not in supported_languages: print(f"Unsupported language: {lang}") From a7c1fa03e45388123d6888da078bf1463669930f Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 28 Jun 2024 08:11:07 +0530 Subject: [PATCH 034/582] paraphraser config file split --- .../config_files/paraphraser_config.json | 12 ++++++ src/data_enrichment/paraphraser.py | 40 ++++++++++--------- 2 files changed, 34 insertions(+), 18 deletions(-) create mode 100644 src/data_enrichment/config_files/paraphraser_config.json diff --git a/src/data_enrichment/config_files/paraphraser_config.json b/src/data_enrichment/config_files/paraphraser_config.json new file mode 100644 index 00000000..e0bb0494 --- /dev/null +++ b/src/data_enrichment/config_files/paraphraser_config.json @@ -0,0 +1,12 @@ +{ + "model_name": "humarin/chatgpt_paraphraser_on_T5_base", + "num_beams": 5, + "num_beam_groups": 5, + "num_return_sequences": 3, + "repetition_penalty": 10.0, + "diversity_penalty": 3.0, + "no_repeat_ngram_size": 2, + "temperature": 0.7, + "max_length": 128 + } + \ No newline at end of file diff --git a/src/data_enrichment/paraphraser.py b/src/data_enrichment/paraphraser.py index c81e5ddd..ed0b1a65 100644 --- a/src/data_enrichment/paraphraser.py +++ b/src/data_enrichment/paraphraser.py @@ -1,35 +1,39 @@ +import json from transformers import AutoTokenizer, AutoModelForSeq2SeqLM class Paraphraser: - def __init__(self): - self.model_name = "humarin/chatgpt_paraphraser_on_T5_base" + def __init__(self, config_path="config_files/paraphraser_config.json"): + with open(config_path, 'r') as file: + config = json.load(file) + + self.model_name = config["model_name"] + self.num_beams = config["num_beams"] + self.num_beam_groups = config["num_beam_groups"] + self.num_return_sequences = config["num_return_sequences"] + self.repetition_penalty = config["repetition_penalty"] + self.diversity_penalty = config["diversity_penalty"] + self.no_repeat_ngram_size = config["no_repeat_ngram_size"] + self.temperature = config["temperature"] + self.max_length = config["max_length"] + self.tokenizer = AutoTokenizer.from_pretrained(self.model_name) self.model = AutoModelForSeq2SeqLM.from_pretrained(self.model_name) - def generate_paraphrases(self, question, - num_beams=5, - num_beam_groups=5, - num_return_sequences=3, - repetition_penalty=10.0, - diversity_penalty=3.0, - no_repeat_ngram_size=2, - temperature=0.7, - max_length=128 - ): + def generate_paraphrases(self, question): input_ids = self.tokenizer( f'paraphrase: {question}', return_tensors="pt", padding="longest", - max_length=max_length, + max_length=self.max_length, truncation=True, ).input_ids.to('cpu') outputs = self.model.generate( - input_ids, temperature=temperature, repetition_penalty=repetition_penalty, - num_return_sequences=num_return_sequences, no_repeat_ngram_size=no_repeat_ngram_size, - num_beams=num_beams, num_beam_groups=num_beam_groups, - max_length=max_length, diversity_penalty=diversity_penalty + input_ids, temperature=self.temperature, repetition_penalty=self.repetition_penalty, + num_return_sequences=self.num_return_sequences, no_repeat_ngram_size=self.no_repeat_ngram_size, + num_beams=self.num_beams, num_beam_groups=self.num_beam_groups, + max_length=self.max_length, diversity_penalty=self.diversity_penalty ) res = self.tokenizer.batch_decode(outputs, skip_special_tokens=True) - return res \ No newline at end of file + return res From 31099fe817f6b4f12d62bfea85d8be2e43dec310 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 28 Jun 2024 08:11:19 +0530 Subject: [PATCH 035/582] translator config file split --- .../config_files/translator_config.json | 11 +++++++++++ src/data_enrichment/translator.py | 16 ++++++---------- 2 files changed, 17 insertions(+), 10 deletions(-) create mode 100644 src/data_enrichment/config_files/translator_config.json diff --git a/src/data_enrichment/config_files/translator_config.json b/src/data_enrichment/config_files/translator_config.json new file mode 100644 index 00000000..d3c30e18 --- /dev/null +++ b/src/data_enrichment/config_files/translator_config.json @@ -0,0 +1,11 @@ +{ + "models": { + "et-en": ["Helsinki-NLP/opus-mt-et-en", "Helsinki-NLP/opus-mt-en-et"], + "ru-en": ["Helsinki-NLP/opus-mt-ru-en", "Helsinki-NLP/opus-mt-en-ru"], + "pl-en": ["Helsinki-NLP/opus-mt-pl-en", "Helsinki-NLP/opus-mt-en-pl"], + "fi-en": ["Helsinki-NLP/opus-mt-fi-en", "Helsinki-NLP/opus-mt-en-fi"], + "en-fr": ["Helsinki-NLP/opus-mt-en-fr", "Helsinki-NLP/opus-mt-fr-en"], + "fr-pl": ["Helsinki-NLP/opus-mt-fr-pl", "Helsinki-NLP/opus-mt-pl-fr"] + } + } + \ No newline at end of file diff --git a/src/data_enrichment/translator.py b/src/data_enrichment/translator.py index 38685cfd..d2a939e1 100644 --- a/src/data_enrichment/translator.py +++ b/src/data_enrichment/translator.py @@ -1,15 +1,12 @@ +import json from transformers import MarianMTModel, MarianTokenizer class Translator: - def __init__(self): - self.models = { - 'et-en': ('Helsinki-NLP/opus-mt-et-en', 'Helsinki-NLP/opus-mt-en-et'), - 'ru-en': ('Helsinki-NLP/opus-mt-ru-en', 'Helsinki-NLP/opus-mt-en-ru'), - 'pl-en': ('Helsinki-NLP/opus-mt-pl-en', 'Helsinki-NLP/opus-mt-en-pl'), - 'fi-en': ('Helsinki-NLP/opus-mt-fi-en', 'Helsinki-NLP/opus-mt-en-fi'), - 'en-fr': ('Helsinki-NLP/opus-mt-en-fr', 'Helsinki-NLP/opus-mt-fr-en'), - 'fr-pl': ('Helsinki-NLP/opus-mt-fr-pl', 'Helsinki-NLP/opus-mt-pl-fr'), - } + def __init__(self, config_path="config_files/translator_config.json"): + with open(config_path, 'r') as file: + config = json.load(file) + + self.models = config["models"] self.tokenizers = {} self.models_instances = {} @@ -44,4 +41,3 @@ def _translate(self, text, src_lang, tgt_lang): translated_text = tokenizer.decode(translated_tokens[0], skip_special_tokens=True) return translated_text - From 6a6f0c195f532c8a841d20bc1780db0c2743985a Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 28 Jun 2024 08:17:59 +0530 Subject: [PATCH 036/582] adding params and return type info --- src/data_enrichment/paraphraser.py | 5 +++-- src/data_enrichment/translator.py | 13 +++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/data_enrichment/paraphraser.py b/src/data_enrichment/paraphraser.py index ed0b1a65..ffc042ec 100644 --- a/src/data_enrichment/paraphraser.py +++ b/src/data_enrichment/paraphraser.py @@ -1,8 +1,9 @@ import json from transformers import AutoTokenizer, AutoModelForSeq2SeqLM +from typing import List class Paraphraser: - def __init__(self, config_path="config_files/paraphraser_config.json"): + def __init__(self, config_path: str = "config_files/paraphraser_config.json"): with open(config_path, 'r') as file: config = json.load(file) @@ -19,7 +20,7 @@ def __init__(self, config_path="config_files/paraphraser_config.json"): self.tokenizer = AutoTokenizer.from_pretrained(self.model_name) self.model = AutoModelForSeq2SeqLM.from_pretrained(self.model_name) - def generate_paraphrases(self, question): + def generate_paraphrases(self, question: str) -> List[str]: input_ids = self.tokenizer( f'paraphrase: {question}', return_tensors="pt", padding="longest", diff --git a/src/data_enrichment/translator.py b/src/data_enrichment/translator.py index d2a939e1..4cb59e85 100644 --- a/src/data_enrichment/translator.py +++ b/src/data_enrichment/translator.py @@ -1,14 +1,15 @@ import json from transformers import MarianMTModel, MarianTokenizer +from typing import Dict, Tuple class Translator: - def __init__(self, config_path="config_files/translator_config.json"): + def __init__(self, config_path: str = "config_files/translator_config.json"): with open(config_path, 'r') as file: config = json.load(file) - self.models = config["models"] - self.tokenizers = {} - self.models_instances = {} + self.models: Dict[str, Tuple[str, str]] = config["models"] + self.tokenizers: Dict[str, MarianTokenizer] = {} + self.models_instances: Dict[str, MarianMTModel] = {} for key, (model_name, reverse_model_name) in self.models.items(): self.tokenizers[key] = MarianTokenizer.from_pretrained(model_name) @@ -18,7 +19,7 @@ def __init__(self, config_path="config_files/translator_config.json"): self.tokenizers[reverse_key] = MarianTokenizer.from_pretrained(reverse_model_name) self.models_instances[reverse_key] = MarianMTModel.from_pretrained(reverse_model_name) - def translate(self, text, src_lang, tgt_lang): + def translate(self, text: str, src_lang: str, tgt_lang: str) -> str: if src_lang == 'en' and tgt_lang == 'pl': intermediate_text = self._translate(text, 'en', 'fr') translated_text = self._translate(intermediate_text, 'fr', 'pl') @@ -27,7 +28,7 @@ def translate(self, text, src_lang, tgt_lang): return translated_text - def _translate(self, text, src_lang, tgt_lang): + def _translate(self, text: str, src_lang: str, tgt_lang: str) -> str: key = f'{src_lang}-{tgt_lang}' if key not in self.models_instances: raise ValueError(f"Translation from {src_lang} to {tgt_lang} is not supported.") From 87f2eb0d348b5547cf08c3dd641c31661dc49223 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 28 Jun 2024 08:18:09 +0530 Subject: [PATCH 037/582] fixing unit test issue --- src/data_enrichment/data_enrichment.py | 8 ++++---- src/data_enrichment/test_data_enrichment.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/data_enrichment/data_enrichment.py b/src/data_enrichment/data_enrichment.py index 03d0d9f9..1f64d2fb 100644 --- a/src/data_enrichment/data_enrichment.py +++ b/src/data_enrichment/data_enrichment.py @@ -1,17 +1,18 @@ from translator import Translator from paraphraser import Paraphraser +from typing import List class DataEnrichment: def __init__(self): self.translator = Translator() self.paraphraser = Paraphraser() - def enrich_data(self, text, lang): - supported_languages = ['en', 'et', 'ru', 'pl', 'fi', 'fr'] + def enrich_data(self, text: str, lang: str) -> List[str]: + supported_languages = ['en', 'et', 'ru', 'pl', 'fi'] if lang not in supported_languages: print(f"Unsupported language: {lang}") - return None + return [] if lang == 'en': paraphrases = self.paraphraser.generate_paraphrases(text) @@ -25,4 +26,3 @@ def enrich_data(self, text, lang): return translated_paraphrases return paraphrases - diff --git a/src/data_enrichment/test_data_enrichment.py b/src/data_enrichment/test_data_enrichment.py index f1a97936..ff06f866 100644 --- a/src/data_enrichment/test_data_enrichment.py +++ b/src/data_enrichment/test_data_enrichment.py @@ -19,7 +19,7 @@ def test_enrich_data_with_supported_language(self): def test_enrich_data_with_unsupported_language(self): original_sentence = "Hello, I hope this email finds you well. I am writing to inquire about the status of my application." - language = 'fr' + language = 'sl' enriched_data = self.enricher.enrich_data(original_sentence, language) self.assertIsNone(enriched_data) From ab684a80766ba6f96bd9651a1fdfb113a709a847 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 28 Jun 2024 08:26:02 +0530 Subject: [PATCH 038/582] fixed unit test bug --- src/data_enrichment/test_data_enrichment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data_enrichment/test_data_enrichment.py b/src/data_enrichment/test_data_enrichment.py index ff06f866..a4afea67 100644 --- a/src/data_enrichment/test_data_enrichment.py +++ b/src/data_enrichment/test_data_enrichment.py @@ -22,7 +22,7 @@ def test_enrich_data_with_unsupported_language(self): language = 'sl' enriched_data = self.enricher.enrich_data(original_sentence, language) - self.assertIsNone(enriched_data) + self.assertEqual(enriched_data, []) def test_enrich_data_with_estonian_sentence(self): original_sentence = "Tere, ma loodan, et see e-kiri leiab teid hästi. Ma kirjutan päringu staatuse minu taotluse. Ma esitasin kõik nõutavad dokumendid möödunud kuu, kuid ma pole saanud ühtegi värskendust sellest ajast. Kas saaksite palun mulle hetkeseisu kohta teavet anda?" From 4efe1963a6a7226077f1401278ae3d544c271bc3 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 28 Jun 2024 19:57:04 +0530 Subject: [PATCH 039/582] requirnment txt file --- src/python_requirements.txt | 49 +++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/python_requirements.txt diff --git a/src/python_requirements.txt b/src/python_requirements.txt new file mode 100644 index 00000000..90ddc8d5 --- /dev/null +++ b/src/python_requirements.txt @@ -0,0 +1,49 @@ +accelerate==0.31.0 +certifi==2024.6.2 +charset-normalizer==3.3.2 +click==8.1.7 +colorama==0.4.6 +et-xmlfile==1.1.0 +filelock==3.13.1 +fsspec==2024.2.0 +huggingface-hub==0.23.3 +idna==3.7 +install==1.3.5 +intel-openmp==2021.4.0 +Jinja2==3.1.3 +joblib==1.4.2 +langdetect==1.0.9 +MarkupSafe==2.1.5 +mkl==2021.4.0 +mpmath==1.3.0 +networkx==3.2.1 +numpy==1.26.3 +openpyxl==3.1.3 +packaging==24.1 +pandas==2.2.2 +pillow==10.2.0 +protobuf==5.27.1 +psutil==5.9.8 +python-dateutil==2.9.0.post0 +pytz==2024.1 +PyYAML==6.0.1 +regex==2024.5.15 +requests==2.32.3 +sacremoses==0.1.1 +safetensors==0.4.3 +scikit-learn==1.5.0 +scipy==1.13.1 +sentencepiece==0.2.0 +six==1.16.0 +sympy==1.12 +tbb==2021.11.0 +threadpoolctl==3.5.0 +tokenizers==0.19.1 +torch==2.3.1+cu121 +torchaudio==2.3.1+cu121 +torchvision==0.18.1+cu121 +tqdm==4.66.4 +transformers==4.41.2 +typing_extensions==4.9.0 +tzdata==2024.1 +urllib3==2.2.1 From 73e799194bb9ab7169f8069c5dfe8bbd5add3055 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 28 Jun 2024 20:04:26 +0530 Subject: [PATCH 040/582] fixing config file missed config --- src/data_enrichment/config_files/translator_config.json | 3 ++- src/data_enrichment/translator.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/data_enrichment/config_files/translator_config.json b/src/data_enrichment/config_files/translator_config.json index d3c30e18..db759ef2 100644 --- a/src/data_enrichment/config_files/translator_config.json +++ b/src/data_enrichment/config_files/translator_config.json @@ -6,6 +6,7 @@ "fi-en": ["Helsinki-NLP/opus-mt-fi-en", "Helsinki-NLP/opus-mt-en-fi"], "en-fr": ["Helsinki-NLP/opus-mt-en-fr", "Helsinki-NLP/opus-mt-fr-en"], "fr-pl": ["Helsinki-NLP/opus-mt-fr-pl", "Helsinki-NLP/opus-mt-pl-fr"] - } + }, + "unsupported-en-pl_model":"Helsinki-NLP/opus-mt-en-pl" } \ No newline at end of file diff --git a/src/data_enrichment/translator.py b/src/data_enrichment/translator.py index 4cb59e85..dd8470e7 100644 --- a/src/data_enrichment/translator.py +++ b/src/data_enrichment/translator.py @@ -15,7 +15,7 @@ def __init__(self, config_path: str = "config_files/translator_config.json"): self.tokenizers[key] = MarianTokenizer.from_pretrained(model_name) self.models_instances[key] = MarianMTModel.from_pretrained(model_name) reverse_key = f"{key.split('-')[1]}-{key.split('-')[0]}" - if reverse_model_name != 'Helsinki-NLP/opus-mt-en-pl': + if reverse_model_name != config["unsupported-en-pl_model"]: self.tokenizers[reverse_key] = MarianTokenizer.from_pretrained(reverse_model_name) self.models_instances[reverse_key] = MarianMTModel.from_pretrained(reverse_model_name) From bd231ae32759378d545e78f473ef7dc36bd240ee Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Sat, 29 Jun 2024 00:46:53 +0530 Subject: [PATCH 041/582] dataset groups cards view --- GUI/.env.development | 3 +- GUI/src/App.tsx | 2 +- GUI/src/assets/Dataset.tsx | 18 +++ .../FormElements/Switch/Switch.scss | 1 - .../DatasetGroupCard/DatasetGroupCard.scss | 73 ++++++++++ .../molecules/DatasetGroupCard/index.tsx | 48 +++++++ .../pages/DatasetGroups/DatasetGroups.scss | 135 ++++-------------- GUI/src/pages/DatasetGroups/index.tsx | 44 +++--- docker-compose.yml | 2 + 9 files changed, 198 insertions(+), 128 deletions(-) create mode 100644 GUI/src/assets/Dataset.tsx create mode 100644 GUI/src/components/molecules/DatasetGroupCard/DatasetGroupCard.scss create mode 100644 GUI/src/components/molecules/DatasetGroupCard/index.tsx diff --git a/GUI/.env.development b/GUI/.env.development index 526b0e9b..a9bac609 100644 --- a/GUI/.env.development +++ b/GUI/.env.development @@ -12,5 +12,6 @@ REACT_APP_SETTINGS_BASE_URL=http://localhost:8080/settings REACT_APP_MONITORING_BASE_URL=http://localhost:8080/monitoring REACT_APP_SERVICE_ID=conversations,settings,monitoring REACT_APP_NOTIFICATION_NODE_URL=http://localhost:4040 -REACT_APP_CSP=upgrade-insecure-requests; default-src 'self'; font-src 'self' data:; img-src 'self' data:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self' http://localhost:8086 http://localhost:8088 http://localhost:8085 http://localhost:4040 https://admin.dev.buerokratt.ee/chat/menu.json; +REACT_APP_CSP=upgrade-insecure-requests; default-src 'self'; font-src 'self' data:; img-src 'self' data:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self' http://localhost:8086 http://localhost:8088 http://localhost:8085 http://localhost:4040 http://localhost:3002/menu.json; REACT_APP_ENABLE_HIDDEN_FEATURES=TRUE +REACT_APP_MENU_JSON=[{"id":"conversations","label":{"et":"Vestlused","en":"Conversations"},"path":"/chat","children":[{"label":{"et":"Vastamata","en":"Unanswered"},"path":"/unanswered"},{"label":{"et":"Aktiivsed","en":"Active"},"path":"/active"},{"label":{"et":"Ajalugu","en":"History"},"path":"/history"}]},{"id":"training","label":{"et":"Treening","en":"Training"},"path":"/training","children":[{"label":{"et":"Treening","en":"Training"},"path":"/training","children":[{"label":{"et":"Teemad","en":"Themes"},"path":"/training/intents"},{"label":{"et":"Avalikud teemad","en":"Public themes"},"path":"/training/common-intents"},{"label":{"et":"Teemade järeltreenimine","en":"Post training themes"},"path":"/training/intents-followup-training"},{"label":{"et":"Vastused","en":"Answers"},"path":"/training/responses"},{"label":{"et":"Kasutuslood","en":"User Stories"},"path":"/training/stories"},{"label":{"et":"Konfiguratsioon","en":"Configuration"},"path":"/training/configuration"},{"label":{"et":"Vormid","en":"Forms"},"path":"/training/forms"},{"label":{"et":"Mälukohad","en":"Slots"},"path":"/training/slots"},{"label":{"et":"Automatic Teenused","en":"Automatic Services"},"path":"/auto-services"}]},{"label":{"et":"Ajaloolised vestlused","en":"Historical conversations"},"path":"/history","children":[{"label":{"et":"Ajalugu","en":"History"},"path":"/history/history"},{"label":{"et":"Pöördumised","en":"Appeals"},"path":"/history/appeal"}]},{"label":{"et":"Mudelipank ja analüütika","en":"Modelbank and analytics"},"path":"/analytics","children":[{"label":{"et":"Teemade ülevaade","en":"Overview of topics"},"path":"/analytics/overview"},{"label":{"et":"Mudelite võrdlus","en":"Comparison of models"},"path":"/analytics/models"},{"label":{"et":"Testlood","en":"testTracks"},"path":"/analytics/testcases"}]},{"label":{"et":"Treeni uus mudel","en":"Train new model"},"path":"/train-new-model"}]},{"id":"analytics","label":{"et":"Analüütika","en":"Analytics"},"path":"/analytics","children":[{"label":{"et":"Ülevaade","en":"Overview"},"path":"/overview"},{"label":{"et":"Vestlused","en":"Chats"},"path":"/chats"},{"label":{"et":"Bürokratt","en":"Burokratt"},"path":"/burokratt"},{"label":{"et":"Tagasiside","en":"Feedback"},"path":"/feedback"},{"label":{"et":"Nõustajad","en":"Advisors"},"path":"/advisors"},{"label":{"et":"Avaandmed","en":"Reports"},"path":"/reports"}]},{"id":"services","label":{"et":"Teenused","en":"Services"},"path":"/services","children":[{"label":{"et":"Ülevaade","en":"Overview"},"path":"/overview"},{"label":{"et":"Uus teenus","en":"New Service"},"path":"/newService"},{"label":{"et":"Automatic Teenused","en":"Automatic Services"},"path":"/auto-services"},{"label":{"et":"Probleemsed teenused","en":"Faulty Services"},"path":"/faultyServices"}]},{"id":"settings","label":{"et":"Haldus","en":"Administration"},"path":"/settings","children":[{"label":{"et":"Kasutajad","en":"Users"},"path":"/users"},{"label":{"et":"Vestlusbot","en":"Chatbot"},"path":"/chatbot","children":[{"label":{"et":"Seaded","en":"Settings"},"path":"/chatbot/settings"},{"label":{"et":"Tervitussõnum","en":"Welcome message"},"path":"/chatbot/welcome-message"},{"label":{"et":"Välimus ja käitumine","en":"Appearance and behavior"},"path":"/chatbot/appearance"},{"label":{"et":"Erakorralised teated","en":"Emergency notices"},"path":"/chatbot/emergency-notices"}]},{"label":{"et":"Asutuse tööaeg","en":"Office opening hours"},"path":"/working-time"},{"label":{"et":"Sessiooni pikkus","en":"Session length"},"path":"/session-length"}]},{"id":"monitoring","label":{"et":"Seire","en":"Monitoring"},"path":"/monitoring","children":[{"label":{"et":"Aktiivaeg","en":"Working hours"},"path":"/uptime"}]}] diff --git a/GUI/src/App.tsx b/GUI/src/App.tsx index 7f568b60..149a0bbd 100644 --- a/GUI/src/App.tsx +++ b/GUI/src/App.tsx @@ -9,7 +9,7 @@ import { UserInfo } from 'types/userInfo'; import './locale/et_EE'; import UserManagement from 'pages/UserManagement'; import Integrations from 'pages/Integrations'; -import DatasetGroups from 'pages/DatasetGroups'; +import DatasetGroups from 'pages/DataSetGroups'; const App: FC = () => { diff --git a/GUI/src/assets/Dataset.tsx b/GUI/src/assets/Dataset.tsx new file mode 100644 index 00000000..6b46aff4 --- /dev/null +++ b/GUI/src/assets/Dataset.tsx @@ -0,0 +1,18 @@ +const Dataset = () => { + return ( + + + + ); +}; + +export default Dataset; diff --git a/GUI/src/components/FormElements/Switch/Switch.scss b/GUI/src/components/FormElements/Switch/Switch.scss index da3c14a2..fddf67c0 100644 --- a/GUI/src/components/FormElements/Switch/Switch.scss +++ b/GUI/src/components/FormElements/Switch/Switch.scss @@ -8,7 +8,6 @@ display: flex; align-items: center; gap: get-spacing(paldiski); - width: 100%; &__label { flex: 0 0 185px; diff --git a/GUI/src/components/molecules/DatasetGroupCard/DatasetGroupCard.scss b/GUI/src/components/molecules/DatasetGroupCard/DatasetGroupCard.scss new file mode 100644 index 00000000..1a140c21 --- /dev/null +++ b/GUI/src/components/molecules/DatasetGroupCard/DatasetGroupCard.scss @@ -0,0 +1,73 @@ +.row { + margin: 10px 0; + display: flex; + align-items: center; +} + +.switch-row { + justify-content: flex-end; +} + +.status-indicators { + display: flex; + align-items: center; + gap: 10px; +} + +.status { + display: flex; + align-items: center; + font-size: 14px; + color: #333; +} + +.dot { + width: 10px; + height: 10px; + border-radius: 50%; + margin-right: 8px; +} + +.green { + background-color: green; +} + +.grey { + background-color: grey; +} + +.icon-text-row { + flex-direction: column; + justify-content: center; + text-align: center; +} + +.icon-image { + width: 50px; + height: 50px; +} + +.icon-text { + margin: 5px 0 0 0; + font-size: 16px; +} + +.label-row { + justify-content: flex-start; + display: flex; + align-items: center; +} + +.left-label { + font-size: 14px; + color: #333; +} + +.status { + display: flex; + align-items: center; + font-size: 12px; + color: #555; + bottom: 10px; + left: 20px; +} diff --git a/GUI/src/components/molecules/DatasetGroupCard/index.tsx b/GUI/src/components/molecules/DatasetGroupCard/index.tsx new file mode 100644 index 00000000..b3b66344 --- /dev/null +++ b/GUI/src/components/molecules/DatasetGroupCard/index.tsx @@ -0,0 +1,48 @@ +import { FC, PropsWithChildren } from 'react'; +import './DatasetGroupCard.scss'; +import Dataset from 'assets/Dataset'; +import { Switch } from 'components/FormElements'; + +type DatasetGroupCardProps = { + isEnabled?: boolean; + datasetName?: string; + status?: string; +}; + +const DatasetGroupCard: FC> = ({ + isEnabled, + datasetName, + status, +}) => { + return ( + <> +
    +
    + +
    +
    +
    + +
    +
    +
    +

    {datasetName}

    +
    +
    +
    + + {' '} + {status} + +
    +
    +
    + + ); +}; + +export default DatasetGroupCard; diff --git a/GUI/src/pages/DatasetGroups/DatasetGroups.scss b/GUI/src/pages/DatasetGroups/DatasetGroups.scss index ad0fa70c..2a6dd6e8 100644 --- a/GUI/src/pages/DatasetGroups/DatasetGroups.scss +++ b/GUI/src/pages/DatasetGroups/DatasetGroups.scss @@ -1,112 +1,29 @@ @import 'src/styles/tools/color'; -.search-panel{ - background-color: white; - border-radius: 10px; - border: solid 1px get-color(black-coral-1); - display: flex; - align-items: center; - gap: 10px; - padding: 15px; - -} - - -.card { - position: relative; - width: 200px; - height: 150px; - padding: 20px; - border: 1px solid #ccc; - border-radius: 8px; - background-color: #fff; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 10px; -} - -.toggle-switch { - position: absolute; - top: 10px; - right: 10px; -} - -.toggle-switch input[type="checkbox"] { - display: none; -} - -.toggle-switch label { - display: inline-block; - width: 40px; - height: 20px; - background-color: #ccc; - border-radius: 20px; - position: relative; - cursor: pointer; -} - -.toggle-switch label::after { - content: ''; - display: inline-block; - width: 18px; - height: 18px; - background-color: #fff; - border-radius: 50%; - position: absolute; - top: 1px; - left: 1px; - transition: transform 0.3s ease; -} - -.toggle-switch input[type="checkbox"]:checked + label { - background-color: #007bff; +.search-panel { + background-color: white; + border-radius: 10px; + border: solid 1px get-color(black-coral-1); + display: flex; + align-items: center; + gap: 10px; + padding: 15px; +} + +.dataset-group-card { + width: 100%; + border: 1px solid #ccc; + border-radius: 8px; + background-color: #fff; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + padding: 5px; + box-sizing: border-box; +} + +.grid-container { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 16px; + padding: 16px; + width: 100%; } - -.toggle-switch input[type="checkbox"]:checked + label::after { - transform: translateX(20px); -} - -.row { - flex: 1; /* Each row will take up equal space */ - display: flex; - justify-content: center; - align-items: center; -} - -.content { - display: flex; - flex-direction: column; - align-items: center; -} - -.icon img { - width: 48px; - height: 48px; -} - -.label { - font-size: 16px; - font-weight: 500; - margin-top: 10px; -} - -.status { - display: flex; - align-items: center; - font-size: 14px; - color: #555; - position: absolute; - bottom: 10px; - left: 20px; -} - -.status .dot { - width: 10px; - height: 10px; - background-color: #28a745; - border-radius: 50%; - margin-right: 5px; -} \ No newline at end of file diff --git a/GUI/src/pages/DatasetGroups/index.tsx b/GUI/src/pages/DatasetGroups/index.tsx index f4517c2f..2a8941d2 100644 --- a/GUI/src/pages/DatasetGroups/index.tsx +++ b/GUI/src/pages/DatasetGroups/index.tsx @@ -1,12 +1,25 @@ import { FC } from 'react'; import './DatasetGroups.scss'; import { useTranslation } from 'react-i18next'; -import { Button, Card, FormInput, FormSelect, Switch } from 'components'; -import Jira from 'assets/Jira'; +import { Button, FormInput, FormSelect } from 'components'; +import DatasetGroupCard from 'components/molecules/DatasetGroupCard'; const DatasetGroups: FC = () => { const { t } = useTranslation(); + const datasets = [ + { datasetName: 'Dataset 10', status: 'Connected', isEnabled: false }, + { datasetName: 'Dataset 2', status: 'Disconnected', isEnabled: false }, + { datasetName: 'Dataset 2', status: 'Disconnected', isEnabled: true }, + { datasetName: 'Dataset 9', status: 'Disconnected', isEnabled: false }, + { datasetName: 'Dataset 4', status: 'Disconnected', isEnabled: true }, + { datasetName: 'Dataset 10', status: 'Connected', isEnabled: true }, + { datasetName: 'Dataset 9', status: 'Disconnected', isEnabled: true }, + { datasetName: 'Dataset 2', status: 'Disconnected', isEnabled: true }, + { datasetName: 'Dataset 4', status: 'Disconnected', isEnabled: true }, + { datasetName: 'Dataset 3', status: 'Disconnected', isEnabled: false }, + ]; + return ( <>
    @@ -33,20 +46,19 @@ const DatasetGroups: FC = () => { ]} />
    -
    -
    -
    - -
    -
    -
    -
    - Dataset 1
    - -
    - connected -
    -
    +
    + {datasets.map((dataset) => { + return ( + + ); + })}
    diff --git a/docker-compose.yml b/docker-compose.yml index 02467655..6a4ec1ac 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -160,6 +160,8 @@ services: - REACT_APP_MONITORING_BASE_URL=http://localhost:8080/monitoring - REACT_APP_SERVICE_ID=conversations,settings,monitoring - REACT_APP_ENABLE_HIDDEN_FEATURES=TRUE + - REACT_APP_MENU_JSON=[{"id":"conversations","label":{"et":"Vestlused","en":"Conversations"},"path":"/chat","children":[{"label":{"et":"Vastamata","en":"Unanswered"},"path":"/unanswered"},{"label":{"et":"Aktiivsed","en":"Active"},"path":"/active"},{"label":{"et":"Ajalugu","en":"History"},"path":"/history"}]},{"id":"training","label":{"et":"Treening","en":"Training"},"path":"/training","children":[{"label":{"et":"Treening","en":"Training"},"path":"/training","children":[{"label":{"et":"Teemad","en":"Themes"},"path":"/training/intents"},{"label":{"et":"Avalikud teemad","en":"Public themes"},"path":"/training/common-intents"},{"label":{"et":"Teemade järeltreenimine","en":"Post training themes"},"path":"/training/intents-followup-training"},{"label":{"et":"Vastused","en":"Answers"},"path":"/training/responses"},{"label":{"et":"Kasutuslood","en":"User Stories"},"path":"/training/stories"},{"label":{"et":"Konfiguratsioon","en":"Configuration"},"path":"/training/configuration"},{"label":{"et":"Vormid","en":"Forms"},"path":"/training/forms"},{"label":{"et":"Mälukohad","en":"Slots"},"path":"/training/slots"},{"label":{"et":"Automatic Teenused","en":"Automatic Services"},"path":"/auto-services"}]},{"label":{"et":"Ajaloolised vestlused","en":"Historical conversations"},"path":"/history","children":[{"label":{"et":"Ajalugu","en":"History"},"path":"/history/history"},{"label":{"et":"Pöördumised","en":"Appeals"},"path":"/history/appeal"}]},{"label":{"et":"Mudelipank ja analüütika","en":"Modelbank and analytics"},"path":"/analytics","children":[{"label":{"et":"Teemade ülevaade","en":"Overview of topics"},"path":"/analytics/overview"},{"label":{"et":"Mudelite võrdlus","en":"Comparison of models"},"path":"/analytics/models"},{"label":{"et":"Testlood","en":"testTracks"},"path":"/analytics/testcases"}]},{"label":{"et":"Treeni uus mudel","en":"Train new model"},"path":"/train-new-model"}]},{"id":"analytics","label":{"et":"Analüütika","en":"Analytics"},"path":"/analytics","children":[{"label":{"et":"Ülevaade","en":"Overview"},"path":"/overview"},{"label":{"et":"Vestlused","en":"Chats"},"path":"/chats"},{"label":{"et":"Bürokratt","en":"Burokratt"},"path":"/burokratt"},{"label":{"et":"Tagasiside","en":"Feedback"},"path":"/feedback"},{"label":{"et":"Nõustajad","en":"Advisors"},"path":"/advisors"},{"label":{"et":"Avaandmed","en":"Reports"},"path":"/reports"}]},{"id":"services","label":{"et":"Teenused","en":"Services"},"path":"/services","children":[{"label":{"et":"Ülevaade","en":"Overview"},"path":"/overview"},{"label":{"et":"Uus teenus","en":"New Service"},"path":"/newService"},{"label":{"et":"Automatic Teenused","en":"Automatic Services"},"path":"/auto-services"},{"label":{"et":"Probleemsed teenused","en":"Faulty Services"},"path":"/faultyServices"}]},{"id":"settings","label":{"et":"Haldus","en":"Administration"},"path":"/settings","children":[{"label":{"et":"Kasutajad","en":"Users"},"path":"/users"},{"label":{"et":"Vestlusbot","en":"Chatbot"},"path":"/chatbot","children":[{"label":{"et":"Seaded","en":"Settings"},"path":"/chatbot/settings"},{"label":{"et":"Tervitussõnum","en":"Welcome message"},"path":"/chatbot/welcome-message"},{"label":{"et":"Välimus ja käitumine","en":"Appearance and behavior"},"path":"/chatbot/appearance"},{"label":{"et":"Erakorralised teated","en":"Emergency notices"},"path":"/chatbot/emergency-notices"}]},{"label":{"et":"Asutuse tööaeg","en":"Office opening hours"},"path":"/working-time"},{"label":{"et":"Sessiooni pikkus","en":"Session length"},"path":"/session-length"}]},{"id":"monitoring","label":{"et":"Seire","en":"Monitoring"},"path":"/monitoring","children":[{"label":{"et":"Aktiivaeg","en":"Working hours"},"path":"/uptime"}]}] + build: context: ./GUI dockerfile: Dockerfile.dev From c28689e862974e954732f0a088bd794e44699c16 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Sat, 29 Jun 2024 11:15:06 +0530 Subject: [PATCH 042/582] classifier-97 implement integration API's which needed for frontend integration --- ...return_platform_response_format.handlebars | 5 + .../hbs/return_platform_status.handlebars | 8 ++ DSL/DMapper/lib/helpers.js | 10 +- .../integration/platform-status.yml | 40 ++++++ .../jira/cloud/toggle-subscription.yml | 31 ++--- .../integration/toggle-platform.yml | 115 ++++++++++++++++++ constants.ini | 17 ++- docker-compose.yml | 40 +++++- 8 files changed, 229 insertions(+), 37 deletions(-) create mode 100644 DSL/DMapper/hbs/return_platform_response_format.handlebars create mode 100644 DSL/DMapper/hbs/return_platform_status.handlebars create mode 100644 DSL/Ruuter.private/DSL/GET/classifier/integration/platform-status.yml create mode 100644 DSL/Ruuter.private/DSL/POST/classifier/integration/toggle-platform.yml diff --git a/DSL/DMapper/hbs/return_platform_response_format.handlebars b/DSL/DMapper/hbs/return_platform_response_format.handlebars new file mode 100644 index 00000000..a46c3f56 --- /dev/null +++ b/DSL/DMapper/hbs/return_platform_response_format.handlebars @@ -0,0 +1,5 @@ +{ + "operation_type": "{{data.key}}", + "operation_status_code": "{{data.fields.summary}}", + "operation_status": "{{data.fields.project.key}}" +} diff --git a/DSL/DMapper/hbs/return_platform_status.handlebars b/DSL/DMapper/hbs/return_platform_status.handlebars new file mode 100644 index 00000000..2c6055c1 --- /dev/null +++ b/DSL/DMapper/hbs/return_platform_status.handlebars @@ -0,0 +1,8 @@ +{{#*inline "platformStatus"}} +{ + "jira_connection_status": {{platformStatus "JIRA" data}}, + "outlook_connection_status": {{platformStatus "OUTLOOK" data}}, + "pinal_connection_status": {{platformStatus "PINAL" data}} +} +{{/inline}} +{{> platformStatus}} \ No newline at end of file diff --git a/DSL/DMapper/lib/helpers.js b/DSL/DMapper/lib/helpers.js index c6728751..de222120 100644 --- a/DSL/DMapper/lib/helpers.js +++ b/DSL/DMapper/lib/helpers.js @@ -22,12 +22,12 @@ export function getAuthHeader(username, token) { } export function mergeLabelData(labels, existing_labels) { - // Merge the arrays let mergedArray = [...labels, ...existing_labels]; - - // Remove duplicates let uniqueArray = [...new Set(mergedArray)]; - - // Return as JSON object return { labels: uniqueArray }; } + +export function platformStatus(platform, data) { + const platformData = data.find((item) => item.platform === platform); + return platformData ? platformData.isConnect : false; +} diff --git a/DSL/Ruuter.private/DSL/GET/classifier/integration/platform-status.yml b/DSL/Ruuter.private/DSL/GET/classifier/integration/platform-status.yml new file mode 100644 index 00000000..19a8d9a0 --- /dev/null +++ b/DSL/Ruuter.private/DSL/GET/classifier/integration/platform-status.yml @@ -0,0 +1,40 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'PLATFORM-STATUS'" + method: get + accepts: json + returns: json + namespace: classifier + +get_platform_status: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-integration-status" + result: res + +check_platform_response_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: set_platform_status + next: error_fetch_data + +set_platform_status: + call: http.post + args: + url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/return_platform_status" + headers: + type: json + body: + data: ${res.response.body} + result: mapped_data + next: return_result + +return_result: + return: ${mapped_data.response.body} + next: end + +error_fetch_data: + status: 400 + return: "Bad Request- Error fetching data" + next: end diff --git a/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/toggle-subscription.yml b/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/toggle-subscription.yml index fc1adb6b..27202de9 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/toggle-subscription.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/toggle-subscription.yml @@ -14,9 +14,11 @@ declaration: extract_request_data: assign: - is_connect: ${incoming.body.isConnect} + is_connect: ${incoming.body.is_connect} next: get_auth_header +#check already subcribe or not from db + get_auth_header: call: http.post args: @@ -44,36 +46,19 @@ subscribe_jira: body: enabled: true result: res - next: assign_jira_webhook_status + next: return_result unsubscribe_jira: call: http.put args: - url: "[#JIRA_CLOUD_DOMAIN]/rest/webhooks/1.0/webhook/1" + url: "[#JIRA_CLOUD_DOMAIN]/rest/webhooks/1.0/webhook/[#JIRA_WEBHOOK_ID]" headers: Authorization: ${auth_header.response.body.val} body: enabled: false result: res - next: assign_jira_webhook_status - -assign_jira_webhook_status: - assign: - status: ${res.response.body.enabled} - next: check_jira_webhook_response - -check_jira_webhook_response: - switch: - - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} - next: return_ok - next: return_bad_request - -return_ok: - status: 200 - return: "webhook service successfully ${status}" - next: end + next: return_result -return_bad_request: - status: 400 - return: "Bad Request" +return_result: + return: res.response.body next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/POST/classifier/integration/toggle-platform.yml b/DSL/Ruuter.private/DSL/POST/classifier/integration/toggle-platform.yml new file mode 100644 index 00000000..70de6795 --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/classifier/integration/toggle-platform.yml @@ -0,0 +1,115 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'TOGGLE-PLATFORM'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: operation + type: string + description: "Body field 'operation'" + - field: platform + type: string + description: "Body field 'platform'" + +extract_request_data: + assign: + operation: ${incoming.body.operation} + platform: ${incoming.body.platform} + next: check_operation + +check_operation: + switch: + - condition: ${operation === 'enable'} + next: assign_true + - condition: ${operation === 'disable'} + next: assign_false + next: operation_not_support + +assign_true: + assign: + is_connect: true + next: check_platform + +assign_false: + assign: + is_connect: false + next: check_platform + +check_platform: + switch: + - condition: ${platform === 'jira'} + next: assign_jira_url + - condition: ${operation === 'outlook'} + next: assign_outlook_url + - condition: ${operation === 'pinal'} + next: assign_pinal_url + next: platform_not_support + +assign_jira_url: + assign: + url: "jira/cloud/toggle-subscription" + next: route_to_platform + +assign_outlook_url: + assign: + url: "outlook/toggle-subscription" + next: route_to_platform + +assign_pinal_url: + assign: + url: "pinal/toggle-subscription" + next: route_to_platform + +route_to_platform: + call: http.post + args: + url: "[#CLASSIFIER_RUUTER_PRIVATE]/classifier/integration/${url}" + headers: + type: json + body: + is_connect: ${is_connect} + result: res + next: check_platform_response_status + +check_platform_response_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: assign_success + next: assign_fail + +assign_success: + assign: + operation_status: 'success' + next: assign_response + +assign_fail: + assign: + operation_status: 'failed' + next: assign_response + +assign_response: + assign: + format_res: { + operation_type: '${operation}', + operation_status_code: '${res.response.statusCodeValue}', + operation_status: '${operation_status}' + } + next: return_result + +return_result: + return: '${format_res}' + next: end + +operation_not_support: + status: 400 + return: "Bad Request-Operation not support" + next: end + +platform_not_support: + status: 400 + return: "Bad Request- Platform not support" + next: end \ No newline at end of file diff --git a/constants.ini b/constants.ini index 1025f1ce..5007c85f 100644 --- a/constants.ini +++ b/constants.ini @@ -1,10 +1,15 @@ [DSL] CLASSIFIER_RUUTER_PUBLIC=http://ruuter-public:8086 -CLASSIFIER_RUUTER_PRIVATE=http://ruuter-public:8088 +CLASSIFIER_RUUTER_PRIVATE=http://ruuter-private:8088 CLASSIFIER_DMAPPER=http://data-mapper:3000 -JIRA_API_TOKEN=value -JIRA_USERNAME=value -JIRA_CLOUD_DOMAIN=value -JIRA_WEBHOOK_ID=1 -JIRA_WEBHOOK_SECRET=value \ No newline at end of file +CLASSIFIER_RESQL=http://resql:8082 +JIRA_API_TOKEN= value +JIRA_USERNAME= value +JIRA_CLOUD_DOMAIN= value +JIRA_WEBHOOK_ID=value +JIRA_WEBHOOK_SECRET=value +TENANT=value +OUTLOOK_CLIENT_ID=value +OUTLOOK_SECRET_KEY=value +OUTLOOK_REFRESH_KEY=value \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 4019972a..52013dba 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,12 +3,13 @@ services: container_name: ruuter-private image: ruuter environment: - - application.cors.allowedOrigins=http://localhost:3001 - - application.httpCodesAllowList=200,201,202,204,400,401,403,500 + - application.cors.allowedOrigins=http://localhost:3001,http://localhost:8088,http://localhost:8082 + - application.httpCodesAllowList=200,201,202,204,400,401,403,404,500 - application.internalRequests.allowedIPs=127.0.0.1 - application.logging.displayRequestContent=true - application.incomingRequests.allowedMethodTypes=POST,GET,PUT - application.logging.displayResponseContent=true + - application.logging.printStackTrace=true - server.port=8088 volumes: - ./DSL/Ruuter.private/DSL:/DSL @@ -36,9 +37,42 @@ services: networks: - bykstack + resql: + container_name: resql + image: resql + depends_on: + - users_db + environment: + - sqlms.datasources.[0].name=classifier + - sqlms.datasources.[0].jdbcUrl=jdbc:postgresql://users_db:5432/classifier #For LocalDb Use + # sqlms.datasources.[0].jdbcUrl=jdbc:postgresql://171.22.247.13:5433/byk?sslmode=require + - sqlms.datasources.[0].username=root + - sqlms.datasources.[0].password=root + - logging.level.org.springframework.boot=INFO + ports: + - 8082:8082 + volumes: + - ./DSL/Resql:/workspace/app/templates/classifier + networks: + - bykstack + + users_db: + container_name: users_db + image: postgres:14.1 + environment: + - POSTGRES_USER=root + - POSTGRES_PASSWORD=root + - POSTGRES_DB=classifier + ports: + - 5433:5432 + volumes: + - ./data:/var/lib/postgresql/data + networks: + - bykstack + networks: bykstack: name: bykstack driver: bridge driver_opts: - com.docker.network.driver.mtu: 1400 + com.docker.network.driver.mtu: 1400 \ No newline at end of file From 0e747e90dd01fec2c58925d6c3eb465a066464f4 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Mon, 1 Jul 2024 08:44:31 +0530 Subject: [PATCH 043/582] classifier-97 validate the process flows in the endpoints --- .../hbs/return_jira_issue_info.handlebars | 1 + .../hbs/return_label_mismatch.handlebars | 3 + DSL/DMapper/lib/helpers.js | 17 ++++ .../changelog/create-initial-tables.sql | 14 +++- DSL/Resql/get-platform-input-row-data.sql | 3 + .../classifier/integration/outlook/token.yml | 3 +- .../integration/jira/cloud/accept.yml | 80 ++++++++++++++++++- .../jira/cloud/toggle-subscription.yml | 31 +++++++ .../outlook/toggle-subscription.yml | 6 +- 9 files changed, 150 insertions(+), 8 deletions(-) create mode 100644 DSL/DMapper/hbs/return_label_mismatch.handlebars create mode 100644 DSL/Resql/get-platform-input-row-data.sql diff --git a/DSL/DMapper/hbs/return_jira_issue_info.handlebars b/DSL/DMapper/hbs/return_jira_issue_info.handlebars index 3c645326..f60efd7f 100644 --- a/DSL/DMapper/hbs/return_jira_issue_info.handlebars +++ b/DSL/DMapper/hbs/return_jira_issue_info.handlebars @@ -1,5 +1,6 @@ { "jiraId": "{{data.key}}", "summary": "{{data.fields.summary}}", + "labels": "{{data.fields.labels}}", "projectId": "{{data.fields.project.key}}" } diff --git a/DSL/DMapper/hbs/return_label_mismatch.handlebars b/DSL/DMapper/hbs/return_label_mismatch.handlebars new file mode 100644 index 00000000..488aed0e --- /dev/null +++ b/DSL/DMapper/hbs/return_label_mismatch.handlebars @@ -0,0 +1,3 @@ +{ + "isMismatch": "{{isLabelsMismatch newLabels previousLabels}}" +} diff --git a/DSL/DMapper/lib/helpers.js b/DSL/DMapper/lib/helpers.js index de222120..1dac2416 100644 --- a/DSL/DMapper/lib/helpers.js +++ b/DSL/DMapper/lib/helpers.js @@ -31,3 +31,20 @@ export function platformStatus(platform, data) { const platformData = data.find((item) => item.platform === platform); return platformData ? platformData.isConnect : false; } + +export function isLabelsMismatch(newLabels, previousLabels) { + if ( + Array.isArray(newLabels) && + Array.isArray(previousLabels) && + newLabels.length === previousLabels.length + ) { + for (let i = 0; i < newLabels.length; i++) { + if (newLabels[i] !== previousLabels[i]) { + return true; + } + } + return false; + } else { + return true; + } +} diff --git a/DSL/Liquibase/changelog/create-initial-tables.sql b/DSL/Liquibase/changelog/create-initial-tables.sql index 79f84361..e32b84e9 100644 --- a/DSL/Liquibase/changelog/create-initial-tables.sql +++ b/DSL/Liquibase/changelog/create-initial-tables.sql @@ -18,4 +18,16 @@ INSERT INTO public."integration_status" (platform, is_connect, subscription_id, VALUES ('JIRA', FALSE, NULL, NULL), ('OUTLOOK', FALSE, NULL, NULL), - ('PINAL', FALSE, NULL, NULL); \ No newline at end of file + ('PINAL', FALSE, NULL, NULL); + +-- changeset kalsara Magamage:classifier-ddl-script-v1-changeset4 +CREATE TABLE public."inputs" ( + id int8 NOT NULL GENERATED BY DEFAULT AS IDENTITY, + input_id VARCHAR(50) DEFAULT NULL, + anonym_text VARCHAR(50) DEFAULT NULL, + platform platform, + corrected BOOLEAN NOT NULL DEFAULT FALSE, + predicted_labels TEXT[] DEFAULT NULL, + corrected_labels TEXT[] DEFAULT NULL, + CONSTRAINT inputs_pkey PRIMARY KEY (id) +); diff --git a/DSL/Resql/get-platform-input-row-data.sql b/DSL/Resql/get-platform-input-row-data.sql new file mode 100644 index 00000000..38becd3d --- /dev/null +++ b/DSL/Resql/get-platform-input-row-data.sql @@ -0,0 +1,3 @@ +SELECT corrected_labels +FROM inputs +WHERE input_id=:inputId AND platform=:platform::platform; diff --git a/DSL/Ruuter.private/DSL/GET/classifier/integration/outlook/token.yml b/DSL/Ruuter.private/DSL/GET/classifier/integration/outlook/token.yml index eb7e610f..66ee1e55 100644 --- a/DSL/Ruuter.private/DSL/GET/classifier/integration/outlook/token.yml +++ b/DSL/Ruuter.private/DSL/GET/classifier/integration/outlook/token.yml @@ -17,7 +17,7 @@ get_refresh_token: set_refresh_token: assign: - refresh_token: ${res.response.body.token} + refresh_token: "[#OUTLOOK_REFRESH_KEY]" #${res.response.body.token} next: check_refresh_token check_refresh_token: @@ -44,6 +44,7 @@ get_access_token: return_result: return: ${res.response.body} + next: end return_not_found: status: 404 diff --git a/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/accept.yml b/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/accept.yml index c4aa6fa3..901b55e0 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/accept.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/accept.yml @@ -17,12 +17,16 @@ declaration: - field: issue_info type: string description: "Body field 'issue_info'" + - field: event_type + type: string + description: "Body field 'event_type'" get_webhook_data: assign: headers: ${incoming.headers} payload: ${incoming.body} issue_info: ${incoming.body.issue} + event_type: ${incoming.body.webhookEvent} next: verify_jira_signature verify_jira_signature: @@ -40,15 +44,71 @@ verify_jira_signature: assign_verification: assign: - is_valid: ${valid_data.response.body.valid} + is_valid: true #${valid_data.response.body.valid} next: validate_url_signature validate_url_signature: switch: - - condition: ${is_valid === "true"} - next: get_jira_issue_info + - condition: ${is_valid === true} + next: check_event_type next: return_error_found +check_event_type: + switch: + - condition: ${event_type === 'jira:issue_updated'} + next: get_existing_labels + next: get_jira_issue_info + +get_existing_labels: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-platform-input-row-data" + body: + inputId: ${issue_info.key} + platform: 'JIRA' + result: res + next: check_input_response + +check_input_response: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: check_previous_labels + next: return_db_request_fail + +check_previous_labels: + switch: + - condition: ${res.response.body.length > 0} + next: assign_previous_labels + next: assign_empty + +assign_previous_labels: + assign: + previous_labels: ${res.response.body[0].correctedLabels} + next: validate_issue_labels + +assign_empty: + assign: + previous_labels: ${[]} + next: validate_issue_labels + +validate_issue_labels: + call: http.post + args: + url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/return_label_mismatch" + headers: + type: json + body: + newLabels: ${issue_info.fields.labels} + previousLabels: ${previous_labels} + result: label_response + next: check_label_mismatch + +check_label_mismatch: + switch: + - condition: ${label_response.response.body.isMismatch === 'true'} + next: get_jira_issue_info + next: end + get_jira_issue_info: call: http.post args: @@ -61,13 +121,15 @@ get_jira_issue_info: next: send_issue_data send_issue_data: - call: http.post + call: reflect.mock args: url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/return_jira_issue_info"# need correct url headers: type: json body: info: ${extract_info} + response: + statusCodeValue: 200 result: res check_response: @@ -81,6 +143,16 @@ return_ok: return: "Jira data send successfully" next: end +return_error_found: + status: 400 + return: "Error Found" + next: end + +return_db_request_fail: + status: 400 + return: "Fetch data for labels failed" + next: end + return_bad_request: status: 400 return: "Bad Request" diff --git a/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/toggle-subscription.yml b/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/toggle-subscription.yml index 27202de9..2a6530d8 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/toggle-subscription.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/toggle-subscription.yml @@ -46,6 +46,22 @@ subscribe_jira: body: enabled: true result: res + next: check_subscribe_response + +check_subscribe_response: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: set_subscription_data + next: return_result + +set_subscription_data: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/connect-platform" + body: + id: '[#JIRA_WEBHOOK_ID]' + platform: 'JIRA' + result: res next: return_result unsubscribe_jira: @@ -57,6 +73,21 @@ unsubscribe_jira: body: enabled: false result: res + next: check_unsubscribe_response + +check_unsubscribe_response: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: remove_subscription_data + next: return_result + +remove_subscription_data: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/disconnect-platform" + body: + platform: 'JIRA' + result: res next: return_result return_result: diff --git a/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/toggle-subscription.yml b/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/toggle-subscription.yml index 9c52ce4c..87c16645 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/toggle-subscription.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/toggle-subscription.yml @@ -66,13 +66,15 @@ subscribe_outlook: Authorization: ${'Bearer ' + access_token} body: changeType: "created" - notificationUrl: "ngrok/classifier/integration/outlook/accept" + notificationUrl: "https://6040-2402-4000-2180-8a3a-a885-a68-2705-d3e3.ngrok-free.app/outlook/callback" resource: "me/mailFolders('inbox')/messages" - expirationDateTime: "2024-06-28T21:10:45.9356913Z" + expirationDateTime: "2024-07-02T21:10:45.9356913Z" clientState: "state" result: res_subscribe next: check_subscribe_response +#classifier/integration/outlook/accept + check_subscribe_response: switch: - condition: ${200 <= res_subscribe.response.statusCodeValue && res_subscribe.response.statusCodeValue < 300} From 7ee98f89c1c594e232c7b93dcb4fb8ecfaf0093d Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 1 Jul 2024 13:18:50 +0530 Subject: [PATCH 044/582] Automaticly detecting language --- src/data_enrichment/data_enrichment.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/data_enrichment/data_enrichment.py b/src/data_enrichment/data_enrichment.py index 1f64d2fb..435f1edb 100644 --- a/src/data_enrichment/data_enrichment.py +++ b/src/data_enrichment/data_enrichment.py @@ -1,5 +1,6 @@ from translator import Translator from paraphraser import Paraphraser +from langdetect import detect from typing import List class DataEnrichment: @@ -7,13 +8,14 @@ def __init__(self): self.translator = Translator() self.paraphraser = Paraphraser() - def enrich_data(self, text: str, lang: str) -> List[str]: + def enrich_data(self, text: str) -> List[str]: supported_languages = ['en', 'et', 'ru', 'pl', 'fi'] + lang = detect(text) if lang not in supported_languages: print(f"Unsupported language: {lang}") return [] - + if lang == 'en': paraphrases = self.paraphraser.generate_paraphrases(text) else: From d5f7670137b0770ca03e9038e42be85cf37ee639 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 1 Jul 2024 13:20:31 +0530 Subject: [PATCH 045/582] updating test cases to match automatic language detection --- src/data_enrichment/test_data_enrichment.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/data_enrichment/test_data_enrichment.py b/src/data_enrichment/test_data_enrichment.py index a4afea67..f581e714 100644 --- a/src/data_enrichment/test_data_enrichment.py +++ b/src/data_enrichment/test_data_enrichment.py @@ -9,26 +9,23 @@ def setUp(self): def test_enrich_data_with_supported_language(self): original_sentence = "Hello, I hope this email finds you well. I am writing to inquire about the status of my application." - language = 'en' - enriched_data = self.enricher.enrich_data(original_sentence, language) + enriched_data = self.enricher.enrich_data(original_sentence) self.assertIsNotNone(enriched_data) for i, paraphrase in enumerate(enriched_data, 1): print(f"Paraphrase {i}: {paraphrase}") def test_enrich_data_with_unsupported_language(self): - original_sentence = "Hello, I hope this email finds you well. I am writing to inquire about the status of my application." - language = 'sl' + original_sentence = "Pozdravljeni, upam, da vas to e-pošto dobro najde. Pišem, da se pozanimam o stanju moje prijave." - enriched_data = self.enricher.enrich_data(original_sentence, language) + enriched_data = self.enricher.enrich_data(original_sentence) self.assertEqual(enriched_data, []) def test_enrich_data_with_estonian_sentence(self): original_sentence = "Tere, ma loodan, et see e-kiri leiab teid hästi. Ma kirjutan päringu staatuse minu taotluse. Ma esitasin kõik nõutavad dokumendid möödunud kuu, kuid ma pole saanud ühtegi värskendust sellest ajast. Kas saaksite palun mulle hetkeseisu kohta teavet anda?" - language = 'et' - enriched_data = self.enricher.enrich_data(original_sentence, language) + enriched_data = self.enricher.enrich_data(original_sentence) self.assertIsNotNone(enriched_data) for i, paraphrase in enumerate(enriched_data, 1): @@ -36,9 +33,8 @@ def test_enrich_data_with_estonian_sentence(self): def test_enrich_data_with_russian_sentence(self): original_sentence = "Привет, надеюсь, что это письмо находит вас в порядке. Я пишу с запросом о статусе моего заявления. Я подал все необходимые документы в прошлом месяце, но с тех пор не получил никаких обновлений. Не могли бы вы, пожалуйста, предоставить мне текущий статус?" - language = 'ru' - enriched_data = self.enricher.enrich_data(original_sentence, language) + enriched_data = self.enricher.enrich_data(original_sentence) self.assertIsNotNone(enriched_data) for i, paraphrase in enumerate(enriched_data, 1): @@ -46,9 +42,8 @@ def test_enrich_data_with_russian_sentence(self): def test_enrich_data_with_polish_sentence(self): original_sentence = "Cześć, mam nadzieję, że to e-mail znajduje cię w dobrym zdrowiu. Piszę z zapytaniem o status mojej aplikacji. Złożyłem wszystkie wymagane dokumenty w zeszłym miesiącu, ale od tego czasu nie otrzymałem żadnej aktualizacji. Czy moglibyście podać mi bieżący status?" - language = 'pl' - enriched_data = self.enricher.enrich_data(original_sentence, language) + enriched_data = self.enricher.enrich_data(original_sentence) self.assertIsNotNone(enriched_data) for i, paraphrase in enumerate(enriched_data, 1): @@ -56,9 +51,8 @@ def test_enrich_data_with_polish_sentence(self): def test_enrich_data_with_finnish_sentence(self): original_sentence = "Hei, toivottavasti tämä sähköposti tavoittaa teidät hyvin. Kirjoitan tiedustelun tilastani hakemukseni suhteen. Lähetin kaikki tarvittavat asiakirjat viime kuussa, mutta en ole sen jälkeen saanut päivityksiä. Voisitteko antaa minulle ajantasaisen tilanteen?" - language = 'fi' - enriched_data = self.enricher.enrich_data(original_sentence, language) + enriched_data = self.enricher.enrich_data(original_sentence) self.assertIsNotNone(enriched_data) for i, paraphrase in enumerate(enriched_data, 1): From e5a87863b924d4962047a3adcc23dc96d8cf8d5b Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 1 Jul 2024 13:23:52 +0530 Subject: [PATCH 046/582] making num_return_sequences configurable by user --- src/data_enrichment/paraphraser.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/data_enrichment/paraphraser.py b/src/data_enrichment/paraphraser.py index ffc042ec..5dc15bcc 100644 --- a/src/data_enrichment/paraphraser.py +++ b/src/data_enrichment/paraphraser.py @@ -10,7 +10,7 @@ def __init__(self, config_path: str = "config_files/paraphraser_config.json"): self.model_name = config["model_name"] self.num_beams = config["num_beams"] self.num_beam_groups = config["num_beam_groups"] - self.num_return_sequences = config["num_return_sequences"] + self.default_num_return_sequences = config["num_return_sequences"] self.repetition_penalty = config["repetition_penalty"] self.diversity_penalty = config["diversity_penalty"] self.no_repeat_ngram_size = config["no_repeat_ngram_size"] @@ -20,7 +20,10 @@ def __init__(self, config_path: str = "config_files/paraphraser_config.json"): self.tokenizer = AutoTokenizer.from_pretrained(self.model_name) self.model = AutoModelForSeq2SeqLM.from_pretrained(self.model_name) - def generate_paraphrases(self, question: str) -> List[str]: + def generate_paraphrases(self, question: str, num_return_sequences: int = None) -> List[str]: + if num_return_sequences is None or num_return_sequences <= 0: + num_return_sequences = self.default_num_return_sequences + input_ids = self.tokenizer( f'paraphrase: {question}', return_tensors="pt", padding="longest", @@ -30,7 +33,7 @@ def generate_paraphrases(self, question: str) -> List[str]: outputs = self.model.generate( input_ids, temperature=self.temperature, repetition_penalty=self.repetition_penalty, - num_return_sequences=self.num_return_sequences, no_repeat_ngram_size=self.no_repeat_ngram_size, + num_return_sequences=num_return_sequences, no_repeat_ngram_size=self.no_repeat_ngram_size, num_beams=self.num_beams, num_beam_groups=self.num_beam_groups, max_length=self.max_length, diversity_penalty=self.diversity_penalty ) From 8c0700d6c20e176f7abd52c663e40794a8114cdd Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 1 Jul 2024 13:27:36 +0530 Subject: [PATCH 047/582] updating data enrichment to have new configuration --- src/data_enrichment/data_enrichment.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/data_enrichment/data_enrichment.py b/src/data_enrichment/data_enrichment.py index 435f1edb..c9527c54 100644 --- a/src/data_enrichment/data_enrichment.py +++ b/src/data_enrichment/data_enrichment.py @@ -8,7 +8,7 @@ def __init__(self): self.translator = Translator() self.paraphraser = Paraphraser() - def enrich_data(self, text: str) -> List[str]: + def enrich_data(self, text: str, num_return_sequences: int = None) -> List[str]: supported_languages = ['en', 'et', 'ru', 'pl', 'fi'] lang = detect(text) @@ -17,10 +17,10 @@ def enrich_data(self, text: str) -> List[str]: return [] if lang == 'en': - paraphrases = self.paraphraser.generate_paraphrases(text) + paraphrases = self.paraphraser.generate_paraphrases(text, num_return_sequences) else: english_text = self.translator.translate(text, lang, 'en') - paraphrases = self.paraphraser.generate_paraphrases(english_text) + paraphrases = self.paraphraser.generate_paraphrases(english_text, num_return_sequences) translated_paraphrases = [] for paraphrase in paraphrases: translated_paraphrase = self.translator.translate(paraphrase, 'en', lang) From 9bb16e9e235637fbf34c18b0d966afa2982cb812 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 1 Jul 2024 13:34:33 +0530 Subject: [PATCH 048/582] requirenment.txt file --- src/{python_requirements.txt => data_enrichment/requirements.txt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{python_requirements.txt => data_enrichment/requirements.txt} (100%) diff --git a/src/python_requirements.txt b/src/data_enrichment/requirements.txt similarity index 100% rename from src/python_requirements.txt rename to src/data_enrichment/requirements.txt From 7b1b443b804a3e4ac14eb1c3151fdfc4f3d37d5e Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 1 Jul 2024 13:34:42 +0530 Subject: [PATCH 049/582] fastapi file --- src/data_enrichment/data_enrichment_api.py | 28 ++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/data_enrichment/data_enrichment_api.py diff --git a/src/data_enrichment/data_enrichment_api.py b/src/data_enrichment/data_enrichment_api.py new file mode 100644 index 00000000..a8310907 --- /dev/null +++ b/src/data_enrichment/data_enrichment_api.py @@ -0,0 +1,28 @@ +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel +from data_enrichment import DataEnrichment +from typing import List, Optional + +app = FastAPI() +data_enrichment = DataEnrichment() + +class ParaphraseRequest(BaseModel): + text: str + num_return_sequences: Optional[int] = None + +class ParaphraseResponse(BaseModel): + paraphrases: List[str] + +@app.post("/paraphrase", response_model=ParaphraseResponse) +def paraphrase(request: ParaphraseRequest): + try: + paraphrases = data_enrichment.enrich_data(request.text, request.num_return_sequences) + if not paraphrases: + raise HTTPException(status_code=400, detail="Unsupported language or other error") + return ParaphraseResponse(paraphrases=paraphrases) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8500) From c3e4dfd9178afbe9271bbc1909d28d19809adf44 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 1 Jul 2024 13:34:50 +0530 Subject: [PATCH 050/582] Docker file --- src/data_enrichment/Dockerfile | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/data_enrichment/Dockerfile diff --git a/src/data_enrichment/Dockerfile b/src/data_enrichment/Dockerfile new file mode 100644 index 00000000..84d7db2a --- /dev/null +++ b/src/data_enrichment/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.10-slim + +WORKDIR /app + +COPY requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 8500 + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8500"] From 2024a3eb8ab3ebd574e27dab1680a9787ee557fc Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 1 Jul 2024 13:47:11 +0530 Subject: [PATCH 051/582] requirenments update with fast api and uvicorn --- src/data_enrichment/requirements.txt | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/data_enrichment/requirements.txt b/src/data_enrichment/requirements.txt index 90ddc8d5..95087b4b 100644 --- a/src/data_enrichment/requirements.txt +++ b/src/data_enrichment/requirements.txt @@ -1,11 +1,22 @@ accelerate==0.31.0 +annotated-types==0.7.0 +anyio==4.4.0 certifi==2024.6.2 charset-normalizer==3.3.2 click==8.1.7 colorama==0.4.6 +dnspython==2.6.1 +email_validator==2.2.0 et-xmlfile==1.1.0 +exceptiongroup==1.2.1 +fastapi==0.111.0 +fastapi-cli==0.0.4 filelock==3.13.1 fsspec==2024.2.0 +h11==0.14.0 +httpcore==1.0.5 +httptools==0.6.1 +httpx==0.27.0 huggingface-hub==0.23.3 idna==3.7 install==1.3.5 @@ -13,28 +24,40 @@ intel-openmp==2021.4.0 Jinja2==3.1.3 joblib==1.4.2 langdetect==1.0.9 +markdown-it-py==3.0.0 MarkupSafe==2.1.5 +mdurl==0.1.2 mkl==2021.4.0 mpmath==1.3.0 networkx==3.2.1 numpy==1.26.3 openpyxl==3.1.3 +orjson==3.10.5 packaging==24.1 pandas==2.2.2 pillow==10.2.0 protobuf==5.27.1 psutil==5.9.8 +pydantic==2.7.4 +pydantic_core==2.18.4 +Pygments==2.18.0 python-dateutil==2.9.0.post0 +python-dotenv==1.0.1 +python-multipart==0.0.9 pytz==2024.1 PyYAML==6.0.1 regex==2024.5.15 requests==2.32.3 +rich==13.7.1 sacremoses==0.1.1 safetensors==0.4.3 scikit-learn==1.5.0 scipy==1.13.1 sentencepiece==0.2.0 +shellingham==1.5.4 six==1.16.0 +sniffio==1.3.1 +starlette==0.37.2 sympy==1.12 tbb==2021.11.0 threadpoolctl==3.5.0 @@ -44,6 +67,11 @@ torchaudio==2.3.1+cu121 torchvision==0.18.1+cu121 tqdm==4.66.4 transformers==4.41.2 +typer==0.12.3 typing_extensions==4.9.0 tzdata==2024.1 +ujson==5.10.0 urllib3==2.2.1 +uvicorn==0.30.1 +watchfiles==0.22.0 +websockets==12.0 From 3732c39df84fc030f5d28c4b10b5ce5b2eb5b956 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 1 Jul 2024 15:57:01 +0530 Subject: [PATCH 052/582] language manually send or automatic classification update --- src/data_enrichment/data_enrichment.py | 16 +++++++++++++--- src/data_enrichment/data_enrichment_api.py | 7 ++++++- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/data_enrichment/data_enrichment.py b/src/data_enrichment/data_enrichment.py index c9527c54..df61facc 100644 --- a/src/data_enrichment/data_enrichment.py +++ b/src/data_enrichment/data_enrichment.py @@ -1,16 +1,26 @@ from translator import Translator from paraphraser import Paraphraser from langdetect import detect -from typing import List +from typing import List, Optional class DataEnrichment: def __init__(self): self.translator = Translator() self.paraphraser = Paraphraser() - def enrich_data(self, text: str, num_return_sequences: int = None) -> List[str]: + def enrich_data(self, text: str, num_return_sequences: int = None, language_id: Optional[str] = None) -> List[str]: supported_languages = ['en', 'et', 'ru', 'pl', 'fi'] - lang = detect(text) + + if language_id: + if language_id not in supported_languages: + print(f"Language ID {language_id} is not supported. Trying to detect the language automatically.") + lang = detect(text) + else: + lang = language_id + else: + lang = detect(text) + + print(f"Detected/Used language: {lang}") if lang not in supported_languages: print(f"Unsupported language: {lang}") diff --git a/src/data_enrichment/data_enrichment_api.py b/src/data_enrichment/data_enrichment_api.py index a8310907..ea291dec 100644 --- a/src/data_enrichment/data_enrichment_api.py +++ b/src/data_enrichment/data_enrichment_api.py @@ -9,6 +9,7 @@ class ParaphraseRequest(BaseModel): text: str num_return_sequences: Optional[int] = None + language_id: Optional[str] = None class ParaphraseResponse(BaseModel): paraphrases: List[str] @@ -16,7 +17,11 @@ class ParaphraseResponse(BaseModel): @app.post("/paraphrase", response_model=ParaphraseResponse) def paraphrase(request: ParaphraseRequest): try: - paraphrases = data_enrichment.enrich_data(request.text, request.num_return_sequences) + paraphrases = data_enrichment.enrich_data( + request.text, + request.num_return_sequences, + request.language_id + ) if not paraphrases: raise HTTPException(status_code=400, detail="Unsupported language or other error") return ParaphraseResponse(paraphrases=paraphrases) From c25d690ba67810bcc4b57ed58bcb260fee4c191a Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Mon, 1 Jul 2024 23:36:34 +0530 Subject: [PATCH 053/582] finetunes in user management and integration --- .../DSL/POST/{ => auth}/login.yml | 0 GUI/.env.development | 9 - GUI/package-lock.json | 144 +++++---- GUI/package.json | 4 +- GUI/src/App.tsx | 2 +- .../FormElements/FormInput/FormInput.scss | 8 +- .../FormElements/FormInput/index.tsx | 8 +- GUI/src/components/Header/Header.scss | 10 + GUI/src/components/Header/index.tsx | 12 +- GUI/src/components/Layout/index.tsx | 6 +- .../MainNavigation/MainNavigation.scss | 129 ++++++++ GUI/src/components/MainNavigation/index.tsx | 288 ++++++------------ .../molecules/IntegrationCard/index.tsx | 132 +++----- GUI/src/config/rolesConfig.json | 10 +- GUI/src/config/users.json | 14 +- GUI/src/constants/menuIcons.tsx | 2 +- GUI/src/mocks/users.ts | 54 ---- GUI/src/pages/Integrations/index.tsx | 8 +- GUI/src/pages/UserManagement/index.tsx | 203 ++++++++---- GUI/src/utils/constants.ts | 1 + GUI/translations/en/common.json | 51 +--- docker-compose.yml | 35 +-- 22 files changed, 548 insertions(+), 582 deletions(-) rename DSL/Ruuter.public/DSL/POST/{ => auth}/login.yml (100%) create mode 100644 GUI/src/components/Header/Header.scss create mode 100644 GUI/src/components/MainNavigation/MainNavigation.scss delete mode 100644 GUI/src/mocks/users.ts diff --git a/DSL/Ruuter.public/DSL/POST/login.yml b/DSL/Ruuter.public/DSL/POST/auth/login.yml similarity index 100% rename from DSL/Ruuter.public/DSL/POST/login.yml rename to DSL/Ruuter.public/DSL/POST/auth/login.yml diff --git a/GUI/.env.development b/GUI/.env.development index a9bac609..13f489e1 100644 --- a/GUI/.env.development +++ b/GUI/.env.development @@ -1,15 +1,6 @@ REACT_APP_RUUTER_API_URL=http://localhost:8086 REACT_APP_RUUTER_PRIVATE_API_URL=http://localhost:8088 -REACT_APP_BUEROKRATT_CHATBOT_URL=http://buerokratt-chat:8086 -REACT_APP_MENU_URL=https://admin.dev.buerokratt.ee -REACT_APP_MENU_PATH=/chat/menu.json REACT_APP_CUSTOMER_SERVICE_LOGIN=http://localhost:3004/et/dev-auth -REACT_APP_CONVERSATIONS_BASE_URL=http://localhost:8080/chat -REACT_APP_TRAINING_BASE_URL=http://localhost:8080/training -REACT_APP_ANALYTICS_BASE_URL=http://localhost:8080/analytics -REACT_APP_SERVICES_BASE_URL=http://localhost:8080/services -REACT_APP_SETTINGS_BASE_URL=http://localhost:8080/settings -REACT_APP_MONITORING_BASE_URL=http://localhost:8080/monitoring REACT_APP_SERVICE_ID=conversations,settings,monitoring REACT_APP_NOTIFICATION_NODE_URL=http://localhost:4040 REACT_APP_CSP=upgrade-insecure-requests; default-src 'self'; font-src 'self' data:; img-src 'self' data:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self' http://localhost:8086 http://localhost:8088 http://localhost:8085 http://localhost:4040 http://localhost:3002/menu.json; diff --git a/GUI/package-lock.json b/GUI/package-lock.json index c29181eb..c11a098a 100644 --- a/GUI/package-lock.json +++ b/GUI/package-lock.json @@ -8,8 +8,6 @@ "name": "byk-training-module-gui", "version": "0.0.0", "dependencies": { - "@buerokratt-ria/header": "^0.1.6", - "@buerokratt-ria/menu": "^0.1.11", "@buerokratt-ria/styles": "^0.0.1", "@fontsource/roboto": "^4.5.8", "@formkit/auto-animate": "^1.0.0-beta.5", @@ -31,6 +29,7 @@ "date-fns": "^2.29.3", "downshift": "^7.0.5", "esbuild": "^0.19.5", + "formik": "^2.4.6", "framer-motion": "^8.5.5", "howler": "^2.2.4", "i18next": "^22.4.5", @@ -59,6 +58,7 @@ "timeago.js": "^4.0.2", "usehooks-ts": "^2.9.1", "uuid": "^9.0.0", + "yup": "^1.4.0", "zustand": "^4.4.4" }, "devDependencies": { @@ -2143,77 +2143,6 @@ "node": ">=6.9.0" } }, - "node_modules/@buerokratt-ria/header": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@buerokratt-ria/header/-/header-0.1.6.tgz", - "integrity": "sha512-sPynHp0LQvBdjqNma6KmAGHzfP3qnpFl4WdZDpHBRJS/f09EPlEvSfV6N6D693i9M7oXD9WhykIvthcugoeCbQ==", - "dependencies": { - "@buerokratt-ria/styles": "^0.0.1", - "@types/react": "^18.2.21", - "react": "^18.2.0" - }, - "peerDependencies": { - "@fontsource/roboto": "^4.5.8", - "@formkit/auto-animate": "^0.7.0", - "@radix-ui/react-accessible-icon": "^1.0.3", - "@radix-ui/react-dialog": "^1.0.4", - "@radix-ui/react-switch": "^1.0.3", - "@radix-ui/react-toast": "^1.1.4", - "@tanstack/react-query": "^4.32.1", - "axios": "^1.4.0", - "clsx": "^1.2.1", - "i18next": "^23.2.3", - "i18next-browser-languagedetector": "^7.1.0", - "path": "^0.12.7", - "react": "^18.2.0", - "react-cookie": "^4.1.1", - "react-dom": "^18.2.0", - "react-hook-form": "^7.45.4", - "react-i18next": "^12.1.1", - "react-icons": "^4.10.1", - "react-idle-timer": "^5.7.2", - "react-router-dom": "^6.14.2", - "rxjs": "^7.8.1", - "tslib": "^2.3.0", - "vite-plugin-dts": "^3.5.2", - "vite-plugin-svgr": "^3.2.0", - "zustand": "^4.4.0" - } - }, - "node_modules/@buerokratt-ria/menu": { - "version": "0.1.16", - "resolved": "https://registry.npmjs.org/@buerokratt-ria/menu/-/menu-0.1.16.tgz", - "integrity": "sha512-k6G9I1Y7y98E7Re8QI+vm5EjYEVpGwTP9n8aStt/ScdiWmwrw12hFd/yRLj3kO8phMvWUPUfBPuE3UajlfoesA==", - "dependencies": { - "@buerokratt-ria/styles": "^0.0.1", - "@types/react": "^18.2.21", - "react": "^18.2.0" - }, - "peerDependencies": { - "@radix-ui/react-accessible-icon": "^1.0.3", - "@radix-ui/react-dialog": "^1.0.4", - "@radix-ui/react-switch": "^1.0.3", - "@radix-ui/react-toast": "^1.1.4", - "@tanstack/react-query": "^4.32.1", - "clsx": "^1.2.1", - "i18next": "^23.2.3", - "i18next-browser-languagedetector": "^7.1.0", - "path": "^0.12.7", - "react": "^18.2.0", - "react-cookie": "^4.1.1", - "react-dom": "^18.2.0", - "react-hook-form": "^7.45.4", - "react-i18next": "^12.1.1", - "react-icons": "^4.10.1", - "react-idle-timer": "^5.7.2", - "react-router-dom": "^6.14.2", - "rxjs": "^7.8.1", - "tslib": "^2.3.0", - "vite-plugin-dts": "^3.5.2", - "vite-plugin-svgr": "^3.2.0", - "zustand": "^4.4.0" - } - }, "node_modules/@buerokratt-ria/styles": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/@buerokratt-ria/styles/-/styles-0.0.1.tgz", @@ -8440,6 +8369,14 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/deepmerge": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", + "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/defaults": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", @@ -9657,6 +9594,35 @@ "node": ">= 6" } }, + "node_modules/formik": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/formik/-/formik-2.4.6.tgz", + "integrity": "sha512-A+2EI7U7aG296q2TLGvNapDNTZp1khVt5Vk0Q/fyfSROss0V/V6+txt2aJnwEos44IxTCW/LYAi/zgWzlevj+g==", + "funding": [ + { + "type": "individual", + "url": "https://opencollective.com/formik" + } + ], + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.1", + "deepmerge": "^2.1.1", + "hoist-non-react-statics": "^3.3.0", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "react-fast-compare": "^2.0.1", + "tiny-warning": "^1.0.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/formik/node_modules/react-fast-compare": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz", + "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==" + }, "node_modules/framer-motion": { "version": "8.5.5", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-8.5.5.tgz", @@ -12313,6 +12279,11 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/property-expr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", + "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==" + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -13509,6 +13480,16 @@ "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", "integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==" }, + "node_modules/tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==" + }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, "node_modules/tinycolor2": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", @@ -13545,6 +13526,11 @@ "node": ">=8.0" } }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -13637,7 +13623,6 @@ "version": "2.19.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "dev": true, "engines": { "node": ">=12.20" }, @@ -14758,6 +14743,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yup": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.4.0.tgz", + "integrity": "sha512-wPbgkJRCqIf+OHyiTBQoJiP5PFuAXaWiJK6AmYkzQAh5/c2K9hzSApBZG5wV9KoKSePF7sAxmNSvh/13YHkFDg==", + "dependencies": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + } + }, "node_modules/zustand": { "version": "4.5.2", "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.2.tgz", diff --git a/GUI/package.json b/GUI/package.json index 99897b07..69b54525 100644 --- a/GUI/package.json +++ b/GUI/package.json @@ -11,8 +11,6 @@ "prettier": "prettier --write \"{,!(node_modules)/**/}*.{ts,tsx,js,json,css,less,scss}\"" }, "dependencies": { - "@buerokratt-ria/header": "^0.1.6", - "@buerokratt-ria/menu": "^0.1.11", "@buerokratt-ria/styles": "^0.0.1", "@fontsource/roboto": "^4.5.8", "@formkit/auto-animate": "^1.0.0-beta.5", @@ -34,6 +32,7 @@ "date-fns": "^2.29.3", "downshift": "^7.0.5", "esbuild": "^0.19.5", + "formik": "^2.4.6", "framer-motion": "^8.5.5", "howler": "^2.2.4", "i18next": "^22.4.5", @@ -62,6 +61,7 @@ "timeago.js": "^4.0.2", "usehooks-ts": "^2.9.1", "uuid": "^9.0.0", + "yup": "^1.4.0", "zustand": "^4.4.4" }, "devDependencies": { diff --git a/GUI/src/App.tsx b/GUI/src/App.tsx index 149a0bbd..02b8dd31 100644 --- a/GUI/src/App.tsx +++ b/GUI/src/App.tsx @@ -52,7 +52,7 @@ const App: FC = () => { }> } /> } /> - } /> + } /> } /> diff --git a/GUI/src/components/FormElements/FormInput/FormInput.scss b/GUI/src/components/FormElements/FormInput/FormInput.scss index 1aab26f6..461f968f 100644 --- a/GUI/src/components/FormElements/FormInput/FormInput.scss +++ b/GUI/src/components/FormElements/FormInput/FormInput.scss @@ -20,7 +20,7 @@ flex: 1; display: flex; flex-direction: column; - gap: 7px; + gap: 0px; position: relative; .icon { @@ -30,6 +30,12 @@ } } + &__inline_error { + color: get-color(jasper-10); + font-size: 12px; + + } + &__error { width: 100%; margin-right: 6px; diff --git a/GUI/src/components/FormElements/FormInput/index.tsx b/GUI/src/components/FormElements/FormInput/index.tsx index dfa2dd04..8933b9a6 100644 --- a/GUI/src/components/FormElements/FormInput/index.tsx +++ b/GUI/src/components/FormElements/FormInput/index.tsx @@ -8,11 +8,12 @@ type InputProps = PropsWithChildren> & { name: string; hideLabel?: boolean; maxLength?: number; + error?: string; }; -const FieldInput = forwardRef( +const FormInput = forwardRef( ( - { label, name, disabled, hideLabel, maxLength, children, ...rest }, + { label, name, disabled, hideLabel, maxLength, error, children, ...rest }, ref ) => { const id = useId(); @@ -36,6 +37,7 @@ const FieldInput = forwardRef( aria-label={hideLabel ? label : undefined} {...rest} /> + {error &&

    {error}

    } {children}
    @@ -43,4 +45,4 @@ const FieldInput = forwardRef( } ); -export default FieldInput; +export default FormInput; diff --git a/GUI/src/components/Header/Header.scss b/GUI/src/components/Header/Header.scss new file mode 100644 index 00000000..542c06f6 --- /dev/null +++ b/GUI/src/components/Header/Header.scss @@ -0,0 +1,10 @@ +@import '@buerokratt-ria/styles/styles/tools/spacing'; +@import '@buerokratt-ria/styles/styles/tools/color'; + +.header { + height: 100px; + padding: 24px 24px 24px 42px; + box-shadow: 0 0 2px rgba(0, 0, 0, 0.14), 0 2px 2px rgba(0, 0, 0, 0.12), 0 1px 3px rgba(0, 0, 0, 0.2); + background-color: get-color(white); + z-index: 99; +} diff --git a/GUI/src/components/Header/index.tsx b/GUI/src/components/Header/index.tsx index 7298e8e9..f636bea9 100644 --- a/GUI/src/components/Header/index.tsx +++ b/GUI/src/components/Header/index.tsx @@ -25,7 +25,6 @@ import apiDev from 'services/api-dev'; import { interval } from 'rxjs'; import { AUTHORITY } from 'types/authorities'; import { useCookies } from 'react-cookie'; -import { useDing } from 'hooks/useAudio'; import './Header.scss'; type CustomerSupportActivity = { @@ -61,7 +60,6 @@ const Header: FC = () => { const [csaStatus, setCsaStatus] = useState<'idle' | 'offline' | 'online'>( 'online' ); - const [ding] = useDing(); const chatCsaActive = useStore((state) => state.chatCsaActive); const [userProfileSettings, setUserProfileSettings] = useState({ @@ -88,8 +86,7 @@ const Header: FC = () => { const currentDate = new Date(Date.now()); if (expirationDate < currentDate) { localStorage.removeItem('exp'); - window.location.href = - import.meta.env.REACT_APP_CUSTOMER_SERVICE_LOGIN; + // window.location.href =import.meta.env.REACT_APP_CUSTOMER_SERVICE_LOGIN; } } }, 2000); @@ -135,9 +132,7 @@ const Header: FC = () => { return; } - if (userProfileSettings.newChatSoundNotifications) { - ding?.play(); - } + if (userProfileSettings.newChatEmailNotifications) { // To be done: send email notification } @@ -166,9 +161,6 @@ const Header: FC = () => { return; } - if (userProfileSettings.forwardedChatSoundNotifications) { - ding?.play(); - } if (userProfileSettings.forwardedChatEmailNotifications) { // To be done: send email notification } diff --git a/GUI/src/components/Layout/index.tsx b/GUI/src/components/Layout/index.tsx index 094b6e2e..0857601e 100644 --- a/GUI/src/components/Layout/index.tsx +++ b/GUI/src/components/Layout/index.tsx @@ -1,10 +1,10 @@ import { FC } from 'react'; import { Outlet } from 'react-router-dom'; import useStore from 'store'; -import { MainNavigation } from '@buerokratt-ria/menu'; -import { Header } from '@buerokratt-ria/header'; import './Layout.scss'; import { useToast } from '../../hooks/useToast'; +import Header from 'components/Header'; +import MainNavigation from 'components/MainNavigation'; const Layout: FC = () => { return ( @@ -12,7 +12,7 @@ const Layout: FC = () => {
    - {/*
    */} +
    diff --git a/GUI/src/components/MainNavigation/MainNavigation.scss b/GUI/src/components/MainNavigation/MainNavigation.scss new file mode 100644 index 00000000..23bc2176 --- /dev/null +++ b/GUI/src/components/MainNavigation/MainNavigation.scss @@ -0,0 +1,129 @@ +@import '@buerokratt-ria/styles/styles/tools/spacing'; +@import '@buerokratt-ria/styles/styles/tools/color'; +@import '@buerokratt-ria/styles/styles/settings/variables/typography'; + +.nav { + $self: &; + width: 208px; + background-color: get-color(sapphire-blue-10); + overflow: auto; + scrollbar-width: none; + transition: width .1s ease-out; + + &::-webkit-scrollbar { + display: none; + } + + li, a, .nav__toggle, .nav__menu-toggle { + font-size: 14px; + line-height: 1.5; + } + + &__menu-toggle { + display: flex; + align-items: center; + + &:hover { + background-color: get-color(sapphire-blue-8); + } + + &:active { + background-color: get-color(sapphire-blue-7); + } + } + + a, .nav__toggle { + width: 100%; + display: flex; + align-items: center; + gap: get-spacing(paldiski); + color: get-color(black-coral-0); + padding: 14px 8px 14px 32px; + box-shadow: inset 0 -1px 0 get-color(sapphire-blue-14); + + span:not(.icon) { + flex: 1; + display: block; + } + + &:hover { + background-color: get-color(sapphire-blue-8); + } + + &:active { + background-color: get-color(sapphire-blue-7); + } + + &.active { + font-weight: 700; + } + } + + &__toggle { + &[aria-expanded=true] { + font-weight: 700; + + .icon { + transform: rotate(180deg); + } + + + ul { + display: block; + } + } + + &.nav__toggle--icon { + padding-left: 8px; + + .icon:first-child { + transform: none; + } + } + } + + &__toggle-icon { + margin-left: auto; + } + + &__menu-toggle { + display: flex; + align-items: center; + gap: get-spacing(paldiski); + width: 100%; + color: get-color(white); + padding: 14px 8px; + box-shadow: inset 0 -1px 0 get-color(sapphire-blue-14); + } + + &__submenu { + display: none; + + a, .nav__toggle { + background-color: get-color(sapphire-blue-14); + box-shadow: inset 0 -1px 0 get-color(sapphire-blue-17); + } + + #{$self} { + &__submenu { + a { + background-color: get-color(sapphire-blue-17); + box-shadow: inset 0 -1px 0 get-color(black); + padding: 14px 48px 14px 40px; + } + } + } + } +} + +.collapsed { + .nav__submenu { + visibility: hidden; + height: 0; + } + + button[aria-expanded=true] { + .icon { + transform: rotate(0deg); + } + } +} diff --git a/GUI/src/components/MainNavigation/index.tsx b/GUI/src/components/MainNavigation/index.tsx index a067baa1..5e59a7d8 100644 --- a/GUI/src/components/MainNavigation/index.tsx +++ b/GUI/src/components/MainNavigation/index.tsx @@ -1,13 +1,13 @@ -import { FC, MouseEvent, useState } from 'react'; +import { FC, MouseEvent, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { NavLink, useLocation } from 'react-router-dom'; -import { MdClose, MdKeyboardArrowDown } from 'react-icons/md'; +import { MdApps, MdClass, MdClose, MdDashboard, MdDataset, MdKeyboardArrowDown, MdOutlineForum, MdPeople, MdSettings, MdTextFormat } from 'react-icons/md'; import { useQuery } from '@tanstack/react-query'; import clsx from 'clsx'; import { Icon } from 'components'; import type { MenuItem } from 'types/mainNavigation'; import { menuIcons } from 'constants/menuIcons'; -// import './MainNavigation.scss'; +import './MainNavigation.scss'; const MainNavigation: FC = () => { const { t } = useTranslation(); @@ -15,206 +15,113 @@ const MainNavigation: FC = () => { const items = [ { - id: 'conversations', - label: t('menu.conversations'), - path: '/chat', - children: [ - { - label: t('menu.unanswered'), - path: '/unanswered', - }, - { - label: t('menu.active'), - path: '/active', - }, - { - label: t('menu.history'), - path: '/history', - }, - ], + id: 'userManagement', + label: t('menu.userManagement'), + path: '/user-management', + icon: + }, + { + id: 'integration', + label: t('menu.integration'), + path: 'integration', + icon: + }, { - id: 'training', - label: t('menu.training'), + id: 'datasets', + label: t('menu.datasets'), path: '#', + icon: , children: [ { - label: t('menu.training'), - path: '#', - children: [ - { - label: t('menu.themes'), - path: '#', - }, - { - label: t('menu.answers'), - path: '#', - }, - { - label: t('menu.userStories'), - path: '#', - }, - { - label: t('menu.configuration'), - path: '#', - }, - { - label: t('menu.forms'), - path: '#', - }, - { - label: t('menu.slots'), - path: '#', - }, - ], + label: t('menu.datasetGroups'), + path: 'dataset-groups', + icon: }, { - label: t('menu.historicalConversations'), - path: '#', - children: [ - { - label: t('menu.history'), - path: '#', - }, - { - label: t('menu.appeals'), - path: '#', - }, - ], - }, - { - label: t('menu.modelBankAndAnalytics'), - path: '#', - children: [ - { - label: t('menu.overviewOfTopics'), - path: '#', - }, - { - label: t('menu.comparisonOfModels'), - path: '#', - }, - { - label: t('menu.testTracks'), - path: '#', - }, - ], - }, - { - label: t('menu.trainNewModel'), - path: '#', - }, + label: t('menu.versions'), + path: 'versions', + icon: + } ], }, { - id: 'analytics', - label: t('menu.analytics'), - path: '/analytics', - children: [ - { - label: t('menu.overview'), - path: '#', - }, - { - label: t('menu.chats'), - path: '#', - }, - { - label: t('menu.burokratt'), - path: '#', - }, - { - label: t('menu.feedback'), - path: '#', - }, - { - label: t('menu.advisors'), - path: '#', - }, - { - label: t('menu.reports'), - path: '#', - }, - ], + id: 'dataModels', + label: t('menu.dataModels'), + path: '/data-models', + icon: + }, { - id: 'settings', - label: t('menu.administration'), - path: '/settings', - children: [ - { - label: t('menu.users'), - path: '/settings/users', - }, - { - label: t('menu.chatbot'), - path: '/settings/chatbot', - children: [ - { - label: t('menu.settings'), - path: '/settings/chatbot/settings', - }, - { - label: t('menu.welcomeMessage'), - path: '/settings/chatbot/welcome-message', - }, - { - label: t('menu.appearanceAndBehavior'), - path: '/settings/chatbot/appearance', - }, - { - label: t('menu.emergencyNotices'), - path: '/settings/chatbot/emergency-notices', - }, - ], - }, - { - label: t('menu.officeOpeningHours'), - path: '/settings/working-time', - }, - { - label: t('menu.sessionLength'), - path: '/settings/session-length', - }, - ], + id: 'classes', + label: t('menu.classes'), + path: '/classes', + icon: + }, { - id: 'monitoring', - label: t('menu.monitoring'), - path: '/monitoring', - children: [ - { - label: t('menu.workingHours'), - path: '/monitoring/uptime', - }, - ], + id: 'stopWords', + label: t('menu.stopWords'), + path: '/stop-words', + icon: + }, - ]; + { + id: 'incomingTexts', + label: t('menu.incomingTexts'), + path: '/incoming-texts', + icon: - useQuery({ - queryKey: ['/accounts/user-role', 'prod'], - onSuccess: (res: any) => { - const filteredItems = - items.filter((item) => { - const role = res.data.get_user[0].authorities[0]; - switch (role) { - case 'ROLE_ADMINISTRATOR': - return item.id; - case 'ROLE_SERVICE_MANAGER': - return item.id != 'settings' && item.id != 'training'; - case 'ROLE_CUSTOMER_SUPPORT_AGENT': - return item.id != 'settings' && item.id != 'analytics'; - case 'ROLE_CHATBOT_TRAINER': - return item.id != 'settings' && item.id != 'conversations'; - case 'ROLE_ANALYST': - return item.id == 'analytics' || item.id == 'monitoring'; - case 'ROLE_UNAUTHENTICATED': - return; - } - }) ?? []; - setMenuItems(filteredItems); }, - }); + ]; + + useEffect(()=>{ + const filteredItems = + items.filter((item) => { + const role = "ROLE_ADMINISTRATOR"; + switch (role) { + case 'ROLE_ADMINISTRATOR': + return item.id; + case 'ROLE_SERVICE_MANAGER': + return item.id != 'settings' && item.id != 'training'; + case 'ROLE_CUSTOMER_SUPPORT_AGENT': + return item.id != 'settings' && item.id != 'analytics'; + case 'ROLE_CHATBOT_TRAINER': + return item.id != 'settings' && item.id != 'conversations'; + case 'ROLE_ANALYST': + return item.id == 'analytics' || item.id == 'monitoring'; + case 'ROLE_UNAUTHENTICATED': + return; + } + }) ?? []; + setMenuItems(filteredItems); + + },[]) + + // useQuery({ + // queryKey: ['/accounts/user-role', 'prod'], + // onSuccess: (res: any) => { + // const filteredItems = + // items.filter((item) => { + // const role = res.data.get_user[0].authorities[0]; + // switch (role) { + // case 'ROLE_ADMINISTRATOR': + // return item.id; + // case 'ROLE_SERVICE_MANAGER': + // return item.id != 'settings' && item.id != 'training'; + // case 'ROLE_CUSTOMER_SUPPORT_AGENT': + // return item.id != 'settings' && item.id != 'analytics'; + // case 'ROLE_CHATBOT_TRAINER': + // return item.id != 'settings' && item.id != 'conversations'; + // case 'ROLE_ANALYST': + // return item.id == 'analytics' || item.id == 'monitoring'; + // case 'ROLE_UNAUTHENTICATED': + // return; + // } + // }) ?? []; + // setMenuItems(filteredItems); + // }, + // }); const location = useLocation(); const [navCollapsed, setNavCollapsed] = useState(false); @@ -244,11 +151,10 @@ const MainNavigation: FC = () => { } onClick={handleNavToggle} > - {menuItem.id && ( + {/* {menuItem.id && ( */} icon.id === menuItem.id)?.icon} + icon={menuItem?.icon} /> - )} {menuItem.label} } /> @@ -257,7 +163,9 @@ const MainNavigation: FC = () => { ) : ( - {menuItem.label} + {menuItem.label} )} )); diff --git a/GUI/src/components/molecules/IntegrationCard/index.tsx b/GUI/src/components/molecules/IntegrationCard/index.tsx index 166525a8..f4c96023 100644 --- a/GUI/src/components/molecules/IntegrationCard/index.tsx +++ b/GUI/src/components/molecules/IntegrationCard/index.tsx @@ -9,7 +9,7 @@ type IntegrationCardProps = { channelDescription?: string; user?: string; isActive?: boolean; - status?: string; + connectedStatus?: { platform: string, status: string }[]; }; const IntegrationCard: FC> = ({ @@ -18,88 +18,59 @@ const IntegrationCard: FC> = ({ channelDescription, user, isActive, - status, + connectedStatus, }) => { const { t } = useTranslation(); const [isModalOpen, setIsModalOpen] = useState(false); + const [isChecked, setIsChecked] = useState(isActive); const [modalType, setModalType] = useState('JIRA_INTEGRATION'); + const renderStatusIndicators = () => { + return connectedStatus?.map((status, index) => ( + + {connectedStatus?.length>1 ? `${status.status} - ${status.platform}`:`${status.status}`} + + )); + }; + + const onSelect=()=>{ + if(isChecked){ + setModalType("DISCONNECT"); + }else{ + setIsChecked(true) + setModalType("SUCCESS"); + + } + setIsModalOpen(true) + + } + return ( <> - - - } + isFullWidth={true} > - <> -
    -
    {logo}
    -
    -

    {channel}

    -

    {channelDescription}

    - -
    -
    +
    +
    {logo}
    +
    +

    {channel}

    +

    {channelDescription}

    +
    +
    - +
    - - connected - Outlook - - - Disconnected - Pinal - + {renderStatusIndicators()}
    -
    -
    - -
    - +
    + +
    - {modalType === 'JIRA_INTEGRATION' && ( - setIsModalOpen(false)} - isOpen={isModalOpen} - title={'Integration with Jira'} - footer={ - <> - <> - - - - - } - > -
    -
    -
    - -
    -
    -
    -
    - )} + {modalType === 'SUCCESS' && ( setIsModalOpen(false)} @@ -107,8 +78,7 @@ const IntegrationCard: FC> = ({ title={'Integration Successful'} >
    - You have successfully connected with Jira! Your integration is now - complete, and you can start working with Jira seamlessly. + You have successfully connected with {channel}! Your integration is now complete, and you can start working with Jira seamlessly.
    )} @@ -119,36 +89,28 @@ const IntegrationCard: FC> = ({ title={'Integration Error'} >
    - Failed to connect with Jira. Please check your settings and try again. If the problem persists, contact support for assistance. + Failed to connect with {channel}. Please check your settings and try again. If the problem persists, contact support for assistance.
    )} - {modalType === 'DISCONNECT' && ( + {modalType === 'DISCONNECT' && ( setIsModalOpen(false)} isOpen={isModalOpen} title={'Are you sure?'} footer={ <> - <> - - - + + } >
    - Are you sure you want to disconnect the Jira integration? This action cannot be undone and may affect your workflow and linked issues. + Are you sure you want to disconnect the {channel} integration? This action cannot be undone and may affect your workflow and linked issues.
    )} diff --git a/GUI/src/config/rolesConfig.json b/GUI/src/config/rolesConfig.json index 35ece24c..02b429cd 100644 --- a/GUI/src/config/rolesConfig.json +++ b/GUI/src/config/rolesConfig.json @@ -1,10 +1,4 @@ [ - { "label": "ROLE_SERVICE_MANAGER", "value": "ROLE_SERVICE_MANAGER" }, - { - "label": "ROLE_CUSTOMER_SUPPORT_AGENT", - "value": "ROLE_CUSTOMER_SUPPORT_AGENT" - }, - { "label": "ROLE_CHATBOT_TRAINER", "value": "ROLE_CHATBOT_TRAINER" }, - { "label": "ROLE_ANALYST", "value": "ROLE_ANALYST" }, - { "label": "ROLE_ADMINISTRATOR", "value": "ROLE_ADMINISTRATOR" } + { "label": "ROLE_ADMINISTRATOR", "value": "ROLE_ADMINISTRATOR" }, + { "label": "ROLE_MODEL_TRAINER", "value": "ROLE_MODEL_TRAINER" } ] diff --git a/GUI/src/config/users.json b/GUI/src/config/users.json index 301e82fd..615079a7 100644 --- a/GUI/src/config/users.json +++ b/GUI/src/config/users.json @@ -22,11 +22,8 @@ "csaTitle": "tester", "csaEmail": "jaanus@clarifiedsecurity.com", "authorities": [ - "ROLE_SERVICE_MANAGER", - "ROLE_CUSTOMER_SUPPORT_AGENT", - "ROLE_CHATBOT_TRAINER", - "ROLE_ANALYST", - "ROLE_ADMINISTRATOR" + "ROLE_ADMINISTRATOR", + "ROLE_MODEL_TRAINER" ], "customerSupportStatus": "offline", "totalPages": 1 @@ -40,7 +37,7 @@ "csaTitle": "kolmas", "csaEmail": "kolmas@admin.ee", "authorities": [ - "ROLE_ADMINISTRATOR" + "ROLE_MODEL_TRAINER" ], "customerSupportStatus": "offline", "totalPages": 1 @@ -68,7 +65,7 @@ "csaTitle": "Dr", "csaEmail": "nipi@tiri.ee", "authorities": [ - "ROLE_CUSTOMER_SUPPORT_AGENT" + "ROLE_ADMINISTRATOR" ], "customerSupportStatus": "offline", "totalPages": 1 @@ -96,8 +93,7 @@ "csaTitle": "Mister", "csaEmail": "valter.aro@ria.ee", "authorities": [ - "ROLE_ANALYST", - "ROLE_CHATBOT_TRAINER" + "ROLE_ADMINISTRATOR" ], "customerSupportStatus": "offline", "totalPages": 1 diff --git a/GUI/src/constants/menuIcons.tsx b/GUI/src/constants/menuIcons.tsx index 06ac2b01..a53fc7c9 100644 --- a/GUI/src/constants/menuIcons.tsx +++ b/GUI/src/constants/menuIcons.tsx @@ -2,7 +2,7 @@ import { MdOutlineForum, MdOutlineAdb, MdOutlineEqualizer, MdSettings, MdOutline export const menuIcons = [ { - id: 'conversations', + id: 'userManagement', icon: , }, { diff --git a/GUI/src/mocks/users.ts b/GUI/src/mocks/users.ts deleted file mode 100644 index 997a6cf1..00000000 --- a/GUI/src/mocks/users.ts +++ /dev/null @@ -1,54 +0,0 @@ -export const usersData = [ - { - login: 'Admin', - firstName: null, - lastName: null, - idCode: 'EE60001019906', - displayName: 'Admin', - authorities: [ - 'ROLE_ADMINISTRATOR', - 'ROLE_SERVICE_MANAGER', - 'ROLE_CUSTOMER_SUPPORT_AGENT', - 'ROLE_CHATBOT_TRAINER', - 'ROLE_ANALYST', - ], - }, - { - login: 'Anne Clowd', - firstName: null, - lastName: null, - idCode: 'EE49002124277', - displayName: 'Anne Clowd', - authorities: [ - 'ROLE_ADMINISTRATOR', - 'ROLE_SERVICE_MANAGER', - 'ROLE_CUSTOMER_SUPPORT_AGENT', - 'ROLE_CHATBOT_TRAINER', - 'ROLE_ANALYST', - ], - }, - { - login: 'Viva Windler', - firstName: null, - lastName: null, - idCode: 'EE38001085718', - displayName: 'Viva Windler', - authorities: [ - 'ROLE_ADMINISTRATOR', - 'ROLE_SERVICE_MANAGER', - 'ROLE_CUSTOMER_SUPPORT_AGENT', - 'ROLE_CHATBOT_TRAINER', - 'ROLE_ANALYST', - ], - }, - { - login: 'Janina', - firstName: null, - lastName: null, - idCode: 'EE49209110848', - displayName: 'Janina', - authorities: [ - 'ROLE_ADMINISTRATOR', - ], - }, -]; diff --git a/GUI/src/pages/Integrations/index.tsx b/GUI/src/pages/Integrations/index.tsx index 1a263197..a2fe4620 100644 --- a/GUI/src/pages/Integrations/index.tsx +++ b/GUI/src/pages/Integrations/index.tsx @@ -21,8 +21,8 @@ const Integrations: FC = () => { channel={"Jira"} channelDescription={"Atlassian vea jälgimise ja projektijuhtimise tarkvara"} user={"Rickey Walker - Admin"} - isActive={true} - status={"Connected"} + isActive={false} + connectedStatus={[{platform:"Jira", status:"Connected"}]} /> } @@ -30,7 +30,7 @@ const Integrations: FC = () => { channelDescription={"Atlassian vea jälgimise ja projektijuhtimise tarkvara"} user={"Rickey Walker - Admin"} isActive={true} - status={"Connected"} + connectedStatus={[{platform:"Outlook", status:"Connected"}]} /> } @@ -38,7 +38,7 @@ const Integrations: FC = () => { channelDescription={"Atlassian vea jälgimise ja projektijuhtimise tarkvara"} user={"Rickey Walker - Admin"} isActive={true} - status={"Connected"} + connectedStatus={[{platform:"Outlook", status:"Connected"}, {platform:"Pinal", status:"Disconnected"}]} />
    diff --git a/GUI/src/pages/UserManagement/index.tsx b/GUI/src/pages/UserManagement/index.tsx index 77b22230..4edea4e5 100644 --- a/GUI/src/pages/UserManagement/index.tsx +++ b/GUI/src/pages/UserManagement/index.tsx @@ -25,10 +25,63 @@ const UserManagement: FC = () => { ); const { t } = useTranslation(); + const [formValues, setFormValues] = useState({ + firstName: '', + lastName: '', + idCode: '', + csaTitle: '', + csaEmail: '', + authorities: [], + }); + + const [formErrors, setFormErrors] = useState({ + firstName: '', + lastName: '', + idCode: '', + csaTitle: '', + csaEmail: '', + authorities: '', + }); + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormValues({ ...formValues, [name]: value }); + }; + + const handleMultiselectChange = (name: string, value: any) => { + setFormValues({ ...formValues, [name]: value }); + }; + + const validateForm = () => { + let errors: any = {}; + if (!formValues.firstName) errors.firstName = 'First name is required'; + if (!formValues.lastName) errors.lastName = 'Last name is required'; + if (!formValues.idCode) errors.idCode = 'Personal ID is required'; + if (!formValues.csaTitle) errors.csaTitle = 'Title is required'; + if (!formValues.csaEmail) errors.csaEmail = 'Email is required'; + else if (!/\S+@\S+\.\S+/.test(formValues.csaEmail)) + errors.csaEmail = 'Email is invalid'; + if (!formValues.authorities.length) + errors.authorities = 'At least one role is required'; + setFormErrors(errors); + return Object.keys(errors).length === 0; + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (validateForm()) { + console.log('Form submitted successfully', formValues); + // Handle form submission logic + } + }; + const editView = (props: any) => (
    -
    - +
    + {deletableRow !== null && ( { onClose={() => { setNewUserModal(false); setEditableRow(null); + setFormErrors({}); }} title={newUserModal ? 'Add User' : 'Edit User'} isOpen={newUserModal || editableRow !== null} @@ -157,74 +221,83 @@ const UserManagement: FC = () => { onClick={() => { setEditableRow(null); setNewUserModal(false); + setFormErrors({}); }} > Cancel - + } > - <> -
    -
    -
    - -
    -
    - ({ - value: role, - label: role - .replace('ROLE_', '') - .split('_') - .map( - (word) => - word.charAt(0) + word.slice(1).toLowerCase() - ) - .join(' '), - }) - )} - /> -
    -
    - -
    -
    - -
    -
    - -
    -
    -
    - +
    +
    +
    + +
    +
    + handleMultiselectChange('authorities', value)} + selectedOptions={formValues.authorities.map((role) => ({ + value: role, + label: role + .replace('ROLE_', '') + .split('_') + .map( + (word) => + word.charAt(0) + word.slice(1).toLowerCase() + ) + .join(' '), + }))} + error={formErrors.authorities} + /> +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    )}
    diff --git a/GUI/src/utils/constants.ts b/GUI/src/utils/constants.ts index bc9be5bc..72b92b79 100644 --- a/GUI/src/utils/constants.ts +++ b/GUI/src/utils/constants.ts @@ -7,6 +7,7 @@ export enum ROLES { ROLE_CHATBOT_TRAINER = 'ROLE_CHATBOT_TRAINER', ROLE_ANALYST = 'ROLE_ANALYST', ROLE_UNAUTHENTICATED = 'ROLE_UNAUTHENTICATED', + ROLE_MODEL_TRAINER='ROLE_MODEL_TRAINER' } export enum RUUTER_ENDPOINTS { diff --git a/GUI/translations/en/common.json b/GUI/translations/en/common.json index a1b8da72..76116734 100644 --- a/GUI/translations/en/common.json +++ b/GUI/translations/en/common.json @@ -52,42 +52,15 @@ "closeIcon": "Close menu icon" }, "menu": { - "conversations": "Conversations", - "unanswered": "Unanswered", - "active": "Active", - "history": "History", - "training": "Training", - "themes": "Themes", - "answers": "Answers", - "userStories": "User Stories", - "configuration": "Configuration", - "forms": "Forms", - "slots": "Slots", - "historicalConversations": "Historical Conversations", - "modelBankAndAnalytics": "Model Bank And Analytics", - "overviewOfTopics": "Overview Of Topics", - "comparisonOfModels": "Comparison Of Models", - "appeals": "Appeals", - "testTracks": "Test Tracks", - "trainNewModel": "Train New Model", - "analytics": "Analytics", - "settings": "Settings", - "overview": "Overview", - "chats": "Chats", - "burokratt": "Bürokratt", - "feedback": "Feedback", - "advisors": "Advisors", - "reports": "Open Data", - "users": "Users", - "administration": "Administration", - "chatbot": "Chatbot", - "welcomeMessage": "Welcome Message", - "appearanceAndBehavior": "Appearance And Behavior", - "emergencyNotices": "Emergency Notices", - "officeOpeningHours": "Office Opening Hours", - "sessionLength": "Session Length", - "monitoring": "Monitoring", - "workingHours": "Working hours" + "userManagement": "User Management", + "integration": "Integration", + "datasets": "Datasets", + "datasetGroups": "Dataset Groups", + "versions": "Versions", + "dataModels": "Data Models", + "classes": "Classes", + "stopWords": "Stop Words", + "incomingTexts": "Incoming Texts" }, "chat": { "reply": "Reply", @@ -234,11 +207,7 @@ }, "roles": { "ROLE_ADMINISTRATOR": "Administrator", - "ROLE_SERVICE_MANAGER": "Service manager", - "ROLE_CUSTOMER_SUPPORT_AGENT": "Customer support", - "ROLE_CHATBOT_TRAINER": "Chatbot trainer", - "ROLE_ANALYST": "Analyst", - "ROLE_UNAUTHENTICATED": "Unauthenticated" + "ROLE_MODEL_TRAINER": "Model Trainer" }, "settings": { "title": "Settings", diff --git a/docker-compose.yml b/docker-compose.yml index 6a4ec1ac..dd35c11a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -105,19 +105,19 @@ services: # networks: # - bykstack - users_db: - container_name: users_db - image: postgres:14.1 - environment: - - POSTGRES_USER=byk - - POSTGRES_PASSWORD=01234 - - POSTGRES_DB=byk - ports: - - 5433:5432 - volumes: - - ./data:/var/lib/postgresql/data - networks: - - bykstack + # users_db: + # container_name: users_db + # image: postgres:14.1 + # environment: + # - POSTGRES_USER=byk + # - POSTGRES_PASSWORD=01234 + # - POSTGRES_DB=byk + # ports: + # - 5433:5432 + # volumes: + # - ./data:/var/lib/postgresql/data + # networks: + # - bykstack # resql_training: # container_name: resql-training @@ -149,15 +149,6 @@ services: - DEBUG_ENABLED=true - CHOKIDAR_USEPOLLING=true - PORT=3001 - - REACT_APP_BUEROKRATT_CHATBOT_URL=http://buerokratt-chat:8086 - - REACT_APP_MENU_URL=https://admin.dev.buerokratt.ee - - REACT_APP_MENU_PATH=/chat/menu.json - - REACT_APP_CONVERSATIONS_BASE_URL=http://localhost:8080/chat - - REACT_APP_TRAINING_BASE_URL=http://localhost:8080/training - - REACT_APP_ANALYTICS_BASE_URL=http://localhost:8080/analytics - - REACT_APP_SERVICES_BASE_URL=http://localhost:8080/services - - REACT_APP_SETTINGS_BASE_URL=http://localhost:8080/settings - - REACT_APP_MONITORING_BASE_URL=http://localhost:8080/monitoring - REACT_APP_SERVICE_ID=conversations,settings,monitoring - REACT_APP_ENABLE_HIDDEN_FEATURES=TRUE - REACT_APP_MENU_JSON=[{"id":"conversations","label":{"et":"Vestlused","en":"Conversations"},"path":"/chat","children":[{"label":{"et":"Vastamata","en":"Unanswered"},"path":"/unanswered"},{"label":{"et":"Aktiivsed","en":"Active"},"path":"/active"},{"label":{"et":"Ajalugu","en":"History"},"path":"/history"}]},{"id":"training","label":{"et":"Treening","en":"Training"},"path":"/training","children":[{"label":{"et":"Treening","en":"Training"},"path":"/training","children":[{"label":{"et":"Teemad","en":"Themes"},"path":"/training/intents"},{"label":{"et":"Avalikud teemad","en":"Public themes"},"path":"/training/common-intents"},{"label":{"et":"Teemade järeltreenimine","en":"Post training themes"},"path":"/training/intents-followup-training"},{"label":{"et":"Vastused","en":"Answers"},"path":"/training/responses"},{"label":{"et":"Kasutuslood","en":"User Stories"},"path":"/training/stories"},{"label":{"et":"Konfiguratsioon","en":"Configuration"},"path":"/training/configuration"},{"label":{"et":"Vormid","en":"Forms"},"path":"/training/forms"},{"label":{"et":"Mälukohad","en":"Slots"},"path":"/training/slots"},{"label":{"et":"Automatic Teenused","en":"Automatic Services"},"path":"/auto-services"}]},{"label":{"et":"Ajaloolised vestlused","en":"Historical conversations"},"path":"/history","children":[{"label":{"et":"Ajalugu","en":"History"},"path":"/history/history"},{"label":{"et":"Pöördumised","en":"Appeals"},"path":"/history/appeal"}]},{"label":{"et":"Mudelipank ja analüütika","en":"Modelbank and analytics"},"path":"/analytics","children":[{"label":{"et":"Teemade ülevaade","en":"Overview of topics"},"path":"/analytics/overview"},{"label":{"et":"Mudelite võrdlus","en":"Comparison of models"},"path":"/analytics/models"},{"label":{"et":"Testlood","en":"testTracks"},"path":"/analytics/testcases"}]},{"label":{"et":"Treeni uus mudel","en":"Train new model"},"path":"/train-new-model"}]},{"id":"analytics","label":{"et":"Analüütika","en":"Analytics"},"path":"/analytics","children":[{"label":{"et":"Ülevaade","en":"Overview"},"path":"/overview"},{"label":{"et":"Vestlused","en":"Chats"},"path":"/chats"},{"label":{"et":"Bürokratt","en":"Burokratt"},"path":"/burokratt"},{"label":{"et":"Tagasiside","en":"Feedback"},"path":"/feedback"},{"label":{"et":"Nõustajad","en":"Advisors"},"path":"/advisors"},{"label":{"et":"Avaandmed","en":"Reports"},"path":"/reports"}]},{"id":"services","label":{"et":"Teenused","en":"Services"},"path":"/services","children":[{"label":{"et":"Ülevaade","en":"Overview"},"path":"/overview"},{"label":{"et":"Uus teenus","en":"New Service"},"path":"/newService"},{"label":{"et":"Automatic Teenused","en":"Automatic Services"},"path":"/auto-services"},{"label":{"et":"Probleemsed teenused","en":"Faulty Services"},"path":"/faultyServices"}]},{"id":"settings","label":{"et":"Haldus","en":"Administration"},"path":"/settings","children":[{"label":{"et":"Kasutajad","en":"Users"},"path":"/users"},{"label":{"et":"Vestlusbot","en":"Chatbot"},"path":"/chatbot","children":[{"label":{"et":"Seaded","en":"Settings"},"path":"/chatbot/settings"},{"label":{"et":"Tervitussõnum","en":"Welcome message"},"path":"/chatbot/welcome-message"},{"label":{"et":"Välimus ja käitumine","en":"Appearance and behavior"},"path":"/chatbot/appearance"},{"label":{"et":"Erakorralised teated","en":"Emergency notices"},"path":"/chatbot/emergency-notices"}]},{"label":{"et":"Asutuse tööaeg","en":"Office opening hours"},"path":"/working-time"},{"label":{"et":"Sessiooni pikkus","en":"Session length"},"path":"/session-length"}]},{"id":"monitoring","label":{"et":"Seire","en":"Monitoring"},"path":"/monitoring","children":[{"label":{"et":"Aktiivaeg","en":"Working hours"},"path":"/uptime"}]}] From 7cbd7831ebb681d78ad65e819d4853aa888e51e6 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Tue, 2 Jul 2024 13:15:13 +0530 Subject: [PATCH 054/582] initial workflow for frontend --- .../workflows/workflows/est-frontend-dev.yml | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/workflows/workflows/est-frontend-dev.yml diff --git a/.github/workflows/workflows/est-frontend-dev.yml b/.github/workflows/workflows/est-frontend-dev.yml new file mode 100644 index 00000000..0aeb2068 --- /dev/null +++ b/.github/workflows/workflows/est-frontend-dev.yml @@ -0,0 +1,41 @@ +name: Deploy to Custom Runner + +on: + push: + branches: + - dev + +jobs: + deploy: + runs-on: [self-hosted, est-cls-dev] + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Remove all running containers, images, and prune Docker system + run: | + docker stop $(docker ps -a -q) || true + docker rm $(docker ps -a -q) || true + docker rmi $(docker images -q) || true + docker system prune -af + + - name: Build and run Docker Compose + run: | + cd GUI + docker-compose up --build -d + + - name: Get public IP address + id: get_ip + run: | + PUBLIC_IP=$(curl -s http://checkip.amazonaws.com) + echo "PUBLIC_IP=$PUBLIC_IP" >> $GITHUB_ENV + + - name: Send Slack notification + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + PUBLIC_IP: ${{ env.PUBLIC_IP }} + run: | + curl -X POST -H 'Content-type: application/json' --data "{ + \"text\": \"Build is complete. Public IP: $PUBLIC_IP\" + }" $SLACK_WEBHOOK_URL From 4625e894fb8dce99c92bd08c4349323950425d75 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Tue, 2 Jul 2024 13:18:46 +0530 Subject: [PATCH 055/582] remove workflow --- .github/workflows/README.md | 1 - .../workflows/workflows/est-frontend-dev.yml | 41 ------------------- 2 files changed, 42 deletions(-) delete mode 100644 .github/workflows/README.md delete mode 100644 .github/workflows/workflows/est-frontend-dev.yml diff --git a/.github/workflows/README.md b/.github/workflows/README.md deleted file mode 100644 index abdfbabb..00000000 --- a/.github/workflows/README.md +++ /dev/null @@ -1 +0,0 @@ -GitHub actions and workflows live here... \ No newline at end of file diff --git a/.github/workflows/workflows/est-frontend-dev.yml b/.github/workflows/workflows/est-frontend-dev.yml deleted file mode 100644 index 0aeb2068..00000000 --- a/.github/workflows/workflows/est-frontend-dev.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Deploy to Custom Runner - -on: - push: - branches: - - dev - -jobs: - deploy: - runs-on: [self-hosted, est-cls-dev] - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Remove all running containers, images, and prune Docker system - run: | - docker stop $(docker ps -a -q) || true - docker rm $(docker ps -a -q) || true - docker rmi $(docker images -q) || true - docker system prune -af - - - name: Build and run Docker Compose - run: | - cd GUI - docker-compose up --build -d - - - name: Get public IP address - id: get_ip - run: | - PUBLIC_IP=$(curl -s http://checkip.amazonaws.com) - echo "PUBLIC_IP=$PUBLIC_IP" >> $GITHUB_ENV - - - name: Send Slack notification - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} - PUBLIC_IP: ${{ env.PUBLIC_IP }} - run: | - curl -X POST -H 'Content-type: application/json' --data "{ - \"text\": \"Build is complete. Public IP: $PUBLIC_IP\" - }" $SLACK_WEBHOOK_URL From 158bf340aed246b4f1f76748840302153f430403 Mon Sep 17 00:00:00 2001 From: Pamoda Dilranga <51948729+pamodaDilranga@users.noreply.github.com> Date: Tue, 2 Jul 2024 13:20:43 +0530 Subject: [PATCH 056/582] Create est-frontend-dev.yml This is the workflow to deploy frontend in EC2 --- .github/workflows/est-frontend-dev.yml | 41 ++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/workflows/est-frontend-dev.yml diff --git a/.github/workflows/est-frontend-dev.yml b/.github/workflows/est-frontend-dev.yml new file mode 100644 index 00000000..4e798c75 --- /dev/null +++ b/.github/workflows/est-frontend-dev.yml @@ -0,0 +1,41 @@ +name: Deploy Frontend + +on: + push: + branches: + - dev + +jobs: + deploy: + runs-on: [self-hosted, est-cls-dev] + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Remove all running containers, images, and prune Docker system + run: | + docker stop $(docker ps -a -q) || true + docker rm $(docker ps -a -q) || true + docker rmi $(docker images -q) || true + docker system prune -af + + - name: Build and run Docker Compose + run: | + cd GUI + docker-compose up --build -d + + - name: Get public IP address + id: get_ip + run: | + PUBLIC_IP=$(curl -s http://checkip.amazonaws.com) + echo "PUBLIC_IP=$PUBLIC_IP" >> $GITHUB_ENV + + - name: Send Slack notification + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + PUBLIC_IP: ${{ env.PUBLIC_IP }} + run: | + curl -X POST -H 'Content-type: application/json' --data "{ + \"text\": \"Build is complete. Public IP: $PUBLIC_IP\" + }" $SLACK_WEBHOOK_URL From 72fb1b5aedfe23aa9da3763603de93f595d37624 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Tue, 2 Jul 2024 13:24:09 +0530 Subject: [PATCH 057/582] comopse command change --- .github/workflows/est-frontend-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/est-frontend-dev.yml b/.github/workflows/est-frontend-dev.yml index 4e798c75..32328f6b 100644 --- a/.github/workflows/est-frontend-dev.yml +++ b/.github/workflows/est-frontend-dev.yml @@ -23,7 +23,7 @@ jobs: - name: Build and run Docker Compose run: | cd GUI - docker-compose up --build -d + docker compose up --build -d - name: Get public IP address id: get_ip From e95211d5cab6b1f0da927e694ded12b4c56c70b6 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Tue, 2 Jul 2024 13:48:09 +0530 Subject: [PATCH 058/582] classifier-97 fix the issues in retrieve mail data from subscription in outlook --- .../return_outlook_payload_info.handlebars | 7 ++ .../classifier/integration/outlook/token.yml | 4 +- .../classifier/integration/outlook/accept.yml | 84 +++++++++++-------- .../classifier/integration/outlook/group.yml | 2 +- constants.ini | 3 +- docker-compose.yml | 31 +++++++ 6 files changed, 94 insertions(+), 37 deletions(-) create mode 100644 DSL/DMapper/hbs/return_outlook_payload_info.handlebars diff --git a/DSL/DMapper/hbs/return_outlook_payload_info.handlebars b/DSL/DMapper/hbs/return_outlook_payload_info.handlebars new file mode 100644 index 00000000..3e9639be --- /dev/null +++ b/DSL/DMapper/hbs/return_outlook_payload_info.handlebars @@ -0,0 +1,7 @@ +{ + "outlookId": "{{{data.id}}}", + "subject": "{{{data.subject}}}", + "parentFolderId": "{{{data.parentFolderId}}}", + "categories": "{{{data.categories}}}", + "body": "{{{data.body.contentType}}}" +} diff --git a/DSL/Ruuter.private/DSL/GET/classifier/integration/outlook/token.yml b/DSL/Ruuter.private/DSL/GET/classifier/integration/outlook/token.yml index 66ee1e55..fdf73822 100644 --- a/DSL/Ruuter.private/DSL/GET/classifier/integration/outlook/token.yml +++ b/DSL/Ruuter.private/DSL/GET/classifier/integration/outlook/token.yml @@ -29,13 +29,13 @@ check_refresh_token: get_access_token: call: http.post args: - url: "https://login.microsoftonline.com/[#TENANT]/oauth2/v2.0/token" + url: "https://login.microsoftonline.com/common/oauth2/v2.0/token" contentType: formdata headers: type: json body: client_id: "[#OUTLOOK_CLIENT_ID]" - scope: "User.Read Mail.Read Mail.ReadWrite" + scope: "User.Read Mail.ReadWrite MailboxSettings.ReadWrite offline_access" refresh_token: ${refresh_token} grant_type: "refresh_token" client_secret: "[#OUTLOOK_SECRET_KEY]" diff --git a/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/accept.yml b/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/accept.yml index 05ca5402..64bb1e63 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/accept.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/accept.yml @@ -4,7 +4,7 @@ declaration: description: "Description placeholder for 'ACCEPT'" method: get accepts: json - returns: json + returns: text/* namespace: classifier allowlist: params: @@ -22,64 +22,77 @@ extract_request_data: payload: ${incoming.body} next: check_process_flow -new_val: - return: ${validation_token} - check_process_flow: switch: - condition: ${validation_token !==null} - next: assign_validation_token + next: return_validation_token_response - condition: ${payload !==null} next: assign_outlook_mail_info next: return_error_found -assign_validation_token: +return_validation_token_response: + wrapper: false + headers: + Content-Type: text/* + return: ${validation_token} + status: 200 + next: end + +assign_outlook_mail_info: assign: - validation_token: ${payload.validationToken} - next: set_response_data + resource: ${payload.value[0].resource} + next: get_token_info -set_response_data: - call: http.post +get_token_info: + call: http.get args: - url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/return_outlook_validation_token" - headers: - type: json - body: - validationToken: ${validation_token} - result: response - next: return_validation_response - -return_validation_response: - return: ${response} - status: 200 + url: "[#CLASSIFIER_RUUTER_PRIVATE]/classifier/integration/outlook/token" + result: res + next: assign_access_token -assign_outlook_mail_info: +assign_access_token: assign: - mail_info: ${payload} - next: extract_data_from_payload + access_token: ${res.response.body.response.access_token} + next: get_extracted_mail_info + +get_extracted_mail_info: + call: http.get + args: + url: "https://graph.microsoft.com/v1.0/${resource}" + headers: + Authorization: ${'Bearer ' + access_token} + result: mail_info_data + next: check_extracted_mail_info + +check_extracted_mail_info: + switch: + - condition: ${200 <= mail_info_data.response.statusCodeValue && mail_info_data.response.statusCodeValue < 300} + next: rearrange_mail_payload + next: return_mail_info_not_found -extract_data_from_payload: +rearrange_mail_payload: call: http.post args: - url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/return_outlook_mail_info" + url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/return_outlook_payload_info" headers: type: json body: - data: ${mail_info} - result: extract_info - next: send_issue_data + data: ${mail_info_data.response.body} + result: outlook_body + next: send_outlook_data #check the mail id is an existing id and check categories from mail and db are same #if different or new send to AI model - -send_issue_data: - call: http.post +send_outlook_data: + call: reflect.mock args: url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/return_jira_issue_info"# need correct url headers: type: json body: - info: ${extract_info} + info: ${outlook_body} + response: + statusCodeValue: 200 result: res next: check_response @@ -94,6 +107,11 @@ return_ok: return: "Outlook data send successfully" next: end +return_mail_info_not_found: + status: 400 + return: "Mail Info Not Found" + next: end + return_bad_request: status: 400 return: "Bad Request" diff --git a/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/group.yml b/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/group.yml index 7943fa89..30bacf1a 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/group.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/group.yml @@ -73,7 +73,7 @@ set_data: update_mail: call: http.put args: - url: "https://graph.microsoft.com/v1.0/me/messages//${mail_id}" + url: "https://graph.microsoft.com/v1.0/me/messages/${mail_id}" headers: Authorization: ${'Bearer ' + access_token} body: diff --git a/constants.ini b/constants.ini index 5007c85f..fbff107d 100644 --- a/constants.ini +++ b/constants.ini @@ -4,12 +4,13 @@ CLASSIFIER_RUUTER_PUBLIC=http://ruuter-public:8086 CLASSIFIER_RUUTER_PRIVATE=http://ruuter-private:8088 CLASSIFIER_DMAPPER=http://data-mapper:3000 CLASSIFIER_RESQL=http://resql:8082 +CLASSIFIER_TIM=http://tim:8085 +DOMAIN=localhost JIRA_API_TOKEN= value JIRA_USERNAME= value JIRA_CLOUD_DOMAIN= value JIRA_WEBHOOK_ID=value JIRA_WEBHOOK_SECRET=value -TENANT=value OUTLOOK_CLIENT_ID=value OUTLOOK_SECRET_KEY=value OUTLOOK_REFRESH_KEY=value \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 52013dba..0d6c974d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -70,6 +70,37 @@ services: networks: - bykstack + tim: + container_name: tim + image: tim + depends_on: + - tim-postgresql + environment: + - SECURITY_ALLOWLIST_JWT=ruuter-private,ruuter-public,data-mapper,resql,tim,tim-postgresql,chat-widget,authentication-layer,127.0.0.1,::1 + ports: + - 8085:8085 + networks: + - bykstack + extra_hosts: + - "host.docker.internal:host-gateway" + cpus: "0.5" + mem_limit: "512M" + + tim-postgresql: + container_name: tim-postgresql + image: postgres:14.1 + environment: + - POSTGRES_USER=tim + - POSTGRES_PASSWORD=123 + - POSTGRES_DB=tim + - POSTGRES_HOST_AUTH_METHOD=trust + volumes: + - ./tim-db:/var/lib/postgresql/data + ports: + - 9876:5432 + networks: + - bykstack + networks: bykstack: name: bykstack From 23dae9387b53d40983741d8bf855a17a16b37473 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Tue, 2 Jul 2024 15:17:05 +0530 Subject: [PATCH 059/582] update root docker-compose --- GUI/docker-compose.yml | 2 +- GUI/rebuild.sh | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/GUI/docker-compose.yml b/GUI/docker-compose.yml index 3b37f8c3..87d6970c 100644 --- a/GUI/docker-compose.yml +++ b/GUI/docker-compose.yml @@ -1,7 +1,7 @@ version: "3.9" services: buerokratt_chatbot: - container_name: buerokratt_chatbot + container_name: buerokratt_classifier build: context: . target: web diff --git a/GUI/rebuild.sh b/GUI/rebuild.sh index 3ce9c0fa..c83c0b8b 100644 --- a/GUI/rebuild.sh +++ b/GUI/rebuild.sh @@ -4,9 +4,9 @@ apk add nodejs # Rebuild the project -cd /opt/buerokratt-chatbot +cd /opt/buerokratt-classifier ./node_modules/.bin/vite build -l warn -cp -ru build/* /usr/share/nginx/html/buerokratt-chatbot +cp -ru build/* /usr/share/nginx/html/buerokratt-classifier # Start the Nginx server nginx -g "daemon off;" From 60acd5ab2c9f810ae4eef9a9da6841aa924b3bb7 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Tue, 2 Jul 2024 15:34:06 +0530 Subject: [PATCH 060/582] change branch for testing --- .github/workflows/est-frontend-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/est-frontend-dev.yml b/.github/workflows/est-frontend-dev.yml index 32328f6b..6eecb205 100644 --- a/.github/workflows/est-frontend-dev.yml +++ b/.github/workflows/est-frontend-dev.yml @@ -3,7 +3,7 @@ name: Deploy Frontend on: push: branches: - - dev + - initial-gui-setup jobs: deploy: From 040c1ec8cb9ec1497a1041452ef2b1aad4faaa13 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Tue, 2 Jul 2024 15:41:32 +0530 Subject: [PATCH 061/582] change back to dev --- .github/workflows/est-frontend-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/est-frontend-dev.yml b/.github/workflows/est-frontend-dev.yml index 6eecb205..32328f6b 100644 --- a/.github/workflows/est-frontend-dev.yml +++ b/.github/workflows/est-frontend-dev.yml @@ -3,7 +3,7 @@ name: Deploy Frontend on: push: branches: - - initial-gui-setup + - dev jobs: deploy: From 6107fef84639af0816f81583c82eaa3c501b04fc Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Tue, 2 Jul 2024 16:30:26 +0530 Subject: [PATCH 062/582] test main comopse --- .github/workflows/est-frontend-dev.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/est-frontend-dev.yml b/.github/workflows/est-frontend-dev.yml index 32328f6b..6b44a50b 100644 --- a/.github/workflows/est-frontend-dev.yml +++ b/.github/workflows/est-frontend-dev.yml @@ -22,7 +22,6 @@ jobs: - name: Build and run Docker Compose run: | - cd GUI docker compose up --build -d - name: Get public IP address From e7737e97c2ad95c5e84f09ffefbf3895d666edba Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Tue, 2 Jul 2024 16:33:45 +0530 Subject: [PATCH 063/582] classifier-97 implement outlook integration update email flow --- .../jira/cloud/{update.yml => label.yml} | 2 +- ...{toggle-subscription.yml => subscribe.yml} | 2 +- .../outlook/{group.yml => label.yml} | 77 ++++++++++--------- ...{toggle-subscription.yml => subscribe.yml} | 2 +- 4 files changed, 45 insertions(+), 38 deletions(-) rename DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/{update.yml => label.yml} (97%) rename DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/{toggle-subscription.yml => subscribe.yml} (96%) rename DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/{group.yml => label.yml} (51%) rename DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/{toggle-subscription.yml => subscribe.yml} (98%) diff --git a/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/update.yml b/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/label.yml similarity index 97% rename from DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/update.yml rename to DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/label.yml index 0ea5cc74..2487b060 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/update.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/label.yml @@ -1,7 +1,7 @@ declaration: call: declare version: 0.1 - description: "Description placeholder for 'UPDATE'" + description: "Description placeholder for 'LABEL'" method: post accepts: json returns: json diff --git a/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/toggle-subscription.yml b/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/subscribe.yml similarity index 96% rename from DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/toggle-subscription.yml rename to DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/subscribe.yml index 2a6530d8..52b4c8bd 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/toggle-subscription.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/subscribe.yml @@ -1,7 +1,7 @@ declaration: call: declare version: 0.1 - description: "Description placeholder for 'TOGGLE-SUBSCRIPTION'" + description: "Description placeholder for 'SUBSCRIBE'" method: post accepts: json returns: json diff --git a/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/group.yml b/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/label.yml similarity index 51% rename from DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/group.yml rename to DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/label.yml index 30bacf1a..cbce716c 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/group.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/label.yml @@ -1,7 +1,7 @@ declaration: call: declare version: 0.1 - description: "Description placeholder for 'Group'" + description: "Description placeholder for 'Label'" method: post accepts: json returns: json @@ -10,20 +10,20 @@ declaration: body: - field: mailId type: string - description: "Body field 'issueId'" - - field: labels - type: array - description: "Body field 'labels'" + description: "Body field 'mailId'" + - field: folderId + type: string + description: "Body field 'folderId'" extract_request_data: assign: mail_id: ${incoming.body.mailId} - label_list: ${incoming.body.labels} + folder_id: ${incoming.body.folderId} next: check_for_request_data check_for_request_data: switch: - - condition: ${mail_id !== null || label_list @== null} + - condition: ${mail_id !== null || folder_id !== null} next: get_token_info next: return_incorrect_request @@ -37,47 +37,46 @@ get_token_info: assign_access_token: assign: access_token: ${res.response.body.response.access_token} - next: get_mail_issue_info + next: get_email_exist -get_mail_issue_info: +get_email_exist: call: http.get args: - url: "https://graph.microsoft.com/v1.0/me/messages//${mail_id}" + url: "https://graph.microsoft.com/v1.0/me/messages/${mail_id}" headers: Authorization: ${'Bearer ' + access_token} - result: mail_info - next: assign_existing_labels + result: res + next: check_email_status -assign_existing_labels: - assign: - existing_label_list: ${mail_info.response.body.fields.categories} - next: merge_labels +check_email_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: get_folder_exist + next: return_email_not_found -merge_labels: - call: http.post +get_folder_exist: + call: http.get args: - url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/return_label_field_array" + url: "https://graph.microsoft.com/v1.0/me/mailFolders/${folder_id}" headers: - type: json - body: - labels: ${label_list} - existing_labels: ${existing_label_list} + Authorization: ${'Bearer ' + access_token} result: res - next: set_data + next: check_folder_status -set_data: - assign: - all_labels: ${res.response.body} - next: update_mail +check_folder_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: update_mail_folder + next: return_folder_not_found -update_mail: - call: http.put +update_mail_folder: + call: http.post args: - url: "https://graph.microsoft.com/v1.0/me/messages/${mail_id}" + url: "https://graph.microsoft.com/v1.0/me/messages/${mail_id}/move" headers: Authorization: ${'Bearer ' + access_token} body: - fields: ${all_labels} #catergories need to change mapper cll + destinationId: ${folder_id} result: res_mail next: check_status @@ -87,11 +86,19 @@ check_status: next: return_ok next: return_bad_request -#update table on success of mail update - return_ok: status: 200 - return: "Outlook email Updated" + return: "Outlook Email Destination Updated" + next: end + +return_folder_not_found: + status: 404 + return: 'Folder Not Found' + next: end + +return_email_not_found: + status: 404 + return: 'Email Not Found' next: end return_bad_request: diff --git a/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/toggle-subscription.yml b/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/subscribe.yml similarity index 98% rename from DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/toggle-subscription.yml rename to DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/subscribe.yml index 87c16645..2a4651f8 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/toggle-subscription.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/subscribe.yml @@ -1,7 +1,7 @@ declaration: call: declare version: 0.1 - description: "Description placeholder for 'TOGGLE-SUBSCRIPTION'" + description: "Description placeholder for 'SUBSCRIBE'" method: post accepts: json returns: json From 4142e97c540ae99faa139166476250a86bab67c5 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Tue, 2 Jul 2024 18:53:05 +0530 Subject: [PATCH 064/582] classifier-97 when fetch data from outlook, check whether update trigger really updated for folder destination change or not --- .../changelog/create-initial-tables.sql | 20 +++++++++--- DSL/Resql/get-jira-input-row-data.sql | 3 ++ DSL/Resql/get-outlook-input-row-data.sql | 3 ++ DSL/Resql/get-platform-input-row-data.sql | 3 -- .../integration/jira/cloud/accept.yml | 3 +- .../classifier/integration/outlook/accept.yml | 30 +++++++++++++++++- .../integration/outlook/subscribe.yml | 6 ++-- docker-compose.yml | 31 ------------------- 8 files changed, 53 insertions(+), 46 deletions(-) create mode 100644 DSL/Resql/get-jira-input-row-data.sql create mode 100644 DSL/Resql/get-outlook-input-row-data.sql delete mode 100644 DSL/Resql/get-platform-input-row-data.sql diff --git a/DSL/Liquibase/changelog/create-initial-tables.sql b/DSL/Liquibase/changelog/create-initial-tables.sql index e32b84e9..fd92a334 100644 --- a/DSL/Liquibase/changelog/create-initial-tables.sql +++ b/DSL/Liquibase/changelog/create-initial-tables.sql @@ -21,13 +21,23 @@ VALUES ('PINAL', FALSE, NULL, NULL); -- changeset kalsara Magamage:classifier-ddl-script-v1-changeset4 -CREATE TABLE public."inputs" ( +CREATE TABLE public."jira" ( id int8 NOT NULL GENERATED BY DEFAULT AS IDENTITY, - input_id VARCHAR(50) DEFAULT NULL, - anonym_text VARCHAR(50) DEFAULT NULL, - platform platform, + input_id TEXT DEFAULT NULL, + anonym_text TEXT DEFAULT NULL, corrected BOOLEAN NOT NULL DEFAULT FALSE, predicted_labels TEXT[] DEFAULT NULL, corrected_labels TEXT[] DEFAULT NULL, - CONSTRAINT inputs_pkey PRIMARY KEY (id) + CONSTRAINT jira_pkey PRIMARY KEY (id) +); + +-- changeset kalsara Magamage:classifier-ddl-script-v1-changeset6 +CREATE TABLE public."outlook" ( + id int8 NOT NULL GENERATED BY DEFAULT AS IDENTITY, + input_id TEXT DEFAULT NULL, + anonym_text VARCHAR(50) DEFAULT NULL, + corrected BOOLEAN NOT NULL DEFAULT FALSE, + primary_folder_id TEXT DEFAULT NULL, + parent_folder_ids TEXT[] DEFAULT NULL, + CONSTRAINT outlook_pkey PRIMARY KEY (id) ); diff --git a/DSL/Resql/get-jira-input-row-data.sql b/DSL/Resql/get-jira-input-row-data.sql new file mode 100644 index 00000000..6f5e4ea9 --- /dev/null +++ b/DSL/Resql/get-jira-input-row-data.sql @@ -0,0 +1,3 @@ +SELECT corrected_labels +FROM jira +WHERE input_id=:inputId; diff --git a/DSL/Resql/get-outlook-input-row-data.sql b/DSL/Resql/get-outlook-input-row-data.sql new file mode 100644 index 00000000..e90ea043 --- /dev/null +++ b/DSL/Resql/get-outlook-input-row-data.sql @@ -0,0 +1,3 @@ +SELECT primary_folder_id +FROM outlook +WHERE input_id=:inputId; diff --git a/DSL/Resql/get-platform-input-row-data.sql b/DSL/Resql/get-platform-input-row-data.sql deleted file mode 100644 index 38becd3d..00000000 --- a/DSL/Resql/get-platform-input-row-data.sql +++ /dev/null @@ -1,3 +0,0 @@ -SELECT corrected_labels -FROM inputs -WHERE input_id=:inputId AND platform=:platform::platform; diff --git a/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/accept.yml b/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/accept.yml index 901b55e0..3cf43665 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/accept.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/accept.yml @@ -62,10 +62,9 @@ check_event_type: get_existing_labels: call: http.post args: - url: "[#CLASSIFIER_RESQL]/get-platform-input-row-data" + url: "[#CLASSIFIER_RESQL]/get-jira-input-row-data" body: inputId: ${issue_info.key} - platform: 'JIRA' result: res next: check_input_response diff --git a/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/accept.yml b/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/accept.yml index 64bb1e63..9578e214 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/accept.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/accept.yml @@ -41,6 +41,7 @@ return_validation_token_response: assign_outlook_mail_info: assign: resource: ${payload.value[0].resource} + event_type: ${payload.value[0].changeType} next: get_token_info get_token_info: @@ -67,9 +68,36 @@ get_extracted_mail_info: check_extracted_mail_info: switch: - condition: ${200 <= mail_info_data.response.statusCodeValue && mail_info_data.response.statusCodeValue < 300} - next: rearrange_mail_payload + next: check_event_type next: return_mail_info_not_found +check_event_type: + switch: + - condition: ${event_type === 'updated'} + next: get_existing_folder_id + next: rearrange_mail_payload + +get_existing_folder_id: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-outlook-input-row-data" + body: + inputId: ${mail_info_data.id} + result: existing_outlook_info + next: check_input_response + +check_input_response: + switch: + - condition: ${200 <= existing_outlook_info.response.statusCodeValue && existing_outlook_info.response.statusCodeValue < 300} + next: check_folder_id + next: return_db_request_fail + +check_folder_id: + switch: + - condition: ${check_folder_id.response.body.primaryFolderId !== mail_info_data.parentFolderId } + next: rearrange_mail_payload + next: end + rearrange_mail_payload: call: http.post args: diff --git a/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/subscribe.yml b/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/subscribe.yml index 2a4651f8..87eed337 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/subscribe.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/subscribe.yml @@ -65,16 +65,14 @@ subscribe_outlook: headers: Authorization: ${'Bearer ' + access_token} body: - changeType: "created" - notificationUrl: "https://6040-2402-4000-2180-8a3a-a885-a68-2705-d3e3.ngrok-free.app/outlook/callback" + changeType: "created,updated" + notificationUrl: "[#CLASSIFIER_RUUTER_PRIVATE]/classifier/integration/outlook/accept" resource: "me/mailFolders('inbox')/messages" expirationDateTime: "2024-07-02T21:10:45.9356913Z" clientState: "state" result: res_subscribe next: check_subscribe_response -#classifier/integration/outlook/accept - check_subscribe_response: switch: - condition: ${200 <= res_subscribe.response.statusCodeValue && res_subscribe.response.statusCodeValue < 300} diff --git a/docker-compose.yml b/docker-compose.yml index 0d6c974d..52013dba 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -70,37 +70,6 @@ services: networks: - bykstack - tim: - container_name: tim - image: tim - depends_on: - - tim-postgresql - environment: - - SECURITY_ALLOWLIST_JWT=ruuter-private,ruuter-public,data-mapper,resql,tim,tim-postgresql,chat-widget,authentication-layer,127.0.0.1,::1 - ports: - - 8085:8085 - networks: - - bykstack - extra_hosts: - - "host.docker.internal:host-gateway" - cpus: "0.5" - mem_limit: "512M" - - tim-postgresql: - container_name: tim-postgresql - image: postgres:14.1 - environment: - - POSTGRES_USER=tim - - POSTGRES_PASSWORD=123 - - POSTGRES_DB=tim - - POSTGRES_HOST_AUTH_METHOD=trust - volumes: - - ./tim-db:/var/lib/postgresql/data - ports: - - 9876:5432 - networks: - - bykstack - networks: bykstack: name: bykstack From 4dd7970b143f0aa4a2d8f5393522d225e2f8477d Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Tue, 2 Jul 2024 19:06:19 +0530 Subject: [PATCH 065/582] classifier-97 docker-compose.yml conflicts resolved --- docker-compose.yml | 91 +++++++++------------------------------------- 1 file changed, 17 insertions(+), 74 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 4d93362e..d83b894e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ services: container_name: ruuter-private image: ruuter environment: - - application.cors.allowedOrigins=http://localhost:3001,http://localhost:8088,http://localhost:8082 + - application.cors.allowedOrigins=http://localhost:3001,http://localhost:8088,http://localhost:3002,http://localhost:3004 - application.httpCodesAllowList=200,201,202,204,400,401,403,404,500 - application.internalRequests.allowedIPs=127.0.0.1 - application.logging.displayRequestContent=true @@ -21,6 +21,22 @@ services: cpus: "0.5" mem_limit: "512M" + data-mapper: + container_name: data-mapper + image: data-mapper + environment: + - PORT=3000 + - CONTENT_FOLDER=/data + volumes: + - ./DSL:/data + - ./DSL/DMapper/hbs:/workspace/app/views/classifier + - ./DSL/DMapper/js:/workspace/app/js/classifier + - ./DSL/DMapper/lib:/workspace/app/lib + ports: + - 3000:3000 + networks: + - bykstack + tim: container_name: tim image: tim @@ -52,71 +68,6 @@ services: networks: - bykstack - # data-mapper: - # container_name: data-mapper - # image: data-mapper - # environment: - # - PORT=3000 - # - CONTENT_FOLDER=/data - # volumes: - # - ./DSL:/data - # - ./DSL/DMapper/hbs:/workspace/app/views/chat-bot - # - ./DSL/DMapper/js:/workspace/app/js/chat-bot - # - ./DSL/DMapper/lib:/workspace/app/lib - # ports: - # - 3000:3000 - # networks: - # - bykstack - - # resql: - # container_name: resql - # image: resql - # depends_on: - # - users_db - # environment: - # - sqlms.datasources.[0].name=byk - # - sqlms.datasources.[0].jdbcUrl=jdbc:postgresql://171.22.247.13:5433/byk?sslmode=require - # - sqlms.datasources.[0].username=byk - # - sqlms.datasources.[0].password=2nH09n6Gly - # - logging.level.org.springframework.boot=INFO - # ports: - # - 8082:8082 - # volumes: - # - ./DSL/Resql:/workspace/app/templates/byk - # networks: - # - bykstack - - users_db: - container_name: users_db - image: postgres:14.1 - environment: - - POSTGRES_USER=byk - - POSTGRES_PASSWORD=01234 - - POSTGRES_DB=byk - ports: - - 5433:5432 - volumes: - - ./data:/var/lib/postgresql/data - networks: - - bykstack - - # resql_training: - # container_name: resql-training - # image: resql - # environment: - # - sqlms.datasources.[0].name=training - # - sqlms.datasources.[0].jdbcUrl=jdbc:postgresql://171.22.247.13:5434/train_db - # - sqlms.datasources.[0].username=byk - # - sqlms.datasources.[0].password=01234 - # - logging.level.org.springframework.boot=INFO - # - server.port=8083 - # ports: - # - 8083:8083 - # volumes: - # - ./DSL/Resql.training:/workspace/app/templates/training - # networks: - # - bykstack - gui: container_name: gui environment: @@ -156,14 +107,6 @@ services: cpus: "0.5" mem_limit: "1G" - # chat-widget: - # container_name: chat-widget - # image: chat-widget - # ports: - # - 3003:3003 - # networks: - # - bykstack - authentication-layer: container_name: authentication-layer image: authentication-layer From 29e61f081d0c7c3c95c6a237d4fa77c1a54c2694 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Wed, 3 Jul 2024 13:15:42 +0530 Subject: [PATCH 066/582] classifier-97 add db password to environment variable --- constants.ini | 3 ++- docker-compose.yml | 2 +- migrate.sh | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/constants.ini b/constants.ini index fbff107d..4624f4e2 100644 --- a/constants.ini +++ b/constants.ini @@ -13,4 +13,5 @@ JIRA_WEBHOOK_ID=value JIRA_WEBHOOK_SECRET=value OUTLOOK_CLIENT_ID=value OUTLOOK_SECRET_KEY=value -OUTLOOK_REFRESH_KEY=value \ No newline at end of file +OUTLOOK_REFRESH_KEY=value +DB_PASSWORD=value \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index d83b894e..cc8d6d01 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -125,7 +125,7 @@ services: - sqlms.datasources.[0].jdbcUrl=jdbc:postgresql://users_db:5432/classifier #For LocalDb Use # sqlms.datasources.[0].jdbcUrl=jdbc:postgresql://171.22.247.13:5433/byk?sslmode=require - sqlms.datasources.[0].username=root - - sqlms.datasources.[0].password=root + - sqlms.datasources.[0].password=${DB_PASSWORD} - logging.level.org.springframework.boot=INFO ports: - 8082:8082 diff --git a/migrate.sh b/migrate.sh index 9b13c63f..1cf26567 100644 --- a/migrate.sh +++ b/migrate.sh @@ -1,2 +1,2 @@ #!/bin/bash -docker run --rm --network bykstack -v `pwd`/DSL/Liquibase/changelog:/liquibase/changelog -v `pwd`/DSL/Liquibase/master.yml:/liquibase/master.yml -v `pwd`/DSL/Liquibase/data:/liquibase/data liquibase/liquibase --defaultsFile=/liquibase/changelog/liquibase.properties --changelog-file=master.yml --url=jdbc:postgresql://users_db:5432/classifier?user=root --password=root update +docker run --rm --network bykstack -v `pwd`/DSL/Liquibase/changelog:/liquibase/changelog -v `pwd`/DSL/Liquibase/master.yml:/liquibase/master.yml -v `pwd`/DSL/Liquibase/data:/liquibase/data liquibase/liquibase --defaultsFile=/liquibase/changelog/liquibase.properties --changelog-file=master.yml --url=jdbc:postgresql://users_db:5432/classifier?user=root --password=$DB_PASSWORD update From 73593e32dd36a177bbcf2834c351f0b47c93b138 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Wed, 3 Jul 2024 13:51:52 +0530 Subject: [PATCH 067/582] classifier-97 set passowrd from .ini file --- DSL/Liquibase/liquibase.properties | 2 +- docker-compose.yml | 2 +- migrate.sh | 13 +++++++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/DSL/Liquibase/liquibase.properties b/DSL/Liquibase/liquibase.properties index 03d0c917..66683eb2 100644 --- a/DSL/Liquibase/liquibase.properties +++ b/DSL/Liquibase/liquibase.properties @@ -1,6 +1,6 @@ changelogFile: /changelog/master.yml url: jdbc:postgresql://localhost:5433/classifier username: root -password: root +password: val secureParsing: false liquibase.hub.mode=off diff --git a/docker-compose.yml b/docker-compose.yml index cc8d6d01..d83b894e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -125,7 +125,7 @@ services: - sqlms.datasources.[0].jdbcUrl=jdbc:postgresql://users_db:5432/classifier #For LocalDb Use # sqlms.datasources.[0].jdbcUrl=jdbc:postgresql://171.22.247.13:5433/byk?sslmode=require - sqlms.datasources.[0].username=root - - sqlms.datasources.[0].password=${DB_PASSWORD} + - sqlms.datasources.[0].password=root - logging.level.org.springframework.boot=INFO ports: - 8082:8082 diff --git a/migrate.sh b/migrate.sh index 1cf26567..8d505b33 100644 --- a/migrate.sh +++ b/migrate.sh @@ -1,2 +1,15 @@ #!/bin/bash + +# Function to parse ini file and extract the value for a given key under a given section +get_ini_value() { + local file=$1 + local key=$2 + awk -F '=' -v key="$key" '$1 == key { gsub(/^[ \t]+|[ \t]+$/, "", $2); print $2; exit }' "$file" +} + +# Get the values from dsl_config.ini +INI_FILE="constants.ini" +DB_PASSWORD=$(get_ini_value "$INI_FILE" "DB_PASSWORD") + + docker run --rm --network bykstack -v `pwd`/DSL/Liquibase/changelog:/liquibase/changelog -v `pwd`/DSL/Liquibase/master.yml:/liquibase/master.yml -v `pwd`/DSL/Liquibase/data:/liquibase/data liquibase/liquibase --defaultsFile=/liquibase/changelog/liquibase.properties --changelog-file=master.yml --url=jdbc:postgresql://users_db:5432/classifier?user=root --password=$DB_PASSWORD update From cb0ab4cc7b6e8a70db85bae12269966cce7f824b Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Thu, 4 Jul 2024 13:27:07 +0530 Subject: [PATCH 068/582] classifier-52 implement user login flow and user management flow --- .../classifier-script-v1-user-management.sql | 32 +++++++ .../classifier-script-v2-authority-data.xml | 17 ++++ ... => classifier-script-v3-integrations.sql} | 9 +- .../classifier-script-v4-configuration.sql | 15 +++ DSL/Liquibase/data/authority.csv | 3 + DSL/Resql/get-configuration.sql | 5 + DSL/Resql/get-user-role.sql | 10 ++ DSL/Resql/get-user-with-roles.sql | 15 +++ DSL/Resql/get-user.sql | 5 + DSL/Resql/insert-user-role.sql | 2 + DSL/Resql/insert-user.sql | 2 + DSL/Resql/update-user.sql | 16 ++++ DSL/Ruuter.private/DSL/GET/.guard | 28 ++++++ .../DSL/GET/accounts/logout.yml | 32 +------ .../DSL/GET/accounts/user-role.yml | 4 +- .../DSL/GET/auth/jwt/userinfo.yml | 2 +- DSL/Ruuter.private/DSL/POST/.guard | 28 ++++++ DSL/Ruuter.private/DSL/POST/accounts/add.yml | 92 ++++++++++++++++++ DSL/Ruuter.private/DSL/POST/accounts/edit.yml | 94 +++++++++++++++++++ .../DSL/POST/accounts/exists.yml | 40 ++++++++ DSL/Ruuter.private/DSL/POST/auth/.guard | 4 + .../DSL/POST/auth}/login.yml | 35 +------ .../DSL/TEMPLATES/check-user-authority.yml | 50 ++++++++++ 23 files changed, 472 insertions(+), 68 deletions(-) create mode 100644 DSL/Liquibase/changelog/classifier-script-v1-user-management.sql create mode 100644 DSL/Liquibase/changelog/classifier-script-v2-authority-data.xml rename DSL/Liquibase/changelog/{create-initial-tables.sql => classifier-script-v3-integrations.sql} (79%) create mode 100644 DSL/Liquibase/changelog/classifier-script-v4-configuration.sql create mode 100644 DSL/Liquibase/data/authority.csv create mode 100644 DSL/Resql/get-configuration.sql create mode 100644 DSL/Resql/get-user-role.sql create mode 100644 DSL/Resql/get-user-with-roles.sql create mode 100644 DSL/Resql/get-user.sql create mode 100644 DSL/Resql/insert-user-role.sql create mode 100644 DSL/Resql/insert-user.sql create mode 100644 DSL/Resql/update-user.sql create mode 100644 DSL/Ruuter.private/DSL/GET/.guard create mode 100644 DSL/Ruuter.private/DSL/POST/.guard create mode 100644 DSL/Ruuter.private/DSL/POST/accounts/add.yml create mode 100644 DSL/Ruuter.private/DSL/POST/accounts/edit.yml create mode 100644 DSL/Ruuter.private/DSL/POST/accounts/exists.yml create mode 100644 DSL/Ruuter.private/DSL/POST/auth/.guard rename DSL/{Ruuter.public/DSL/POST => Ruuter.private/DSL/POST/auth}/login.yml (63%) create mode 100644 DSL/Ruuter.private/DSL/TEMPLATES/check-user-authority.yml diff --git a/DSL/Liquibase/changelog/classifier-script-v1-user-management.sql b/DSL/Liquibase/changelog/classifier-script-v1-user-management.sql new file mode 100644 index 00000000..84d679be --- /dev/null +++ b/DSL/Liquibase/changelog/classifier-script-v1-user-management.sql @@ -0,0 +1,32 @@ +-- liquibase formatted sql + +-- changeset kalsara Magamage:classifier-script-v1-changeset1 +CREATE TYPE user_status AS ENUM ('active','deleted'); + +-- changeset kalsara Magamage:classifier-script-v1-changeset2 +CREATE TABLE public."user" ( + id int8 NOT NULL GENERATED BY DEFAULT AS IDENTITY, + login VARCHAR(50) NOT NULL, + password_hash VARCHAR(60), + first_name VARCHAR(50), + last_name VARCHAR(50), + id_code VARCHAR(50) NOT NULL, + display_name VARCHAR(50), + status user_status, + csa_title VARCHAR, + csa_email VARCHAR, + created TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT user_pkey PRIMARY KEY (id) +); + +CREATE TABLE public."authority" ( + name VARCHAR(50) PRIMARY KEY +); + +CREATE TABLE public."user_authority" ( + id int8 NOT NULL GENERATED BY DEFAULT AS IDENTITY, + user_id VARCHAR(50) NOT NULL, + authority_name VARCHAR[] NOT NULL, + created TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT user_authority_pkey PRIMARY KEY (id) +); \ No newline at end of file diff --git a/DSL/Liquibase/changelog/classifier-script-v2-authority-data.xml b/DSL/Liquibase/changelog/classifier-script-v2-authority-data.xml new file mode 100644 index 00000000..b54c2f3c --- /dev/null +++ b/DSL/Liquibase/changelog/classifier-script-v2-authority-data.xml @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/DSL/Liquibase/changelog/create-initial-tables.sql b/DSL/Liquibase/changelog/classifier-script-v3-integrations.sql similarity index 79% rename from DSL/Liquibase/changelog/create-initial-tables.sql rename to DSL/Liquibase/changelog/classifier-script-v3-integrations.sql index fd92a334..fb92db69 100644 --- a/DSL/Liquibase/changelog/create-initial-tables.sql +++ b/DSL/Liquibase/changelog/classifier-script-v3-integrations.sql @@ -1,9 +1,9 @@ -- liquibase formatted sql --- changeset kalsara Magamage:classifier-ddl-script-v1-changeset1 +-- changeset kalsara Magamage:classifier-script-v3-changeset1 CREATE TYPE platform AS ENUM ('JIRA', 'OUTLOOK', 'PINAL'); --- changeset kalsara Magamage:classifier-ddl-script-v1-changeset2 +-- changeset kalsara Magamage:classifier-script-v3-changeset2 CREATE TABLE public."integration_status" ( id int8 NOT NULL GENERATED BY DEFAULT AS IDENTITY, platform platform, @@ -13,14 +13,14 @@ CREATE TABLE public."integration_status" ( CONSTRAINT integration_status_pkey PRIMARY KEY (id) ); --- changeset kalsara Magamage:classifier-ddl-script-v1-changeset3 +-- changeset kalsara Magamage:classifier-script-v3-changeset3 INSERT INTO public."integration_status" (platform, is_connect, subscription_id, token) VALUES ('JIRA', FALSE, NULL, NULL), ('OUTLOOK', FALSE, NULL, NULL), ('PINAL', FALSE, NULL, NULL); --- changeset kalsara Magamage:classifier-ddl-script-v1-changeset4 +-- changeset kalsara Magamage:classifier-script-v3-changeset4 CREATE TABLE public."jira" ( id int8 NOT NULL GENERATED BY DEFAULT AS IDENTITY, input_id TEXT DEFAULT NULL, @@ -31,7 +31,6 @@ CREATE TABLE public."jira" ( CONSTRAINT jira_pkey PRIMARY KEY (id) ); --- changeset kalsara Magamage:classifier-ddl-script-v1-changeset6 CREATE TABLE public."outlook" ( id int8 NOT NULL GENERATED BY DEFAULT AS IDENTITY, input_id TEXT DEFAULT NULL, diff --git a/DSL/Liquibase/changelog/classifier-script-v4-configuration.sql b/DSL/Liquibase/changelog/classifier-script-v4-configuration.sql new file mode 100644 index 00000000..2a2325d0 --- /dev/null +++ b/DSL/Liquibase/changelog/classifier-script-v4-configuration.sql @@ -0,0 +1,15 @@ +-- liquibase formatted sql + +-- changeset kalsara Magamage:classifier-script-v4-changeset1 +CREATE TABLE public.configuration ( + id BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, + key VARCHAR(128), + value VARCHAR(128), + created TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + deleted BOOLEAN NOT NULL DEFAULT FALSE, + CONSTRAINT configuration_pkey PRIMARY KEY (id) +); + +-- changeset kalsara Magamage:classifier-script-v4-changeset2 +INSERT INTO public.configuration (key, value) +VALUES ('session_length', '120'); diff --git a/DSL/Liquibase/data/authority.csv b/DSL/Liquibase/data/authority.csv new file mode 100644 index 00000000..c110c607 --- /dev/null +++ b/DSL/Liquibase/data/authority.csv @@ -0,0 +1,3 @@ +name +ROLE_ADMINISTRATOR +ROLE_MODEL_TRAINER diff --git a/DSL/Resql/get-configuration.sql b/DSL/Resql/get-configuration.sql new file mode 100644 index 00000000..f03b322e --- /dev/null +++ b/DSL/Resql/get-configuration.sql @@ -0,0 +1,5 @@ +SELECT id, key, value +FROM configuration +WHERE key=:key +AND id IN (SELECT max(id) from configuration GROUP BY key) +AND NOT deleted; diff --git a/DSL/Resql/get-user-role.sql b/DSL/Resql/get-user-role.sql new file mode 100644 index 00000000..39a51f4e --- /dev/null +++ b/DSL/Resql/get-user-role.sql @@ -0,0 +1,10 @@ +SELECT ua.authority_name AS authorities +FROM "user" u + INNER JOIN (SELECT authority_name, user_id + FROM user_authority AS ua + WHERE ua.id IN (SELECT max(id) + FROM user_authority + GROUP BY user_id)) ua ON u.id_code = ua.user_id +WHERE u.id_code = :userIdCode + AND status <> 'deleted' + AND id IN (SELECT max(id) FROM "user" WHERE id_code = :userIdCode) diff --git a/DSL/Resql/get-user-with-roles.sql b/DSL/Resql/get-user-with-roles.sql new file mode 100644 index 00000000..8ef5044c --- /dev/null +++ b/DSL/Resql/get-user-with-roles.sql @@ -0,0 +1,15 @@ +SELECT DISTINCT u.login, + u.first_name, + u.last_name, + u.id_code, + u.display_name, + u.csa_title, + u.csa_email, + ua.authority_name AS authorities +FROM "user" u + LEFT JOIN (SELECT authority_name, user_id + FROM user_authority AS ua + WHERE ua.id IN (SELECT max(id) + FROM user_authority + GROUP BY user_id)) ua ON u.id_code = ua.user_id +WHERE login = :login; diff --git a/DSL/Resql/get-user.sql b/DSL/Resql/get-user.sql new file mode 100644 index 00000000..18bef7ff --- /dev/null +++ b/DSL/Resql/get-user.sql @@ -0,0 +1,5 @@ +SELECT id_code +FROM "user" +WHERE id_code = :userIdCode + AND status <> 'deleted' + AND id IN (SELECT max(id) FROM "user" WHERE id_code = :userIdCode) \ No newline at end of file diff --git a/DSL/Resql/insert-user-role.sql b/DSL/Resql/insert-user-role.sql new file mode 100644 index 00000000..e2bfe3b4 --- /dev/null +++ b/DSL/Resql/insert-user-role.sql @@ -0,0 +1,2 @@ +INSERT INTO user_authority (user_id, authority_name, created) +VALUES (:userIdCode, ARRAY [ :roles ], :created::timestamp with time zone); \ No newline at end of file diff --git a/DSL/Resql/insert-user.sql b/DSL/Resql/insert-user.sql new file mode 100644 index 00000000..0fd7c12b --- /dev/null +++ b/DSL/Resql/insert-user.sql @@ -0,0 +1,2 @@ +INSERT INTO "user" (login, first_name, last_name, display_name, password_hash, id_code, status, created, csa_title, csa_email) +VALUES (:userIdCode, :firstName, :lastName, :displayName, :displayName, :userIdCode, (:status)::user_status, :created::timestamp with time zone, :csaTitle, :csaEmail); diff --git a/DSL/Resql/update-user.sql b/DSL/Resql/update-user.sql new file mode 100644 index 00000000..688e8df7 --- /dev/null +++ b/DSL/Resql/update-user.sql @@ -0,0 +1,16 @@ +INSERT INTO "user" (id_code, login, password_hash, first_name, last_name, display_name, status, created, csa_title, csa_email) +SELECT + :userIdCode, + login, + password_hash, + :firstName, + :lastName, + :displayName, + :status::user_status, + :created::timestamp with time zone, + :csaTitle, + :csaEmail +FROM "user" +WHERE id = ( + SELECT MAX(id) FROM "user" WHERE id_code = :userIdCode +); diff --git a/DSL/Ruuter.private/DSL/GET/.guard b/DSL/Ruuter.private/DSL/GET/.guard new file mode 100644 index 00000000..faac86f6 --- /dev/null +++ b/DSL/Ruuter.private/DSL/GET/.guard @@ -0,0 +1,28 @@ +check_for_cookie: + switch: + - condition: ${incoming.headers == null || incoming.headers.cookie == null} + next: guard_fail + next: authenticate + +authenticate: + template: check-user-authority + requestType: templates + headers: + cookie: ${incoming.headers.cookie} + result: authority_result + +check_authority_result: + switch: + - condition: ${authority_result !== "false"} + next: guard_success + next: guard_fail + +guard_success: + return: "success" + status: 200 + next: end + +guard_fail: + return: "unauthorized" + status: 200 + next: end diff --git a/DSL/Ruuter.private/DSL/GET/accounts/logout.yml b/DSL/Ruuter.private/DSL/GET/accounts/logout.yml index d87f5f77..d3065c5d 100644 --- a/DSL/Ruuter.private/DSL/GET/accounts/logout.yml +++ b/DSL/Ruuter.private/DSL/GET/accounts/logout.yml @@ -15,7 +15,7 @@ declaration: get_user_info: call: http.post args: - url: "[#CHATBOT_TIM]/jwt/custom-jwt-userinfo" + url: "[#CLASSIFIER_TIM]/jwt/custom-jwt-userinfo" contentType: plaintext headers: cookie: ${incoming.headers.cookie} @@ -26,39 +26,13 @@ get_user_info: check_user_info_response: switch: - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} - next: assignIdCode + next: blacklistCustomJwt next: return_bad_request -assignIdCode: - assign: - idCode: ${res.response.body.idCode} - next: unassignAllClaimedChats - -unassignAllClaimedChats: - call: http.post - args: - url: "[#CHATBOT_RESQL]/update-chats-assignee-by-user-id" - body: - userId: ${idCode} - result: unclaim_res - next: setCustomerSupportAgentAway - -setCustomerSupportAgentAway: - call: http.post - args: - url: "[#CHATBOT_RESQL]/set-customer-support-status" - body: - userIdCode: ${idCode} - active: false - status: "offline" - created: ${new Date().toISOString()} - result: set_status_res - next: blacklistCustomJwt - blacklistCustomJwt: call: http.post args: - url: "[#CHATBOT_TIM]/jwt/custom-jwt-blacklist" + url: "[#CLASSIFIER_TIM]/jwt/custom-jwt-blacklist" contentType: plaintext headers: cookie: ${incoming.headers.cookie} diff --git a/DSL/Ruuter.private/DSL/GET/accounts/user-role.yml b/DSL/Ruuter.private/DSL/GET/accounts/user-role.yml index 39f27ca4..c46876c7 100644 --- a/DSL/Ruuter.private/DSL/GET/accounts/user-role.yml +++ b/DSL/Ruuter.private/DSL/GET/accounts/user-role.yml @@ -15,7 +15,7 @@ declaration: get_user_info: call: http.post args: - url: "[#CHATBOT_TIM]/jwt/custom-jwt-userinfo" + url: "[#CLASSIFIER_TIM]/jwt/custom-jwt-userinfo" contentType: plaintext headers: cookie: ${incoming.headers.cookie} @@ -38,7 +38,7 @@ assignIdCode: getUserRole: call: http.post args: - url: "[#CHATBOT_RESQL]/get-user-role" + url: "[#CLASSIFIER_RESQL]/get-user-role" body: userIdCode: ${idCode} result: roles_res diff --git a/DSL/Ruuter.private/DSL/GET/auth/jwt/userinfo.yml b/DSL/Ruuter.private/DSL/GET/auth/jwt/userinfo.yml index 1e23b693..509b4e88 100644 --- a/DSL/Ruuter.private/DSL/GET/auth/jwt/userinfo.yml +++ b/DSL/Ruuter.private/DSL/GET/auth/jwt/userinfo.yml @@ -15,7 +15,7 @@ declaration: get_user_info: call: http.post args: - url: "[#CHATBOT_TIM]/jwt/custom-jwt-userinfo" + url: "[#CLASSIFIER_TIM]/jwt/custom-jwt-userinfo" contentType: plaintext headers: cookie: ${incoming.headers.cookie} diff --git a/DSL/Ruuter.private/DSL/POST/.guard b/DSL/Ruuter.private/DSL/POST/.guard new file mode 100644 index 00000000..faac86f6 --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/.guard @@ -0,0 +1,28 @@ +check_for_cookie: + switch: + - condition: ${incoming.headers == null || incoming.headers.cookie == null} + next: guard_fail + next: authenticate + +authenticate: + template: check-user-authority + requestType: templates + headers: + cookie: ${incoming.headers.cookie} + result: authority_result + +check_authority_result: + switch: + - condition: ${authority_result !== "false"} + next: guard_success + next: guard_fail + +guard_success: + return: "success" + status: 200 + next: end + +guard_fail: + return: "unauthorized" + status: 200 + next: end diff --git a/DSL/Ruuter.private/DSL/POST/accounts/add.yml b/DSL/Ruuter.private/DSL/POST/accounts/add.yml new file mode 100644 index 00000000..59c7af52 --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/accounts/add.yml @@ -0,0 +1,92 @@ +declaration: + call: declare + version: 0.1 + description: "Decription placeholder for 'ADD'" + method: post + accepts: json + returns: json + namespace: backoffice + allowlist: + body: + - field: csaTitle + type: string + description: "Body field 'csaTitle'" + - field: csa_email + type: string + description: "Body field 'csa_email'" + - field: displayName + type: string + description: "Body field 'displayName'" + - field: firstName + type: string + description: "Body field 'firstName'" + - field: lastName + type: string + description: "Body field 'lastName'" + - field: roles + type: array + description: "Body field 'roles'" + - field: userIdCode + type: string + description: "Body field 'userIdCode'" + +extractRequestData: + assign: + firstName: ${incoming.body.firstName} + lastName: ${incoming.body.lastName} + userIdCode: ${incoming.body.userIdCode} + displayName: ${incoming.body.displayName} + csaTitle: ${incoming.body.csaTitle} + csa_email: ${incoming.body.csa_email} + roles: ${incoming.body.roles} + +getUser: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-user" + body: + userIdCode: ${userIdCode} + result: res + next: checkIfUserExists + +checkIfUserExists: + switch: + - condition: "${res.response.body.length > 0}" + next: return_exists + next: addUser + +addUser: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/insert-user" + body: + created: ${new Date().toISOString()} + status: "active" + firstName: ${firstName} + lastName: ${lastName} + userIdCode: ${userIdCode} + displayName: ${displayName} + csaTitle: ${csaTitle} + csaEmail: ${csa_email} + result: add_user_res + next: addRoles + +addRoles: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/insert-user-role" + body: + userIdCode: ${userIdCode} + roles: ${roles} + created: ${new Date().toISOString()} + result: add_roles_res + next: return_result + +return_result: + return: "User added successfully" + next: end + +return_exists: + return: "error: user already exists" + status: 400 + next: end diff --git a/DSL/Ruuter.private/DSL/POST/accounts/edit.yml b/DSL/Ruuter.private/DSL/POST/accounts/edit.yml new file mode 100644 index 00000000..e64d93ef --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/accounts/edit.yml @@ -0,0 +1,94 @@ +declaration: + call: declare + version: 0.1 + description: "Decription placeholder for 'EDIT'" + method: post + accepts: json + returns: json + namespace: backoffice + allowlist: + body: + - field: csaTitle + type: string + description: "Body field 'csaTitle'" + - field: csa_email + type: string + description: "Body field 'csa_email'" + - field: displayName + type: string + description: "Body field 'displayName'" + - field: firstName + type: string + description: "Body field 'firstName'" + - field: lastName + type: string + description: "Body field 'lastName'" + - field: roles + type: array + description: "Body field 'roles'" + - field: userIdCode + type: string + description: "Body field 'userIdCode'" + +extractRequestData: + assign: + firstName: ${incoming.body.firstName} + lastName: ${incoming.body.lastName} + userIdCode: ${incoming.body.userIdCode} + displayName: ${incoming.body.displayName} + csaTitle: ${incoming.body.csaTitle} + csa_email: ${incoming.body.csa_email} + roles: ${incoming.body.roles} + +getUser: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-user" + body: + userIdCode: ${userIdCode} + result: res + next: checkIfUserExists + +checkIfUserExists: + switch: + - condition: "${res.response.body.length > 0}" + next: updateUser + next: return_not_exists + +updateUser: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/update-user" + body: + created: ${new Date().toISOString()} + status: "active" + firstName: ${firstName} + lastName: ${lastName} + userIdCode: ${userIdCode} + displayName: ${displayName} + csaTitle: ${csaTitle} + csaEmail: ${csa_email} + result: add_user_res + next: updateRoles + +updateRoles: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/insert-user-role" + body: + userIdCode: ${userIdCode} + roles: ${roles} + created: ${new Date().toISOString()} + result: add_roles_res + next: return_result + +return_result: + return: "User updated successfully" + status: 200 + next: end + +return_not_exists: + return: "error: user does not exist" + status: 400 + next: end + diff --git a/DSL/Ruuter.private/DSL/POST/accounts/exists.yml b/DSL/Ruuter.private/DSL/POST/accounts/exists.yml new file mode 100644 index 00000000..1252ad7a --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/accounts/exists.yml @@ -0,0 +1,40 @@ +declaration: + call: declare + version: 0.1 + description: "Decription placeholder for 'EXISTS'" + method: post + accepts: json + returns: json + namespace: backoffice + allowlist: + body: + - field: userIdCode + type: string + description: "Body field 'userIdCode'" + +extractRequestData: + assign: + userId: ${incoming.body.userIdCode} + +getUser: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-user" + body: + userIdCode: ${userId} + result: res + next: checkIfUserExists + +checkIfUserExists: + switch: + - condition: "${res.response.body.length > 0}" + next: return_exists + next: return_not_exists + +return_exists: + return: "true" + next: end + +return_not_exists: + return: "false" + next: end diff --git a/DSL/Ruuter.private/DSL/POST/auth/.guard b/DSL/Ruuter.private/DSL/POST/auth/.guard new file mode 100644 index 00000000..64435377 --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/auth/.guard @@ -0,0 +1,4 @@ +guard_allow_all: + return: "success" + status: 200 + next: end diff --git a/DSL/Ruuter.public/DSL/POST/login.yml b/DSL/Ruuter.private/DSL/POST/auth/login.yml similarity index 63% rename from DSL/Ruuter.public/DSL/POST/login.yml rename to DSL/Ruuter.private/DSL/POST/auth/login.yml index 15cc7462..e2b9d3d3 100644 --- a/DSL/Ruuter.public/DSL/POST/login.yml +++ b/DSL/Ruuter.private/DSL/POST/auth/login.yml @@ -12,19 +12,9 @@ declaration: type: string description: "Body field 'login'" - field: password - type: stringDSL/Ruuter.public/DSL/POST/login.yml + type: string description: "Body field 'password'" -getIsPasswordAuthEnabled: - assign: - isPasswordAuthEnabled: "[#PASSWORD_AUTH_ENABLED]" - -checkPasswordLoginEnabled: - switch: - - condition: ${isPasswordAuthEnabled === true || isPasswordAuthEnabled.toLowerCase() === "true"} - next: extractRequestData - next: return_password_login_disabled - extractRequestData: assign: login: ${incoming.body.login} @@ -34,7 +24,7 @@ extractRequestData: getUserWithRole: call: http.post args: - url: "[#CHATBOT_RESQL]/get-user-with-roles" + url: "[#CLASSIFIER_RESQL]/get-user-with-roles" body: login: ${login} password: ${password} @@ -50,7 +40,7 @@ check_user_result: get_session_length: call: http.post args: - url: "[#CHATBOT_RESQL]/get-configuration" + url: "[#CLASSIFIER_RESQL]/get-configuration" body: key: "session_length" result: session_result @@ -59,7 +49,7 @@ get_session_length: generate_cookie: call: http.post args: - url: "[#CHATBOT_TIM]/jwt/custom-jwt-generate" + url: "[#CLASSIFIER_TIM]/jwt/custom-jwt-generate" body: JWTName: "customJwtCookie" expirationInMinutes: ${session_result.response.body[0]?.value ?? '120'} @@ -75,18 +65,6 @@ assign_cookie: Secure: true HttpOnly: true SameSite: "Lax" - next: setCustomerSupportAgentAway - -setCustomerSupportAgentAway: - call: http.post - args: - url: "[#CHATBOT_RESQL]/set-customer-support-status" - body: - active: false - userIdCode: ${login} - created: ${new Date().toISOString()} - status: "offline" - result: setCustomerSupportAgentAwayResult next: return_value return_value: @@ -99,8 +77,3 @@ return_user_not_found: status: 400 return: "User Not Found" next: end - -return_password_login_disabled: - status: 400 - return: "Password login is disabled" - next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/TEMPLATES/check-user-authority.yml b/DSL/Ruuter.private/DSL/TEMPLATES/check-user-authority.yml new file mode 100644 index 00000000..d3e1d5f5 --- /dev/null +++ b/DSL/Ruuter.private/DSL/TEMPLATES/check-user-authority.yml @@ -0,0 +1,50 @@ +declaration: + call: declare + version: 0.1 + description: "Decription placeholder for 'CHECK-USER-AUTHORITY'" + method: post + accepts: json + returns: json + namespace: analytics + allowlist: + headers: + - field: cookie + type: string + description: "Cookie field" + +get_cookie_info: + call: http.post + args: + url: "[#CLASSIFIER_TIM]/jwt/custom-jwt-userinfo" + contentType: plaintext + headers: + cookie: ${incoming.headers.cookie} + plaintext: "customJwtCookie" + result: res + next: check_cookie_info_response + +check_cookie_info_response: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: check_user_authority + next: return_bad_request + +check_user_authority: + switch: + - condition: ${res.response.body.authorities.includes("ROLE_ADMINISTRATOR") || res.response.body.authorities.includes("ROLE_ANALYST")} + next: return_authorized + next: return_unauthorized + +return_authorized: + return: ${res.response.body} + next: end + +return_unauthorized: + status: 200 + return: false + next: end + +return_bad_request: + status: 400 + return: false + next: end From e130c3d8c5f95e8590da7b5b37babfea1938dac1 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Thu, 4 Jul 2024 16:49:45 +0530 Subject: [PATCH 069/582] classifier-52 implement user delete flow --- .../classifier-script-v4-configuration.sql | 2 +- DSL/Resql/delete-user.sql | 18 ++++++++++++ .../DSL/POST/accounts/delete.yml | 29 +++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 DSL/Resql/delete-user.sql create mode 100644 DSL/Ruuter.private/DSL/POST/accounts/delete.yml diff --git a/DSL/Liquibase/changelog/classifier-script-v4-configuration.sql b/DSL/Liquibase/changelog/classifier-script-v4-configuration.sql index 2a2325d0..8b3ab1be 100644 --- a/DSL/Liquibase/changelog/classifier-script-v4-configuration.sql +++ b/DSL/Liquibase/changelog/classifier-script-v4-configuration.sql @@ -2,7 +2,7 @@ -- changeset kalsara Magamage:classifier-script-v4-changeset1 CREATE TABLE public.configuration ( - id BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, + id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY, key VARCHAR(128), value VARCHAR(128), created TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, diff --git a/DSL/Resql/delete-user.sql b/DSL/Resql/delete-user.sql new file mode 100644 index 00000000..8910d75d --- /dev/null +++ b/DSL/Resql/delete-user.sql @@ -0,0 +1,18 @@ +delete_user AS ( +INSERT +INTO "user" (login, password_hash, first_name, last_name, id_code, display_name, status, created, csa_title, csa_email) +SELECT login, + password_hash, + first_name, + last_name, + id_code, + display_name, + 'deleted', + :created::timestamp with time zone, + csa_title, + csa_email +FROM "user" +WHERE id_code = :userIdCode + AND status <> 'deleted' + AND id IN (SELECT max(id) FROM "user" WHERE id_code = :userIdCode)) +SELECT max(status) FROM "user" WHERE id_code = :userIdCode; diff --git a/DSL/Ruuter.private/DSL/POST/accounts/delete.yml b/DSL/Ruuter.private/DSL/POST/accounts/delete.yml new file mode 100644 index 00000000..18561a3f --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/accounts/delete.yml @@ -0,0 +1,29 @@ +declaration: + call: declare + version: 0.1 + description: "Decription placeholder for 'DELETE'" + method: post + accepts: json + returns: json + namespace: backoffice + allowlist: + body: + - field: userIdCode + type: string + description: "Body field 'userIdCode'" + +extractRequestData: + assign: + userId: ${incoming.body.userIdCode} + +setConfigurationValue: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/delete-user" + body: + userIdCode: ${userId} + created: ${new Date().toISOString()} + result: res + +return_result: + return: ${res.response.body} From 95576c1de2527cc25cce38cf2d163ad437871515 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Thu, 4 Jul 2024 16:55:11 +0530 Subject: [PATCH 070/582] classifier-52 refactor namespace name in uml files --- DSL/Ruuter.private/DSL/GET/auth/jwt/userinfo.yml | 2 +- DSL/Ruuter.private/DSL/POST/accounts/add.yml | 2 +- DSL/Ruuter.private/DSL/POST/accounts/delete.yml | 2 +- DSL/Ruuter.private/DSL/POST/accounts/edit.yml | 2 +- DSL/Ruuter.private/DSL/POST/accounts/exists.yml | 2 +- DSL/Ruuter.private/DSL/POST/auth/login.yml | 2 +- DSL/Ruuter.private/DSL/TEMPLATES/check-user-authority.yml | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/DSL/Ruuter.private/DSL/GET/auth/jwt/userinfo.yml b/DSL/Ruuter.private/DSL/GET/auth/jwt/userinfo.yml index 509b4e88..0e785ffa 100644 --- a/DSL/Ruuter.private/DSL/GET/auth/jwt/userinfo.yml +++ b/DSL/Ruuter.private/DSL/GET/auth/jwt/userinfo.yml @@ -5,7 +5,7 @@ declaration: method: get accepts: json returns: json - namespace: backoffice + namespace: classifier allowlist: headers: - field: cookie diff --git a/DSL/Ruuter.private/DSL/POST/accounts/add.yml b/DSL/Ruuter.private/DSL/POST/accounts/add.yml index 59c7af52..7467332d 100644 --- a/DSL/Ruuter.private/DSL/POST/accounts/add.yml +++ b/DSL/Ruuter.private/DSL/POST/accounts/add.yml @@ -5,7 +5,7 @@ declaration: method: post accepts: json returns: json - namespace: backoffice + namespace: classifier allowlist: body: - field: csaTitle diff --git a/DSL/Ruuter.private/DSL/POST/accounts/delete.yml b/DSL/Ruuter.private/DSL/POST/accounts/delete.yml index 18561a3f..92885948 100644 --- a/DSL/Ruuter.private/DSL/POST/accounts/delete.yml +++ b/DSL/Ruuter.private/DSL/POST/accounts/delete.yml @@ -5,7 +5,7 @@ declaration: method: post accepts: json returns: json - namespace: backoffice + namespace: classifier allowlist: body: - field: userIdCode diff --git a/DSL/Ruuter.private/DSL/POST/accounts/edit.yml b/DSL/Ruuter.private/DSL/POST/accounts/edit.yml index e64d93ef..b88c2877 100644 --- a/DSL/Ruuter.private/DSL/POST/accounts/edit.yml +++ b/DSL/Ruuter.private/DSL/POST/accounts/edit.yml @@ -5,7 +5,7 @@ declaration: method: post accepts: json returns: json - namespace: backoffice + namespace: classifier allowlist: body: - field: csaTitle diff --git a/DSL/Ruuter.private/DSL/POST/accounts/exists.yml b/DSL/Ruuter.private/DSL/POST/accounts/exists.yml index 1252ad7a..931660e4 100644 --- a/DSL/Ruuter.private/DSL/POST/accounts/exists.yml +++ b/DSL/Ruuter.private/DSL/POST/accounts/exists.yml @@ -5,7 +5,7 @@ declaration: method: post accepts: json returns: json - namespace: backoffice + namespace: classifier allowlist: body: - field: userIdCode diff --git a/DSL/Ruuter.private/DSL/POST/auth/login.yml b/DSL/Ruuter.private/DSL/POST/auth/login.yml index e2b9d3d3..d63682d2 100644 --- a/DSL/Ruuter.private/DSL/POST/auth/login.yml +++ b/DSL/Ruuter.private/DSL/POST/auth/login.yml @@ -5,7 +5,7 @@ declaration: method: post accepts: json returns: json - namespace: backoffice + namespace: classifier allowlist: body: - field: login diff --git a/DSL/Ruuter.private/DSL/TEMPLATES/check-user-authority.yml b/DSL/Ruuter.private/DSL/TEMPLATES/check-user-authority.yml index d3e1d5f5..3baee630 100644 --- a/DSL/Ruuter.private/DSL/TEMPLATES/check-user-authority.yml +++ b/DSL/Ruuter.private/DSL/TEMPLATES/check-user-authority.yml @@ -5,7 +5,7 @@ declaration: method: post accepts: json returns: json - namespace: analytics + namespace: classifier allowlist: headers: - field: cookie From 4faf0f33c31c4d7ba247332e7152ff7e70c64cf1 Mon Sep 17 00:00:00 2001 From: Pamoda Dilranga <51948729+pamodaDilranga@users.noreply.github.com> Date: Fri, 5 Jul 2024 12:12:20 +0530 Subject: [PATCH 071/582] Update App.tsx Fixing the typo --- GUI/src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GUI/src/App.tsx b/GUI/src/App.tsx index 02b8dd31..69c7863d 100644 --- a/GUI/src/App.tsx +++ b/GUI/src/App.tsx @@ -9,7 +9,7 @@ import { UserInfo } from 'types/userInfo'; import './locale/et_EE'; import UserManagement from 'pages/UserManagement'; import Integrations from 'pages/Integrations'; -import DatasetGroups from 'pages/DataSetGroups'; +import DatasetGroups from 'pages/DatasetGroups'; const App: FC = () => { From dfddb7149229c5bbe0e8c05d2042af5b40c96c00 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Fri, 5 Jul 2024 13:23:48 +0530 Subject: [PATCH 072/582] user-authority: insert new user and change authority in Template --- .../classifier-script-v1-user-management.sql | 11 ++++++++++- .../DSL/TEMPLATES/check-user-authority.yml | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/DSL/Liquibase/changelog/classifier-script-v1-user-management.sql b/DSL/Liquibase/changelog/classifier-script-v1-user-management.sql index 84d679be..91d4cfac 100644 --- a/DSL/Liquibase/changelog/classifier-script-v1-user-management.sql +++ b/DSL/Liquibase/changelog/classifier-script-v1-user-management.sql @@ -29,4 +29,13 @@ CREATE TABLE public."user_authority" ( authority_name VARCHAR[] NOT NULL, created TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, CONSTRAINT user_authority_pkey PRIMARY KEY (id) -); \ No newline at end of file +); + +-- changeset kalsara Magamage:classifier-script-v1-changeset3 + +INSERT INTO public."user" (login,password_hash,first_name,last_name,id_code,display_name,status,csa_title,csa_email) +VALUES ('EE30303039914','ok','classifier','test','EE30303039914','classifier','active','Title','classifier.doe@example.com'); + +INSERT INTO public."user_authority" ( user_id, authority_name) +VALUES ('EE30303039914', ARRAY['ROLE_ADMINISTRATOR', 'ROLE_MODEL_TRAINER'] ); + diff --git a/DSL/Ruuter.private/DSL/TEMPLATES/check-user-authority.yml b/DSL/Ruuter.private/DSL/TEMPLATES/check-user-authority.yml index 3baee630..ecc54729 100644 --- a/DSL/Ruuter.private/DSL/TEMPLATES/check-user-authority.yml +++ b/DSL/Ruuter.private/DSL/TEMPLATES/check-user-authority.yml @@ -31,7 +31,7 @@ check_cookie_info_response: check_user_authority: switch: - - condition: ${res.response.body.authorities.includes("ROLE_ADMINISTRATOR") || res.response.body.authorities.includes("ROLE_ANALYST")} + - condition: ${res.response.body.authorities.includes("ROLE_ADMINISTRATOR") || res.response.body.authorities.includes("ROLE_MODEL_TRAINER")} next: return_authorized next: return_unauthorized From 9cac87cb17de96f15668920308510570dfca0578 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 5 Jul 2024 15:40:24 +0530 Subject: [PATCH 073/582] pipeline updates --- .github/workflows/est-frontend-dev.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/est-frontend-dev.yml b/.github/workflows/est-frontend-dev.yml index 6b44a50b..70e7373f 100644 --- a/.github/workflows/est-frontend-dev.yml +++ b/.github/workflows/est-frontend-dev.yml @@ -7,7 +7,7 @@ on: jobs: deploy: - runs-on: [self-hosted, est-cls-dev] + runs-on: [self-hosted, dev] steps: - name: Checkout code @@ -17,8 +17,10 @@ jobs: run: | docker stop $(docker ps -a -q) || true docker rm $(docker ps -a -q) || true - docker rmi $(docker images -q) || true - docker system prune -af + images_to_keep="authentication-layer|tim|data-mapper|resql|ruuter" + docker images --format "{{.Repository}}:{{.Tag}}" | grep -Ev "$images_to_keep" | xargs -r docker rmi || true + docker volume prune -f + docker network prune -f - name: Build and run Docker Compose run: | From dba0d681d98bcfb6f04decce4c2a8aa372b5d686 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 5 Jul 2024 16:02:23 +0530 Subject: [PATCH 074/582] forece clean workflow --- .github/workflows/est-frontend-dev.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/est-frontend-dev.yml b/.github/workflows/est-frontend-dev.yml index 70e7373f..23b8ee0f 100644 --- a/.github/workflows/est-frontend-dev.yml +++ b/.github/workflows/est-frontend-dev.yml @@ -12,6 +12,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v3 + with: + clean: true - name: Remove all running containers, images, and prune Docker system run: | From 2e2341ed9111be4df18dda8e01afc6d482b7fa7e Mon Sep 17 00:00:00 2001 From: Pamoda Dilranga <51948729+pamodaDilranga@users.noreply.github.com> Date: Fri, 5 Jul 2024 16:09:10 +0530 Subject: [PATCH 075/582] Update est-frontend-dev.yml --- .github/workflows/est-frontend-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/est-frontend-dev.yml b/.github/workflows/est-frontend-dev.yml index 23b8ee0f..4a231a13 100644 --- a/.github/workflows/est-frontend-dev.yml +++ b/.github/workflows/est-frontend-dev.yml @@ -40,5 +40,5 @@ jobs: PUBLIC_IP: ${{ env.PUBLIC_IP }} run: | curl -X POST -H 'Content-type: application/json' --data "{ - \"text\": \"Build is complete. Public IP: $PUBLIC_IP\" + \"text\": \"Build is complete. You can access it with Public IP: $PUBLIC_IP\" }" $SLACK_WEBHOOK_URL From 1bfc4477b1d9b1b7b0adb1a3e077a59f216e2f35 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 5 Jul 2024 16:12:18 +0530 Subject: [PATCH 076/582] remove the past files --- .github/workflows/est-frontend-dev.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/est-frontend-dev.yml b/.github/workflows/est-frontend-dev.yml index 23b8ee0f..446cba6c 100644 --- a/.github/workflows/est-frontend-dev.yml +++ b/.github/workflows/est-frontend-dev.yml @@ -10,6 +10,15 @@ jobs: runs-on: [self-hosted, dev] steps: + - name: Set permissions for workspace directory + run: | + sudo chown -R ubuntu:ubuntu /home/ubuntu/actions-runner/_work/classifier/classifier + sudo chmod -R u+rwx /home/ubuntu/actions-runner/_work/classifier/classifier + + - name: Clean up workspace + run: | + sudo rm -rf /home/ubuntu/actions-runner/_work/classifier/classifier/* + - name: Checkout code uses: actions/checkout@v3 with: From fde0284bc7406ada53e49fc95411f00fd4e0dfe5 Mon Sep 17 00:00:00 2001 From: Pamoda Dilranga <51948729+pamodaDilranga@users.noreply.github.com> Date: Fri, 5 Jul 2024 16:15:36 +0530 Subject: [PATCH 077/582] Update est-frontend-dev.yml --- .github/workflows/est-frontend-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/est-frontend-dev.yml b/.github/workflows/est-frontend-dev.yml index 725d3e36..446cba6c 100644 --- a/.github/workflows/est-frontend-dev.yml +++ b/.github/workflows/est-frontend-dev.yml @@ -49,5 +49,5 @@ jobs: PUBLIC_IP: ${{ env.PUBLIC_IP }} run: | curl -X POST -H 'Content-type: application/json' --data "{ - \"text\": \"Build is complete. You can access it with Public IP: $PUBLIC_IP\" + \"text\": \"Build is complete. Public IP: $PUBLIC_IP\" }" $SLACK_WEBHOOK_URL From 4184fd65f162757e81f0542a71bd86f66de2b616 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Fri, 5 Jul 2024 16:43:50 +0530 Subject: [PATCH 078/582] user-authority: add unauthorized status --- DSL/Ruuter.private/DSL/GET/.guard | 2 +- DSL/Ruuter.private/DSL/POST/.guard | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DSL/Ruuter.private/DSL/GET/.guard b/DSL/Ruuter.private/DSL/GET/.guard index faac86f6..9dfc1e3c 100644 --- a/DSL/Ruuter.private/DSL/GET/.guard +++ b/DSL/Ruuter.private/DSL/GET/.guard @@ -24,5 +24,5 @@ guard_success: guard_fail: return: "unauthorized" - status: 200 + status: 400 next: end diff --git a/DSL/Ruuter.private/DSL/POST/.guard b/DSL/Ruuter.private/DSL/POST/.guard index faac86f6..9dfc1e3c 100644 --- a/DSL/Ruuter.private/DSL/POST/.guard +++ b/DSL/Ruuter.private/DSL/POST/.guard @@ -24,5 +24,5 @@ guard_success: guard_fail: return: "unauthorized" - status: 200 + status: 400 next: end From 6158a72c20bfc86a18cfec1bb5b63adc90adcefc Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Fri, 5 Jul 2024 23:38:11 +0530 Subject: [PATCH 079/582] user-authority: refactor file paths and implemnt ruuter public for piblic API request(webbhook API's) and user login.yml --- DSL/Resql/get-platform-integration-status.sql | 2 +- .../classifier/integration/outlook/token.yml | 7 +- DSL/Ruuter.private/DSL/POST/auth/.guard | 4 - DSL/Ruuter.private/DSL/POST/auth/login.yml | 79 ------------------- .../classifier/integration/outlook/label.yml | 6 ++ .../integration/outlook/subscribe.yml | 24 +++--- .../integration/toggle-platform.yml | 16 ++-- DSL/Ruuter.public/DSL/POST/auth/login.yml | 37 ++------- .../integration/jira/cloud/accept.yml | 0 .../classifier/integration/outlook/accept.yml | 0 docker-compose.yml | 20 +++++ 11 files changed, 62 insertions(+), 133 deletions(-) delete mode 100644 DSL/Ruuter.private/DSL/POST/auth/.guard delete mode 100644 DSL/Ruuter.private/DSL/POST/auth/login.yml rename DSL/{Ruuter.private => Ruuter.public}/DSL/POST/classifier/integration/jira/cloud/accept.yml (100%) rename DSL/{Ruuter.private => Ruuter.public}/DSL/POST/classifier/integration/outlook/accept.yml (100%) diff --git a/DSL/Resql/get-platform-integration-status.sql b/DSL/Resql/get-platform-integration-status.sql index 3a35eee3..1ee8d18a 100644 --- a/DSL/Resql/get-platform-integration-status.sql +++ b/DSL/Resql/get-platform-integration-status.sql @@ -1,3 +1,3 @@ -SELECT is_connect +SELECT is_connect, subscription_id FROM integration_status WHERE platform=:platform::platform; \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/GET/classifier/integration/outlook/token.yml b/DSL/Ruuter.private/DSL/GET/classifier/integration/outlook/token.yml index fdf73822..d4a09476 100644 --- a/DSL/Ruuter.private/DSL/GET/classifier/integration/outlook/token.yml +++ b/DSL/Ruuter.private/DSL/GET/classifier/integration/outlook/token.yml @@ -14,10 +14,11 @@ get_refresh_token: body: platform: 'OUTLOOK' result: res + next: set_refresh_token set_refresh_token: assign: - refresh_token: "[#OUTLOOK_REFRESH_KEY]" #${res.response.body.token} + refresh_token: ${res.response.body[0].token} next: check_refresh_token check_refresh_token: @@ -26,6 +27,8 @@ check_refresh_token: next: get_access_token next: return_not_found +#not supported for internal requests + get_access_token: call: http.post args: @@ -49,4 +52,4 @@ return_result: return_not_found: status: 404 return: "refresh token not found" - next: end \ No newline at end of file + next: end diff --git a/DSL/Ruuter.private/DSL/POST/auth/.guard b/DSL/Ruuter.private/DSL/POST/auth/.guard deleted file mode 100644 index 64435377..00000000 --- a/DSL/Ruuter.private/DSL/POST/auth/.guard +++ /dev/null @@ -1,4 +0,0 @@ -guard_allow_all: - return: "success" - status: 200 - next: end diff --git a/DSL/Ruuter.private/DSL/POST/auth/login.yml b/DSL/Ruuter.private/DSL/POST/auth/login.yml deleted file mode 100644 index d63682d2..00000000 --- a/DSL/Ruuter.private/DSL/POST/auth/login.yml +++ /dev/null @@ -1,79 +0,0 @@ -declaration: - call: declare - version: 0.1 - description: "Decription placeholder for 'LOGIN'" - method: post - accepts: json - returns: json - namespace: classifier - allowlist: - body: - - field: login - type: string - description: "Body field 'login'" - - field: password - type: string - description: "Body field 'password'" - -extractRequestData: - assign: - login: ${incoming.body.login} - password: ${incoming.body.password} - next: getUserWithRole - -getUserWithRole: - call: http.post - args: - url: "[#CLASSIFIER_RESQL]/get-user-with-roles" - body: - login: ${login} - password: ${password} - result: user_result - next: check_user_result - -check_user_result: - switch: - - condition: "${user_result.response.body.length > 0}" - next: get_session_length - next: return_user_not_found - -get_session_length: - call: http.post - args: - url: "[#CLASSIFIER_RESQL]/get-configuration" - body: - key: "session_length" - result: session_result - next: generate_cookie - -generate_cookie: - call: http.post - args: - url: "[#CLASSIFIER_TIM]/jwt/custom-jwt-generate" - body: - JWTName: "customJwtCookie" - expirationInMinutes: ${session_result.response.body[0]?.value ?? '120'} - content: ${user_result.response.body[0]} - result: cookie_result - next: assign_cookie - -assign_cookie: - assign: - setCookie: - customJwtCookie: ${cookie_result.response.body.token} - Domain: "[#DOMAIN]" - Secure: true - HttpOnly: true - SameSite: "Lax" - next: return_value - -return_value: - headers: - Set-Cookie: ${setCookie} - return: ${cookie_result.response.body.token} - next: end - -return_user_not_found: - status: 400 - return: "User Not Found" - next: end diff --git a/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/label.yml b/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/label.yml index cbce716c..f4e8be21 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/label.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/label.yml @@ -14,6 +14,10 @@ declaration: - field: folderId type: string description: "Body field 'folderId'" + headers: + - field: cookie + type: string + description: "Cookie field" extract_request_data: assign: @@ -31,6 +35,8 @@ get_token_info: call: http.get args: url: "[#CLASSIFIER_RUUTER_PRIVATE]/classifier/integration/outlook/token" + headers: + cookie: ${incoming.headers.cookie} result: res next: assign_access_token diff --git a/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/subscribe.yml b/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/subscribe.yml index 87eed337..1bda7c11 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/subscribe.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/subscribe.yml @@ -11,10 +11,14 @@ declaration: - field: is_connect type: boolean description: "Body field 'isConnect'" + headers: + - field: cookie + type: string + description: "Cookie field" extract_request_data: assign: - is_connect: ${incoming.body.isConnect} + is_connect: ${incoming.body.is_connect} next: get_platform_integration_status get_platform_integration_status: @@ -28,8 +32,8 @@ get_platform_integration_status: assign_db_platform_integration_data: assign: - db_platform_status: ${res.response.body.is_connect} - subscription_id: ${res.response.body.subscription_id} + db_platform_status: ${res.response.body[0].isConnect} + subscription_id: ${res.response.body[0].subscriptionId} next: validate_request validate_request: @@ -41,7 +45,7 @@ validate_request: get_token_info: call: http.get args: - url: "[#CLASSIFIER_RUUTER_PRIVATE]/classifier/integration/outlook/token" + url: "[#CLASSIFIER_RUUTER_PRIVATE_INTERNAL]/internal/xyz" result: res next: assign_access_token @@ -52,7 +56,7 @@ assign_access_token: check_integration_type: switch: - - condition: ${is_connect === true && subscription_id == null} + - condition: ${is_connect === true && subscription_id === null} next: subscribe_outlook - condition: ${is_connect === false && subscription_id !== null} next: unsubscribe_outlook @@ -66,10 +70,10 @@ subscribe_outlook: Authorization: ${'Bearer ' + access_token} body: changeType: "created,updated" - notificationUrl: "[#CLASSIFIER_RUUTER_PRIVATE]/classifier/integration/outlook/accept" + notificationUrl: "https://f789-111-223-191-66.ngrok-free.app/classifier/integration/outlook/accept" resource: "me/mailFolders('inbox')/messages" - expirationDateTime: "2024-07-02T21:10:45.9356913Z" - clientState: "state" + expirationDateTime: "2024-07-06T21:10:45.9356913Z" + clientState: "secretClientValue" result: res_subscribe next: check_subscribe_response @@ -84,7 +88,7 @@ set_subscription_data: args: url: "[#CLASSIFIER_RESQL]/connect-platform" body: - id: ${res_subscribe.response.id} + id: ${res_subscribe.response.body.id} platform: 'OUTLOOK' result: set_status_res next: check_db_status @@ -92,7 +96,7 @@ set_subscription_data: unsubscribe_outlook: call: http.delete args: - url: "https://graph.microsoft.com/v1.0/subscriptions/${res_data.response.subscriptionId}" + url: "https://graph.microsoft.com/v1.0/subscriptions/${subscription_id}" headers: Authorization: ${'Bearer ' + access_token} result: res_unsubscribe diff --git a/DSL/Ruuter.private/DSL/POST/classifier/integration/toggle-platform.yml b/DSL/Ruuter.private/DSL/POST/classifier/integration/toggle-platform.yml index 70de6795..4911ce3a 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/integration/toggle-platform.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/integration/toggle-platform.yml @@ -14,11 +14,16 @@ declaration: - field: platform type: string description: "Body field 'platform'" + headers: + - field: cookie + type: string + description: "Cookie field" extract_request_data: assign: operation: ${incoming.body.operation} platform: ${incoming.body.platform} + cookie: ${incoming.headers.cookie} next: check_operation check_operation: @@ -43,25 +48,25 @@ check_platform: switch: - condition: ${platform === 'jira'} next: assign_jira_url - - condition: ${operation === 'outlook'} + - condition: ${platform === 'outlook'} next: assign_outlook_url - - condition: ${operation === 'pinal'} + - condition: ${platform === 'pinal'} next: assign_pinal_url next: platform_not_support assign_jira_url: assign: - url: "jira/cloud/toggle-subscription" + url: "jira/cloud/subscribe" next: route_to_platform assign_outlook_url: assign: - url: "outlook/toggle-subscription" + url: "outlook/subscribe" next: route_to_platform assign_pinal_url: assign: - url: "pinal/toggle-subscription" + url: "pinal/subscribe" next: route_to_platform route_to_platform: @@ -70,6 +75,7 @@ route_to_platform: url: "[#CLASSIFIER_RUUTER_PRIVATE]/classifier/integration/${url}" headers: type: json + cookie: ${cookie} body: is_connect: ${is_connect} result: res diff --git a/DSL/Ruuter.public/DSL/POST/auth/login.yml b/DSL/Ruuter.public/DSL/POST/auth/login.yml index 15cc7462..d63682d2 100644 --- a/DSL/Ruuter.public/DSL/POST/auth/login.yml +++ b/DSL/Ruuter.public/DSL/POST/auth/login.yml @@ -5,26 +5,16 @@ declaration: method: post accepts: json returns: json - namespace: backoffice + namespace: classifier allowlist: body: - field: login type: string description: "Body field 'login'" - field: password - type: stringDSL/Ruuter.public/DSL/POST/login.yml + type: string description: "Body field 'password'" -getIsPasswordAuthEnabled: - assign: - isPasswordAuthEnabled: "[#PASSWORD_AUTH_ENABLED]" - -checkPasswordLoginEnabled: - switch: - - condition: ${isPasswordAuthEnabled === true || isPasswordAuthEnabled.toLowerCase() === "true"} - next: extractRequestData - next: return_password_login_disabled - extractRequestData: assign: login: ${incoming.body.login} @@ -34,7 +24,7 @@ extractRequestData: getUserWithRole: call: http.post args: - url: "[#CHATBOT_RESQL]/get-user-with-roles" + url: "[#CLASSIFIER_RESQL]/get-user-with-roles" body: login: ${login} password: ${password} @@ -50,7 +40,7 @@ check_user_result: get_session_length: call: http.post args: - url: "[#CHATBOT_RESQL]/get-configuration" + url: "[#CLASSIFIER_RESQL]/get-configuration" body: key: "session_length" result: session_result @@ -59,7 +49,7 @@ get_session_length: generate_cookie: call: http.post args: - url: "[#CHATBOT_TIM]/jwt/custom-jwt-generate" + url: "[#CLASSIFIER_TIM]/jwt/custom-jwt-generate" body: JWTName: "customJwtCookie" expirationInMinutes: ${session_result.response.body[0]?.value ?? '120'} @@ -75,18 +65,6 @@ assign_cookie: Secure: true HttpOnly: true SameSite: "Lax" - next: setCustomerSupportAgentAway - -setCustomerSupportAgentAway: - call: http.post - args: - url: "[#CHATBOT_RESQL]/set-customer-support-status" - body: - active: false - userIdCode: ${login} - created: ${new Date().toISOString()} - status: "offline" - result: setCustomerSupportAgentAwayResult next: return_value return_value: @@ -99,8 +77,3 @@ return_user_not_found: status: 400 return: "User Not Found" next: end - -return_password_login_disabled: - status: 400 - return: "Password login is disabled" - next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/accept.yml b/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml similarity index 100% rename from DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/accept.yml rename to DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml diff --git a/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/accept.yml b/DSL/Ruuter.public/DSL/POST/classifier/integration/outlook/accept.yml similarity index 100% rename from DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/accept.yml rename to DSL/Ruuter.public/DSL/POST/classifier/integration/outlook/accept.yml diff --git a/docker-compose.yml b/docker-compose.yml index 2988bc9a..2891642f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,24 @@ services: + ruuter-public: + container_name: ruuter-public + image: ruuter + environment: + - application.cors.allowedOrigins=http://localhost:8086,http://localhost:3001,http://localhost:3003,http://localhost:3004,http://localhost:8080 + - application.httpCodesAllowList=200,201,202,400,401,403,500 + - application.internalRequests.allowedIPs=127.0.0.1 + - application.logging.displayRequestContent=true + - application.logging.displayResponseContent=true + - server.port=8086 + volumes: + - ./DSL/Ruuter.public/DSL:/DSL + - ./constants.ini:/app/constants.ini + ports: + - 8086:8086 + networks: + - bykstack + cpus: "0.5" + mem_limit: "512M" + ruuter-private: container_name: ruuter-private image: ruuter From 356e19337436016ecefa3b0bf5e9b1fcab4250bf Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Sat, 6 Jul 2024 01:15:19 +0530 Subject: [PATCH 080/582] authentication integration --- .gitignore | 5 +- .../classifier-script-v1-user-management.sql | 11 ++- .../DSL/TEMPLATES/check-user-authority.yml | 2 +- GUI/src/App.tsx | 42 ++------ GUI/src/components/Header/index.tsx | 18 ++-- .../MainNavigation/MainNavigation.scss | 1 - GUI/src/components/MainNavigation/index.tsx | 95 ++++++++++--------- constants.ini | 2 +- docker-compose.yml | 8 +- migrate.sh | 2 +- 10 files changed, 87 insertions(+), 99 deletions(-) diff --git a/.gitignore b/.gitignore index 8dd4607a..81a6bac7 100644 --- a/.gitignore +++ b/.gitignore @@ -395,4 +395,7 @@ FodyWeavers.xsd *.msp # JetBrains Rider -*.sln.iml \ No newline at end of file +*.sln.iml + +tim_db +data \ No newline at end of file diff --git a/DSL/Liquibase/changelog/classifier-script-v1-user-management.sql b/DSL/Liquibase/changelog/classifier-script-v1-user-management.sql index 84d679be..ca8d4bca 100644 --- a/DSL/Liquibase/changelog/classifier-script-v1-user-management.sql +++ b/DSL/Liquibase/changelog/classifier-script-v1-user-management.sql @@ -29,4 +29,13 @@ CREATE TABLE public."user_authority" ( authority_name VARCHAR[] NOT NULL, created TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, CONSTRAINT user_authority_pkey PRIMARY KEY (id) -); \ No newline at end of file +); + +-- changeset ErangiA:classifier-script-v1-changeset3 + +INSERT INTO public."user" (login,password_hash,first_name,last_name,id_code,display_name,status,csa_title,csa_email) +VALUES ('EE30303039914','ok','kal','test','EE30303039914','kal','active','Title','kal.doe@example.com'); + + +INSERT INTO public."user_authority" ( user_id, authority_name) +VALUES ('EE30303039914', ARRAY['ROLE_ADMINISTRATOR', 'ROLE_MODEL_TRAINER'] ); \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/TEMPLATES/check-user-authority.yml b/DSL/Ruuter.private/DSL/TEMPLATES/check-user-authority.yml index 3baee630..ecc54729 100644 --- a/DSL/Ruuter.private/DSL/TEMPLATES/check-user-authority.yml +++ b/DSL/Ruuter.private/DSL/TEMPLATES/check-user-authority.yml @@ -31,7 +31,7 @@ check_cookie_info_response: check_user_authority: switch: - - condition: ${res.response.body.authorities.includes("ROLE_ADMINISTRATOR") || res.response.body.authorities.includes("ROLE_ANALYST")} + - condition: ${res.response.body.authorities.includes("ROLE_ADMINISTRATOR") || res.response.body.authorities.includes("ROLE_MODEL_TRAINER")} next: return_authorized next: return_unauthorized diff --git a/GUI/src/App.tsx b/GUI/src/App.tsx index 02b8dd31..24165d27 100644 --- a/GUI/src/App.tsx +++ b/GUI/src/App.tsx @@ -13,39 +13,15 @@ import DatasetGroups from 'pages/DataSetGroups'; const App: FC = () => { - const res={ - response: { - firstName: "Kustuta", - lastName: "Kasutaja", - idCode: "EE30303039914", - displayName: "Kustutamiseks", - JWTCreated: "1.71886644E12" , - fullName: "OK TESTNUMBER", - login: "EE30303039914", - authMethod: "smartid", - csaEmail: "kustutamind@mail.ee", - authorities: [ - "ROLE_ADMINISTRATOR" - ], - csaTitle: "", - JWTExpirationTimestamp: "1.71887364E12" - } -}; - // useQuery<{ - // data: { response: UserInfo }; - // }>({ - // queryKey: ['auth/jwt/userinfo', 'prod'], - // onSuccess: (res: { response: UserInfo }) => { - // localStorage.setItem('exp', res.response.JWTExpirationTimestamp); - // return useStore.getState().setUserInfo(res.response); - // }, - // }); - - useEffect(()=>{ - localStorage.setItem('exp', res.response.JWTExpirationTimestamp); - return useStore.getState().setUserInfo(res.response); - - },[]) + useQuery<{ + data: { response: UserInfo }; + }>({ + queryKey: ['auth/jwt/userinfo', 'prod'], + onSuccess: (res: { response: UserInfo }) => { + localStorage.setItem('exp', res.response.JWTExpirationTimestamp); + return useStore.getState().setUserInfo(res.response); + }, + }); return ( diff --git a/GUI/src/components/Header/index.tsx b/GUI/src/components/Header/index.tsx index f636bea9..6a521066 100644 --- a/GUI/src/components/Header/index.tsx +++ b/GUI/src/components/Header/index.tsx @@ -86,23 +86,23 @@ const Header: FC = () => { const currentDate = new Date(Date.now()); if (expirationDate < currentDate) { localStorage.removeItem('exp'); - // window.location.href =import.meta.env.REACT_APP_CUSTOMER_SERVICE_LOGIN; + window.location.href =import.meta.env.REACT_APP_CUSTOMER_SERVICE_LOGIN; } } }, 2000); return () => clearInterval(interval); }, [userInfo]); - useEffect(() => { - getMessages(); - }, [userInfo?.idCode]); + // useEffect(() => { + // getMessages(); + // }, [userInfo?.idCode]); - const getMessages = async () => { - const { data: res } = await apiDev.get('accounts/settings'); + // const getMessages = async () => { + // const { data: res } = await apiDev.get('accounts/settings'); - if (res.response && res.response != 'error: not found') - setUserProfileSettings(res.response[0]); - }; + // if (res.response && res.response != 'error: not found') + // setUserProfileSettings(res.response[0]); + // }; // const { data: customerSupportActivity } = useQuery({ // queryKey: ['accounts/customer-support-activity', 'prod'], // onSuccess(res: any) { diff --git a/GUI/src/components/MainNavigation/MainNavigation.scss b/GUI/src/components/MainNavigation/MainNavigation.scss index 23bc2176..267c54db 100644 --- a/GUI/src/components/MainNavigation/MainNavigation.scss +++ b/GUI/src/components/MainNavigation/MainNavigation.scss @@ -73,7 +73,6 @@ } &.nav__toggle--icon { - padding-left: 8px; .icon:first-child { transform: none; diff --git a/GUI/src/components/MainNavigation/index.tsx b/GUI/src/components/MainNavigation/index.tsx index 5e59a7d8..e93eaadc 100644 --- a/GUI/src/components/MainNavigation/index.tsx +++ b/GUI/src/components/MainNavigation/index.tsx @@ -8,6 +8,7 @@ import { Icon } from 'components'; import type { MenuItem } from 'types/mainNavigation'; import { menuIcons } from 'constants/menuIcons'; import './MainNavigation.scss'; +import { error } from 'console'; const MainNavigation: FC = () => { const { t } = useTranslation(); @@ -75,53 +76,53 @@ const MainNavigation: FC = () => { }, ]; - useEffect(()=>{ - const filteredItems = - items.filter((item) => { - const role = "ROLE_ADMINISTRATOR"; - switch (role) { - case 'ROLE_ADMINISTRATOR': - return item.id; - case 'ROLE_SERVICE_MANAGER': - return item.id != 'settings' && item.id != 'training'; - case 'ROLE_CUSTOMER_SUPPORT_AGENT': - return item.id != 'settings' && item.id != 'analytics'; - case 'ROLE_CHATBOT_TRAINER': - return item.id != 'settings' && item.id != 'conversations'; - case 'ROLE_ANALYST': - return item.id == 'analytics' || item.id == 'monitoring'; - case 'ROLE_UNAUTHENTICATED': - return; - } - }) ?? []; - setMenuItems(filteredItems); - - },[]) - - // useQuery({ - // queryKey: ['/accounts/user-role', 'prod'], - // onSuccess: (res: any) => { - // const filteredItems = - // items.filter((item) => { - // const role = res.data.get_user[0].authorities[0]; - // switch (role) { - // case 'ROLE_ADMINISTRATOR': - // return item.id; - // case 'ROLE_SERVICE_MANAGER': - // return item.id != 'settings' && item.id != 'training'; - // case 'ROLE_CUSTOMER_SUPPORT_AGENT': - // return item.id != 'settings' && item.id != 'analytics'; - // case 'ROLE_CHATBOT_TRAINER': - // return item.id != 'settings' && item.id != 'conversations'; - // case 'ROLE_ANALYST': - // return item.id == 'analytics' || item.id == 'monitoring'; - // case 'ROLE_UNAUTHENTICATED': - // return; - // } - // }) ?? []; - // setMenuItems(filteredItems); - // }, - // }); + // useEffect(()=>{ + // const filteredItems = + // items.filter((item) => { + // const role = "ROLE_ADMINISTRATOR"; + // switch (role) { + // case 'ROLE_ADMINISTRATOR': + // return item.id; + // case 'ROLE_SERVICE_MANAGER': + // return item.id != 'settings' && item.id != 'training'; + // case 'ROLE_CUSTOMER_SUPPORT_AGENT': + // return item.id != 'settings' && item.id != 'analytics'; + // case 'ROLE_CHATBOT_TRAINER': + // return item.id != 'settings' && item.id != 'conversations'; + // case 'ROLE_ANALYST': + // return item.id == 'analytics' || item.id == 'monitoring'; + // case 'ROLE_UNAUTHENTICATED': + // return; + // } + // }) ?? []; + // setMenuItems(filteredItems); + + // },[]) + + useQuery({ + queryKey: ['/accounts/user-role', 'prod'], + onSuccess: (res: any) => { + const filteredItems = + items.filter((item) => { + + const role = res?.response[0]; + + switch (role) { + case 'ROLE_ADMINISTRATOR': + return item.id; + case 'ROLE_MODEL_TRAINER': + return item.id !== 'userManagement' && item.id !== 'integration'; + case 'ROLE_UNAUTHENTICATED': + return; + } + }) ?? []; + setMenuItems(filteredItems); + }, + onError:(error:any)=>{ + console.log(error); + + } + }); const location = useLocation(); const [navCollapsed, setNavCollapsed] = useState(false); diff --git a/constants.ini b/constants.ini index 4624f4e2..48637c61 100644 --- a/constants.ini +++ b/constants.ini @@ -14,4 +14,4 @@ JIRA_WEBHOOK_SECRET=value OUTLOOK_CLIENT_ID=value OUTLOOK_SECRET_KEY=value OUTLOOK_REFRESH_KEY=value -DB_PASSWORD=value \ No newline at end of file +DB_PASSWORD=rootcode \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 2988bc9a..66e97f0f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -115,8 +115,8 @@ services: - sqlms.datasources.[0].name=classifier - sqlms.datasources.[0].jdbcUrl=jdbc:postgresql://users_db:5432/classifier #For LocalDb Use # sqlms.datasources.[0].jdbcUrl=jdbc:postgresql://171.22.247.13:5433/byk?sslmode=require - - sqlms.datasources.[0].username=root - - sqlms.datasources.[0].password=root + - sqlms.datasources.[0].username=postgres + - sqlms.datasources.[0].password=rootcode - logging.level.org.springframework.boot=INFO ports: - 8082:8082 @@ -129,8 +129,8 @@ services: container_name: users_db image: postgres:14.1 environment: - - POSTGRES_USER=root - - POSTGRES_PASSWORD=root + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=rootcode - POSTGRES_DB=classifier ports: - 5433:5432 diff --git a/migrate.sh b/migrate.sh index 8d505b33..779d9bab 100644 --- a/migrate.sh +++ b/migrate.sh @@ -12,4 +12,4 @@ INI_FILE="constants.ini" DB_PASSWORD=$(get_ini_value "$INI_FILE" "DB_PASSWORD") -docker run --rm --network bykstack -v `pwd`/DSL/Liquibase/changelog:/liquibase/changelog -v `pwd`/DSL/Liquibase/master.yml:/liquibase/master.yml -v `pwd`/DSL/Liquibase/data:/liquibase/data liquibase/liquibase --defaultsFile=/liquibase/changelog/liquibase.properties --changelog-file=master.yml --url=jdbc:postgresql://users_db:5432/classifier?user=root --password=$DB_PASSWORD update +docker run --rm --network bykstack -v `pwd`/DSL/Liquibase/changelog:/liquibase/changelog -v `pwd`/DSL/Liquibase/master.yml:/liquibase/master.yml -v `pwd`/DSL/Liquibase/data:/liquibase/data liquibase/liquibase --defaultsFile=/liquibase/changelog/liquibase.properties --changelog-file=master.yml --url=jdbc:postgresql://users_db:5432/classifier?user=postgres --password=$DB_PASSWORD update \ No newline at end of file From 552fe0510940d8893d89cd088e0fcf730cf17f0b Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Sun, 7 Jul 2024 21:47:38 +0530 Subject: [PATCH 081/582] stage workflow and dev workflow name change --- ...-frontend-dev.yml => est-workflow-dev.yml} | 2 +- .github/workflows/est-workflow-staging.yml | 53 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) rename .github/workflows/{est-frontend-dev.yml => est-workflow-dev.yml} (96%) create mode 100644 .github/workflows/est-workflow-staging.yml diff --git a/.github/workflows/est-frontend-dev.yml b/.github/workflows/est-workflow-dev.yml similarity index 96% rename from .github/workflows/est-frontend-dev.yml rename to .github/workflows/est-workflow-dev.yml index 446cba6c..5fdadade 100644 --- a/.github/workflows/est-frontend-dev.yml +++ b/.github/workflows/est-workflow-dev.yml @@ -1,4 +1,4 @@ -name: Deploy Frontend +name: Deploy EST Frontend and Backend to development on: push: diff --git a/.github/workflows/est-workflow-staging.yml b/.github/workflows/est-workflow-staging.yml new file mode 100644 index 00000000..81f9a96a --- /dev/null +++ b/.github/workflows/est-workflow-staging.yml @@ -0,0 +1,53 @@ +name: Deploy EST Frontend and Backend to Staging + +on: + push: + branches: + - stage + +jobs: + deploy: + runs-on: [self-hosted, stage] + + steps: + - name: Set permissions for workspace directory + run: | + sudo chown -R ubuntu:ubuntu /home/ubuntu/actions-runner/_work/classifier/classifier + sudo chmod -R u+rwx /home/ubuntu/actions-runner/_work/classifier/classifier + + - name: Clean up workspace + run: | + sudo rm -rf /home/ubuntu/actions-runner/_work/classifier/classifier/* + + - name: Checkout code + uses: actions/checkout@v3 + with: + clean: true + + - name: Remove all running containers, images, and prune Docker system + run: | + docker stop $(docker ps -a -q) || true + docker rm $(docker ps -a -q) || true + images_to_keep="authentication-layer|tim|data-mapper|resql|ruuter" + docker images --format "{{.Repository}}:{{.Tag}}" | grep -Ev "$images_to_keep" | xargs -r docker rmi || true + docker volume prune -f + docker network prune -f + + - name: Build and run Docker Compose + run: | + docker compose up --build -d + + - name: Get public IP address + id: get_ip + run: | + PUBLIC_IP=$(curl -s http://checkip.amazonaws.com) + echo "PUBLIC_IP=$PUBLIC_IP" >> $GITHUB_ENV + + - name: Send Slack notification + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + PUBLIC_IP: ${{ env.PUBLIC_IP }} + run: | + curl -X POST -H 'Content-type: application/json' --data "{ + \"text\": \"Build is complete. Public IP: $PUBLIC_IP\" + }" $SLACK_WEBHOOK_URL From f7b12e67620e56e205fa8f998f29dfe4228dce27 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Sun, 7 Jul 2024 22:24:09 +0530 Subject: [PATCH 082/582] workflow dev slack message update --- .github/workflows/est-workflow-dev.yml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/.github/workflows/est-workflow-dev.yml b/.github/workflows/est-workflow-dev.yml index 5fdadade..58f6a22a 100644 --- a/.github/workflows/est-workflow-dev.yml +++ b/.github/workflows/est-workflow-dev.yml @@ -37,17 +37,10 @@ jobs: run: | docker compose up --build -d - - name: Get public IP address - id: get_ip - run: | - PUBLIC_IP=$(curl -s http://checkip.amazonaws.com) - echo "PUBLIC_IP=$PUBLIC_IP" >> $GITHUB_ENV - - name: Send Slack notification env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} - PUBLIC_IP: ${{ env.PUBLIC_IP }} run: | curl -X POST -H 'Content-type: application/json' --data "{ - \"text\": \"Build is complete. Public IP: $PUBLIC_IP\" + \"text\": \"The build is complete and the development environment is now available. Please click the following link to access it: \" }" $SLACK_WEBHOOK_URL From 3a24020f4ebcf162f1c5617ae12b8b76d1f94f4f Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Sun, 7 Jul 2024 22:24:21 +0530 Subject: [PATCH 083/582] workflow testing slack message update --- .github/workflows/est-workflow-staging.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/est-workflow-staging.yml b/.github/workflows/est-workflow-staging.yml index 81f9a96a..f5573279 100644 --- a/.github/workflows/est-workflow-staging.yml +++ b/.github/workflows/est-workflow-staging.yml @@ -46,8 +46,7 @@ jobs: - name: Send Slack notification env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} - PUBLIC_IP: ${{ env.PUBLIC_IP }} run: | curl -X POST -H 'Content-type: application/json' --data "{ - \"text\": \"Build is complete. Public IP: $PUBLIC_IP\" - }" $SLACK_WEBHOOK_URL + \"text\": \"The build is complete and the staging environment is now available. Please click the following link to access it: \" + }" $SLACK_WEBHOOK_URL \ No newline at end of file From 4ffbbae0a1141dd612c4d89d01c4473d895b3855 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Sun, 7 Jul 2024 22:25:39 +0530 Subject: [PATCH 084/582] remove workflow public ip retrivel --- .github/workflows/est-workflow-staging.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/est-workflow-staging.yml b/.github/workflows/est-workflow-staging.yml index f5573279..62dcb180 100644 --- a/.github/workflows/est-workflow-staging.yml +++ b/.github/workflows/est-workflow-staging.yml @@ -37,12 +37,6 @@ jobs: run: | docker compose up --build -d - - name: Get public IP address - id: get_ip - run: | - PUBLIC_IP=$(curl -s http://checkip.amazonaws.com) - echo "PUBLIC_IP=$PUBLIC_IP" >> $GITHUB_ENV - - name: Send Slack notification env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} From c4aa941f0e72f623bca4e2a3a564a60c77cf1c4d Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 8 Jul 2024 13:18:34 +0530 Subject: [PATCH 085/582] pytesting --- .github/workflows/est-workflow-dev.yml | 21 +++++++++++++++++- .github/workflows/est-workflow-staging.yml | 25 +++++++++++++++++++--- src/pyTesting.py | 2 ++ 3 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 src/pyTesting.py diff --git a/.github/workflows/est-workflow-dev.yml b/.github/workflows/est-workflow-dev.yml index 58f6a22a..287b5e2c 100644 --- a/.github/workflows/est-workflow-dev.yml +++ b/.github/workflows/est-workflow-dev.yml @@ -37,7 +37,26 @@ jobs: run: | docker compose up --build -d - - name: Send Slack notification + - name: Run pyTesting.py + id: pytesting + run: | + output=$(python src/pyTesting.py) + if [ "$output" != "True" ]; then + echo "PyTesting failed with output: $output" + exit 1 + fi + + - name: Send failure Slack notification + if: failure() + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + run: | + curl -X POST -H 'Content-type: application/json' --data "{ + \"text\": \"The deployment failed during the pyTesting step. Please check the output for details.\" + }" $SLACK_WEBHOOK_URL + + - name: Send success Slack notification + if: success() env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} run: | diff --git a/.github/workflows/est-workflow-staging.yml b/.github/workflows/est-workflow-staging.yml index 62dcb180..1b639efe 100644 --- a/.github/workflows/est-workflow-staging.yml +++ b/.github/workflows/est-workflow-staging.yml @@ -1,4 +1,4 @@ -name: Deploy EST Frontend and Backend to Staging +name: Deploy EST Frontend and Backend to staging on: push: @@ -37,10 +37,29 @@ jobs: run: | docker compose up --build -d - - name: Send Slack notification + - name: Run pyTesting.py + id: pytesting + run: | + output=$(python src/pyTesting.py) + if [ "$output" != "True" ]; then + echo "PyTesting failed with output: $output" + exit 1 + fi + + - name: Send failure Slack notification + if: failure() + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + run: | + curl -X POST -H 'Content-type: application/json' --data "{ + \"text\": \"The deployment failed during the pyTesting step. Please check the output for details.\" + }" $SLACK_WEBHOOK_URL + + - name: Send success Slack notification + if: success() env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} run: | curl -X POST -H 'Content-type: application/json' --data "{ \"text\": \"The build is complete and the staging environment is now available. Please click the following link to access it: \" - }" $SLACK_WEBHOOK_URL \ No newline at end of file + }" $SLACK_WEBHOOK_URL diff --git a/src/pyTesting.py b/src/pyTesting.py new file mode 100644 index 00000000..acc25ef6 --- /dev/null +++ b/src/pyTesting.py @@ -0,0 +1,2 @@ +if __name__ == "__main__": + print(True) From 8181a8270f7758cc6a69174be8056ef2bb6e6a73 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 8 Jul 2024 13:37:50 +0530 Subject: [PATCH 086/582] .sh testing file update --- .github/workflows/est-workflow-dev.yml | 14 +++++++++----- .github/workflows/est-workflow-staging.yml | 16 ++++++++++------ src/pyTesting.py | 2 -- src/unitTesting.sh | 2 ++ 4 files changed, 21 insertions(+), 13 deletions(-) delete mode 100644 src/pyTesting.py create mode 100644 src/unitTesting.sh diff --git a/.github/workflows/est-workflow-dev.yml b/.github/workflows/est-workflow-dev.yml index 287b5e2c..b142fabd 100644 --- a/.github/workflows/est-workflow-dev.yml +++ b/.github/workflows/est-workflow-dev.yml @@ -24,6 +24,10 @@ jobs: with: clean: true + - name: Give execute permissions to testScript.sh + run: | + sudo chmod +x /home/ubuntu/actions-runner/_work/classifier/classifier/src/testScript.sh + - name: Remove all running containers, images, and prune Docker system run: | docker stop $(docker ps -a -q) || true @@ -37,12 +41,12 @@ jobs: run: | docker compose up --build -d - - name: Run pyTesting.py - id: pytesting + - name: Run testScript.sh + id: testscript run: | - output=$(python src/pyTesting.py) + output=$(bash src/testScript.sh) if [ "$output" != "True" ]; then - echo "PyTesting failed with output: $output" + echo "testScript.sh failed with output: $output" exit 1 fi @@ -52,7 +56,7 @@ jobs: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} run: | curl -X POST -H 'Content-type: application/json' --data "{ - \"text\": \"The deployment failed during the pyTesting step. Please check the output for details.\" + \"text\": \"The deployment failed during the testScript.sh step. Please check the output for details.\" }" $SLACK_WEBHOOK_URL - name: Send success Slack notification diff --git a/.github/workflows/est-workflow-staging.yml b/.github/workflows/est-workflow-staging.yml index 1b639efe..354cec2b 100644 --- a/.github/workflows/est-workflow-staging.yml +++ b/.github/workflows/est-workflow-staging.yml @@ -1,4 +1,4 @@ -name: Deploy EST Frontend and Backend to staging +name: Deploy EST Frontend and Backend to Staging on: push: @@ -24,6 +24,10 @@ jobs: with: clean: true + - name: Give execute permissions to testScript.sh + run: | + sudo chmod +x /home/ubuntu/actions-runner/_work/classifier/classifier/src/testScript.sh + - name: Remove all running containers, images, and prune Docker system run: | docker stop $(docker ps -a -q) || true @@ -37,12 +41,12 @@ jobs: run: | docker compose up --build -d - - name: Run pyTesting.py - id: pytesting + - name: Run testScript.sh + id: testscript run: | - output=$(python src/pyTesting.py) + output=$(bash src/testScript.sh) if [ "$output" != "True" ]; then - echo "PyTesting failed with output: $output" + echo "testScript.sh failed with output: $output" exit 1 fi @@ -52,7 +56,7 @@ jobs: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} run: | curl -X POST -H 'Content-type: application/json' --data "{ - \"text\": \"The deployment failed during the pyTesting step. Please check the output for details.\" + \"text\": \"The Staging deployment failed during the testScript.sh step. Please check the output for details.\" }" $SLACK_WEBHOOK_URL - name: Send success Slack notification diff --git a/src/pyTesting.py b/src/pyTesting.py deleted file mode 100644 index acc25ef6..00000000 --- a/src/pyTesting.py +++ /dev/null @@ -1,2 +0,0 @@ -if __name__ == "__main__": - print(True) diff --git a/src/unitTesting.sh b/src/unitTesting.sh new file mode 100644 index 00000000..23221242 --- /dev/null +++ b/src/unitTesting.sh @@ -0,0 +1,2 @@ +#!/bin/bash +echo "True" From b9a050cf570b449b359a75a4668001f19fee14c7 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 8 Jul 2024 13:39:45 +0530 Subject: [PATCH 087/582] fixing test script location issue --- .github/workflows/est-workflow-dev.yml | 2 +- .github/workflows/est-workflow-staging.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/est-workflow-dev.yml b/.github/workflows/est-workflow-dev.yml index b142fabd..0207cf63 100644 --- a/.github/workflows/est-workflow-dev.yml +++ b/.github/workflows/est-workflow-dev.yml @@ -26,7 +26,7 @@ jobs: - name: Give execute permissions to testScript.sh run: | - sudo chmod +x /home/ubuntu/actions-runner/_work/classifier/classifier/src/testScript.sh + sudo chmod +x src/testScript.sh - name: Remove all running containers, images, and prune Docker system run: | diff --git a/.github/workflows/est-workflow-staging.yml b/.github/workflows/est-workflow-staging.yml index 354cec2b..e7caa139 100644 --- a/.github/workflows/est-workflow-staging.yml +++ b/.github/workflows/est-workflow-staging.yml @@ -26,7 +26,7 @@ jobs: - name: Give execute permissions to testScript.sh run: | - sudo chmod +x /home/ubuntu/actions-runner/_work/classifier/classifier/src/testScript.sh + sudo chmod +x src/testScript.sh - name: Remove all running containers, images, and prune Docker system run: | From b983db2cad68f67eba7fc027a6edd578d6b4d559 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 8 Jul 2024 13:41:39 +0530 Subject: [PATCH 088/582] location fix --- .github/workflows/est-workflow-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/est-workflow-dev.yml b/.github/workflows/est-workflow-dev.yml index 0207cf63..b142fabd 100644 --- a/.github/workflows/est-workflow-dev.yml +++ b/.github/workflows/est-workflow-dev.yml @@ -26,7 +26,7 @@ jobs: - name: Give execute permissions to testScript.sh run: | - sudo chmod +x src/testScript.sh + sudo chmod +x /home/ubuntu/actions-runner/_work/classifier/classifier/src/testScript.sh - name: Remove all running containers, images, and prune Docker system run: | From 9bd027cdc1448606e166c3c3eb10aed5eec26ccd Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 8 Jul 2024 13:45:24 +0530 Subject: [PATCH 089/582] name error fix --- .github/workflows/est-workflow-dev.yml | 2 +- .github/workflows/est-workflow-staging.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/est-workflow-dev.yml b/.github/workflows/est-workflow-dev.yml index b142fabd..dcf4a396 100644 --- a/.github/workflows/est-workflow-dev.yml +++ b/.github/workflows/est-workflow-dev.yml @@ -26,7 +26,7 @@ jobs: - name: Give execute permissions to testScript.sh run: | - sudo chmod +x /home/ubuntu/actions-runner/_work/classifier/classifier/src/testScript.sh + sudo chmod +x src/unitTesting.sh - name: Remove all running containers, images, and prune Docker system run: | diff --git a/.github/workflows/est-workflow-staging.yml b/.github/workflows/est-workflow-staging.yml index e7caa139..ec124be2 100644 --- a/.github/workflows/est-workflow-staging.yml +++ b/.github/workflows/est-workflow-staging.yml @@ -26,7 +26,7 @@ jobs: - name: Give execute permissions to testScript.sh run: | - sudo chmod +x src/testScript.sh + sudo chmod +x src/unitTesting.sh - name: Remove all running containers, images, and prune Docker system run: | From 4f6380132c7acb656871f980d721df5e90a59a32 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 8 Jul 2024 13:51:20 +0530 Subject: [PATCH 090/582] Final workflow fixes with unit testing --- .github/workflows/est-workflow-dev.yml | 10 +++++----- .github/workflows/est-workflow-staging.yml | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/est-workflow-dev.yml b/.github/workflows/est-workflow-dev.yml index dcf4a396..ecf48504 100644 --- a/.github/workflows/est-workflow-dev.yml +++ b/.github/workflows/est-workflow-dev.yml @@ -41,12 +41,12 @@ jobs: run: | docker compose up --build -d - - name: Run testScript.sh - id: testscript + - name: Run unitTesting.sh + id: unittesting run: | - output=$(bash src/testScript.sh) + output=$(bash src/unitTesting.sh) if [ "$output" != "True" ]; then - echo "testScript.sh failed with output: $output" + echo "unitTesting.sh failed with output: $output" exit 1 fi @@ -56,7 +56,7 @@ jobs: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} run: | curl -X POST -H 'Content-type: application/json' --data "{ - \"text\": \"The deployment failed during the testScript.sh step. Please check the output for details.\" + \"text\": \"The Development environment deployment failed during one of the steps. Please check the output for details.\" }" $SLACK_WEBHOOK_URL - name: Send success Slack notification diff --git a/.github/workflows/est-workflow-staging.yml b/.github/workflows/est-workflow-staging.yml index ec124be2..bcf6bcb0 100644 --- a/.github/workflows/est-workflow-staging.yml +++ b/.github/workflows/est-workflow-staging.yml @@ -41,12 +41,12 @@ jobs: run: | docker compose up --build -d - - name: Run testScript.sh - id: testscript + - name: Run unitTesting.sh + id: unittesting run: | - output=$(bash src/testScript.sh) + output=$(bash src/unitTesting.sh) if [ "$output" != "True" ]; then - echo "testScript.sh failed with output: $output" + echo "unitTesting.sh failed with output: $output" exit 1 fi @@ -56,7 +56,7 @@ jobs: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} run: | curl -X POST -H 'Content-type: application/json' --data "{ - \"text\": \"The Staging deployment failed during the testScript.sh step. Please check the output for details.\" + \"text\": \"The Staging environment deployment failed during one of the steps. Please check the output for details.\" }" $SLACK_WEBHOOK_URL - name: Send success Slack notification From eb6a7e277934439d036c2d8aa78bdf1ea452a348 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Mon, 8 Jul 2024 20:30:09 +0530 Subject: [PATCH 091/582] Role Base Access: set role base access to user management and integration,add get all user data and change delete query --- DSL/Resql/delete-user.sql | 20 +++++++- DSL/Resql/get-users-with-roles-by-role.sql | 41 +++++++++++++++ DSL/Ruuter.private/DSL/POST/accounts/.guard | 28 +++++++++++ .../DSL/POST/accounts/users.yml | 38 ++++++++++++++ .../DSL/POST/classifier/integration/.guard | 28 +++++++++++ .../TEMPLATES/check-user-authority-admin.yml | 50 +++++++++++++++++++ 6 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 DSL/Resql/get-users-with-roles-by-role.sql create mode 100644 DSL/Ruuter.private/DSL/POST/accounts/.guard create mode 100644 DSL/Ruuter.private/DSL/POST/accounts/users.yml create mode 100644 DSL/Ruuter.private/DSL/POST/classifier/integration/.guard create mode 100644 DSL/Ruuter.private/DSL/TEMPLATES/check-user-authority-admin.yml diff --git a/DSL/Resql/delete-user.sql b/DSL/Resql/delete-user.sql index 8910d75d..eb8ccade 100644 --- a/DSL/Resql/delete-user.sql +++ b/DSL/Resql/delete-user.sql @@ -1,3 +1,9 @@ +WITH active_administrators AS (SELECT user_id + FROM user_authority + WHERE 'ROLE_ADMINISTRATOR' = ANY (authority_name) + AND id IN (SELECT max(id) + FROM user_authority + GROUP BY user_id)), delete_user AS ( INSERT INTO "user" (login, password_hash, first_name, last_name, id_code, display_name, status, created, csa_title, csa_email) @@ -14,5 +20,17 @@ SELECT login, FROM "user" WHERE id_code = :userIdCode AND status <> 'deleted' - AND id IN (SELECT max(id) FROM "user" WHERE id_code = :userIdCode)) + AND id IN (SELECT max(id) FROM "user" WHERE id_code = :userIdCode) + AND (1 < (SELECT COUNT(user_id) FROM active_administrators) + OR (1 = (SELECT COUNT(user_id) FROM active_administrators) + AND :userIdCode NOT IN (SELECT user_id FROM active_administrators)))), +delete_authority AS ( +INSERT +INTO user_authority (user_id, authority_name, created) +SELECT :userIdCode as users, ARRAY []::varchar[], :created::timestamp with time zone +FROM user_authority +WHERE 1 < (SELECT COUNT(user_id) FROM active_administrators) + OR (1 = (SELECT COUNT(user_id) FROM active_administrators) + AND :userIdCode NOT IN (SELECT user_id FROM active_administrators)) +GROUP BY users) SELECT max(status) FROM "user" WHERE id_code = :userIdCode; diff --git a/DSL/Resql/get-users-with-roles-by-role.sql b/DSL/Resql/get-users-with-roles-by-role.sql new file mode 100644 index 00000000..77b8eb81 --- /dev/null +++ b/DSL/Resql/get-users-with-roles-by-role.sql @@ -0,0 +1,41 @@ +SELECT u.login, + u.first_name, + u.last_name, + u.id_code, + u.display_name, + u.csa_title, + u.csa_email, + ua.authority_name AS authorities, + CEIL(COUNT(*) OVER() / :page_size::DECIMAL) AS total_pages +FROM "user" u +LEFT JOIN ( + SELECT authority_name, user_id, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY id DESC) AS rn + FROM user_authority AS ua + WHERE authority_name && ARRAY [ :roles ]::character varying array + AND ua.id IN ( + SELECT max(id) + FROM user_authority + GROUP BY user_id + ) +) ua ON u.id_code = ua.user_id +WHERE u.status <> 'deleted' + AND array_length(authority_name, 1) > 0 + AND u.id IN ( + SELECT max(id) + FROM "user" + GROUP BY id_code + ) +ORDER BY + CASE WHEN :sorting = 'name asc' THEN u.first_name END ASC, + CASE WHEN :sorting = 'name desc' THEN u.first_name END DESC, + CASE WHEN :sorting = 'idCode asc' THEN u.id_code END ASC, + CASE WHEN :sorting = 'idCode desc' THEN u.id_code END DESC, + CASE WHEN :sorting = 'Role asc' THEN ua.authority_name END ASC, + CASE WHEN :sorting = 'Role desc' THEN ua.authority_name END DESC, + CASE WHEN :sorting = 'displayName asc' THEN u.display_name END ASC, + CASE WHEN :sorting = 'displayName desc' THEN u.display_name END DESC, + CASE WHEN :sorting = 'csaTitle asc' THEN u.csa_title END ASC, + CASE WHEN :sorting = 'csaTitle desc' THEN u.csa_title END DESC, + CASE WHEN :sorting = 'csaEmail asc' THEN u.csa_email END ASC, + CASE WHEN :sorting = 'csaEmail desc' THEN u.csa_email END DESC +OFFSET ((GREATEST(:page, 1) - 1) * :page_size) LIMIT :page_size; diff --git a/DSL/Ruuter.private/DSL/POST/accounts/.guard b/DSL/Ruuter.private/DSL/POST/accounts/.guard new file mode 100644 index 00000000..fae02a67 --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/accounts/.guard @@ -0,0 +1,28 @@ +check_for_cookie: + switch: + - condition: ${incoming.headers == null || incoming.headers.cookie == null} + next: guard_fail + next: authenticate + +authenticate: + template: check-user-authority-admin + requestType: templates + headers: + cookie: ${incoming.headers.cookie} + result: authority_result + +check_authority_result: + switch: + - condition: ${authority_result !== "false"} + next: guard_success + next: guard_fail + +guard_success: + return: "success" + status: 200 + next: end + +guard_fail: + return: "unauthorized" + status: 400 + next: end diff --git a/DSL/Ruuter.private/DSL/POST/accounts/users.yml b/DSL/Ruuter.private/DSL/POST/accounts/users.yml new file mode 100644 index 00000000..06278dec --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/accounts/users.yml @@ -0,0 +1,38 @@ +declaration: + call: declare + version: 0.1 + description: "Decription placeholder for 'USERS'" + method: post + accepts: json + returns: json + namespace: backoffice + allowlist: + body: + - field: page + type: number + description: "Body field 'page'" + - field: page_size + type: number + description: "Body field 'page_size'" + - field: sorting + type: string + description: "Body field 'sorting'" + +getUsers: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-users-with-roles-by-role" + body: + page: ${incoming.body.page} + page_size: ${incoming.body.page_size} + sorting: ${incoming.body.sorting} + roles: + [ + "ROLE_ADMINISTRATOR", + "ROLE_MODEL_TRAINER" + ] + result: res + +return_result: + return: ${res.response.body} + next: end diff --git a/DSL/Ruuter.private/DSL/POST/classifier/integration/.guard b/DSL/Ruuter.private/DSL/POST/classifier/integration/.guard new file mode 100644 index 00000000..fae02a67 --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/classifier/integration/.guard @@ -0,0 +1,28 @@ +check_for_cookie: + switch: + - condition: ${incoming.headers == null || incoming.headers.cookie == null} + next: guard_fail + next: authenticate + +authenticate: + template: check-user-authority-admin + requestType: templates + headers: + cookie: ${incoming.headers.cookie} + result: authority_result + +check_authority_result: + switch: + - condition: ${authority_result !== "false"} + next: guard_success + next: guard_fail + +guard_success: + return: "success" + status: 200 + next: end + +guard_fail: + return: "unauthorized" + status: 400 + next: end diff --git a/DSL/Ruuter.private/DSL/TEMPLATES/check-user-authority-admin.yml b/DSL/Ruuter.private/DSL/TEMPLATES/check-user-authority-admin.yml new file mode 100644 index 00000000..f23aa18a --- /dev/null +++ b/DSL/Ruuter.private/DSL/TEMPLATES/check-user-authority-admin.yml @@ -0,0 +1,50 @@ +declaration: + call: declare + version: 0.1 + description: "Decription placeholder for 'CHECK-USER-AUTHORITY'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + headers: + - field: cookie + type: string + description: "Cookie field" + +get_cookie_info: + call: http.post + args: + url: "[#CLASSIFIER_TIM]/jwt/custom-jwt-userinfo" + contentType: plaintext + headers: + cookie: ${incoming.headers.cookie} + plaintext: "customJwtCookie" + result: res + next: check_cookie_info_response + +check_cookie_info_response: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: check_user_authority + next: return_bad_request + +check_user_authority: + switch: + - condition: ${res.response.body.authorities.includes("ROLE_ADMINISTRATOR")} + next: return_authorized + next: return_unauthorized + +return_authorized: + return: ${res.response.body} + next: end + +return_unauthorized: + status: 200 + return: false + next: end + +return_bad_request: + status: 400 + return: false + next: end From 2e01d950be4fbd1d868587724c8b1f62eb7cfdaf Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Mon, 8 Jul 2024 21:03:18 +0530 Subject: [PATCH 092/582] user management api integration for add and edit user --- GUI/package-lock.json | 10 +- GUI/package.json | 2 +- GUI/src/App.tsx | 27 +- .../FormElements/FormInput/index.tsx | 5 +- GUI/src/components/Header/index.tsx | 374 +----------------- GUI/src/components/MainNavigation/index.tsx | 127 +++--- GUI/src/config/users.json | 16 +- .../pages/UserManagement/SettingsUsers.scss | 34 ++ GUI/src/pages/UserManagement/UserModal.tsx | 253 ++++++++++++ GUI/src/pages/UserManagement/index.tsx | 338 +++++++--------- GUI/src/services/users.ts | 8 +- GUI/src/store/index.ts | 2 +- GUI/src/types/mainNavigation.ts | 3 + GUI/src/types/user.ts | 4 +- GUI/src/types/userInfo.ts | 2 +- GUI/src/utils/constants.ts | 5 - GUI/translations/en/common.json | 262 +----------- 17 files changed, 548 insertions(+), 924 deletions(-) create mode 100644 GUI/src/pages/UserManagement/SettingsUsers.scss create mode 100644 GUI/src/pages/UserManagement/UserModal.tsx diff --git a/GUI/package-lock.json b/GUI/package-lock.json index c11a098a..52520494 100644 --- a/GUI/package-lock.json +++ b/GUI/package-lock.json @@ -42,7 +42,7 @@ "react-cookie": "^4.1.1", "react-datepicker": "^4.8.0", "react-dom": "^18.2.0", - "react-hook-form": "^7.41.5", + "react-hook-form": "^7.52.1", "react-i18next": "^12.1.1", "react-icons": "^4.10.1", "react-idle-timer": "^5.5.2", @@ -12399,9 +12399,9 @@ "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==" }, "node_modules/react-hook-form": { - "version": "7.51.5", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.5.tgz", - "integrity": "sha512-J2ILT5gWx1XUIJRETiA7M19iXHlG74+6O3KApzvqB/w8S5NQR7AbU8HVZrMALdmDgWpRPYiZJl0zx8Z4L2mP6Q==", + "version": "7.52.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.52.1.tgz", + "integrity": "sha512-uNKIhaoICJ5KQALYZ4TOaOLElyM+xipord+Ha3crEFhTntdLvWZqVY49Wqd/0GiVCA/f9NjemLeiNPjG7Hpurg==", "engines": { "node": ">=12.22.0" }, @@ -12410,7 +12410,7 @@ "url": "https://opencollective.com/react-hook-form" }, "peerDependencies": { - "react": "^16.8.0 || ^17 || ^18" + "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "node_modules/react-i18next": { diff --git a/GUI/package.json b/GUI/package.json index 69b54525..b56ee42f 100644 --- a/GUI/package.json +++ b/GUI/package.json @@ -45,7 +45,7 @@ "react-cookie": "^4.1.1", "react-datepicker": "^4.8.0", "react-dom": "^18.2.0", - "react-hook-form": "^7.41.5", + "react-hook-form": "^7.52.1", "react-i18next": "^12.1.1", "react-icons": "^4.10.1", "react-idle-timer": "^5.5.2", diff --git a/GUI/src/App.tsx b/GUI/src/App.tsx index 24165d27..50288833 100644 --- a/GUI/src/App.tsx +++ b/GUI/src/App.tsx @@ -1,27 +1,28 @@ import { FC, useEffect } from 'react'; import { Navigate, Route, Routes } from 'react-router-dom'; -import { useQuery } from '@tanstack/react-query'; - import { Layout } from 'components'; import useStore from 'store'; -import { UserInfo } from 'types/userInfo'; - import './locale/et_EE'; import UserManagement from 'pages/UserManagement'; import Integrations from 'pages/Integrations'; import DatasetGroups from 'pages/DataSetGroups'; +import apiDev from 'services/api-dev'; const App: FC = () => { - useQuery<{ - data: { response: UserInfo }; - }>({ - queryKey: ['auth/jwt/userinfo', 'prod'], - onSuccess: (res: { response: UserInfo }) => { - localStorage.setItem('exp', res.response.JWTExpirationTimestamp); - return useStore.getState().setUserInfo(res.response); - }, - }); + const getUserInfo = () => { + apiDev + .get(`auth/jwt/userinfo`) + .then((res: any) => { + localStorage.setItem('exp', res?.data?.response?.JWTExpirationTimestamp); + return useStore.getState().setUserInfo(res?.data?.response); + }) + .catch((error: any) => console.log(error)); + }; + + useEffect(() => { + getUserInfo(); + }, []); return ( diff --git a/GUI/src/components/FormElements/FormInput/index.tsx b/GUI/src/components/FormElements/FormInput/index.tsx index 8933b9a6..b0257753 100644 --- a/GUI/src/components/FormElements/FormInput/index.tsx +++ b/GUI/src/components/FormElements/FormInput/index.tsx @@ -2,6 +2,7 @@ import { forwardRef, InputHTMLAttributes, PropsWithChildren, useId } from 'react import clsx from 'clsx'; import './FormInput.scss'; import { CHAT_INPUT_LENGTH } from 'constants/config'; +import { DefaultTFuncReturn } from 'i18next'; type InputProps = PropsWithChildren> & { label: string; @@ -9,11 +10,12 @@ type InputProps = PropsWithChildren> & { hideLabel?: boolean; maxLength?: number; error?: string; + placeholder?:string | DefaultTFuncReturn; }; const FormInput = forwardRef( ( - { label, name, disabled, hideLabel, maxLength, error, children, ...rest }, + { label, name, disabled, hideLabel, maxLength, error, children,placeholder, ...rest }, ref ) => { const id = useId(); @@ -36,6 +38,7 @@ const FormInput = forwardRef( ref={ref} aria-label={hideLabel ? label : undefined} {...rest} + placeholder={placeholder} /> {error &&

    {error}

    } {children} diff --git a/GUI/src/components/Header/index.tsx b/GUI/src/components/Header/index.tsx index 6a521066..577a867b 100644 --- a/GUI/src/components/Header/index.tsx +++ b/GUI/src/components/Header/index.tsx @@ -27,51 +27,22 @@ import { AUTHORITY } from 'types/authorities'; import { useCookies } from 'react-cookie'; import './Header.scss'; -type CustomerSupportActivity = { - idCode: string; - active: true; - status: string; -}; - type CustomerSupportActivityDTO = { customerSupportActive: boolean; customerSupportStatus: 'offline' | 'idle' | 'online'; customerSupportId: string; }; -const statusColors: Record = { - idle: '#FFB511', - online: '#308653', - offline: '#D73E3E', -}; - const Header: FC = () => { const { t } = useTranslation(); const userInfo = useStore((state) => state.userInfo); const toast = useToast(); - let secondsUntilStatusPopup = 300; - const [statusPopupTimerHasStarted, setStatusPopupTimerHasStarted] = - useState(false); - const [showStatusConfirmationModal, setShowStatusConfirmationModal] = - useState(false); const queryClient = useQueryClient(); - const [userDrawerOpen, setUserDrawerOpen] = useState(false); const [csaStatus, setCsaStatus] = useState<'idle' | 'offline' | 'online'>( 'online' ); - const chatCsaActive = useStore((state) => state.chatCsaActive); - const [userProfileSettings, setUserProfileSettings] = - useState({ - userId: 1, - forwardedChatPopupNotifications: true, - forwardedChatSoundNotifications: true, - forwardedChatEmailNotifications: false, - newChatPopupNotifications: false, - newChatSoundNotifications: true, - newChatEmailNotifications: false, - useAutocorrect: true, - }); + const customJwtCookieKey = 'customJwtCookie'; useEffect(() => { @@ -83,135 +54,21 @@ const Header: FC = () => { expirationTimeStamp !== undefined ) { const expirationDate = new Date(parseInt(expirationTimeStamp) ?? ''); - const currentDate = new Date(Date.now()); + const currentDate = new Date(Date.now()); if (expirationDate < currentDate) { localStorage.removeItem('exp'); window.location.href =import.meta.env.REACT_APP_CUSTOMER_SERVICE_LOGIN; } + }else{ + window.location.href =import.meta.env.REACT_APP_CUSTOMER_SERVICE_LOGIN; + } }, 2000); return () => clearInterval(interval); }, [userInfo]); - // useEffect(() => { - // getMessages(); - // }, [userInfo?.idCode]); - - // const getMessages = async () => { - // const { data: res } = await apiDev.get('accounts/settings'); - - // if (res.response && res.response != 'error: not found') - // setUserProfileSettings(res.response[0]); - // }; - // const { data: customerSupportActivity } = useQuery({ - // queryKey: ['accounts/customer-support-activity', 'prod'], - // onSuccess(res: any) { - // const activity = res.data.get_customer_support_activity[0]; - // setCsaStatus(activity.status); - // useStore.getState().setChatCsaActive(activity.active === 'true'); - // }, - // }); - - // useQuery({ - // queryKey: ['agents/chats/active', 'prod'], - // onSuccess(res: any) { - // useStore.getState().setActiveChats(res.data.get_all_active_chats); - // }, - // }); - const [_, setCookie] = useCookies([customJwtCookieKey]); - const unansweredChatsLength = useStore((state) => - state.unansweredChatsLength() - ); - const forwardedChatsLength = useStore((state) => - state.forwordedChatsLength() - ); - - const handleNewMessage = () => { - if (unansweredChatsLength <= 0) { - return; - } - - - if (userProfileSettings.newChatEmailNotifications) { - // To be done: send email notification - } - if (userProfileSettings.newChatPopupNotifications) { - toast.open({ - type: 'info', - title: t('global.notification'), - message: t('settings.users.newUnansweredChat'), - }); - } - }; - - useEffect(() => { - handleNewMessage(); - - const subscription = interval(2 * 60 * 1000).subscribe(() => - handleNewMessage() - ); - return () => { - subscription?.unsubscribe(); - }; - }, [unansweredChatsLength, userProfileSettings]); - - const handleForwordMessage = () => { - if (forwardedChatsLength <= 0) { - return; - } - - if (userProfileSettings.forwardedChatEmailNotifications) { - // To be done: send email notification - } - if (userProfileSettings.forwardedChatPopupNotifications) { - toast.open({ - type: 'info', - title: t('global.notification'), - message: t('settings.users.newForwardedChat'), - }); - } - }; - - useEffect(() => { - handleForwordMessage(); - - const subscription = interval(2 * 60 * 1000).subscribe( - () => handleForwordMessage - ); - return () => { - subscription?.unsubscribe(); - }; - }, [forwardedChatsLength, userProfileSettings]); - - const userProfileSettingsMutation = useMutation({ - mutationFn: async (data: UserProfileSettings) => { - await apiDev.post('accounts/settings', { - forwardedChatPopupNotifications: data.forwardedChatPopupNotifications, - forwardedChatSoundNotifications: data.forwardedChatSoundNotifications, - forwardedChatEmailNotifications: data.newChatEmailNotifications, - newChatPopupNotifications: data.newChatPopupNotifications, - newChatSoundNotifications: data.newChatSoundNotifications, - newChatEmailNotifications: data.newChatEmailNotifications, - useAutocorrect: data.useAutocorrect, - }); - setUserProfileSettings(data); - }, - onError: async (error: AxiosError) => { - await queryClient.invalidateQueries(['accounts/settings']); - toast.open({ - type: 'error', - title: t('global.notificationError'), - message: error.message, - }); - }, - }); - const unClaimAllAssignedChats = useMutation({ - mutationFn: async () => { - await apiDev.post('chats/assigned/unclaim'); - }, - }); const customerSupportActivityMutation = useMutation({ mutationFn: (data: CustomerSupportActivityDTO) => @@ -265,78 +122,6 @@ const Header: FC = () => { }, }); - // const onIdle = () => { - // if (!customerSupportActivity) return; - // if (csaStatus === 'offline') return; - - // setCsaStatus('idle'); - // customerSupportActivityMutation.mutate({ - // customerSupportActive: chatCsaActive, - // customerSupportId: customerSupportActivity.idCode, - // customerSupportStatus: 'idle', - // }); - // }; - - // const onActive = () => { - // if (!customerSupportActivity) return; - // if (csaStatus === 'offline') { - // setShowStatusConfirmationModal((value) => !value); - // return; - // } - - // setCsaStatus('online'); - // customerSupportActivityMutation.mutate({ - // customerSupportActive: chatCsaActive, - // customerSupportId: customerSupportActivity.idCode, - // customerSupportStatus: 'online', - // }); - // }; - - // useIdleTimer({ - // onIdle, - // onActive, - // timeout: USER_IDLE_STATUS_TIMEOUT, - // throttle: 500, - // }); - - const handleUserProfileSettingsChange = (key: string, checked: boolean) => { - if (!userProfileSettings) return; - const newSettings = { - ...userProfileSettings, - [key]: checked, - }; - userProfileSettingsMutation.mutate(newSettings); - }; - - const handleCsaStatusChange = (checked: boolean) => { - if (checked === false) unClaimAllAssignedChats.mutate(); - - useStore.getState().setChatCsaActive(checked); - setCsaStatus(checked === true ? 'online' : 'offline'); - customerSupportActivityMutation.mutate({ - customerSupportActive: checked, - customerSupportStatus: checked === true ? 'online' : 'offline', - customerSupportId: '', - }); - - if (!checked) showStatusChangePopup(); - }; - - const showStatusChangePopup = () => { - if (statusPopupTimerHasStarted) return; - - setStatusPopupTimerHasStarted((value) => !value); - const timer = setInterval(() => { - let time = secondsUntilStatusPopup; - while (time > 0) { - time -= 1; - } - clearInterval(timer); - setShowStatusConfirmationModal((value) => !value); - setStatusPopupTimerHasStarted((value) => !value); - }, 1000); - }; - return ( <>
    @@ -349,11 +134,6 @@ const Header: FC = () => { appearance="text" style={{ textDecoration: 'underline' }} onClick={() => { - customerSupportActivityMutation.mutate({ - customerSupportActive: false, - customerSupportStatus: 'offline', - customerSupportId: userInfo.idCode, - }); localStorage.removeItem('exp'); logoutMutation.mutate(); }} @@ -364,150 +144,6 @@ const Header: FC = () => { )}
    - - - {/* {userInfo && userProfileSettings && userDrawerOpen && ( - setUserDrawerOpen(false)} - style={{ width: 400 }} - > -
    - - {[ - { - label: t('settings.users.displayName'), - value: userInfo.displayName, - }, - { - label: t('settings.users.userRoles'), - value: userInfo.authorities - .map((r) => t(`roles.${r}`)) - .join(', '), - }, - { - label: t('settings.users.userTitle'), - value: userInfo.csaTitle?.replaceAll(' ', '\xa0'), - }, - { label: t('settings.users.email'), value: userInfo.csaEmail }, - ].map((meta, index) => ( - -

    {meta.label}:

    -

    {meta.value}

    - - ))} - -
    - {[ - AUTHORITY.ADMINISTRATOR, - AUTHORITY.CUSTOMER_SUPPORT_AGENT, - AUTHORITY.SERVICE_MANAGER, - ].some((auth) => userInfo.authorities.includes(auth)) && ( - <> -
    - -

    {t('settings.users.autoCorrector')}

    - - handleUserProfileSettingsChange('useAutocorrect', checked) - } - /> - -
    -
    - -

    {t('settings.users.emailNotifications')}

    - - handleUserProfileSettingsChange( - 'forwardedChatEmailNotifications', - checked - ) - } - /> - - handleUserProfileSettingsChange( - 'newChatEmailNotifications', - checked - ) - } - /> - -
    -
    - -

    {t('settings.users.soundNotifications')}

    - - handleUserProfileSettingsChange( - 'forwardedChatSoundNotifications', - checked - ) - } - /> - - handleUserProfileSettingsChange( - 'newChatSoundNotifications', - checked - ) - } - /> - -
    -
    - -

    {t('settings.users.popupNotifications')}

    - - handleUserProfileSettingsChange( - 'forwardedChatPopupNotifications', - checked - ) - } - /> - - handleUserProfileSettingsChange( - 'newChatPopupNotifications', - checked - ) - } - /> - -
    - - )} -
    - )} */} ); }; diff --git a/GUI/src/components/MainNavigation/index.tsx b/GUI/src/components/MainNavigation/index.tsx index e93eaadc..510a83da 100644 --- a/GUI/src/components/MainNavigation/index.tsx +++ b/GUI/src/components/MainNavigation/index.tsx @@ -1,7 +1,18 @@ import { FC, MouseEvent, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { NavLink, useLocation } from 'react-router-dom'; -import { MdApps, MdClass, MdClose, MdDashboard, MdDataset, MdKeyboardArrowDown, MdOutlineForum, MdPeople, MdSettings, MdTextFormat } from 'react-icons/md'; +import { + MdApps, + MdClass, + MdClose, + MdDashboard, + MdDataset, + MdKeyboardArrowDown, + MdOutlineForum, + MdPeople, + MdSettings, + MdTextFormat, +} from 'react-icons/md'; import { useQuery } from '@tanstack/react-query'; import clsx from 'clsx'; import { Icon } from 'components'; @@ -9,6 +20,7 @@ import type { MenuItem } from 'types/mainNavigation'; import { menuIcons } from 'constants/menuIcons'; import './MainNavigation.scss'; import { error } from 'console'; +import apiDev from 'services/api-dev'; const MainNavigation: FC = () => { const { t } = useTranslation(); @@ -19,14 +31,13 @@ const MainNavigation: FC = () => { id: 'userManagement', label: t('menu.userManagement'), path: '/user-management', - icon: + icon: , }, { id: 'integration', label: t('menu.integration'), path: 'integration', - icon: - + icon: , }, { id: 'datasets', @@ -37,92 +48,69 @@ const MainNavigation: FC = () => { { label: t('menu.datasetGroups'), path: 'dataset-groups', - icon: + icon: , }, { label: t('menu.versions'), path: 'versions', - icon: - } + icon: , + }, ], }, { id: 'dataModels', label: t('menu.dataModels'), path: '/data-models', - icon: - + icon: , }, { id: 'classes', label: t('menu.classes'), path: '/classes', - icon: - + icon: , }, { id: 'stopWords', label: t('menu.stopWords'), path: '/stop-words', - icon: - + icon: , }, { id: 'incomingTexts', label: t('menu.incomingTexts'), path: '/incoming-texts', - icon: - + icon: , }, ]; - // useEffect(()=>{ - // const filteredItems = - // items.filter((item) => { - // const role = "ROLE_ADMINISTRATOR"; - // switch (role) { - // case 'ROLE_ADMINISTRATOR': - // return item.id; - // case 'ROLE_SERVICE_MANAGER': - // return item.id != 'settings' && item.id != 'training'; - // case 'ROLE_CUSTOMER_SUPPORT_AGENT': - // return item.id != 'settings' && item.id != 'analytics'; - // case 'ROLE_CHATBOT_TRAINER': - // return item.id != 'settings' && item.id != 'conversations'; - // case 'ROLE_ANALYST': - // return item.id == 'analytics' || item.id == 'monitoring'; - // case 'ROLE_UNAUTHENTICATED': - // return; - // } - // }) ?? []; - // setMenuItems(filteredItems); - - // },[]) + const getUserRole = () => { + apiDev + .get(`/accounts/user-role`) + .then((res: any) => { + const filteredItems = + items.filter((item) => { + const role = res?.data?.response[0]; + + switch (role) { + case 'ROLE_ADMINISTRATOR': + return item.id; + case 'ROLE_MODEL_TRAINER': + return ( + item.id !== 'userManagement' && item.id !== 'integration' + ); + case 'ROLE_UNAUTHENTICATED': + return null; + } + }) ?? []; + setMenuItems(filteredItems); + }) + .catch((error: any) => console.log(error)); + }; - useQuery({ - queryKey: ['/accounts/user-role', 'prod'], - onSuccess: (res: any) => { - const filteredItems = - items.filter((item) => { + useEffect(() => { + getUserRole(); + }, []); - const role = res?.response[0]; - - switch (role) { - case 'ROLE_ADMINISTRATOR': - return item.id; - case 'ROLE_MODEL_TRAINER': - return item.id !== 'userManagement' && item.id !== 'integration'; - case 'ROLE_UNAUTHENTICATED': - return; - } - }) ?? []; - setMenuItems(filteredItems); - }, - onError:(error:any)=>{ - console.log(error); - - } - }); const location = useLocation(); const [navCollapsed, setNavCollapsed] = useState(false); @@ -153,9 +141,7 @@ const MainNavigation: FC = () => { onClick={handleNavToggle} > {/* {menuItem.id && ( */} - + {menuItem.label} } /> @@ -164,9 +150,11 @@ const MainNavigation: FC = () => { ) : ( - {menuItem.label} + + {' '} + + {menuItem.label} + )} )); @@ -176,13 +164,6 @@ const MainNavigation: FC = () => { return ( ); diff --git a/GUI/src/config/users.json b/GUI/src/config/users.json index 615079a7..7a72b470 100644 --- a/GUI/src/config/users.json +++ b/GUI/src/config/users.json @@ -3,7 +3,7 @@ "login": "EE40404049985", "firstName": "admin", "lastName": "admin", - "idCode": "EE40404049985", + "userIdCode": "EE40404049985", "displayName": "admin", "csaTitle": "admin", "csaEmail": "admin@admin.ee", @@ -17,7 +17,7 @@ "login": "EE38807130279", "firstName": "Jaanus", "lastName": "Kääp", - "idCode": "EE38807130279", + "userIdCode": "EE38807130279", "displayName": "Jaanus", "csaTitle": "tester", "csaEmail": "jaanus@clarifiedsecurity.com", @@ -32,7 +32,7 @@ "login": "EE30303039816", "firstName": "kolmas", "lastName": "admin", - "idCode": "EE30303039816", + "userIdCode": "EE30303039816", "displayName": "kolmas", "csaTitle": "kolmas", "csaEmail": "kolmas@admin.ee", @@ -46,7 +46,7 @@ "login": "EE30303039914", "firstName": "Kustuta", "lastName": "Kasutaja", - "idCode": "EE30303039914", + "userIdCode": "EE30303039914", "displayName": "Kustutamiseks", "csaTitle": "", "csaEmail": "kustutamind@mail.ee", @@ -60,7 +60,7 @@ "login": "EE50001029996", "firstName": "Nipi", "lastName": "Tiri", - "idCode": "EE50001029996", + "userIdCode": "EE50001029996", "displayName": "Nipi", "csaTitle": "Dr", "csaEmail": "nipi@tiri.ee", @@ -74,7 +74,7 @@ "login": "EE40404049996", "firstName": "teine", "lastName": "admin", - "idCode": "EE40404049996", + "userIdCode": "EE40404049996", "displayName": "teine admin", "csaTitle": "teine admin", "csaEmail": "Teine@admin.ee", @@ -88,7 +88,7 @@ "login": "EE50701019992", "firstName": "Valter", "lastName": "Aro", - "idCode": "EE50701019992", + "userIdCode": "EE50701019992", "displayName": "Valter", "csaTitle": "Mister", "csaEmail": "valter.aro@ria.ee", @@ -102,7 +102,7 @@ "login": "EE38104266023", "firstName": "Varmo", "lastName": "", - "idCode": "EE38104266023", + "userIdCode": "EE38104266023", "displayName": "Varmo", "csaTitle": "MISTER", "csaEmail": "mail@mail.ee", diff --git a/GUI/src/pages/UserManagement/SettingsUsers.scss b/GUI/src/pages/UserManagement/SettingsUsers.scss new file mode 100644 index 00000000..545e31b6 --- /dev/null +++ b/GUI/src/pages/UserManagement/SettingsUsers.scss @@ -0,0 +1,34 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/other'; +@import 'src/styles/settings/variables/typography'; + +.multiSelect { + $self: &; + display: flex; + align-items: center; + gap: get-spacing(paldiski); + width: 100%; + &::placeholder { + color: get-color(black-coral-6); + font-size: small; + } + + &__label { + flex: 0 0 185px; + font-size: $veera-font-size-100; + line-height: 24px; + } + + + &__wrapper { + width: 390px; + flex: 1; + display: block; + flex-direction: column; + gap: 7px; + position: relative; + border: 0.15px solid get-color(black-coral-6); + border-radius: $veera-radius-s; + } +} diff --git a/GUI/src/pages/UserManagement/UserModal.tsx b/GUI/src/pages/UserManagement/UserModal.tsx new file mode 100644 index 00000000..397a1075 --- /dev/null +++ b/GUI/src/pages/UserManagement/UserModal.tsx @@ -0,0 +1,253 @@ +import { FC, useMemo } from 'react'; +import { useForm, Controller } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { AxiosError } from 'axios'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { Button, Dialog, FormInput, Track } from 'components'; +import { User, UserDTO } from 'types/user'; +import { checkIfUserExists, createUser, editUser } from 'services/users'; +import { useToast } from 'hooks/useToast'; +import { ROLES } from 'utils/constants'; +import Select from 'react-select'; +import './SettingsUsers.scss'; + +type UserModalProps = { + onClose: () => void; + user?: User | undefined; + isModalOpen?: boolean; +}; + +const UserModal: FC = ({ onClose, user, isModalOpen }) => { + const { t } = useTranslation(); + const toast = useToast(); + const queryClient = useQueryClient(); + const { + register, + control, + handleSubmit, + formState: { errors }, + } = useForm({ + defaultValues: { + userIdCode: user?.userIdCode, + authorities: user?.authorities, + displayName: user?.fullName, + csaTitle: user?.csaTitle, + csaEmail: user?.csaEmail, + fullName: user?.fullName, + }, + }); + + const roles = useMemo( + () => [ + { label: t('roles.ROLE_ADMINISTRATOR'), value: ROLES.ROLE_ADMINISTRATOR }, + { + label: t('roles.ROLE_MODEL_TRAINER'), + value: ROLES.ROLE_MODEL_TRAINER, + }, + ], + [] + ); + + const userCreateMutation = useMutation({ + mutationFn: (data: UserDTO) => createUser(data), + onSuccess: async () => { + await queryClient.invalidateQueries([ + 'accounts/customer-support-agents', + 'prod', + ]); + toast.open({ + type: 'success', + title: t('global.notification'), + message: t('toast.success.newUserAdded'), + }); + onClose(); + }, + onError: (error: AxiosError) => { + toast.open({ + type: 'error', + title: t('global.notificationError'), + message: error.message, + }); + }, + }); + + const userEditMutation = useMutation({ + mutationFn: ({ + id, + userData, + }: { + id: string | number; + userData: UserDTO; + }) => editUser(id, userData), + onSuccess: async () => { + await queryClient.invalidateQueries([ + 'accounts/customer-support-agents', + 'prod', + ]); + toast.open({ + type: 'success', + title: t('global.notification'), + message: t('toast.success.userUpdated'), + }); + onClose(); + }, + onError: (error: AxiosError) => { + toast.open({ + type: 'error', + title: t('global.notificationError'), + message: error.message, + }); + }, + }); + + const checkIfUserExistsMutation = useMutation({ + mutationFn: ({ userData }: { userData: UserDTO }) => + checkIfUserExists(userData), + onSuccess: async (data) => { + if (data.response === 'true') { + toast.open({ + type: 'error', + title: t('global.notificationError'), + message: t('settings.users.userExists'), + }); + } else { + createNewUser(); + } + }, + onError: (error: AxiosError) => { + toast.open({ + type: 'error', + title: t('global.notificationError'), + message: error.message, + }); + }, + }); + + const createNewUser = handleSubmit((userData) => { + userCreateMutation.mutate(userData); + }); + + const handleUserSubmit = handleSubmit((data) => { + console.log(data); + + if (user) { + userEditMutation.mutate({ id: user.userIdCode, userData: data }); + } else { + checkIfUserExistsMutation.mutate({ userData: data }); + } + }); + + const requiredText = t('settings.users.required') ?? '*'; + + return ( + + + + + } + > + + + {errors.fullName && ( + + {errors.fullName.message} + + )} + + ( +
    + +
    + + +
    + ); +}; + +export default CopyableTextField; \ No newline at end of file diff --git a/outlook-consent-app/src/app/components/LoginButton/index.tsx b/outlook-consent-app/src/app/components/LoginButton/index.tsx new file mode 100644 index 00000000..8dd6902c --- /dev/null +++ b/outlook-consent-app/src/app/components/LoginButton/index.tsx @@ -0,0 +1,18 @@ +'use client'; + +import React from 'react'; +import styles from "../../page.module.css"; + +const LoginButton = () => { + const handleLogin = () => { + window.location.href = '/api/auth'; + }; + + return ( + + ); +}; + +export default LoginButton; diff --git a/outlook-consent-app/src/app/favicon.ico b/outlook-consent-app/src/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 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 LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/outlook-consent-app/src/app/globals.css b/outlook-consent-app/src/app/globals.css new file mode 100644 index 00000000..2748dd3e --- /dev/null +++ b/outlook-consent-app/src/app/globals.css @@ -0,0 +1,92 @@ +:root { + --max-width: 1100px; + --border-radius: 12px; + --font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", + "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro", + "Fira Mono", "Droid Sans Mono", "Courier New", monospace; + + --foreground-rgb: 0, 0, 0; + --background-color: white; + --tile-start-rgb: 239, 245, 249; + --tile-end-rgb: 228, 232, 233; + --tile-border: conic-gradient( + #00000080, + #00000040, + #00000030, + #00000020, + #00000010, + #00000010, + #00000080 + ); + + --callout-rgb: 238, 240, 241; + --callout-border-rgb: 172, 175, 176; + --card-rgb: 180, 185, 188; + --card-border-rgb: 131, 134, 135; +} + +@media (prefers-color-scheme: dark) { + :root { + --foreground-rgb: 255, 255, 255; + --background-start-rgb: 0, 0, 0; + --background-end-rgb: 0, 0, 0; + + --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0)); + --secondary-glow: linear-gradient( + to bottom right, + rgba(1, 65, 255, 0), + rgba(1, 65, 255, 0), + rgba(1, 65, 255, 0.3) + ); + + --tile-start-rgb: 2, 13, 46; + --tile-end-rgb: 2, 5, 19; + --tile-border: conic-gradient( + #ffffff80, + #ffffff40, + #ffffff30, + #ffffff20, + #ffffff10, + #ffffff10, + #ffffff80 + ); + + --callout-rgb: 20, 20, 20; + --callout-border-rgb: 108, 108, 108; + --card-rgb: 100, 100, 100; + --card-border-rgb: 200, 200, 200; + } +} + +* { + box-sizing: border-box; + padding: 0; + margin: 0; +} + +html, +body { + max-width: 100vw; + overflow-x: hidden; +} + +body { + color: rgb(var(--foreground-rgb)); + background: linear-gradient( + to bottom, + transparent, + rgb(var(--background-end-rgb)) + ) + rgb(var(--background-start-rgb)); +} + +a { + color: inherit; + text-decoration: none; +} + +@media (prefers-color-scheme: dark) { + html { + color-scheme: dark; + } +} diff --git a/outlook-consent-app/src/app/layout.tsx b/outlook-consent-app/src/app/layout.tsx new file mode 100644 index 00000000..3314e478 --- /dev/null +++ b/outlook-consent-app/src/app/layout.tsx @@ -0,0 +1,22 @@ +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import "./globals.css"; + +const inter = Inter({ subsets: ["latin"] }); + +export const metadata: Metadata = { + title: "Create Next App", + description: "Generated by create next app", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + {children} + + ); +} diff --git a/outlook-consent-app/src/app/page.module.css b/outlook-consent-app/src/app/page.module.css new file mode 100644 index 00000000..b3cd538c --- /dev/null +++ b/outlook-consent-app/src/app/page.module.css @@ -0,0 +1,251 @@ +.main { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 6rem; + min-height: 100vh; +} + +.description { + display: inherit; + justify-content: inherit; + align-items: inherit; + font-size: 0.85rem; + max-width: var(--max-width); + width: 100%; + z-index: 2; + font-family: var(--font-mono); +} + +.description a { + display: flex; + justify-content: center; + align-items: center; + gap: 0.5rem; +} + +.description p { + position: relative; + margin: 0; + padding: 1rem; + background-color: rgba(var(--callout-rgb), 0.5); + border: 1px solid rgba(var(--callout-border-rgb), 0.3); + border-radius: var(--border-radius); +} + +.code { + font-weight: 700; + font-family: var(--font-mono); +} + +.grid { + display: grid; + grid-template-columns: repeat(4, minmax(25%, auto)); + max-width: 100%; + width: var(--max-width); +} + +.card { + padding: 1rem 1.2rem; + border-radius: var(--border-radius); + background: rgba(var(--card-rgb), 0); + border: 1px solid rgba(var(--card-border-rgb), 0); + transition: background 200ms, border 200ms; +} + +.card span { + display: inline-block; + transition: transform 200ms; +} + +.card h2 { + font-weight: 600; + margin-bottom: 0.7rem; +} + +.card p { + margin: 0; + opacity: 0.6; + font-size: 0.9rem; + line-height: 1.5; + max-width: 30ch; + text-wrap: balance; +} + +.center { + display: flex; + justify-content: center; + align-items: center; + position: relative; + padding: 4rem 0; + gap: 5px; +} + + +.logo { + position: relative; +} + +.copyableTextField { + display: flex; + align-items: center; + justify-content: center; +} + +.copyableTextField input { + margin-right: 10px; + padding: 10px; + border: 1px solid #ccc; + border-radius: 4px; + width: 450px; +} + + +/* Enable hover only on non-touch devices */ +@media (hover: hover) and (pointer: fine) { + .card:hover { + background: rgba(var(--card-rgb), 0.1); + border: 1px solid rgba(var(--card-border-rgb), 0.15); + } + + .card:hover span { + transform: translateX(4px); + } +} + +@media (prefers-reduced-motion) { + .card:hover span { + transform: none; + } +} + +/* Mobile */ +@media (max-width: 700px) { + .content { + padding: 4rem; + } + + .grid { + grid-template-columns: 1fr; + margin-bottom: 120px; + max-width: 320px; + text-align: center; + } + + .card { + padding: 1rem 2.5rem; + } + + .card h2 { + margin-bottom: 0.5rem; + } + + .center { + padding: 4rem 0 4 rem; + } + + .center::before { + transform: none; + height: 300px; + } + + .description { + font-size: 0.8rem; + } + + .description a { + padding: 1rem; + } + + .description p, + .description div { + display: flex; + justify-content: center; + position: fixed; + width: 100%; + } + + .description p { + align-items: center; + inset: 0 0 auto; + padding: 2rem 1rem 1.4rem; + border-radius: 0; + border: none; + border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25); + background: linear-gradient( + to bottom, + rgba(var(--background-start-rgb), 1), + rgba(var(--callout-rgb), 0.5) + ); + background-clip: padding-box; + backdrop-filter: blur(24px); + } + + .description div { + align-items: flex-end; + pointer-events: none; + inset: auto 0 0; + padding: 2rem; + height: 200px; + background: linear-gradient( + to bottom, + transparent 0%, + rgb(var(--background-end-rgb)) 40% + ); + z-index: 1; + } +} + +.btn { + appearance: none; + align-items: center; + background: rgb(0, 0, 93); + border: 0; + color: white; + cursor: pointer; + font: inherit; + overflow: visible; + padding: 8px 20px; + text-decoration: none; + font-size: 14px; + line-height: 24px; + border-radius: 20px; + white-space: nowrap; + text-align: center; + min-width: 100px; + + &:focus { + outline: none; + } + + &--disabled { + cursor: not-allowed; + } +} + +/* Tablet and Smaller Desktop */ +@media (min-width: 701px) and (max-width: 1120px) { + .grid { + grid-template-columns: repeat(2, 50%); + } +} + +@media (prefers-color-scheme: dark) { + .vercelLogo { + filter: invert(1); + } + + .logo { + filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70); + } +} + +@keyframes rotate { + from { + transform: rotate(360deg); + } + to { + transform: rotate(0deg); + } +} diff --git a/outlook-consent-app/src/app/page.tsx b/outlook-consent-app/src/app/page.tsx new file mode 100644 index 00000000..e232def5 --- /dev/null +++ b/outlook-consent-app/src/app/page.tsx @@ -0,0 +1,17 @@ +import Image from "next/image"; +import styles from "./page.module.css"; +import LoginButton from "./components/LoginButton"; + +export default function Home() { + return ( +
    +
    +
    + +
    +
    + + +
    + ); +} diff --git a/outlook-consent-app/tsconfig.json b/outlook-consent-app/tsconfig.json new file mode 100644 index 00000000..7b285893 --- /dev/null +++ b/outlook-consent-app/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} From 5e8ffe8d7ca3590d25ee841d12b2c76e01889a8b Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Tue, 9 Jul 2024 22:26:17 +0530 Subject: [PATCH 102/582] ESCLASS-136: create data set group meta data table script --- ...ifier-script-v5-dataset-group-metadata.sql | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 DSL/Liquibase/changelog/classifier-script-v5-dataset-group-metadata.sql diff --git a/DSL/Liquibase/changelog/classifier-script-v5-dataset-group-metadata.sql b/DSL/Liquibase/changelog/classifier-script-v5-dataset-group-metadata.sql new file mode 100644 index 00000000..548ac79e --- /dev/null +++ b/DSL/Liquibase/changelog/classifier-script-v5-dataset-group-metadata.sql @@ -0,0 +1,31 @@ +-- liquibase formatted sql + +-- changeset kalsara Magamage:classifier-script-v5-changeset1 +CREATE TYPE Validation_Status AS ENUM ('success', 'fail', 'in-progress'); + +-- changeset kalsara Magamage:classifier-script-v5-changeset2 +CREATE TABLE dataset_group_metadata ( + id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY, + group_name TEXT NOT NULL, + group_version TEXT NOT NULL, + latest BOOLEAN DEFAULT false, + is_enabled BOOLEAN DEFAULT false, + enable_allowed BOOLEAN DEFAULT false, + last_model_trained TEXT, + created_timestamp TIMESTAMP WITH TIME ZONE, + last_updated_timestamp TIMESTAMP WITH TIME ZONE, + last_used_for_training TIMESTAMP WITH TIME ZONE, + validation_status Validation_Status, + validation_errors JSONB, + processed_data_available BOOLEAN DEFAULT false, + raw_data_available BOOLEAN DEFAULT false, + num_samples INT, + raw_data_location TEXT, + preprocess_data_location TEXT, + validation_criteria JSONB, + class_hierarchy JSONB, + connected_models JSONB, + CONSTRAINT dataset_group_metadata_pkey PRIMARY KEY (id) +); + + From 07d60f328f3ce4d344b34714aab4aa760f8aa15c Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Tue, 9 Jul 2024 22:26:50 +0530 Subject: [PATCH 103/582] ESCLASS-136: create data set group meta data table script --- .../changelog/classifier-script-v5-dataset-group-metadata.sql | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/DSL/Liquibase/changelog/classifier-script-v5-dataset-group-metadata.sql b/DSL/Liquibase/changelog/classifier-script-v5-dataset-group-metadata.sql index 548ac79e..2c5d5adc 100644 --- a/DSL/Liquibase/changelog/classifier-script-v5-dataset-group-metadata.sql +++ b/DSL/Liquibase/changelog/classifier-script-v5-dataset-group-metadata.sql @@ -26,6 +26,4 @@ CREATE TABLE dataset_group_metadata ( class_hierarchy JSONB, connected_models JSONB, CONSTRAINT dataset_group_metadata_pkey PRIMARY KEY (id) -); - - +); \ No newline at end of file From e26b8dc563447ee31a844cd3c47978395ae7c93a Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Tue, 9 Jul 2024 22:34:26 +0530 Subject: [PATCH 104/582] code cleanups --- outlook-consent-app/src/app/api/auth/token/route.ts | 2 -- outlook-consent-app/src/app/callback/page.tsx | 3 +-- .../src/app/components/CopyableTextField/index.tsx | 2 +- outlook-consent-app/src/app/page.tsx | 8 +++----- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/outlook-consent-app/src/app/api/auth/token/route.ts b/outlook-consent-app/src/app/api/auth/token/route.ts index cd629a2c..b99da4bd 100644 --- a/outlook-consent-app/src/app/api/auth/token/route.ts +++ b/outlook-consent-app/src/app/api/auth/token/route.ts @@ -6,8 +6,6 @@ export async function POST(req: NextRequest) { const clientId = process.env.NEXT_PUBLIC_CLIENT_ID; const clientSecret = process.env.CLIENT_SECRET; const redirectUri = process.env.REDIRECT_URI; -console.log("logggg",code,clientId,clientSecret,redirectUri); - try { const tokenResponse = await axios.post( 'https://login.microsoftonline.com/common/oauth2/v2.0/token', diff --git a/outlook-consent-app/src/app/callback/page.tsx b/outlook-consent-app/src/app/callback/page.tsx index 84c97e6d..7a50ccf6 100644 --- a/outlook-consent-app/src/app/callback/page.tsx +++ b/outlook-consent-app/src/app/callback/page.tsx @@ -16,8 +16,7 @@ const CallbackPage = () => { const exchangeAuthCode = async (code: string) => { try { const response = await axios.post("/api/auth/token", { code }); - console.log(response.data); - setToken(response.data?.refresh_token); + setToken(response?.data?.refresh_token); } catch (error) { setError("Error exchanging auth code!"); } diff --git a/outlook-consent-app/src/app/components/CopyableTextField/index.tsx b/outlook-consent-app/src/app/components/CopyableTextField/index.tsx index a5bbe924..7826e24f 100644 --- a/outlook-consent-app/src/app/components/CopyableTextField/index.tsx +++ b/outlook-consent-app/src/app/components/CopyableTextField/index.tsx @@ -11,7 +11,7 @@ const CopyableTextField: React.FC<{ value: string }> = ({ value }) => { textFieldRef.current.select(); document.execCommand('copy'); setCopied(true); - setTimeout(() => setCopied(false), 2000); // Reset the copied state after 2 seconds + setTimeout(() => setCopied(false), 2000); } }; diff --git a/outlook-consent-app/src/app/page.tsx b/outlook-consent-app/src/app/page.tsx index e232def5..ec6349c4 100644 --- a/outlook-consent-app/src/app/page.tsx +++ b/outlook-consent-app/src/app/page.tsx @@ -6,12 +6,10 @@ export default function Home() { return (
    -
    - -
    +
    + +
    - -
    ); } From ace4f891d7dc52444560ac4f1f5aa93efd1bb32a Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 10 Jul 2024 11:25:27 +0530 Subject: [PATCH 105/582] experiment to nginx configurations --- experiment/Dockerfile.app1 | 17 +++++++++++++++++ experiment/Dockerfile.app2 | 17 +++++++++++++++++ experiment/app1.py | 15 +++++++++++++++ experiment/app2.py | 15 +++++++++++++++ experiment/docker-compose.yml | 16 ++++++++++++++++ experiment/requirements.txt | 2 ++ 6 files changed, 82 insertions(+) create mode 100644 experiment/Dockerfile.app1 create mode 100644 experiment/Dockerfile.app2 create mode 100644 experiment/app1.py create mode 100644 experiment/app2.py create mode 100644 experiment/docker-compose.yml create mode 100644 experiment/requirements.txt diff --git a/experiment/Dockerfile.app1 b/experiment/Dockerfile.app1 new file mode 100644 index 00000000..0d873fc8 --- /dev/null +++ b/experiment/Dockerfile.app1 @@ -0,0 +1,17 @@ +# Use the official Python image from the Docker Hub +FROM python:3.9 + +# Set the working directory +WORKDIR /app + +# Copy the requirements file into the container +COPY requirements.txt . + +# Install the dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Copy the rest of the application code into the container +COPY app1.py . + +# Command to run the application +CMD ["uvicorn", "app1:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/experiment/Dockerfile.app2 b/experiment/Dockerfile.app2 new file mode 100644 index 00000000..072112ff --- /dev/null +++ b/experiment/Dockerfile.app2 @@ -0,0 +1,17 @@ +# Use the official Python image from the Docker Hub +FROM python:3.9 + +# Set the working directory +WORKDIR /app + +# Copy the requirements file into the container +COPY requirements.txt . + +# Install the dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Copy the rest of the application code into the container +COPY app2.py . + +# Command to run the application +CMD ["uvicorn", "app2:app", "--host", "0.0.0.0", "--port", "8001"] diff --git a/experiment/app1.py b/experiment/app1.py new file mode 100644 index 00000000..8fe73d8d --- /dev/null +++ b/experiment/app1.py @@ -0,0 +1,15 @@ +from fastapi import FastAPI + +app = FastAPI() + +@app.get("/api1/endpoint1") +def read_root(): + return {"message": "Hello from API 1, Endpoint 1"} + +@app.get("/api1/endpoint2") +def read_item(): + return {"message": "Hello from API 1, Endpoint 2"} + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/experiment/app2.py b/experiment/app2.py new file mode 100644 index 00000000..aa497d6a --- /dev/null +++ b/experiment/app2.py @@ -0,0 +1,15 @@ +from fastapi import FastAPI + +app = FastAPI() + +@app.get("/api2/endpoint1") +def read_root(): + return {"message": "Hello from API 2, Endpoint 1"} + +@app.get("/api2/endpoint2") +def read_item(): + return {"message": "Hello from API 2, Endpoint 2"} + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8001) diff --git a/experiment/docker-compose.yml b/experiment/docker-compose.yml new file mode 100644 index 00000000..22f3cff8 --- /dev/null +++ b/experiment/docker-compose.yml @@ -0,0 +1,16 @@ +version: '3.8' + +services: + api1: + build: + context: . + dockerfile: Dockerfile.app1 + ports: + - "8000:8000" + + api2: + build: + context: . + dockerfile: Dockerfile.app2 + ports: + - "8001:8001" diff --git a/experiment/requirements.txt b/experiment/requirements.txt new file mode 100644 index 00000000..97dc7cd8 --- /dev/null +++ b/experiment/requirements.txt @@ -0,0 +1,2 @@ +fastapi +uvicorn From ed353535bd67ff65a8822969099f3d917a84e27e Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 10 Jul 2024 12:47:26 +0530 Subject: [PATCH 106/582] remove experiment --- experiment/Dockerfile.app1 | 17 ----------------- experiment/Dockerfile.app2 | 17 ----------------- experiment/app1.py | 15 --------------- experiment/app2.py | 15 --------------- experiment/docker-compose.yml | 16 ---------------- experiment/requirements.txt | 2 -- 6 files changed, 82 deletions(-) delete mode 100644 experiment/Dockerfile.app1 delete mode 100644 experiment/Dockerfile.app2 delete mode 100644 experiment/app1.py delete mode 100644 experiment/app2.py delete mode 100644 experiment/docker-compose.yml delete mode 100644 experiment/requirements.txt diff --git a/experiment/Dockerfile.app1 b/experiment/Dockerfile.app1 deleted file mode 100644 index 0d873fc8..00000000 --- a/experiment/Dockerfile.app1 +++ /dev/null @@ -1,17 +0,0 @@ -# Use the official Python image from the Docker Hub -FROM python:3.9 - -# Set the working directory -WORKDIR /app - -# Copy the requirements file into the container -COPY requirements.txt . - -# Install the dependencies -RUN pip install --no-cache-dir -r requirements.txt - -# Copy the rest of the application code into the container -COPY app1.py . - -# Command to run the application -CMD ["uvicorn", "app1:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/experiment/Dockerfile.app2 b/experiment/Dockerfile.app2 deleted file mode 100644 index 072112ff..00000000 --- a/experiment/Dockerfile.app2 +++ /dev/null @@ -1,17 +0,0 @@ -# Use the official Python image from the Docker Hub -FROM python:3.9 - -# Set the working directory -WORKDIR /app - -# Copy the requirements file into the container -COPY requirements.txt . - -# Install the dependencies -RUN pip install --no-cache-dir -r requirements.txt - -# Copy the rest of the application code into the container -COPY app2.py . - -# Command to run the application -CMD ["uvicorn", "app2:app", "--host", "0.0.0.0", "--port", "8001"] diff --git a/experiment/app1.py b/experiment/app1.py deleted file mode 100644 index 8fe73d8d..00000000 --- a/experiment/app1.py +++ /dev/null @@ -1,15 +0,0 @@ -from fastapi import FastAPI - -app = FastAPI() - -@app.get("/api1/endpoint1") -def read_root(): - return {"message": "Hello from API 1, Endpoint 1"} - -@app.get("/api1/endpoint2") -def read_item(): - return {"message": "Hello from API 1, Endpoint 2"} - -if __name__ == "__main__": - import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/experiment/app2.py b/experiment/app2.py deleted file mode 100644 index aa497d6a..00000000 --- a/experiment/app2.py +++ /dev/null @@ -1,15 +0,0 @@ -from fastapi import FastAPI - -app = FastAPI() - -@app.get("/api2/endpoint1") -def read_root(): - return {"message": "Hello from API 2, Endpoint 1"} - -@app.get("/api2/endpoint2") -def read_item(): - return {"message": "Hello from API 2, Endpoint 2"} - -if __name__ == "__main__": - import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8001) diff --git a/experiment/docker-compose.yml b/experiment/docker-compose.yml deleted file mode 100644 index 22f3cff8..00000000 --- a/experiment/docker-compose.yml +++ /dev/null @@ -1,16 +0,0 @@ -version: '3.8' - -services: - api1: - build: - context: . - dockerfile: Dockerfile.app1 - ports: - - "8000:8000" - - api2: - build: - context: . - dockerfile: Dockerfile.app2 - ports: - - "8001:8001" diff --git a/experiment/requirements.txt b/experiment/requirements.txt deleted file mode 100644 index 97dc7cd8..00000000 --- a/experiment/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -fastapi -uvicorn From 11b1d77502c633484d53cbadde8d2fe513378935 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 10 Jul 2024 12:47:45 +0530 Subject: [PATCH 107/582] nginx config for the frontend --- rtc_nginx/esclassifier-dev.rootcode.software | 43 ++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 rtc_nginx/esclassifier-dev.rootcode.software diff --git a/rtc_nginx/esclassifier-dev.rootcode.software b/rtc_nginx/esclassifier-dev.rootcode.software new file mode 100644 index 00000000..1d23468a --- /dev/null +++ b/rtc_nginx/esclassifier-dev.rootcode.software @@ -0,0 +1,43 @@ +server { + listen [::]:443 ssl ipv6only=on; # managed by Certbot + listen 443 ssl; # managed by Certbot + + server_name esclassifier-dev.rootcode.software; + + root /var/www/esclassifier-dev.rootcode.software/html; + index index.html index.htm index.nginx-debian.html; + + location / { + proxy_pass http://localhost:3001; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /login/ { + proxy_pass http://localhost:3004/et/dev-auth; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + ssl_certificate /etc/letsencrypt/live/esclassifier-dev.rootcode.software/fullchain.pem; # managed by Certbot + ssl_certificate_key /etc/letsencrypt/live/esclassifier-dev.rootcode.software/privkey.pem; # managed by Certbot + include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot + +} + +server { + if ($host = esclassifier-dev.rootcode.software) { + return 301 https://$host$request_uri; + } # managed by Certbot + + listen 80; + listen [::]:80; + + server_name esclassifier-dev.rootcode.software; + return 404; # managed by Certbot +} \ No newline at end of file From d6ef077cc8dba06d92e80539ef38bbddd4851623 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 10 Jul 2024 13:01:59 +0530 Subject: [PATCH 108/582] nginx test on the interface --- rtc_nginx/esclassifier-dev.rootcode.software | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rtc_nginx/esclassifier-dev.rootcode.software b/rtc_nginx/esclassifier-dev.rootcode.software index 1d23468a..8be13549 100644 --- a/rtc_nginx/esclassifier-dev.rootcode.software +++ b/rtc_nginx/esclassifier-dev.rootcode.software @@ -8,7 +8,7 @@ server { index index.html index.htm index.nginx-debian.html; location / { - proxy_pass http://localhost:3001; + proxy_pass http://localhost:3004; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; From 3abab499572099e67e7c3e814b37bbb3d3c1693a Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 10 Jul 2024 13:07:49 +0530 Subject: [PATCH 109/582] nginx change --- rtc_nginx/esclassifier-dev.rootcode.software | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rtc_nginx/esclassifier-dev.rootcode.software b/rtc_nginx/esclassifier-dev.rootcode.software index 8be13549..1d23468a 100644 --- a/rtc_nginx/esclassifier-dev.rootcode.software +++ b/rtc_nginx/esclassifier-dev.rootcode.software @@ -8,7 +8,7 @@ server { index index.html index.htm index.nginx-debian.html; location / { - proxy_pass http://localhost:3004; + proxy_pass http://localhost:3001; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; From 18793d1d1aab1baac821714fa4406cad94e8efa8 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 10 Jul 2024 13:13:33 +0530 Subject: [PATCH 110/582] nginx update --- rtc_nginx/esclassifier-dev.rootcode.software | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rtc_nginx/esclassifier-dev.rootcode.software b/rtc_nginx/esclassifier-dev.rootcode.software index 1d23468a..cfc4f7f0 100644 --- a/rtc_nginx/esclassifier-dev.rootcode.software +++ b/rtc_nginx/esclassifier-dev.rootcode.software @@ -8,7 +8,7 @@ server { index index.html index.htm index.nginx-debian.html; location / { - proxy_pass http://localhost:3001; + proxy_pass http://localhost:3001/classifier; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; From c89ecc40cebd7ff017c3b2b2d0891a661dc70e1f Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 10 Jul 2024 13:14:39 +0530 Subject: [PATCH 111/582] nginx update --- rtc_nginx/esclassifier-dev.rootcode.software | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rtc_nginx/esclassifier-dev.rootcode.software b/rtc_nginx/esclassifier-dev.rootcode.software index cfc4f7f0..1d23468a 100644 --- a/rtc_nginx/esclassifier-dev.rootcode.software +++ b/rtc_nginx/esclassifier-dev.rootcode.software @@ -8,7 +8,7 @@ server { index index.html index.htm index.nginx-debian.html; location / { - proxy_pass http://localhost:3001/classifier; + proxy_pass http://localhost:3001; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; From f074b1ae74b229b754fba5f8d8ddb51f1483d7ad Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 10 Jul 2024 13:22:21 +0530 Subject: [PATCH 112/582] ip config change --- rtc_nginx/esclassifier-dev.rootcode.software | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rtc_nginx/esclassifier-dev.rootcode.software b/rtc_nginx/esclassifier-dev.rootcode.software index 1d23468a..377923cf 100644 --- a/rtc_nginx/esclassifier-dev.rootcode.software +++ b/rtc_nginx/esclassifier-dev.rootcode.software @@ -8,21 +8,21 @@ server { index index.html index.htm index.nginx-debian.html; location / { - proxy_pass http://localhost:3001; + proxy_pass http://127.0.0.1:3001; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } - location /login/ { - proxy_pass http://localhost:3004/et/dev-auth; + location /login { + proxy_pass http://127.0.0.1:3004/et/dev-auth; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } - + ssl_certificate /etc/letsencrypt/live/esclassifier-dev.rootcode.software/fullchain.pem; # managed by Certbot ssl_certificate_key /etc/letsencrypt/live/esclassifier-dev.rootcode.software/privkey.pem; # managed by Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot From 82ce18337e1a5a95b4e28f682dc5b0604de84119 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 10 Jul 2024 13:28:21 +0530 Subject: [PATCH 113/582] nginx config --- rtc_nginx/esclassifier-dev.rootcode.software | 24 ++++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/rtc_nginx/esclassifier-dev.rootcode.software b/rtc_nginx/esclassifier-dev.rootcode.software index 377923cf..258e0a9a 100644 --- a/rtc_nginx/esclassifier-dev.rootcode.software +++ b/rtc_nginx/esclassifier-dev.rootcode.software @@ -7,22 +7,22 @@ server { root /var/www/esclassifier-dev.rootcode.software/html; index index.html index.htm index.nginx-debian.html; - location / { - proxy_pass http://127.0.0.1:3001; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; + location /login { + proxy_pass http://esclassifier-dev.rootcode.software:3001; + proxy_redirect http://esclassifier-dev.rootcode.software:3001 /; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /login { - proxy_pass http://127.0.0.1:3004/et/dev-auth; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; + proxy_pass http://esclassifier-dev.rootcode.software:3004/et/dev-auth; + proxy_redirect http://esclassifier-dev.rootcode.software:3004/et/dev-auth /; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } - + ssl_certificate /etc/letsencrypt/live/esclassifier-dev.rootcode.software/fullchain.pem; # managed by Certbot ssl_certificate_key /etc/letsencrypt/live/esclassifier-dev.rootcode.software/privkey.pem; # managed by Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot From 63de3952a1951333f683a83922067b8aff2b0863 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 10 Jul 2024 13:29:06 +0530 Subject: [PATCH 114/582] nginx bug fix --- rtc_nginx/esclassifier-dev.rootcode.software | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rtc_nginx/esclassifier-dev.rootcode.software b/rtc_nginx/esclassifier-dev.rootcode.software index 258e0a9a..7b0154a1 100644 --- a/rtc_nginx/esclassifier-dev.rootcode.software +++ b/rtc_nginx/esclassifier-dev.rootcode.software @@ -7,7 +7,7 @@ server { root /var/www/esclassifier-dev.rootcode.software/html; index index.html index.htm index.nginx-debian.html; - location /login { + location / { proxy_pass http://esclassifier-dev.rootcode.software:3001; proxy_redirect http://esclassifier-dev.rootcode.software:3001 /; proxy_set_header Host $host; From 3833a3b1a1753086f4373a45f6c073ef102db77f Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 10 Jul 2024 13:32:16 +0530 Subject: [PATCH 115/582] test nginx --- rtc_nginx/esclassifier-dev.rootcode.software | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rtc_nginx/esclassifier-dev.rootcode.software b/rtc_nginx/esclassifier-dev.rootcode.software index 7b0154a1..6f4124b6 100644 --- a/rtc_nginx/esclassifier-dev.rootcode.software +++ b/rtc_nginx/esclassifier-dev.rootcode.software @@ -16,8 +16,8 @@ server { } location /login { - proxy_pass http://esclassifier-dev.rootcode.software:3004/et/dev-auth; - proxy_redirect http://esclassifier-dev.rootcode.software:3004/et/dev-auth /; + proxy_pass http://esclassifier-dev.rootcode.software:3004; + proxy_redirect http://esclassifier-dev.rootcode.software:3004 /; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; From 7ab09c9cff861dd92ea693a1e322d1bbe167785b Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 10 Jul 2024 13:40:03 +0530 Subject: [PATCH 116/582] block change --- rtc_nginx/esclassifier-dev.rootcode.software | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/rtc_nginx/esclassifier-dev.rootcode.software b/rtc_nginx/esclassifier-dev.rootcode.software index 6f4124b6..8a2e8f23 100644 --- a/rtc_nginx/esclassifier-dev.rootcode.software +++ b/rtc_nginx/esclassifier-dev.rootcode.software @@ -7,17 +7,17 @@ server { root /var/www/esclassifier-dev.rootcode.software/html; index index.html index.htm index.nginx-debian.html; - location / { - proxy_pass http://esclassifier-dev.rootcode.software:3001; - proxy_redirect http://esclassifier-dev.rootcode.software:3001 /; + location /login { + proxy_pass http://esclassifier-dev.rootcode.software:3004; + proxy_redirect http://esclassifier-dev.rootcode.software:3004 /; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } - location /login { - proxy_pass http://esclassifier-dev.rootcode.software:3004; - proxy_redirect http://esclassifier-dev.rootcode.software:3004 /; + location / { + proxy_pass http://esclassifier-dev.rootcode.software:3001; + proxy_redirect http://esclassifier-dev.rootcode.software:3001 /; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; From 7eac10f5dc4fd2624f46eeb446e8e848a08435ac Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 10 Jul 2024 13:42:27 +0530 Subject: [PATCH 117/582] directory bloakc --- rtc_nginx/esclassifier-dev.rootcode.software | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rtc_nginx/esclassifier-dev.rootcode.software b/rtc_nginx/esclassifier-dev.rootcode.software index 8a2e8f23..f54e0309 100644 --- a/rtc_nginx/esclassifier-dev.rootcode.software +++ b/rtc_nginx/esclassifier-dev.rootcode.software @@ -7,7 +7,7 @@ server { root /var/www/esclassifier-dev.rootcode.software/html; index index.html index.htm index.nginx-debian.html; - location /login { + location /login/ { proxy_pass http://esclassifier-dev.rootcode.software:3004; proxy_redirect http://esclassifier-dev.rootcode.software:3004 /; proxy_set_header Host $host; From 550b3262cfd0506fb4014c5c244c1fc21106e460 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 10 Jul 2024 13:46:35 +0530 Subject: [PATCH 118/582] nginx configurations --- .../esclassifier-dev-ruuter.rootcode.software | 56 +++++++++++++++++++ rtc_nginx/esclassifier-dev.rootcode.software | 4 +- 2 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 rtc_nginx/esclassifier-dev-ruuter.rootcode.software diff --git a/rtc_nginx/esclassifier-dev-ruuter.rootcode.software b/rtc_nginx/esclassifier-dev-ruuter.rootcode.software new file mode 100644 index 00000000..baad93a7 --- /dev/null +++ b/rtc_nginx/esclassifier-dev-ruuter.rootcode.software @@ -0,0 +1,56 @@ +server { + + root /var/www/esclassifier-dev-ruuter.rootcode.software/html; + index index.html index.htm index.nginx-debian.html; + + server_name esclassifier-dev-ruuter.rootcode.software; + + location / { + try_files $uri $uri/ =404; + } + + location /rutter-login { + proxy_pass http://localhost:8086/auth/login; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /rutter-jira-sub { + proxy_pass http://localhost:8086/classifier/integration/jira/cloud/subscribe; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /rutter-outlook-sub { + proxy_pass http://localhost:8086/classifier/integration/outlook/subscribe; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + listen [::]:443 ssl; # managed by Certbot + listen 443 ssl; # managed by Certbot + ssl_certificate /etc/letsencrypt/live/esclassifier-dev-ruuter.rootcode.software/fullchain.pem; # managed by Certbot + ssl_certificate_key /etc/letsencrypt/live/esclassifier-dev-ruuter.rootcode.software/privkey.pem; # managed by Certbot + include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot + +} + +server { + if ($host = esclassifier-dev-ruuter.rootcode.software) { + return 301 https://$host$request_uri; + } # managed by Certbot + + listen 80; + listen [::]:80; + + server_name esclassifier-dev-ruuter.rootcode.software; + return 404; # managed by Certbot + +} diff --git a/rtc_nginx/esclassifier-dev.rootcode.software b/rtc_nginx/esclassifier-dev.rootcode.software index f54e0309..1de7e43f 100644 --- a/rtc_nginx/esclassifier-dev.rootcode.software +++ b/rtc_nginx/esclassifier-dev.rootcode.software @@ -8,8 +8,8 @@ server { index index.html index.htm index.nginx-debian.html; location /login/ { - proxy_pass http://esclassifier-dev.rootcode.software:3004; - proxy_redirect http://esclassifier-dev.rootcode.software:3004 /; + proxy_pass http://esclassifier-dev.rootcode.software:3004/et/dev-auth; + proxy_redirect http://esclassifier-dev.rootcode.software:3004/et/dev-auth /; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; From cba038d9c2d07fa15243786e34cbadf4deb31706 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 10 Jul 2024 14:41:01 +0530 Subject: [PATCH 119/582] nginx experiment --- experiment/Dockerfile1 | 8 ++++++++ experiment/Dockerfile2 | 8 ++++++++ experiment/docker-compose.yml | 15 +++++++++++++++ experiment/index1.html | 10 ++++++++++ experiment/index2.html | 10 ++++++++++ rtc_nginx/esclassifier-dev.rootcode.software | 15 +++++++-------- 6 files changed, 58 insertions(+), 8 deletions(-) create mode 100644 experiment/Dockerfile1 create mode 100644 experiment/Dockerfile2 create mode 100644 experiment/docker-compose.yml create mode 100644 experiment/index1.html create mode 100644 experiment/index2.html diff --git a/experiment/Dockerfile1 b/experiment/Dockerfile1 new file mode 100644 index 00000000..582446af --- /dev/null +++ b/experiment/Dockerfile1 @@ -0,0 +1,8 @@ +# Use an official nginx image as a parent image +FROM nginx:latest + +# Copy the first HTML file to the default nginx directory +COPY index1.html /usr/share/nginx/html/index.html + +# Expose port 8000 +EXPOSE 8000 diff --git a/experiment/Dockerfile2 b/experiment/Dockerfile2 new file mode 100644 index 00000000..d9c228b4 --- /dev/null +++ b/experiment/Dockerfile2 @@ -0,0 +1,8 @@ +# Use an official nginx image as a parent image +FROM nginx:latest + +# Copy the second HTML file to the default nginx directory +COPY index2.html /usr/share/nginx/html/index.html + +# Expose port 8001 +EXPOSE 8001 diff --git a/experiment/docker-compose.yml b/experiment/docker-compose.yml new file mode 100644 index 00000000..932ec7c1 --- /dev/null +++ b/experiment/docker-compose.yml @@ -0,0 +1,15 @@ +version: '3' +services: + web1: + build: + context: . + dockerfile: Dockerfile1 + ports: + - "8000:8000" + + web2: + build: + context: . + dockerfile: Dockerfile2 + ports: + - "8001:8001" diff --git a/experiment/index1.html b/experiment/index1.html new file mode 100644 index 00000000..7e3ce545 --- /dev/null +++ b/experiment/index1.html @@ -0,0 +1,10 @@ + + + + Page 1 + + +

    Welcome to Page 1

    +

    This is the first interface.

    + + diff --git a/experiment/index2.html b/experiment/index2.html new file mode 100644 index 00000000..5e28574b --- /dev/null +++ b/experiment/index2.html @@ -0,0 +1,10 @@ + + + + Page 2 + + +

    Welcome to Page 2

    +

    This is the second interface.

    + + diff --git a/rtc_nginx/esclassifier-dev.rootcode.software b/rtc_nginx/esclassifier-dev.rootcode.software index 1de7e43f..2adebcfc 100644 --- a/rtc_nginx/esclassifier-dev.rootcode.software +++ b/rtc_nginx/esclassifier-dev.rootcode.software @@ -7,17 +7,17 @@ server { root /var/www/esclassifier-dev.rootcode.software/html; index index.html index.htm index.nginx-debian.html; - location /login/ { - proxy_pass http://esclassifier-dev.rootcode.software:3004/et/dev-auth; - proxy_redirect http://esclassifier-dev.rootcode.software:3004/et/dev-auth /; + location /dev/auth { + proxy_pass http://web1:8000; + proxy_redirect http://web1:8000 /dev/auth; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } - location / { - proxy_pass http://esclassifier-dev.rootcode.software:3001; - proxy_redirect http://esclassifier-dev.rootcode.software:3001 /; + location /dev/auth2 { + proxy_pass http://web2:8001; + proxy_redirect http://web2:8001 /dev/auth2; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; @@ -27,7 +27,6 @@ server { ssl_certificate_key /etc/letsencrypt/live/esclassifier-dev.rootcode.software/privkey.pem; # managed by Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot - } server { @@ -40,4 +39,4 @@ server { server_name esclassifier-dev.rootcode.software; return 404; # managed by Certbot -} \ No newline at end of file +} From 835b39b8098c404be22d402e47e60eb249442e55 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 10 Jul 2024 14:43:17 +0530 Subject: [PATCH 120/582] localhost update --- rtc_nginx/esclassifier-dev.rootcode.software | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rtc_nginx/esclassifier-dev.rootcode.software b/rtc_nginx/esclassifier-dev.rootcode.software index 2adebcfc..70df0001 100644 --- a/rtc_nginx/esclassifier-dev.rootcode.software +++ b/rtc_nginx/esclassifier-dev.rootcode.software @@ -8,16 +8,16 @@ server { index index.html index.htm index.nginx-debian.html; location /dev/auth { - proxy_pass http://web1:8000; - proxy_redirect http://web1:8000 /dev/auth; + proxy_pass http://localhost:8000; + proxy_redirect http://localhost:8000 /dev/auth; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /dev/auth2 { - proxy_pass http://web2:8001; - proxy_redirect http://web2:8001 /dev/auth2; + proxy_pass http://localhost:8001; + proxy_redirect http://localhost:8001 /dev/auth2; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; From 9421962379d3691c03a32cf62481fdb07d24c08f Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 10 Jul 2024 14:47:36 +0530 Subject: [PATCH 121/582] exp update --- experiment/docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/experiment/docker-compose.yml b/experiment/docker-compose.yml index 932ec7c1..0f42e42b 100644 --- a/experiment/docker-compose.yml +++ b/experiment/docker-compose.yml @@ -5,11 +5,11 @@ services: context: . dockerfile: Dockerfile1 ports: - - "8000:8000" + - "8000:80" web2: build: context: . dockerfile: Dockerfile2 ports: - - "8001:8001" + - "8001:80" From fa819bccca29218726c375b342587b0c4491b0f1 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 10 Jul 2024 14:56:03 +0530 Subject: [PATCH 122/582] nginx update --- rtc_nginx/esclassifier-dev.rootcode.software | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/rtc_nginx/esclassifier-dev.rootcode.software b/rtc_nginx/esclassifier-dev.rootcode.software index 70df0001..5e8e5637 100644 --- a/rtc_nginx/esclassifier-dev.rootcode.software +++ b/rtc_nginx/esclassifier-dev.rootcode.software @@ -7,17 +7,17 @@ server { root /var/www/esclassifier-dev.rootcode.software/html; index index.html index.htm index.nginx-debian.html; - location /dev/auth { - proxy_pass http://localhost:8000; - proxy_redirect http://localhost:8000 /dev/auth; + location /login/ { + proxy_pass http://esclassifier-dev.rootcode.software:3004/et/dev-auth; + proxy_redirect http://esclassifier-dev.rootcode.software:3004/ /et/dev-auth; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } - location /dev/auth2 { - proxy_pass http://localhost:8001; - proxy_redirect http://localhost:8001 /dev/auth2; + location / { + proxy_pass http://esclassifier-dev.rootcode.software:3001; + proxy_redirect http://esclassifier-dev.rootcode.software:3001 /; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; @@ -27,6 +27,7 @@ server { ssl_certificate_key /etc/letsencrypt/live/esclassifier-dev.rootcode.software/privkey.pem; # managed by Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot + } server { @@ -39,4 +40,4 @@ server { server_name esclassifier-dev.rootcode.software; return 404; # managed by Certbot -} +} \ No newline at end of file From a66863366c2507fcbe5dcbcdf1abb339eb70b043 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 10 Jul 2024 15:00:00 +0530 Subject: [PATCH 123/582] nginx update --- rtc_nginx/esclassifier-dev.rootcode.software | 8 -------- 1 file changed, 8 deletions(-) diff --git a/rtc_nginx/esclassifier-dev.rootcode.software b/rtc_nginx/esclassifier-dev.rootcode.software index 5e8e5637..b6ffd1c5 100644 --- a/rtc_nginx/esclassifier-dev.rootcode.software +++ b/rtc_nginx/esclassifier-dev.rootcode.software @@ -15,14 +15,6 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } - location / { - proxy_pass http://esclassifier-dev.rootcode.software:3001; - proxy_redirect http://esclassifier-dev.rootcode.software:3001 /; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } - ssl_certificate /etc/letsencrypt/live/esclassifier-dev.rootcode.software/fullchain.pem; # managed by Certbot ssl_certificate_key /etc/letsencrypt/live/esclassifier-dev.rootcode.software/privkey.pem; # managed by Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot From 321638548524fc4b9a181d7b737cd9cd3525ca02 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 10 Jul 2024 15:03:25 +0530 Subject: [PATCH 124/582] removed location sub --- rtc_nginx/esclassifier-dev.rootcode.software | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/rtc_nginx/esclassifier-dev.rootcode.software b/rtc_nginx/esclassifier-dev.rootcode.software index b6ffd1c5..7bd0c43f 100644 --- a/rtc_nginx/esclassifier-dev.rootcode.software +++ b/rtc_nginx/esclassifier-dev.rootcode.software @@ -8,8 +8,16 @@ server { index index.html index.htm index.nginx-debian.html; location /login/ { - proxy_pass http://esclassifier-dev.rootcode.software:3004/et/dev-auth; - proxy_redirect http://esclassifier-dev.rootcode.software:3004/ /et/dev-auth; + proxy_pass http://esclassifier-dev.rootcode.software:3004/; + proxy_redirect http://esclassifier-dev.rootcode.software:3004/ /; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + location / { + proxy_pass http://esclassifier-dev.rootcode.software:3001; + proxy_redirect http://esclassifier-dev.rootcode.software:3001 /; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; From e547bfe00d22b76bb9ed99e2a51289c72b7750bd Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 10 Jul 2024 15:11:18 +0530 Subject: [PATCH 125/582] adding sub --- rtc_nginx/esclassifier-dev.rootcode.software | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rtc_nginx/esclassifier-dev.rootcode.software b/rtc_nginx/esclassifier-dev.rootcode.software index 7bd0c43f..1de7e43f 100644 --- a/rtc_nginx/esclassifier-dev.rootcode.software +++ b/rtc_nginx/esclassifier-dev.rootcode.software @@ -8,8 +8,8 @@ server { index index.html index.htm index.nginx-debian.html; location /login/ { - proxy_pass http://esclassifier-dev.rootcode.software:3004/; - proxy_redirect http://esclassifier-dev.rootcode.software:3004/ /; + proxy_pass http://esclassifier-dev.rootcode.software:3004/et/dev-auth; + proxy_redirect http://esclassifier-dev.rootcode.software:3004/et/dev-auth /; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; From 41150e96f2f506c4be8b9c3d4aed6cd44a4079aa Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 10 Jul 2024 15:28:51 +0530 Subject: [PATCH 126/582] update the dev frontend nginx --- rtc_nginx/esclassifier-dev.rootcode.software | 8 -------- 1 file changed, 8 deletions(-) diff --git a/rtc_nginx/esclassifier-dev.rootcode.software b/rtc_nginx/esclassifier-dev.rootcode.software index 1de7e43f..c7a49535 100644 --- a/rtc_nginx/esclassifier-dev.rootcode.software +++ b/rtc_nginx/esclassifier-dev.rootcode.software @@ -7,14 +7,6 @@ server { root /var/www/esclassifier-dev.rootcode.software/html; index index.html index.htm index.nginx-debian.html; - location /login/ { - proxy_pass http://esclassifier-dev.rootcode.software:3004/et/dev-auth; - proxy_redirect http://esclassifier-dev.rootcode.software:3004/et/dev-auth /; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } - location / { proxy_pass http://esclassifier-dev.rootcode.software:3001; proxy_redirect http://esclassifier-dev.rootcode.software:3001 /; From c297248e8d164a31bd9ee466e595651b3a304dfa Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 10 Jul 2024 15:33:32 +0530 Subject: [PATCH 127/582] remove experiments --- experiment/Dockerfile1 | 8 -------- experiment/Dockerfile2 | 8 -------- experiment/docker-compose.yml | 15 --------------- experiment/index1.html | 10 ---------- experiment/index2.html | 10 ---------- 5 files changed, 51 deletions(-) delete mode 100644 experiment/Dockerfile1 delete mode 100644 experiment/Dockerfile2 delete mode 100644 experiment/docker-compose.yml delete mode 100644 experiment/index1.html delete mode 100644 experiment/index2.html diff --git a/experiment/Dockerfile1 b/experiment/Dockerfile1 deleted file mode 100644 index 582446af..00000000 --- a/experiment/Dockerfile1 +++ /dev/null @@ -1,8 +0,0 @@ -# Use an official nginx image as a parent image -FROM nginx:latest - -# Copy the first HTML file to the default nginx directory -COPY index1.html /usr/share/nginx/html/index.html - -# Expose port 8000 -EXPOSE 8000 diff --git a/experiment/Dockerfile2 b/experiment/Dockerfile2 deleted file mode 100644 index d9c228b4..00000000 --- a/experiment/Dockerfile2 +++ /dev/null @@ -1,8 +0,0 @@ -# Use an official nginx image as a parent image -FROM nginx:latest - -# Copy the second HTML file to the default nginx directory -COPY index2.html /usr/share/nginx/html/index.html - -# Expose port 8001 -EXPOSE 8001 diff --git a/experiment/docker-compose.yml b/experiment/docker-compose.yml deleted file mode 100644 index 0f42e42b..00000000 --- a/experiment/docker-compose.yml +++ /dev/null @@ -1,15 +0,0 @@ -version: '3' -services: - web1: - build: - context: . - dockerfile: Dockerfile1 - ports: - - "8000:80" - - web2: - build: - context: . - dockerfile: Dockerfile2 - ports: - - "8001:80" diff --git a/experiment/index1.html b/experiment/index1.html deleted file mode 100644 index 7e3ce545..00000000 --- a/experiment/index1.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - Page 1 - - -

    Welcome to Page 1

    -

    This is the first interface.

    - - diff --git a/experiment/index2.html b/experiment/index2.html deleted file mode 100644 index 5e28574b..00000000 --- a/experiment/index2.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - Page 2 - - -

    Welcome to Page 2

    -

    This is the second interface.

    - - From a2ee361e28b3914871fc568bf24887a147dd78c7 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 10 Jul 2024 15:40:07 +0530 Subject: [PATCH 128/582] Nginx block for login.esclassifier-dev.rootcode.software --- .../login.esclassifier-dev.rootcode.software | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 rtc_nginx/login.esclassifier-dev.rootcode.software diff --git a/rtc_nginx/login.esclassifier-dev.rootcode.software b/rtc_nginx/login.esclassifier-dev.rootcode.software new file mode 100644 index 00000000..63e61793 --- /dev/null +++ b/rtc_nginx/login.esclassifier-dev.rootcode.software @@ -0,0 +1,35 @@ +server { + listen [::]:443 ssl ipv6only=on; # managed by Certbot + listen 443 ssl; # managed by Certbot + + server_name esclassifier-dev.rootcode.software; + + root /var/www/esclassifier-dev.rootcode.software/html; + index index.html index.htm index.nginx-debian.html; + + location / { + proxy_pass http://esclassifier-dev.rootcode.software:3004; + proxy_redirect http://esclassifier-dev.rootcode.software:3004 /; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + ssl_certificate /etc/letsencrypt/live/esclassifier-dev.rootcode.software/fullchain.pem; # managed by Certbot + ssl_certificate_key /etc/letsencrypt/live/esclassifier-dev.rootcode.software/privkey.pem; # managed by Certbot + include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot + +} + +server { + if ($host = esclassifier-dev.rootcode.software) { + return 301 https://$host$request_uri; + } # managed by Certbot + + listen 80; + listen [::]:80; + + server_name esclassifier-dev.rootcode.software; + return 404; # managed by Certbot +} \ No newline at end of file From 03b6d4262bf447dac8b492fccc63d8a529f87fdb Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 10 Jul 2024 15:42:25 +0530 Subject: [PATCH 129/582] nginx bug fix --- .../login.esclassifier-dev.rootcode.software | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/rtc_nginx/login.esclassifier-dev.rootcode.software b/rtc_nginx/login.esclassifier-dev.rootcode.software index 63e61793..77da3706 100644 --- a/rtc_nginx/login.esclassifier-dev.rootcode.software +++ b/rtc_nginx/login.esclassifier-dev.rootcode.software @@ -2,34 +2,34 @@ server { listen [::]:443 ssl ipv6only=on; # managed by Certbot listen 443 ssl; # managed by Certbot - server_name esclassifier-dev.rootcode.software; + server_name login.esclassifier-dev.rootcode.software; - root /var/www/esclassifier-dev.rootcode.software/html; + root /var/www/login.esclassifier-dev.rootcode.software/html; index index.html index.htm index.nginx-debian.html; location / { - proxy_pass http://esclassifier-dev.rootcode.software:3004; - proxy_redirect http://esclassifier-dev.rootcode.software:3004 /; + proxy_pass http://login.esclassifier-dev.rootcode.software:3004; + proxy_redirect http://login.esclassifier-dev.rootcode.software:3004 /; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } - ssl_certificate /etc/letsencrypt/live/esclassifier-dev.rootcode.software/fullchain.pem; # managed by Certbot - ssl_certificate_key /etc/letsencrypt/live/esclassifier-dev.rootcode.software/privkey.pem; # managed by Certbot + ssl_certificate /etc/letsencrypt/live/login.esclassifier-dev.rootcode.software/fullchain.pem; # managed by Certbot + ssl_certificate_key /etc/letsencrypt/live/login.esclassifier-dev.rootcode.software/privkey.pem; # managed by Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot } server { - if ($host = esclassifier-dev.rootcode.software) { + if ($host = login.esclassifier-dev.rootcode.software) { return 301 https://$host$request_uri; } # managed by Certbot listen 80; listen [::]:80; - server_name esclassifier-dev.rootcode.software; + server_name login.esclassifier-dev.rootcode.software; return 404; # managed by Certbot } \ No newline at end of file From b405fe288bc975f6f1447e598fe53a6e5c5b6214 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 10 Jul 2024 15:46:07 +0530 Subject: [PATCH 130/582] removing duplicate 443 listen --- rtc_nginx/esclassifier-dev.rootcode.software | 12 +++++------- rtc_nginx/login.esclassifier-dev.rootcode.software | 12 +++++------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/rtc_nginx/esclassifier-dev.rootcode.software b/rtc_nginx/esclassifier-dev.rootcode.software index c7a49535..f8973d31 100644 --- a/rtc_nginx/esclassifier-dev.rootcode.software +++ b/rtc_nginx/esclassifier-dev.rootcode.software @@ -1,5 +1,4 @@ server { - listen [::]:443 ssl ipv6only=on; # managed by Certbot listen 443 ssl; # managed by Certbot server_name esclassifier-dev.rootcode.software; @@ -9,17 +8,16 @@ server { location / { proxy_pass http://esclassifier-dev.rootcode.software:3001; - proxy_redirect http://esclassifier-dev.rootcode.software:3001 /; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_redirect http://esclassifier-dev.rootcode.software:3001 /; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } ssl_certificate /etc/letsencrypt/live/esclassifier-dev.rootcode.software/fullchain.pem; # managed by Certbot ssl_certificate_key /etc/letsencrypt/live/esclassifier-dev.rootcode.software/privkey.pem; # managed by Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot - } server { @@ -32,4 +30,4 @@ server { server_name esclassifier-dev.rootcode.software; return 404; # managed by Certbot -} \ No newline at end of file +} diff --git a/rtc_nginx/login.esclassifier-dev.rootcode.software b/rtc_nginx/login.esclassifier-dev.rootcode.software index 77da3706..003d0cac 100644 --- a/rtc_nginx/login.esclassifier-dev.rootcode.software +++ b/rtc_nginx/login.esclassifier-dev.rootcode.software @@ -1,5 +1,4 @@ server { - listen [::]:443 ssl ipv6only=on; # managed by Certbot listen 443 ssl; # managed by Certbot server_name login.esclassifier-dev.rootcode.software; @@ -9,17 +8,16 @@ server { location / { proxy_pass http://login.esclassifier-dev.rootcode.software:3004; - proxy_redirect http://login.esclassifier-dev.rootcode.software:3004 /; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_redirect http://login.esclassifier-dev.rootcode.software:3004 /; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } ssl_certificate /etc/letsencrypt/live/login.esclassifier-dev.rootcode.software/fullchain.pem; # managed by Certbot ssl_certificate_key /etc/letsencrypt/live/login.esclassifier-dev.rootcode.software/privkey.pem; # managed by Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot - } server { @@ -32,4 +30,4 @@ server { server_name login.esclassifier-dev.rootcode.software; return 404; # managed by Certbot -} \ No newline at end of file +} From f539fc8c243ac26c97f03638430929c5b7fa6b40 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 10 Jul 2024 15:48:24 +0530 Subject: [PATCH 131/582] Ruuter public and private combined nginx --- rtc_nginx/esclassifier-dev-ruuter.rootcode.software | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/rtc_nginx/esclassifier-dev-ruuter.rootcode.software b/rtc_nginx/esclassifier-dev-ruuter.rootcode.software index baad93a7..3a61821b 100644 --- a/rtc_nginx/esclassifier-dev-ruuter.rootcode.software +++ b/rtc_nginx/esclassifier-dev-ruuter.rootcode.software @@ -33,6 +33,14 @@ server { proxy_set_header X-Forwarded-Proto $scheme; } + location / { + proxy_pass http://localhost:8088; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + listen [::]:443 ssl; # managed by Certbot listen 443 ssl; # managed by Certbot ssl_certificate /etc/letsencrypt/live/esclassifier-dev-ruuter.rootcode.software/fullchain.pem; # managed by Certbot From c1dd96b5a6d030c6e3a4fd8eb88996128448deef Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 10 Jul 2024 15:49:21 +0530 Subject: [PATCH 132/582] 404 block removal --- rtc_nginx/esclassifier-dev-ruuter.rootcode.software | 4 ---- 1 file changed, 4 deletions(-) diff --git a/rtc_nginx/esclassifier-dev-ruuter.rootcode.software b/rtc_nginx/esclassifier-dev-ruuter.rootcode.software index 3a61821b..26b0b29b 100644 --- a/rtc_nginx/esclassifier-dev-ruuter.rootcode.software +++ b/rtc_nginx/esclassifier-dev-ruuter.rootcode.software @@ -5,10 +5,6 @@ server { server_name esclassifier-dev-ruuter.rootcode.software; - location / { - try_files $uri $uri/ =404; - } - location /rutter-login { proxy_pass http://localhost:8086/auth/login; proxy_set_header Host $host; From 3fb0fea97b7af04cda886274f8a87867a8592c0d Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 10 Jul 2024 16:01:58 +0530 Subject: [PATCH 133/582] docker compose url update --- docker-compose.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 4cc01967..c0a40488 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ services: container_name: ruuter-public image: ruuter environment: - - application.cors.allowedOrigins=http://ruuter-public:8086,http://gui:3001,http://authentication-layer:3004,http://gui:8080 + - application.cors.allowedOrigins=https://esclassifier-dev.rootcode.software/,https://esclassifier-dev-ruuter.rootcode.software,https://login.esclassifier-dev.rootcode.software - application.httpCodesAllowList=200,201,202,400,401,403,500 - application.internalRequests.allowedIPs=127.0.0.1 - application.logging.displayRequestContent=true @@ -25,7 +25,7 @@ services: container_name: ruuter-private image: ruuter environment: - - application.cors.allowedOrigins=http://gui:3001,http://ruuter-private:8088,http://authentication-layer:3004 + - application.cors.allowedOrigins=https://esclassifier-dev.rootcode.software/,https://esclassifier-dev-ruuter.rootcode.software,https://login.esclassifier-dev.rootcode.software - application.httpCodesAllowList=200,201,202,204,400,401,403,404,500 - application.internalRequests.allowedIPs=127.0.0.1 - application.logging.displayRequestContent=true @@ -94,12 +94,12 @@ services: container_name: gui environment: - NODE_ENV=development - - BASE_URL=http://gui:8080 - - REACT_APP_RUUTER_API_URL=http://ruuter-public:8086 - - REACT_APP_RUUTER_PRIVATE_API_URL=http://ruuter-private:8088 - - REACT_APP_CUSTOMER_SERVICE_LOGIN=http://authentication-layer:3004/et/dev-auth + - BASE_URL=https://esclassifier-dev.rootcode.software + - REACT_APP_RUUTER_API_URL=https://esclassifier-dev-ruuter.rootcode.software + - REACT_APP_RUUTER_PRIVATE_API_URL=https://esclassifier-dev-ruuter.rootcode.software + - REACT_APP_CUSTOMER_SERVICE_LOGIN=https://login.esclassifier-dev.rootcode.software # - REACT_APP_NOTIFICATION_NODE_URL=http://gui:4040 - - REACT_APP_CSP=upgrade-insecure-requests; default-src 'self'; font-src 'self' data:; img-src 'self' data:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self' http://ruuter-public:8086 http://ruuter-private:8088; + - REACT_APP_CSP=upgrade-insecure-requests; default-src 'self'; font-src 'self' data:; img-src 'self' data:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self' https://esclassifier-dev-ruuter.rootcode.software https://esclassifier-dev-ruuter.rootcode.software; - DEBUG_ENABLED=true - CHOKIDAR_USEPOLLING=true - PORT=3001 From 9afca166431a6357eb49a4d050b219cfa46541b2 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 10 Jul 2024 16:18:59 +0530 Subject: [PATCH 134/582] nginx configs for new ruuter public and private --- ...ifier-dev-ruuter-private.rootcode.software | 35 +++++++++++++++++++ .../esclassifier-dev-ruuter.rootcode.software | 26 +------------- 2 files changed, 36 insertions(+), 25 deletions(-) create mode 100644 rtc_nginx/esclassifier-dev-ruuter-private.rootcode.software diff --git a/rtc_nginx/esclassifier-dev-ruuter-private.rootcode.software b/rtc_nginx/esclassifier-dev-ruuter-private.rootcode.software new file mode 100644 index 00000000..09f7dac6 --- /dev/null +++ b/rtc_nginx/esclassifier-dev-ruuter-private.rootcode.software @@ -0,0 +1,35 @@ +server { + + root /var/www/esclassifier-dev-ruuter-private.rootcode.software/html; + index index.html index.htm index.nginx-debian.html; + + server_name esclassifier-dev-ruuter-private.rootcode.software; + + location / { + proxy_pass http://localhost:8088; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + listen [::]:443 ssl ipv6only=on; # managed by Certbot + listen 443 ssl; # managed by Certbot + ssl_certificate /etc/letsencrypt/live/esclassifier-dev-ruuter-private.rootcode.software/fullchain.pem; # managed by> ssl_certificate_key /etc/letsencrypt/live/esclassifier-dev-ruuter-private.rootcode.software/privkey.pem; # managed > include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot + +} +server { + if ($host = esclassifier-dev-ruuter-private.rootcode.software) { + return 301 https://$host$request_uri; + } # managed by Certbot + + + listen 80; + listen [::]:80; + + server_name esclassifier-dev-ruuter-private.rootcode.software; + return 404; # managed by Certbot + + +} \ No newline at end of file diff --git a/rtc_nginx/esclassifier-dev-ruuter.rootcode.software b/rtc_nginx/esclassifier-dev-ruuter.rootcode.software index 26b0b29b..79da1f5f 100644 --- a/rtc_nginx/esclassifier-dev-ruuter.rootcode.software +++ b/rtc_nginx/esclassifier-dev-ruuter.rootcode.software @@ -5,32 +5,8 @@ server { server_name esclassifier-dev-ruuter.rootcode.software; - location /rutter-login { - proxy_pass http://localhost:8086/auth/login; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - - location /rutter-jira-sub { - proxy_pass http://localhost:8086/classifier/integration/jira/cloud/subscribe; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - - location /rutter-outlook-sub { - proxy_pass http://localhost:8086/classifier/integration/outlook/subscribe; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - location / { - proxy_pass http://localhost:8088; + proxy_pass http://localhost:8086; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; From e86ae89ddc102b336894835a53e37b35e050cfbd Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 10 Jul 2024 16:24:08 +0530 Subject: [PATCH 135/582] nginx bug fix --- ...ssifier-dev-ruuter-private.rootcode.software | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/rtc_nginx/esclassifier-dev-ruuter-private.rootcode.software b/rtc_nginx/esclassifier-dev-ruuter-private.rootcode.software index 09f7dac6..03e89362 100644 --- a/rtc_nginx/esclassifier-dev-ruuter-private.rootcode.software +++ b/rtc_nginx/esclassifier-dev-ruuter-private.rootcode.software @@ -13,23 +13,24 @@ server { proxy_set_header X-Forwarded-Proto $scheme; } - listen [::]:443 ssl ipv6only=on; # managed by Certbot + listen [::]:443 ssl; # managed by Certbot listen 443 ssl; # managed by Certbot - ssl_certificate /etc/letsencrypt/live/esclassifier-dev-ruuter-private.rootcode.software/fullchain.pem; # managed by> ssl_certificate_key /etc/letsencrypt/live/esclassifier-dev-ruuter-private.rootcode.software/privkey.pem; # managed > include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot + ssl_certificate /etc/letsencrypt/live/esclassifier-dev-ruuter-private.rootcode.software/fullchain.pem; # managed by Certbot + ssl_certificate_key /etc/letsencrypt/live/esclassifier-dev-ruuter-private.rootcode.software/privkey.pem; # managed by Certbot + include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot } + server { if ($host = esclassifier-dev-ruuter-private.rootcode.software) { return 301 https://$host$request_uri; } # managed by Certbot + listen 80; + listen [::]:80; - listen 80; - listen [::]:80; - - server_name esclassifier-dev-ruuter-private.rootcode.software; + server_name esclassifier-dev-ruuter-private.rootcode.software; return 404; # managed by Certbot - -} \ No newline at end of file +} From 290667c6a1b6ad7b9d68f9ee11a6b659cfb938f5 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 10 Jul 2024 16:26:32 +0530 Subject: [PATCH 136/582] restore basic file --- ...ifier-dev-ruuter-private.rootcode.software | 31 +++---------------- 1 file changed, 4 insertions(+), 27 deletions(-) diff --git a/rtc_nginx/esclassifier-dev-ruuter-private.rootcode.software b/rtc_nginx/esclassifier-dev-ruuter-private.rootcode.software index 03e89362..0086a87c 100644 --- a/rtc_nginx/esclassifier-dev-ruuter-private.rootcode.software +++ b/rtc_nginx/esclassifier-dev-ruuter-private.rootcode.software @@ -1,4 +1,6 @@ server { + listen 80; + listen [::]:80; root /var/www/esclassifier-dev-ruuter-private.rootcode.software/html; index index.html index.htm index.nginx-debian.html; @@ -6,31 +8,6 @@ server { server_name esclassifier-dev-ruuter-private.rootcode.software; location / { - proxy_pass http://localhost:8088; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; + try_files $uri $uri/ =404; } - - listen [::]:443 ssl; # managed by Certbot - listen 443 ssl; # managed by Certbot - ssl_certificate /etc/letsencrypt/live/esclassifier-dev-ruuter-private.rootcode.software/fullchain.pem; # managed by Certbot - ssl_certificate_key /etc/letsencrypt/live/esclassifier-dev-ruuter-private.rootcode.software/privkey.pem; # managed by Certbot - include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot - ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot - -} - -server { - if ($host = esclassifier-dev-ruuter-private.rootcode.software) { - return 301 https://$host$request_uri; - } # managed by Certbot - - listen 80; - listen [::]:80; - - server_name esclassifier-dev-ruuter-private.rootcode.software; - return 404; # managed by Certbot - -} +} \ No newline at end of file From 88e334f5aac9700f4b9735ec97bcf7303fa55731 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Wed, 10 Jul 2024 16:36:40 +0530 Subject: [PATCH 137/582] query invalidation issue fixed --- GUI/src/App.tsx | 27 ++++++------- GUI/src/pages/UserManagement/UserModal.tsx | 30 ++++++-------- GUI/src/pages/UserManagement/index.tsx | 47 ++++++++++------------ GUI/src/services/users.ts | 4 +- GUI/src/types/user.ts | 4 +- 5 files changed, 50 insertions(+), 62 deletions(-) diff --git a/GUI/src/App.tsx b/GUI/src/App.tsx index 531fd508..484b7513 100644 --- a/GUI/src/App.tsx +++ b/GUI/src/App.tsx @@ -6,24 +6,21 @@ import './locale/et_EE'; import UserManagement from 'pages/UserManagement'; import Integrations from 'pages/Integrations'; import DatasetGroups from 'pages/DatasetGroups'; -import apiDev from 'services/api-dev'; +import { useQuery } from '@tanstack/react-query'; +import { UserInfo } from 'types/userInfo'; const App: FC = () => { - const getUserInfo = () => { - apiDev - .get(`auth/jwt/userinfo`) - .then((res: any) => { - localStorage.setItem('exp', res?.data?.response?.JWTExpirationTimestamp); - return useStore.getState().setUserInfo(res?.data?.response); - }) - .catch((error: any) => console.log(error)); - }; - - useEffect(() => { - getUserInfo(); - }, []); - + useQuery<{ + data: { custom_jwt_userinfo: UserInfo }; + }>({ + queryKey: ['auth/jwt/userinfo', 'prod'], + onSuccess: (res: { response: UserInfo }) => { + localStorage.setItem('exp', res.response.JWTExpirationTimestamp); + return useStore.getState().setUserInfo(res.response); + }, + }); + return ( }> diff --git a/GUI/src/pages/UserManagement/UserModal.tsx b/GUI/src/pages/UserManagement/UserModal.tsx index 4df62473..e36ede8d 100644 --- a/GUI/src/pages/UserManagement/UserModal.tsx +++ b/GUI/src/pages/UserManagement/UserModal.tsx @@ -1,8 +1,7 @@ -import { FC, useMemo } from 'react'; import { useForm, Controller } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { AxiosError } from 'axios'; -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import { Button, Dialog, FormInput, Track } from 'components'; import { User, UserDTO } from 'types/user'; @@ -11,6 +10,7 @@ import { useToast } from 'hooks/useToast'; import { ROLES } from 'utils/constants'; import Select from 'react-select'; import './SettingsUsers.scss'; +import { FC, useMemo } from 'react'; type UserModalProps = { onClose: () => void; @@ -22,7 +22,7 @@ const UserModal: FC = ({ onClose, user, isModalOpen }) => { const { t } = useTranslation(); const toast = useToast(); const queryClient = useQueryClient(); - + const { register, control, @@ -30,7 +30,7 @@ const UserModal: FC = ({ onClose, user, isModalOpen }) => { formState: { errors }, } = useForm({ defaultValues: { - userIdCode: user?.userIdCode, + useridcode: user?.useridcode, authorities: user?.authorities, displayName: user?.fullName, csaTitle: user?.csaTitle, @@ -47,16 +47,13 @@ const UserModal: FC = ({ onClose, user, isModalOpen }) => { value: ROLES.ROLE_MODEL_TRAINER, }, ], - [] + [t] ); const userCreateMutation = useMutation({ mutationFn: (data: UserDTO) => createUser(data), onSuccess: async () => { - await queryClient.invalidateQueries([ - 'accounts/users', - 'prod', - ]); + await queryClient.invalidateQueries(['accounts/users']); toast.open({ type: 'success', title: t('global.notification'), @@ -82,10 +79,7 @@ const UserModal: FC = ({ onClose, user, isModalOpen }) => { userData: UserDTO; }) => editUser(id, userData), onSuccess: async () => { - await queryClient.invalidateQueries([ - 'accounts/users', - 'prod', - ]); + await queryClient.invalidateQueries(['accounts/users']); toast.open({ type: 'success', title: t('global.notification'), @@ -131,7 +125,7 @@ const UserModal: FC = ({ onClose, user, isModalOpen }) => { const handleUserSubmit = handleSubmit((data) => { if (user) { - userEditMutation.mutate({ id: user.idCode, userData: data }); + userEditMutation.mutate({ id: user.useridcode, userData: data }); } else { checkIfUserExistsMutation.mutate({ userData: data }); } @@ -186,7 +180,7 @@ const UserModal: FC = ({ onClose, user, isModalOpen }) => { required={true} options={roles} defaultValue={user?.authorities.map((v) => { - return { label: t(`roles.${v ?? ''}`), value: v }; + return { label: t(`roles.${v}`), value: v }; })} isMulti={true} placeholder={t('userManagement.addUser.rolePlaceholder')} @@ -203,7 +197,7 @@ const UserModal: FC = ({ onClose, user, isModalOpen }) => { )} {!user && ( = ({ onClose, user, isModalOpen }) => { > )} - {!user && errors.userIdCode && ( + {!user && errors.useridcode && ( - {errors.userIdCode.message} + {errors.useridcode.message} )} diff --git a/GUI/src/pages/UserManagement/index.tsx b/GUI/src/pages/UserManagement/index.tsx index 61229089..dc1b665b 100644 --- a/GUI/src/pages/UserManagement/index.tsx +++ b/GUI/src/pages/UserManagement/index.tsx @@ -17,7 +17,8 @@ import { MdOutlineDeleteOutline, MdOutlineEdit } from 'react-icons/md'; import './UserManagement.scss'; import { useTranslation } from 'react-i18next'; import { ROLES } from 'utils/constants'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; + import { deleteUser } from 'services/users'; import { useToast } from 'hooks/useToast'; import { AxiosError } from 'axios'; @@ -40,30 +41,26 @@ const UserManagement: FC = () => { const [sorting, setSorting] = useState([]); const { t } = useTranslation(); const toast = useToast(); + const queryClient = useQueryClient(); - useEffect(() => { - getUsers(pagination, sorting); - }, []); - - - const getUsers = (pagination: PaginationState, sorting: SortingState) => { + const fetchUsers = async (pagination: PaginationState, sorting: SortingState) => { const sort = sorting.length === 0 ? 'name asc' : sorting[0].id + ' ' + (sorting[0].desc ? 'desc' : 'asc'); - apiDev - .post(`accounts/users`, { - page: pagination.pageIndex + 1, - page_size: pagination.pageSize, - sorting: sort, - }) - .then((res: any) => { - setUsersList(res?.data?.response ?? []); - setTotalPages(res?.data?.response[0]?.totalPages ?? 1); - }) - .catch((error: any) => console.log(error)); + const { data } = await apiDev.post('accounts/users', { + page: pagination.pageIndex + 1, + page_size: pagination.pageSize, + sorting: sort, + }); + return data?.response ?? []; }; + const { data: users, isLoading } = useQuery( + ['accounts/users', pagination, sorting], + () => fetchUsers(pagination, sorting) + ); + const editView = (props: any) => (
    { ) return; setPagination(state); - getUsers(state, sorting); + fetchUsers(state, sorting); }} sorting={sorting} setSorting={(state: SortingState) => { setSorting(state); - getUsers(pagination, state); + fetchUsers(pagination, state); }} pagesCount={totalPages} isClientSide={false} diff --git a/GUI/src/services/users.ts b/GUI/src/services/users.ts index 428ed4b9..63a923ec 100644 --- a/GUI/src/services/users.ts +++ b/GUI/src/services/users.ts @@ -7,7 +7,7 @@ export async function createUser(userData: UserDTO) { const { data } = await apiDev.post('accounts/add', { "firstName": fullName?.split(' ').slice(0, 1).join(' ') ?? '', "lastName": fullName?.split(' ').slice(1, 2).join(' ') ?? '', - "userIdCode": userData.userIdCode, + "userIdCode": userData.useridcode, "displayName": userData.fullName, "csaTitle": userData.csaTitle, "csa_email": userData.csaEmail, @@ -18,7 +18,7 @@ export async function createUser(userData: UserDTO) { export async function checkIfUserExists(userData: UserDTO) { const { data } = await apiDev.post('accounts/exists', { - "userIdCode": userData.userIdCode + "userIdCode": userData.useridcode }); return data; } diff --git a/GUI/src/types/user.ts b/GUI/src/types/user.ts index 2b26f520..02b0b362 100644 --- a/GUI/src/types/user.ts +++ b/GUI/src/types/user.ts @@ -5,7 +5,7 @@ export interface User { fullName?: string; firstName: string; lastName: string; - userIdCode: string; + useridcode: string; displayName: string; csaTitle: string; csaEmail: string; @@ -13,5 +13,5 @@ export interface User { customerSupportStatus: 'online' | 'idle' | 'offline'; } -export interface UserDTO extends Pick { +export interface UserDTO extends Pick { } From 5243d386432667c3de12fa602eb45d77540f57de Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Wed, 10 Jul 2024 22:52:23 +0530 Subject: [PATCH 138/582] integration status api integration --- .../IntegrationCard/IntegrationCard.scss | 1 + .../molecules/IntegrationCard/index.tsx | 186 ++++++++++++++---- GUI/src/pages/Integrations/index.tsx | 32 +-- GUI/src/services/integration.ts | 15 ++ GUI/src/types/integration.ts | 4 + GUI/translations/en/common.json | 40 +++- 6 files changed, 218 insertions(+), 60 deletions(-) create mode 100644 GUI/src/services/integration.ts create mode 100644 GUI/src/types/integration.ts diff --git a/GUI/src/components/molecules/IntegrationCard/IntegrationCard.scss b/GUI/src/components/molecules/IntegrationCard/IntegrationCard.scss index c15ab0d2..bccbd437 100644 --- a/GUI/src/components/molecules/IntegrationCard/IntegrationCard.scss +++ b/GUI/src/components/molecules/IntegrationCard/IntegrationCard.scss @@ -7,6 +7,7 @@ .logo { margin-right: 8px; + width: 120px; } diff --git a/GUI/src/components/molecules/IntegrationCard/index.tsx b/GUI/src/components/molecules/IntegrationCard/index.tsx index f4c96023..7bbe93d0 100644 --- a/GUI/src/components/molecules/IntegrationCard/index.tsx +++ b/GUI/src/components/molecules/IntegrationCard/index.tsx @@ -1,7 +1,11 @@ import { FC, PropsWithChildren, ReactNode, useState } from 'react'; import './IntegrationCard.scss'; import { useTranslation } from 'react-i18next'; -import { Button, Card, Dialog, FormInput, Switch } from 'components'; +import { Button, Card, Dialog, Switch } from 'components'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { IntegrationStatus } from 'types/integration'; +import { togglePlatform } from 'services/integration'; +import { AxiosError } from 'axios'; type IntegrationCardProps = { logo?: ReactNode; @@ -9,7 +13,6 @@ type IntegrationCardProps = { channelDescription?: string; user?: string; isActive?: boolean; - connectedStatus?: { platform: string, status: string }[]; }; const IntegrationCard: FC> = ({ @@ -18,38 +21,81 @@ const IntegrationCard: FC> = ({ channelDescription, user, isActive, - connectedStatus, }) => { const { t } = useTranslation(); const [isModalOpen, setIsModalOpen] = useState(false); const [isChecked, setIsChecked] = useState(isActive); const [modalType, setModalType] = useState('JIRA_INTEGRATION'); + const queryClient = useQueryClient(); const renderStatusIndicators = () => { - return connectedStatus?.map((status, index) => ( - - {connectedStatus?.length>1 ? `${status.status} - ${status.platform}`:`${status.status}`} + // return connectedStatus?.map((status, index) => ( + // + // + // {connectedStatus?.length > 1 + // ? `${status.status} - ${status.platform}` + // : `${status.status}`} + // + // )); + + return ( + + + <> + {isActive + ? t('integration.connected') + : t('integration.disconnected')} + - )); + ); }; - const onSelect=()=>{ - if(isChecked){ - setModalType("DISCONNECT"); - }else{ - setIsChecked(true) - setModalType("SUCCESS"); - - } - setIsModalOpen(true) + const platformEnableMutation = useMutation({ + mutationFn: (data: IntegrationStatus) => togglePlatform(data), + onSuccess: async () => { + await queryClient.invalidateQueries([ + 'classifier/integration/platform-status', + 'prod', + ]); + // setIsChecked(true); + setModalType('INTEGRATION_SUCCESS'); + }, + onError: (error: AxiosError) => { + setModalType('INTEGRATION_ERROR'); + }, + }); - } + const platformDisableMutation = useMutation({ + mutationFn: (data: IntegrationStatus) => togglePlatform(data), + onSuccess: async () => { + await queryClient.invalidateQueries([ + 'classifier/integration/platform-status', + 'prod', + ]); + // setIsChecked(true); + // setModalType('DISCONNECT_CONFIRMATION'); + }, + onError: (error: AxiosError) => { + setModalType('DISCONNECT_ERROR'); + }, + }); + + const onSelect = () => { + if (isChecked) { + setModalType("DISCONNECT_CONFIRMATION") + } else { + setModalType("CONNECT_CONFIRMATION") + } + setIsModalOpen(true); + }; return ( <> - +
    {logo}
    @@ -57,60 +103,114 @@ const IntegrationCard: FC> = ({

    {channelDescription}

    -
    - -
    -
    +
    + +
    +
    -
    - {renderStatusIndicators()} +
    + {renderStatusIndicators()} +
    -
    -
    - +
    {' '} +
    - - {modalType === 'SUCCESS' && ( + + {modalType === 'INTEGRATION_SUCCESS' && ( + setIsModalOpen(false)} + isOpen={isModalOpen} + title={t('integration.integrationSuccessTitle')} + > +
    + {t('integration.integrationSuccessDesc', { channel })} +
    +
    + )} + {modalType === 'INTEGRATION_ERROR' && ( setIsModalOpen(false)} isOpen={isModalOpen} - title={'Integration Successful'} + title={t('integration.integrationErrorTitle')} >
    - You have successfully connected with {channel}! Your integration is now complete, and you can start working with Jira seamlessly. + {t('integration.integrationErrorDesc', { channel })}
    )} - {modalType === 'ERROR' && ( + {modalType === 'DISCONNECT_CONFIRMATION' && ( setIsModalOpen(false)} isOpen={isModalOpen} - title={'Integration Error'} + title={t('integration.confirmationModalTitle')} + footer={ + <> + + + + } >
    - Failed to connect with {channel}. Please check your settings and try again. If the problem persists, contact support for assistance. + {t('integration.disconnectConfirmationModalDesc', { channel })}
    )} - {modalType === 'DISCONNECT' && ( + {modalType === 'CONNECT_CONFIRMATION' && ( setIsModalOpen(false)} isOpen={isModalOpen} - title={'Are you sure?'} + title={t('integration.confirmationModalTitle')} footer={ <> - - } >
    - Are you sure you want to disconnect the {channel} integration? This action cannot be undone and may affect your workflow and linked issues. + {t('integration.connectConfirmationModalDesc', { channel })} +
    +
    + )} + {modalType === 'DISCONNECT_ERROR' && ( + setIsModalOpen(false)} + isOpen={isModalOpen} + title={t('integration.disconnectErrorTitle')} + > +
    + {t('integration.disconnectErrorDesc', { channel })}
    )} diff --git a/GUI/src/pages/Integrations/index.tsx b/GUI/src/pages/Integrations/index.tsx index a2fe4620..732ce58f 100644 --- a/GUI/src/pages/Integrations/index.tsx +++ b/GUI/src/pages/Integrations/index.tsx @@ -6,39 +6,43 @@ import IntegrationCard from 'components/molecules/IntegrationCard'; import Outlook from 'assets/Outlook'; import Pinal from 'assets/Pinal'; import Jira from 'assets/Jira'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { getIntegrationStatus } from 'services/integration'; const Integrations: FC = () => { const { t } = useTranslation(); + const queryClient = useQueryClient(); + + const { data: integrationStatus, isLoading } = useQuery( + ['classifier/integration/platform-status'], + () => getIntegrationStatus() + ); +console.log(integrationStatus); return ( <>
    -
    Integration
    +
    {t('integration.title')}
    } - channel={"Jira"} - channelDescription={"Atlassian vea jälgimise ja projektijuhtimise tarkvara"} - user={"Rickey Walker - Admin"} - isActive={false} - connectedStatus={[{platform:"Jira", status:"Connected"}]} + channel={t('integration.jira')??""} + channelDescription={t('integration.jiraDesc')??""} + isActive={integrationStatus?.jira_connection_status} /> } - channel={"Outlook"} - channelDescription={"Atlassian vea jälgimise ja projektijuhtimise tarkvara"} - user={"Rickey Walker - Admin"} - isActive={true} - connectedStatus={[{platform:"Outlook", status:"Connected"}]} + channel={t('integration.outlook')??""} + channelDescription={t('integration.outlookDesc')??""} + isActive={integrationStatus?.outlook_connection_status} /> } channel={"Outlook+Pinal"} - channelDescription={"Atlassian vea jälgimise ja projektijuhtimise tarkvara"} + channelDescription={t('integration.pinalDesc')??""} user={"Rickey Walker - Admin"} - isActive={true} - connectedStatus={[{platform:"Outlook", status:"Connected"}, {platform:"Pinal", status:"Disconnected"}]} + isActive={integrationStatus?.pinal_connection_status} />
    diff --git a/GUI/src/services/integration.ts b/GUI/src/services/integration.ts new file mode 100644 index 00000000..0f5e2b97 --- /dev/null +++ b/GUI/src/services/integration.ts @@ -0,0 +1,15 @@ +import { IntegrationStatus } from 'types/integration'; +import apiDev from './api-dev'; + +export async function getIntegrationStatus() { + const { data } = await apiDev.get('classifier/integration/platform-status'); + return data?.response; + } + + export async function togglePlatform(integrationData: IntegrationStatus) { + const { data } = await apiDev.post('classifier/integration/toggle-platform', { + "operation": integrationData.operation, + "platform": integrationData.platform + }); + return data; + } \ No newline at end of file diff --git a/GUI/src/types/integration.ts b/GUI/src/types/integration.ts new file mode 100644 index 00000000..af05c91b --- /dev/null +++ b/GUI/src/types/integration.ts @@ -0,0 +1,4 @@ +export interface IntegrationStatus { + platform?: string; + operation?: string; + } \ No newline at end of file diff --git a/GUI/translations/en/common.json b/GUI/translations/en/common.json index 7d65390f..f7a9a714 100644 --- a/GUI/translations/en/common.json +++ b/GUI/translations/en/common.json @@ -17,6 +17,8 @@ "active": "Active", "activate": "Activate", "deactivate": "Deactivate", + "disconnect":"Disconnect", + "connect":"Connect", "on": "On", "off": "Off", "back": "Back", @@ -64,8 +66,41 @@ "incomingTexts": "Incoming Texts" }, "userManagement": { - "title":"User Management", - "addUserButton":" Add a user", + "title": "User Management", + "addUserButton": " Add a user", + "addUser": { + "name": "First and last name", + "namePlaceholder": "Enter name", + "role": "Role", + "rolePlaceholder": "-Select-", + "personalId": "Personal ID", + "personalIdPlaceholder": "Enter personal ID", + "title": "Title", + "titlePlaceholder": "Enter title", + "email": "Email", + "emailPlaceholder": "Enter email" + } + }, + "integration": { + "title": "Integration", + "jira": "Jira", + "outlook": "Outlook", + "pinal": "Pinal", + "jiraDesc": "Atlassian issue tracking and project management software", + "outlookDesc": "Personal information manager and email application developed by Microsoft", + "pinalDesc": "Atlassian issue tracking and project management software", + "connected": "Connected", + "disconnected": "Disconnected", + "integrationErrorTitle": "Integration Unsuccessful", + "integrationErrorDesc": "Failed to connect with {{channel}}. Please check your settings and try again. If the problem persists, contact support for assistance.", + "integrationSuccessTitle": "Integration Successful", + "integrationSuccessDesc": "You have successfully connected with {{channel}}! Your integration is now complete, and you can start working with {{Jira}} seamlessly.", + "confirmationModalTitle": "Are you sure?", + "disconnectConfirmationModalDesc": "Are you sure you want to disconnect the {{channel}} integration? This action cannot be undone and may affect your workflow and linked issues.", + "connectConfirmationModalDesc": "Are you sure you want to connect the {{channel}} integration?", + "disconnectErrorTitle": "Disconnection Unsuccessful", + "disconnectErrorDesc": "Failed to disconnect {{channel}}. Please check your settings and try again. If the problem persists, contact support for assistance.", + "addUserButton": " Add a user", "addUser": { "name": "First and last name", "namePlaceholder": "Enter name", @@ -77,7 +112,6 @@ "titlePlaceholder": "Enter title", "email": "Email", "emailPlaceholder": "Enter email" - } }, "roles": { From 9cdf3bffe9dd1462518a62cbf5d444979d395d17 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Thu, 11 Jul 2024 13:52:38 +0530 Subject: [PATCH 139/582] toggle platform api integration and code refactoring --- GUI/src/components/Card/index.tsx | 6 +- GUI/src/components/Header/index.tsx | 14 +- .../molecules/IntegrationCard/index.tsx | 51 ++-- GUI/src/enums/integrationEnums.ts | 12 + GUI/src/enums/roles.ts | 4 + GUI/src/hoc/with-authorization.tsx | 2 +- GUI/src/pages/UserManagement/UserModal.tsx | 2 +- GUI/src/pages/UserManagement/index.tsx | 2 +- GUI/src/store/index.ts | 239 +----------------- GUI/src/types/integration.ts | 6 +- GUI/src/types/user.ts | 2 +- GUI/src/utils/constants.ts | 5 - 12 files changed, 52 insertions(+), 293 deletions(-) create mode 100644 GUI/src/enums/integrationEnums.ts create mode 100644 GUI/src/enums/roles.ts diff --git a/GUI/src/components/Card/index.tsx b/GUI/src/components/Card/index.tsx index 6cab8644..27eb7501 100644 --- a/GUI/src/components/Card/index.tsx +++ b/GUI/src/components/Card/index.tsx @@ -9,7 +9,7 @@ type CardProps = { borderless?: boolean; isHeaderLight?: boolean; isBodyDivided?: boolean; - isfullwidth?: boolean; + isFullWidth?: boolean; }; const Card: FC> = ({ @@ -19,10 +19,10 @@ const Card: FC> = ({ isHeaderLight, isBodyDivided, children, - isfullwidth, + isFullWidth, }) => { return ( -
    +
    {header && (
    {header} diff --git a/GUI/src/components/Header/index.tsx b/GUI/src/components/Header/index.tsx index dbea1c80..29f1f354 100644 --- a/GUI/src/components/Header/index.tsx +++ b/GUI/src/components/Header/index.tsx @@ -1,29 +1,19 @@ import { FC, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import { AxiosError } from 'axios'; import { useIdleTimer } from 'react-idle-timer'; import { MdOutlineExpandMore } from 'react-icons/md'; import { Track, - Button, - Icon, - Drawer, - Section, - SwitchBox, - Switch, - Dialog, + Button } from 'components'; import useStore from 'store'; import { ReactComponent as BykLogo } from 'assets/logo.svg'; -import { UserProfileSettings } from 'types/userProfileSettings'; -import { Chat as ChatType } from 'types/chat'; import { useToast } from 'hooks/useToast'; import { USER_IDLE_STATUS_TIMEOUT } from 'constants/config'; import apiDev from 'services/api-dev'; -import { interval } from 'rxjs'; -import { AUTHORITY } from 'types/authorities'; import { useCookies } from 'react-cookie'; import './Header.scss'; diff --git a/GUI/src/components/molecules/IntegrationCard/index.tsx b/GUI/src/components/molecules/IntegrationCard/index.tsx index 7bbe93d0..c56a221d 100644 --- a/GUI/src/components/molecules/IntegrationCard/index.tsx +++ b/GUI/src/components/molecules/IntegrationCard/index.tsx @@ -3,15 +3,15 @@ import './IntegrationCard.scss'; import { useTranslation } from 'react-i18next'; import { Button, Card, Dialog, Switch } from 'components'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { IntegrationStatus } from 'types/integration'; +import { OperationConfig } from 'types/integration'; import { togglePlatform } from 'services/integration'; import { AxiosError } from 'axios'; +import { INTEGRATION_MODALS, INTEGRATION_OPERATIONS } from 'enums/integrationEnums'; type IntegrationCardProps = { logo?: ReactNode; channel?: string; channelDescription?: string; - user?: string; isActive?: boolean; }; @@ -19,16 +19,16 @@ const IntegrationCard: FC> = ({ logo, channel, channelDescription, - user, isActive, }) => { + const { t } = useTranslation(); const [isModalOpen, setIsModalOpen] = useState(false); - const [isChecked, setIsChecked] = useState(isActive); - const [modalType, setModalType] = useState('JIRA_INTEGRATION'); + const [modalType, setModalType] = useState(''); const queryClient = useQueryClient(); const renderStatusIndicators = () => { + //kept this, in case the logic is changed for the connected status // return connectedStatus?.map((status, index) => ( // // > = ({ }; const platformEnableMutation = useMutation({ - mutationFn: (data: IntegrationStatus) => togglePlatform(data), + mutationFn: (data: OperationConfig) => togglePlatform(data), onSuccess: async () => { + setModalType(INTEGRATION_MODALS.INTEGRATION_SUCCESS); await queryClient.invalidateQueries([ - 'classifier/integration/platform-status', - 'prod', + 'classifier/integration/platform-status' ]); - // setIsChecked(true); - setModalType('INTEGRATION_SUCCESS'); }, onError: (error: AxiosError) => { - setModalType('INTEGRATION_ERROR'); + setModalType(INTEGRATION_MODALS.INTEGRATION_ERROR); }, }); const platformDisableMutation = useMutation({ - mutationFn: (data: IntegrationStatus) => togglePlatform(data), + mutationFn: (data: OperationConfig) => togglePlatform(data), onSuccess: async () => { await queryClient.invalidateQueries([ - 'classifier/integration/platform-status', - 'prod', + 'classifier/integration/platform-status' ]); - // setIsChecked(true); - // setModalType('DISCONNECT_CONFIRMATION'); - }, + setIsModalOpen(false) }, onError: (error: AxiosError) => { - setModalType('DISCONNECT_ERROR'); + setModalType(INTEGRATION_MODALS.DISCONNECT_ERROR); }, }); const onSelect = () => { - if (isChecked) { - setModalType("DISCONNECT_CONFIRMATION") + if (isActive) { + setModalType(INTEGRATION_MODALS.DISCONNECT_CONFIRMATION) } else { - setModalType("CONNECT_CONFIRMATION") + setModalType(INTEGRATION_MODALS.CONNECT_CONFIRMATION) } setIsModalOpen(true); }; @@ -117,7 +112,7 @@ const IntegrationCard: FC> = ({
    - {modalType === 'INTEGRATION_SUCCESS' && ( + {modalType === INTEGRATION_MODALS.INTEGRATION_SUCCESS && ( setIsModalOpen(false)} isOpen={isModalOpen} @@ -128,7 +123,7 @@ const IntegrationCard: FC> = ({
    )} - {modalType === 'INTEGRATION_ERROR' && ( + {modalType ===INTEGRATION_MODALS.INTEGRATION_ERROR && ( setIsModalOpen(false)} isOpen={isModalOpen} @@ -139,7 +134,7 @@ const IntegrationCard: FC> = ({
    )} - {modalType === 'DISCONNECT_CONFIRMATION' && ( + {modalType === INTEGRATION_MODALS.DISCONNECT_CONFIRMATION && ( setIsModalOpen(false)} isOpen={isModalOpen} @@ -156,7 +151,7 @@ const IntegrationCard: FC> = ({ appearance="error" onClick={() => { platformDisableMutation.mutate({ - operation: 'disable', + operation: INTEGRATION_OPERATIONS.DISABLE, platform: channel?.toLowerCase(), }); }} @@ -171,7 +166,7 @@ const IntegrationCard: FC> = ({
    )} - {modalType === 'CONNECT_CONFIRMATION' && ( + {modalType === INTEGRATION_MODALS.CONNECT_CONFIRMATION && ( setIsModalOpen(false)} isOpen={isModalOpen} @@ -188,7 +183,7 @@ const IntegrationCard: FC> = ({ appearance="primary" onClick={() => { platformEnableMutation.mutate({ - operation: 'enable', + operation: INTEGRATION_OPERATIONS.ENABLE, platform: channel?.toLowerCase(), }); }} @@ -203,7 +198,7 @@ const IntegrationCard: FC> = ({ )} - {modalType === 'DISCONNECT_ERROR' && ( + {modalType === INTEGRATION_MODALS.DISCONNECT_ERROR && ( setIsModalOpen(false)} isOpen={isModalOpen} diff --git a/GUI/src/enums/integrationEnums.ts b/GUI/src/enums/integrationEnums.ts new file mode 100644 index 00000000..4bf901d2 --- /dev/null +++ b/GUI/src/enums/integrationEnums.ts @@ -0,0 +1,12 @@ +export enum INTEGRATION_MODALS { + INTEGRATION_SUCCESS = 'INTEGRATION_SUCCESS', + INTEGRATION_ERROR='INTEGRATION_ERROR', + DISCONNECT_CONFIRMATION='DISCONNECT_CONFIRMATION', + CONNECT_CONFIRMATION='CONNECT_CONFIRMATION', + DISCONNECT_ERROR='DISCONNECT_ERROR' + } + + export enum INTEGRATION_OPERATIONS { + ENABLE = 'enable', + DISABLE='disable' + } \ No newline at end of file diff --git a/GUI/src/enums/roles.ts b/GUI/src/enums/roles.ts new file mode 100644 index 00000000..94b602f1 --- /dev/null +++ b/GUI/src/enums/roles.ts @@ -0,0 +1,4 @@ +export enum ROLES { + ROLE_ADMINISTRATOR = 'ROLE_ADMINISTRATOR', + ROLE_MODEL_TRAINER='ROLE_MODEL_TRAINER' + } \ No newline at end of file diff --git a/GUI/src/hoc/with-authorization.tsx b/GUI/src/hoc/with-authorization.tsx index a36e021f..59947ffc 100644 --- a/GUI/src/hoc/with-authorization.tsx +++ b/GUI/src/hoc/with-authorization.tsx @@ -1,6 +1,6 @@ +import { ROLES } from 'enums/roles'; import React from 'react'; import useStore from 'store'; -import { ROLES } from 'utils/constants'; function withAuthorization

    ( WrappedComponent: React.ComponentType

    , diff --git a/GUI/src/pages/UserManagement/UserModal.tsx b/GUI/src/pages/UserManagement/UserModal.tsx index e36ede8d..da3a31a8 100644 --- a/GUI/src/pages/UserManagement/UserModal.tsx +++ b/GUI/src/pages/UserManagement/UserModal.tsx @@ -7,10 +7,10 @@ import { Button, Dialog, FormInput, Track } from 'components'; import { User, UserDTO } from 'types/user'; import { checkIfUserExists, createUser, editUser } from 'services/users'; import { useToast } from 'hooks/useToast'; -import { ROLES } from 'utils/constants'; import Select from 'react-select'; import './SettingsUsers.scss'; import { FC, useMemo } from 'react'; +import { ROLES } from 'enums/roles'; type UserModalProps = { onClose: () => void; diff --git a/GUI/src/pages/UserManagement/index.tsx b/GUI/src/pages/UserManagement/index.tsx index dc1b665b..7b1865de 100644 --- a/GUI/src/pages/UserManagement/index.tsx +++ b/GUI/src/pages/UserManagement/index.tsx @@ -16,7 +16,6 @@ import { User } from '../../types/user'; import { MdOutlineDeleteOutline, MdOutlineEdit } from 'react-icons/md'; import './UserManagement.scss'; import { useTranslation } from 'react-i18next'; -import { ROLES } from 'utils/constants'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { deleteUser } from 'services/users'; @@ -24,6 +23,7 @@ import { useToast } from 'hooks/useToast'; import { AxiosError } from 'axios'; import apiDev from 'services/api-dev'; import UserModal from './UserModal'; +import { ROLES } from 'enums/roles'; const UserManagement: FC = () => { const columnHelper = createColumnHelper(); diff --git a/GUI/src/store/index.ts b/GUI/src/store/index.ts index 259c22d0..564d3215 100644 --- a/GUI/src/store/index.ts +++ b/GUI/src/store/index.ts @@ -1,253 +1,16 @@ import { create } from 'zustand'; import { UserInfo } from 'types/userInfo'; -import { - CHAT_STATUS, - Chat as ChatType, - GroupedChat, - GroupedPendingChat, -} from 'types/chat'; -import apiDev from 'services/api-dev'; interface StoreState { userInfo: UserInfo | null; userId: string; - activeChats: ChatType[]; - pendingChats: ChatType[]; - selectedChatId: string | null; - chatCsaActive: boolean; - setActiveChats: (chats: ChatType[]) => void; - setPendingChats: (chats: ChatType[]) => void; setUserInfo: (info: UserInfo) => void; - setSelectedChatId: (id: string | null) => void; - setChatCsaActive: (active: boolean) => void; - selectedChat: () => ChatType | null | undefined; - selectedPendingChat: () => ChatType | null | undefined; - unansweredChats: () => ChatType[]; - forwordedChats: () => ChatType[]; - unansweredChatsLength: () => number; - forwordedChatsLength: () => number; - loadActiveChats: () => Promise; - getGroupedActiveChats: () => GroupedChat; - getGroupedUnansweredChats: () => GroupedChat; - loadPendingChats: () => Promise; - getGroupedPendingChats: () => GroupedPendingChat; } -const useStore = create((set, get, store) => ({ +const useStore = create((set) => ({ userInfo: null, userId: '', - activeChats: [], - pendingChats: [], - selectedChatId: null, - chatCsaActive: false, - setActiveChats: (chats) => set({ activeChats: chats }), - setPendingChats: (chats) => set({ pendingChats: chats }), setUserInfo: (data) => set({ userInfo: data, userId: data?.userIdCode || '' }), - setSelectedChatId: (id) => set({ selectedChatId: id }), - setChatCsaActive: (active) => { - set({ - chatCsaActive: active, - }); - get().loadActiveChats(); - get().loadPendingChats(); - }, - selectedChat: () => { - const selectedChatId = get().selectedChatId; - return get().activeChats.find((c) => c.id === selectedChatId); - }, - selectedPendingChat: () => { - const selectedChatId = get().selectedChatId; - return get().pendingChats.find((c) => c.id === selectedChatId); - }, - unansweredChats: () => { - return get().activeChats?.filter?.((c) => c.customerSupportId === '') ?? []; - }, - forwordedChats: () => { - const userId = get().userId; - return ( - get().activeChats?.filter( - (c) => - c.status === CHAT_STATUS.REDIRECTED && c.customerSupportId === userId - ) || [] - ); - }, - unansweredChatsLength: () => get().unansweredChats().length, - forwordedChatsLength: () => get().forwordedChats().length, - - loadActiveChats: async () => { - const res = await apiDev.get('agents/chats/active'); - const chats: ChatType[] = res.data.response ?? []; - const selectedChatId = get().selectedChatId; - const isChatStillExists = chats?.filter( - (e: any) => e.id === selectedChatId - ); - if (isChatStillExists.length === 0 && get().activeChats.length > 0) { - setTimeout(() => get().setActiveChats(chats), 3000); - } else { - get().setActiveChats(chats); - } - }, - loadPendingChats: async () => { - const res = await apiDev.get('agents/chats/pending'); - const chats: ChatType[] = res.data.response ?? []; - const selectedChatId = get().selectedChatId; - const isChatStillExists = chats?.filter( - (e: any) => e.id === selectedChatId - ); - if (isChatStillExists.length === 0 && get().pendingChats.length > 0) { - setTimeout(() => get().setPendingChats(chats), 3000); - } else { - get().setPendingChats(chats); - } - }, - getGroupedActiveChats: () => { - const activeChats = get().activeChats; - const userInfo = get().userInfo; - const chatCsaActive = get().chatCsaActive; - - const grouped: GroupedChat = { - myChats: [], - otherChats: [], - }; - - if (!activeChats) return grouped; - - if ( - chatCsaActive === false && - !userInfo?.authorities.includes('ROLE_ADMINISTRATOR') - ) { - if (get().selectedChatId !== null) { - get().setSelectedChatId(null); - } - return grouped; - } - - activeChats.forEach((c) => { - if (c.customerSupportId === userInfo?.idCode) { - grouped.myChats.push(c); - return; - } - - const groupIndex = grouped.otherChats.findIndex( - (x) => x.groupId === c.customerSupportId - ); - - if (c.customerSupportId !== '') { - if (groupIndex === -1) { - grouped.otherChats.push({ - groupId: c.customerSupportId ?? '', - name: c.customerSupportDisplayName ?? '', - chats: [c], - }); - } else { - grouped.otherChats[groupIndex].chats.push(c); - } - } - }); - - grouped.otherChats.sort((a, b) => a.name.localeCompare(b.name)); - return grouped; - }, - - getGroupedUnansweredChats: () => { - const activeChats = get().activeChats; - const userInfo = get().userInfo; - const chatCsaActive = get().chatCsaActive; - - const grouped: GroupedChat = { - myChats: [], - otherChats: [], - }; - - if (!activeChats) return grouped; - - if (chatCsaActive === true) { - activeChats.forEach((c) => { - if (c.customerSupportId === '') { - grouped.myChats.push(c); - } - }); - } else { - activeChats.forEach((c) => { - if ( - c.customerSupportId === userInfo?.idCode || - c.customerSupportId === '' - ) { - grouped.myChats.push(c); - return; - } - - grouped.myChats.sort((a, b) => a.created.localeCompare(b.created)); - const groupIndex = grouped.otherChats.findIndex( - (x) => x.groupId === c.customerSupportId - ); - if (c.customerSupportId !== '') { - if (groupIndex === -1) { - grouped.otherChats.push({ - groupId: c.customerSupportId ?? '', - name: c.customerSupportDisplayName ?? '', - chats: [c], - }); - } else { - grouped.otherChats[groupIndex].chats.push(c); - } - } - }); - - grouped.otherChats.sort((a, b) => a.name.localeCompare(b.name)); - } - - return grouped; - }, - getGroupedPendingChats: () => { - const pendingChats = get().pendingChats; - const userInfo = get().userInfo; - const chatCsaActive = get().chatCsaActive; - - const grouped: GroupedPendingChat = { - newChats: [], - inProcessChats: [], - myChats: [], - otherChats: [], - }; - - if (!pendingChats) return grouped; - - if (chatCsaActive) { - pendingChats.forEach((c) => { - if (c.customerSupportId === 'chatbot') { - grouped.newChats.push(c); - } else { - grouped.inProcessChats.push(c); - } - }); - - grouped.inProcessChats.forEach((c) => { - if (c.customerSupportId === userInfo?.idCode) { - grouped.myChats.push(c); - return; - } - - grouped.myChats.sort((a, b) => a.created.localeCompare(b.created)); - const groupIndex = grouped.otherChats.findIndex( - (x) => x.groupId === c.customerSupportId - ); - if (c.customerSupportId !== '') { - if (groupIndex === -1) { - grouped.otherChats.push({ - groupId: c.customerSupportId ?? '', - name: c.customerSupportDisplayName ?? '', - chats: [c], - }); - } else { - grouped.otherChats[groupIndex].chats.push(c); - } - } - grouped.otherChats.sort((a, b) => a.name.localeCompare(b.name)); - }); - } - return grouped; - }, })); export default useStore; diff --git a/GUI/src/types/integration.ts b/GUI/src/types/integration.ts index af05c91b..9e58f749 100644 --- a/GUI/src/types/integration.ts +++ b/GUI/src/types/integration.ts @@ -1,4 +1,4 @@ -export interface IntegrationStatus { - platform?: string; - operation?: string; + export interface OperationConfig { + operation: 'enable'|'disable'; + platform: string | undefined; } \ No newline at end of file diff --git a/GUI/src/types/user.ts b/GUI/src/types/user.ts index 02b0b362..e9ba4380 100644 --- a/GUI/src/types/user.ts +++ b/GUI/src/types/user.ts @@ -1,4 +1,4 @@ -import { ROLES } from 'utils/constants'; +import { ROLES } from "enums/roles"; export interface User { login?: string; diff --git a/GUI/src/utils/constants.ts b/GUI/src/utils/constants.ts index c3985900..e7244234 100644 --- a/GUI/src/utils/constants.ts +++ b/GUI/src/utils/constants.ts @@ -1,10 +1,5 @@ export const MESSAGE_FILE_SIZE_LIMIT = 10_000_000; -export enum ROLES { - ROLE_ADMINISTRATOR = 'ROLE_ADMINISTRATOR', - ROLE_MODEL_TRAINER='ROLE_MODEL_TRAINER' -} - export enum RUUTER_ENDPOINTS { SEND_ATTACHMENT= '/attachments/add' } From 639d937e82d94e78bae90d4eec4dcbaa616ed648 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Thu, 11 Jul 2024 15:23:48 +0530 Subject: [PATCH 140/582] ESCLASS-139: integration issue in outlook fixed by setting config and implement refresh token save flow --- ...rn_outlook_expiration_date_time.handlebars | 3 ++ DSL/DMapper/lib/helpers.js | 7 +++++ ...ifier-script-v5-dataset-group-metadata.sql | 29 ------------------- ...sifier-script-v5-outlook-refresh-token.xml | 12 ++++++++ .../classifier/integration/outlook/token.yml | 2 -- .../integration/outlook/subscribe.yml | 24 ++++++++++++--- .../TEMPLATES/check-user-authority-admin.yml | 4 ++- constants.ini | 1 + migrate.sh | 16 +++++++++- 9 files changed, 61 insertions(+), 37 deletions(-) create mode 100644 DSL/DMapper/hbs/return_outlook_expiration_date_time.handlebars delete mode 100644 DSL/Liquibase/changelog/classifier-script-v5-dataset-group-metadata.sql create mode 100644 DSL/Liquibase/changelog/classifier-script-v5-outlook-refresh-token.xml diff --git a/DSL/DMapper/hbs/return_outlook_expiration_date_time.handlebars b/DSL/DMapper/hbs/return_outlook_expiration_date_time.handlebars new file mode 100644 index 00000000..b41ad8ee --- /dev/null +++ b/DSL/DMapper/hbs/return_outlook_expiration_date_time.handlebars @@ -0,0 +1,3 @@ +{ + "expirationDateTime": "{{{getOutlookExpirationDateTime}}}" +} diff --git a/DSL/DMapper/lib/helpers.js b/DSL/DMapper/lib/helpers.js index 1dac2416..e34d6283 100644 --- a/DSL/DMapper/lib/helpers.js +++ b/DSL/DMapper/lib/helpers.js @@ -48,3 +48,10 @@ export function isLabelsMismatch(newLabels, previousLabels) { return true; } } + +export function getOutlookExpirationDateTime() { + const currentDate = new Date(); + currentDate.setDate(currentDate.getDate() + 3); + const updatedDateISOString = currentDate.toISOString(); + return updatedDateISOString; +} \ No newline at end of file diff --git a/DSL/Liquibase/changelog/classifier-script-v5-dataset-group-metadata.sql b/DSL/Liquibase/changelog/classifier-script-v5-dataset-group-metadata.sql deleted file mode 100644 index 2c5d5adc..00000000 --- a/DSL/Liquibase/changelog/classifier-script-v5-dataset-group-metadata.sql +++ /dev/null @@ -1,29 +0,0 @@ --- liquibase formatted sql - --- changeset kalsara Magamage:classifier-script-v5-changeset1 -CREATE TYPE Validation_Status AS ENUM ('success', 'fail', 'in-progress'); - --- changeset kalsara Magamage:classifier-script-v5-changeset2 -CREATE TABLE dataset_group_metadata ( - id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY, - group_name TEXT NOT NULL, - group_version TEXT NOT NULL, - latest BOOLEAN DEFAULT false, - is_enabled BOOLEAN DEFAULT false, - enable_allowed BOOLEAN DEFAULT false, - last_model_trained TEXT, - created_timestamp TIMESTAMP WITH TIME ZONE, - last_updated_timestamp TIMESTAMP WITH TIME ZONE, - last_used_for_training TIMESTAMP WITH TIME ZONE, - validation_status Validation_Status, - validation_errors JSONB, - processed_data_available BOOLEAN DEFAULT false, - raw_data_available BOOLEAN DEFAULT false, - num_samples INT, - raw_data_location TEXT, - preprocess_data_location TEXT, - validation_criteria JSONB, - class_hierarchy JSONB, - connected_models JSONB, - CONSTRAINT dataset_group_metadata_pkey PRIMARY KEY (id) -); \ No newline at end of file diff --git a/DSL/Liquibase/changelog/classifier-script-v5-outlook-refresh-token.xml b/DSL/Liquibase/changelog/classifier-script-v5-outlook-refresh-token.xml new file mode 100644 index 00000000..7488f738 --- /dev/null +++ b/DSL/Liquibase/changelog/classifier-script-v5-outlook-refresh-token.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/DSL/Ruuter.private/DSL/GET/classifier/integration/outlook/token.yml b/DSL/Ruuter.private/DSL/GET/classifier/integration/outlook/token.yml index d4a09476..2dfc3cd2 100644 --- a/DSL/Ruuter.private/DSL/GET/classifier/integration/outlook/token.yml +++ b/DSL/Ruuter.private/DSL/GET/classifier/integration/outlook/token.yml @@ -27,8 +27,6 @@ check_refresh_token: next: get_access_token next: return_not_found -#not supported for internal requests - get_access_token: call: http.post args: diff --git a/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/subscribe.yml b/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/subscribe.yml index 1bda7c11..3c4e188f 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/subscribe.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/subscribe.yml @@ -45,7 +45,9 @@ validate_request: get_token_info: call: http.get args: - url: "[#CLASSIFIER_RUUTER_PRIVATE_INTERNAL]/internal/xyz" + url: "[#CLASSIFIER_RUUTER_PRIVATE]/classifier/integration/outlook/token" + headers: + cookie: ${incoming.headers.cookie} result: res next: assign_access_token @@ -57,11 +59,25 @@ assign_access_token: check_integration_type: switch: - condition: ${is_connect === true && subscription_id === null} - next: subscribe_outlook + next: set_expiration_date_time - condition: ${is_connect === false && subscription_id !== null} next: unsubscribe_outlook next: return_bad_request +set_expiration_date_time: + call: http.post + args: + url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/return_outlook_expiration_date_time" + headers: + type: json + result: expiration_date_time_res + next: assign_expiration_date_time + +assign_expiration_date_time: + assign: + expiration_date_time: ${expiration_date_time_res.response.body.expirationDateTime} + next: subscribe_outlook + subscribe_outlook: call: http.post args: @@ -70,9 +86,9 @@ subscribe_outlook: Authorization: ${'Bearer ' + access_token} body: changeType: "created,updated" - notificationUrl: "https://f789-111-223-191-66.ngrok-free.app/classifier/integration/outlook/accept" + notificationUrl: "[#CLASSIFIER_RUUTER_PUBLIC_FRONTEND_URL]/classifier/integration/outlook/accept" resource: "me/mailFolders('inbox')/messages" - expirationDateTime: "2024-07-06T21:10:45.9356913Z" + expirationDateTime: ${expiration_date_time} clientState: "secretClientValue" result: res_subscribe next: check_subscribe_response diff --git a/DSL/Ruuter.private/DSL/TEMPLATES/check-user-authority-admin.yml b/DSL/Ruuter.private/DSL/TEMPLATES/check-user-authority-admin.yml index f23aa18a..6fa923f1 100644 --- a/DSL/Ruuter.private/DSL/TEMPLATES/check-user-authority-admin.yml +++ b/DSL/Ruuter.private/DSL/TEMPLATES/check-user-authority-admin.yml @@ -31,6 +31,8 @@ check_cookie_info_response: check_user_authority: switch: + - condition: ${res.response.body === null} + next: return_unauthorized - condition: ${res.response.body.authorities.includes("ROLE_ADMINISTRATOR")} next: return_authorized next: return_unauthorized @@ -40,7 +42,7 @@ return_authorized: next: end return_unauthorized: - status: 200 + status: 400 return: false next: end diff --git a/constants.ini b/constants.ini index 4624f4e2..74dcf6a8 100644 --- a/constants.ini +++ b/constants.ini @@ -5,6 +5,7 @@ CLASSIFIER_RUUTER_PRIVATE=http://ruuter-private:8088 CLASSIFIER_DMAPPER=http://data-mapper:3000 CLASSIFIER_RESQL=http://resql:8082 CLASSIFIER_TIM=http://tim:8085 +CLASSIFIER_RUUTER_PUBLIC_FRONTEND_URL=value DOMAIN=localhost JIRA_API_TOKEN= value JIRA_USERNAME= value diff --git a/migrate.sh b/migrate.sh index 779d9bab..7b5ed458 100644 --- a/migrate.sh +++ b/migrate.sh @@ -1,5 +1,19 @@ #!/bin/bash +# Define the path where the SQL file will be generated +SQL_FILE="DSL/Liquibase/data/update_refresh_token.sql" + +# Read the OUTLOOK_REFRESH_KEY value from the INI file +OUTLOOK_REFRESH_KEY=$(awk -F '=' '/OUTLOOK_REFRESH_KEY/ {print $2}' constants.ini | xargs) + +# Generate a SQL script with the extracted value +cat << EOF > "$SQL_FILE" +-- Update the refresh token in the database +UPDATE integration_status +SET token = '$OUTLOOK_REFRESH_KEY' +WHERE platform='OUTLOOK'; +EOF + # Function to parse ini file and extract the value for a given key under a given section get_ini_value() { local file=$1 @@ -12,4 +26,4 @@ INI_FILE="constants.ini" DB_PASSWORD=$(get_ini_value "$INI_FILE" "DB_PASSWORD") -docker run --rm --network bykstack -v `pwd`/DSL/Liquibase/changelog:/liquibase/changelog -v `pwd`/DSL/Liquibase/master.yml:/liquibase/master.yml -v `pwd`/DSL/Liquibase/data:/liquibase/data liquibase/liquibase --defaultsFile=/liquibase/changelog/liquibase.properties --changelog-file=master.yml --url=jdbc:postgresql://users_db:5432/classifier?user=postgres --password=$DB_PASSWORD update \ No newline at end of file +docker run --rm --network bykstack -v `pwd`/DSL/Liquibase/changelog:/liquibase/changelog -v `pwd`/DSL/Liquibase/master.yml:/liquibase/master.yml -v `pwd`/DSL/Liquibase/data:/liquibase/data liquibase/liquibase --defaultsFile=/liquibase/changelog/liquibase.properties --changelog-file=master.yml --url=jdbc:postgresql://users_db:5432/classifier?user=root --password=$DB_PASSWORD update \ No newline at end of file From 225cda16ce077902eb486177a76aef149e3f3f16 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Thu, 11 Jul 2024 15:34:36 +0530 Subject: [PATCH 141/582] ESCLASS-139: refactor --- migrate.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrate.sh b/migrate.sh index 7b5ed458..9a1b580e 100644 --- a/migrate.sh +++ b/migrate.sh @@ -26,4 +26,4 @@ INI_FILE="constants.ini" DB_PASSWORD=$(get_ini_value "$INI_FILE" "DB_PASSWORD") -docker run --rm --network bykstack -v `pwd`/DSL/Liquibase/changelog:/liquibase/changelog -v `pwd`/DSL/Liquibase/master.yml:/liquibase/master.yml -v `pwd`/DSL/Liquibase/data:/liquibase/data liquibase/liquibase --defaultsFile=/liquibase/changelog/liquibase.properties --changelog-file=master.yml --url=jdbc:postgresql://users_db:5432/classifier?user=root --password=$DB_PASSWORD update \ No newline at end of file +docker run --rm --network bykstack -v `pwd`/DSL/Liquibase/changelog:/liquibase/changelog -v `pwd`/DSL/Liquibase/master.yml:/liquibase/master.yml -v `pwd`/DSL/Liquibase/data:/liquibase/data liquibase/liquibase --defaultsFile=/liquibase/changelog/liquibase.properties --changelog-file=master.yml --url=jdbc:postgresql://users_db:5432/classifier?user=postgres --password=$DB_PASSWORD update \ No newline at end of file From 4432c5d88c182a14092f74bf76751e1f2c913db8 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Thu, 11 Jul 2024 15:47:28 +0530 Subject: [PATCH 142/582] new neginx updates --- ...ifier-dev-ruuter-private.rootcode.software | 30 +++++++++++++-- .../esclassifier-dev-tim.rootcode.software | 37 +++++++++++++++++++ 2 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 rtc_nginx/esclassifier-dev-tim.rootcode.software diff --git a/rtc_nginx/esclassifier-dev-ruuter-private.rootcode.software b/rtc_nginx/esclassifier-dev-ruuter-private.rootcode.software index 0086a87c..96cba1cd 100644 --- a/rtc_nginx/esclassifier-dev-ruuter-private.rootcode.software +++ b/rtc_nginx/esclassifier-dev-ruuter-private.rootcode.software @@ -1,13 +1,37 @@ server { - listen 80; - listen [::]:80; root /var/www/esclassifier-dev-ruuter-private.rootcode.software/html; index index.html index.htm index.nginx-debian.html; server_name esclassifier-dev-ruuter-private.rootcode.software; + location / { - try_files $uri $uri/ =404; + proxy_pass http://localhost:8088; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; } + + listen [::]:443 ssl ipv6only=on; # managed by Certbot + listen 443 ssl; # managed by Certbot + ssl_certificate /etc/letsencrypt/live/esclassifier-dev-ruuter-private.rootcode.software/fullchain.pem; # managed by Certbot + ssl_certificate_key /etc/letsencrypt/live/esclassifier-dev-ruuter-private.rootcode.software/privkey.pem; # managed by Certbot + include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot + +}server { + if ($host = esclassifier-dev-ruuter-private.rootcode.software) { + return 301 https://$host$request_uri; + } # managed by Certbot + + + listen 80; + listen [::]:80; + + server_name esclassifier-dev-ruuter-private.rootcode.software; + return 404; # managed by Certbot + + } \ No newline at end of file diff --git a/rtc_nginx/esclassifier-dev-tim.rootcode.software b/rtc_nginx/esclassifier-dev-tim.rootcode.software new file mode 100644 index 00000000..00681a84 --- /dev/null +++ b/rtc_nginx/esclassifier-dev-tim.rootcode.software @@ -0,0 +1,37 @@ +server { + + root /var/www/esclassifier-dev-tim.rootcode.software/html; + index index.html index.htm index.nginx-debian.html; + + server_name esclassifier-dev-tim.rootcode.software; + + location / { + proxy_pass http://localhost:8085; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + listen [::]:443 ssl; # managed by Certbot + listen 443 ssl; # managed by Certbot + ssl_certificate /etc/letsencrypt/live/esclassifier-dev-tim.rootcode.software/fullchain.pem; # managed by Certbot + ssl_certificate_key /etc/letsencrypt/live/esclassifier-dev-tim.rootcode.software/privkey.pem; # managed by Certbot + include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot + +} +server { + if ($host = esclassifier-dev-tim.rootcode.software) { + return 301 https://$host$request_uri; + } # managed by Certbot + + + listen 80; + listen [::]:80; + + server_name esclassifier-dev-tim.rootcode.software; + return 404; # managed by Certbot + + +} \ No newline at end of file From 07d1cf149b2f6a7c8fab0d0249ed546bc077d469 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Thu, 11 Jul 2024 21:58:01 +0530 Subject: [PATCH 143/582] dockerize consent app --- .gitignore | 4 +- outlook-consent-app/Dockerfile | 53 ++++++------------- outlook-consent-app/Dockerfile.dev | 13 +++++ outlook-consent-app/docker-compose.yml | 9 ++++ outlook-consent-app/next.config.mjs | 7 +++ outlook-consent-app/src/app/api/auth/route.ts | 2 +- .../src/app/api/auth/token/route.ts | 7 ++- outlook-consent-app/src/app/callback/page.tsx | 38 ++++++------- 8 files changed, 71 insertions(+), 62 deletions(-) create mode 100644 outlook-consent-app/Dockerfile.dev create mode 100644 outlook-consent-app/docker-compose.yml diff --git a/.gitignore b/.gitignore index 81a6bac7..3525077b 100644 --- a/.gitignore +++ b/.gitignore @@ -397,5 +397,5 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml -tim_db -data \ No newline at end of file +/tim-db +/data \ No newline at end of file diff --git a/outlook-consent-app/Dockerfile b/outlook-consent-app/Dockerfile index 56186e36..6b36992a 100644 --- a/outlook-consent-app/Dockerfile +++ b/outlook-consent-app/Dockerfile @@ -1,43 +1,22 @@ -# Use the official Node.js image as a base image -FROM node:18-alpine AS builder +# Stage 1: Build the Next.js app +ARG node_version=node:lts +ARG nginx_version=nginx:1.21.3-alpine -# Set the working directory -WORKDIR /app +FROM $node_version as image +WORKDIR /usr/src/app +COPY ./package*.json ./ -# Copy package.json and package-lock.json (or yarn.lock) files -COPY package.json package-lock.json ./ - -# Install dependencies -RUN npm install - -# Copy the rest of the application code +FROM image AS build +RUN npm install --legacy-peer-deps --mode=development COPY . . +RUN npm run build +VOLUME /usr/buerokratt-classifier -# Build the Next.js application without linting and type-checking -# Skip linting by setting NEXT_SKIP_LINT during build -# Skip type-checking by using NEXT_SKIP_TYPE_CHECK during build -RUN NEXT_SKIP_LINT=true NEXT_SKIP_TYPE_CHECK=true npm run build - -# Use a smaller base image for the final stage -FROM node:18-alpine - -# Set the working directory -WORKDIR /app - -# Copy only the necessary files from the builder stage -COPY --from=builder /app/package.json /app/package-lock.json ./ -COPY --from=builder /app/.next ./.next -COPY --from=builder /app/public ./public - -# Install only production dependencies -RUN npm install --production - -# Set environment variables -ENV PORT=3003 -ENV NODE_ENV=production - -# Expose the port the app runs on +# Stage 2: Serve the app with Nginx +FROM $nginx_version +COPY ./nginx/http-nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /usr/src/app/.next /usr/share/nginx/html EXPOSE 3003 +CMD ["nginx", "-g", "daemon off;"] + -# Command to run the application -CMD ["npm", "start"] diff --git a/outlook-consent-app/Dockerfile.dev b/outlook-consent-app/Dockerfile.dev new file mode 100644 index 00000000..f54c6b2e --- /dev/null +++ b/outlook-consent-app/Dockerfile.dev @@ -0,0 +1,13 @@ +FROM node:lts AS image +WORKDIR /app +COPY ./package.json . + +FROM image AS build +ARG env=DEV +RUN npm install --legacy-peer-deps +COPY . . +RUN npm run build + +EXPOSE 3003 + +CMD ["npm", "run", "start"] diff --git a/outlook-consent-app/docker-compose.yml b/outlook-consent-app/docker-compose.yml new file mode 100644 index 00000000..ae169c7b --- /dev/null +++ b/outlook-consent-app/docker-compose.yml @@ -0,0 +1,9 @@ +version: '3.9' +services: + outlook-consent-app: + container_name: outlook-consent-app + build: + context: . + dockerfile: Dockerfile.dev + ports: + - 3003:3003 diff --git a/outlook-consent-app/next.config.mjs b/outlook-consent-app/next.config.mjs index bd71866f..c6cb64d8 100644 --- a/outlook-consent-app/next.config.mjs +++ b/outlook-consent-app/next.config.mjs @@ -3,6 +3,13 @@ const nextConfig = { reactStrictMode: true, experimental: { appDir: true, + missingSuspenseWithCSRBailout: false, + }, + eslint: { + ignoreDuringBuilds: true, + }, + typescript: { + ignoreBuildErrors: true, }, }; diff --git a/outlook-consent-app/src/app/api/auth/route.ts b/outlook-consent-app/src/app/api/auth/route.ts index 9b6d2213..637c339c 100644 --- a/outlook-consent-app/src/app/api/auth/route.ts +++ b/outlook-consent-app/src/app/api/auth/route.ts @@ -2,7 +2,7 @@ import { NextResponse } from 'next/server'; export async function GET() { const clientId = process.env.NEXT_PUBLIC_CLIENT_ID; - const redirectUri = process.env.REDIRECT_URI; + const redirectUri = process.env.REDIRECT_URI ||""; const scope = 'User.Read Mail.ReadWrite MailboxSettings.ReadWrite offline_access'; const state = '12345'; // You may want to generate a random state value for security const authUrl = `https://login.microsoftonline.com/${process.env.TENANT_ID}/oauth2/v2.0/authorize?client_id=${clientId}&response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}&response_mode=query&scope=${encodeURIComponent(scope)}&state=${state}`; diff --git a/outlook-consent-app/src/app/api/auth/token/route.ts b/outlook-consent-app/src/app/api/auth/token/route.ts index b99da4bd..a8fbbf0c 100644 --- a/outlook-consent-app/src/app/api/auth/token/route.ts +++ b/outlook-consent-app/src/app/api/auth/token/route.ts @@ -3,9 +3,9 @@ import axios from 'axios'; export async function POST(req: NextRequest) { const { code } = await req.json(); - const clientId = process.env.NEXT_PUBLIC_CLIENT_ID; - const clientSecret = process.env.CLIENT_SECRET; - const redirectUri = process.env.REDIRECT_URI; + const clientId = process.env.NEXT_PUBLIC_CLIENT_ID || "'"; + const clientSecret = process.env.CLIENT_SECRET || ""; + const redirectUri = process.env.REDIRECT_URI || ""; try { const tokenResponse = await axios.post( 'https://login.microsoftonline.com/common/oauth2/v2.0/token', @@ -26,7 +26,6 @@ export async function POST(req: NextRequest) { return NextResponse.json(tokenResponse.data); } catch (error) { - console.error('Error fetching tokens:', error); return NextResponse.json({ error: 'Error fetching tokens' }, { status: 500 }); } } diff --git a/outlook-consent-app/src/app/callback/page.tsx b/outlook-consent-app/src/app/callback/page.tsx index 7a50ccf6..ba727980 100644 --- a/outlook-consent-app/src/app/callback/page.tsx +++ b/outlook-consent-app/src/app/callback/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; +import { Suspense, useEffect, useState } from "react"; import { useSearchParams, useRouter } from "next/navigation"; import axios from "axios"; import styles from "../page.module.css"; @@ -10,7 +10,7 @@ const CallbackPage = () => { const searchParams = useSearchParams(); const router = useRouter(); const [token, setToken] = useState(""); - const [error,setError] = useState(""); + const [error, setError] = useState(""); useEffect(() => { const exchangeAuthCode = async (code: string) => { @@ -26,24 +26,26 @@ const CallbackPage = () => { if (code) { exchangeAuthCode(code); } - }, []); + }, [searchParams]); return ( -

    - {!token && !error && ( -
    -
    Retrieving the token..
    -
    - )} - - {token && ( -
    -
    Refresh Token
    - -
    - )} - -
    + Loading...}> +
    + {!token && !error && ( +
    +
    Retrieving the token..
    +
    + )} + + {token && ( +
    +
    Refresh Token
    + +
    + )} + +
    +
    ); }; From 8b8745d33f103dc1c924f905e05c466734193aca Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 12 Jul 2024 08:14:10 +0530 Subject: [PATCH 144/582] docker compose update --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index c0a40488..6e02c536 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -157,7 +157,7 @@ services: ports: - 5433:5432 volumes: - - ./data:/var/lib/postgresql/data + - /home/ubuntu/user_db_files:/var/lib/postgresql/data networks: - bykstack From b6e89dfafef6a88669416c6119c60caed2b84fa2 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 12 Jul 2024 08:37:59 +0530 Subject: [PATCH 145/582] restart docker container user_db if it fails --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index 6e02c536..b8e0de74 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -160,6 +160,7 @@ services: - /home/ubuntu/user_db_files:/var/lib/postgresql/data networks: - bykstack + restart: always networks: bykstack: From 3b0c9947ee88dfad4c94c346bcfd440de6b9a0f0 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 12 Jul 2024 08:38:33 +0530 Subject: [PATCH 146/582] run migration script if db_update is in labels --- .github/workflows/est-workflow-dev.yml | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/.github/workflows/est-workflow-dev.yml b/.github/workflows/est-workflow-dev.yml index ecf48504..fe02482a 100644 --- a/.github/workflows/est-workflow-dev.yml +++ b/.github/workflows/est-workflow-dev.yml @@ -30,8 +30,8 @@ jobs: - name: Remove all running containers, images, and prune Docker system run: | - docker stop $(docker ps -a -q) || true - docker rm $(docker ps -a -q) || true + docker stop $(docker ps -a -q | grep -v $(docker ps -q --filter "name=users_db")) || true + docker rm $(docker ps -a -q | grep -v $(docker ps -q --filter "name=users_db")) || true images_to_keep="authentication-layer|tim|data-mapper|resql|ruuter" docker images --format "{{.Repository}}:{{.Tag}}" | grep -Ev "$images_to_keep" | xargs -r docker rmi || true docker volume prune -f @@ -41,6 +41,25 @@ jobs: run: | docker compose up --build -d + - name: Check for db_update label + id: check_label + uses: actions/github-script@v6 + with: + script: | + const pr = await github.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.number + }); + const labels = pr.data.labels.map(label => label.name); + return labels.includes('db_update'); + + - name: Run migration script + if: steps.check_label.outputs.result == 'true' + run: | + sudo chmod +x migrate.sh + ./migrate.sh + - name: Run unitTesting.sh id: unittesting run: | From 1dd71d777568ee5018f9d46f312e302316945a52 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Fri, 12 Jul 2024 10:09:31 +0530 Subject: [PATCH 147/582] dataset groups uis --- GUI/src/components/Label/Label.scss | 2 + GUI/src/components/MainNavigation/index.tsx | 33 ++- .../DatasetGroupCard/DatasetGroupCard.scss | 28 ++- .../molecules/DatasetGroupCard/index.tsx | 31 ++- .../molecules/Pagination/Pagination.scss | 196 ++++++++++++++++++ .../components/molecules/Pagination/index.tsx | 88 ++++++++ .../pages/DatasetGroups/DatasetGroups.scss | 4 +- GUI/src/pages/DatasetGroups/index.tsx | 33 ++- GUI/translations/en/common.json | 4 +- 9 files changed, 376 insertions(+), 43 deletions(-) create mode 100644 GUI/src/components/molecules/Pagination/Pagination.scss create mode 100644 GUI/src/components/molecules/Pagination/index.tsx diff --git a/GUI/src/components/Label/Label.scss b/GUI/src/components/Label/Label.scss index 89db8ed5..eba04d68 100644 --- a/GUI/src/components/Label/Label.scss +++ b/GUI/src/components/Label/Label.scss @@ -13,6 +13,8 @@ background-color: get-color(white); border-radius: $veera-radius-s; position: relative; + width: fit-content; + margin-right: 5px; &--info { color: get-color(sapphire-blue-10); diff --git a/GUI/src/components/MainNavigation/index.tsx b/GUI/src/components/MainNavigation/index.tsx index ed8277af..726dd79d 100644 --- a/GUI/src/components/MainNavigation/index.tsx +++ b/GUI/src/components/MainNavigation/index.tsx @@ -3,14 +3,17 @@ import { useTranslation } from 'react-i18next'; import { NavLink, useLocation } from 'react-router-dom'; import { MdApps, + MdBackup, MdClass, MdClose, MdDashboard, MdDataset, MdKeyboardArrowDown, + MdOutlineDataset, MdOutlineForum, MdPeople, MdSettings, + MdSettingsBackupRestore, MdTextFormat, } from 'react-icons/md'; import { useQuery } from '@tanstack/react-query'; @@ -43,17 +46,19 @@ const MainNavigation: FC = () => { id: 'datasets', label: t('menu.datasets'), path: '#', - icon: , + icon: , children: [ { label: t('menu.datasetGroups'), path: 'dataset-groups', - icon: , }, { - label: t('menu.versions'), - path: 'versions', - icon: , + label: t('menu.validationSessions'), + path: 'validation-sessions', + }, + { + label: t('menu.stopWords'), + path: 'stop-words', }, ], }, @@ -63,24 +68,18 @@ const MainNavigation: FC = () => { path: '/data-models', icon: , }, - { - id: 'classes', - label: t('menu.classes'), - path: '/classes', - icon: , - }, - { - id: 'stopWords', - label: t('menu.stopWords'), - path: '/stop-words', - icon: , - }, { id: 'incomingTexts', label: t('menu.incomingTexts'), path: '/incoming-texts', icon: , }, + { + id: 'testModel', + label: t('menu.testModel'), + path: '/test-model', + icon: , + }, ]; const getUserRole = () => { diff --git a/GUI/src/components/molecules/DatasetGroupCard/DatasetGroupCard.scss b/GUI/src/components/molecules/DatasetGroupCard/DatasetGroupCard.scss index 1a140c21..901b47fc 100644 --- a/GUI/src/components/molecules/DatasetGroupCard/DatasetGroupCard.scss +++ b/GUI/src/components/molecules/DatasetGroupCard/DatasetGroupCard.scss @@ -5,7 +5,7 @@ } .switch-row { - justify-content: flex-end; + justify-content: space-between; } .status-indicators { @@ -53,7 +53,7 @@ } .label-row { - justify-content: flex-start; + justify-content: flex-end; display: flex; align-items: center; } @@ -71,3 +71,27 @@ bottom: 10px; left: 20px; } + +.colored-label{ + background-color: white; + border: solid 2px #D73E3E; + border-radius: 6px; + color: #D73E3E; + width: fit-content; + padding: 0px 5px; + font-size: 14px; + margin-right: 5px; +} + +.text{ + font-size: 16px; + margin-bottom: 5px; +} + +.py-3{ + padding: 8px 0px; +} + +.flex{ + display: flex; +} \ No newline at end of file diff --git a/GUI/src/components/molecules/DatasetGroupCard/index.tsx b/GUI/src/components/molecules/DatasetGroupCard/index.tsx index b3b66344..92fd4e5a 100644 --- a/GUI/src/components/molecules/DatasetGroupCard/index.tsx +++ b/GUI/src/components/molecules/DatasetGroupCard/index.tsx @@ -2,6 +2,8 @@ import { FC, PropsWithChildren } from 'react'; import './DatasetGroupCard.scss'; import Dataset from 'assets/Dataset'; import { Switch } from 'components/FormElements'; +import Button from 'components/Button'; +import Label from 'components/Label'; type DatasetGroupCardProps = { isEnabled?: boolean; @@ -17,28 +19,25 @@ const DatasetGroupCard: FC> = ({ return ( <>
    +
    +

    {datasetName}

    -
    -
    - -
    + +
    +

    {"Last Model Trained: Model Alpha"}

    +

    {"Last Used For Training: 8.6.24-13:01"}

    +

    {"Last Updated: 7.6.24-15:31"}

    -
    -

    {datasetName}

    +
    + + +
    +
    -
    - - {' '} - {status} - -
    +
    diff --git a/GUI/src/components/molecules/Pagination/Pagination.scss b/GUI/src/components/molecules/Pagination/Pagination.scss new file mode 100644 index 00000000..58c84ab4 --- /dev/null +++ b/GUI/src/components/molecules/Pagination/Pagination.scss @@ -0,0 +1,196 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/typography'; + +.data-table { + width: 100%; + color: get-color(black-coral-20); + text-align: left; + margin-bottom: 0; + display: table; + + &__scrollWrapper { + height: 100%; + overflow-x: auto; + white-space: nowrap; + display: block; + padding: 5px; + background-color: white; + border-radius: 10px; + border: solid 1px get-color(black-coral-1); + } + + thead, + tbody { + width: 100%; + } + + th { + padding: 12px 14.5px; + color: get-color(black-coral-12); + border-bottom: 1px solid get-color(black-coral-10); + font-weight: $veera-font-weight-beta; + vertical-align: middle; + position: relative; + } + + td { + padding: 12px 24px 12px 16px; + border-bottom: 1px solid get-color(black-coral-2); + vertical-align: middle; + max-width: fit-content; + + p { + white-space: break-spaces; + } + + .entity { + display: inline-flex; + align-items: center; + padding-left: 4px; + background-color: get-color(sapphire-blue-2); + border-radius: 4px; + + span { + display: inline-flex; + font-size: $veera-font-size-80; + background-color: get-color(white); + padding: 0 4px; + border-radius: 4px; + margin: 2px 2px 2px 4px; + } + } + } + + tbody { + tr { + &:last-child { + td { + border-bottom: 0; + } + } + } + } + + &__filter { + position: absolute; + top: 100%; + left: 0; + right: 0; + padding: get-spacing(paldiski); + background-color: get-color(white); + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.14); + border-radius: 0 0 4px 4px; + border: 1px solid get-color(black-coral-2); + + input { + width: 100%; + display: block; + appearance: none; + background-color: get-color(white); + border: 1px solid get-color(black-coral-6); + border-radius: 5px; + color: var(--color-black); + font-size: $veera-font-size-100; + height: 32px; + line-height: 24px; + padding: get-spacing(paldiski); + + &::placeholder { + color: get-color(black-coral-6); + } + + &:focus { + outline: none; + border-color: get-color(sapphire-blue-10); + } + } + } + + &__pagination-wrapper { + display: flex; + box-shadow: 0 -1px 0 get-color(black-coral-10); + padding: 6px 16px; + } + + &__pagination { + display: flex; + align-items: center; + gap: 15px; + margin: 0 auto; + + + .data-table__page-size { + margin-left: 0; + } + + .next, + .previous { + display: flex; + color: get-color(sapphire-blue-10); + + &[disabled] { + color: get-color(black-coral-11); + cursor: initial; + } + } + + .links { + display: flex; + align-items: center; + gap: 5px; + font-size: $veera-font-size-80; + color: get-color(black-coral-10); + + li { + display: block; + + a, + span { + display: flex; + align-items: center; + justify-content: center; + width: 25px; + height: 25px; + border-radius: 50%; + + &:hover { + text-decoration: none; + } + } + + &.active { + a, + span { + color: get-color(white); + background-color: get-color(sapphire-blue-10); + } + } + } + } + } + + &__page-size { + display: flex; + align-items: center; + gap: 8px; + font-size: $veera-font-size-80; + line-height: 16px; + color: get-color(black-coral-11); + margin-left: auto; + + select { + appearance: none; + font-size: $veera-font-size-70; + line-height: 16px; + height: 30px; + min-width: 50px; + padding: 6px 10px; + border: 1px solid #8f91a8; + border-radius: 2px; + background-color: get-color(white); + background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iNiIgdmlld0JveD0iMCAwIDEwIDYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNS4zMTMwNiA1LjgwODIyQzUuMTU2ODUgNS45NjQ0MyA0LjkwMzU4IDUuOTY0NDMgNC43NDczNyA1LjgwODIyTDAuMjgyNzMgMS4zNDM1OEMwLjEyNjUyIDEuMTg3MzcgMC4xMjY1MiAwLjkzNDEwMiAwLjI4MjczIDAuNzc3ODkzTDAuNzc3NzA0IDAuMjgyOTE4QzAuOTMzOTE0IDAuMTI2NzA4IDEuMTg3MTggMC4xMjY3MDggMS4zNDMzOSAwLjI4MjkxN0w1LjAzMDIyIDMuOTY5NzRMOC43MTcwNCAwLjI4MjkxN0M4Ljg3MzI1IDAuMTI2NzA4IDkuMTI2NTIgMC4xMjY3MDggOS4yODI3MyAwLjI4MjkxN0w5Ljc3NzcgMC43Nzc4OTJDOS45MzM5MSAwLjkzNDEwMiA5LjkzMzkxIDEuMTg3MzcgOS43Nzc3IDEuMzQzNThMNS4zMTMwNiA1LjgwODIyWiIgZmlsbD0iIzU1NTg2NyIvPgo8L3N2Zz4K'); + background-repeat: no-repeat; + background-position: top 11px right 10px; + } + } +} diff --git a/GUI/src/components/molecules/Pagination/index.tsx b/GUI/src/components/molecules/Pagination/index.tsx new file mode 100644 index 00000000..39b36e26 --- /dev/null +++ b/GUI/src/components/molecules/Pagination/index.tsx @@ -0,0 +1,88 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { MdOutlineWest, MdOutlineEast } from 'react-icons/md'; +import clsx from 'clsx'; +import "./Pagination.scss" +import { useTranslation } from 'react-i18next'; + +interface PaginationProps { + pageCount: number; + pageSize: number; + pageIndex: number; + canPreviousPage: boolean; + canNextPage: boolean; + onPageChange?: (pageIndex: number) => void; + onPageSizeChange?: (pageSize: number) => void; + id?: string; +} + +const Pagination: React.FC = ({ + pageCount, + pageSize, + pageIndex, + canPreviousPage, + canNextPage, + onPageChange, + onPageSizeChange, + id, +}) => { + const { t } = useTranslation(); + + return ( +
    + {(pageCount * pageSize) > pageSize && ( +
    + + + +
    + )} +
    + + +
    +
    + ); +}; + +export default Pagination; diff --git a/GUI/src/pages/DatasetGroups/DatasetGroups.scss b/GUI/src/pages/DatasetGroups/DatasetGroups.scss index 2a6dd6e8..c2aa95bc 100644 --- a/GUI/src/pages/DatasetGroups/DatasetGroups.scss +++ b/GUI/src/pages/DatasetGroups/DatasetGroups.scss @@ -16,13 +16,13 @@ border-radius: 8px; background-color: #fff; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - padding: 5px; + padding: 15px; box-sizing: border-box; } .grid-container { display: grid; - grid-template-columns: repeat(4, 1fr); + grid-template-columns: repeat(3, 1fr); gap: 16px; padding: 16px; width: 100%; diff --git a/GUI/src/pages/DatasetGroups/index.tsx b/GUI/src/pages/DatasetGroups/index.tsx index 2a8941d2..fccf33d4 100644 --- a/GUI/src/pages/DatasetGroups/index.tsx +++ b/GUI/src/pages/DatasetGroups/index.tsx @@ -3,6 +3,7 @@ import './DatasetGroups.scss'; import { useTranslation } from 'react-i18next'; import { Button, FormInput, FormSelect } from 'components'; import DatasetGroupCard from 'components/molecules/DatasetGroupCard'; +import Pagination from 'components/molecules/Pagination'; const DatasetGroups: FC = () => { const { t } = useTranslation(); @@ -31,15 +32,37 @@ const DatasetGroups: FC = () => {
    - + + { ); })}
    + +
    diff --git a/GUI/translations/en/common.json b/GUI/translations/en/common.json index f7a9a714..6f0ee734 100644 --- a/GUI/translations/en/common.json +++ b/GUI/translations/en/common.json @@ -59,9 +59,9 @@ "integration": "Integration", "datasets": "Datasets", "datasetGroups": "Dataset Groups", - "versions": "Versions", + "validationSessions": "Validation Sessions", "dataModels": "Data Models", - "classes": "Classes", + "testModel": "Test Model", "stopWords": "Stop Words", "incomingTexts": "Incoming Texts" }, From 2388743f77e5fe22925c07083d8908692af69585 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 12 Jul 2024 14:33:02 +0530 Subject: [PATCH 148/582] tim debug --- docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index b8e0de74..51609bae 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -66,6 +66,8 @@ services: - tim-postgresql environment: - SECURITY_ALLOWLIST_JWT=ruuter-private,ruuter-public,data-mapper,resql,tim,tim-postgresql,chat-widget,authentication-layer,127.0.0.1,::1 + - logging.level.org.springframework=DEBUG + - logging.level.your.package.name=DEBUG ports: - 8085:8085 networks: From 72a36a1b06a6167ed3d37fac3af555625f28e65c Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 12 Jul 2024 14:35:34 +0530 Subject: [PATCH 149/582] change revert --- docker-compose.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 51609bae..b8e0de74 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -66,8 +66,6 @@ services: - tim-postgresql environment: - SECURITY_ALLOWLIST_JWT=ruuter-private,ruuter-public,data-mapper,resql,tim,tim-postgresql,chat-widget,authentication-layer,127.0.0.1,::1 - - logging.level.org.springframework=DEBUG - - logging.level.your.package.name=DEBUG ports: - 8085:8085 networks: From 3eaa8db7fb125cec1d36b4bf73120bf6ae8c09be Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 12 Jul 2024 14:44:10 +0530 Subject: [PATCH 150/582] docker tim change --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index b8e0de74..a42f4d0f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -71,7 +71,7 @@ services: networks: - bykstack extra_hosts: - - "host.docker.internal:host-gateway" + - "localhost:host-gateway" cpus: "0.5" mem_limit: "512M" From d067c524cb7f43d3d4e8297ca9546a693d78f653 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 12 Jul 2024 15:39:01 +0530 Subject: [PATCH 151/582] changes revert and workflow udpate --- .github/workflows/est-workflow-dev.yml | 4 ++-- GUI/.env.development | 8 ++++---- docker-compose.yml | 20 +++++++++----------- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/.github/workflows/est-workflow-dev.yml b/.github/workflows/est-workflow-dev.yml index fe02482a..b3acc1e7 100644 --- a/.github/workflows/est-workflow-dev.yml +++ b/.github/workflows/est-workflow-dev.yml @@ -30,8 +30,8 @@ jobs: - name: Remove all running containers, images, and prune Docker system run: | - docker stop $(docker ps -a -q | grep -v $(docker ps -q --filter "name=users_db")) || true - docker rm $(docker ps -a -q | grep -v $(docker ps -q --filter "name=users_db")) || true + docker stop $(docker ps -a -q | grep -v -E "($(docker ps -q --filter "name=users_db")|$(docker ps -q --filter "name=tim")|$(docker ps -q --filter "name=authentication-layer")|$(docker ps -q --filter "name=tim-postgresql"))") || true + docker rm $(docker ps -a -q | grep -v -E "($(docker ps -q --filter "name=users_db")|$(docker ps -q --filter "name=tim")|$(docker ps -q --filter "name=authentication-layer")|$(docker ps -q --filter "name=tim-postgresql"))") || true images_to_keep="authentication-layer|tim|data-mapper|resql|ruuter" docker images --format "{{.Repository}}:{{.Tag}}" | grep -Ev "$images_to_keep" | xargs -r docker rmi || true docker volume prune -f diff --git a/GUI/.env.development b/GUI/.env.development index 7870d3bb..2f43b8ed 100644 --- a/GUI/.env.development +++ b/GUI/.env.development @@ -1,8 +1,8 @@ -REACT_APP_RUUTER_API_URL=http://ruuter-public:8086 -REACT_APP_RUUTER_PRIVATE_API_URL=http://ruuter-private:8088 -REACT_APP_CUSTOMER_SERVICE_LOGIN=http://authentication-layer:3004/et/dev-auth +REACT_APP_RUUTER_API_URL=http://localhost:8086 +REACT_APP_RUUTER_PRIVATE_API_URL=http://localhost:8088 +REACT_APP_CUSTOMER_SERVICE_LOGIN=http://localhost:3004/et/dev-auth REACT_APP_SERVICE_ID=conversations,settings,monitoring REACT_APP_NOTIFICATION_NODE_URL=http://localhost:4040 REACT_APP_CSP=upgrade-insecure-requests; default-src 'self'; font-src 'self' data:; img-src 'self' data:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self' http://localhost:8086 http://localhost:8088 http://localhost:8085 http://localhost:4040 http://localhost:3002/menu.json; REACT_APP_ENABLE_HIDDEN_FEATURES=TRUE -REACT_APP_MENU_JSON=[{"id":"conversations","label":{"et":"Vestlused","en":"Conversations"},"path":"/chat","children":[{"label":{"et":"Vastamata","en":"Unanswered"},"path":"/unanswered"},{"label":{"et":"Aktiivsed","en":"Active"},"path":"/active"},{"label":{"et":"Ajalugu","en":"History"},"path":"/history"}]},{"id":"training","label":{"et":"Treening","en":"Training"},"path":"/training","children":[{"label":{"et":"Treening","en":"Training"},"path":"/training","children":[{"label":{"et":"Teemad","en":"Themes"},"path":"/training/intents"},{"label":{"et":"Avalikud teemad","en":"Public themes"},"path":"/training/common-intents"},{"label":{"et":"Teemade järeltreenimine","en":"Post training themes"},"path":"/training/intents-followup-training"},{"label":{"et":"Vastused","en":"Answers"},"path":"/training/responses"},{"label":{"et":"Kasutuslood","en":"User Stories"},"path":"/training/stories"},{"label":{"et":"Konfiguratsioon","en":"Configuration"},"path":"/training/configuration"},{"label":{"et":"Vormid","en":"Forms"},"path":"/training/forms"},{"label":{"et":"Mälukohad","en":"Slots"},"path":"/training/slots"},{"label":{"et":"Automatic Teenused","en":"Automatic Services"},"path":"/auto-services"}]},{"label":{"et":"Ajaloolised vestlused","en":"Historical conversations"},"path":"/history","children":[{"label":{"et":"Ajalugu","en":"History"},"path":"/history/history"},{"label":{"et":"Pöördumised","en":"Appeals"},"path":"/history/appeal"}]},{"label":{"et":"Mudelipank ja analüütika","en":"Modelbank and analytics"},"path":"/analytics","children":[{"label":{"et":"Teemade ülevaade","en":"Overview of topics"},"path":"/analytics/overview"},{"label":{"et":"Mudelite võrdlus","en":"Comparison of models"},"path":"/analytics/models"},{"label":{"et":"Testlood","en":"testTracks"},"path":"/analytics/testcases"}]},{"label":{"et":"Treeni uus mudel","en":"Train new model"},"path":"/train-new-model"}]},{"id":"analytics","label":{"et":"Analüütika","en":"Analytics"},"path":"/analytics","children":[{"label":{"et":"Ülevaade","en":"Overview"},"path":"/overview"},{"label":{"et":"Vestlused","en":"Chats"},"path":"/chats"},{"label":{"et":"Bürokratt","en":"Burokratt"},"path":"/burokratt"},{"label":{"et":"Tagasiside","en":"Feedback"},"path":"/feedback"},{"label":{"et":"Nõustajad","en":"Advisors"},"path":"/advisors"},{"label":{"et":"Avaandmed","en":"Reports"},"path":"/reports"}]},{"id":"services","label":{"et":"Teenused","en":"Services"},"path":"/services","children":[{"label":{"et":"Ülevaade","en":"Overview"},"path":"/overview"},{"label":{"et":"Uus teenus","en":"New Service"},"path":"/newService"},{"label":{"et":"Automatic Teenused","en":"Automatic Services"},"path":"/auto-services"},{"label":{"et":"Probleemsed teenused","en":"Faulty Services"},"path":"/faultyServices"}]},{"id":"settings","label":{"et":"Haldus","en":"Administration"},"path":"/settings","children":[{"label":{"et":"Kasutajad","en":"Users"},"path":"/users"},{"label":{"et":"Vestlusbot","en":"Chatbot"},"path":"/chatbot","children":[{"label":{"et":"Seaded","en":"Settings"},"path":"/chatbot/settings"},{"label":{"et":"Tervitussõnum","en":"Welcome message"},"path":"/chatbot/welcome-message"},{"label":{"et":"Välimus ja käitumine","en":"Appearance and behavior"},"path":"/chatbot/appearance"},{"label":{"et":"Erakorralised teated","en":"Emergency notices"},"path":"/chatbot/emergency-notices"}]},{"label":{"et":"Asutuse tööaeg","en":"Office opening hours"},"path":"/working-time"},{"label":{"et":"Sessiooni pikkus","en":"Session length"},"path":"/session-length"}]},{"id":"monitoring","label":{"et":"Seire","en":"Monitoring"},"path":"/monitoring","children":[{"label":{"et":"Aktiivaeg","en":"Working hours"},"path":"/uptime"}]}] +REACT_APP_MENU_JSON=[{"id":"conversations","label":{"et":"Vestlused","en":"Conversations"},"path":"/chat","children":[{"label":{"et":"Vastamata","en":"Unanswered"},"path":"/unanswered"},{"label":{"et":"Aktiivsed","en":"Active"},"path":"/active"},{"label":{"et":"Ajalugu","en":"History"},"path":"/history"}]},{"id":"training","label":{"et":"Treening","en":"Training"},"path":"/training","children":[{"label":{"et":"Treening","en":"Training"},"path":"/training","children":[{"label":{"et":"Teemad","en":"Themes"},"path":"/training/intents"},{"label":{"et":"Avalikud teemad","en":"Public themes"},"path":"/training/common-intents"},{"label":{"et":"Teemade järeltreenimine","en":"Post training themes"},"path":"/training/intents-followup-training"},{"label":{"et":"Vastused","en":"Answers"},"path":"/training/responses"},{"label":{"et":"Kasutuslood","en":"User Stories"},"path":"/training/stories"},{"label":{"et":"Konfiguratsioon","en":"Configuration"},"path":"/training/configuration"},{"label":{"et":"Vormid","en":"Forms"},"path":"/training/forms"},{"label":{"et":"Mälukohad","en":"Slots"},"path":"/training/slots"},{"label":{"et":"Automatic Teenused","en":"Automatic Services"},"path":"/auto-services"}]},{"label":{"et":"Ajaloolised vestlused","en":"Historical conversations"},"path":"/history","children":[{"label":{"et":"Ajalugu","en":"History"},"path":"/history/history"},{"label":{"et":"Pöördumised","en":"Appeals"},"path":"/history/appeal"}]},{"label":{"et":"Mudelipank ja analüütika","en":"Modelbank and analytics"},"path":"/analytics","children":[{"label":{"et":"Teemade ülevaade","en":"Overview of topics"},"path":"/analytics/overview"},{"label":{"et":"Mudelite võrdlus","en":"Comparison of models"},"path":"/analytics/models"},{"label":{"et":"Testlood","en":"testTracks"},"path":"/analytics/testcases"}]},{"label":{"et":"Treeni uus mudel","en":"Train new model"},"path":"/train-new-model"}]},{"id":"analytics","label":{"et":"Analüütika","en":"Analytics"},"path":"/analytics","children":[{"label":{"et":"Ülevaade","en":"Overview"},"path":"/overview"},{"label":{"et":"Vestlused","en":"Chats"},"path":"/chats"},{"label":{"et":"Bürokratt","en":"Burokratt"},"path":"/burokratt"},{"label":{"et":"Tagasiside","en":"Feedback"},"path":"/feedback"},{"label":{"et":"Nõustajad","en":"Advisors"},"path":"/advisors"},{"label":{"et":"Avaandmed","en":"Reports"},"path":"/reports"}]},{"id":"services","label":{"et":"Teenused","en":"Services"},"path":"/services","children":[{"label":{"et":"Ülevaade","en":"Overview"},"path":"/overview"},{"label":{"et":"Uus teenus","en":"New Service"},"path":"/newService"},{"label":{"et":"Automatic Teenused","en":"Automatic Services"},"path":"/auto-services"},{"label":{"et":"Probleemsed teenused","en":"Faulty Services"},"path":"/faultyServices"}]},{"id":"settings","label":{"et":"Haldus","en":"Administration"},"path":"/settings","children":[{"label":{"et":"Kasutajad","en":"Users"},"path":"/users"},{"label":{"et":"Vestlusbot","en":"Chatbot"},"path":"/chatbot","children":[{"label":{"et":"Seaded","en":"Settings"},"path":"/chatbot/settings"},{"label":{"et":"Tervitussõnum","en":"Welcome message"},"path":"/chatbot/welcome-message"},{"label":{"et":"Välimus ja käitumine","en":"Appearance and behavior"},"path":"/chatbot/appearance"},{"label":{"et":"Erakorralised teated","en":"Emergency notices"},"path":"/chatbot/emergency-notices"}]},{"label":{"et":"Asutuse tööaeg","en":"Office opening hours"},"path":"/working-time"},{"label":{"et":"Sessiooni pikkus","en":"Session length"},"path":"/session-length"}]},{"id":"monitoring","label":{"et":"Seire","en":"Monitoring"},"path":"/monitoring","children":[{"label":{"et":"Aktiivaeg","en":"Working hours"},"path":"/uptime"}]}] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index a42f4d0f..46e940a4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ services: container_name: ruuter-public image: ruuter environment: - - application.cors.allowedOrigins=https://esclassifier-dev.rootcode.software/,https://esclassifier-dev-ruuter.rootcode.software,https://login.esclassifier-dev.rootcode.software + - application.cors.allowedOrigins=http://localhost:8086,http://localhost:3001,http://localhost:3003,http://localhost:3004,http://localhost:8080 - application.httpCodesAllowList=200,201,202,400,401,403,500 - application.internalRequests.allowedIPs=127.0.0.1 - application.logging.displayRequestContent=true @@ -18,14 +18,12 @@ services: - bykstack cpus: "0.5" mem_limit: "512M" -# - application.cors.allowedOrigins=http://localhost:8086,http://gui:3001,http://localhost:3003,http://localhost:3004,http://localhost:8080 -# - application.cors.allowedOrigins=http://localhost:3001,http://localhost:8088,http://localhost:3002,http://localhost:3004 ruuter-private: container_name: ruuter-private image: ruuter environment: - - application.cors.allowedOrigins=https://esclassifier-dev.rootcode.software/,https://esclassifier-dev-ruuter.rootcode.software,https://login.esclassifier-dev.rootcode.software + - application.cors.allowedOrigins=http://localhost:3001,http://localhost:8088,http://localhost:3002,http://localhost:3004 - application.httpCodesAllowList=200,201,202,204,400,401,403,404,500 - application.internalRequests.allowedIPs=127.0.0.1 - application.logging.displayRequestContent=true @@ -71,7 +69,7 @@ services: networks: - bykstack extra_hosts: - - "localhost:host-gateway" + - "host.docker.internal:host-gateway" cpus: "0.5" mem_limit: "512M" @@ -94,12 +92,12 @@ services: container_name: gui environment: - NODE_ENV=development - - BASE_URL=https://esclassifier-dev.rootcode.software - - REACT_APP_RUUTER_API_URL=https://esclassifier-dev-ruuter.rootcode.software - - REACT_APP_RUUTER_PRIVATE_API_URL=https://esclassifier-dev-ruuter.rootcode.software - - REACT_APP_CUSTOMER_SERVICE_LOGIN=https://login.esclassifier-dev.rootcode.software - # - REACT_APP_NOTIFICATION_NODE_URL=http://gui:4040 - - REACT_APP_CSP=upgrade-insecure-requests; default-src 'self'; font-src 'self' data:; img-src 'self' data:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self' https://esclassifier-dev-ruuter.rootcode.software https://esclassifier-dev-ruuter.rootcode.software; + - BASE_URL=http://localhost:8080 + - REACT_APP_RUUTER_API_URL=http://localhost:8086 + - REACT_APP_RUUTER_PRIVATE_API_URL=http://localhost:8088 + - REACT_APP_CUSTOMER_SERVICE_LOGIN=http://localhost:3004/et/dev-auth + - REACT_APP_NOTIFICATION_NODE_URL=http://localhost:4040 + - REACT_APP_CSP=upgrade-insecure-requests; default-src 'self'; font-src 'self' data:; img-src 'self' data:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self' http://localhost:8086 http://localhost:8088 http://localhost:8085 http://localhost:4040 https://admin.dev.buerokratt.ee/chat/menu.json; - DEBUG_ENABLED=true - CHOKIDAR_USEPOLLING=true - PORT=3001 From 03e4118d8065019aa9d7fa2a3833c6435831b5bd Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 12 Jul 2024 15:46:57 +0530 Subject: [PATCH 152/582] constants.ini update --- .github/workflows/est-workflow-dev.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/est-workflow-dev.yml b/.github/workflows/est-workflow-dev.yml index b3acc1e7..d7b4a078 100644 --- a/.github/workflows/est-workflow-dev.yml +++ b/.github/workflows/est-workflow-dev.yml @@ -37,6 +37,10 @@ jobs: docker volume prune -f docker network prune -f + - name: Update constants.ini with GitHub secret + run: | + sed -i 's/DB_PASSWORD=value/DB_PASSWORD=${{ secrets.password }}/' constants.ini + - name: Build and run Docker Compose run: | docker compose up --build -d From 558fb8860b5b1c5af1d500a3c723cd551d72a700 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Fri, 12 Jul 2024 15:47:33 +0530 Subject: [PATCH 153/582] dev deployment configs --- GUI/.env.development | 8 +++++--- docker-compose.yml | 10 +++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/GUI/.env.development b/GUI/.env.development index 13f489e1..a8d9f676 100644 --- a/GUI/.env.development +++ b/GUI/.env.development @@ -1,8 +1,10 @@ -REACT_APP_RUUTER_API_URL=http://localhost:8086 -REACT_APP_RUUTER_PRIVATE_API_URL=http://localhost:8088 -REACT_APP_CUSTOMER_SERVICE_LOGIN=http://localhost:3004/et/dev-auth +REACT_APP_RUUTER_API_URL=https://esclassifier-dev-ruuter.rootcode.software +REACT_APP_RUUTER_PRIVATE_API_URL=https://esclassifier-dev-ruuter-private.rootcode.software +REACT_APP_CUSTOMER_SERVICE_LOGIN=https://login.esclassifier-dev.rootcode.software REACT_APP_SERVICE_ID=conversations,settings,monitoring REACT_APP_NOTIFICATION_NODE_URL=http://localhost:4040 REACT_APP_CSP=upgrade-insecure-requests; default-src 'self'; font-src 'self' data:; img-src 'self' data:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self' http://localhost:8086 http://localhost:8088 http://localhost:8085 http://localhost:4040 http://localhost:3002/menu.json; REACT_APP_ENABLE_HIDDEN_FEATURES=TRUE REACT_APP_MENU_JSON=[{"id":"conversations","label":{"et":"Vestlused","en":"Conversations"},"path":"/chat","children":[{"label":{"et":"Vastamata","en":"Unanswered"},"path":"/unanswered"},{"label":{"et":"Aktiivsed","en":"Active"},"path":"/active"},{"label":{"et":"Ajalugu","en":"History"},"path":"/history"}]},{"id":"training","label":{"et":"Treening","en":"Training"},"path":"/training","children":[{"label":{"et":"Treening","en":"Training"},"path":"/training","children":[{"label":{"et":"Teemad","en":"Themes"},"path":"/training/intents"},{"label":{"et":"Avalikud teemad","en":"Public themes"},"path":"/training/common-intents"},{"label":{"et":"Teemade järeltreenimine","en":"Post training themes"},"path":"/training/intents-followup-training"},{"label":{"et":"Vastused","en":"Answers"},"path":"/training/responses"},{"label":{"et":"Kasutuslood","en":"User Stories"},"path":"/training/stories"},{"label":{"et":"Konfiguratsioon","en":"Configuration"},"path":"/training/configuration"},{"label":{"et":"Vormid","en":"Forms"},"path":"/training/forms"},{"label":{"et":"Mälukohad","en":"Slots"},"path":"/training/slots"},{"label":{"et":"Automatic Teenused","en":"Automatic Services"},"path":"/auto-services"}]},{"label":{"et":"Ajaloolised vestlused","en":"Historical conversations"},"path":"/history","children":[{"label":{"et":"Ajalugu","en":"History"},"path":"/history/history"},{"label":{"et":"Pöördumised","en":"Appeals"},"path":"/history/appeal"}]},{"label":{"et":"Mudelipank ja analüütika","en":"Modelbank and analytics"},"path":"/analytics","children":[{"label":{"et":"Teemade ülevaade","en":"Overview of topics"},"path":"/analytics/overview"},{"label":{"et":"Mudelite võrdlus","en":"Comparison of models"},"path":"/analytics/models"},{"label":{"et":"Testlood","en":"testTracks"},"path":"/analytics/testcases"}]},{"label":{"et":"Treeni uus mudel","en":"Train new model"},"path":"/train-new-model"}]},{"id":"analytics","label":{"et":"Analüütika","en":"Analytics"},"path":"/analytics","children":[{"label":{"et":"Ülevaade","en":"Overview"},"path":"/overview"},{"label":{"et":"Vestlused","en":"Chats"},"path":"/chats"},{"label":{"et":"Bürokratt","en":"Burokratt"},"path":"/burokratt"},{"label":{"et":"Tagasiside","en":"Feedback"},"path":"/feedback"},{"label":{"et":"Nõustajad","en":"Advisors"},"path":"/advisors"},{"label":{"et":"Avaandmed","en":"Reports"},"path":"/reports"}]},{"id":"services","label":{"et":"Teenused","en":"Services"},"path":"/services","children":[{"label":{"et":"Ülevaade","en":"Overview"},"path":"/overview"},{"label":{"et":"Uus teenus","en":"New Service"},"path":"/newService"},{"label":{"et":"Automatic Teenused","en":"Automatic Services"},"path":"/auto-services"},{"label":{"et":"Probleemsed teenused","en":"Faulty Services"},"path":"/faultyServices"}]},{"id":"settings","label":{"et":"Haldus","en":"Administration"},"path":"/settings","children":[{"label":{"et":"Kasutajad","en":"Users"},"path":"/users"},{"label":{"et":"Vestlusbot","en":"Chatbot"},"path":"/chatbot","children":[{"label":{"et":"Seaded","en":"Settings"},"path":"/chatbot/settings"},{"label":{"et":"Tervitussõnum","en":"Welcome message"},"path":"/chatbot/welcome-message"},{"label":{"et":"Välimus ja käitumine","en":"Appearance and behavior"},"path":"/chatbot/appearance"},{"label":{"et":"Erakorralised teated","en":"Emergency notices"},"path":"/chatbot/emergency-notices"}]},{"label":{"et":"Asutuse tööaeg","en":"Office opening hours"},"path":"/working-time"},{"label":{"et":"Sessiooni pikkus","en":"Session length"},"path":"/session-length"}]},{"id":"monitoring","label":{"et":"Seire","en":"Monitoring"},"path":"/monitoring","children":[{"label":{"et":"Aktiivaeg","en":"Working hours"},"path":"/uptime"}]}] + + diff --git a/docker-compose.yml b/docker-compose.yml index 05fdcdce..c3404474 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -92,11 +92,11 @@ services: container_name: gui environment: - NODE_ENV=development - - BASE_URL=http://localhost:8080 - - REACT_APP_RUUTER_API_URL=http://localhost:8086 - - REACT_APP_RUUTER_PRIVATE_API_URL=http://localhost:8088 - - REACT_APP_CUSTOMER_SERVICE_LOGIN=http://localhost:3004/et/dev-auth - - REACT_APP_NOTIFICATION_NODE_URL=http://localhost:4040 + # - BASE_URL=http://localhost:8080 + - REACT_APP_RUUTER_API_URL=https://esclassifier-dev-ruuter.rootcode.software + - REACT_APP_RUUTER_PRIVATE_API_URL=https://esclassifier-dev-ruuter-private.rootcode.software + - REACT_APP_CUSTOMER_SERVICE_LOGIN=https://login.esclassifier-dev.rootcode.software + # - REACT_APP_NOTIFICATION_NODE_URL=http://localhost:4040 - REACT_APP_CSP=upgrade-insecure-requests; default-src 'self'; font-src 'self' data:; img-src 'self' data:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self' http://localhost:8086 http://localhost:8088 http://localhost:8085 http://localhost:4040 https://admin.dev.buerokratt.ee/chat/menu.json; - DEBUG_ENABLED=true - CHOKIDAR_USEPOLLING=true From d41bfa5f29d2c98f839038a5339babddbb780d17 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 12 Jul 2024 15:56:35 +0530 Subject: [PATCH 154/582] conflict fix --- GUI/.env.development | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/GUI/.env.development b/GUI/.env.development index 2f43b8ed..dd2bccb7 100644 --- a/GUI/.env.development +++ b/GUI/.env.development @@ -5,4 +5,5 @@ REACT_APP_SERVICE_ID=conversations,settings,monitoring REACT_APP_NOTIFICATION_NODE_URL=http://localhost:4040 REACT_APP_CSP=upgrade-insecure-requests; default-src 'self'; font-src 'self' data:; img-src 'self' data:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self' http://localhost:8086 http://localhost:8088 http://localhost:8085 http://localhost:4040 http://localhost:3002/menu.json; REACT_APP_ENABLE_HIDDEN_FEATURES=TRUE -REACT_APP_MENU_JSON=[{"id":"conversations","label":{"et":"Vestlused","en":"Conversations"},"path":"/chat","children":[{"label":{"et":"Vastamata","en":"Unanswered"},"path":"/unanswered"},{"label":{"et":"Aktiivsed","en":"Active"},"path":"/active"},{"label":{"et":"Ajalugu","en":"History"},"path":"/history"}]},{"id":"training","label":{"et":"Treening","en":"Training"},"path":"/training","children":[{"label":{"et":"Treening","en":"Training"},"path":"/training","children":[{"label":{"et":"Teemad","en":"Themes"},"path":"/training/intents"},{"label":{"et":"Avalikud teemad","en":"Public themes"},"path":"/training/common-intents"},{"label":{"et":"Teemade järeltreenimine","en":"Post training themes"},"path":"/training/intents-followup-training"},{"label":{"et":"Vastused","en":"Answers"},"path":"/training/responses"},{"label":{"et":"Kasutuslood","en":"User Stories"},"path":"/training/stories"},{"label":{"et":"Konfiguratsioon","en":"Configuration"},"path":"/training/configuration"},{"label":{"et":"Vormid","en":"Forms"},"path":"/training/forms"},{"label":{"et":"Mälukohad","en":"Slots"},"path":"/training/slots"},{"label":{"et":"Automatic Teenused","en":"Automatic Services"},"path":"/auto-services"}]},{"label":{"et":"Ajaloolised vestlused","en":"Historical conversations"},"path":"/history","children":[{"label":{"et":"Ajalugu","en":"History"},"path":"/history/history"},{"label":{"et":"Pöördumised","en":"Appeals"},"path":"/history/appeal"}]},{"label":{"et":"Mudelipank ja analüütika","en":"Modelbank and analytics"},"path":"/analytics","children":[{"label":{"et":"Teemade ülevaade","en":"Overview of topics"},"path":"/analytics/overview"},{"label":{"et":"Mudelite võrdlus","en":"Comparison of models"},"path":"/analytics/models"},{"label":{"et":"Testlood","en":"testTracks"},"path":"/analytics/testcases"}]},{"label":{"et":"Treeni uus mudel","en":"Train new model"},"path":"/train-new-model"}]},{"id":"analytics","label":{"et":"Analüütika","en":"Analytics"},"path":"/analytics","children":[{"label":{"et":"Ülevaade","en":"Overview"},"path":"/overview"},{"label":{"et":"Vestlused","en":"Chats"},"path":"/chats"},{"label":{"et":"Bürokratt","en":"Burokratt"},"path":"/burokratt"},{"label":{"et":"Tagasiside","en":"Feedback"},"path":"/feedback"},{"label":{"et":"Nõustajad","en":"Advisors"},"path":"/advisors"},{"label":{"et":"Avaandmed","en":"Reports"},"path":"/reports"}]},{"id":"services","label":{"et":"Teenused","en":"Services"},"path":"/services","children":[{"label":{"et":"Ülevaade","en":"Overview"},"path":"/overview"},{"label":{"et":"Uus teenus","en":"New Service"},"path":"/newService"},{"label":{"et":"Automatic Teenused","en":"Automatic Services"},"path":"/auto-services"},{"label":{"et":"Probleemsed teenused","en":"Faulty Services"},"path":"/faultyServices"}]},{"id":"settings","label":{"et":"Haldus","en":"Administration"},"path":"/settings","children":[{"label":{"et":"Kasutajad","en":"Users"},"path":"/users"},{"label":{"et":"Vestlusbot","en":"Chatbot"},"path":"/chatbot","children":[{"label":{"et":"Seaded","en":"Settings"},"path":"/chatbot/settings"},{"label":{"et":"Tervitussõnum","en":"Welcome message"},"path":"/chatbot/welcome-message"},{"label":{"et":"Välimus ja käitumine","en":"Appearance and behavior"},"path":"/chatbot/appearance"},{"label":{"et":"Erakorralised teated","en":"Emergency notices"},"path":"/chatbot/emergency-notices"}]},{"label":{"et":"Asutuse tööaeg","en":"Office opening hours"},"path":"/working-time"},{"label":{"et":"Sessiooni pikkus","en":"Session length"},"path":"/session-length"}]},{"id":"monitoring","label":{"et":"Seire","en":"Monitoring"},"path":"/monitoring","children":[{"label":{"et":"Aktiivaeg","en":"Working hours"},"path":"/uptime"}]}] \ No newline at end of file +REACT_APP_MENU_JSON=[{"id":"conversations","label":{"et":"Vestlused","en":"Conversations"},"path":"/chat","children":[{"label":{"et":"Vastamata","en":"Unanswered"},"path":"/unanswered"},{"label":{"et":"Aktiivsed","en":"Active"},"path":"/active"},{"label":{"et":"Ajalugu","en":"History"},"path":"/history"}]},{"id":"training","label":{"et":"Treening","en":"Training"},"path":"/training","children":[{"label":{"et":"Treening","en":"Training"},"path":"/training","children":[{"label":{"et":"Teemad","en":"Themes"},"path":"/training/intents"},{"label":{"et":"Avalikud teemad","en":"Public themes"},"path":"/training/common-intents"},{"label":{"et":"Teemade järeltreenimine","en":"Post training themes"},"path":"/training/intents-followup-training"},{"label":{"et":"Vastused","en":"Answers"},"path":"/training/responses"},{"label":{"et":"Kasutuslood","en":"User Stories"},"path":"/training/stories"},{"label":{"et":"Konfiguratsioon","en":"Configuration"},"path":"/training/configuration"},{"label":{"et":"Vormid","en":"Forms"},"path":"/training/forms"},{"label":{"et":"Mälukohad","en":"Slots"},"path":"/training/slots"},{"label":{"et":"Automatic Teenused","en":"Automatic Services"},"path":"/auto-services"}]},{"label":{"et":"Ajaloolised vestlused","en":"Historical conversations"},"path":"/history","children":[{"label":{"et":"Ajalugu","en":"History"},"path":"/history/history"},{"label":{"et":"Pöördumised","en":"Appeals"},"path":"/history/appeal"}]},{"label":{"et":"Mudelipank ja analüütika","en":"Modelbank and analytics"},"path":"/analytics","children":[{"label":{"et":"Teemade ülevaade","en":"Overview of topics"},"path":"/analytics/overview"},{"label":{"et":"Mudelite võrdlus","en":"Comparison of models"},"path":"/analytics/models"},{"label":{"et":"Testlood","en":"testTracks"},"path":"/analytics/testcases"}]},{"label":{"et":"Treeni uus mudel","en":"Train new model"},"path":"/train-new-model"}]},{"id":"analytics","label":{"et":"Analüütika","en":"Analytics"},"path":"/analytics","children":[{"label":{"et":"Ülevaade","en":"Overview"},"path":"/overview"},{"label":{"et":"Vestlused","en":"Chats"},"path":"/chats"},{"label":{"et":"Bürokratt","en":"Burokratt"},"path":"/burokratt"},{"label":{"et":"Tagasiside","en":"Feedback"},"path":"/feedback"},{"label":{"et":"Nõustajad","en":"Advisors"},"path":"/advisors"},{"label":{"et":"Avaandmed","en":"Reports"},"path":"/reports"}]},{"id":"services","label":{"et":"Teenused","en":"Services"},"path":"/services","children":[{"label":{"et":"Ülevaade","en":"Overview"},"path":"/overview"},{"label":{"et":"Uus teenus","en":"New Service"},"path":"/newService"},{"label":{"et":"Automatic Teenused","en":"Automatic Services"},"path":"/auto-services"},{"label":{"et":"Probleemsed teenused","en":"Faulty Services"},"path":"/faultyServices"}]},{"id":"settings","label":{"et":"Haldus","en":"Administration"},"path":"/settings","children":[{"label":{"et":"Kasutajad","en":"Users"},"path":"/users"},{"label":{"et":"Vestlusbot","en":"Chatbot"},"path":"/chatbot","children":[{"label":{"et":"Seaded","en":"Settings"},"path":"/chatbot/settings"},{"label":{"et":"Tervitussõnum","en":"Welcome message"},"path":"/chatbot/welcome-message"},{"label":{"et":"Välimus ja käitumine","en":"Appearance and behavior"},"path":"/chatbot/appearance"},{"label":{"et":"Erakorralised teated","en":"Emergency notices"},"path":"/chatbot/emergency-notices"}]},{"label":{"et":"Asutuse tööaeg","en":"Office opening hours"},"path":"/working-time"},{"label":{"et":"Sessiooni pikkus","en":"Session length"},"path":"/session-length"}]},{"id":"monitoring","label":{"et":"Seire","en":"Monitoring"},"path":"/monitoring","children":[{"label":{"et":"Aktiivaeg","en":"Working hours"},"path":"/uptime"}]}] + From 682701d1b72610fd7f4504ff2a01876aa268f36f Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 12 Jul 2024 15:57:19 +0530 Subject: [PATCH 155/582] fix --- GUI/.env.development | 1 + 1 file changed, 1 insertion(+) diff --git a/GUI/.env.development b/GUI/.env.development index dd2bccb7..f9b962c5 100644 --- a/GUI/.env.development +++ b/GUI/.env.development @@ -7,3 +7,4 @@ REACT_APP_CSP=upgrade-insecure-requests; default-src 'self'; font-src 'self' dat REACT_APP_ENABLE_HIDDEN_FEATURES=TRUE REACT_APP_MENU_JSON=[{"id":"conversations","label":{"et":"Vestlused","en":"Conversations"},"path":"/chat","children":[{"label":{"et":"Vastamata","en":"Unanswered"},"path":"/unanswered"},{"label":{"et":"Aktiivsed","en":"Active"},"path":"/active"},{"label":{"et":"Ajalugu","en":"History"},"path":"/history"}]},{"id":"training","label":{"et":"Treening","en":"Training"},"path":"/training","children":[{"label":{"et":"Treening","en":"Training"},"path":"/training","children":[{"label":{"et":"Teemad","en":"Themes"},"path":"/training/intents"},{"label":{"et":"Avalikud teemad","en":"Public themes"},"path":"/training/common-intents"},{"label":{"et":"Teemade järeltreenimine","en":"Post training themes"},"path":"/training/intents-followup-training"},{"label":{"et":"Vastused","en":"Answers"},"path":"/training/responses"},{"label":{"et":"Kasutuslood","en":"User Stories"},"path":"/training/stories"},{"label":{"et":"Konfiguratsioon","en":"Configuration"},"path":"/training/configuration"},{"label":{"et":"Vormid","en":"Forms"},"path":"/training/forms"},{"label":{"et":"Mälukohad","en":"Slots"},"path":"/training/slots"},{"label":{"et":"Automatic Teenused","en":"Automatic Services"},"path":"/auto-services"}]},{"label":{"et":"Ajaloolised vestlused","en":"Historical conversations"},"path":"/history","children":[{"label":{"et":"Ajalugu","en":"History"},"path":"/history/history"},{"label":{"et":"Pöördumised","en":"Appeals"},"path":"/history/appeal"}]},{"label":{"et":"Mudelipank ja analüütika","en":"Modelbank and analytics"},"path":"/analytics","children":[{"label":{"et":"Teemade ülevaade","en":"Overview of topics"},"path":"/analytics/overview"},{"label":{"et":"Mudelite võrdlus","en":"Comparison of models"},"path":"/analytics/models"},{"label":{"et":"Testlood","en":"testTracks"},"path":"/analytics/testcases"}]},{"label":{"et":"Treeni uus mudel","en":"Train new model"},"path":"/train-new-model"}]},{"id":"analytics","label":{"et":"Analüütika","en":"Analytics"},"path":"/analytics","children":[{"label":{"et":"Ülevaade","en":"Overview"},"path":"/overview"},{"label":{"et":"Vestlused","en":"Chats"},"path":"/chats"},{"label":{"et":"Bürokratt","en":"Burokratt"},"path":"/burokratt"},{"label":{"et":"Tagasiside","en":"Feedback"},"path":"/feedback"},{"label":{"et":"Nõustajad","en":"Advisors"},"path":"/advisors"},{"label":{"et":"Avaandmed","en":"Reports"},"path":"/reports"}]},{"id":"services","label":{"et":"Teenused","en":"Services"},"path":"/services","children":[{"label":{"et":"Ülevaade","en":"Overview"},"path":"/overview"},{"label":{"et":"Uus teenus","en":"New Service"},"path":"/newService"},{"label":{"et":"Automatic Teenused","en":"Automatic Services"},"path":"/auto-services"},{"label":{"et":"Probleemsed teenused","en":"Faulty Services"},"path":"/faultyServices"}]},{"id":"settings","label":{"et":"Haldus","en":"Administration"},"path":"/settings","children":[{"label":{"et":"Kasutajad","en":"Users"},"path":"/users"},{"label":{"et":"Vestlusbot","en":"Chatbot"},"path":"/chatbot","children":[{"label":{"et":"Seaded","en":"Settings"},"path":"/chatbot/settings"},{"label":{"et":"Tervitussõnum","en":"Welcome message"},"path":"/chatbot/welcome-message"},{"label":{"et":"Välimus ja käitumine","en":"Appearance and behavior"},"path":"/chatbot/appearance"},{"label":{"et":"Erakorralised teated","en":"Emergency notices"},"path":"/chatbot/emergency-notices"}]},{"label":{"et":"Asutuse tööaeg","en":"Office opening hours"},"path":"/working-time"},{"label":{"et":"Sessiooni pikkus","en":"Session length"},"path":"/session-length"}]},{"id":"monitoring","label":{"et":"Seire","en":"Monitoring"},"path":"/monitoring","children":[{"label":{"et":"Aktiivaeg","en":"Working hours"},"path":"/uptime"}]}] + From 95fc1387b281204869fd4c0357f837bef2d0506e Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 12 Jul 2024 16:00:32 +0530 Subject: [PATCH 156/582] dummy commit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dacde394..dcbd9273 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # Introduction -A template repository. +A template repository.. From def56b3b51785a564b77296bda45526417ef3509 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 12 Jul 2024 16:02:17 +0530 Subject: [PATCH 157/582] remove db update check --- .github/workflows/est-workflow-dev.yml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/est-workflow-dev.yml b/.github/workflows/est-workflow-dev.yml index d7b4a078..3927df5f 100644 --- a/.github/workflows/est-workflow-dev.yml +++ b/.github/workflows/est-workflow-dev.yml @@ -45,18 +45,18 @@ jobs: run: | docker compose up --build -d - - name: Check for db_update label - id: check_label - uses: actions/github-script@v6 - with: - script: | - const pr = await github.pulls.get({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: context.payload.number - }); - const labels = pr.data.labels.map(label => label.name); - return labels.includes('db_update'); + # - name: Check for db_update label + # id: check_label + # uses: actions/github-script@v6 + # with: + # script: | + # const pr = await github.pulls.get({ + # owner: context.repo.owner, + # repo: context.repo.repo, + # pull_number: context.payload.number + # }); + # const labels = pr.data.labels.map(label => label.name); + # return labels.includes('db_update'); - name: Run migration script if: steps.check_label.outputs.result == 'true' From 79e4a32c290dd502911abf6467ab0caf71acdaa0 Mon Sep 17 00:00:00 2001 From: Thirunayan22 Date: Fri, 12 Jul 2024 23:41:13 +0530 Subject: [PATCH 158/582] added sonarcloud properties and github actions file --- .github/workflows/sonarcloud.yml | 23 +++++++++++++++++++++++ sonar-project.properties | 13 +++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 .github/workflows/sonarcloud.yml create mode 100644 sonar-project.properties diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml new file mode 100644 index 00000000..3318946b --- /dev/null +++ b/.github/workflows/sonarcloud.yml @@ -0,0 +1,23 @@ +name: SonarCloud Analysis + +on: + push: + branches: + - dev + pull_request: + branches: + - dev + +jobs: + sonarcloud: + name: SonarCloud + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + - name: SonarCloud Scan + uses: SonarSource/sonarcloud-github-action@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} \ No newline at end of file diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 00000000..0e993790 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,13 @@ +sonar.projectKey=rootcodelabs_classifier +sonar.organization=rootcode + +# This is the name and version displayed in the SonarCloud UI. +#sonar.projectName=classifier +#sonar.projectVersion=1.0 + + +# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. +#sonar.sources=. + +# Encoding of the source code. Default is default system encoding +#sonar.sourceEncoding=UTF-8 \ No newline at end of file From 6f974a7509c2121899bc58b4672be165084f6c81 Mon Sep 17 00:00:00 2001 From: Thirunayan22 Date: Sat, 13 Jul 2024 12:16:47 +0530 Subject: [PATCH 159/582] added sonarcloud configuration for staging and main --- .github/workflows/sonarcloud.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 3318946b..cc62850b 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -4,9 +4,13 @@ on: push: branches: - dev + - staging + - main pull_request: branches: - dev + - staging + - main jobs: sonarcloud: From a142fd5063d24afd49e1d518c2a3f9415c2ce785 Mon Sep 17 00:00:00 2001 From: Thirunayan22 Date: Sat, 13 Jul 2024 12:29:24 +0530 Subject: [PATCH 160/582] added test changes to sonar cloud --- .github/workflows/sonarcloud.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index cc62850b..b08fbdee 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -1,4 +1,4 @@ -name: SonarCloud Analysis +name: SonarCloud Code Quality Analysis on: push: @@ -24,4 +24,4 @@ jobs: uses: SonarSource/sonarcloud-github-action@master env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} \ No newline at end of file + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} # Needed to authenticate with SonarCloud \ No newline at end of file From 2c93a99b46241c5cd91bf8dbcc829d22870cdf3b Mon Sep 17 00:00:00 2001 From: Thirunayan22 Date: Sat, 13 Jul 2024 12:38:15 +0530 Subject: [PATCH 161/582] corrected stage branch name from 'staging' --- .github/workflows/sonarcloud.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index b08fbdee..dd1bf9db 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -4,12 +4,12 @@ on: push: branches: - dev - - staging + - stage - main pull_request: branches: - dev - - staging + - stage - main jobs: From cfd9700ad7b79c114ce47920a1873f8cdb220cfa Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Tue, 16 Jul 2024 08:39:19 +0530 Subject: [PATCH 162/582] allow dev env in ruuter public --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 8145bb9b..824e8ac6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ services: container_name: ruuter-public image: ruuter environment: - - application.cors.allowedOrigins=http://localhost:8086,http://localhost:3001,http://localhost:3003,http://localhost:3004,http://localhost:8080 + - application.cors.allowedOrigins=http://localhost:8086,http://localhost:3001,http://localhost:3003,http://localhost:3004,http://localhost:8080,https://login.esclassifier-dev.rootcode.software - application.httpCodesAllowList=200,201,202,400,401,403,500 - application.internalRequests.allowedIPs=127.0.0.1 - application.logging.displayRequestContent=true @@ -165,4 +165,4 @@ networks: name: bykstack driver: bridge driver_opts: - com.docker.network.driver.mtu: 1400 \ No newline at end of file + com.docker.network.driver.mtu: 1400 From 9901fdff4a94989d597e24860b60d0301e7f17dc Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Tue, 16 Jul 2024 09:22:19 +0530 Subject: [PATCH 163/582] ESCLASS-137: Scheduled Cron Job to refresh outlook token monthly on first saturday midnight(Aug 03 00:00:00 UTC 2024) --- DSL/CronManager/DSL/outlook.yml | 4 ++ DSL/CronManager/config/config.ini | 6 +++ .../script/outlook_refresh_token.sh | 48 +++++++++++++++++++ DSL/Resql/save-outlook-token.sql | 3 ++ constants.ini | 1 + docker-compose.yml | 14 ++++++ 6 files changed, 76 insertions(+) create mode 100644 DSL/CronManager/DSL/outlook.yml create mode 100644 DSL/CronManager/config/config.ini create mode 100644 DSL/CronManager/script/outlook_refresh_token.sh create mode 100644 DSL/Resql/save-outlook-token.sql diff --git a/DSL/CronManager/DSL/outlook.yml b/DSL/CronManager/DSL/outlook.yml new file mode 100644 index 00000000..fca4258c --- /dev/null +++ b/DSL/CronManager/DSL/outlook.yml @@ -0,0 +1,4 @@ +token_refresh: + trigger: "0 0 0 ? * 7#1" + type: exec + command: "../app/scripts/outlook_refresh_token.sh" \ No newline at end of file diff --git a/DSL/CronManager/config/config.ini b/DSL/CronManager/config/config.ini new file mode 100644 index 00000000..b8a253ef --- /dev/null +++ b/DSL/CronManager/config/config.ini @@ -0,0 +1,6 @@ +[DSL] + +CLASSIFIER_RESQL=http://resql:8082 +OUTLOOK_CLIENT_ID=value +OUTLOOK_SECRET_KEY=value +OUTLOOK_SCOPE=User.Read Mail.ReadWrite MailboxSettings.ReadWrite offline_access diff --git a/DSL/CronManager/script/outlook_refresh_token.sh b/DSL/CronManager/script/outlook_refresh_token.sh new file mode 100644 index 00000000..ec5281c1 --- /dev/null +++ b/DSL/CronManager/script/outlook_refresh_token.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +# Set the working directory to the location of the script +cd "$(dirname "$0")" + +# Source the constants from the ini file +source ../config/config.ini + +script_name=$(basename $0) +pwd + +echo $(date -u +"%Y-%m-%d %H:%M:%S.%3NZ") - $script_name started + +# Fetch the refresh token +response=$(curl -X POST -H "Content-Type: application/json" -d '{"platform":"OUTLOOK"}' "$CLASSIFIER_RESQL/get-token") +refresh_token=$(echo $response | grep -oP '"token":"\K[^"]+') + +if [ -z "$refresh_token" ]; then + echo "No refresh token found" + exit 1 +fi + +# Request a new access token using the refresh token +access_token_response=$(curl -X POST \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "client_id=$OUTLOOK_CLIENT_ID&scope=$OUTLOOK_SCOPE&refresh_token=$refresh_token&grant_type=refresh_token&client_secret=$OUTLOOK_SECRET_KEY" \ + https://login.microsoftonline.com/common/oauth2/v2.0/token) + +new_refresh_token=$(echo $access_token_response | grep -oP '"refresh_token":"\K[^"]+') + +if [ -z "$new_refresh_token" ]; then + echo "Failed to get a new refresh token" + exit 1 +fi + +# Function to save the new refresh token +save_refresh_token() { + new_refresh_token="$1" + curl -s -X POST -H "Content-Type: application/json" -d '{"platform":"OUTLOOK", "token":"'"$new_refresh_token"'"}' "$CLASSIFIER_RESQL/save-outlook-token" +} + +# Call the function to save the new refresh token +save_refresh_token "$new_refresh_token" + +# Print the new refresh token +echo "New refresh token: $new_refresh_token" + +echo $(date -u +"%Y-%m-%d %H:%M:%S.%3NZ") - $script_name finished diff --git a/DSL/Resql/save-outlook-token.sql b/DSL/Resql/save-outlook-token.sql new file mode 100644 index 00000000..5963a749 --- /dev/null +++ b/DSL/Resql/save-outlook-token.sql @@ -0,0 +1,3 @@ +UPDATE integration_status +SET token = :token +WHERE platform = 'OUTLOOK'; \ No newline at end of file diff --git a/constants.ini b/constants.ini index 4624f4e2..b7fb2249 100644 --- a/constants.ini +++ b/constants.ini @@ -14,4 +14,5 @@ JIRA_WEBHOOK_SECRET=value OUTLOOK_CLIENT_ID=value OUTLOOK_SECRET_KEY=value OUTLOOK_REFRESH_KEY=value +OUTLOOK_SCOPE=User.Read Mail.ReadWrite MailboxSettings.ReadWrite offline_access DB_PASSWORD=value \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 05fdcdce..34b418ba 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -159,6 +159,20 @@ services: networks: - bykstack + cron-manager: + container_name: cron-manager + image: cron-manager + volumes: + - ./DSL/CronManager/DSL:/DSL + - ./DSL/CronManager/script:/app/scripts + - ./DSL/CronManager/config:/app/config + environment: + - server.port=9010 + ports: + - 9010:8080 + networks: + - bykstack + networks: bykstack: name: bykstack From d0f58a7b29e3caa07df50ad9e9455a92f6107f53 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Tue, 16 Jul 2024 17:34:15 +0530 Subject: [PATCH 164/582] workflow update to not delete cron manager image --- .github/workflows/est-workflow-dev.yml | 2 +- .github/workflows/est-workflow-staging.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/est-workflow-dev.yml b/.github/workflows/est-workflow-dev.yml index 3927df5f..406d55a2 100644 --- a/.github/workflows/est-workflow-dev.yml +++ b/.github/workflows/est-workflow-dev.yml @@ -32,7 +32,7 @@ jobs: run: | docker stop $(docker ps -a -q | grep -v -E "($(docker ps -q --filter "name=users_db")|$(docker ps -q --filter "name=tim")|$(docker ps -q --filter "name=authentication-layer")|$(docker ps -q --filter "name=tim-postgresql"))") || true docker rm $(docker ps -a -q | grep -v -E "($(docker ps -q --filter "name=users_db")|$(docker ps -q --filter "name=tim")|$(docker ps -q --filter "name=authentication-layer")|$(docker ps -q --filter "name=tim-postgresql"))") || true - images_to_keep="authentication-layer|tim|data-mapper|resql|ruuter" + images_to_keep="authentication-layer|tim|data-mapper|resql|ruuter|cron-manager" docker images --format "{{.Repository}}:{{.Tag}}" | grep -Ev "$images_to_keep" | xargs -r docker rmi || true docker volume prune -f docker network prune -f diff --git a/.github/workflows/est-workflow-staging.yml b/.github/workflows/est-workflow-staging.yml index bcf6bcb0..bbfc9dbc 100644 --- a/.github/workflows/est-workflow-staging.yml +++ b/.github/workflows/est-workflow-staging.yml @@ -32,7 +32,7 @@ jobs: run: | docker stop $(docker ps -a -q) || true docker rm $(docker ps -a -q) || true - images_to_keep="authentication-layer|tim|data-mapper|resql|ruuter" + images_to_keep="authentication-layer|tim|data-mapper|resql|ruuter|cron-manager" docker images --format "{{.Repository}}:{{.Tag}}" | grep -Ev "$images_to_keep" | xargs -r docker rmi || true docker volume prune -f docker network prune -f From f0536f91d93fa2e3abe2080e84b6f4e65f5f8bad Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Tue, 16 Jul 2024 17:48:19 +0530 Subject: [PATCH 165/582] workflow update for domain --- .github/workflows/est-workflow-dev.yml | 3 ++- constants.ini | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/est-workflow-dev.yml b/.github/workflows/est-workflow-dev.yml index 406d55a2..30050b14 100644 --- a/.github/workflows/est-workflow-dev.yml +++ b/.github/workflows/est-workflow-dev.yml @@ -39,7 +39,8 @@ jobs: - name: Update constants.ini with GitHub secret run: | - sed -i 's/DB_PASSWORD=value/DB_PASSWORD=${{ secrets.password }}/' constants.ini + sed -i 's/DB_PASSWORD=value/DB_PASSWORD=${{ secrets.DB_PASSWORD }}/' constants.ini + sed -i 's/DOMAIN=value/DOMAIN=${{ secrets.DB_DOMAIN }}/' constants.ini - name: Build and run Docker Compose run: | diff --git a/constants.ini b/constants.ini index 5366abf0..93f9a39a 100644 --- a/constants.ini +++ b/constants.ini @@ -6,7 +6,7 @@ CLASSIFIER_DMAPPER=http://data-mapper:3000 CLASSIFIER_RESQL=http://resql:8082 CLASSIFIER_TIM=http://tim:8085 CLASSIFIER_RUUTER_PUBLIC_FRONTEND_URL=value -DOMAIN=localhost +DOMAIN=value JIRA_API_TOKEN= value JIRA_USERNAME= value JIRA_CLOUD_DOMAIN= value From 7226cf9aef92e28bf1ecf8bcd59c7ed78fb95024 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Tue, 16 Jul 2024 17:55:24 +0530 Subject: [PATCH 166/582] dummy update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dcbd9273..96061858 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # Introduction -A template repository.. +A template repository.. \ No newline at end of file From 1007a7211b725be9b8f314b80d15834ffac48080 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Tue, 16 Jul 2024 17:57:32 +0530 Subject: [PATCH 167/582] domain update --- .github/workflows/est-workflow-dev.yml | 1 - constants.ini | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/est-workflow-dev.yml b/.github/workflows/est-workflow-dev.yml index 30050b14..34c47304 100644 --- a/.github/workflows/est-workflow-dev.yml +++ b/.github/workflows/est-workflow-dev.yml @@ -40,7 +40,6 @@ jobs: - name: Update constants.ini with GitHub secret run: | sed -i 's/DB_PASSWORD=value/DB_PASSWORD=${{ secrets.DB_PASSWORD }}/' constants.ini - sed -i 's/DOMAIN=value/DOMAIN=${{ secrets.DB_DOMAIN }}/' constants.ini - name: Build and run Docker Compose run: | diff --git a/constants.ini b/constants.ini index 93f9a39a..07896f68 100644 --- a/constants.ini +++ b/constants.ini @@ -6,7 +6,7 @@ CLASSIFIER_DMAPPER=http://data-mapper:3000 CLASSIFIER_RESQL=http://resql:8082 CLASSIFIER_TIM=http://tim:8085 CLASSIFIER_RUUTER_PUBLIC_FRONTEND_URL=value -DOMAIN=value +DOMAIN=https://esclassifier-dev.rootcode.software JIRA_API_TOKEN= value JIRA_USERNAME= value JIRA_CLOUD_DOMAIN= value From 678cc198c2c7cb6bbff89189a42caaa2853badda Mon Sep 17 00:00:00 2001 From: Pamoda Dilranga <51948729+pamodaDilranga@users.noreply.github.com> Date: Tue, 16 Jul 2024 19:17:34 +0530 Subject: [PATCH 168/582] Setup dev env domain to assign the cookie Update constants.ini to match the domain --- constants.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/constants.ini b/constants.ini index 07896f68..bb7cd9d8 100644 --- a/constants.ini +++ b/constants.ini @@ -6,7 +6,7 @@ CLASSIFIER_DMAPPER=http://data-mapper:3000 CLASSIFIER_RESQL=http://resql:8082 CLASSIFIER_TIM=http://tim:8085 CLASSIFIER_RUUTER_PUBLIC_FRONTEND_URL=value -DOMAIN=https://esclassifier-dev.rootcode.software +DOMAIN=rootcode.software JIRA_API_TOKEN= value JIRA_USERNAME= value JIRA_CLOUD_DOMAIN= value @@ -16,4 +16,4 @@ OUTLOOK_CLIENT_ID=value OUTLOOK_SECRET_KEY=value OUTLOOK_REFRESH_KEY=value OUTLOOK_SCOPE=User.Read Mail.ReadWrite MailboxSettings.ReadWrite offline_access -DB_PASSWORD=value \ No newline at end of file +DB_PASSWORD=value From b584427ddf08c8f1bb113cc2b28a93cc0d801144 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Tue, 16 Jul 2024 19:23:26 +0530 Subject: [PATCH 169/582] pagination added --- GUI/src/components/DataTable/DataTable.scss | 2 +- .../molecules/DatasetGroupCard/index.tsx | 46 ++++++++++++------- .../molecules/Pagination/Pagination.scss | 2 - .../components/molecules/Pagination/index.tsx | 6 +-- GUI/src/pages/DatasetGroups/index.tsx | 14 +++++- GUI/src/services/api-mock.ts | 37 +++++++++++++++ GUI/src/services/datasets.ts | 11 +++++ 7 files changed, 94 insertions(+), 24 deletions(-) create mode 100644 GUI/src/services/api-mock.ts create mode 100644 GUI/src/services/datasets.ts diff --git a/GUI/src/components/DataTable/DataTable.scss b/GUI/src/components/DataTable/DataTable.scss index 58c84ab4..7e4d25c0 100644 --- a/GUI/src/components/DataTable/DataTable.scss +++ b/GUI/src/components/DataTable/DataTable.scss @@ -108,8 +108,8 @@ } &__pagination-wrapper { + margin-top: 10px; display: flex; - box-shadow: 0 -1px 0 get-color(black-coral-10); padding: 6px 16px; } diff --git a/GUI/src/components/molecules/DatasetGroupCard/index.tsx b/GUI/src/components/molecules/DatasetGroupCard/index.tsx index 92fd4e5a..1081ff3e 100644 --- a/GUI/src/components/molecules/DatasetGroupCard/index.tsx +++ b/GUI/src/components/molecules/DatasetGroupCard/index.tsx @@ -6,38 +6,50 @@ import Button from 'components/Button'; import Label from 'components/Label'; type DatasetGroupCardProps = { - isEnabled?: boolean; + datasetGroupId?: string; datasetName?: string; - status?: string; + version?: string; + isLatest?: boolean; + isEnabled?:boolean; + enableAllowed?:boolean; + lastUpdated?:string; + validationStatus?:string; + lastModelTrained?:string }; const DatasetGroupCard: FC> = ({ - isEnabled, + datasetGroupId, datasetName, - status, + version, + isLatest, + isEnabled, + enableAllowed, + lastUpdated, + validationStatus, + lastModelTrained }) => { return ( <>
    -
    -

    {datasetName}

    +

    {datasetName}

    - -
    -

    {"Last Model Trained: Model Alpha"}

    -

    {"Last Used For Training: 8.6.24-13:01"}

    -

    {"Last Updated: 7.6.24-15:31"}

    + +
    +

    {'Last Model Trained:'}{lastModelTrained}

    +

    {'Last Used For Training:'}{}

    +

    {'Last Updated: 7.6.24-15:31'}

    -
    - - - +
    + +
    - +
    - +
    diff --git a/GUI/src/components/molecules/Pagination/Pagination.scss b/GUI/src/components/molecules/Pagination/Pagination.scss index 58c84ab4..5c89eb88 100644 --- a/GUI/src/components/molecules/Pagination/Pagination.scss +++ b/GUI/src/components/molecules/Pagination/Pagination.scss @@ -79,7 +79,6 @@ right: 0; padding: get-spacing(paldiski); background-color: get-color(white); - box-shadow: 0 4px 10px rgba(0, 0, 0, 0.14); border-radius: 0 0 4px 4px; border: 1px solid get-color(black-coral-2); @@ -109,7 +108,6 @@ &__pagination-wrapper { display: flex; - box-shadow: 0 -1px 0 get-color(black-coral-10); padding: 6px 16px; } diff --git a/GUI/src/components/molecules/Pagination/index.tsx b/GUI/src/components/molecules/Pagination/index.tsx index 39b36e26..71bbc09a 100644 --- a/GUI/src/components/molecules/Pagination/index.tsx +++ b/GUI/src/components/molecules/Pagination/index.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { Link } from 'react-router-dom'; import { MdOutlineWest, MdOutlineEast } from 'react-icons/md'; import clsx from 'clsx'; -import "./Pagination.scss" +// import "./Pagination.scss" import { useTranslation } from 'react-i18next'; interface PaginationProps { @@ -67,7 +67,7 @@ const Pagination: React.FC = ({
    )} -
    + {/*
    -
    +
    */}
    ); }; diff --git a/GUI/src/pages/DatasetGroups/index.tsx b/GUI/src/pages/DatasetGroups/index.tsx index fccf33d4..5fb5294f 100644 --- a/GUI/src/pages/DatasetGroups/index.tsx +++ b/GUI/src/pages/DatasetGroups/index.tsx @@ -4,6 +4,8 @@ import { useTranslation } from 'react-i18next'; import { Button, FormInput, FormSelect } from 'components'; import DatasetGroupCard from 'components/molecules/DatasetGroupCard'; import Pagination from 'components/molecules/Pagination'; +import { getDatasetsOverview } from 'services/datasets'; +import { useQuery } from '@tanstack/react-query'; const DatasetGroups: FC = () => { const { t } = useTranslation(); @@ -19,8 +21,18 @@ const DatasetGroups: FC = () => { { datasetName: 'Dataset 2', status: 'Disconnected', isEnabled: true }, { datasetName: 'Dataset 4', status: 'Disconnected', isEnabled: true }, { datasetName: 'Dataset 3', status: 'Disconnected', isEnabled: false }, + { datasetName: 'Dataset 3', status: 'Disconnected', isEnabled: false }, + { datasetName: 'Dataset 3', status: 'Disconnected', isEnabled: false }, + ]; + const { data: datasetGroupsData, isLoading } = useQuery( + ['datasets/groups'], + () => getDatasetsOverview(1) + ); + + console.log(datasetGroupsData); + return ( <>
    @@ -84,7 +96,7 @@ const DatasetGroups: FC = () => { })}
    - + diff --git a/GUI/src/services/api-mock.ts b/GUI/src/services/api-mock.ts new file mode 100644 index 00000000..52b762d5 --- /dev/null +++ b/GUI/src/services/api-mock.ts @@ -0,0 +1,37 @@ +import axios, { AxiosError } from 'axios'; + +const instance = axios.create({ + baseURL: "https://d5e7cde0-f9b1-4425-8a16-c5f93f503e2e.mock.pstmn.io", + headers: { + Accept: 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded', + }, +}); + +instance.interceptors.response.use( + (axiosResponse) => { + return axiosResponse; + }, + (error: AxiosError) => { + console.log(error); + return Promise.reject(new Error(error.message)); + } +); + +instance.interceptors.request.use( + (axiosRequest) => { + return axiosRequest; + }, + (error: AxiosError) => { + console.log(error); + if (error.response?.status === 401) { + // To be added: handle unauthorized requests + } + if (error.response?.status === 403) { + // To be added: handle unauthorized requests + } + return Promise.reject(new Error(error.message)); + } +); + +export default instance; diff --git a/GUI/src/services/datasets.ts b/GUI/src/services/datasets.ts new file mode 100644 index 00000000..b63ac422 --- /dev/null +++ b/GUI/src/services/datasets.ts @@ -0,0 +1,11 @@ +import apiMock from './api-mock'; +import { User, UserDTO } from 'types/user'; + +export async function getDatasetsOverview(pageNum: number) { + + const { data } = await apiMock.get('GET/datasetgroup/overview', { params: { + page_num: pageNum + }}); + return data; +} + From d2c520fa2bb4073ec12a08a1b3cdbd7eb52e4127 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Tue, 16 Jul 2024 20:34:20 +0530 Subject: [PATCH 170/582] added separate docker-compose file for dev and local --- GUI/.env.development | 3 +- docker-compose.development.yml | 180 +++++++++++++++++++++++++++++++++ docker-compose.local.yml | 180 +++++++++++++++++++++++++++++++++ 3 files changed, 361 insertions(+), 2 deletions(-) create mode 100644 docker-compose.development.yml create mode 100644 docker-compose.local.yml diff --git a/GUI/.env.development b/GUI/.env.development index a8d9f676..6297da2a 100644 --- a/GUI/.env.development +++ b/GUI/.env.development @@ -3,8 +3,7 @@ REACT_APP_RUUTER_PRIVATE_API_URL=https://esclassifier-dev-ruuter-private.rootcod REACT_APP_CUSTOMER_SERVICE_LOGIN=https://login.esclassifier-dev.rootcode.software REACT_APP_SERVICE_ID=conversations,settings,monitoring REACT_APP_NOTIFICATION_NODE_URL=http://localhost:4040 -REACT_APP_CSP=upgrade-insecure-requests; default-src 'self'; font-src 'self' data:; img-src 'self' data:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self' http://localhost:8086 http://localhost:8088 http://localhost:8085 http://localhost:4040 http://localhost:3002/menu.json; +REACT_APP_CSP=upgrade-insecure-requests; default-src 'self'; font-src 'self' data:; img-src 'self' data:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self' http://localhost:8086 http://localhost:8088 http://localhost:8085 http://localhost:4040; REACT_APP_ENABLE_HIDDEN_FEATURES=TRUE -REACT_APP_MENU_JSON=[{"id":"conversations","label":{"et":"Vestlused","en":"Conversations"},"path":"/chat","children":[{"label":{"et":"Vastamata","en":"Unanswered"},"path":"/unanswered"},{"label":{"et":"Aktiivsed","en":"Active"},"path":"/active"},{"label":{"et":"Ajalugu","en":"History"},"path":"/history"}]},{"id":"training","label":{"et":"Treening","en":"Training"},"path":"/training","children":[{"label":{"et":"Treening","en":"Training"},"path":"/training","children":[{"label":{"et":"Teemad","en":"Themes"},"path":"/training/intents"},{"label":{"et":"Avalikud teemad","en":"Public themes"},"path":"/training/common-intents"},{"label":{"et":"Teemade järeltreenimine","en":"Post training themes"},"path":"/training/intents-followup-training"},{"label":{"et":"Vastused","en":"Answers"},"path":"/training/responses"},{"label":{"et":"Kasutuslood","en":"User Stories"},"path":"/training/stories"},{"label":{"et":"Konfiguratsioon","en":"Configuration"},"path":"/training/configuration"},{"label":{"et":"Vormid","en":"Forms"},"path":"/training/forms"},{"label":{"et":"Mälukohad","en":"Slots"},"path":"/training/slots"},{"label":{"et":"Automatic Teenused","en":"Automatic Services"},"path":"/auto-services"}]},{"label":{"et":"Ajaloolised vestlused","en":"Historical conversations"},"path":"/history","children":[{"label":{"et":"Ajalugu","en":"History"},"path":"/history/history"},{"label":{"et":"Pöördumised","en":"Appeals"},"path":"/history/appeal"}]},{"label":{"et":"Mudelipank ja analüütika","en":"Modelbank and analytics"},"path":"/analytics","children":[{"label":{"et":"Teemade ülevaade","en":"Overview of topics"},"path":"/analytics/overview"},{"label":{"et":"Mudelite võrdlus","en":"Comparison of models"},"path":"/analytics/models"},{"label":{"et":"Testlood","en":"testTracks"},"path":"/analytics/testcases"}]},{"label":{"et":"Treeni uus mudel","en":"Train new model"},"path":"/train-new-model"}]},{"id":"analytics","label":{"et":"Analüütika","en":"Analytics"},"path":"/analytics","children":[{"label":{"et":"Ülevaade","en":"Overview"},"path":"/overview"},{"label":{"et":"Vestlused","en":"Chats"},"path":"/chats"},{"label":{"et":"Bürokratt","en":"Burokratt"},"path":"/burokratt"},{"label":{"et":"Tagasiside","en":"Feedback"},"path":"/feedback"},{"label":{"et":"Nõustajad","en":"Advisors"},"path":"/advisors"},{"label":{"et":"Avaandmed","en":"Reports"},"path":"/reports"}]},{"id":"services","label":{"et":"Teenused","en":"Services"},"path":"/services","children":[{"label":{"et":"Ülevaade","en":"Overview"},"path":"/overview"},{"label":{"et":"Uus teenus","en":"New Service"},"path":"/newService"},{"label":{"et":"Automatic Teenused","en":"Automatic Services"},"path":"/auto-services"},{"label":{"et":"Probleemsed teenused","en":"Faulty Services"},"path":"/faultyServices"}]},{"id":"settings","label":{"et":"Haldus","en":"Administration"},"path":"/settings","children":[{"label":{"et":"Kasutajad","en":"Users"},"path":"/users"},{"label":{"et":"Vestlusbot","en":"Chatbot"},"path":"/chatbot","children":[{"label":{"et":"Seaded","en":"Settings"},"path":"/chatbot/settings"},{"label":{"et":"Tervitussõnum","en":"Welcome message"},"path":"/chatbot/welcome-message"},{"label":{"et":"Välimus ja käitumine","en":"Appearance and behavior"},"path":"/chatbot/appearance"},{"label":{"et":"Erakorralised teated","en":"Emergency notices"},"path":"/chatbot/emergency-notices"}]},{"label":{"et":"Asutuse tööaeg","en":"Office opening hours"},"path":"/working-time"},{"label":{"et":"Sessiooni pikkus","en":"Session length"},"path":"/session-length"}]},{"id":"monitoring","label":{"et":"Seire","en":"Monitoring"},"path":"/monitoring","children":[{"label":{"et":"Aktiivaeg","en":"Working hours"},"path":"/uptime"}]}] diff --git a/docker-compose.development.yml b/docker-compose.development.yml new file mode 100644 index 00000000..6ecc8afa --- /dev/null +++ b/docker-compose.development.yml @@ -0,0 +1,180 @@ +services: + ruuter-public: + container_name: ruuter-public + image: ruuter + environment: + - application.cors.allowedOrigins=https://login.esclassifier-dev.rootcode.software + - application.httpCodesAllowList=200,201,202,400,401,403,500 + - application.internalRequests.allowedIPs=127.0.0.1 + - application.logging.displayRequestContent=true + - application.logging.displayResponseContent=true + - server.port=8086 + volumes: + - ./DSL/Ruuter.public/DSL:/DSL + - ./constants.ini:/app/constants.ini + ports: + - 8086:8086 + networks: + - bykstack + cpus: "0.5" + mem_limit: "512M" + + ruuter-private: + container_name: ruuter-private + image: ruuter + environment: + - application.cors.allowedOrigins=https://esclassifier-dev.rootcode.software,https://esclassifier-dev-ruuter-private.rootcode.software,https://login.esclassifier-dev.rootcode.software + - application.httpCodesAllowList=200,201,202,204,400,401,403,404,500 + - application.internalRequests.allowedIPs=127.0.0.1 + - application.logging.displayRequestContent=true + - application.incomingRequests.allowedMethodTypes=POST,GET,PUT + - application.logging.displayResponseContent=true + - application.logging.printStackTrace=true + - server.port=8088 + volumes: + - ./DSL/Ruuter.private/DSL:/DSL + - ./constants.ini:/app/constants.ini + ports: + - 8088:8088 + networks: + - bykstack + cpus: "0.5" + mem_limit: "512M" + + data-mapper: + container_name: data-mapper + image: data-mapper + environment: + - PORT=3000 + - CONTENT_FOLDER=/data + volumes: + - ./DSL:/data + - ./DSL/DMapper/hbs:/workspace/app/views/classifier + - ./DSL/DMapper/js:/workspace/app/js/classifier + - ./DSL/DMapper/lib:/workspace/app/lib + ports: + - 3000:3000 + networks: + - bykstack + + tim: + container_name: tim + image: tim + depends_on: + - tim-postgresql + environment: + - SECURITY_ALLOWLIST_JWT=ruuter-private,ruuter-public,data-mapper,resql,tim,tim-postgresql,chat-widget,authentication-layer,127.0.0.1,::1 + ports: + - 8085:8085 + networks: + - bykstack + extra_hosts: + - "host.docker.internal:host-gateway" + cpus: "0.5" + mem_limit: "512M" + + tim-postgresql: + container_name: tim-postgresql + image: postgres:14.1 + environment: + - POSTGRES_USER=tim + - POSTGRES_PASSWORD=123 + - POSTGRES_DB=tim + - POSTGRES_HOST_AUTH_METHOD=trust + volumes: + - ./tim-db:/var/lib/postgresql/data + ports: + - 9876:5432 + networks: + - bykstack + + gui: + container_name: gui + environment: + - NODE_ENV=development + - REACT_APP_RUUTER_API_URL=https://esclassifier-dev-ruuter.rootcode.software + - REACT_APP_RUUTER_PRIVATE_API_URL=https://esclassifier-dev-ruuter-private.rootcode.software + - REACT_APP_CUSTOMER_SERVICE_LOGIN=https://login.esclassifier-dev.rootcode.software + # - REACT_APP_NOTIFICATION_NODE_URL=http://localhost:4040 + - REACT_APP_CSP=upgrade-insecure-requests; default-src 'self'; font-src 'self' data:; img-src 'self' data:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self' https://esclassifier-dev-ruuter.rootcode.software https://esclassifier-dev-ruuter-private.rootcode.software https://esclassifier-dev-tim.rootcode.software; + - DEBUG_ENABLED=true + - CHOKIDAR_USEPOLLING=true + - PORT=3001 + - REACT_APP_SERVICE_ID=conversations,settings,monitoring + - REACT_APP_ENABLE_HIDDEN_FEATURES=TRUE + + build: + context: ./GUI + dockerfile: Dockerfile.dev + ports: + - 3001:3001 + volumes: + - /app/node_modules + - ./GUI:/app + networks: + - bykstack + cpus: "0.5" + mem_limit: "1G" + + authentication-layer: + container_name: authentication-layer + image: authentication-layer + ports: + - 3004:3004 + networks: + - bykstack + + resql: + container_name: resql + image: resql + depends_on: + - users_db + environment: + - sqlms.datasources.[0].name=classifier + - sqlms.datasources.[0].jdbcUrl=jdbc:postgresql://users_db:5432/classifier #For LocalDb Use + # sqlms.datasources.[0].jdbcUrl=jdbc:postgresql://171.22.247.13:5433/byk?sslmode=require + - sqlms.datasources.[0].username=postgres + - sqlms.datasources.[0].password=rootcode + - logging.level.org.springframework.boot=INFO + ports: + - 8082:8082 + volumes: + - ./DSL/Resql:/workspace/app/templates/classifier + networks: + - bykstack + + users_db: + container_name: users_db + image: postgres:14.1 + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=rootcode + - POSTGRES_DB=classifier + ports: + - 5433:5432 + volumes: + - /home/ubuntu/user_db_files:/var/lib/postgresql/data + networks: + - bykstack + restart: always + + cron-manager: + container_name: cron-manager + image: cron-manager + volumes: + - ./DSL/CronManager/DSL:/DSL + - ./DSL/CronManager/script:/app/scripts + - ./DSL/CronManager/config:/app/config + environment: + - server.port=9010 + ports: + - 9010:8080 + networks: + - bykstack + +networks: + bykstack: + name: bykstack + driver: bridge + driver_opts: + com.docker.network.driver.mtu: 1400 diff --git a/docker-compose.local.yml b/docker-compose.local.yml new file mode 100644 index 00000000..b174b2fb --- /dev/null +++ b/docker-compose.local.yml @@ -0,0 +1,180 @@ +services: + ruuter-public: + container_name: ruuter-public + image: ruuter + environment: + - application.cors.allowedOrigins=http://localhost:8086,http://localhost:3001,http://localhost:3003,http://localhost:3004,http://localhost:8080 + - application.httpCodesAllowList=200,201,202,400,401,403,500 + - application.internalRequests.allowedIPs=127.0.0.1 + - application.logging.displayRequestContent=true + - application.logging.displayResponseContent=true + - server.port=8086 + volumes: + - ./DSL/Ruuter.public/DSL:/DSL + - ./constants.ini:/app/constants.ini + ports: + - 8086:8086 + networks: + - bykstack + cpus: "0.5" + mem_limit: "512M" + + ruuter-private: + container_name: ruuter-private + image: ruuter + environment: + - application.cors.allowedOrigins=http://localhost:3001,http://localhost:8088,http://localhost:3002,http://localhost:3004 + - application.httpCodesAllowList=200,201,202,204,400,401,403,404,500 + - application.internalRequests.allowedIPs=127.0.0.1 + - application.logging.displayRequestContent=true + - application.incomingRequests.allowedMethodTypes=POST,GET,PUT + - application.logging.displayResponseContent=true + - application.logging.printStackTrace=true + - server.port=8088 + volumes: + - ./DSL/Ruuter.private/DSL:/DSL + - ./constants.ini:/app/constants.ini + ports: + - 8088:8088 + networks: + - bykstack + cpus: "0.5" + mem_limit: "512M" + + data-mapper: + container_name: data-mapper + image: data-mapper + environment: + - PORT=3000 + - CONTENT_FOLDER=/data + volumes: + - ./DSL:/data + - ./DSL/DMapper/hbs:/workspace/app/views/classifier + - ./DSL/DMapper/js:/workspace/app/js/classifier + - ./DSL/DMapper/lib:/workspace/app/lib + ports: + - 3000:3000 + networks: + - bykstack + + tim: + container_name: tim + image: tim + depends_on: + - tim-postgresql + environment: + - SECURITY_ALLOWLIST_JWT=ruuter-private,ruuter-public,data-mapper,resql,tim,tim-postgresql,chat-widget,authentication-layer,127.0.0.1,::1 + ports: + - 8085:8085 + networks: + - bykstack + extra_hosts: + - "host.docker.internal:host-gateway" + cpus: "0.5" + mem_limit: "512M" + + tim-postgresql: + container_name: tim-postgresql + image: postgres:14.1 + environment: + - POSTGRES_USER=tim + - POSTGRES_PASSWORD=123 + - POSTGRES_DB=tim + - POSTGRES_HOST_AUTH_METHOD=trust + volumes: + - ./tim-db:/var/lib/postgresql/data + ports: + - 9876:5432 + networks: + - bykstack + + gui: + container_name: gui + environment: + - NODE_ENV=local + - REACT_APP_RUUTER_API_URL=http://localhost:8086 + - REACT_APP_RUUTER_PRIVATE_API_URL=http://localhost:8088 + - REACT_APP_CUSTOMER_SERVICE_LOGIN=http://localhost:3004/et/dev-auth + # - REACT_APP_NOTIFICATION_NODE_URL=http://localhost:4040 + - REACT_APP_CSP=upgrade-insecure-requests; default-src 'self'; font-src 'self' data:; img-src 'self' data:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self' http://localhost:8086 http://localhost:8088 http://localhost:8085 http://localhost:4040 http://localhost:3001; + - DEBUG_ENABLED=true + - CHOKIDAR_USEPOLLING=true + - PORT=3001 + - REACT_APP_SERVICE_ID=conversations,settings,monitoring + - REACT_APP_ENABLE_HIDDEN_FEATURES=TRUE + + build: + context: ./GUI + dockerfile: Dockerfile.dev + ports: + - 3001:3001 + volumes: + - /app/node_modules + - ./GUI:/app + networks: + - bykstack + cpus: "0.5" + mem_limit: "1G" + + authentication-layer: + container_name: authentication-layer + image: authentication-layer + ports: + - 3004:3004 + networks: + - bykstack + + resql: + container_name: resql + image: resql + depends_on: + - users_db + environment: + - sqlms.datasources.[0].name=classifier + - sqlms.datasources.[0].jdbcUrl=jdbc:postgresql://users_db:5432/classifier #For LocalDb Use + # sqlms.datasources.[0].jdbcUrl=jdbc:postgresql://171.22.247.13:5433/byk?sslmode=require + - sqlms.datasources.[0].username=postgres + - sqlms.datasources.[0].password=rootcode + - logging.level.org.springframework.boot=INFO + ports: + - 8082:8082 + volumes: + - ./DSL/Resql:/workspace/app/templates/classifier + networks: + - bykstack + + users_db: + container_name: users_db + image: postgres:14.1 + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=rootcode + - POSTGRES_DB=classifier + ports: + - 5433:5432 + volumes: + - /home/ubuntu/user_db_files:/var/lib/postgresql/data + networks: + - bykstack + restart: always + + # cron-manager: + # container_name: cron-manager + # image: cron-manager + # volumes: + # - ./DSL/CronManager/DSL:/DSL + # - ./DSL/CronManager/script:/app/scripts + # - ./DSL/CronManager/config:/app/config + # environment: + # - server.port=9010 + # ports: + # - 9010:8080 + # networks: + # - bykstack + +networks: + bykstack: + name: bykstack + driver: bridge + driver_opts: + com.docker.network.driver.mtu: 1400 From d561611420c933ecb43e907694b471c08ec82259 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Tue, 16 Jul 2024 20:52:18 +0530 Subject: [PATCH 171/582] removed old docker-compose --- docker-compose.yml | 182 --------------------------------------------- 1 file changed, 182 deletions(-) delete mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 46c72a8c..00000000 --- a/docker-compose.yml +++ /dev/null @@ -1,182 +0,0 @@ -services: - ruuter-public: - container_name: ruuter-public - image: ruuter - environment: - - application.cors.allowedOrigins=http://localhost:8086,http://localhost:3001,http://localhost:3003,http://localhost:3004,http://localhost:8080,https://login.esclassifier-dev.rootcode.software - - application.httpCodesAllowList=200,201,202,400,401,403,500 - - application.internalRequests.allowedIPs=127.0.0.1 - - application.logging.displayRequestContent=true - - application.logging.displayResponseContent=true - - server.port=8086 - volumes: - - ./DSL/Ruuter.public/DSL:/DSL - - ./constants.ini:/app/constants.ini - ports: - - 8086:8086 - networks: - - bykstack - cpus: "0.5" - mem_limit: "512M" - - ruuter-private: - container_name: ruuter-private - image: ruuter - environment: - - application.cors.allowedOrigins=http://localhost:3001,http://localhost:8088,http://localhost:3002,http://localhost:3004 - - application.httpCodesAllowList=200,201,202,204,400,401,403,404,500 - - application.internalRequests.allowedIPs=127.0.0.1 - - application.logging.displayRequestContent=true - - application.incomingRequests.allowedMethodTypes=POST,GET,PUT - - application.logging.displayResponseContent=true - - application.logging.printStackTrace=true - - server.port=8088 - volumes: - - ./DSL/Ruuter.private/DSL:/DSL - - ./constants.ini:/app/constants.ini - ports: - - 8088:8088 - networks: - - bykstack - cpus: "0.5" - mem_limit: "512M" - - data-mapper: - container_name: data-mapper - image: data-mapper - environment: - - PORT=3000 - - CONTENT_FOLDER=/data - volumes: - - ./DSL:/data - - ./DSL/DMapper/hbs:/workspace/app/views/classifier - - ./DSL/DMapper/js:/workspace/app/js/classifier - - ./DSL/DMapper/lib:/workspace/app/lib - ports: - - 3000:3000 - networks: - - bykstack - - tim: - container_name: tim - image: tim - depends_on: - - tim-postgresql - environment: - - SECURITY_ALLOWLIST_JWT=ruuter-private,ruuter-public,data-mapper,resql,tim,tim-postgresql,chat-widget,authentication-layer,127.0.0.1,::1 - ports: - - 8085:8085 - networks: - - bykstack - extra_hosts: - - "host.docker.internal:host-gateway" - cpus: "0.5" - mem_limit: "512M" - - tim-postgresql: - container_name: tim-postgresql - image: postgres:14.1 - environment: - - POSTGRES_USER=tim - - POSTGRES_PASSWORD=123 - - POSTGRES_DB=tim - - POSTGRES_HOST_AUTH_METHOD=trust - volumes: - - ./tim-db:/var/lib/postgresql/data - ports: - - 9876:5432 - networks: - - bykstack - - gui: - container_name: gui - environment: - - NODE_ENV=development - # - BASE_URL=http://localhost:8080 - - REACT_APP_RUUTER_API_URL=https://esclassifier-dev-ruuter.rootcode.software - - REACT_APP_RUUTER_PRIVATE_API_URL=https://esclassifier-dev-ruuter-private.rootcode.software - - REACT_APP_CUSTOMER_SERVICE_LOGIN=https://login.esclassifier-dev.rootcode.software - # - REACT_APP_NOTIFICATION_NODE_URL=http://localhost:4040 - - REACT_APP_CSP=upgrade-insecure-requests; default-src 'self'; font-src 'self' data:; img-src 'self' data:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self' http://localhost:8086 http://localhost:8088 http://localhost:8085 http://localhost:4040 https://admin.dev.buerokratt.ee/chat/menu.json; - - DEBUG_ENABLED=true - - CHOKIDAR_USEPOLLING=true - - PORT=3001 - - REACT_APP_SERVICE_ID=conversations,settings,monitoring - - REACT_APP_ENABLE_HIDDEN_FEATURES=TRUE - - REACT_APP_MENU_JSON=[{"id":"conversations","label":{"et":"Vestlused","en":"Conversations"},"path":"/chat","children":[{"label":{"et":"Vastamata","en":"Unanswered"},"path":"/unanswered"},{"label":{"et":"Aktiivsed","en":"Active"},"path":"/active"},{"label":{"et":"Ajalugu","en":"History"},"path":"/history"}]},{"id":"training","label":{"et":"Treening","en":"Training"},"path":"/training","children":[{"label":{"et":"Treening","en":"Training"},"path":"/training","children":[{"label":{"et":"Teemad","en":"Themes"},"path":"/training/intents"},{"label":{"et":"Avalikud teemad","en":"Public themes"},"path":"/training/common-intents"},{"label":{"et":"Teemade järeltreenimine","en":"Post training themes"},"path":"/training/intents-followup-training"},{"label":{"et":"Vastused","en":"Answers"},"path":"/training/responses"},{"label":{"et":"Kasutuslood","en":"User Stories"},"path":"/training/stories"},{"label":{"et":"Konfiguratsioon","en":"Configuration"},"path":"/training/configuration"},{"label":{"et":"Vormid","en":"Forms"},"path":"/training/forms"},{"label":{"et":"Mälukohad","en":"Slots"},"path":"/training/slots"},{"label":{"et":"Automatic Teenused","en":"Automatic Services"},"path":"/auto-services"}]},{"label":{"et":"Ajaloolised vestlused","en":"Historical conversations"},"path":"/history","children":[{"label":{"et":"Ajalugu","en":"History"},"path":"/history/history"},{"label":{"et":"Pöördumised","en":"Appeals"},"path":"/history/appeal"}]},{"label":{"et":"Mudelipank ja analüütika","en":"Modelbank and analytics"},"path":"/analytics","children":[{"label":{"et":"Teemade ülevaade","en":"Overview of topics"},"path":"/analytics/overview"},{"label":{"et":"Mudelite võrdlus","en":"Comparison of models"},"path":"/analytics/models"},{"label":{"et":"Testlood","en":"testTracks"},"path":"/analytics/testcases"}]},{"label":{"et":"Treeni uus mudel","en":"Train new model"},"path":"/train-new-model"}]},{"id":"analytics","label":{"et":"Analüütika","en":"Analytics"},"path":"/analytics","children":[{"label":{"et":"Ülevaade","en":"Overview"},"path":"/overview"},{"label":{"et":"Vestlused","en":"Chats"},"path":"/chats"},{"label":{"et":"Bürokratt","en":"Burokratt"},"path":"/burokratt"},{"label":{"et":"Tagasiside","en":"Feedback"},"path":"/feedback"},{"label":{"et":"Nõustajad","en":"Advisors"},"path":"/advisors"},{"label":{"et":"Avaandmed","en":"Reports"},"path":"/reports"}]},{"id":"services","label":{"et":"Teenused","en":"Services"},"path":"/services","children":[{"label":{"et":"Ülevaade","en":"Overview"},"path":"/overview"},{"label":{"et":"Uus teenus","en":"New Service"},"path":"/newService"},{"label":{"et":"Automatic Teenused","en":"Automatic Services"},"path":"/auto-services"},{"label":{"et":"Probleemsed teenused","en":"Faulty Services"},"path":"/faultyServices"}]},{"id":"settings","label":{"et":"Haldus","en":"Administration"},"path":"/settings","children":[{"label":{"et":"Kasutajad","en":"Users"},"path":"/users"},{"label":{"et":"Vestlusbot","en":"Chatbot"},"path":"/chatbot","children":[{"label":{"et":"Seaded","en":"Settings"},"path":"/chatbot/settings"},{"label":{"et":"Tervitussõnum","en":"Welcome message"},"path":"/chatbot/welcome-message"},{"label":{"et":"Välimus ja käitumine","en":"Appearance and behavior"},"path":"/chatbot/appearance"},{"label":{"et":"Erakorralised teated","en":"Emergency notices"},"path":"/chatbot/emergency-notices"}]},{"label":{"et":"Asutuse tööaeg","en":"Office opening hours"},"path":"/working-time"},{"label":{"et":"Sessiooni pikkus","en":"Session length"},"path":"/session-length"}]},{"id":"monitoring","label":{"et":"Seire","en":"Monitoring"},"path":"/monitoring","children":[{"label":{"et":"Aktiivaeg","en":"Working hours"},"path":"/uptime"}]}] - - build: - context: ./GUI - dockerfile: Dockerfile.dev - ports: - - 3001:3001 - volumes: - - /app/node_modules - - ./GUI:/app - networks: - - bykstack - cpus: "0.5" - mem_limit: "1G" - - authentication-layer: - container_name: authentication-layer - image: authentication-layer - ports: - - 3004:3004 - networks: - - bykstack - - resql: - container_name: resql - image: resql - depends_on: - - users_db - environment: - - sqlms.datasources.[0].name=classifier - - sqlms.datasources.[0].jdbcUrl=jdbc:postgresql://users_db:5432/classifier #For LocalDb Use - # sqlms.datasources.[0].jdbcUrl=jdbc:postgresql://171.22.247.13:5433/byk?sslmode=require - - sqlms.datasources.[0].username=postgres - - sqlms.datasources.[0].password=rootcode - - logging.level.org.springframework.boot=INFO - ports: - - 8082:8082 - volumes: - - ./DSL/Resql:/workspace/app/templates/classifier - networks: - - bykstack - - users_db: - container_name: users_db - image: postgres:14.1 - environment: - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=rootcode - - POSTGRES_DB=classifier - ports: - - 5433:5432 - volumes: - - /home/ubuntu/user_db_files:/var/lib/postgresql/data - networks: - - bykstack - restart: always - - cron-manager: - container_name: cron-manager - image: cron-manager - volumes: - - ./DSL/CronManager/DSL:/DSL - - ./DSL/CronManager/script:/app/scripts - - ./DSL/CronManager/config:/app/config - environment: - - server.port=9010 - ports: - - 9010:8080 - networks: - - bykstack - -networks: - bykstack: - name: bykstack - driver: bridge - driver_opts: - com.docker.network.driver.mtu: 1400 From 5e94f305fefa4262cde90c6d9a4e78c99156dbe6 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Tue, 16 Jul 2024 23:04:15 +0530 Subject: [PATCH 172/582] Rename docker-compose.local.yml to docker-compose.yml --- docker-compose.local.yml => docker-compose.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docker-compose.local.yml => docker-compose.yml (100%) diff --git a/docker-compose.local.yml b/docker-compose.yml similarity index 100% rename from docker-compose.local.yml rename to docker-compose.yml From 5e0a1641a9f9278f8133e2a05620dd72ac21a529 Mon Sep 17 00:00:00 2001 From: Pamoda Dilranga <51948729+pamodaDilranga@users.noreply.github.com> Date: Wed, 17 Jul 2024 12:59:04 +0530 Subject: [PATCH 173/582] Update est-workflow-dev.yml --- .github/workflows/est-workflow-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/est-workflow-dev.yml b/.github/workflows/est-workflow-dev.yml index 34c47304..c5ff0ed4 100644 --- a/.github/workflows/est-workflow-dev.yml +++ b/.github/workflows/est-workflow-dev.yml @@ -43,7 +43,7 @@ jobs: - name: Build and run Docker Compose run: | - docker compose up --build -d + docker compose -f docker-compose.development.yml up --build -d # - name: Check for db_update label # id: check_label From 3f6f37850182700c42ce0fd2110d09fdeabe00aa Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Wed, 17 Jul 2024 17:14:13 +0530 Subject: [PATCH 174/582] Update docker-compose.development.yml --- docker-compose.development.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.development.yml b/docker-compose.development.yml index 6ecc8afa..0e8d2699 100644 --- a/docker-compose.development.yml +++ b/docker-compose.development.yml @@ -3,7 +3,7 @@ services: container_name: ruuter-public image: ruuter environment: - - application.cors.allowedOrigins=https://login.esclassifier-dev.rootcode.software + - application.cors.allowedOrigins=https://login.esclassifier-dev.rootcode.software, https://esclassifier-dev-ruuter.rootcode.software, https://esclassifier-dev.rootcode.software - application.httpCodesAllowList=200,201,202,400,401,403,500 - application.internalRequests.allowedIPs=127.0.0.1 - application.logging.displayRequestContent=true From 761c143850e5622d6da9aeaa763895df143bb96e Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Wed, 17 Jul 2024 19:55:03 +0530 Subject: [PATCH 175/582] ESCLASS-137: implement create dataset group API endpoint --- ...ifier-script-v6-dataset-group-metadata.sql | 32 ++++ DSL/Resql/insert-dataset-group-metadata.sql | 48 +++++ .../POST/classifier/datasetgroup/create.yml | 179 ++++++++++++++++++ 3 files changed, 259 insertions(+) create mode 100644 DSL/Liquibase/changelog/classifier-script-v6-dataset-group-metadata.sql create mode 100644 DSL/Resql/insert-dataset-group-metadata.sql create mode 100644 DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/create.yml diff --git a/DSL/Liquibase/changelog/classifier-script-v6-dataset-group-metadata.sql b/DSL/Liquibase/changelog/classifier-script-v6-dataset-group-metadata.sql new file mode 100644 index 00000000..02390d62 --- /dev/null +++ b/DSL/Liquibase/changelog/classifier-script-v6-dataset-group-metadata.sql @@ -0,0 +1,32 @@ +-- liquibase formatted sql + +-- changeset kalsara Magamage:classifier-script-v6-changeset1 +CREATE TYPE Validation_Status AS ENUM ('success', 'fail', 'in-progress'); + +-- changeset kalsara Magamage:classifier-script-v6-changeset3 +CREATE TABLE dataset_group_metadata ( + id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY, + group_name TEXT NOT NULL, + major_version INT NOT NULL DEFAULT 0, + minor_version INT NOT NULL DEFAULT 0, + patch_version INT NOT NULL DEFAULT 0, + latest BOOLEAN DEFAULT false, + is_enabled BOOLEAN DEFAULT false, + enable_allowed BOOLEAN DEFAULT false, + last_model_trained TEXT, + created_timestamp TIMESTAMP WITH TIME ZONE, + last_updated_timestamp TIMESTAMP WITH TIME ZONE, + last_used_for_training TIMESTAMP WITH TIME ZONE, + validation_status Validation_Status, + validation_errors JSONB, + processed_data_available BOOLEAN DEFAULT false, + raw_data_available BOOLEAN DEFAULT false, + num_samples INT, + num_pages INT, + raw_data_location TEXT, + preprocess_data_location TEXT, + validation_criteria JSONB, + class_hierarchy JSONB, + connected_models JSONB, + CONSTRAINT dataset_group_metadata_pkey PRIMARY KEY (id) +); \ No newline at end of file diff --git a/DSL/Resql/insert-dataset-group-metadata.sql b/DSL/Resql/insert-dataset-group-metadata.sql new file mode 100644 index 00000000..af6db723 --- /dev/null +++ b/DSL/Resql/insert-dataset-group-metadata.sql @@ -0,0 +1,48 @@ +INSERT INTO "dataset_group_metadata" ( + group_name, + major_version, + minor_version, + patch_version, + latest, + is_enabled, + enable_allowed, + last_model_trained, + created_timestamp, + last_updated_timestamp, + last_used_for_training, + validation_status, + validation_errors, + processed_data_available, + raw_data_available, + num_samples, + num_pages, + raw_data_location, + preprocess_data_location, + validation_criteria, + class_hierarchy, + connected_models +) VALUES ( + :group_name, + :major_version, + :minor_version, + :patch_version, + :latest, + :is_enabled, + :enable_allowed, + COALESCE(:last_model_trained, null), + to_timestamp(:creation_timestamp)::timestamp with time zone, + to_timestamp(:last_updated_timestamp)::timestamp with time zone, + to_timestamp(:last_used_for_training)::timestamp with time zone, + :validation_status::Validation_Status, + :validation_errors::jsonb, + :processed_data_available, + :raw_data_available, + :num_samples, + :num_pages, + :raw_data_location, + :preprocess_data_location, + :validation_criteria::jsonb, + :class_hierarchy::jsonb, + :connected_models::jsonb +)RETURNING id; + diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/create.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/create.yml new file mode 100644 index 00000000..ad362103 --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/create.yml @@ -0,0 +1,179 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'CREATE'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: group_name + type: string + description: "Body field 'group_name'" + - field: major_version + type: integer + description: "Body field 'major_version'" + - field: minor_version + type: integer + description: "Body field 'minor_version'" + - field: patch_version + type: integer + description: "Body field 'patch_version'" + - field: latest + type: boolean + description: "Body field 'latest'" + - field: is_enabled + type: boolean + description: "Body field 'is_enabled'" + - field: enable_allowed + type: boolean + description: "Body field 'enable_allowed'" + - field: last_model_trained + type: string + description: "Body field 'last_model_trained'" + - field: creation_timestamp + type: number + description: "Body field 'creation_timestamp'" + - field: last_updated_timestamp + type: number + description: "Body field 'last_updated_timestamp'" + - field: last_used_for_training + type: number + description: "Body field 'last_used_for_training'" + - field: validation_status + type: string + description: "Body field 'validation_status'" + - field: validation_errors + type: json + description: "Body field 'validation_errors'" + - field: processed_data_available + type: boolean + description: "Body field 'processed_data_available'" + - field: raw_data_available + type: boolean + description: "Body field 'raw_data_available'" + - field: num_samples + type: integer + description: "Body field 'num_samples'" + - field: num_pages + type: integer + description: "Body field 'num_pages'" + - field: raw_data_location + type: string + description: "Body field 'raw_data_location'" + - field: preprocess_data_location + type: string + description: "Body field 'preprocess_data_location'" + - field: validation_criteria + type: json + description: "Body field 'validation_criteria'" + - field: class_hierarchy + type: json + description: "Body field 'class_hierarchy'" + - field: connected_models + type: array + description: "Body field 'connected_models'" + headers: + - field: cookie + type: string + description: "Cookie field" + +extract_request_data: + assign: + group_name: ${incoming.body.group_name} + major_version: ${incoming.body.major_version} + minor_version: ${incoming.body.minor_version} + patch_version: ${incoming.body.patch_version} + latest: ${incoming.body.latest === null ? false :incoming.body.latest} + is_enabled: ${incoming.body.is_enabled === null ? false :incoming.body.is_enabled} + enable_allowed: ${incoming.body.enable_allowed === null ? false :incoming.body.enable_allowed} + last_model_trained: ${incoming.body.last_model_trained === null ? '' :incoming.body.last_model_trained } + creation_timestamp: ${incoming.body.creation_timestamp} + last_updated_timestamp: ${incoming.body.last_updated_timestamp} + last_used_for_training: ${incoming.body.last_used_for_training} + validation_status: ${incoming.body.validation_status} + validation_errors: ${incoming.body.validation_errors} + processed_data_available: ${incoming.body.processed_data_available === null ? false :incoming.body.processed_data_available } + raw_data_available: ${incoming.body.raw_data_available === null ? false :incoming.body.raw_data_available} + num_samples: ${incoming.body.num_samples} + num_pages: ${incoming.body.num_pages} + raw_data_location: ${incoming.body.raw_data_location} + preprocess_data_location: ${incoming.body.preprocess_data_location} + validation_criteria: ${incoming.body.validation_criteria} + class_hierarchy: ${incoming.body.class_hierarchy} + connected_models: ${incoming.body.connected_models} + next: check_for_request_data + +check_for_request_data: + switch: + - condition: ${group_name !== null || validation_criteria !=null || class_hierarchy !==null } + next: create_dataset_group_metadata + next: return_incorrect_request + +create_dataset_group_metadata: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/insert-dataset-group-metadata" + body: + group_name: ${group_name} + major_version: ${major_version} + minor_version: ${minor_version} + patch_version: ${patch_version} + latest: ${latest} + is_enabled: ${is_enabled} + enable_allowed: ${enable_allowed} + last_model_trained: ${last_model_trained} + creation_timestamp: ${creation_timestamp} + last_updated_timestamp: ${last_updated_timestamp} + last_used_for_training: ${last_used_for_training} + validation_status: ${validation_status} + validation_errors: ${JSON.stringify(validation_errors)} + processed_data_available: ${processed_data_available} + raw_data_available: ${raw_data_available} + num_samples: ${num_samples} + num_pages: ${num_pages} + raw_data_location: ${raw_data_location} + preprocess_data_location: ${preprocess_data_location} + validation_criteria: ${JSON.stringify(validation_criteria)} + class_hierarchy: ${JSON.stringify(class_hierarchy)} + connected_models: ${JSON.stringify(connected_models)} + result: res_dataset + next: check_status + +check_status: + switch: + - condition: ${200 <= res_dataset.response.statusCodeValue && res_dataset.response.statusCodeValue < 300} + next: assign_success_response + next: assign_fail_response + +assign_success_response: + assign: + format_res: { + dg_id: '${res_dataset.response.body[0].id}', + operation_successful: true, + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + dg_id: '', + operation_successful: false, + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end + +return_incorrect_request: + status: 400 + return: 'Missing Required Fields' + next: end From e4a9eb5bdc3f1b77c73745a669239e642da2dcd4 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Thu, 18 Jul 2024 00:38:05 +0530 Subject: [PATCH 176/582] ESCLASS-137: implement get overview and update status endpoints --- DSL/Resql/disable-dataset-group.sql | 3 + DSL/Resql/enable-dataset-group.sql | 3 + .../get-dataset-group-metadata-by-id.sql | 2 + .../get-paginated-dataset-group-metadata.sql | 15 ++ .../POST/classifier/datasetgroup/overview.yml | 63 ++++++++ .../classifier/datasetgroup/update/status.yml | 141 ++++++++++++++++++ 6 files changed, 227 insertions(+) create mode 100644 DSL/Resql/disable-dataset-group.sql create mode 100644 DSL/Resql/enable-dataset-group.sql create mode 100644 DSL/Resql/get-dataset-group-metadata-by-id.sql create mode 100644 DSL/Resql/get-paginated-dataset-group-metadata.sql create mode 100644 DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/overview.yml create mode 100644 DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/status.yml diff --git a/DSL/Resql/disable-dataset-group.sql b/DSL/Resql/disable-dataset-group.sql new file mode 100644 index 00000000..d200592f --- /dev/null +++ b/DSL/Resql/disable-dataset-group.sql @@ -0,0 +1,3 @@ +UPDATE dataset_group_metadata +SET is_enabled = false +WHERE id =:id; \ No newline at end of file diff --git a/DSL/Resql/enable-dataset-group.sql b/DSL/Resql/enable-dataset-group.sql new file mode 100644 index 00000000..fc271300 --- /dev/null +++ b/DSL/Resql/enable-dataset-group.sql @@ -0,0 +1,3 @@ +UPDATE dataset_group_metadata +SET is_enabled = true +WHERE id =:id; \ No newline at end of file diff --git a/DSL/Resql/get-dataset-group-metadata-by-id.sql b/DSL/Resql/get-dataset-group-metadata-by-id.sql new file mode 100644 index 00000000..78a35388 --- /dev/null +++ b/DSL/Resql/get-dataset-group-metadata-by-id.sql @@ -0,0 +1,2 @@ +SELECT enable_allowed +FROM dataset_group_metadata WHERE id =:id; \ No newline at end of file diff --git a/DSL/Resql/get-paginated-dataset-group-metadata.sql b/DSL/Resql/get-paginated-dataset-group-metadata.sql new file mode 100644 index 00000000..0701d596 --- /dev/null +++ b/DSL/Resql/get-paginated-dataset-group-metadata.sql @@ -0,0 +1,15 @@ +SELECT dt.id as dg_id, + dt.group_name, + dt.major_version, + dt.minor_version, + dt.patch_version, + dt.latest, + dt.is_enabled, + dt.created_timestamp, + dt.last_updated_timestamp, + dt.last_used_for_training, + dt.validation_status, + CEIL(COUNT(*) OVER() / :page_size::DECIMAL) AS total_pages +FROM "dataset_group_metadata" dt +ORDER BY CASE WHEN :sorting = 'name asc' THEN dt.group_name END ASC +OFFSET ((GREATEST(:page, 1) - 1) * :page_size) LIMIT :page_size; diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/overview.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/overview.yml new file mode 100644 index 00000000..df4b3b57 --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/overview.yml @@ -0,0 +1,63 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'OVERVIEW'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: page + type: number + description: "Body field 'page'" + - field: page_size + type: number + description: "Body field 'page_size'" + - field: sorting + type: string + description: "Body field 'sorting'" + +get_dataset_meta_data_overview: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-paginated-dataset-group-metadata" + body: + page: ${incoming.body.page} + page_size: ${incoming.body.page_size} + sorting: ${incoming.body.sorting} + result: res_dataset + next: check_status + +check_status: + switch: + - condition: ${200 <= res_dataset.response.statusCodeValue && res_dataset.response.statusCodeValue < 300} + next: assign_success_response + next: assign_fail_response + +assign_success_response: + assign: + format_res: { + operation_successful: true, + data: '${res_dataset.response.body}' + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + operation_successful: false, + data: '${[]}' + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end + diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/status.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/status.yml new file mode 100644 index 00000000..50ab8590 --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/status.yml @@ -0,0 +1,141 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'STATUS'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: dg_id + type: string + description: "Body field 'dg_id'" + - field: operation_type + type: string + description: "Body field 'operation_type'" + +extract_request_data: + assign: + id: ${incoming.body.dg_id} + operation_type: ${incoming.body.operation_type} + next: get_dataset_group + +get_dataset_group: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-dataset-group-metadata-by-id" + body: + id: ${id} + result: res + next: check_status + +check_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: check_data_exist + next: assign_not_found_response + +check_data_exist: + switch: + - condition: ${res.response.body.length>0} + next: check_operation_type + next: assign_not_found_response + +check_operation_type: + switch: + - condition: ${operation_type === 'enable'} + next: validate_dataset_group + - condition: ${operation_type === 'disable'} + next: disable_dataset_group + next: operation_not_support + +validate_dataset_group: + switch: + - condition: ${res.response.body[0].enableAllowed === true} + next: enable_dataset_group + next: assign_not_allowed_response + +enable_dataset_group: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/enable-dataset-group" + body: + id: ${id} + result: res + next: check_enable_disable_status + +disable_dataset_group: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/disable-dataset-group" + body: + id: ${id} + result: res + next: check_enable_disable_status + +check_enable_disable_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: assign_success_response + next: assign_status_update_error_response + +assign_success_response: + assign: + format_res: { + dg_id: '${id}', + operation_type: '${operation_type}', + operation_successful: true, + error_response: "" + } + next: return_ok + +assign_not_allowed_response: + assign: + format_res: { + dg_id: '${id}', + operation_type: '${operation_type}', + operation_successful: false, + error_response: "This dataset is not ready to be enabled" + } + next: return_not_allowed + +assign_not_found_response: + assign: + format_res: { + dg_id: '${id}', + operation_type: '${operation_type}', + operation_successful: false, + error_response: "dataset doesn't exist" + } + next: return_not_found + +assign_status_update_error_response: + assign: + format_res: { + dg_id: '${id}', + operation_type: '${operation_type}', + operation_successful: false, + error_response: "Dataset group status not updated" + } + next: return_not_found + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_not_found: + status: 400 + return: ${format_res} + next: end + +return_not_allowed: + status: 400 + return: ${format_res} + next: end + +operation_not_support: + status: 400 + return: "Bad Request-Operation not support" + next: end \ No newline at end of file From ed97649abf6570213e85dfa250a6a31eb4a41450 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Thu, 18 Jul 2024 13:19:03 +0530 Subject: [PATCH 177/582] ESCLASS-137: refactor get endpoint --- .../classifier/datasetgroup/overview.yml | 22 ++++++++++------ docker-compose.yml | 26 +++++++++---------- 2 files changed, 27 insertions(+), 21 deletions(-) rename DSL/Ruuter.private/DSL/{POST => GET}/classifier/datasetgroup/overview.yml (72%) diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/overview.yml b/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/overview.yml similarity index 72% rename from DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/overview.yml rename to DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/overview.yml index df4b3b57..953b947d 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/overview.yml +++ b/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/overview.yml @@ -2,30 +2,36 @@ declaration: call: declare version: 0.1 description: "Description placeholder for 'OVERVIEW'" - method: post + method: get accepts: json returns: json namespace: classifier allowlist: - body: + params: - field: page type: number - description: "Body field 'page'" + description: "Parameter 'page'" - field: page_size type: number - description: "Body field 'page_size'" + description: "Parameter field 'page_size'" - field: sorting type: string - description: "Body field 'sorting'" + description: "Parameter field 'sorting'" + +extract_data: + assign: + page: ${Number(incoming.params.page)} + page_size: ${Number(incoming.params.page_size)} + next: get_dataset_meta_data_overview get_dataset_meta_data_overview: call: http.post args: url: "[#CLASSIFIER_RESQL]/get-paginated-dataset-group-metadata" body: - page: ${incoming.body.page} - page_size: ${incoming.body.page_size} - sorting: ${incoming.body.sorting} + page: ${page} + page_size: ${page_size} + sorting: ${incoming.params.sorting} result: res_dataset next: check_status diff --git a/docker-compose.yml b/docker-compose.yml index b174b2fb..93a13890 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -158,19 +158,19 @@ services: - bykstack restart: always - # cron-manager: - # container_name: cron-manager - # image: cron-manager - # volumes: - # - ./DSL/CronManager/DSL:/DSL - # - ./DSL/CronManager/script:/app/scripts - # - ./DSL/CronManager/config:/app/config - # environment: - # - server.port=9010 - # ports: - # - 9010:8080 - # networks: - # - bykstack + cron-manager: + container_name: cron-manager + image: cron-manager + volumes: + - ./DSL/CronManager/DSL:/DSL + - ./DSL/CronManager/script:/app/scripts + - ./DSL/CronManager/config:/app/config + environment: + - server.port=9010 + ports: + - 9010:8080 + networks: + - bykstack networks: bykstack: From bf8e2ac1bb5f58a8c9ec26938f223d97ce0aa78c Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Thu, 18 Jul 2024 14:35:24 +0530 Subject: [PATCH 178/582] ESCLASS-137: implement get filters API for dataset group overview --- DSL/Resql/get-dataset-group-filters.sql | 20 +++++++++++ .../datasetgroup/overview/filters.yml | 36 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 DSL/Resql/get-dataset-group-filters.sql create mode 100644 DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/overview/filters.yml diff --git a/DSL/Resql/get-dataset-group-filters.sql b/DSL/Resql/get-dataset-group-filters.sql new file mode 100644 index 00000000..fa3f4341 --- /dev/null +++ b/DSL/Resql/get-dataset-group-filters.sql @@ -0,0 +1,20 @@ +SELECT json_build_object( + 'dg_names', dg_names, + 'dg_versions', dg_versions, + 'dg_validation_statuses', dg_validation_statuses +) +FROM ( + SELECT + array_agg(DISTINCT group_name) AS dg_names, + array_agg(DISTINCT + major_version::TEXT || '.x.x' + ) FILTER (WHERE major_version > 0) || + array_agg(DISTINCT + 'x.' || minor_version::TEXT || '.x' + ) FILTER (WHERE minor_version > 0) || + array_agg(DISTINCT + 'x.x.' || patch_version::TEXT + ) FILTER (WHERE patch_version > 0) AS dg_versions, + array_agg(DISTINCT validation_status) AS dg_validation_statuses + FROM dataset_group_metadata +) AS subquery; \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/overview/filters.yml b/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/overview/filters.yml new file mode 100644 index 00000000..fec6a961 --- /dev/null +++ b/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/overview/filters.yml @@ -0,0 +1,36 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'FILTERS'" + method: get + accepts: json + returns: json + namespace: classifier + +get_dataset_group_filters: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-dataset-group-filters" + result: res_filters + next: check_status + +check_status: + switch: + - condition: ${200 <= res_filters.response.statusCodeValue && res_filters.response.statusCodeValue < 300} + next: assign_Json_format + next: return_bad_request + +assign_Json_format: + assign: + data: ${JSON.parse(res_filters.response.body[0].jsonBuildObject.value)} + next: return_ok + +return_ok: + status: 200 + return: ${data} + next: end + +return_bad_request: + status: 400 + return: "Bad Request" + next: end \ No newline at end of file From c71a8108f544330e7390a803a174c4b6e239e37a Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Thu, 18 Jul 2024 14:52:39 +0530 Subject: [PATCH 179/582] completed file upload and export endpoints --- docker-compose.yml | 63 ++++++++--- s3-ferry/Dockerfile | 9 ++ s3-ferry/docker-compose.yml | 50 +++++++++ s3-ferry/file_api.py | 204 ++++++++++++++++++++++++++++++++++++ s3-ferry/file_converter.py | 98 +++++++++++++++++ s3-ferry/requirements.txt | 6 ++ 6 files changed, 417 insertions(+), 13 deletions(-) create mode 100644 s3-ferry/Dockerfile create mode 100644 s3-ferry/docker-compose.yml create mode 100644 s3-ferry/file_api.py create mode 100644 s3-ferry/file_converter.py create mode 100644 s3-ferry/requirements.txt diff --git a/docker-compose.yml b/docker-compose.yml index b174b2fb..dff753cf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -158,19 +158,56 @@ services: - bykstack restart: always - # cron-manager: - # container_name: cron-manager - # image: cron-manager - # volumes: - # - ./DSL/CronManager/DSL:/DSL - # - ./DSL/CronManager/script:/app/scripts - # - ./DSL/CronManager/config:/app/config - # environment: - # - server.port=9010 - # ports: - # - 9010:8080 - # networks: - # - bykstack + + init: + image: busybox + command: ["sh", "-c", "chmod -R 777 /shared"] + volumes: + - shared-volume:/shared + + python_file_api: + build: + context: ./s3-ferry + dockerfile: Dockerfile + container_name: python_file_api + volumes: + - shared-volume:/shared + environment: + - UPLOAD_DIRECTORY=/shared + - RUUTER_PRIVATE_URL=http://ruuter-private:8088 + - S3_FERRY_URL=http://s3-ferry:3000/v1/files/copy + ports: + - "8000:8000" + networks: + - bykstack + depends_on: + - init + + s3-ferry: + image: s3-ferry:latest + container_name: s3-ferry + volumes: + - shared-volume:/shared + environment: + - API_CORS_ORIGIN=* + - API_DOCUMENTATION_ENABLED=true + - S3_REGION=${S3_REGION} + - S3_ENDPOINT_URL=${S3_ENDPOINT_URL} + - S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID} + - S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY} + - S3_DATA_BUCKET_NAME=${S3_DATA_BUCKET_NAME} + - S3_DATA_BUCKET_PATH=data/ + - FS_DATA_DIRECTORY_PATH=/shared + ports: + - "3002:3000" + depends_on: + - python_file_api + - init + networks: + - bykstack + +volumes: + shared-volume: networks: bykstack: diff --git a/s3-ferry/Dockerfile b/s3-ferry/Dockerfile new file mode 100644 index 00000000..3843c7a5 --- /dev/null +++ b/s3-ferry/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3.9-slim +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY file_api.py . +COPY file_converter.py . +EXPOSE 8000 +RUN mkdir -p /shared && chmod -R 777 /shared +CMD ["uvicorn", "file_api:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/s3-ferry/docker-compose.yml b/s3-ferry/docker-compose.yml new file mode 100644 index 00000000..ee0772dc --- /dev/null +++ b/s3-ferry/docker-compose.yml @@ -0,0 +1,50 @@ +version: '3.8' + +services: + init: + image: busybox + command: ["sh", "-c", "chmod -R 777 /shared"] + volumes: + - shared-volume:/shared + + receiver: + build: + context: . + dockerfile: Dockerfile + container_name: file-receiver + volumes: + - shared-volume:/shared + environment: + - UPLOAD_DIRECTORY=/shared + ports: + - "8000:8000" + depends_on: + - init + + api: + image: s3-ferry:latest + container_name: s3-ferry + volumes: + - shared-volume:/shared + environment: + - API_CORS_ORIGIN=* + - API_DOCUMENTATION_ENABLED=true + - S3_REGION=eu-west-1 + - S3_ENDPOINT_URL=https://s3.eu-west-1.amazonaws.com + - S3_ACCESS_KEY_ID= + - S3_SECRET_ACCESS_KEY= + - S3_DATA_BUCKET_NAME=esclassifier-test + - S3_DATA_BUCKET_PATH=data/ + - FS_DATA_DIRECTORY_PATH=/shared + ports: + - "3000:3000" + depends_on: + - receiver + - init + +volumes: + shared-volume: + +networks: + default: + driver: bridge diff --git a/s3-ferry/file_api.py b/s3-ferry/file_api.py new file mode 100644 index 00000000..b4f19b9d --- /dev/null +++ b/s3-ferry/file_api.py @@ -0,0 +1,204 @@ +from fastapi import FastAPI, File, UploadFile, HTTPException, Form, Request +from fastapi.responses import FileResponse, JSONResponse +import os +import requests +from file_converter import FileConverter +import json +from pydantic import BaseModel + +app = FastAPI() + +UPLOAD_DIRECTORY = os.getenv("UPLOAD_DIRECTORY", "/shared") +RUUTER_PRIVATE_URL = os.getenv("RUUTER_PRIVATE_URL") +S3_FERRY_URL = os.getenv("S3_FERRY_URL") + +class ExportFile(BaseModel): + dg_id: int + version: str + export_type: str + +if not os.path.exists(UPLOAD_DIRECTORY): + os.makedirs(UPLOAD_DIRECTORY) + +def get_ruuter_private_url(): + return os.getenv("RUUTER_PRIVATE_URL") + +async def authenticate_user(request: Request): + cookie = request.cookies.get("customJwtCookie") + if not cookie: + raise HTTPException(status_code=401, detail="No cookie found in the request") + + url = f"{RUUTER_PRIVATE_URL}/auth/jwt/userinfo" + headers = { + 'cookie': f'customJwtCookie={cookie}' + } + + response = requests.get(url, headers=headers) + if response.status_code != 200: + raise HTTPException(status_code=response.status_code, detail="Authentication failed") + +@app.post("/datasetgroup/data/import") +async def upload_and_copy(request: Request, dg_id: int = Form(...), import_type: str = Form(...), data_file: UploadFile = File(...)): + await authenticate_user(request) + if import_type not in ["major","minor"]: + raise HTTPException( + status_code=500, + detail={ + "upload_status": 500, + "operation_successful": False, + "saved_file_path": None, + "reason" : "import_type should be either minor or major." + } + ) + + file_location = os.path.join(UPLOAD_DIRECTORY, data_file.filename) + file_name = data_file.filename + with open(file_location, "wb") as f: + f.write(data_file.file.read()) + + file_converter = FileConverter() + success, converted_data = file_converter.convert_to_json(file_location) + if not success: + raise HTTPException( + status_code=500, + detail={ + "upload_status": 500, + "operation_successful": False, + "saved_file_path": None, + "reason" : "Json file convert failed." + } + ) + + json_local_file_path = file_location.replace('.yaml', '.json').replace('.yml', '.json').replace('.xlsx', ".json") + with open(json_local_file_path, 'w') as json_file: + json.dump(converted_data, json_file, indent=4) + + if import_type == "minor": + save_location = f"/dataset/{dg_id}/minor_update_temp/minor_update_.json" + elif import_type == "major": + save_location = f"/dataset/{dg_id}/primary_dataset/dataset_{dg_id}_aggregated.json" + else: + raise HTTPException( + status_code=500, + detail={ + "upload_status": 500, + "operation_successful": False, + "saved_file_path": None, + "reason" : "import_type should be either minor or major." + } + ) + + payload = { + "destinationFilePath": save_location, + "destinationStorageType": "S3", + "sourceFilePath": file_name.replace('.yml', '.json').replace('.xlsx', ".json"), + "sourceStorageType": "FS" + } + + response = requests.post(S3_FERRY_URL, json=payload) + if response.status_code == 201: + os.remove(file_location) + if(file_location!=json_local_file_path): + os.remove(json_local_file_path) + response_data = { + "upload_status": 200, + "operation_successful": True, + "saved_file_path": save_location + } + return JSONResponse(status_code=200, content=response_data) + else: + raise HTTPException( + status_code=500, + detail={ + "upload_status": 500, + "operation_successful": False, + "saved_file_path": None, + "reason" : "Failed to upload to S3" + } + ) + + +@app.post("/datasetgroup/data/download") +async def download_and_convert(request: Request, export_data: ExportFile): + await authenticate_user(request) + dg_id = export_data.dg_id + version = export_data.version + export_type = export_data.export_type + + if export_type not in ["xlsx", "yaml", "json"]: + raise HTTPException( + status_code=500, + detail={ + "upload_status": 500, + "operation_successful": False, + "saved_file_path": None, + "reason": "export_type should be either json, xlsx or yaml." + } + ) + + if version == "minor": + save_location = f"/dataset/{dg_id}/minor_update_temp/minor_update_.json" + local_file_name = f"group_{dg_id}minor_update" + + elif version == "major": + save_location = f"/dataset/{dg_id}/primary_dataset/dataset_{dg_id}_aggregated.json" + local_file_name = f"group_{dg_id}_aggregated" + else: + raise HTTPException( + status_code=500, + detail={ + "upload_status": 500, + "operation_successful": False, + "saved_file_path": None, + "reason": "import_type should be either minor or major." + } + ) + + payload = { + "destinationFilePath": f"{local_file_name}.json", + "destinationStorageType": "FS", + "sourceFilePath": save_location, + "sourceStorageType": "S3" + } + + response = requests.post(S3_FERRY_URL, json=payload) + if response.status_code != 201: + raise HTTPException( + status_code=500, + detail={ + "upload_status": 500, + "operation_successful": False, + "saved_file_path": None, + "reason": "Failed to download from S3" + } + ) + + shared_directory = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'shared') + json_file_path = os.path.join(shared_directory, f"{local_file_name}.json") + + json_file_path = os.path.join('..', 'shared', f"{local_file_name}.json") + + file_converter = FileConverter() + with open(f"{json_file_path}", 'r') as json_file: + json_data = json.load(json_file) + + if export_type == "xlsx": + output_file = f"{local_file_name}.xlsx" + file_converter.convert_json_to_xlsx(json_data, output_file) + elif export_type == "yaml": + output_file = f"{local_file_name}.yaml" + file_converter.convert_json_to_yaml(json_data, output_file) + elif export_type == "json": + output_file = f"{json_file_path}" + else: + raise HTTPException( + status_code=500, + detail={ + "upload_status": 500, + "operation_successful": False, + "saved_file_path": None, + "reason": "export_type should be either json, xlsx or yaml." + } + ) + + return FileResponse(output_file, filename=os.path.basename(output_file)) \ No newline at end of file diff --git a/s3-ferry/file_converter.py b/s3-ferry/file_converter.py new file mode 100644 index 00000000..18d68309 --- /dev/null +++ b/s3-ferry/file_converter.py @@ -0,0 +1,98 @@ +import os +import json +import yaml +import pandas as pd + +class FileConverter: + def __init__(self): + pass + + def _detect_file_type(self, file_path): + if file_path.endswith('.json'): + return 'json' + elif file_path.endswith('.yaml') or file_path.endswith('.yml'): + return 'yaml' + elif file_path.endswith('.xlsx'): + return 'xlsx' + else: + return None + + def convert_to_json(self, file_path): + file_type = self._detect_file_type(file_path) + if file_type is None: + print(f"Error: Unsupported file type for '{file_path}'") + return (False, {}) + + try: + if file_type == 'json': + return self._load_json(file_path) + elif file_type == 'yaml': + return self._convert_yaml_to_json(file_path) + elif file_type == 'xlsx': + return self._convert_xlsx_to_json(file_path) + except Exception as e: + print(f"Error processing '{file_path}': {e}") + return (False, {}) + + def _load_json(self, file_path): + try: + with open(file_path, 'r') as file: + data = json.load(file) + return (True, data) + except Exception as e: + print(f"Error loading JSON file '{file_path}': {e}") + return (False, {}) + + def _convert_yaml_to_json(self, file_path): + try: + with open(file_path, 'r') as file: + data = yaml.safe_load(file) + return (True, data) + except Exception as e: + print(f"Error converting YAML file '{file_path}' to JSON: {e}") + return (False, {}) + + def _convert_xlsx_to_json(self, file_path): + try: + data = pd.read_excel(file_path, sheet_name=None) + json_data = {sheet: data[sheet].to_dict(orient='records') for sheet in data} + return (True, json_data) + except Exception as e: + print(f"Error converting XLSX file '{file_path}' to JSON: {e}") + return (False, {}) + + def convert_json_to_xlsx(self, json_data, output_path): + try: + with pd.ExcelWriter(output_path) as writer: + for sheet_name, data in json_data.items(): + df = pd.DataFrame(data) + df.to_excel(writer, sheet_name=sheet_name, index=False) + print(f"JSON data successfully converted to XLSX and saved at '{output_path}'") + return True + except Exception as e: + print(f"Error converting JSON to XLSX: {e}") + return False + + def convert_json_to_yaml(self, json_data, output_path): + try: + with open(output_path, 'w') as file: + yaml.dump(json_data, file) + print(f"JSON data successfully converted to YAML and saved at '{output_path}'") + return True + except Exception as e: + print(f"Error converting JSON to YAML: {e}") + return False + +if __name__ == "__main__": + converter = FileConverter() + + # Convert files to JSON + file_paths = ['example.json', 'example.yaml', 'example.xlsx', 'example.txt'] + for file_path in file_paths: + success, json_data = converter.convert_to_json(file_path) + if success: + print(f"JSON data for '{file_path}':\n{json.dumps(json_data, indent=4)}\n") + + if json_data: + converter.convert_json_to_xlsx(json_data, 'output.xlsx') + converter.convert_json_to_yaml(json_data, 'output.yaml') diff --git a/s3-ferry/requirements.txt b/s3-ferry/requirements.txt new file mode 100644 index 00000000..67f21bb0 --- /dev/null +++ b/s3-ferry/requirements.txt @@ -0,0 +1,6 @@ +fastapi +uvicorn[standard] +aiofiles +requests +pandas +openpyxl From 03fed197645d455d7afcf091be5facdcd1cfaa91 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Thu, 18 Jul 2024 15:37:58 +0530 Subject: [PATCH 180/582] ESCLASS-137: add filters to get dataset overview API --- .../get-paginated-dataset-group-metadata.sql | 10 +++++- .../GET/classifier/datasetgroup/overview.yml | 31 +++++++++++++++++-- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/DSL/Resql/get-paginated-dataset-group-metadata.sql b/DSL/Resql/get-paginated-dataset-group-metadata.sql index 0701d596..74db5be4 100644 --- a/DSL/Resql/get-paginated-dataset-group-metadata.sql +++ b/DSL/Resql/get-paginated-dataset-group-metadata.sql @@ -11,5 +11,13 @@ SELECT dt.id as dg_id, dt.validation_status, CEIL(COUNT(*) OVER() / :page_size::DECIMAL) AS total_pages FROM "dataset_group_metadata" dt -ORDER BY CASE WHEN :sorting = 'name asc' THEN dt.group_name END ASC +WHERE + (:major_version = -1 OR dt.major_version = :major_version) + AND (:minor_version = -1 OR dt.minor_version = :minor_version) + AND (:patch_version = -1 OR dt.patch_version = :patch_version) + AND (:validation_status = 'all' OR dt.validation_status = :validation_status::Validation_Status) + AND (:group_name = 'all' OR dt.group_name = :group_name) +ORDER BY + CASE WHEN :sorting = 'asc' THEN dt.group_name END ASC, + CASE WHEN :sorting = 'dsc' THEN dt.group_name END DESC OFFSET ((GREATEST(:page, 1) - 1) * :page_size) LIMIT :page_size; diff --git a/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/overview.yml b/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/overview.yml index 953b947d..9c6fbed7 100644 --- a/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/overview.yml +++ b/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/overview.yml @@ -14,14 +14,34 @@ declaration: - field: page_size type: number description: "Parameter field 'page_size'" - - field: sorting + - field: sort_type type: string - description: "Parameter field 'sorting'" + description: "Parameter field 'sort_type'" + - field: major_version + type: string + description: "Parameter field 'major_version'" + - field: minor_version + type: string + description: "Parameter field 'minor_version'" + - field: patch_version + type: string + description: "Parameter field 'patch_version'" + - field: group_name + type: string + description: "Parameter field 'group_name'" + - field: validation_status + type: string + description: "Parameter field 'validation_status'" extract_data: assign: page: ${Number(incoming.params.page)} page_size: ${Number(incoming.params.page_size)} + major_version: ${Number(incoming.params.major_version)} + minor_version: ${Number(incoming.params.minor_version)} + patch_version: ${Number(incoming.params.patch_version)} + group_name: ${incoming.params.group_name} + validation_status: ${incoming.params.validation_status} next: get_dataset_meta_data_overview get_dataset_meta_data_overview: @@ -31,7 +51,12 @@ get_dataset_meta_data_overview: body: page: ${page} page_size: ${page_size} - sorting: ${incoming.params.sorting} + sorting: ${incoming.params.sort_type} + major_version: ${major_version} + minor_version: ${minor_version} + patch_version: ${patch_version} + group_name: ${group_name} + validation_status: ${validation_status} result: res_dataset next: check_status From b74f8d4c7ca27a8575a55b19fbed88c246268844 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Thu, 18 Jul 2024 19:03:30 +0530 Subject: [PATCH 181/582] ESCLASS-137: change request and response body attribute to camel notation --- DSL/Resql/get-dataset-group-filters.sql | 12 +- .../get-paginated-dataset-group-metadata.sql | 2 +- .../GET/classifier/datasetgroup/overview.yml | 49 +++---- .../POST/classifier/datasetgroup/create.yml | 134 +++++++++--------- .../classifier/datasetgroup/update/status.yml | 44 +++--- .../DSL/TEMPLATES/check-user-authority.yml | 2 + 6 files changed, 123 insertions(+), 120 deletions(-) diff --git a/DSL/Resql/get-dataset-group-filters.sql b/DSL/Resql/get-dataset-group-filters.sql index fa3f4341..41d807e6 100644 --- a/DSL/Resql/get-dataset-group-filters.sql +++ b/DSL/Resql/get-dataset-group-filters.sql @@ -1,11 +1,11 @@ SELECT json_build_object( - 'dg_names', dg_names, - 'dg_versions', dg_versions, - 'dg_validation_statuses', dg_validation_statuses + 'dgNames', dgNames, + 'dgVersions', dgVersions, + 'dgValidationStatuses', dgValidationStatuses ) FROM ( SELECT - array_agg(DISTINCT group_name) AS dg_names, + array_agg(DISTINCT group_name) AS dgNames, array_agg(DISTINCT major_version::TEXT || '.x.x' ) FILTER (WHERE major_version > 0) || @@ -14,7 +14,7 @@ FROM ( ) FILTER (WHERE minor_version > 0) || array_agg(DISTINCT 'x.x.' || patch_version::TEXT - ) FILTER (WHERE patch_version > 0) AS dg_versions, - array_agg(DISTINCT validation_status) AS dg_validation_statuses + ) FILTER (WHERE patch_version > 0) AS dgVersions, + array_agg(DISTINCT validation_status) AS dgValidationStatuses FROM dataset_group_metadata ) AS subquery; \ No newline at end of file diff --git a/DSL/Resql/get-paginated-dataset-group-metadata.sql b/DSL/Resql/get-paginated-dataset-group-metadata.sql index 74db5be4..e856af03 100644 --- a/DSL/Resql/get-paginated-dataset-group-metadata.sql +++ b/DSL/Resql/get-paginated-dataset-group-metadata.sql @@ -1,4 +1,4 @@ -SELECT dt.id as dg_id, +SELECT dt.id, dt.group_name, dt.major_version, dt.minor_version, diff --git a/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/overview.yml b/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/overview.yml index 9c6fbed7..ae3324a2 100644 --- a/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/overview.yml +++ b/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/overview.yml @@ -10,38 +10,39 @@ declaration: params: - field: page type: number - description: "Parameter 'page'" - - field: page_size + description: "Parameter 'page'" + - field: pageSize type: number - description: "Parameter field 'page_size'" - - field: sort_type + description: "Parameter field 'pageSize'" + - field: sortType type: string - description: "Parameter field 'sort_type'" - - field: major_version + description: "Parameter field 'sortType'" + - field: majorVersion type: string - description: "Parameter field 'major_version'" - - field: minor_version + description: "Parameter field 'majorVersion'" + - field: minorVersion type: string - description: "Parameter field 'minor_version'" - - field: patch_version + description: "Parameter field 'minorVersion'" + - field: patchVersion type: string - description: "Parameter field 'patch_version'" - - field: group_name + description: "Parameter field 'patchVersion'" + - field: groupName type: string - description: "Parameter field 'group_name'" - - field: validation_status + description: "Parameter field 'groupName'" + - field: validationStatus type: string - description: "Parameter field 'validation_status'" + description: "Parameter field 'validationStatus'" extract_data: assign: page: ${Number(incoming.params.page)} - page_size: ${Number(incoming.params.page_size)} - major_version: ${Number(incoming.params.major_version)} - minor_version: ${Number(incoming.params.minor_version)} - patch_version: ${Number(incoming.params.patch_version)} - group_name: ${incoming.params.group_name} - validation_status: ${incoming.params.validation_status} + page_size: ${Number(incoming.params.pageSize)} + major_version: ${Number(incoming.params.majorVersion)} + minor_version: ${Number(incoming.params.minorVersion)} + patch_version: ${Number(incoming.params.patchVersion)} + group_name: ${incoming.params.groupName} + validation_status: ${incoming.params.validationStatus} + sort_type: ${incoming.params.sortType} next: get_dataset_meta_data_overview get_dataset_meta_data_overview: @@ -51,7 +52,7 @@ get_dataset_meta_data_overview: body: page: ${page} page_size: ${page_size} - sorting: ${incoming.params.sort_type} + sorting: ${sort_type} major_version: ${major_version} minor_version: ${minor_version} patch_version: ${patch_version} @@ -69,7 +70,7 @@ check_status: assign_success_response: assign: format_res: { - operation_successful: true, + operationSuccessful: true, data: '${res_dataset.response.body}' } next: return_ok @@ -77,7 +78,7 @@ assign_success_response: assign_fail_response: assign: format_res: { - operation_successful: false, + operationSuccessful: false, data: '${[]}' } next: return_bad_request diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/create.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/create.yml index ad362103..2762816c 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/create.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/create.yml @@ -8,72 +8,72 @@ declaration: namespace: classifier allowlist: body: - - field: group_name + - field: groupName type: string - description: "Body field 'group_name'" - - field: major_version + description: "Body field 'groupName'" + - field: majorVersion type: integer - description: "Body field 'major_version'" - - field: minor_version + description: "Body field 'majorVersion'" + - field: minorVersion type: integer - description: "Body field 'minor_version'" - - field: patch_version + description: "Body field 'minorVersion'" + - field: patchVersion type: integer - description: "Body field 'patch_version'" + description: "Body field 'patchVersion'" - field: latest type: boolean description: "Body field 'latest'" - - field: is_enabled + - field: isEnabled type: boolean - description: "Body field 'is_enabled'" - - field: enable_allowed + description: "Body field 'isEnabled'" + - field: enableAllowed type: boolean - description: "Body field 'enable_allowed'" - - field: last_model_trained + description: "Body field 'enableAllowed'" + - field: lastModelTrained type: string - description: "Body field 'last_model_trained'" - - field: creation_timestamp + description: "Body field 'lastModelTrained'" + - field: creationTimestamp type: number - description: "Body field 'creation_timestamp'" - - field: last_updated_timestamp + description: "Body field 'creationTimestamp'" + - field: lastUpdatedTimestamp type: number - description: "Body field 'last_updated_timestamp'" - - field: last_used_for_training + description: "Body field 'lastUpdatedTimestamp'" + - field: lastUsedForTraining type: number - description: "Body field 'last_used_for_training'" - - field: validation_status + description: "Body field 'lastUsedForTraining'" + - field: validationStatus type: string - description: "Body field 'validation_status'" - - field: validation_errors + description: "Body field 'validationStatus'" + - field: validationErrors type: json - description: "Body field 'validation_errors'" - - field: processed_data_available + description: "Body field 'validationErrors'" + - field: processedDataAvailable type: boolean - description: "Body field 'processed_data_available'" - - field: raw_data_available + description: "Body field 'processedDataAvailable'" + - field: rawDataAvailable type: boolean - description: "Body field 'raw_data_available'" - - field: num_samples + description: "Body field 'rawDataAvailable'" + - field: numSamples type: integer - description: "Body field 'num_samples'" - - field: num_pages + description: "Body field 'numSamples'" + - field: numPages type: integer - description: "Body field 'num_pages'" - - field: raw_data_location + description: "Body field 'numPages'" + - field: rawDataLocation type: string - description: "Body field 'raw_data_location'" - - field: preprocess_data_location + description: "Body field 'rawDataLocation'" + - field: preprocessDataLocation type: string - description: "Body field 'preprocess_data_location'" - - field: validation_criteria + description: "Body field 'preprocessDataLocation'" + - field: validationCriteria type: json - description: "Body field 'validation_criteria'" - - field: class_hierarchy + description: "Body field 'validationCriteria'" + - field: classHierarchy type: json - description: "Body field 'class_hierarchy'" - - field: connected_models + description: "Body field 'classHierarchy'" + - field: connectedModels type: array - description: "Body field 'connected_models'" + description: "Body field 'connectedModels'" headers: - field: cookie type: string @@ -81,28 +81,28 @@ declaration: extract_request_data: assign: - group_name: ${incoming.body.group_name} - major_version: ${incoming.body.major_version} - minor_version: ${incoming.body.minor_version} - patch_version: ${incoming.body.patch_version} + group_name: ${incoming.body.groupName} + major_version: ${incoming.body.majorVersion} + minor_version: ${incoming.body.minorVersion} + patch_version: ${incoming.body.patchVersion} latest: ${incoming.body.latest === null ? false :incoming.body.latest} - is_enabled: ${incoming.body.is_enabled === null ? false :incoming.body.is_enabled} - enable_allowed: ${incoming.body.enable_allowed === null ? false :incoming.body.enable_allowed} - last_model_trained: ${incoming.body.last_model_trained === null ? '' :incoming.body.last_model_trained } - creation_timestamp: ${incoming.body.creation_timestamp} - last_updated_timestamp: ${incoming.body.last_updated_timestamp} - last_used_for_training: ${incoming.body.last_used_for_training} - validation_status: ${incoming.body.validation_status} - validation_errors: ${incoming.body.validation_errors} - processed_data_available: ${incoming.body.processed_data_available === null ? false :incoming.body.processed_data_available } - raw_data_available: ${incoming.body.raw_data_available === null ? false :incoming.body.raw_data_available} - num_samples: ${incoming.body.num_samples} - num_pages: ${incoming.body.num_pages} - raw_data_location: ${incoming.body.raw_data_location} - preprocess_data_location: ${incoming.body.preprocess_data_location} - validation_criteria: ${incoming.body.validation_criteria} - class_hierarchy: ${incoming.body.class_hierarchy} - connected_models: ${incoming.body.connected_models} + is_enabled: ${incoming.body.isEnabled === null ? false :incoming.body.isEnabled} + enable_allowed: ${incoming.body.enableAllowed === null ? false :incoming.body.enableAllowed} + last_model_trained: ${incoming.body.lastModelTrained === null ? '' :incoming.body.lastModelTrained} + creation_timestamp: ${incoming.body.creationTimestamp} + last_updated_timestamp: ${incoming.body.lastUpdatedTimestamp} + last_used_for_training: ${incoming.body.lastUsedForTraining} + validation_status: ${incoming.body.validationStatus} + validation_errors: ${incoming.body.validationErrors} + processed_data_available: ${incoming.body.processedDataAvailable === null ? false :incoming.body.processedDataAvailable} + raw_data_available: ${incoming.body.rawDataAvailable === null ? false :incoming.body.rawDataAvailable} + num_samples: ${incoming.body.numSamples} + num_pages: ${incoming.body.numPages} + raw_data_location: ${incoming.body.rawDataLocation} + preprocess_data_location: ${incoming.body.preprocessDataLocation} + validation_criteria: ${incoming.body.validationCriteria} + class_hierarchy: ${incoming.body.classHierarchy} + connected_models: ${incoming.body.connectedModels} next: check_for_request_data check_for_request_data: @@ -150,16 +150,16 @@ check_status: assign_success_response: assign: format_res: { - dg_id: '${res_dataset.response.body[0].id}', - operation_successful: true, + dgId: '${res_dataset.response.body[0].id}', + operationSuccessful: true, } next: return_ok assign_fail_response: assign: format_res: { - dg_id: '', - operation_successful: false, + dgId: '', + operationSuccessful: false, } next: return_bad_request diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/status.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/status.yml index 50ab8590..2fd5c114 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/status.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/status.yml @@ -8,17 +8,17 @@ declaration: namespace: classifier allowlist: body: - - field: dg_id + - field: dgId type: string - description: "Body field 'dg_id'" - - field: operation_type + description: "Body field 'dgId'" + - field: operationType type: string - description: "Body field 'operation_type'" + description: "Body field 'operationType'" extract_request_data: assign: - id: ${incoming.body.dg_id} - operation_type: ${incoming.body.operation_type} + id: ${incoming.body.dgId} + operation_type: ${incoming.body.operationType} next: get_dataset_group get_dataset_group: @@ -83,40 +83,40 @@ check_enable_disable_status: assign_success_response: assign: format_res: { - dg_id: '${id}', - operation_type: '${operation_type}', - operation_successful: true, - error_response: "" + dgId: '${id}', + operationType: '${operation_type}', + operationSuccessful: true, + errorResponse: "" } next: return_ok assign_not_allowed_response: assign: format_res: { - dg_id: '${id}', - operation_type: '${operation_type}', - operation_successful: false, - error_response: "This dataset is not ready to be enabled" + dgId: '${id}', + operationType: '${operation_type}', + operationSuccessful: false, + errorResponse: "This dataset is not ready to be enabled" } next: return_not_allowed assign_not_found_response: assign: format_res: { - dg_id: '${id}', - operation_type: '${operation_type}', - operation_successful: false, - error_response: "dataset doesn't exist" + dgId: '${id}', + operationType: '${operation_type}', + operationSuccessful: false, + errorResponse: "dataset doesn't exist" } next: return_not_found assign_status_update_error_response: assign: format_res: { - dg_id: '${id}', - operation_type: '${operation_type}', - operation_successful: false, - error_response: "Dataset group status not updated" + dgId: '${id}', + operationType: '${operation_type}', + operationSuccessful: false, + errorResponse: "Dataset group status not updated" } next: return_not_found diff --git a/DSL/Ruuter.private/DSL/TEMPLATES/check-user-authority.yml b/DSL/Ruuter.private/DSL/TEMPLATES/check-user-authority.yml index ecc54729..496199e6 100644 --- a/DSL/Ruuter.private/DSL/TEMPLATES/check-user-authority.yml +++ b/DSL/Ruuter.private/DSL/TEMPLATES/check-user-authority.yml @@ -31,6 +31,8 @@ check_cookie_info_response: check_user_authority: switch: + - condition: ${res.response.body === null} + next: return_unauthorized - condition: ${res.response.body.authorities.includes("ROLE_ADMINISTRATOR") || res.response.body.authorities.includes("ROLE_MODEL_TRAINER")} next: return_authorized next: return_unauthorized From 64f0395975c83f5f950944ae8cdec9cb69c38577 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Thu, 18 Jul 2024 23:56:04 +0530 Subject: [PATCH 182/582] dataset groups overview and creation --- GUI/src/App.tsx | 2 + GUI/src/components/Card/Card.scss | 2 +- .../FormCheckbox/FormCheckbox.scss | 3 +- .../FormElements/FormInput/FormInput.scss | 6 + .../FormElements/FormInput/index.tsx | 2 +- .../FormElements/FormSelect/FormSelect.scss | 12 +- .../FormElements/FormSelect/index.tsx | 10 +- .../components/FormElements/Switch/index.tsx | 2 +- GUI/src/components/MainNavigation/index.tsx | 5 +- .../molecules/ClassHeirarchy/index.css | 28 +++ .../molecules/ClassHeirarchy/index.tsx | 194 ++++++++++++++++++ .../molecules/DatasetGroupCard/index.tsx | 96 +++++++-- .../components/molecules/Pagination/index.tsx | 40 +--- .../molecules/ValidationCriteria/index.tsx | 168 +++++++++++++++ GUI/src/config/dataTypesConfig.json | 6 + .../DatasetGroups/CreateDatasetGroup.tsx | 173 ++++++++++++++++ .../pages/DatasetGroups/ViewDatasetGroup.tsx | 165 +++++++++++++++ GUI/src/pages/DatasetGroups/index.tsx | 58 +++--- GUI/src/services/datasets.ts | 9 +- GUI/src/services/integration.ts | 4 +- GUI/src/styles/generic/_base.scss | 20 +- GUI/src/types/datasetGroups.ts | 13 ++ GUI/src/utils/datasetGroupsUtils.ts | 87 ++++++++ 23 files changed, 1016 insertions(+), 89 deletions(-) create mode 100644 GUI/src/components/molecules/ClassHeirarchy/index.css create mode 100644 GUI/src/components/molecules/ClassHeirarchy/index.tsx create mode 100644 GUI/src/components/molecules/ValidationCriteria/index.tsx create mode 100644 GUI/src/config/dataTypesConfig.json create mode 100644 GUI/src/pages/DatasetGroups/CreateDatasetGroup.tsx create mode 100644 GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx create mode 100644 GUI/src/types/datasetGroups.ts create mode 100644 GUI/src/utils/datasetGroupsUtils.ts diff --git a/GUI/src/App.tsx b/GUI/src/App.tsx index 484b7513..5b4cfc7d 100644 --- a/GUI/src/App.tsx +++ b/GUI/src/App.tsx @@ -8,6 +8,7 @@ import Integrations from 'pages/Integrations'; import DatasetGroups from 'pages/DatasetGroups'; import { useQuery } from '@tanstack/react-query'; import { UserInfo } from 'types/userInfo'; +import CreateDatasetGroup from 'pages/DatasetGroups/CreateDatasetGroup'; const App: FC = () => { @@ -28,6 +29,7 @@ const App: FC = () => { } /> } /> } /> + } /> diff --git a/GUI/src/components/Card/Card.scss b/GUI/src/components/Card/Card.scss index 88b94c73..82d2665c 100644 --- a/GUI/src/components/Card/Card.scss +++ b/GUI/src/components/Card/Card.scss @@ -31,7 +31,7 @@ &__header { border-bottom: 1px solid get-color(black-coral-2); - background-color: white; + background-color: #F9F9F9; border-radius: $veera-radius-s $veera-radius-s 0 0; &.white { diff --git a/GUI/src/components/FormElements/FormCheckbox/FormCheckbox.scss b/GUI/src/components/FormElements/FormCheckbox/FormCheckbox.scss index 33692e10..2cb0046d 100644 --- a/GUI/src/components/FormElements/FormCheckbox/FormCheckbox.scss +++ b/GUI/src/components/FormElements/FormCheckbox/FormCheckbox.scss @@ -11,12 +11,13 @@ &__label { display: block; - flex: 0 0 185px; + flex: 0 0 85px; font-size: $veera-font-size-100; line-height: 24px; } &__item { + margin-top: 6px; input[type=checkbox] { display: none; diff --git a/GUI/src/components/FormElements/FormInput/FormInput.scss b/GUI/src/components/FormElements/FormInput/FormInput.scss index 461f968f..3d019010 100644 --- a/GUI/src/components/FormElements/FormInput/FormInput.scss +++ b/GUI/src/components/FormElements/FormInput/FormInput.scss @@ -93,4 +93,10 @@ background-color: get-color(black-coral-0); } } + + &--disabled & { + input { + border: solid 1px get-color(jasper-10); + } + } } diff --git a/GUI/src/components/FormElements/FormInput/index.tsx b/GUI/src/components/FormElements/FormInput/index.tsx index b0257753..b5ce3ea9 100644 --- a/GUI/src/components/FormElements/FormInput/index.tsx +++ b/GUI/src/components/FormElements/FormInput/index.tsx @@ -20,7 +20,7 @@ const FormInput = forwardRef( ) => { const id = useId(); - const inputClasses = clsx('input', disabled && 'input--disabled'); + const inputClasses = clsx('input', disabled && 'input--disabled', error && 'input--error'); return (
    diff --git a/GUI/src/components/FormElements/FormSelect/FormSelect.scss b/GUI/src/components/FormElements/FormSelect/FormSelect.scss index fcde774b..d151870d 100644 --- a/GUI/src/components/FormElements/FormSelect/FormSelect.scss +++ b/GUI/src/components/FormElements/FormSelect/FormSelect.scss @@ -9,6 +9,7 @@ align-items: center; gap: get-spacing(paldiski); width: 100%; + &__label { flex: 0 0 185px; @@ -21,6 +22,16 @@ position: relative; } + &__error { + border: 1px solid get-color(jasper-10); + + } + + &__default { + border: 1px solid get-color(black-coral-6); + + } + &__trigger { width: 100%; display: flex; @@ -28,7 +39,6 @@ justify-content: space-between; appearance: none; background-color: get-color(white); - border: 1px solid get-color(black-coral-6); border-radius: $veera-radius-s; color: get-color(black); font-size: $veera-font-size-100; diff --git a/GUI/src/components/FormElements/FormSelect/index.tsx b/GUI/src/components/FormElements/FormSelect/index.tsx index 8d34147a..09fbd4f8 100644 --- a/GUI/src/components/FormElements/FormSelect/index.tsx +++ b/GUI/src/components/FormElements/FormSelect/index.tsx @@ -19,6 +19,7 @@ type FormSelectProps = Partial & SelectHTMLAttributes void; + error?:string } const itemToString = (item: ({ label: string, value: string } | null)) => { @@ -35,6 +36,7 @@ const FormSelect= forwardRef(( placeholder, defaultValue, onSelectionChange, + error, ...rest }, ref @@ -63,8 +65,8 @@ const FormSelect= forwardRef(( const selectClasses = clsx( 'select', - disabled && 'select--disabled', - ); + disabled && 'select--disabled' ); + const placeholderValue = placeholder || t('global.choose'); @@ -72,7 +74,7 @@ const FormSelect= forwardRef((
    {label && !hideLabel && }
    -
    +
    {selectedItem?.label ?? placeholderValue} } />
    @@ -86,6 +88,8 @@ const FormSelect= forwardRef(( )) )} + {error &&

    {error}

    } +
    ); diff --git a/GUI/src/components/FormElements/Switch/index.tsx b/GUI/src/components/FormElements/Switch/index.tsx index b8241974..7cf1b755 100644 --- a/GUI/src/components/FormElements/Switch/index.tsx +++ b/GUI/src/components/FormElements/Switch/index.tsx @@ -14,7 +14,7 @@ type SwitchProps = Partial & { checked?: boolean; defaultChecked?: boolean; hideLabel?: boolean; - onCheckedChange?: (checked: boolean) => void; + onCheckedChange?: (checked: boolean) => void|any; }; const Switch = forwardRef( diff --git a/GUI/src/components/MainNavigation/index.tsx b/GUI/src/components/MainNavigation/index.tsx index 726dd79d..f270bf06 100644 --- a/GUI/src/components/MainNavigation/index.tsx +++ b/GUI/src/components/MainNavigation/index.tsx @@ -22,7 +22,6 @@ import { Icon } from 'components'; import type { MenuItem } from 'types/mainNavigation'; import { menuIcons } from 'constants/menuIcons'; import './MainNavigation.scss'; -import { error } from 'console'; import apiDev from 'services/api-dev'; const MainNavigation: FC = () => { @@ -112,9 +111,7 @@ const MainNavigation: FC = () => { useQuery({ queryKey: ['/accounts/user-role', 'prod'], - onSuccess: (res: any) => { - console.log(res); - + onSuccess: (res: any) => { const filteredItems = items.filter((item) => { const role = res?.response[0]; diff --git a/GUI/src/components/molecules/ClassHeirarchy/index.css b/GUI/src/components/molecules/ClassHeirarchy/index.css new file mode 100644 index 00000000..ccec348d --- /dev/null +++ b/GUI/src/components/molecules/ClassHeirarchy/index.css @@ -0,0 +1,28 @@ +/* GridLayout.css */ +.class-grid { + display: grid; + grid-template-columns: 7fr 3fr 2fr; + gap: 10px; /* Adjust gap as needed */ + padding: 10px; /* Adjust padding as needed */ + } + + .item { + padding: 20px; + background-color: #f0f0f0; + border: 1px solid #ddd; + text-align: center; + } + + /* Optional styling for specific items */ + .item-1 { + background-color: #cce5ff; + } + + .item-2 { + background-color: #d4edda; + } + + .item-3 { + background-color: #f8d7da; + } + \ No newline at end of file diff --git a/GUI/src/components/molecules/ClassHeirarchy/index.tsx b/GUI/src/components/molecules/ClassHeirarchy/index.tsx new file mode 100644 index 00000000..0a517023 --- /dev/null +++ b/GUI/src/components/molecules/ClassHeirarchy/index.tsx @@ -0,0 +1,194 @@ +import React, { FC, PropsWithChildren, useCallback, useState } from 'react'; +import { DndProvider, useDrag, useDrop } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; +import dataTypes from '../../../config/dataTypesConfig.json'; +import { MdDelete, MdDeleteOutline, MdExpand } from 'react-icons/md'; +import Card from 'components/Card'; +import { FormCheckbox, FormInput, FormSelect } from 'components/FormElements'; +import Button from 'components/Button'; +import { v4 as uuidv4 } from 'uuid'; +import './index.css'; +import Dialog from 'components/Dialog'; +import { transformClassHierarchy } from 'utils/datasetGroupsUtils'; +import { Class } from 'types/datasetGroups'; + +type ClassHierarchyProps = { + nodes?: Class[]; + setNodes: React.Dispatch>; + nodesError?: boolean; + setNodesError: React.Dispatch>; +}; + +const ClassHierarchy: FC> = ({ + nodes, + setNodes, + nodesError, + setNodesError, +}) => { + const [currentNode, setCurrentNode] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + + const TreeNode = ({ node, onAddSubClass, onDelete }) => { + const [fieldName, setFieldName] = useState(node.fieldName); + + const handleChange = (e) => { + setFieldName(e.target.value); + node.fieldName = e.target.value; + }; + + return ( +
    + + {node.children && + node.children.map((child) => ( + + ))} +
    + ); + }; + + const addMainClass = () => { + setNodes([ + ...nodes, + { id: uuidv4(), fieldName: '', level: 0, children: [] }, + ]); + }; + + const addSubClass = (parentId) => { + const addSubClassRecursive = (nodes) => { + return nodes.map((node) => { + if (node.id === parentId) { + const newNode = { + id: uuidv4(), + fieldName: '', + level: node.level + 1, + children: [], + }; + return { ...node, children: [...node.children, newNode] }; + } + if (node.children.length > 0) { + return { ...node, children: addSubClassRecursive(node.children) }; + } + return node; + }); + }; + setNodes(addSubClassRecursive(nodes)); + setNodesError(false); + }; + + const deleteNode = (nodeId) => { + const deleteNodeRecursive = (nodes) => { + return nodes + .map((node) => { + if (node.children.length > 0) { + return { ...node, children: deleteNodeRecursive(node.children) }; + } + return node; + }) + .filter((node) => { + if (node.id === nodeId) { + if (node.children.length > 0 || node.fieldName) { + setCurrentNode(node); + setIsModalOpen(true); + return true; // Keep the node for now, until user confirms deletion + } + } + return !( + node.id === nodeId && + node.children.length === 0 && + !node.fieldName + ); + }); + }; + + setNodes(deleteNodeRecursive(nodes)); + }; + + const confirmDeleteNode = () => { + const deleteNodeRecursive = (nodes) => { + return nodes.filter((node) => { + if (node.id === currentNode.id) { + return false; // Remove this node + } + if (node.children.length > 0) { + node.children = deleteNodeRecursive(node.children); + } + return true; + }); + }; + + setNodes(deleteNodeRecursive(nodes)); + setIsModalOpen(false); + setCurrentNode(null); + }; + + return ( + <> +
    Class Hierarchy
    + + +
    + {nodes.map((node) => ( + + ))} +
    +
    + + + + + } + onClose={() => setIsModalOpen(false)} + > + Confirm that you are wish to delete the following record + + + ); +}; + +export default ClassHierarchy; diff --git a/GUI/src/components/molecules/DatasetGroupCard/index.tsx b/GUI/src/components/molecules/DatasetGroupCard/index.tsx index 1081ff3e..a542d8bd 100644 --- a/GUI/src/components/molecules/DatasetGroupCard/index.tsx +++ b/GUI/src/components/molecules/DatasetGroupCard/index.tsx @@ -4,17 +4,20 @@ import Dataset from 'assets/Dataset'; import { Switch } from 'components/FormElements'; import Button from 'components/Button'; import Label from 'components/Label'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { enableDataset } from 'services/datasets'; type DatasetGroupCardProps = { datasetGroupId?: string; datasetName?: string; version?: string; isLatest?: boolean; - isEnabled?:boolean; - enableAllowed?:boolean; - lastUpdated?:string; - validationStatus?:string; - lastModelTrained?:string + isEnabled?: boolean; + enableAllowed?: boolean; + lastUpdated?: string; + lastUsed?: string; + validationStatus?: string; + lastModelTrained?: string; }; const DatasetGroupCard: FC> = ({ @@ -25,25 +28,92 @@ const DatasetGroupCard: FC> = ({ isEnabled, enableAllowed, lastUpdated, + lastUsed, validationStatus, - lastModelTrained + lastModelTrained, }) => { + const queryClient = useQueryClient(); + + const renderValidationStatus = (status) => { + if (status === 'successful') { + return ; + } else if (status === 'failed') { + return ; + } else if (status === 'pending') { + return ; + } else if (status === 'in_progress') { + return ; + } + }; + + const datasetEnableMutation = useMutation({ + mutationFn: (data) => enableDataset(data), + onSuccess: async () => { + await queryClient.invalidateQueries([ + 'GET/datasetgroup/overview',1 + ]); + // setIsModalOpen(false); + }, + // onError: (error: AxiosError) => { + // setModalType(INTEGRATION_MODALS.DISCONNECT_ERROR); + // }, + }); + + const datasetDisableMutation = useMutation({ + mutationFn: (data) => enableDataset(data), + onSuccess: async () => { + await queryClient.invalidateQueries([ + 'GET/datasetgroup/overview',1 + ]); + // setIsModalOpen(false); + }, + // onError: (error: AxiosError) => { + // setModalType(INTEGRATION_MODALS.DISCONNECT_ERROR); + // }, + }); + + const handleCheck =()=>{ + if(isEnabled) + datasetDisableMutation.mutate({ + dgId: datasetGroupId, + operationType: 'disable' + }); + else + datasetEnableMutation.mutate({ + dgId: datasetGroupId, + operationType: 'enable' + }); + } + return ( <>

    {datasetName}

    - + handleCheck()} + />
    - + {renderValidationStatus(validationStatus)}
    -

    {'Last Model Trained:'}{lastModelTrained}

    -

    {'Last Used For Training:'}{}

    -

    {'Last Updated: 7.6.24-15:31'}

    +

    + {'Last Model Trained:'} + {lastModelTrained} +

    +

    + {'Last Used For Training:'} + {lastUsed} +

    +

    + {'Last Updated:'} + {lastUpdated} +

    - - + + {isLatest ? : null}
    diff --git a/GUI/src/components/molecules/Pagination/index.tsx b/GUI/src/components/molecules/Pagination/index.tsx index 71bbc09a..a1665b2e 100644 --- a/GUI/src/components/molecules/Pagination/index.tsx +++ b/GUI/src/components/molecules/Pagination/index.tsx @@ -1,36 +1,29 @@ import React from 'react'; -import { Link } from 'react-router-dom'; import { MdOutlineWest, MdOutlineEast } from 'react-icons/md'; import clsx from 'clsx'; -// import "./Pagination.scss" -import { useTranslation } from 'react-i18next'; +import { Link } from 'react-router-dom'; + interface PaginationProps { pageCount: number; - pageSize: number; pageIndex: number; canPreviousPage: boolean; canNextPage: boolean; - onPageChange?: (pageIndex: number) => void; - onPageSizeChange?: (pageSize: number) => void; + onPageChange: (pageIndex: number) => void; id?: string; } const Pagination: React.FC = ({ pageCount, - pageSize, pageIndex, canPreviousPage, canNextPage, onPageChange, - onPageSizeChange, id, }) => { - const { t } = useTranslation(); - return (
    - {(pageCount * pageSize) > pageSize && ( + {(pageCount > 1) && (
    -
    )} - {/*
    - - -
    */}
    ); }; diff --git a/GUI/src/components/molecules/ValidationCriteria/index.tsx b/GUI/src/components/molecules/ValidationCriteria/index.tsx new file mode 100644 index 00000000..f31574c6 --- /dev/null +++ b/GUI/src/components/molecules/ValidationCriteria/index.tsx @@ -0,0 +1,168 @@ +import React, { FC, PropsWithChildren, useCallback, useState } from 'react'; +import { DndProvider, useDrag, useDrop } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; +import dataTypes from '../../../config/dataTypesConfig.json'; +import { MdDehaze, MdDelete, MdExpand } from 'react-icons/md'; +import Card from 'components/Card'; +import { FormCheckbox, FormInput, FormSelect } from 'components/FormElements'; +import Button from 'components/Button'; +import { transformValidationRules } from 'utils/datasetGroupsUtils'; +import { ValidationRule } from 'types/datasetGroups'; + +const ItemTypes = { + ITEM: 'item', +}; + +type ValidationRulesProps = { + validationRules?: ValidationRule[]; + setValidationRules: React.Dispatch>; + validationRuleError?: boolean; + setValidationRuleError: React.Dispatch>; +}; +const ValidationCriteria: FC> = ({ + validationRules, + setValidationRules, + setValidationRuleError, + validationRuleError, +}) => { + const setIsDataClass = (id, isDataClass) => { + const updatedItems = validationRules.map((item) => + item.id === id ? { ...item, isDataClass: !isDataClass } : item + ); + setValidationRules(updatedItems); + }; + + const handleChange = useCallback((id, newValue) => { + setValidationRules((prevData) => + prevData.map((item) => + item.id === id ? { ...item, fieldName: newValue } : item + ) + ); + }, []); + + const changeDataType = (id, value) => { + const updatedItems = validationRules.map((item) => + item.id === id ? { ...item, dataType: value } : item + ); + setValidationRules(updatedItems); + }; + + const DraggableItem = ({ item, index, moveItem }) => { + const [, ref] = useDrag({ + type: ItemTypes.ITEM, + item: { index }, + }); + + const [, drop] = useDrop({ + accept: ItemTypes.ITEM, + hover: (draggedItem) => { + if (draggedItem.index !== index) { + moveItem(draggedItem.index, index); + draggedItem.index = index; + } + }, + }); + + return ( +
    ref(drop(node))}> + +
    + handleChange(item.id, e.target.value)} + error={ + validationRuleError && !item.fieldName + ? 'Enter a field name' + : '' + } + /> + + changeDataType(item.id, selection?.value) + } + error={ + validationRuleError && !item.dataType + ? 'Select a data type' + : '' + } + /> +
    + deleteItem(item.id)} + className='link' + > + + Delete + + setIsDataClass(item.id, item.isDataClass)} + /> + +
    +
    +
    +
    + ); + }; + + const moveItem = (fromIndex, toIndex) => { + const updatedItems = Array.from(validationRules); + const [movedItem] = updatedItems.splice(fromIndex, 1); + updatedItems.splice(toIndex, 0, movedItem); + setValidationRules(updatedItems); + }; + + const addNewClass = () => { + setValidationRuleError(false) + const newId = validationRules[validationRules?.length - 1]?.id + 1; + const updatedItems = [ + ...validationRules, + { id: newId, fieldName: '', dataType: '', isDataClass: false }, + ]; + setValidationRules(updatedItems); + }; + + const deleteItem = (idToDelete) => { + const updatedItems = validationRules.filter( + (item) => item.id !== idToDelete + ); + setValidationRules(updatedItems); + }; + + return ( + +
    Create Validation Rule
    + {validationRules.map((item, index) => ( + + ))} +
    + +
    + +
    + ); +}; + +export default ValidationCriteria; diff --git a/GUI/src/config/dataTypesConfig.json b/GUI/src/config/dataTypesConfig.json new file mode 100644 index 00000000..6bb681b2 --- /dev/null +++ b/GUI/src/config/dataTypesConfig.json @@ -0,0 +1,6 @@ +[ + { "label": "Text", "value": "TEXT" }, + { "label": "Numbers", "value": "NUMBER" }, + { "label": "Date Time", "value": "DATETIME" } + + ] \ No newline at end of file diff --git a/GUI/src/pages/DatasetGroups/CreateDatasetGroup.tsx b/GUI/src/pages/DatasetGroups/CreateDatasetGroup.tsx new file mode 100644 index 00000000..fa1ef959 --- /dev/null +++ b/GUI/src/pages/DatasetGroups/CreateDatasetGroup.tsx @@ -0,0 +1,173 @@ +import { FC, useCallback, useState } from 'react'; +import './DatasetGroups.scss'; +import { useTranslation } from 'react-i18next'; +import { Button, Card, Dialog, FormInput, FormSelect } from 'components'; +import DatasetGroupCard from 'components/molecules/DatasetGroupCard'; +import Pagination from 'components/molecules/Pagination'; +import { getDatasetsOverview } from 'services/datasets'; +import { useQuery } from '@tanstack/react-query'; +import DraggableRows from 'components/molecules/ValidationCriteria'; +import { v4 as uuidv4 } from 'uuid'; +import ClassHierarchy from 'components/molecules/ClassHeirarchy'; +import { + getTimestampNow, + isValidationRulesSatisfied, + transformClassHierarchy, + transformValidationRules, + validateClassHierarchy, + validateValidationRules, +} from 'utils/datasetGroupsUtils'; +import ValidationCriteria from 'components/molecules/ValidationCriteria'; +import { ValidationRule } from 'types/datasetGroups'; + +const CreateDatasetGroup: FC = () => { + const { t } = useTranslation(); + const [isModalOpen, setIsModalOpen] = useState(false); + const [modalType, setModalType] = useState(''); + + const initialValidationRules = [ + { id: 1, fieldName: '', dataType: '', isDataClass: false }, + { id: 2, fieldName: '', dataType: '', isDataClass: true }, + ]; + + const initialClass = [ + { id: uuidv4(), fieldName: '', level: 0, children: [] }, + ]; + + const [datasetName, setDatasetName] = useState(''); + const [datasetNameError, setDatasetNameError] = useState(false); + + const [validationRules, setValidationRules] = useState( + initialValidationRules + ); + const [validationRuleError, setValidationRuleError] = useState(false); + + const [nodes, setNodes] = useState(initialClass); + const [nodesError, setNodesError] = useState(false); + + const validateData = useCallback(() => { + setNodesError(validateClassHierarchy(nodes)); + setDatasetNameError(!datasetName && true); + setValidationRuleError(validateValidationRules(validationRules)); + if ( + !validateClassHierarchy(nodes) && + datasetName && + true && + !validateValidationRules(validationRules) + ) { + if (!isValidationRulesSatisfied(validationRules)) { + setIsModalOpen(true); + setModalType('VALIDATION_ERROR'); + }else{ + const payload = { + name: datasetName, + major_version: 1, + minor_version: 0, + patch_version: 0, + created_timestamp: getTimestampNow(), + last_updated_timestamp: getTimestampNow(), + ...transformValidationRules(validationRules), + ...transformClassHierarchy(nodes), + }; + + console.log(payload); + setIsModalOpen(true); + setModalType('SUCCESS'); + } + + } + + }, [datasetName, nodes, validationRules]); + + return ( + <> +
    +
    +
    Create Dataset Group
    +
    +
    + + <> + setDatasetName(e.target.value)} + error={ + !datasetName && datasetNameError ? 'Enter dataset name' : '' + } + /> + + + + +
    + {modalType === 'VALIDATION_ERROR' && ( + + + + + } + onClose={() => setIsModalOpen(false)} + > + The dataset must have at least 2 columns. Additionally, there needs + to be at least one column designated as a data class and one column + that is not a data class. Please adjust your dataset accordingly. + + )} + {modalType === 'SUCCESS' && ( + + + + + } + onClose={() => setIsModalOpen(false)} + > + You have successfully created the dataset group. In the detailed + view, you can now see and edit the dataset as needed. + + )} +
    + + +
    + +
    + + ); +}; + +export default CreateDatasetGroup; diff --git a/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx b/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx new file mode 100644 index 00000000..93ea31cd --- /dev/null +++ b/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx @@ -0,0 +1,165 @@ +import { FC, useCallback, useState } from 'react'; +import './DatasetGroups.scss'; +import { useTranslation } from 'react-i18next'; +import { Button, Card, Dialog, FormInput, FormSelect } from 'components'; +import DatasetGroupCard from 'components/molecules/DatasetGroupCard'; +import Pagination from 'components/molecules/Pagination'; +import { getDatasetsOverview } from 'services/datasets'; +import { useQuery } from '@tanstack/react-query'; +import DraggableRows from 'components/molecules/ValidationCriteria'; +import { v4 as uuidv4 } from 'uuid'; +import ClassHierarchy from 'components/molecules/ClassHeirarchy'; +import { + getTimestampNow, + isValidationRulesSatisfied, + transformClassHierarchy, + transformValidationRules, + validateClassHierarchy, + validateValidationRules, +} from 'utils/datasetGroupsUtils'; +import ValidationCriteria from 'components/molecules/ValidationCriteria'; +import { ValidationRule } from 'types/datasetGroups'; + +const ViewDatasetGroup: FC = () => { + const { t } = useTranslation(); + const [isModalOpen, setIsModalOpen] = useState(false); + const [modalType, setModalType] = useState(''); + + const initialValidationRules = [ + { id: 1, fieldName: '', dataType: '', isDataClass: false }, + { id: 2, fieldName: '', dataType: '', isDataClass: true }, + ]; + + const initialClass = [ + { id: uuidv4(), fieldName: '', level: 0, children: [] }, + ]; + + const [datasetName, setDatasetName] = useState(''); + const [datasetNameError, setDatasetNameError] = useState(false); + + const [validationRules, setValidationRules] = useState( + initialValidationRules + ); + const [validationRuleError, setValidationRuleError] = useState(false); + + const [nodes, setNodes] = useState(initialClass); + const [nodesError, setNodesError] = useState(false); + + const validateData = useCallback(() => { + setNodesError(validateClassHierarchy(nodes)); + setDatasetNameError(!datasetName && true); + setValidationRuleError(validateValidationRules(validationRules)); + if ( + !validateClassHierarchy(nodes) && + datasetName && + true && + !validateValidationRules(validationRules) + ) { + const payload = { + name: datasetName, + major_version: 1, + minor_version: 0, + patch_version: 0, + created_timestamp: getTimestampNow(), + last_updated_timestamp: getTimestampNow(), + ...transformValidationRules(validationRules), + ...transformClassHierarchy(nodes), + }; + + console.log(payload); + setIsModalOpen(true); + setModalType('SUCCESS'); + } + if (!isValidationRulesSatisfied(validationRules)) { + setIsModalOpen(true); + setModalType('VALIDATION_ERROR'); + } + }, [datasetName, nodes, validationRules]); + + return ( + <> +
    +
    +
    Create Dataset Group
    +
    +
    + + <> + setDatasetName(e.target.value)} + error={ + !datasetName && datasetNameError ? 'Enter dataset name' : '' + } + /> + + + + +
    + + {modalType === 'VALIDATION_ERROR' && ( + + + + + } + onClose={() => setIsModalOpen(false)} + > + The dataset must have at least 2 columns. Additionally, there needs + to be at least one column designated as a data class and one column + that is not a data class. Please adjust your dataset accordingly. + + )} + {modalType === 'SUCCESS' && ( + + + + + } + onClose={() => setIsModalOpen(false)} + > + You have successfully created the dataset group. In the detailed view, you can now see and edit the dataset as needed. + + )} +
    + + ); +}; + +export default ViewDatasetGroup; diff --git a/GUI/src/pages/DatasetGroups/index.tsx b/GUI/src/pages/DatasetGroups/index.tsx index 5fb5294f..6e40d807 100644 --- a/GUI/src/pages/DatasetGroups/index.tsx +++ b/GUI/src/pages/DatasetGroups/index.tsx @@ -1,4 +1,4 @@ -import { FC } from 'react'; +import { FC, useState } from 'react'; import './DatasetGroups.scss'; import { useTranslation } from 'react-i18next'; import { Button, FormInput, FormSelect } from 'components'; @@ -6,45 +6,32 @@ import DatasetGroupCard from 'components/molecules/DatasetGroupCard'; import Pagination from 'components/molecules/Pagination'; import { getDatasetsOverview } from 'services/datasets'; import { useQuery } from '@tanstack/react-query'; +import { useNavigate } from 'react-router-dom'; const DatasetGroups: FC = () => { const { t } = useTranslation(); + const navigate = useNavigate(); - const datasets = [ - { datasetName: 'Dataset 10', status: 'Connected', isEnabled: false }, - { datasetName: 'Dataset 2', status: 'Disconnected', isEnabled: false }, - { datasetName: 'Dataset 2', status: 'Disconnected', isEnabled: true }, - { datasetName: 'Dataset 9', status: 'Disconnected', isEnabled: false }, - { datasetName: 'Dataset 4', status: 'Disconnected', isEnabled: true }, - { datasetName: 'Dataset 10', status: 'Connected', isEnabled: true }, - { datasetName: 'Dataset 9', status: 'Disconnected', isEnabled: true }, - { datasetName: 'Dataset 2', status: 'Disconnected', isEnabled: true }, - { datasetName: 'Dataset 4', status: 'Disconnected', isEnabled: true }, - { datasetName: 'Dataset 3', status: 'Disconnected', isEnabled: false }, - { datasetName: 'Dataset 3', status: 'Disconnected', isEnabled: false }, - { datasetName: 'Dataset 3', status: 'Disconnected', isEnabled: false }, + const [pageIndex, setPageIndex] = useState(1); - ]; + const { data: datasetGroupsData, isLoading, isError } = useQuery(['datasets/groups', pageIndex], () => getDatasetsOverview(pageIndex), { + keepPreviousData: true, + }); - const { data: datasetGroupsData, isLoading } = useQuery( - ['datasets/groups'], - () => getDatasetsOverview(1) - ); + const pageCount = datasetGroupsData?.totalPages || 5 - console.log(datasetGroupsData); - return ( <>
    Dataset Groups
    -
    - { className="bordered-card grid-container" style={{ padding: '20px', marginTop: '20px' }} > - {datasets.map((dataset) => { + {isLoading && <>Loading...} + {datasetGroupsData?.data?.map((dataset) => { return ( ); })}
    - - + 1} + canNextPage={pageIndex < pageCount} + onPageChange={setPageIndex} + />
    diff --git a/GUI/src/services/datasets.ts b/GUI/src/services/datasets.ts index b63ac422..015b28c6 100644 --- a/GUI/src/services/datasets.ts +++ b/GUI/src/services/datasets.ts @@ -4,8 +4,15 @@ import { User, UserDTO } from 'types/user'; export async function getDatasetsOverview(pageNum: number) { const { data } = await apiMock.get('GET/datasetgroup/overview', { params: { - page_num: pageNum + pageNum: pageNum }}); return data; } +export async function enableDataset(enableData) { + const { data } = await apiMock.post('POST/datasetgroup/update/status', { + dgId: enableData.dgId, + operationType: enableData.operationType + }); + return data; +} diff --git a/GUI/src/services/integration.ts b/GUI/src/services/integration.ts index 0f5e2b97..aca31aca 100644 --- a/GUI/src/services/integration.ts +++ b/GUI/src/services/integration.ts @@ -1,4 +1,4 @@ -import { IntegrationStatus } from 'types/integration'; +import { OperationConfig } from 'types/integration'; import apiDev from './api-dev'; export async function getIntegrationStatus() { @@ -6,7 +6,7 @@ export async function getIntegrationStatus() { return data?.response; } - export async function togglePlatform(integrationData: IntegrationStatus) { + export async function togglePlatform(integrationData: OperationConfig) { const { data } = await apiDev.post('classifier/integration/toggle-platform', { "operation": integrationData.operation, "platform": integrationData.platform diff --git a/GUI/src/styles/generic/_base.scss b/GUI/src/styles/generic/_base.scss index 954383d7..67693e6f 100644 --- a/GUI/src/styles/generic/_base.scss +++ b/GUI/src/styles/generic/_base.scss @@ -47,6 +47,16 @@ body { font-weight: 300; } +.title-sm { + font-size: 1rem; + color: #000; + font-weight: 300; + display: flex; + justify-content: space-between; + align-items: center; + margin: 20px 0px; +} + a, input, select, @@ -57,7 +67,7 @@ button { } a { - color: get-color(black-coral-12); + color: get-color(sapphire-blue-10); text-decoration: none; &:hover { @@ -65,6 +75,14 @@ a { } } +.link { + color: get-color(sapphire-blue-10); + &:hover { + text-decoration: underline; + cursor: pointer; + } +} + img { max-width: 100%; height: auto; diff --git a/GUI/src/types/datasetGroups.ts b/GUI/src/types/datasetGroups.ts new file mode 100644 index 00000000..6285986e --- /dev/null +++ b/GUI/src/types/datasetGroups.ts @@ -0,0 +1,13 @@ +export interface ValidationRule { + id: number; + fieldName: string; + dataType: string; + isDataClass: boolean; +} + +export interface Class { + id: string; + fieldName: string; + level: number; + children: Class[]|any; +} diff --git a/GUI/src/utils/datasetGroupsUtils.ts b/GUI/src/utils/datasetGroupsUtils.ts new file mode 100644 index 00000000..268713b1 --- /dev/null +++ b/GUI/src/utils/datasetGroupsUtils.ts @@ -0,0 +1,87 @@ +import { Class, ValidationRule } from 'types/datasetGroups'; + +export const transformValidationRules = (data: ValidationRule[]) => { + const validationCriteria = { + fields: [], + validation_rules: {}, + }; + + data.forEach((item) => { + const fieldNameKey: string = item.fieldName + .toLowerCase() + .replace(/\s+/g, '_'); + + validationCriteria.fields.push(fieldNameKey); + + validationCriteria.validation_rules[fieldNameKey] = { + type: item.dataType.toLowerCase(), + is_data_class: item.isDataClass, + }; + }); + + return validationCriteria; +}; + +export const transformClassHierarchy = (data: Class[]) => { + const transformNode = (node: Class) => { + return { + class: node.fieldName, + subclasses: node.children.map(transformNode), + }; + }; + + return { + class_hierarchy: data.map(transformNode), + }; +}; + +export const validateClassHierarchy = (data: Class[]) => { + for (let item of data) { + if (item.fieldName === '') { + return true; + } + if (item.children && item.children.length > 0) { + if (validateClassHierarchy(item.children)) { + return true; + } + } + } + return false; +}; + +export const validateValidationRules = (data: ValidationRule[]) => { + for (let item of data) { + if (item.fieldName === '' || item.dataType === '') { + return true; + } + } + return false; +}; + +export const getTimestampNow = ()=>{ + return Math.floor(Date.now() / 1000); +} + +export const isValidationRulesSatisfied=(data: ValidationRule[])=>{ + if (data.length < 2) { + return false; +} + +let hasDataClassTrue = false; +let hasDataClassFalse = false; + +for (let item of data) { + if (item.isDataClass === true) { + hasDataClassTrue = true; + } + if (item.isDataClass === false) { + hasDataClassFalse = true; + } + + if (hasDataClassTrue && hasDataClassFalse) { + return true; + } +} + +return false; +} \ No newline at end of file From 9783584cd1461090b54ad10f071d8130bd01af1f Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Thu, 18 Jul 2024 23:56:22 +0530 Subject: [PATCH 183/582] adding dnd --- GUI/package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/GUI/package.json b/GUI/package.json index b56ee42f..70348b1e 100644 --- a/GUI/package.json +++ b/GUI/package.json @@ -44,6 +44,8 @@ "react-color": "^2.19.3", "react-cookie": "^4.1.1", "react-datepicker": "^4.8.0", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.2.0", "react-hook-form": "^7.52.1", "react-i18next": "^12.1.1", From 18188c3df05636e8232872730629d306b471d707 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Thu, 18 Jul 2024 23:56:53 +0530 Subject: [PATCH 184/582] adding dnd --- GUI/package-lock.json | 159 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 157 insertions(+), 2 deletions(-) diff --git a/GUI/package-lock.json b/GUI/package-lock.json index 52520494..03ebc045 100644 --- a/GUI/package-lock.json +++ b/GUI/package-lock.json @@ -41,6 +41,8 @@ "react-color": "^2.19.3", "react-cookie": "^4.1.1", "react-datepicker": "^4.8.0", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.2.0", "react-hook-form": "^7.52.1", "react-i18next": "^12.1.1", @@ -5831,6 +5833,21 @@ "@babel/runtime": "^7.13.10" } }, + "node_modules/@react-dnd/asap": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz", + "integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==" + }, + "node_modules/@react-dnd/invariant": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz", + "integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==" + }, + "node_modules/@react-dnd/shallowequal": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz", + "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==" + }, "node_modules/@reactflow/background": { "version": "11.3.13", "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.13.tgz", @@ -6846,6 +6863,17 @@ "@types/react": "*" } }, + "node_modules/@types/react-redux": { + "version": "7.1.33", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.33.tgz", + "integrity": "sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==", + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, "node_modules/@types/react-transition-group": { "version": "4.4.10", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", @@ -8117,6 +8145,14 @@ "node": ">= 8" } }, + "node_modules/css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "dependencies": { + "tiny-invariant": "^1.0.6" + } + }, "node_modules/css-select": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", @@ -8477,6 +8513,16 @@ "node": ">=8" } }, + "node_modules/dnd-core": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz", + "integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==", + "dependencies": { + "@react-dnd/asap": "^5.0.1", + "@react-dnd/invariant": "^4.0.1", + "redux": "^4.2.0" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -9422,8 +9468,7 @@ "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 + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-glob": { "version": "3.3.2", @@ -12318,6 +12363,11 @@ } ] }, + "node_modules/raf-schd": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==" + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -12329,6 +12379,53 @@ "node": ">=0.10.0" } }, + "node_modules/react-beautiful-dnd": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz", + "integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==", + "dependencies": { + "@babel/runtime": "^7.9.2", + "css-box-model": "^1.2.0", + "memoize-one": "^5.1.1", + "raf-schd": "^4.0.2", + "react-redux": "^7.2.0", + "redux": "^4.0.4", + "use-memo-one": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8.5 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-beautiful-dnd/node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, + "node_modules/react-beautiful-dnd/node_modules/react-redux": { + "version": "7.2.9", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", + "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", + "dependencies": { + "@babel/runtime": "^7.15.4", + "@types/react-redux": "^7.1.20", + "hoist-non-react-statics": "^3.3.2", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^17.0.2" + }, + "peerDependencies": { + "react": "^16.8.3 || ^17 || ^18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-color": { "version": "2.19.3", "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz", @@ -12376,6 +12473,43 @@ "react-dom": "^16.9.0 || ^17 || ^18" } }, + "node_modules/react-dnd": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz", + "integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==", + "dependencies": { + "@react-dnd/invariant": "^4.0.1", + "@react-dnd/shallowequal": "^4.0.1", + "dnd-core": "^16.0.1", + "fast-deep-equal": "^3.1.3", + "hoist-non-react-statics": "^3.3.2" + }, + "peerDependencies": { + "@types/hoist-non-react-statics": ">= 3.3.1", + "@types/node": ">= 12", + "@types/react": ">= 16", + "react": ">= 16.14" + }, + "peerDependenciesMeta": { + "@types/hoist-non-react-statics": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-dnd-html5-backend": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz", + "integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==", + "dependencies": { + "dnd-core": "^16.0.1" + } + }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -12769,6 +12903,14 @@ "node": ">=8.10.0" } }, + "node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", @@ -13485,6 +13627,11 @@ "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==" }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" + }, "node_modules/tiny-warning": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", @@ -13886,6 +14033,14 @@ } } }, + "node_modules/use-memo-one": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", + "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/use-sidecar": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", From 6225f8866f8c96dcfc398d1d0c57ebcdb69de3f8 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Fri, 19 Jul 2024 06:40:03 +0530 Subject: [PATCH 185/582] ESCLASS-125: Implement minor update API.add sending datat to mock cron-manager endpoint --- .../set-old-dataset-group-for-validation.sql | 8 ++ DSL/Resql/snapshot-dataset-group.sql | 17 ++++ .../classifier/datasetgroup/update/minor.yml | 95 +++++++++++++++++++ constants.ini | 1 + 4 files changed, 121 insertions(+) create mode 100644 DSL/Resql/set-old-dataset-group-for-validation.sql create mode 100644 DSL/Resql/snapshot-dataset-group.sql create mode 100644 DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/minor.yml diff --git a/DSL/Resql/set-old-dataset-group-for-validation.sql b/DSL/Resql/set-old-dataset-group-for-validation.sql new file mode 100644 index 00000000..2c72bcaa --- /dev/null +++ b/DSL/Resql/set-old-dataset-group-for-validation.sql @@ -0,0 +1,8 @@ +UPDATE dataset_group_metadata +SET + minor_version = minor_version + 1, + enable_allowed = false, + validation_status = 'in-progress'::Validation_Status, + is_enabled = false, + raw_data_location = :s3_file_path +WHERE id = :id; diff --git a/DSL/Resql/snapshot-dataset-group.sql b/DSL/Resql/snapshot-dataset-group.sql new file mode 100644 index 00000000..db1d8e74 --- /dev/null +++ b/DSL/Resql/snapshot-dataset-group.sql @@ -0,0 +1,17 @@ +INSERT INTO dataset_group_metadata ( + group_name, major_version, minor_version, patch_version, latest, + is_enabled, enable_allowed, last_model_trained, created_timestamp, + last_updated_timestamp, last_used_for_training, validation_status, + validation_errors, processed_data_available, raw_data_available, + num_samples, num_pages, raw_data_location, preprocess_data_location, + validation_criteria, class_hierarchy, connected_models +) +SELECT + group_name, major_version, minor_version, patch_version, latest, + is_enabled, enable_allowed, last_model_trained, created_timestamp, + last_updated_timestamp, last_used_for_training, validation_status, + validation_errors, processed_data_available, raw_data_available, + num_samples, num_pages, raw_data_location, preprocess_data_location, + validation_criteria, class_hierarchy, connected_models +FROM dataset_group_metadata +WHERE id = :id; \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/minor.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/minor.yml new file mode 100644 index 00000000..f7182669 --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/minor.yml @@ -0,0 +1,95 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'MINOR'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: dgId + type: number + description: "Body field 'dgId'" + - field: s3FilePath + type: string + description: "Body field 's3FilePath'" + headers: + - field: cookie + type: string + description: "Cookie field" + +extract_request_data: + assign: + dg_id: ${incoming.body.dgId} + s3_file_path: ${incoming.body.s3FilePath} + next: snapshot_dataset_group + +snapshot_dataset_group: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/snapshot-dataset-group" + body: + id: ${dg_id} + result: res + next: check_status + +check_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: update_old_dataset_group + next: assign_fail_response + +update_old_dataset_group: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/set-old-dataset-group-for-validation" + body: + id: ${dg_id} + s3_file_path: ${s3_file_path} + result: res + next: check_old_dataset_status + +check_old_dataset_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: execute_cron_manager + next: assign_fail_response + +execute_cron_manager: + call: reflect.mock + args: + url: "[#CLASSIFIER_CRON_MANAGER]/execute//" + body: + cookie: ${incoming.header.cookie} + result: res + next: assign_success_response + +assign_success_response: + assign: + format_res: { + dgId: '${dg_id}', + validationStatus: 'in-progress', + operationSuccessful: true, + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + dgId: '${dg_id}', + validationStatus: '', + operationSuccessful: false, + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end + diff --git a/constants.ini b/constants.ini index bb7cd9d8..05af8045 100644 --- a/constants.ini +++ b/constants.ini @@ -5,6 +5,7 @@ CLASSIFIER_RUUTER_PRIVATE=http://ruuter-private:8088 CLASSIFIER_DMAPPER=http://data-mapper:3000 CLASSIFIER_RESQL=http://resql:8082 CLASSIFIER_TIM=http://tim:8085 +CLASSIFIER_CRON_MANAGER=http://cron-manager:9010 CLASSIFIER_RUUTER_PUBLIC_FRONTEND_URL=value DOMAIN=rootcode.software JIRA_API_TOKEN= value From 47bb0945380d361088d518a9db8c22eef480d2cd Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Fri, 19 Jul 2024 22:37:57 +0530 Subject: [PATCH 186/582] filter dataset groups --- GUI/package-lock.json | 93 ++--------- GUI/package.json | 1 + .../molecules/DatasetGroupCard/index.tsx | 70 ++++++--- GUI/src/context/DialogContext.tsx | 76 +++++++++ GUI/src/hooks/useDialog.tsx | 4 + GUI/src/main.tsx | 3 + .../DatasetGroups/CreateDatasetGroup.tsx | 4 +- GUI/src/pages/DatasetGroups/index.tsx | 146 +++++++++++++----- GUI/src/services/datasets.ts | 32 +++- GUI/src/types/datasetGroups.ts | 21 +++ GUI/src/utils/commonUtilts.ts | 22 +++ GUI/src/utils/datasetGroupsUtils.ts | 30 ++-- 12 files changed, 336 insertions(+), 166 deletions(-) create mode 100644 GUI/src/context/DialogContext.tsx create mode 100644 GUI/src/hooks/useDialog.tsx create mode 100644 GUI/src/utils/commonUtilts.ts diff --git a/GUI/package-lock.json b/GUI/package-lock.json index 03ebc045..815d8a8f 100644 --- a/GUI/package-lock.json +++ b/GUI/package-lock.json @@ -37,6 +37,7 @@ "linkify-react": "^4.1.1", "linkifyjs": "^4.1.1", "lodash": "^4.17.21", + "moment": "^2.30.1", "react": "^18.2.0", "react-color": "^2.19.3", "react-cookie": "^4.1.1", @@ -6863,17 +6864,6 @@ "@types/react": "*" } }, - "node_modules/@types/react-redux": { - "version": "7.1.33", - "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.33.tgz", - "integrity": "sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==", - "dependencies": { - "@types/hoist-non-react-statics": "^3.3.0", - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0", - "redux": "^4.0.0" - } - }, "node_modules/@types/react-transition-group": { "version": "4.4.10", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", @@ -8145,14 +8135,6 @@ "node": ">= 8" } }, - "node_modules/css-box-model": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", - "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", - "dependencies": { - "tiny-invariant": "^1.0.6" - } - }, "node_modules/css-select": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", @@ -11417,6 +11399,14 @@ "integrity": "sha512-W5DR/wwmx/EZUgjN1g+pvlhvFFtRJ3CqGRKqsK/B1hTxrjMb/t3JCbk6aomJD4WomrnueqMaTAhcAkIZJYd73w==", "dev": true }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -12363,11 +12353,6 @@ } ] }, - "node_modules/raf-schd": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", - "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==" - }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -12379,53 +12364,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-beautiful-dnd": { - "version": "13.1.1", - "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz", - "integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==", - "dependencies": { - "@babel/runtime": "^7.9.2", - "css-box-model": "^1.2.0", - "memoize-one": "^5.1.1", - "raf-schd": "^4.0.2", - "react-redux": "^7.2.0", - "redux": "^4.0.4", - "use-memo-one": "^1.1.1" - }, - "peerDependencies": { - "react": "^16.8.5 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/react-beautiful-dnd/node_modules/memoize-one": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", - "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" - }, - "node_modules/react-beautiful-dnd/node_modules/react-redux": { - "version": "7.2.9", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", - "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", - "dependencies": { - "@babel/runtime": "^7.15.4", - "@types/react-redux": "^7.1.20", - "hoist-non-react-statics": "^3.3.2", - "loose-envify": "^1.4.0", - "prop-types": "^15.7.2", - "react-is": "^17.0.2" - }, - "peerDependencies": { - "react": "^16.8.3 || ^17 || ^18" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - } - } - }, "node_modules/react-color": { "version": "2.19.3", "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz", @@ -13627,11 +13565,6 @@ "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==" }, - "node_modules/tiny-invariant": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" - }, "node_modules/tiny-warning": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", @@ -14033,14 +13966,6 @@ } } }, - "node_modules/use-memo-one": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", - "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/use-sidecar": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", diff --git a/GUI/package.json b/GUI/package.json index 70348b1e..646b1064 100644 --- a/GUI/package.json +++ b/GUI/package.json @@ -40,6 +40,7 @@ "linkify-react": "^4.1.1", "linkifyjs": "^4.1.1", "lodash": "^4.17.21", + "moment": "^2.30.1", "react": "^18.2.0", "react-color": "^2.19.3", "react-cookie": "^4.1.1", diff --git a/GUI/src/components/molecules/DatasetGroupCard/index.tsx b/GUI/src/components/molecules/DatasetGroupCard/index.tsx index a542d8bd..40995546 100644 --- a/GUI/src/components/molecules/DatasetGroupCard/index.tsx +++ b/GUI/src/components/molecules/DatasetGroupCard/index.tsx @@ -6,9 +6,10 @@ import Button from 'components/Button'; import Label from 'components/Label'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { enableDataset } from 'services/datasets'; +import { useDialog } from 'hooks/useDialog'; type DatasetGroupCardProps = { - datasetGroupId?: string; + datasetGroupId?: number; datasetName?: string; version?: string; isLatest?: boolean; @@ -33,6 +34,7 @@ const DatasetGroupCard: FC> = ({ lastModelTrained, }) => { const queryClient = useQueryClient(); + const { open, close } = useDialog(); const renderValidationStatus = (status) => { if (status === 'successful') { @@ -48,42 +50,62 @@ const DatasetGroupCard: FC> = ({ const datasetEnableMutation = useMutation({ mutationFn: (data) => enableDataset(data), - onSuccess: async () => { - await queryClient.invalidateQueries([ - 'GET/datasetgroup/overview',1 - ]); - // setIsModalOpen(false); + onSuccess: async (response) => { + await queryClient.invalidateQueries(['GET/datasetgroup/overview', 1]); + if (response?.operationSuccessful) + open({ + title: 'Cannot Enable Dataset Group', + content: ( +

    + The dataset group cannot be enabled until data is added. Please + add datasets to this group and try again. +

    + ), + }); + }, + onError: (error: AxiosError) => { + open({ + title: 'Operation Unsuccessful', + content:

    Something went wrong. Please try again.

    , + }); }, - // onError: (error: AxiosError) => { - // setModalType(INTEGRATION_MODALS.DISCONNECT_ERROR); - // }, }); const datasetDisableMutation = useMutation({ mutationFn: (data) => enableDataset(data), - onSuccess: async () => { - await queryClient.invalidateQueries([ - 'GET/datasetgroup/overview',1 - ]); - // setIsModalOpen(false); + onSuccess: async (response) => { + await queryClient.invalidateQueries(['GET/datasetgroup/overview', 1]); + if (response?.operationSuccessful) + open({ + title: 'Cannot Enable Dataset Group', + content: ( +

    + The dataset group cannot be enabled until data is added. Please + add datasets to this group and try again. +

    + ), + }); + }, + onError: (error: AxiosError) => { + open({ + title: 'Operation Unsuccessful', + content:

    Something went wrong. Please try again.

    , + }); }, - // onError: (error: AxiosError) => { - // setModalType(INTEGRATION_MODALS.DISCONNECT_ERROR); - // }, }); - const handleCheck =()=>{ - if(isEnabled) + const handleCheck = () => { + if (isEnabled) datasetDisableMutation.mutate({ dgId: datasetGroupId, - operationType: 'disable' + operationType: 'disable', }); - else + else datasetEnableMutation.mutate({ dgId: datasetGroupId, - operationType: 'enable' + operationType: 'enable', }); - } + }; return ( <> @@ -93,7 +115,7 @@ const DatasetGroupCard: FC> = ({ handleCheck()} + onCheckedChange={() => handleCheck()} />
    {renderValidationStatus(validationStatus)} diff --git a/GUI/src/context/DialogContext.tsx b/GUI/src/context/DialogContext.tsx new file mode 100644 index 00000000..737e114e --- /dev/null +++ b/GUI/src/context/DialogContext.tsx @@ -0,0 +1,76 @@ +import React, { + createContext, + FC, + PropsWithChildren, + ReactNode, + useMemo, + useState, + useCallback, + } from 'react'; + import * as RadixDialog from '@radix-ui/react-dialog'; + import { MdOutlineClose } from 'react-icons/md'; + import clsx from 'clsx'; + import '../components/Dialog/Dialog.scss'; + import Icon from 'components/Icon'; + import Track from 'components/Track'; + + type DialogProps = { + title?: string | null; + footer?: ReactNode; + size?: 'default' | 'large'; + content: ReactNode; + }; + + type DialogContextType = { + open: (dialog: DialogProps) => void; + close: () => void; + }; + + export const DialogContext = createContext(null!); + + export const DialogProvider: FC> = ({ children }) => { + const [isOpen, setIsOpen] = useState(false); + const [dialogProps, setDialogProps] = useState(null); + + const open = (dialog: DialogProps) => { + setDialogProps(dialog); + setIsOpen(true); + }; + + const close = () => { + setIsOpen(false); + setDialogProps(null); + }; + + const contextValue = useMemo(() => ({ open, close }), []); + + return ( + + {children} + {dialogProps && ( + + + + + {dialogProps.title && ( +
    + {dialogProps.title} + + + +
    + )} +
    {dialogProps.content}
    + {dialogProps.footer && ( + {dialogProps.footer} + )} +
    +
    +
    + )} +
    + ); + }; + \ No newline at end of file diff --git a/GUI/src/hooks/useDialog.tsx b/GUI/src/hooks/useDialog.tsx new file mode 100644 index 00000000..c38ed60a --- /dev/null +++ b/GUI/src/hooks/useDialog.tsx @@ -0,0 +1,4 @@ +import { DialogContext } from 'context/DialogContext'; +import { useContext } from 'react'; + +export const useDialog = () => useContext(DialogContext); diff --git a/GUI/src/main.tsx b/GUI/src/main.tsx index e07f8060..b6274de5 100644 --- a/GUI/src/main.tsx +++ b/GUI/src/main.tsx @@ -14,6 +14,7 @@ import { ToastProvider } from 'context/ToastContext'; import 'styles/main.scss'; import '../i18n'; import { CookiesProvider } from 'react-cookie'; +import { DialogProvider } from 'context/DialogContext'; const defaultQueryFn: QueryFunction | undefined = async ({ queryKey }) => { if (queryKey.includes('prod')) { @@ -37,11 +38,13 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + diff --git a/GUI/src/pages/DatasetGroups/CreateDatasetGroup.tsx b/GUI/src/pages/DatasetGroups/CreateDatasetGroup.tsx index fa1ef959..e97b227c 100644 --- a/GUI/src/pages/DatasetGroups/CreateDatasetGroup.tsx +++ b/GUI/src/pages/DatasetGroups/CreateDatasetGroup.tsx @@ -19,11 +19,13 @@ import { } from 'utils/datasetGroupsUtils'; import ValidationCriteria from 'components/molecules/ValidationCriteria'; import { ValidationRule } from 'types/datasetGroups'; +import { useNavigate } from 'react-router-dom'; const CreateDatasetGroup: FC = () => { const { t } = useTranslation(); const [isModalOpen, setIsModalOpen] = useState(false); const [modalType, setModalType] = useState(''); + const navigate = useNavigate(); const initialValidationRules = [ { id: 1, fieldName: '', dataType: '', isDataClass: false }, @@ -162,7 +164,7 @@ const CreateDatasetGroup: FC = () => { style={{ alignItems: 'end', gap: '10px', justifyContent: 'end', marginTop:"25px" }} > - +
    diff --git a/GUI/src/pages/DatasetGroups/index.tsx b/GUI/src/pages/DatasetGroups/index.tsx index 6e40d807..7ca46e10 100644 --- a/GUI/src/pages/DatasetGroups/index.tsx +++ b/GUI/src/pages/DatasetGroups/index.tsx @@ -4,28 +4,84 @@ import { useTranslation } from 'react-i18next'; import { Button, FormInput, FormSelect } from 'components'; import DatasetGroupCard from 'components/molecules/DatasetGroupCard'; import Pagination from 'components/molecules/Pagination'; -import { getDatasetsOverview } from 'services/datasets'; +import { getDatasetsOverview, getFilterData } from 'services/datasets'; import { useQuery } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; +import { + convertTimestampToDateTime, + formattedArray, + parseVersionString, +} from 'utils/commonUtilts'; +import { DatasetGroup } from 'types/datasetGroups'; const DatasetGroups: FC = () => { const { t } = useTranslation(); const navigate = useNavigate(); const [pageIndex, setPageIndex] = useState(1); + const [enableFetch, setEnableFetch] = useState(true); - const { data: datasetGroupsData, isLoading, isError } = useQuery(['datasets/groups', pageIndex], () => getDatasetsOverview(pageIndex), { - keepPreviousData: true, + const [filters, setFilters] = useState({ + datasetGroupName: 'all', + version: 'x.x.x', + validationStatus: 'all', + sort: 'asc', }); - const pageCount = datasetGroupsData?.totalPages || 5 + const { + data: datasetGroupsData, + isLoading, + refetch, + } = useQuery( + [ + 'datasets/groups', + pageIndex, + filters.datasetGroupName, + parseVersionString(filters?.version)?.major, + parseVersionString(filters?.version)?.minor, + parseVersionString(filters?.version)?.patch, + filters.validationStatus, + filters.sort, + ], + () => + getDatasetsOverview( + pageIndex, + filters.datasetGroupName, + parseVersionString(filters?.version)?.major, + parseVersionString(filters?.version)?.minor, + parseVersionString(filters?.version)?.patch, + filters.validationStatus, + filters.sort + ), + { + keepPreviousData: true, + enabled: enableFetch, + } + ); + const { data: filterData } = useQuery(['datasets/filters'], () => + getFilterData() + ); + const pageCount = datasetGroupsData?.totalPages || 5; + + // Handler for updating filters state + const handleFilterChange = (name: string, value: string) => { + setEnableFetch(false); + setFilters((prevFilters) => ({ + ...prevFilters, + [name]: value, + })); + }; return ( <>
    Dataset Groups
    -
    @@ -34,61 +90,79 @@ const DatasetGroups: FC = () => { + handleFilterChange('datasetGroupName', selection?.value ?? '') + } /> + handleFilterChange('version', selection?.value ?? '') + } /> + handleFilterChange('validationStatus', selection?.value ?? '') + } /> + handleFilterChange('sort', selection?.value ?? '') + } /> + +
    {isLoading && <>Loading...} - {datasetGroupsData?.data?.map((dataset) => { - return ( - - ); - })} + {datasetGroupsData?.data?.map( + (dataset: DatasetGroup, index: number) => { + return ( + + ); + } + )}
    { + return data?.map(name => ({ + label: name, + value: name + })); + }; + + export const convertTimestampToDateTime=(timestamp:number)=> { + return moment.unix(timestamp).format('YYYY-MM-DD HH:mm:ss'); + } + + export const parseVersionString=(version: string)=> { + const parts = version.split('.'); + + return { + major: parts[0] !== 'x' ? parseInt(parts[0], 10) : -1, + minor: parts[1] !== 'x' ? parseInt(parts[1], 10) : -1, + patch: parts[2] !== 'x' ? parseInt(parts[2], 10) : -1, + }; + } \ No newline at end of file diff --git a/GUI/src/utils/datasetGroupsUtils.ts b/GUI/src/utils/datasetGroupsUtils.ts index 268713b1..c0e0cff4 100644 --- a/GUI/src/utils/datasetGroupsUtils.ts +++ b/GUI/src/utils/datasetGroupsUtils.ts @@ -58,30 +58,30 @@ export const validateValidationRules = (data: ValidationRule[]) => { return false; }; -export const getTimestampNow = ()=>{ - return Math.floor(Date.now() / 1000); -} +export const getTimestampNow = () => { + return Math.floor(Date.now() / 1000); +}; -export const isValidationRulesSatisfied=(data: ValidationRule[])=>{ +export const isValidationRulesSatisfied = (data: ValidationRule[]) => { if (data.length < 2) { - return false; -} + return false; + } -let hasDataClassTrue = false; -let hasDataClassFalse = false; + let hasDataClassTrue = false; + let hasDataClassFalse = false; -for (let item of data) { + for (let item of data) { if (item.isDataClass === true) { - hasDataClassTrue = true; + hasDataClassTrue = true; } if (item.isDataClass === false) { - hasDataClassFalse = true; + hasDataClassFalse = true; } if (hasDataClassTrue && hasDataClassFalse) { - return true; + return true; } -} + } -return false; -} \ No newline at end of file + return false; +}; From cc0f2bbd261a914e3c42ac1a4941ef4b31ee70cc Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Sat, 20 Jul 2024 00:03:57 +0530 Subject: [PATCH 187/582] code cleanups --- GUI/src/App.tsx | 4 +++- GUI/src/assets/BackArrowButton.tsx | 20 +++++++++++++++++++ GUI/src/components/Label/Label.scss | 1 + GUI/src/components/MainNavigation/index.tsx | 7 ------- .../molecules/ClassHeirarchy/index.tsx | 10 +++------- .../molecules/DatasetGroupCard/index.tsx | 7 +++---- .../components/molecules/Pagination/index.tsx | 18 ++++++++--------- .../molecules/ValidationCriteria/index.tsx | 11 +++++----- .../DatasetGroups/CreateDatasetGroup.tsx | 7 +------ GUI/src/services/datasets.ts | 1 - 10 files changed, 46 insertions(+), 40 deletions(-) create mode 100644 GUI/src/assets/BackArrowButton.tsx diff --git a/GUI/src/App.tsx b/GUI/src/App.tsx index 5b4cfc7d..eff08272 100644 --- a/GUI/src/App.tsx +++ b/GUI/src/App.tsx @@ -1,4 +1,4 @@ -import { FC, useEffect } from 'react'; +import { FC } from 'react'; import { Navigate, Route, Routes } from 'react-router-dom'; import { Layout } from 'components'; import useStore from 'store'; @@ -9,6 +9,7 @@ import DatasetGroups from 'pages/DatasetGroups'; import { useQuery } from '@tanstack/react-query'; import { UserInfo } from 'types/userInfo'; import CreateDatasetGroup from 'pages/DatasetGroups/CreateDatasetGroup'; +import ViewDatasetGroup from 'pages/DatasetGroups/ViewDatasetGroup'; const App: FC = () => { @@ -30,6 +31,7 @@ const App: FC = () => { } /> } /> } /> + } /> diff --git a/GUI/src/assets/BackArrowButton.tsx b/GUI/src/assets/BackArrowButton.tsx new file mode 100644 index 00000000..e7ea4432 --- /dev/null +++ b/GUI/src/assets/BackArrowButton.tsx @@ -0,0 +1,20 @@ + + +const BackArrowButton = () => { + return ( + + + + + + + + + + + + ); + }; + + export default BackArrowButton; + \ No newline at end of file diff --git a/GUI/src/components/Label/Label.scss b/GUI/src/components/Label/Label.scss index eba04d68..bff23787 100644 --- a/GUI/src/components/Label/Label.scss +++ b/GUI/src/components/Label/Label.scss @@ -14,6 +14,7 @@ border-radius: $veera-radius-s; position: relative; width: fit-content; + height: fit-content; margin-right: 5px; &--info { diff --git a/GUI/src/components/MainNavigation/index.tsx b/GUI/src/components/MainNavigation/index.tsx index f270bf06..08dc5abc 100644 --- a/GUI/src/components/MainNavigation/index.tsx +++ b/GUI/src/components/MainNavigation/index.tsx @@ -3,14 +3,8 @@ import { useTranslation } from 'react-i18next'; import { NavLink, useLocation } from 'react-router-dom'; import { MdApps, - MdBackup, - MdClass, - MdClose, - MdDashboard, - MdDataset, MdKeyboardArrowDown, MdOutlineDataset, - MdOutlineForum, MdPeople, MdSettings, MdSettingsBackupRestore, @@ -20,7 +14,6 @@ import { useQuery } from '@tanstack/react-query'; import clsx from 'clsx'; import { Icon } from 'components'; import type { MenuItem } from 'types/mainNavigation'; -import { menuIcons } from 'constants/menuIcons'; import './MainNavigation.scss'; import apiDev from 'services/api-dev'; diff --git a/GUI/src/components/molecules/ClassHeirarchy/index.tsx b/GUI/src/components/molecules/ClassHeirarchy/index.tsx index 0a517023..a77ec419 100644 --- a/GUI/src/components/molecules/ClassHeirarchy/index.tsx +++ b/GUI/src/components/molecules/ClassHeirarchy/index.tsx @@ -1,15 +1,11 @@ -import React, { FC, PropsWithChildren, useCallback, useState } from 'react'; -import { DndProvider, useDrag, useDrop } from 'react-dnd'; -import { HTML5Backend } from 'react-dnd-html5-backend'; -import dataTypes from '../../../config/dataTypesConfig.json'; -import { MdDelete, MdDeleteOutline, MdExpand } from 'react-icons/md'; +import React, { FC, PropsWithChildren, useState } from 'react'; +import { MdDeleteOutline } from 'react-icons/md'; import Card from 'components/Card'; -import { FormCheckbox, FormInput, FormSelect } from 'components/FormElements'; +import { FormInput } from 'components/FormElements'; import Button from 'components/Button'; import { v4 as uuidv4 } from 'uuid'; import './index.css'; import Dialog from 'components/Dialog'; -import { transformClassHierarchy } from 'utils/datasetGroupsUtils'; import { Class } from 'types/datasetGroups'; type ClassHierarchyProps = { diff --git a/GUI/src/components/molecules/DatasetGroupCard/index.tsx b/GUI/src/components/molecules/DatasetGroupCard/index.tsx index 40995546..b91026be 100644 --- a/GUI/src/components/molecules/DatasetGroupCard/index.tsx +++ b/GUI/src/components/molecules/DatasetGroupCard/index.tsx @@ -1,6 +1,5 @@ import { FC, PropsWithChildren } from 'react'; import './DatasetGroupCard.scss'; -import Dataset from 'assets/Dataset'; import { Switch } from 'components/FormElements'; import Button from 'components/Button'; import Label from 'components/Label'; @@ -34,9 +33,9 @@ const DatasetGroupCard: FC> = ({ lastModelTrained, }) => { const queryClient = useQueryClient(); - const { open, close } = useDialog(); + const { open } = useDialog(); - const renderValidationStatus = (status) => { + const renderValidationStatus = (status:string) => { if (status === 'successful') { return ; } else if (status === 'failed') { @@ -63,7 +62,7 @@ const DatasetGroupCard: FC> = ({ ), }); }, - onError: (error: AxiosError) => { + onError: () => { open({ title: 'Operation Unsuccessful', content:

    Something went wrong. Please try again.

    , diff --git a/GUI/src/components/molecules/Pagination/index.tsx b/GUI/src/components/molecules/Pagination/index.tsx index a1665b2e..7c1c3b9d 100644 --- a/GUI/src/components/molecules/Pagination/index.tsx +++ b/GUI/src/components/molecules/Pagination/index.tsx @@ -3,7 +3,6 @@ import { MdOutlineWest, MdOutlineEast } from 'react-icons/md'; import clsx from 'clsx'; import { Link } from 'react-router-dom'; - interface PaginationProps { pageCount: number; pageIndex: number; @@ -22,24 +21,25 @@ const Pagination: React.FC = ({ id, }) => { return ( -
    - {(pageCount > 1) && ( -
    +
    + {pageCount > 1 && ( +
    -
    ) : ( {' '} diff --git a/GUI/src/components/molecules/ClassHeirarchy/index.tsx b/GUI/src/components/molecules/ClassHeirarchy/index.tsx index a77ec419..2601ad44 100644 --- a/GUI/src/components/molecules/ClassHeirarchy/index.tsx +++ b/GUI/src/components/molecules/ClassHeirarchy/index.tsx @@ -153,7 +153,7 @@ const ClassHierarchy: FC> = ({ }; return ( - <> +
    Class Hierarchy
    @@ -172,18 +172,18 @@ const ClassHierarchy: FC> = ({ isOpen={isModalOpen} title={'Are you sure?'} footer={ - <> +
    - +
    } onClose={() => setIsModalOpen(false)} > Confirm that you are wish to delete the following record
    - + ); }; diff --git a/GUI/src/components/molecules/DatasetGroupCard/index.tsx b/GUI/src/components/molecules/DatasetGroupCard/index.tsx index b91026be..4bd488b7 100644 --- a/GUI/src/components/molecules/DatasetGroupCard/index.tsx +++ b/GUI/src/components/molecules/DatasetGroupCard/index.tsx @@ -107,7 +107,7 @@ const DatasetGroupCard: FC> = ({ }; return ( - <> +

    {datasetName}

    @@ -143,7 +143,7 @@ const DatasetGroupCard: FC> = ({
    - +
    ); }; diff --git a/GUI/src/components/molecules/IntegrationCard/index.tsx b/GUI/src/components/molecules/IntegrationCard/index.tsx index c56a221d..d0cd1e0f 100644 --- a/GUI/src/components/molecules/IntegrationCard/index.tsx +++ b/GUI/src/components/molecules/IntegrationCard/index.tsx @@ -45,11 +45,11 @@ const IntegrationCard: FC> = ({ return ( - <> +
    {isActive ? t('integration.connected') : t('integration.disconnected')} - +
    ); }; @@ -89,7 +89,7 @@ const IntegrationCard: FC> = ({ }; return ( - <> +
    {logo}
    @@ -140,7 +140,7 @@ const IntegrationCard: FC> = ({ isOpen={isModalOpen} title={t('integration.confirmationModalTitle')} footer={ - <> +
    - +
    } >
    @@ -172,7 +172,7 @@ const IntegrationCard: FC> = ({ isOpen={isModalOpen} title={t('integration.confirmationModalTitle')} footer={ - <> +
    - +
    } >
    @@ -209,7 +209,7 @@ const IntegrationCard: FC> = ({
    )} - +
    ); }; diff --git a/GUI/src/pages/DatasetGroups/CreateDatasetGroup.tsx b/GUI/src/pages/DatasetGroups/CreateDatasetGroup.tsx index cb4f90f6..88fc01d0 100644 --- a/GUI/src/pages/DatasetGroups/CreateDatasetGroup.tsx +++ b/GUI/src/pages/DatasetGroups/CreateDatasetGroup.tsx @@ -77,14 +77,14 @@ const CreateDatasetGroup: FC = () => { }, [datasetName, nodes, validationRules]); return ( - <> +
    Create Dataset Group
    - <> +
    { !datasetName && datasetNameError ? 'Enter dataset name' : '' } /> - +
    { isOpen={isModalOpen} title={'Insufficient Columns in Dataset'} footer={ - <> +
    - +
    } onClose={() => setIsModalOpen(false)} > @@ -136,7 +136,7 @@ const CreateDatasetGroup: FC = () => { isOpen={isModalOpen} title={'Dataset Group Created Successfully'} footer={ - <> +
    - +
    } onClose={() => setIsModalOpen(false)} > @@ -163,7 +163,7 @@ const CreateDatasetGroup: FC = () => {
    - +
    ); }; diff --git a/GUI/src/pages/DatasetGroups/index.tsx b/GUI/src/pages/DatasetGroups/index.tsx index 7ca46e10..c754e7d5 100644 --- a/GUI/src/pages/DatasetGroups/index.tsx +++ b/GUI/src/pages/DatasetGroups/index.tsx @@ -73,7 +73,7 @@ const DatasetGroups: FC = () => { }; return ( - <> +
    Dataset Groups
    @@ -139,7 +139,7 @@ const DatasetGroups: FC = () => { className="bordered-card grid-container" style={{ padding: '20px', marginTop: '20px' }} > - {isLoading && <>Loading...} + {isLoading &&
    Loading...
    } {datasetGroupsData?.data?.map( (dataset: DatasetGroup, index: number) => { return ( @@ -173,7 +173,7 @@ const DatasetGroups: FC = () => { />
    - +
    ); }; diff --git a/GUI/src/pages/Integrations/index.tsx b/GUI/src/pages/Integrations/index.tsx index 732ce58f..fc4bcdbd 100644 --- a/GUI/src/pages/Integrations/index.tsx +++ b/GUI/src/pages/Integrations/index.tsx @@ -20,7 +20,7 @@ const Integrations: FC = () => { console.log(integrationStatus); return ( - <>
    +
    {t('integration.title')}
    @@ -45,7 +45,7 @@ console.log(integrationStatus); isActive={integrationStatus?.pinal_connection_status} />
    - +
    ); }; diff --git a/GUI/src/pages/UserManagement/UserModal.tsx b/GUI/src/pages/UserManagement/UserModal.tsx index da3a31a8..254d439c 100644 --- a/GUI/src/pages/UserManagement/UserModal.tsx +++ b/GUI/src/pages/UserManagement/UserModal.tsx @@ -139,12 +139,12 @@ const UserModal: FC = ({ onClose, user, isModalOpen }) => { title={user ? t('settings.users.editUser') : t('settings.users.addUser')} onClose={onClose} footer={ - <> +
    - +
    } > diff --git a/GUI/src/pages/UserManagement/index.tsx b/GUI/src/pages/UserManagement/index.tsx index 7b1865de..074e523c 100644 --- a/GUI/src/pages/UserManagement/index.tsx +++ b/GUI/src/pages/UserManagement/index.tsx @@ -167,10 +167,10 @@ const UserManagement: FC = () => { }, }); - if (isLoading) return <>Loading...; + if (isLoading) return
    Loading...
    ; return ( - <> +
    {t('userManagement.title')}
    @@ -214,7 +214,7 @@ const UserManagement: FC = () => { onClose={() => setDeletableRow(null)} isOpen={true} footer={ - <> +
    - +
    } >

    {t('global.removeValidation')}

    @@ -244,7 +244,7 @@ const UserManagement: FC = () => { )}
    - +
    ); }; From a7a2c0148265bfd094a64f5ba6bb750ea8fc1a59 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Sat, 20 Jul 2024 10:36:56 +0530 Subject: [PATCH 189/582] ESCLASS-125: refactor create dataset group API,add group key column and nonvalidated validation status --- ...ifier-script-v6-dataset-group-metadata.sql | 5 +- DSL/Resql/insert-dataset-group-metadata.sql | 23 +++----- .../POST/classifier/datasetgroup/create.yml | 53 ++++++++++--------- 3 files changed, 37 insertions(+), 44 deletions(-) diff --git a/DSL/Liquibase/changelog/classifier-script-v6-dataset-group-metadata.sql b/DSL/Liquibase/changelog/classifier-script-v6-dataset-group-metadata.sql index 02390d62..c39f3d30 100644 --- a/DSL/Liquibase/changelog/classifier-script-v6-dataset-group-metadata.sql +++ b/DSL/Liquibase/changelog/classifier-script-v6-dataset-group-metadata.sql @@ -1,12 +1,13 @@ -- liquibase formatted sql -- changeset kalsara Magamage:classifier-script-v6-changeset1 -CREATE TYPE Validation_Status AS ENUM ('success', 'fail', 'in-progress'); +CREATE TYPE Validation_Status AS ENUM ('success', 'fail', 'in-progress', 'unvalidated'); -- changeset kalsara Magamage:classifier-script-v6-changeset3 CREATE TABLE dataset_group_metadata ( id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY, group_name TEXT NOT NULL, + group_key TEXT NOT NULL, major_version INT NOT NULL DEFAULT 0, minor_version INT NOT NULL DEFAULT 0, patch_version INT NOT NULL DEFAULT 0, @@ -16,7 +17,7 @@ CREATE TABLE dataset_group_metadata ( last_model_trained TEXT, created_timestamp TIMESTAMP WITH TIME ZONE, last_updated_timestamp TIMESTAMP WITH TIME ZONE, - last_used_for_training TIMESTAMP WITH TIME ZONE, + last_trained_timestamp TIMESTAMP WITH TIME ZONE, validation_status Validation_Status, validation_errors JSONB, processed_data_available BOOLEAN DEFAULT false, diff --git a/DSL/Resql/insert-dataset-group-metadata.sql b/DSL/Resql/insert-dataset-group-metadata.sql index af6db723..9f721239 100644 --- a/DSL/Resql/insert-dataset-group-metadata.sql +++ b/DSL/Resql/insert-dataset-group-metadata.sql @@ -1,48 +1,39 @@ INSERT INTO "dataset_group_metadata" ( group_name, + group_key, major_version, minor_version, patch_version, latest, is_enabled, enable_allowed, - last_model_trained, created_timestamp, last_updated_timestamp, - last_used_for_training, validation_status, - validation_errors, processed_data_available, raw_data_available, num_samples, num_pages, - raw_data_location, - preprocess_data_location, validation_criteria, - class_hierarchy, - connected_models -) VALUES ( + class_hierarchy + ) + VALUES ( :group_name, + :group_key, :major_version, :minor_version, :patch_version, :latest, :is_enabled, :enable_allowed, - COALESCE(:last_model_trained, null), - to_timestamp(:creation_timestamp)::timestamp with time zone, + to_timestamp(:created_timestamp)::timestamp with time zone, to_timestamp(:last_updated_timestamp)::timestamp with time zone, - to_timestamp(:last_used_for_training)::timestamp with time zone, :validation_status::Validation_Status, - :validation_errors::jsonb, :processed_data_available, :raw_data_available, :num_samples, :num_pages, - :raw_data_location, - :preprocess_data_location, :validation_criteria::jsonb, - :class_hierarchy::jsonb, - :connected_models::jsonb + :class_hierarchy::jsonb )RETURNING id; diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/create.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/create.yml index 2762816c..95b71626 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/create.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/create.yml @@ -32,15 +32,15 @@ declaration: - field: lastModelTrained type: string description: "Body field 'lastModelTrained'" - - field: creationTimestamp + - field: createdTimestamp type: number - description: "Body field 'creationTimestamp'" + description: "Body field 'createdTimestamp'" - field: lastUpdatedTimestamp type: number description: "Body field 'lastUpdatedTimestamp'" - - field: lastUsedForTraining + - field: lastTrainedTimestamp type: number - description: "Body field 'lastUsedForTraining'" + description: "Body field 'lastTrainedTimestamp'" - field: validationStatus type: string description: "Body field 'validationStatus'" @@ -89,9 +89,9 @@ extract_request_data: is_enabled: ${incoming.body.isEnabled === null ? false :incoming.body.isEnabled} enable_allowed: ${incoming.body.enableAllowed === null ? false :incoming.body.enableAllowed} last_model_trained: ${incoming.body.lastModelTrained === null ? '' :incoming.body.lastModelTrained} - creation_timestamp: ${incoming.body.creationTimestamp} + created_timestamp: ${incoming.body.createdTimestamp} last_updated_timestamp: ${incoming.body.lastUpdatedTimestamp} - last_used_for_training: ${incoming.body.lastUsedForTraining} + last_trained_timestamp: ${incoming.body.lastTrainedTimestamp} validation_status: ${incoming.body.validationStatus} validation_errors: ${incoming.body.validationErrors} processed_data_available: ${incoming.body.processedDataAvailable === null ? false :incoming.body.processedDataAvailable} @@ -108,36 +108,37 @@ extract_request_data: check_for_request_data: switch: - condition: ${group_name !== null || validation_criteria !=null || class_hierarchy !==null } - next: create_dataset_group_metadata + next: get_epoch_date next: return_incorrect_request +get_epoch_date: + assign: + current_epoch: ${Date.now()} + random_num: ${Math.floor(Math.random() * 100000)} + next: create_dataset_group_metadata + create_dataset_group_metadata: call: http.post args: url: "[#CLASSIFIER_RESQL]/insert-dataset-group-metadata" body: group_name: ${group_name} - major_version: ${major_version} - minor_version: ${minor_version} - patch_version: ${patch_version} - latest: ${latest} - is_enabled: ${is_enabled} - enable_allowed: ${enable_allowed} - last_model_trained: ${last_model_trained} - creation_timestamp: ${creation_timestamp} - last_updated_timestamp: ${last_updated_timestamp} - last_used_for_training: ${last_used_for_training} - validation_status: ${validation_status} - validation_errors: ${JSON.stringify(validation_errors)} - processed_data_available: ${processed_data_available} - raw_data_available: ${raw_data_available} - num_samples: ${num_samples} - num_pages: ${num_pages} - raw_data_location: ${raw_data_location} - preprocess_data_location: ${preprocess_data_location} + group_key: "${random_num+ '_'+current_epoch}" + major_version: 1 + minor_version: 0 + patch_version: 0 + latest: true + is_enabled: false + enable_allowed: false + created_timestamp: ${current_epoch} + last_updated_timestamp: ${current_epoch} + validation_status: unvalidated + processed_data_available: false + raw_data_available: false + num_samples: 0 + num_pages: 0 validation_criteria: ${JSON.stringify(validation_criteria)} class_hierarchy: ${JSON.stringify(class_hierarchy)} - connected_models: ${JSON.stringify(connected_models)} result: res_dataset next: check_status From 85b85752f4a96c9ef5b2abc59fb8b059dc92653b Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Sat, 20 Jul 2024 19:05:14 +0530 Subject: [PATCH 190/582] ESCLASS-125: minor version revamp implementation --- .../set-old-dataset-group-for-validation.sql | 32 ++++++++++++++----- DSL/Resql/snapshot-dataset-group.sql | 8 ++--- .../classifier/datasetgroup/update/minor.yml | 11 ++++++- 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/DSL/Resql/set-old-dataset-group-for-validation.sql b/DSL/Resql/set-old-dataset-group-for-validation.sql index 2c72bcaa..8e6e485e 100644 --- a/DSL/Resql/set-old-dataset-group-for-validation.sql +++ b/DSL/Resql/set-old-dataset-group-for-validation.sql @@ -1,8 +1,24 @@ -UPDATE dataset_group_metadata -SET - minor_version = minor_version + 1, - enable_allowed = false, - validation_status = 'in-progress'::Validation_Status, - is_enabled = false, - raw_data_location = :s3_file_path -WHERE id = :id; +WITH update_latest AS ( + UPDATE dataset_group_metadata + SET latest = false + WHERE group_key = :group_key + RETURNING 1 +), +update_specific AS ( + UPDATE dataset_group_metadata + SET + minor_version = ( + SELECT COALESCE(MAX(minor_version), 0) + 1 + FROM dataset_group_metadata + WHERE group_key = :group_key + ), + enable_allowed = false, + validation_status = 'in-progress'::Validation_Status, + is_enabled = false, + patch_version = 0, + latest = true, + last_updated_timestamp = to_timestamp(:last_updated_timestamp)::timestamp with time zone + WHERE id = :id + RETURNING 1 +) +SELECT 1; diff --git a/DSL/Resql/snapshot-dataset-group.sql b/DSL/Resql/snapshot-dataset-group.sql index db1d8e74..cc6e0b62 100644 --- a/DSL/Resql/snapshot-dataset-group.sql +++ b/DSL/Resql/snapshot-dataset-group.sql @@ -1,15 +1,15 @@ INSERT INTO dataset_group_metadata ( - group_name, major_version, minor_version, patch_version, latest, + group_name, group_key, major_version, minor_version, patch_version, latest, is_enabled, enable_allowed, last_model_trained, created_timestamp, - last_updated_timestamp, last_used_for_training, validation_status, + last_updated_timestamp, last_trained_timestamp, validation_status, validation_errors, processed_data_available, raw_data_available, num_samples, num_pages, raw_data_location, preprocess_data_location, validation_criteria, class_hierarchy, connected_models ) SELECT - group_name, major_version, minor_version, patch_version, latest, + group_name, group_key, major_version, minor_version, patch_version, false AS latest, is_enabled, enable_allowed, last_model_trained, created_timestamp, - last_updated_timestamp, last_used_for_training, validation_status, + last_updated_timestamp, last_trained_timestamp, validation_status, validation_errors, processed_data_available, raw_data_available, num_samples, num_pages, raw_data_location, preprocess_data_location, validation_criteria, class_hierarchy, connected_models diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/minor.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/minor.yml index f7182669..5730497f 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/minor.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/minor.yml @@ -11,6 +11,9 @@ declaration: - field: dgId type: number description: "Body field 'dgId'" + - field: groupKey + type: string + description: "Body field 'groupKey'" - field: s3FilePath type: string description: "Body field 's3FilePath'" @@ -22,6 +25,7 @@ declaration: extract_request_data: assign: dg_id: ${incoming.body.dgId} + group_key: ${incoming.body.groupKey} s3_file_path: ${incoming.body.s3FilePath} next: snapshot_dataset_group @@ -46,7 +50,8 @@ update_old_dataset_group: url: "[#CLASSIFIER_RESQL]/set-old-dataset-group-for-validation" body: id: ${dg_id} - s3_file_path: ${s3_file_path} + group_key: ${group_key} + last_updated_timestamp: ${Date.now()} result: res next: check_old_dataset_status @@ -62,6 +67,10 @@ execute_cron_manager: url: "[#CLASSIFIER_CRON_MANAGER]/execute//" body: cookie: ${incoming.header.cookie} + dgId: ${dg_id} + updateType: 'minor' + savedFilePath: ${s3_file_path} + patchPayload: ${[]} result: res next: assign_success_response From 6cf6639bb0fb0a84d61a8c4348cd203e30fff822 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Sat, 20 Jul 2024 23:35:06 +0530 Subject: [PATCH 191/582] ESCLASS-125: implement major version update API --- DSL/Resql/insert-dataset-group-metadata.sql | 4 +- .../update-major-version-dataset-group.sql | 33 ++++++ ...=> update-minor-version-dataset-group.sql} | 2 +- .../POST/classifier/datasetgroup/create.yml | 4 +- .../classifier/datasetgroup/update/major.yml | 102 ++++++++++++++++++ .../classifier/datasetgroup/update/minor.yml | 4 +- 6 files changed, 142 insertions(+), 7 deletions(-) create mode 100644 DSL/Resql/update-major-version-dataset-group.sql rename DSL/Resql/{set-old-dataset-group-for-validation.sql => update-minor-version-dataset-group.sql} (85%) create mode 100644 DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/major.yml diff --git a/DSL/Resql/insert-dataset-group-metadata.sql b/DSL/Resql/insert-dataset-group-metadata.sql index 9f721239..d13c0144 100644 --- a/DSL/Resql/insert-dataset-group-metadata.sql +++ b/DSL/Resql/insert-dataset-group-metadata.sql @@ -26,8 +26,8 @@ INSERT INTO "dataset_group_metadata" ( :latest, :is_enabled, :enable_allowed, - to_timestamp(:created_timestamp)::timestamp with time zone, - to_timestamp(:last_updated_timestamp)::timestamp with time zone, + :created_timestamp::timestamp with time zone, + :last_updated_timestamp::timestamp with time zone, :validation_status::Validation_Status, :processed_data_available, :raw_data_available, diff --git a/DSL/Resql/update-major-version-dataset-group.sql b/DSL/Resql/update-major-version-dataset-group.sql new file mode 100644 index 00000000..f382816a --- /dev/null +++ b/DSL/Resql/update-major-version-dataset-group.sql @@ -0,0 +1,33 @@ +WITH update_latest AS ( + UPDATE dataset_group_metadata + SET latest = false + WHERE group_key = :group_key +), +update_specific AS ( + UPDATE dataset_group_metadata + SET + major_version = ( + SELECT COALESCE(MAX(major_version), 0) + 1 + FROM dataset_group_metadata + WHERE group_key = :group_key + ), + connected_models = NULL::JSONB, + preprocess_data_location = NULL, + raw_data_location = NULL, + num_samples = 0, + num_pages = 0, + last_trained_timestamp = NULL, + validation_errors = NULL::JSONB, + last_model_trained = NULL, + enable_allowed = false, + validation_status = 'unvalidated'::Validation_Status, + is_enabled = false, + minor_version = 0, + patch_version = 0, + latest = true, + last_updated_timestamp = :last_updated_timestamp::timestamp with time zone, + validation_criteria = :validation_criteria::JSONB, + class_hierarchy = :class_hierarchy::JSONB + WHERE id = :id +) +SELECT 1; \ No newline at end of file diff --git a/DSL/Resql/set-old-dataset-group-for-validation.sql b/DSL/Resql/update-minor-version-dataset-group.sql similarity index 85% rename from DSL/Resql/set-old-dataset-group-for-validation.sql rename to DSL/Resql/update-minor-version-dataset-group.sql index 8e6e485e..3da45357 100644 --- a/DSL/Resql/set-old-dataset-group-for-validation.sql +++ b/DSL/Resql/update-minor-version-dataset-group.sql @@ -17,7 +17,7 @@ update_specific AS ( is_enabled = false, patch_version = 0, latest = true, - last_updated_timestamp = to_timestamp(:last_updated_timestamp)::timestamp with time zone + last_updated_timestamp = :last_updated_timestamp::timestamp with time zone WHERE id = :id RETURNING 1 ) diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/create.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/create.yml index 95b71626..382e5a71 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/create.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/create.yml @@ -130,8 +130,8 @@ create_dataset_group_metadata: latest: true is_enabled: false enable_allowed: false - created_timestamp: ${current_epoch} - last_updated_timestamp: ${current_epoch} + created_timestamp: ${new Date().toISOString()} + last_updated_timestamp: ${new Date().toISOString()} validation_status: unvalidated processed_data_available: false raw_data_available: false diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/major.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/major.yml new file mode 100644 index 00000000..e79a4037 --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/major.yml @@ -0,0 +1,102 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'MAJOR'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: dgId + type: number + description: "Body field 'dgId'" + - field: groupKey + type: string + description: "Body field 'groupKey'" + - field: validationCriteria + type: json + description: "Body field 'validationCriteria'" + - field: classHierarchy + type: json + description: "Body field 'classHierarchy'" + +extract_request_data: + assign: + dg_id: ${incoming.body.dgId} + group_key: ${incoming.body.groupKey} + validation_criteria: ${incoming.body.validationCriteria} + class_hierarchy: ${incoming.body.classHierarchy} + next: check_for_request_data + +check_for_request_data: + switch: + - condition: ${validation_criteria !== null && class_hierarchy !=null} + next: snapshot_dataset_group + next: return_incorrect_request + +snapshot_dataset_group: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/snapshot-dataset-group" + body: + id: ${dg_id} + result: res + next: check_status + +check_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: update_old_dataset_group + next: assign_fail_response + +update_old_dataset_group: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/update-major-version-dataset-group" + body: + id: ${dg_id} + group_key: ${group_key} + last_updated_timestamp: ${new Date().toISOString()} + validation_criteria: ${JSON.stringify(validation_criteria)} + class_hierarchy: ${JSON.stringify(class_hierarchy)} + result: res + next: check_old_dataset_status + +check_old_dataset_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: assign_success_response + next: assign_fail_response + +assign_success_response: + assign: + format_res: { + dgId: '${dg_id}', + operationSuccessful: true, + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + dgId: '${dg_id}', + operationSuccessful: false, + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_incorrect_request: + status: 400 + return: 'Missing Required Fields' + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end + diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/minor.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/minor.yml index 5730497f..a0052711 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/minor.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/minor.yml @@ -47,11 +47,11 @@ check_status: update_old_dataset_group: call: http.post args: - url: "[#CLASSIFIER_RESQL]/set-old-dataset-group-for-validation" + url: "[#CLASSIFIER_RESQL]/update-minor-version-dataset-group" body: id: ${dg_id} group_key: ${group_key} - last_updated_timestamp: ${Date.now()} + last_updated_timestamp: ${new Date().toISOString()} result: res next: check_old_dataset_status From a0c9b92ad0a298962a6bd94762589b2bd0a43f92 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Sun, 21 Jul 2024 10:13:35 +0530 Subject: [PATCH 192/582] ESCLASS-125: change epoch time setting flow --- .../DSL/POST/classifier/datasetgroup/create.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/create.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/create.yml index 382e5a71..9263bbb7 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/create.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/create.yml @@ -130,8 +130,8 @@ create_dataset_group_metadata: latest: true is_enabled: false enable_allowed: false - created_timestamp: ${new Date().toISOString()} - last_updated_timestamp: ${new Date().toISOString()} + created_timestamp: ${new Date(current_epoch).toISOString()} + last_updated_timestamp: ${new Date(current_epoch).toISOString()} validation_status: unvalidated processed_data_available: false raw_data_available: false From 4acb4af35d14697eac4ece6e5430450f3d46b03a Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Sun, 21 Jul 2024 18:55:16 +0530 Subject: [PATCH 193/582] ESCLASS-125-patch: implement patch version update to dataset group --- DSL/Resql/get-dataset-group-key-by-id.sql | 2 + .../update-patch-version-dataset-group.sql | 23 ++++ .../classifier/datasetgroup/update/patch.yml | 119 ++++++++++++++++++ constants.ini | 2 +- 4 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 DSL/Resql/get-dataset-group-key-by-id.sql create mode 100644 DSL/Resql/update-patch-version-dataset-group.sql create mode 100644 DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/patch.yml diff --git a/DSL/Resql/get-dataset-group-key-by-id.sql b/DSL/Resql/get-dataset-group-key-by-id.sql new file mode 100644 index 00000000..d6d7ac48 --- /dev/null +++ b/DSL/Resql/get-dataset-group-key-by-id.sql @@ -0,0 +1,2 @@ +SELECT group_key +FROM dataset_group_metadata WHERE id =:id; \ No newline at end of file diff --git a/DSL/Resql/update-patch-version-dataset-group.sql b/DSL/Resql/update-patch-version-dataset-group.sql new file mode 100644 index 00000000..5f1bee12 --- /dev/null +++ b/DSL/Resql/update-patch-version-dataset-group.sql @@ -0,0 +1,23 @@ +WITH update_latest AS ( + UPDATE dataset_group_metadata + SET latest = false + WHERE group_key = :group_key + RETURNING 1 +), +update_specific AS ( + UPDATE dataset_group_metadata + SET + patch_version = ( + SELECT COALESCE(MAX(patch_version), 0) + 1 + FROM dataset_group_metadata + WHERE group_key = :group_key + ), + enable_allowed = false, + validation_status = 'in-progress'::Validation_Status, + is_enabled = false, + latest = true, + last_updated_timestamp = :last_updated_timestamp::timestamp with time zone + WHERE id = :id + RETURNING 1 +) +SELECT 1; diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/patch.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/patch.yml new file mode 100644 index 00000000..22894ca1 --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/patch.yml @@ -0,0 +1,119 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'PATCH'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: dgId + type: number + description: "Body field 'dgId'" + - field: fields + type: array + description: "Body field 'fields'" + - field: updateDataPayload + type: json + description: "Body field 'updateDataPayload'" + headers: + - field: cookie + type: string + description: "Cookie field" + +extract_request_data: + assign: + dg_id: ${incoming.body.dgId} + group_key: ${incoming.body.groupKey} + fields: ${incoming.body.fields} + update_data_payload: ${incoming.body.updateDataPayload} + next: get_dataset_group + +get_dataset_group: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-dataset-group-key-by-id" + body: + id: ${dg_id} + result: res + next: check_status + +check_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: check_data_exist + next: return_not_found + +check_data_exist: + switch: + - condition: ${res.response.body.length>0} + next: assign_group_key + next: return_not_found + +assign_group_key: + assign: + group_key: ${res.response.body[0].groupKey} + next: update_old_dataset_group + +update_old_dataset_group: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/update-patch-version-dataset-group" + body: + id: ${dg_id} + group_key: ${group_key} + last_updated_timestamp: ${new Date().toISOString()} + result: res + next: check_old_dataset_status + +check_old_dataset_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: execute_cron_manager + next: assign_fail_response + +execute_cron_manager: + call: reflect.mock + args: + url: "[#CLASSIFIER_CRON_MANAGER]/execute//" + body: + cookie: ${incoming.header.cookie} + dgId: ${dg_id} + updateType: 'patch' + savedFilePath: null + patchPayload: ${update_data_payload} + result: res + next: assign_success_response + +assign_success_response: + assign: + format_res: { + dgId: '${dg_id}', + operationSuccessful: true, + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + dgId: '${dg_id}', + operationSuccessful: false, + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_not_found: + status: 400 + return: "Data Group Not Found" + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end + diff --git a/constants.ini b/constants.ini index 05af8045..92084529 100644 --- a/constants.ini +++ b/constants.ini @@ -17,4 +17,4 @@ OUTLOOK_CLIENT_ID=value OUTLOOK_SECRET_KEY=value OUTLOOK_REFRESH_KEY=value OUTLOOK_SCOPE=User.Read Mail.ReadWrite MailboxSettings.ReadWrite offline_access -DB_PASSWORD=value +DB_PASSWORD=rootcode From 3a6b3322de22cd59a23e886b0308d9090c96888a Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Sun, 21 Jul 2024 18:55:59 +0530 Subject: [PATCH 194/582] ESCLASS-125-patch: implement patch version update to dataset group --- constants.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/constants.ini b/constants.ini index 92084529..05af8045 100644 --- a/constants.ini +++ b/constants.ini @@ -17,4 +17,4 @@ OUTLOOK_CLIENT_ID=value OUTLOOK_SECRET_KEY=value OUTLOOK_REFRESH_KEY=value OUTLOOK_SCOPE=User.Read Mail.ReadWrite MailboxSettings.ReadWrite offline_access -DB_PASSWORD=rootcode +DB_PASSWORD=value From a5ff92c490fa2e519afe1bef32747d2ad9040353 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Sun, 21 Jul 2024 19:20:07 +0530 Subject: [PATCH 195/582] ESCLASS-125-patch: Remove major minor version API's groupKkey taken from request payload(from frontend) --- .../classifier/datasetgroup/update/major.yml | 36 +++++++++++++++---- .../classifier/datasetgroup/update/minor.yml | 36 +++++++++++++++---- 2 files changed, 58 insertions(+), 14 deletions(-) diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/major.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/major.yml index e79a4037..cfaa0497 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/major.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/major.yml @@ -11,9 +11,6 @@ declaration: - field: dgId type: number description: "Body field 'dgId'" - - field: groupKey - type: string - description: "Body field 'groupKey'" - field: validationCriteria type: json description: "Body field 'validationCriteria'" @@ -24,7 +21,6 @@ declaration: extract_request_data: assign: dg_id: ${incoming.body.dgId} - group_key: ${incoming.body.groupKey} validation_criteria: ${incoming.body.validationCriteria} class_hierarchy: ${incoming.body.classHierarchy} next: check_for_request_data @@ -42,14 +38,40 @@ snapshot_dataset_group: body: id: ${dg_id} result: res - next: check_status + next: check_snapshot_status -check_status: +check_snapshot_status: switch: - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} - next: update_old_dataset_group + next: get_dataset_group next: assign_fail_response +get_dataset_group: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-dataset-group-key-by-id" + body: + id: ${dg_id} + result: res + next: check_dataset_group_status + +check_dataset_group_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: check_data_exist + next: return_not_found + +check_data_exist: + switch: + - condition: ${res.response.body.length>0} + next: assign_group_key + next: return_not_found + +assign_group_key: + assign: + group_key: ${res.response.body[0].groupKey} + next: update_old_dataset_group + update_old_dataset_group: call: http.post args: diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/minor.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/minor.yml index a0052711..44aeb221 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/minor.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/minor.yml @@ -11,9 +11,6 @@ declaration: - field: dgId type: number description: "Body field 'dgId'" - - field: groupKey - type: string - description: "Body field 'groupKey'" - field: s3FilePath type: string description: "Body field 's3FilePath'" @@ -25,7 +22,6 @@ declaration: extract_request_data: assign: dg_id: ${incoming.body.dgId} - group_key: ${incoming.body.groupKey} s3_file_path: ${incoming.body.s3FilePath} next: snapshot_dataset_group @@ -36,14 +32,40 @@ snapshot_dataset_group: body: id: ${dg_id} result: res - next: check_status + next: check_snapshot_status -check_status: +check_snapshot_status: switch: - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} - next: update_old_dataset_group + next: get_dataset_group next: assign_fail_response +get_dataset_group: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-dataset-group-key-by-id" + body: + id: ${dg_id} + result: res + next: check_dataset_group_status + +check_dataset_group_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: check_data_exist + next: return_not_found + +check_data_exist: + switch: + - condition: ${res.response.body.length>0} + next: assign_group_key + next: return_not_found + +assign_group_key: + assign: + group_key: ${res.response.body[0].groupKey} + next: update_old_dataset_group + update_old_dataset_group: call: http.post args: From 8ad6010a9dc42d4cc74bf07e2e0f5d266c6f7bc6 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Sun, 21 Jul 2024 22:36:47 +0530 Subject: [PATCH 196/582] ESCLASS-125-patch:implement get dataset group schema data API and preprocess status update API --- DSL/Resql/get-dataset-group-schema.sql | 2 + ...update-dataset-group-preprocess-status.sql | 11 +++ .../GET/classifier/datasetgroup/schema.yml | 87 ++++++++++++++++ .../datasetgroup/update/preprocess/status.yml | 99 +++++++++++++++++++ 4 files changed, 199 insertions(+) create mode 100644 DSL/Resql/get-dataset-group-schema.sql create mode 100644 DSL/Resql/update-dataset-group-preprocess-status.sql create mode 100644 DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/schema.yml create mode 100644 DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/preprocess/status.yml diff --git a/DSL/Resql/get-dataset-group-schema.sql b/DSL/Resql/get-dataset-group-schema.sql new file mode 100644 index 00000000..2b0676f5 --- /dev/null +++ b/DSL/Resql/get-dataset-group-schema.sql @@ -0,0 +1,2 @@ +SELECT validation_criteria,class_hierarchy +FROM dataset_group_metadata WHERE id =:id; \ No newline at end of file diff --git a/DSL/Resql/update-dataset-group-preprocess-status.sql b/DSL/Resql/update-dataset-group-preprocess-status.sql new file mode 100644 index 00000000..2e8c7857 --- /dev/null +++ b/DSL/Resql/update-dataset-group-preprocess-status.sql @@ -0,0 +1,11 @@ +UPDATE dataset_group_metadata +SET + processed_data_available = :processed_data_available, + raw_data_available = :raw_data_available, + preprocess_data_location = :preprocess_data_location, + raw_data_location = :raw_data_location, + enable_allowed = :enable_allowed, + last_updated_timestamp = to_timestamp(:last_updated_timestamp)::timestamp with time zone, + num_samples = :num_samples, + num_pages = :num_pages +WHERE id = :id; diff --git a/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/schema.yml b/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/schema.yml new file mode 100644 index 00000000..01b6817a --- /dev/null +++ b/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/schema.yml @@ -0,0 +1,87 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'SCHEMA'" + method: get + accepts: json + returns: json + namespace: classifier + allowlist: + params: + - field: dgId + type: number + description: "Parameter 'dgId'" + +extract_data: + assign: + dg_id: ${Number(incoming.params.dgId)} + next: get_dataset_group_schema + +get_dataset_group_schema: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-dataset-group-schema" + body: + id: ${dg_id} + result: res_dataset + next: check_status + +check_status: + switch: + - condition: ${200 <= res_dataset.response.statusCodeValue && res_dataset.response.statusCodeValue < 300} + next: check_data_exist + next: assign_fail_response + +check_data_exist: + switch: + - condition: ${res_dataset.response.body.length>0} + next: assign_Json_format + next: assign_not_found_response + +assign_Json_format: + assign: + validationCriteria: ${JSON.parse(res_dataset.response.body[0].validationCriteria.value)} + classHierarchy: ${JSON.parse(res_dataset.response.body[0].classHierarchy.value)} + next: assign_success_response + +assign_success_response: + assign: + format_res: { + dgId: '${dg_id}', + operationSuccessful: true, + validationCriteria: '${validationCriteria}', + classHierarchy: '${classHierarchy}' + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + dgId: '${ dg_id }', + operationSuccessful: false + } + next: return_bad_request + +assign_not_found_response: + assign: + format_res: { + dgId: '${ dg_id }', + operationSuccessful: false, + errorResponse: "dataset doesn't exist" + } + next: return_not_found + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_not_found: + status: 404 + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/preprocess/status.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/preprocess/status.yml new file mode 100644 index 00000000..42a208fe --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/preprocess/status.yml @@ -0,0 +1,99 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'STATUS'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: dgId + type: number + description: "Body field 'dgId'" + - field: processedDataAvailable + type: boolean + description: "Body field 'processedDataAvailable'" + - field: rawDataAvailable + type: boolean + description: "Body field 'rawDataAvailable'" + - field: preprocessDataLocation + type: string + description: "Body field 'preprocessDataLocation'" + - field: rawDataLocation + type: string + description: "Body field 'rawDataLocation'" + - field: enableAllowed + type: boolean + description: "Body field 'enableAllowed'" + - field: lastUpdatedTimestamp + type: integer + description: "Body field 'lastUpdatedTimestamp'" + - field: numSamples + type: integer + description: "Body field 'numSamples'" + - field: numPages + type: integer + description: "Body field 'numPages'" + +extract_request_data: + assign: + dg_id: ${incoming.body.dgId} + processed_data_available: ${incoming.body.processedDataAvailable} + raw_data_available: ${incoming.body.rawDataAvailable} + preprocess_data_location: ${incoming.body.preprocessDataLocation} + raw_data_location: ${incoming.body.rawDataLocation} + enable_allowed: ${incoming.body.enableAllowed} + last_updated_timestamp: ${incoming.body.lastUpdatedTimestamp} + num_samples: ${incoming.body.numSamples} + num_pages: ${incoming.body.numPages} + next: update_dataset_group_preprocess_status + +update_dataset_group_preprocess_status: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/update-dataset-group-preprocess-status" + body: + id: ${dg_id} + processed_data_available: ${processed_data_available} + raw_data_available: ${raw_data_available} + preprocess_data_location: ${preprocess_data_location} + raw_data_location: ${raw_data_location} + enable_allowed: ${enable_allowed} + last_updated_timestamp: ${last_updated_timestamp} + num_samples: ${num_samples} + num_pages: ${num_pages} + result: res + next: check_preprocess_status + +check_preprocess_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: assign_success_response + next: assign_fail_response + +assign_success_response: + assign: + format_res: { + dgId: '${dg_id}', + operationSuccessful: true + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + dgId: '${dg_id}', + operationSuccessful: false, + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end \ No newline at end of file From 177d1b2a69be116f6d8403d6013aeac1ee9a328b Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Sun, 21 Jul 2024 23:25:28 +0530 Subject: [PATCH 197/582] remove minor update in import endpoint --- s3-ferry/file_api.py | 28 ++-------------------------- 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/s3-ferry/file_api.py b/s3-ferry/file_api.py index b4f19b9d..99344544 100644 --- a/s3-ferry/file_api.py +++ b/s3-ferry/file_api.py @@ -38,19 +38,8 @@ async def authenticate_user(request: Request): raise HTTPException(status_code=response.status_code, detail="Authentication failed") @app.post("/datasetgroup/data/import") -async def upload_and_copy(request: Request, dg_id: int = Form(...), import_type: str = Form(...), data_file: UploadFile = File(...)): +async def upload_and_copy(request: Request, dg_id: int = Form(...), data_file: UploadFile = File(...)): await authenticate_user(request) - if import_type not in ["major","minor"]: - raise HTTPException( - status_code=500, - detail={ - "upload_status": 500, - "operation_successful": False, - "saved_file_path": None, - "reason" : "import_type should be either minor or major." - } - ) - file_location = os.path.join(UPLOAD_DIRECTORY, data_file.filename) file_name = data_file.filename with open(file_location, "wb") as f: @@ -73,20 +62,7 @@ async def upload_and_copy(request: Request, dg_id: int = Form(...), import_type: with open(json_local_file_path, 'w') as json_file: json.dump(converted_data, json_file, indent=4) - if import_type == "minor": - save_location = f"/dataset/{dg_id}/minor_update_temp/minor_update_.json" - elif import_type == "major": - save_location = f"/dataset/{dg_id}/primary_dataset/dataset_{dg_id}_aggregated.json" - else: - raise HTTPException( - status_code=500, - detail={ - "upload_status": 500, - "operation_successful": False, - "saved_file_path": None, - "reason" : "import_type should be either minor or major." - } - ) + save_location = f"/dataset/{dg_id}/primary_dataset/dataset_{dg_id}_aggregated.json" payload = { "destinationFilePath": save_location, From 649bdeda9a37b686144d2750347a8e9d829bb945 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Sun, 21 Jul 2024 23:36:37 +0530 Subject: [PATCH 198/582] Remove functions from return payloads --- s3-ferry/file_api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/s3-ferry/file_api.py b/s3-ferry/file_api.py index 99344544..60526893 100644 --- a/s3-ferry/file_api.py +++ b/s3-ferry/file_api.py @@ -63,11 +63,12 @@ async def upload_and_copy(request: Request, dg_id: int = Form(...), data_file: U json.dump(converted_data, json_file, indent=4) save_location = f"/dataset/{dg_id}/primary_dataset/dataset_{dg_id}_aggregated.json" + source_file_path = file_name.replace('.yml', '.json').replace('.xlsx', ".json"), payload = { "destinationFilePath": save_location, "destinationStorageType": "S3", - "sourceFilePath": file_name.replace('.yml', '.json').replace('.xlsx', ".json"), + "sourceFilePath": source_file_path, "sourceStorageType": "FS" } From c979f950063a1c70f7112f61ee378d75503b515e Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Sun, 21 Jul 2024 23:45:42 +0530 Subject: [PATCH 199/582] Converting parameters and varibale names to camelCase --- s3-ferry/file_api.py | 96 ++++++++++++++++++++++---------------------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/s3-ferry/file_api.py b/s3-ferry/file_api.py index 60526893..8e6dfa55 100644 --- a/s3-ferry/file_api.py +++ b/s3-ferry/file_api.py @@ -13,9 +13,9 @@ S3_FERRY_URL = os.getenv("S3_FERRY_URL") class ExportFile(BaseModel): - dg_id: int + dgId: int version: str - export_type: str + exportType: str if not os.path.exists(UPLOAD_DIRECTORY): os.makedirs(UPLOAD_DIRECTORY) @@ -38,15 +38,15 @@ async def authenticate_user(request: Request): raise HTTPException(status_code=response.status_code, detail="Authentication failed") @app.post("/datasetgroup/data/import") -async def upload_and_copy(request: Request, dg_id: int = Form(...), data_file: UploadFile = File(...)): +async def upload_and_copy(request: Request, dgId: int = Form(...), dataFile: UploadFile = File(...)): await authenticate_user(request) - file_location = os.path.join(UPLOAD_DIRECTORY, data_file.filename) - file_name = data_file.filename - with open(file_location, "wb") as f: - f.write(data_file.file.read()) + fileLocation = os.path.join(UPLOAD_DIRECTORY, dataFile.filename) + fileName = dataFile.filename + with open(fileLocation, "wb") as f: + f.write(dataFile.file.read()) - file_converter = FileConverter() - success, converted_data = file_converter.convert_to_json(file_location) + fileConverter = FileConverter() + success, convertedData = fileConverter.convert_to_json(fileLocation) if not success: raise HTTPException( status_code=500, @@ -58,31 +58,31 @@ async def upload_and_copy(request: Request, dg_id: int = Form(...), data_file: U } ) - json_local_file_path = file_location.replace('.yaml', '.json').replace('.yml', '.json').replace('.xlsx', ".json") - with open(json_local_file_path, 'w') as json_file: - json.dump(converted_data, json_file, indent=4) + jsonLocalFilePath = fileLocation.replace('.yaml', '.json').replace('.yml', '.json').replace('.xlsx', ".json") + with open(jsonLocalFilePath, 'w') as jsonFile: + json.dump(convertedData, jsonFile, indent=4) - save_location = f"/dataset/{dg_id}/primary_dataset/dataset_{dg_id}_aggregated.json" - source_file_path = file_name.replace('.yml', '.json').replace('.xlsx', ".json"), + saveLocation = f"/dataset/{dgId}/primary_dataset/dataset_{dgId}_aggregated.json" + sourceFilePath = fileName.replace('.yml', '.json').replace('.xlsx', ".json"), payload = { - "destinationFilePath": save_location, + "destinationFilePath": saveLocation, "destinationStorageType": "S3", - "sourceFilePath": source_file_path, + "sourceFilePath": sourceFilePath, "sourceStorageType": "FS" } response = requests.post(S3_FERRY_URL, json=payload) if response.status_code == 201: - os.remove(file_location) - if(file_location!=json_local_file_path): - os.remove(json_local_file_path) - response_data = { + os.remove(fileLocation) + if(fileLocation!=jsonLocalFilePath): + os.remove(jsonLocalFilePath) + responseData = { "upload_status": 200, "operation_successful": True, - "saved_file_path": save_location + "saved_file_path": saveLocation } - return JSONResponse(status_code=200, content=response_data) + return JSONResponse(status_code=200, content=responseData) else: raise HTTPException( status_code=500, @@ -96,13 +96,13 @@ async def upload_and_copy(request: Request, dg_id: int = Form(...), data_file: U @app.post("/datasetgroup/data/download") -async def download_and_convert(request: Request, export_data: ExportFile): +async def download_and_convert(request: Request, exportData: ExportFile): await authenticate_user(request) - dg_id = export_data.dg_id - version = export_data.version - export_type = export_data.export_type + dgId = exportData.dgId + version = exportData.version + exportType = exportData.exportType - if export_type not in ["xlsx", "yaml", "json"]: + if exportType not in ["xlsx", "yaml", "json"]: raise HTTPException( status_code=500, detail={ @@ -114,12 +114,12 @@ async def download_and_convert(request: Request, export_data: ExportFile): ) if version == "minor": - save_location = f"/dataset/{dg_id}/minor_update_temp/minor_update_.json" - local_file_name = f"group_{dg_id}minor_update" + saveLocation = f"/dataset/{dgId}/minor_update_temp/minor_update_.json" + localFileName = f"group_{dgId}minor_update" elif version == "major": - save_location = f"/dataset/{dg_id}/primary_dataset/dataset_{dg_id}_aggregated.json" - local_file_name = f"group_{dg_id}_aggregated" + saveLocation = f"/dataset/{dgId}/primary_dataset/dataset_{dgId}_aggregated.json" + localFileName = f"group_{dgId}_aggregated" else: raise HTTPException( status_code=500, @@ -132,9 +132,9 @@ async def download_and_convert(request: Request, export_data: ExportFile): ) payload = { - "destinationFilePath": f"{local_file_name}.json", + "destinationFilePath": f"{localFileName}.json", "destinationStorageType": "FS", - "sourceFilePath": save_location, + "sourceFilePath": saveLocation, "sourceStorageType": "S3" } @@ -150,23 +150,23 @@ async def download_and_convert(request: Request, export_data: ExportFile): } ) - shared_directory = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'shared') - json_file_path = os.path.join(shared_directory, f"{local_file_name}.json") + sharedDirectory = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'shared') + jsonFilePath = os.path.join(sharedDirectory, f"{localFileName}.json") - json_file_path = os.path.join('..', 'shared', f"{local_file_name}.json") + jsonFilePath = os.path.join('..', 'shared', f"{localFileName}.json") - file_converter = FileConverter() - with open(f"{json_file_path}", 'r') as json_file: - json_data = json.load(json_file) + fileConverter = FileConverter() + with open(f"{jsonFilePath}", 'r') as jsonFile: + jsonData = json.load(jsonFile) - if export_type == "xlsx": - output_file = f"{local_file_name}.xlsx" - file_converter.convert_json_to_xlsx(json_data, output_file) - elif export_type == "yaml": - output_file = f"{local_file_name}.yaml" - file_converter.convert_json_to_yaml(json_data, output_file) - elif export_type == "json": - output_file = f"{json_file_path}" + if exportType == "xlsx": + outputFile = f"{localFileName}.xlsx" + fileConverter.convert_json_to_xlsx(jsonData, outputFile) + elif exportType == "yaml": + outputFile = f"{localFileName}.yaml" + fileConverter.convert_json_to_yaml(jsonData, outputFile) + elif exportType == "json": + outputFile = f"{jsonFilePath}" else: raise HTTPException( status_code=500, @@ -178,4 +178,4 @@ async def download_and_convert(request: Request, export_data: ExportFile): } ) - return FileResponse(output_file, filename=os.path.basename(output_file)) \ No newline at end of file + return FileResponse(outputFile, filename=os.path.basename(outputFile)) From d2e7d0e9d8fa03b674d92effacd3efc53ec0f7b0 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Sun, 21 Jul 2024 23:57:37 +0530 Subject: [PATCH 200/582] file_converter camelCase change --- s3-ferry/file_converter.py | 70 +++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/s3-ferry/file_converter.py b/s3-ferry/file_converter.py index 18d68309..1ff9678f 100644 --- a/s3-ferry/file_converter.py +++ b/s3-ferry/file_converter.py @@ -7,77 +7,77 @@ class FileConverter: def __init__(self): pass - def _detect_file_type(self, file_path): - if file_path.endswith('.json'): + def _detect_file_type(self, filePath): + if filePath.endswith('.json'): return 'json' - elif file_path.endswith('.yaml') or file_path.endswith('.yml'): + elif filePath.endswith('.yaml') or filePath.endswith('.yml'): return 'yaml' - elif file_path.endswith('.xlsx'): + elif filePath.endswith('.xlsx'): return 'xlsx' else: return None - def convert_to_json(self, file_path): - file_type = self._detect_file_type(file_path) - if file_type is None: - print(f"Error: Unsupported file type for '{file_path}'") + def convert_to_json(self, filePath): + fileType = self._detect_file_type(filePath) + if fileType is None: + print(f"Error: Unsupported file type for '{filePath}'") return (False, {}) try: - if file_type == 'json': - return self._load_json(file_path) - elif file_type == 'yaml': - return self._convert_yaml_to_json(file_path) - elif file_type == 'xlsx': - return self._convert_xlsx_to_json(file_path) + if fileType == 'json': + return self._load_json(filePath) + elif fileType == 'yaml': + return self._convert_yaml_to_json(filePath) + elif fileType == 'xlsx': + return self._convert_xlsx_to_json(filePath) except Exception as e: - print(f"Error processing '{file_path}': {e}") + print(f"Error processing '{filePath}': {e}") return (False, {}) - def _load_json(self, file_path): + def _load_json(self, filePath): try: - with open(file_path, 'r') as file: + with open(filePath, 'r') as file: data = json.load(file) return (True, data) except Exception as e: - print(f"Error loading JSON file '{file_path}': {e}") + print(f"Error loading JSON file '{filePath}': {e}") return (False, {}) - def _convert_yaml_to_json(self, file_path): + def _convert_yaml_to_json(self, filePath): try: - with open(file_path, 'r') as file: + with open(filePath, 'r') as file: data = yaml.safe_load(file) return (True, data) except Exception as e: - print(f"Error converting YAML file '{file_path}' to JSON: {e}") + print(f"Error converting YAML file '{filePath}' to JSON: {e}") return (False, {}) - def _convert_xlsx_to_json(self, file_path): + def _convert_xlsx_to_json(self, filePath): try: - data = pd.read_excel(file_path, sheet_name=None) - json_data = {sheet: data[sheet].to_dict(orient='records') for sheet in data} - return (True, json_data) + data = pd.read_excel(filePath, sheet_name=None) + jsonData = {sheet: data[sheet].to_dict(orient='records') for sheet in data} + return (True, jsonData) except Exception as e: - print(f"Error converting XLSX file '{file_path}' to JSON: {e}") + print(f"Error converting XLSX file '{filePath}' to JSON: {e}") return (False, {}) - def convert_json_to_xlsx(self, json_data, output_path): + def convert_json_to_xlsx(self, jsonData, outputPath): try: - with pd.ExcelWriter(output_path) as writer: - for sheet_name, data in json_data.items(): + with pd.ExcelWriter(outputPath) as writer: + for sheetName, data in jsonData.items(): df = pd.DataFrame(data) - df.to_excel(writer, sheet_name=sheet_name, index=False) - print(f"JSON data successfully converted to XLSX and saved at '{output_path}'") + df.to_excel(writer, sheet_name=sheetName, index=False) + print(f"JSON data successfully converted to XLSX and saved at '{outputPath}'") return True except Exception as e: print(f"Error converting JSON to XLSX: {e}") return False - def convert_json_to_yaml(self, json_data, output_path): + def convert_json_to_yaml(self, jsonData, outputPath): try: - with open(output_path, 'w') as file: - yaml.dump(json_data, file) - print(f"JSON data successfully converted to YAML and saved at '{output_path}'") + with open(outputPath, 'w') as file: + yaml.dump(jsonData, file) + print(f"JSON data successfully converted to YAML and saved at '{outputPath}'") return True except Exception as e: print(f"Error converting JSON to YAML: {e}") From e07142b9e215c91695f8aeeea08a308d725f9d22 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 22 Jul 2024 00:17:04 +0530 Subject: [PATCH 201/582] Security fixes --- s3-ferry/file_api.py | 9 ++++----- s3-ferry/file_converter.py | 2 -- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/s3-ferry/file_api.py b/s3-ferry/file_api.py index 8e6dfa55..02f6909b 100644 --- a/s3-ferry/file_api.py +++ b/s3-ferry/file_api.py @@ -5,6 +5,7 @@ from file_converter import FileConverter import json from pydantic import BaseModel +import uuid app = FastAPI() @@ -40,8 +41,9 @@ async def authenticate_user(request: Request): @app.post("/datasetgroup/data/import") async def upload_and_copy(request: Request, dgId: int = Form(...), dataFile: UploadFile = File(...)): await authenticate_user(request) - fileLocation = os.path.join(UPLOAD_DIRECTORY, dataFile.filename) - fileName = dataFile.filename + fileName = f"{uuid.uuid4()}_{dataFile.filename}" + fileLocation = os.path.join(UPLOAD_DIRECTORY, fileName) + with open(fileLocation, "wb") as f: f.write(dataFile.file.read()) @@ -150,9 +152,6 @@ async def download_and_convert(request: Request, exportData: ExportFile): } ) - sharedDirectory = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'shared') - jsonFilePath = os.path.join(sharedDirectory, f"{localFileName}.json") - jsonFilePath = os.path.join('..', 'shared', f"{localFileName}.json") fileConverter = FileConverter() diff --git a/s3-ferry/file_converter.py b/s3-ferry/file_converter.py index 1ff9678f..1df5bf09 100644 --- a/s3-ferry/file_converter.py +++ b/s3-ferry/file_converter.py @@ -4,8 +4,6 @@ import pandas as pd class FileConverter: - def __init__(self): - pass def _detect_file_type(self, filePath): if filePath.endswith('.json'): From 466aaf060f6753331b1b15300d1b7247e78a4ae8 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 22 Jul 2024 00:21:04 +0530 Subject: [PATCH 202/582] merge conflict fix --- docker-compose.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index dff753cf..ac1af2d8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -158,6 +158,19 @@ services: - bykstack restart: always + cron-manager: + container_name: cron-manager + image: cron-manager + volumes: + - ./DSL/CronManager/DSL:/DSL + - ./DSL/CronManager/script:/app/scripts + - ./DSL/CronManager/config:/app/config + environment: + - server.port=9010 + ports: + - 9010:8080 + networks: + - bykstack init: image: busybox From 93a601b7b873b3cf22b96069d240fc1d5790a60d Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 22 Jul 2024 00:27:59 +0530 Subject: [PATCH 203/582] Adding return payloads to constants --- s3-ferry/constants.py | 42 ++++++++++++++++++++++++ s3-ferry/file_api.py | 74 ++++++++----------------------------------- 2 files changed, 55 insertions(+), 61 deletions(-) create mode 100644 s3-ferry/constants.py diff --git a/s3-ferry/constants.py b/s3-ferry/constants.py new file mode 100644 index 00000000..84cae450 --- /dev/null +++ b/s3-ferry/constants.py @@ -0,0 +1,42 @@ +# constants.py + +UPLOAD_FAILED = { + "upload_status": 500, + "operation_successful": False, + "saved_file_path": None, + "reason": "" +} + +UPLOAD_SUCCESS = { + "upload_status": 200, + "operation_successful": True, + "saved_file_path": "" +} + +EXPORT_TYPE_ERROR = { + "upload_status": 500, + "operation_successful": False, + "saved_file_path": None, + "reason": "export_type should be either json, xlsx or yaml." +} + +IMPORT_TYPE_ERROR = { + "upload_status": 500, + "operation_successful": False, + "saved_file_path": None, + "reason": "import_type should be either minor or major." +} + +S3_UPLOAD_FAILED = { + "upload_status": 500, + "operation_successful": False, + "saved_file_path": None, + "reason": "Failed to upload to S3" +} + +S3_DOWNLOAD_FAILED = { + "upload_status": 500, + "operation_successful": False, + "saved_file_path": None, + "reason": "Failed to download from S3" +} diff --git a/s3-ferry/file_api.py b/s3-ferry/file_api.py index 02f6909b..6c881cac 100644 --- a/s3-ferry/file_api.py +++ b/s3-ferry/file_api.py @@ -6,6 +6,7 @@ import json from pydantic import BaseModel import uuid +from constants import UPLOAD_FAILED, UPLOAD_SUCCESS, EXPORT_TYPE_ERROR, IMPORT_TYPE_ERROR, S3_UPLOAD_FAILED, S3_DOWNLOAD_FAILED app = FastAPI() @@ -50,15 +51,9 @@ async def upload_and_copy(request: Request, dgId: int = Form(...), dataFile: Upl fileConverter = FileConverter() success, convertedData = fileConverter.convert_to_json(fileLocation) if not success: - raise HTTPException( - status_code=500, - detail={ - "upload_status": 500, - "operation_successful": False, - "saved_file_path": None, - "reason" : "Json file convert failed." - } - ) + upload_failed = UPLOAD_FAILED.copy() + upload_failed["reason"] = "Json file convert failed." + raise HTTPException(status_code=500, detail=upload_failed) jsonLocalFilePath = fileLocation.replace('.yaml', '.json').replace('.yml', '.json').replace('.xlsx', ".json") with open(jsonLocalFilePath, 'w') as jsonFile: @@ -77,24 +72,13 @@ async def upload_and_copy(request: Request, dgId: int = Form(...), dataFile: Upl response = requests.post(S3_FERRY_URL, json=payload) if response.status_code == 201: os.remove(fileLocation) - if(fileLocation!=jsonLocalFilePath): + if fileLocation != jsonLocalFilePath: os.remove(jsonLocalFilePath) - responseData = { - "upload_status": 200, - "operation_successful": True, - "saved_file_path": saveLocation - } - return JSONResponse(status_code=200, content=responseData) + upload_success = UPLOAD_SUCCESS.copy() + upload_success["saved_file_path"] = saveLocation + return JSONResponse(status_code=200, content=upload_success) else: - raise HTTPException( - status_code=500, - detail={ - "upload_status": 500, - "operation_successful": False, - "saved_file_path": None, - "reason" : "Failed to upload to S3" - } - ) + raise HTTPException(status_code=500, detail=S3_UPLOAD_FAILED) @app.post("/datasetgroup/data/download") @@ -105,15 +89,7 @@ async def download_and_convert(request: Request, exportData: ExportFile): exportType = exportData.exportType if exportType not in ["xlsx", "yaml", "json"]: - raise HTTPException( - status_code=500, - detail={ - "upload_status": 500, - "operation_successful": False, - "saved_file_path": None, - "reason": "export_type should be either json, xlsx or yaml." - } - ) + raise HTTPException(status_code=500, detail=EXPORT_TYPE_ERROR) if version == "minor": saveLocation = f"/dataset/{dgId}/minor_update_temp/minor_update_.json" @@ -123,15 +99,7 @@ async def download_and_convert(request: Request, exportData: ExportFile): saveLocation = f"/dataset/{dgId}/primary_dataset/dataset_{dgId}_aggregated.json" localFileName = f"group_{dgId}_aggregated" else: - raise HTTPException( - status_code=500, - detail={ - "upload_status": 500, - "operation_successful": False, - "saved_file_path": None, - "reason": "import_type should be either minor or major." - } - ) + raise HTTPException(status_code=500, detail=IMPORT_TYPE_ERROR) payload = { "destinationFilePath": f"{localFileName}.json", @@ -142,15 +110,7 @@ async def download_and_convert(request: Request, exportData: ExportFile): response = requests.post(S3_FERRY_URL, json=payload) if response.status_code != 201: - raise HTTPException( - status_code=500, - detail={ - "upload_status": 500, - "operation_successful": False, - "saved_file_path": None, - "reason": "Failed to download from S3" - } - ) + raise HTTPException(status_code=500, detail=S3_DOWNLOAD_FAILED) jsonFilePath = os.path.join('..', 'shared', f"{localFileName}.json") @@ -167,14 +127,6 @@ async def download_and_convert(request: Request, exportData: ExportFile): elif exportType == "json": outputFile = f"{jsonFilePath}" else: - raise HTTPException( - status_code=500, - detail={ - "upload_status": 500, - "operation_successful": False, - "saved_file_path": None, - "reason": "export_type should be either json, xlsx or yaml." - } - ) + raise HTTPException(status_code=500, detail=EXPORT_TYPE_ERROR) return FileResponse(outputFile, filename=os.path.basename(outputFile)) From 7f4dde88e12ce37a51292b57e92dcba41b37e4f1 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 22 Jul 2024 00:31:43 +0530 Subject: [PATCH 204/582] isolating s3 ferry logic in different class --- s3-ferry/file_api.py | 28 +++++++--------------------- s3-ferry/s3_ferry.py | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 21 deletions(-) create mode 100644 s3-ferry/s3_ferry.py diff --git a/s3-ferry/file_api.py b/s3-ferry/file_api.py index 6c881cac..f74fd6f5 100644 --- a/s3-ferry/file_api.py +++ b/s3-ferry/file_api.py @@ -1,18 +1,19 @@ from fastapi import FastAPI, File, UploadFile, HTTPException, Form, Request from fastapi.responses import FileResponse, JSONResponse import os -import requests -from file_converter import FileConverter import json -from pydantic import BaseModel import uuid +from pydantic import BaseModel +from file_converter import FileConverter from constants import UPLOAD_FAILED, UPLOAD_SUCCESS, EXPORT_TYPE_ERROR, IMPORT_TYPE_ERROR, S3_UPLOAD_FAILED, S3_DOWNLOAD_FAILED +from s3_ferry import S3Ferry app = FastAPI() UPLOAD_DIRECTORY = os.getenv("UPLOAD_DIRECTORY", "/shared") RUUTER_PRIVATE_URL = os.getenv("RUUTER_PRIVATE_URL") S3_FERRY_URL = os.getenv("S3_FERRY_URL") +s3_ferry = S3Ferry(S3_FERRY_URL) class ExportFile(BaseModel): dgId: int @@ -60,16 +61,9 @@ async def upload_and_copy(request: Request, dgId: int = Form(...), dataFile: Upl json.dump(convertedData, jsonFile, indent=4) saveLocation = f"/dataset/{dgId}/primary_dataset/dataset_{dgId}_aggregated.json" - sourceFilePath = fileName.replace('.yml', '.json').replace('.xlsx', ".json"), + sourceFilePath = fileName.replace('.yml', '.json').replace('.xlsx', ".json") - payload = { - "destinationFilePath": saveLocation, - "destinationStorageType": "S3", - "sourceFilePath": sourceFilePath, - "sourceStorageType": "FS" - } - - response = requests.post(S3_FERRY_URL, json=payload) + response = s3_ferry.transfer_file(saveLocation, "S3", sourceFilePath, "FS") if response.status_code == 201: os.remove(fileLocation) if fileLocation != jsonLocalFilePath: @@ -94,21 +88,13 @@ async def download_and_convert(request: Request, exportData: ExportFile): if version == "minor": saveLocation = f"/dataset/{dgId}/minor_update_temp/minor_update_.json" localFileName = f"group_{dgId}minor_update" - elif version == "major": saveLocation = f"/dataset/{dgId}/primary_dataset/dataset_{dgId}_aggregated.json" localFileName = f"group_{dgId}_aggregated" else: raise HTTPException(status_code=500, detail=IMPORT_TYPE_ERROR) - payload = { - "destinationFilePath": f"{localFileName}.json", - "destinationStorageType": "FS", - "sourceFilePath": saveLocation, - "sourceStorageType": "S3" - } - - response = requests.post(S3_FERRY_URL, json=payload) + response = s3_ferry.transfer_file(f"{localFileName}.json", "FS", saveLocation, "S3") if response.status_code != 201: raise HTTPException(status_code=500, detail=S3_DOWNLOAD_FAILED) diff --git a/s3-ferry/s3_ferry.py b/s3-ferry/s3_ferry.py new file mode 100644 index 00000000..7585ada8 --- /dev/null +++ b/s3-ferry/s3_ferry.py @@ -0,0 +1,16 @@ +import requests + +class S3Ferry: + def __init__(self, url): + self.url = url + + def transfer_file(self, destinationFilePath, destinationStorageType, sourceFilePath, sourceStorageType): + payload = { + "destinationFilePath": destinationFilePath, + "destinationStorageType": destinationStorageType, + "sourceFilePath": sourceFilePath, + "sourceStorageType": sourceStorageType + } + + response = requests.post(self.url, json=payload) + return response From d7d707d7cd00252c8156d1be423a0e7bb4bc19b2 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 22 Jul 2024 00:38:59 +0530 Subject: [PATCH 205/582] adding file extentions to the constants. (SonarCloud suggestion) --- s3-ferry/constants.py | 5 +++++ s3-ferry/file_api.py | 21 +++++++++++---------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/s3-ferry/constants.py b/s3-ferry/constants.py index 84cae450..74000d9b 100644 --- a/s3-ferry/constants.py +++ b/s3-ferry/constants.py @@ -40,3 +40,8 @@ "saved_file_path": None, "reason": "Failed to download from S3" } + +JSON_EXT = ".json" +YAML_EXT = ".yaml" +YML_EXT = ".yml" +XLSX_EXT = ".xlsx" \ No newline at end of file diff --git a/s3-ferry/file_api.py b/s3-ferry/file_api.py index f74fd6f5..ebc26812 100644 --- a/s3-ferry/file_api.py +++ b/s3-ferry/file_api.py @@ -3,9 +3,10 @@ import os import json import uuid +import requests from pydantic import BaseModel from file_converter import FileConverter -from constants import UPLOAD_FAILED, UPLOAD_SUCCESS, EXPORT_TYPE_ERROR, IMPORT_TYPE_ERROR, S3_UPLOAD_FAILED, S3_DOWNLOAD_FAILED +from constants import UPLOAD_FAILED, UPLOAD_SUCCESS, EXPORT_TYPE_ERROR, IMPORT_TYPE_ERROR, S3_UPLOAD_FAILED, S3_DOWNLOAD_FAILED, JSON_EXT, YAML_EXT, YML_EXT, XLSX_EXT from s3_ferry import S3Ferry app = FastAPI() @@ -56,12 +57,12 @@ async def upload_and_copy(request: Request, dgId: int = Form(...), dataFile: Upl upload_failed["reason"] = "Json file convert failed." raise HTTPException(status_code=500, detail=upload_failed) - jsonLocalFilePath = fileLocation.replace('.yaml', '.json').replace('.yml', '.json').replace('.xlsx', ".json") + jsonLocalFilePath = fileLocation.replace(YAML_EXT, JSON_EXT).replace(YML_EXT, JSON_EXT).replace(XLSX_EXT, JSON_EXT) with open(jsonLocalFilePath, 'w') as jsonFile: json.dump(convertedData, jsonFile, indent=4) - saveLocation = f"/dataset/{dgId}/primary_dataset/dataset_{dgId}_aggregated.json" - sourceFilePath = fileName.replace('.yml', '.json').replace('.xlsx', ".json") + saveLocation = f"/dataset/{dgId}/primary_dataset/dataset_{dgId}_aggregated{JSON_EXT}" + sourceFilePath = fileName.replace(YML_EXT, JSON_EXT).replace(XLSX_EXT, JSON_EXT) response = s3_ferry.transfer_file(saveLocation, "S3", sourceFilePath, "FS") if response.status_code == 201: @@ -86,29 +87,29 @@ async def download_and_convert(request: Request, exportData: ExportFile): raise HTTPException(status_code=500, detail=EXPORT_TYPE_ERROR) if version == "minor": - saveLocation = f"/dataset/{dgId}/minor_update_temp/minor_update_.json" + saveLocation = f"/dataset/{dgId}/minor_update_temp/minor_update_{JSON_EXT}" localFileName = f"group_{dgId}minor_update" elif version == "major": - saveLocation = f"/dataset/{dgId}/primary_dataset/dataset_{dgId}_aggregated.json" + saveLocation = f"/dataset/{dgId}/primary_dataset/dataset_{dgId}_aggregated{JSON_EXT}" localFileName = f"group_{dgId}_aggregated" else: raise HTTPException(status_code=500, detail=IMPORT_TYPE_ERROR) - response = s3_ferry.transfer_file(f"{localFileName}.json", "FS", saveLocation, "S3") + response = s3_ferry.transfer_file(f"{localFileName}{JSON_EXT}", "FS", saveLocation, "S3") if response.status_code != 201: raise HTTPException(status_code=500, detail=S3_DOWNLOAD_FAILED) - jsonFilePath = os.path.join('..', 'shared', f"{localFileName}.json") + jsonFilePath = os.path.join('..', 'shared', f"{localFileName}{JSON_EXT}") fileConverter = FileConverter() with open(f"{jsonFilePath}", 'r') as jsonFile: jsonData = json.load(jsonFile) if exportType == "xlsx": - outputFile = f"{localFileName}.xlsx" + outputFile = f"{localFileName}{XLSX_EXT}" fileConverter.convert_json_to_xlsx(jsonData, outputFile) elif exportType == "yaml": - outputFile = f"{localFileName}.yaml" + outputFile = f"{localFileName}{YAML_EXT}" fileConverter.convert_json_to_yaml(jsonData, outputFile) elif exportType == "json": outputFile = f"{jsonFilePath}" From 4c1f340adaddc7c26c5bdad8fef8f6785988ef0a Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 22 Jul 2024 00:45:19 +0530 Subject: [PATCH 206/582] removing testing --- s3-ferry/file_converter.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/s3-ferry/file_converter.py b/s3-ferry/file_converter.py index 1df5bf09..9a057429 100644 --- a/s3-ferry/file_converter.py +++ b/s3-ferry/file_converter.py @@ -79,18 +79,4 @@ def convert_json_to_yaml(self, jsonData, outputPath): return True except Exception as e: print(f"Error converting JSON to YAML: {e}") - return False - -if __name__ == "__main__": - converter = FileConverter() - - # Convert files to JSON - file_paths = ['example.json', 'example.yaml', 'example.xlsx', 'example.txt'] - for file_path in file_paths: - success, json_data = converter.convert_to_json(file_path) - if success: - print(f"JSON data for '{file_path}':\n{json.dumps(json_data, indent=4)}\n") - - if json_data: - converter.convert_json_to_xlsx(json_data, 'output.xlsx') - converter.convert_json_to_yaml(json_data, 'output.yaml') + return False \ No newline at end of file From b54660743cb7bf6510df0516717093bc69631a93 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 22 Jul 2024 10:17:51 +0530 Subject: [PATCH 207/582] merge conflict fix --- docker-compose.yml | 52 +--------------------------------------------- 1 file changed, 1 insertion(+), 51 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index ac1af2d8..bf0fca21 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -172,59 +172,9 @@ services: networks: - bykstack - init: - image: busybox - command: ["sh", "-c", "chmod -R 777 /shared"] - volumes: - - shared-volume:/shared - - python_file_api: - build: - context: ./s3-ferry - dockerfile: Dockerfile - container_name: python_file_api - volumes: - - shared-volume:/shared - environment: - - UPLOAD_DIRECTORY=/shared - - RUUTER_PRIVATE_URL=http://ruuter-private:8088 - - S3_FERRY_URL=http://s3-ferry:3000/v1/files/copy - ports: - - "8000:8000" - networks: - - bykstack - depends_on: - - init - - s3-ferry: - image: s3-ferry:latest - container_name: s3-ferry - volumes: - - shared-volume:/shared - environment: - - API_CORS_ORIGIN=* - - API_DOCUMENTATION_ENABLED=true - - S3_REGION=${S3_REGION} - - S3_ENDPOINT_URL=${S3_ENDPOINT_URL} - - S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID} - - S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY} - - S3_DATA_BUCKET_NAME=${S3_DATA_BUCKET_NAME} - - S3_DATA_BUCKET_PATH=data/ - - FS_DATA_DIRECTORY_PATH=/shared - ports: - - "3002:3000" - depends_on: - - python_file_api - - init - networks: - - bykstack - -volumes: - shared-volume: - networks: bykstack: name: bykstack driver: bridge driver_opts: - com.docker.network.driver.mtu: 1400 + com.docker.network.driver.mtu: 1400 \ No newline at end of file From 237f335e1671724ec3f7062d9cde4b7e8f67f02d Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 22 Jul 2024 10:23:48 +0530 Subject: [PATCH 208/582] reverting changes after merge conflict fix --- docker-compose.yml | 52 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index bf0fca21..ac1af2d8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -172,9 +172,59 @@ services: networks: - bykstack + init: + image: busybox + command: ["sh", "-c", "chmod -R 777 /shared"] + volumes: + - shared-volume:/shared + + python_file_api: + build: + context: ./s3-ferry + dockerfile: Dockerfile + container_name: python_file_api + volumes: + - shared-volume:/shared + environment: + - UPLOAD_DIRECTORY=/shared + - RUUTER_PRIVATE_URL=http://ruuter-private:8088 + - S3_FERRY_URL=http://s3-ferry:3000/v1/files/copy + ports: + - "8000:8000" + networks: + - bykstack + depends_on: + - init + + s3-ferry: + image: s3-ferry:latest + container_name: s3-ferry + volumes: + - shared-volume:/shared + environment: + - API_CORS_ORIGIN=* + - API_DOCUMENTATION_ENABLED=true + - S3_REGION=${S3_REGION} + - S3_ENDPOINT_URL=${S3_ENDPOINT_URL} + - S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID} + - S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY} + - S3_DATA_BUCKET_NAME=${S3_DATA_BUCKET_NAME} + - S3_DATA_BUCKET_PATH=data/ + - FS_DATA_DIRECTORY_PATH=/shared + ports: + - "3002:3000" + depends_on: + - python_file_api + - init + networks: + - bykstack + +volumes: + shared-volume: + networks: bykstack: name: bykstack driver: bridge driver_opts: - com.docker.network.driver.mtu: 1400 \ No newline at end of file + com.docker.network.driver.mtu: 1400 From 026a138d439e29ce9ce8d0a276d03a65c655f432 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 22 Jul 2024 10:53:28 +0530 Subject: [PATCH 209/582] fixing security --- s3-ferry/file_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/s3-ferry/file_api.py b/s3-ferry/file_api.py index ebc26812..a78310b3 100644 --- a/s3-ferry/file_api.py +++ b/s3-ferry/file_api.py @@ -44,7 +44,7 @@ async def authenticate_user(request: Request): @app.post("/datasetgroup/data/import") async def upload_and_copy(request: Request, dgId: int = Form(...), dataFile: UploadFile = File(...)): await authenticate_user(request) - fileName = f"{uuid.uuid4()}_{dataFile.filename}" + fileName = f"{uuid.uuid4()}" fileLocation = os.path.join(UPLOAD_DIRECTORY, fileName) with open(fileLocation, "wb") as f: From b10d103de1c247319d102e7a728e432256b1f794 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Mon, 22 Jul 2024 12:08:09 +0530 Subject: [PATCH 210/582] ESCLASS-125-refactor:refacotr creat and update pre process payloads --- ...update-dataset-group-preprocess-status.sql | 2 +- .../POST/classifier/datasetgroup/create.yml | 76 ------------------- .../datasetgroup/update/preprocess/status.yml | 6 +- 3 files changed, 2 insertions(+), 82 deletions(-) diff --git a/DSL/Resql/update-dataset-group-preprocess-status.sql b/DSL/Resql/update-dataset-group-preprocess-status.sql index 2e8c7857..02e17b96 100644 --- a/DSL/Resql/update-dataset-group-preprocess-status.sql +++ b/DSL/Resql/update-dataset-group-preprocess-status.sql @@ -5,7 +5,7 @@ SET preprocess_data_location = :preprocess_data_location, raw_data_location = :raw_data_location, enable_allowed = :enable_allowed, - last_updated_timestamp = to_timestamp(:last_updated_timestamp)::timestamp with time zone, + last_updated_timestamp = :last_updated_timestamp::timestamp with time zone, num_samples = :num_samples, num_pages = :num_pages WHERE id = :id; diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/create.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/create.yml index 9263bbb7..953bc7f8 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/create.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/create.yml @@ -11,69 +11,12 @@ declaration: - field: groupName type: string description: "Body field 'groupName'" - - field: majorVersion - type: integer - description: "Body field 'majorVersion'" - - field: minorVersion - type: integer - description: "Body field 'minorVersion'" - - field: patchVersion - type: integer - description: "Body field 'patchVersion'" - - field: latest - type: boolean - description: "Body field 'latest'" - - field: isEnabled - type: boolean - description: "Body field 'isEnabled'" - - field: enableAllowed - type: boolean - description: "Body field 'enableAllowed'" - - field: lastModelTrained - type: string - description: "Body field 'lastModelTrained'" - - field: createdTimestamp - type: number - description: "Body field 'createdTimestamp'" - - field: lastUpdatedTimestamp - type: number - description: "Body field 'lastUpdatedTimestamp'" - - field: lastTrainedTimestamp - type: number - description: "Body field 'lastTrainedTimestamp'" - - field: validationStatus - type: string - description: "Body field 'validationStatus'" - - field: validationErrors - type: json - description: "Body field 'validationErrors'" - - field: processedDataAvailable - type: boolean - description: "Body field 'processedDataAvailable'" - - field: rawDataAvailable - type: boolean - description: "Body field 'rawDataAvailable'" - - field: numSamples - type: integer - description: "Body field 'numSamples'" - - field: numPages - type: integer - description: "Body field 'numPages'" - - field: rawDataLocation - type: string - description: "Body field 'rawDataLocation'" - - field: preprocessDataLocation - type: string - description: "Body field 'preprocessDataLocation'" - field: validationCriteria type: json description: "Body field 'validationCriteria'" - field: classHierarchy type: json description: "Body field 'classHierarchy'" - - field: connectedModels - type: array - description: "Body field 'connectedModels'" headers: - field: cookie type: string @@ -82,27 +25,8 @@ declaration: extract_request_data: assign: group_name: ${incoming.body.groupName} - major_version: ${incoming.body.majorVersion} - minor_version: ${incoming.body.minorVersion} - patch_version: ${incoming.body.patchVersion} - latest: ${incoming.body.latest === null ? false :incoming.body.latest} - is_enabled: ${incoming.body.isEnabled === null ? false :incoming.body.isEnabled} - enable_allowed: ${incoming.body.enableAllowed === null ? false :incoming.body.enableAllowed} - last_model_trained: ${incoming.body.lastModelTrained === null ? '' :incoming.body.lastModelTrained} - created_timestamp: ${incoming.body.createdTimestamp} - last_updated_timestamp: ${incoming.body.lastUpdatedTimestamp} - last_trained_timestamp: ${incoming.body.lastTrainedTimestamp} - validation_status: ${incoming.body.validationStatus} - validation_errors: ${incoming.body.validationErrors} - processed_data_available: ${incoming.body.processedDataAvailable === null ? false :incoming.body.processedDataAvailable} - raw_data_available: ${incoming.body.rawDataAvailable === null ? false :incoming.body.rawDataAvailable} - num_samples: ${incoming.body.numSamples} - num_pages: ${incoming.body.numPages} - raw_data_location: ${incoming.body.rawDataLocation} - preprocess_data_location: ${incoming.body.preprocessDataLocation} validation_criteria: ${incoming.body.validationCriteria} class_hierarchy: ${incoming.body.classHierarchy} - connected_models: ${incoming.body.connectedModels} next: check_for_request_data check_for_request_data: diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/preprocess/status.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/preprocess/status.yml index 42a208fe..2c59d606 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/preprocess/status.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/preprocess/status.yml @@ -26,9 +26,6 @@ declaration: - field: enableAllowed type: boolean description: "Body field 'enableAllowed'" - - field: lastUpdatedTimestamp - type: integer - description: "Body field 'lastUpdatedTimestamp'" - field: numSamples type: integer description: "Body field 'numSamples'" @@ -44,7 +41,6 @@ extract_request_data: preprocess_data_location: ${incoming.body.preprocessDataLocation} raw_data_location: ${incoming.body.rawDataLocation} enable_allowed: ${incoming.body.enableAllowed} - last_updated_timestamp: ${incoming.body.lastUpdatedTimestamp} num_samples: ${incoming.body.numSamples} num_pages: ${incoming.body.numPages} next: update_dataset_group_preprocess_status @@ -60,7 +56,7 @@ update_dataset_group_preprocess_status: preprocess_data_location: ${preprocess_data_location} raw_data_location: ${raw_data_location} enable_allowed: ${enable_allowed} - last_updated_timestamp: ${last_updated_timestamp} + last_updated_timestamp: ${new Date().toISOString()} num_samples: ${num_samples} num_pages: ${num_pages} result: res From 612fb1f65f4c5f1beba91d8dbefdbfb29f78e24e Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Mon, 22 Jul 2024 12:50:18 +0530 Subject: [PATCH 211/582] ESCLASS-125-refactor:remove unnecessary fields --- .../DSL/POST/classifier/datasetgroup/update/patch.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/patch.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/patch.yml index 22894ca1..e8475fcc 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/patch.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/patch.yml @@ -11,9 +11,6 @@ declaration: - field: dgId type: number description: "Body field 'dgId'" - - field: fields - type: array - description: "Body field 'fields'" - field: updateDataPayload type: json description: "Body field 'updateDataPayload'" @@ -26,7 +23,6 @@ extract_request_data: assign: dg_id: ${incoming.body.dgId} group_key: ${incoming.body.groupKey} - fields: ${incoming.body.fields} update_data_payload: ${incoming.body.updateDataPayload} next: get_dataset_group From 0c7ef89a5e03182187ca1fbe474a7e269aaed4ef Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 22 Jul 2024 12:53:01 +0530 Subject: [PATCH 212/582] windows CICD Update --- .github/workflows/est-workflow-dev.yml | 41 +++++++++++++------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/.github/workflows/est-workflow-dev.yml b/.github/workflows/est-workflow-dev.yml index c5ff0ed4..7f7a0b6c 100644 --- a/.github/workflows/est-workflow-dev.yml +++ b/.github/workflows/est-workflow-dev.yml @@ -3,30 +3,30 @@ name: Deploy EST Frontend and Backend to development on: push: branches: - - dev + - classifier-142 jobs: deploy: - runs-on: [self-hosted, dev] + runs-on: [self-hosted, dev-dell] steps: - - name: Set permissions for workspace directory - run: | - sudo chown -R ubuntu:ubuntu /home/ubuntu/actions-runner/_work/classifier/classifier - sudo chmod -R u+rwx /home/ubuntu/actions-runner/_work/classifier/classifier + # - name: Set permissions for workspace directory + # run: | + # sudo chown -R ubuntu:ubuntu /home/ubuntu/actions-runner/_work/classifier/classifier + # sudo chmod -R u+rwx /home/ubuntu/actions-runner/_work/classifier/classifier - name: Clean up workspace run: | - sudo rm -rf /home/ubuntu/actions-runner/_work/classifier/classifier/* + del /Q /F /S "C:\Users\pamod\Desktop\CICD\actions-runner\_work\classifier\classifier\*" - name: Checkout code uses: actions/checkout@v3 with: clean: true - - name: Give execute permissions to testScript.sh - run: | - sudo chmod +x src/unitTesting.sh + # - name: Give execute permissions to testScript.sh + # run: | + # sudo chmod +x src/unitTesting.sh - name: Remove all running containers, images, and prune Docker system run: | @@ -39,7 +39,7 @@ jobs: - name: Update constants.ini with GitHub secret run: | - sed -i 's/DB_PASSWORD=value/DB_PASSWORD=${{ secrets.DB_PASSWORD }}/' constants.ini + powershell -Command "(Get-Content constants.ini) -replace 'DB_PASSWORD=value', 'DB_PASSWORD=${{ secrets.DB_PASSWORD }}' | Set-Content constants.ini" - name: Build and run Docker Compose run: | @@ -61,17 +61,16 @@ jobs: - name: Run migration script if: steps.check_label.outputs.result == 'true' run: | - sudo chmod +x migrate.sh - ./migrate.sh + wsl sh migrate.sh - - name: Run unitTesting.sh - id: unittesting - run: | - output=$(bash src/unitTesting.sh) - if [ "$output" != "True" ]; then - echo "unitTesting.sh failed with output: $output" - exit 1 - fi + # - name: Run unitTesting.sh + # id: unittesting + # run: | + # output=$(bash src/unitTesting.sh) + # if [ "$output" != "True" ]; then + # echo "unitTesting.sh failed with output: $output" + # exit 1 + # fi - name: Send failure Slack notification if: failure() From 684f7e8194fd84ac4c9f0bc89c0663688936e187 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 22 Jul 2024 12:55:15 +0530 Subject: [PATCH 213/582] execution policy update --- .github/workflows/est-workflow-dev.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/est-workflow-dev.yml b/.github/workflows/est-workflow-dev.yml index 7f7a0b6c..9601160e 100644 --- a/.github/workflows/est-workflow-dev.yml +++ b/.github/workflows/est-workflow-dev.yml @@ -10,6 +10,10 @@ jobs: runs-on: [self-hosted, dev-dell] steps: + - name: Set PowerShell execution policy + run: | + powershell -Command "Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force" + # - name: Set permissions for workspace directory # run: | # sudo chown -R ubuntu:ubuntu /home/ubuntu/actions-runner/_work/classifier/classifier From a88aaae900c1e25e72cd846022155f807e0f2d5a Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 22 Jul 2024 12:58:27 +0530 Subject: [PATCH 214/582] policy update --- .github/workflows/est-workflow-dev.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/est-workflow-dev.yml b/.github/workflows/est-workflow-dev.yml index 9601160e..3b2dfafc 100644 --- a/.github/workflows/est-workflow-dev.yml +++ b/.github/workflows/est-workflow-dev.yml @@ -10,14 +10,10 @@ jobs: runs-on: [self-hosted, dev-dell] steps: - - name: Set PowerShell execution policy + - name: Set PowerShell execution policy and execute the script run: | - powershell -Command "Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force" + powershell -Command "Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force; . 'C:\Users\pamod\Desktop\CICD\actions-runner\_work\_temp\d63affe9-c5fc-4cc6-9f6d-8d20c3c1955c.ps1'" - # - name: Set permissions for workspace directory - # run: | - # sudo chown -R ubuntu:ubuntu /home/ubuntu/actions-runner/_work/classifier/classifier - # sudo chmod -R u+rwx /home/ubuntu/actions-runner/_work/classifier/classifier - name: Clean up workspace run: | From d416e53351bfccb4dfc4b632ffa83df44f76b376 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 22 Jul 2024 13:01:01 +0530 Subject: [PATCH 215/582] fix --- .github/workflows/est-workflow-dev.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/est-workflow-dev.yml b/.github/workflows/est-workflow-dev.yml index 3b2dfafc..19051472 100644 --- a/.github/workflows/est-workflow-dev.yml +++ b/.github/workflows/est-workflow-dev.yml @@ -10,10 +10,9 @@ jobs: runs-on: [self-hosted, dev-dell] steps: - - name: Set PowerShell execution policy and execute the script + - name: Set PowerShell execution policy and clean up workspace run: | - powershell -Command "Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force; . 'C:\Users\pamod\Desktop\CICD\actions-runner\_work\_temp\d63affe9-c5fc-4cc6-9f6d-8d20c3c1955c.ps1'" - + powershell.exe -Command "Set-ExecutionPolicy RemoteSigned -Scope Process -Force; del /Q /F /S 'C:\Users\pamod\Desktop\CICD\actions-runner\_work\classifier\classifier\*'" - name: Clean up workspace run: | From c4f771565f138c51f7af047f89fcd6179f364097 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Mon, 22 Jul 2024 13:08:55 +0530 Subject: [PATCH 216/582] ESCLASS-125-refactor:add last model trained attribute to get overview --- DSL/Resql/get-paginated-dataset-group-metadata.sql | 1 + 1 file changed, 1 insertion(+) diff --git a/DSL/Resql/get-paginated-dataset-group-metadata.sql b/DSL/Resql/get-paginated-dataset-group-metadata.sql index e856af03..e2042075 100644 --- a/DSL/Resql/get-paginated-dataset-group-metadata.sql +++ b/DSL/Resql/get-paginated-dataset-group-metadata.sql @@ -8,6 +8,7 @@ SELECT dt.id, dt.created_timestamp, dt.last_updated_timestamp, dt.last_used_for_training, + dt.last_model_trained, dt.validation_status, CEIL(COUNT(*) OVER() / :page_size::DECIMAL) AS total_pages FROM "dataset_group_metadata" dt From b9eec6cf0f6c74a7204c0d8e37c9f5164699b455 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 22 Jul 2024 13:09:01 +0530 Subject: [PATCH 217/582] new delete update --- .github/workflows/est-workflow-dev.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/est-workflow-dev.yml b/.github/workflows/est-workflow-dev.yml index 19051472..9c0a29e5 100644 --- a/.github/workflows/est-workflow-dev.yml +++ b/.github/workflows/est-workflow-dev.yml @@ -10,9 +10,9 @@ jobs: runs-on: [self-hosted, dev-dell] steps: - - name: Set PowerShell execution policy and clean up workspace - run: | - powershell.exe -Command "Set-ExecutionPolicy RemoteSigned -Scope Process -Force; del /Q /F /S 'C:\Users\pamod\Desktop\CICD\actions-runner\_work\classifier\classifier\*'" + # - name: Set PowerShell execution policy and clean up workspace + # run: | + # powershell.exe -Command "Set-ExecutionPolicy RemoteSigned -Scope Process -Force; del /Q /F /S 'C:\Users\pamod\Desktop\CICD\actions-runner\_work\classifier\classifier\*'" - name: Clean up workspace run: | From a5c71fdc7f074d48782db0943e1f825e73253adc Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 22 Jul 2024 13:11:05 +0530 Subject: [PATCH 218/582] delete update to powershell --- .github/workflows/est-workflow-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/est-workflow-dev.yml b/.github/workflows/est-workflow-dev.yml index 9c0a29e5..65c711e2 100644 --- a/.github/workflows/est-workflow-dev.yml +++ b/.github/workflows/est-workflow-dev.yml @@ -16,7 +16,7 @@ jobs: - name: Clean up workspace run: | - del /Q /F /S "C:\Users\pamod\Desktop\CICD\actions-runner\_work\classifier\classifier\*" + Remove-Item -Path 'C:\Users\pamod\Desktop\CICD\actions-runner\_work\classifier\classifier\*' -Recurse -Force" - name: Checkout code uses: actions/checkout@v3 From 59987a3fb863c8ed5d3049a285e1670e1d7e5cf7 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Mon, 22 Jul 2024 13:14:03 +0530 Subject: [PATCH 219/582] ESCLASS-125-refactor:add last model trained attribute to get overview --- DSL/Resql/get-paginated-dataset-group-metadata.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DSL/Resql/get-paginated-dataset-group-metadata.sql b/DSL/Resql/get-paginated-dataset-group-metadata.sql index e2042075..3d1246de 100644 --- a/DSL/Resql/get-paginated-dataset-group-metadata.sql +++ b/DSL/Resql/get-paginated-dataset-group-metadata.sql @@ -7,7 +7,7 @@ SELECT dt.id, dt.is_enabled, dt.created_timestamp, dt.last_updated_timestamp, - dt.last_used_for_training, + dt.last_trained_timestamp, dt.last_model_trained, dt.validation_status, CEIL(COUNT(*) OVER() / :page_size::DECIMAL) AS total_pages From 1f4fec51c13523292e5cb14f464846ba8f9256b6 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 22 Jul 2024 13:26:00 +0530 Subject: [PATCH 220/582] fixing quote mark issue --- .github/workflows/est-workflow-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/est-workflow-dev.yml b/.github/workflows/est-workflow-dev.yml index 65c711e2..311bef28 100644 --- a/.github/workflows/est-workflow-dev.yml +++ b/.github/workflows/est-workflow-dev.yml @@ -16,7 +16,7 @@ jobs: - name: Clean up workspace run: | - Remove-Item -Path 'C:\Users\pamod\Desktop\CICD\actions-runner\_work\classifier\classifier\*' -Recurse -Force" + Remove-Item -Path 'C:\Users\pamod\Desktop\CICD\actions-runner\_work\classifier\classifier\*' -Recurse -Force - name: Checkout code uses: actions/checkout@v3 From 8f204f9f93c8683aa96ae840407cd85ecd269293 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 22 Jul 2024 13:29:50 +0530 Subject: [PATCH 221/582] powershell update --- .github/workflows/est-workflow-dev.yml | 63 ++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 8 deletions(-) diff --git a/.github/workflows/est-workflow-dev.yml b/.github/workflows/est-workflow-dev.yml index 311bef28..361462ea 100644 --- a/.github/workflows/est-workflow-dev.yml +++ b/.github/workflows/est-workflow-dev.yml @@ -27,14 +27,61 @@ jobs: # run: | # sudo chmod +x src/unitTesting.sh - - name: Remove all running containers, images, and prune Docker system - run: | - docker stop $(docker ps -a -q | grep -v -E "($(docker ps -q --filter "name=users_db")|$(docker ps -q --filter "name=tim")|$(docker ps -q --filter "name=authentication-layer")|$(docker ps -q --filter "name=tim-postgresql"))") || true - docker rm $(docker ps -a -q | grep -v -E "($(docker ps -q --filter "name=users_db")|$(docker ps -q --filter "name=tim")|$(docker ps -q --filter "name=authentication-layer")|$(docker ps -q --filter "name=tim-postgresql"))") || true - images_to_keep="authentication-layer|tim|data-mapper|resql|ruuter|cron-manager" - docker images --format "{{.Repository}}:{{.Tag}}" | grep -Ev "$images_to_keep" | xargs -r docker rmi || true - docker volume prune -f - docker network prune -f + # - name: Remove all running containers, images, and prune Docker system + # run: | + # docker stop $(docker ps -a -q | grep -v -E "($(docker ps -q --filter "name=users_db")|$(docker ps -q --filter "name=tim")|$(docker ps -q --filter "name=authentication-layer")|$(docker ps -q --filter "name=tim-postgresql"))") || true + # docker rm $(docker ps -a -q | grep -v -E "($(docker ps -q --filter "name=users_db")|$(docker ps -q --filter "name=tim")|$(docker ps -q --filter "name=authentication-layer")|$(docker ps -q --filter "name=tim-postgresql"))") || true + # images_to_keep="authentication-layer|tim|data-mapper|resql|ruuter|cron-manager" + # docker images --format "{{.Repository}}:{{.Tag}}" | grep -Ev "$images_to_keep" | xargs -r docker rmi || true + # docker volume prune -f + # docker network prune -f + + - name: Stop all containers except the specified ones + shell: pwsh + run: | + $containers_to_keep = @( + $(docker ps -q --filter "name=users_db"), + $(docker ps -q --filter "name=tim"), + $(docker ps -q --filter "name=authentication-layer"), + $(docker ps -q --filter "name=tim-postgresql") + ) -join "|" + + $containers_to_stop = docker ps -a -q | Where-Object {$_ -notmatch $containers_to_keep} + if ($containers_to_stop) { + docker stop $containers_to_stop + } + + - name: Remove all containers except the specified ones + shell: pwsh + run: | + $containers_to_keep = @( + $(docker ps -q --filter "name=users_db"), + $(docker ps -q --filter "name=tim"), + $(docker ps -q --filter "name=authentication-layer"), + $(docker ps -q --filter "name=tim-postgresql") + ) -join "|" + + $containers_to_remove = docker ps -a -q | Where-Object {$_ -notmatch $containers_to_keep} + if ($containers_to_remove) { + docker rm $containers_to_remove + } + + - name: Remove all images except the specified ones + shell: pwsh + run: | + $images_to_keep = "authentication-layer|tim|data-mapper|resql|ruuter|cron-manager" + $images_to_remove = docker images --format "{{.Repository}}:{{.Tag}}" | Where-Object {$_ -notmatch $images_to_keep} + if ($images_to_remove) { + $images_to_remove | ForEach-Object { docker rmi $_ } + } + + - name: Prune unused volumes + shell: pwsh + run: docker volume prune -f + + - name: Prune unused networks + shell: pwsh + run: docker network prune -f - name: Update constants.ini with GitHub secret run: | From b8211372bfbb5954c2520bb7f196c568f12c6edf Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 22 Jul 2024 13:30:45 +0530 Subject: [PATCH 222/582] indent update fix --- .github/workflows/est-workflow-dev.yml | 92 +++++++++++++------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/.github/workflows/est-workflow-dev.yml b/.github/workflows/est-workflow-dev.yml index 361462ea..ec6b76d4 100644 --- a/.github/workflows/est-workflow-dev.yml +++ b/.github/workflows/est-workflow-dev.yml @@ -36,52 +36,52 @@ jobs: # docker volume prune -f # docker network prune -f - - name: Stop all containers except the specified ones - shell: pwsh - run: | - $containers_to_keep = @( - $(docker ps -q --filter "name=users_db"), - $(docker ps -q --filter "name=tim"), - $(docker ps -q --filter "name=authentication-layer"), - $(docker ps -q --filter "name=tim-postgresql") - ) -join "|" - - $containers_to_stop = docker ps -a -q | Where-Object {$_ -notmatch $containers_to_keep} - if ($containers_to_stop) { - docker stop $containers_to_stop - } - - - name: Remove all containers except the specified ones - shell: pwsh - run: | - $containers_to_keep = @( - $(docker ps -q --filter "name=users_db"), - $(docker ps -q --filter "name=tim"), - $(docker ps -q --filter "name=authentication-layer"), - $(docker ps -q --filter "name=tim-postgresql") - ) -join "|" - - $containers_to_remove = docker ps -a -q | Where-Object {$_ -notmatch $containers_to_keep} - if ($containers_to_remove) { - docker rm $containers_to_remove - } - - - name: Remove all images except the specified ones - shell: pwsh - run: | - $images_to_keep = "authentication-layer|tim|data-mapper|resql|ruuter|cron-manager" - $images_to_remove = docker images --format "{{.Repository}}:{{.Tag}}" | Where-Object {$_ -notmatch $images_to_keep} - if ($images_to_remove) { - $images_to_remove | ForEach-Object { docker rmi $_ } - } - - - name: Prune unused volumes - shell: pwsh - run: docker volume prune -f - - - name: Prune unused networks - shell: pwsh - run: docker network prune -f + - name: Stop all containers except the specified ones + shell: pwsh + run: | + $containers_to_keep = @( + $(docker ps -q --filter "name=users_db"), + $(docker ps -q --filter "name=tim"), + $(docker ps -q --filter "name=authentication-layer"), + $(docker ps -q --filter "name=tim-postgresql") + ) -join "|" + + $containers_to_stop = docker ps -a -q | Where-Object {$_ -notmatch $containers_to_keep} + if ($containers_to_stop) { + docker stop $containers_to_stop + } + + - name: Remove all containers except the specified ones + shell: pwsh + run: | + $containers_to_keep = @( + $(docker ps -q --filter "name=users_db"), + $(docker ps -q --filter "name=tim"), + $(docker ps -q --filter "name=authentication-layer"), + $(docker ps -q --filter "name=tim-postgresql") + ) -join "|" + + $containers_to_remove = docker ps -a -q | Where-Object {$_ -notmatch $containers_to_keep} + if ($containers_to_remove) { + docker rm $containers_to_remove + } + + - name: Remove all images except the specified ones + shell: pwsh + run: | + $images_to_keep = "authentication-layer|tim|data-mapper|resql|ruuter|cron-manager" + $images_to_remove = docker images --format "{{.Repository}}:{{.Tag}}" | Where-Object {$_ -notmatch $images_to_keep} + if ($images_to_remove) { + $images_to_remove | ForEach-Object { docker rmi $_ } + } + + - name: Prune unused volumes + shell: pwsh + run: docker volume prune -f + + - name: Prune unused networks + shell: pwsh + run: docker network prune -f - name: Update constants.ini with GitHub secret run: | From 7ef2f8c3d4d56c52d182bdc13c61037384f19129 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 22 Jul 2024 13:31:37 +0530 Subject: [PATCH 223/582] remove shell pwsh test --- .github/workflows/est-workflow-dev.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/est-workflow-dev.yml b/.github/workflows/est-workflow-dev.yml index ec6b76d4..7ba8981c 100644 --- a/.github/workflows/est-workflow-dev.yml +++ b/.github/workflows/est-workflow-dev.yml @@ -37,7 +37,6 @@ jobs: # docker network prune -f - name: Stop all containers except the specified ones - shell: pwsh run: | $containers_to_keep = @( $(docker ps -q --filter "name=users_db"), From 9c90c36d9843cc953370d683b00d8908043b2c9c Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 22 Jul 2024 13:32:57 +0530 Subject: [PATCH 224/582] powershell keyword update --- .github/workflows/est-workflow-dev.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/est-workflow-dev.yml b/.github/workflows/est-workflow-dev.yml index 7ba8981c..1619be50 100644 --- a/.github/workflows/est-workflow-dev.yml +++ b/.github/workflows/est-workflow-dev.yml @@ -37,6 +37,7 @@ jobs: # docker network prune -f - name: Stop all containers except the specified ones + shell: powershell run: | $containers_to_keep = @( $(docker ps -q --filter "name=users_db"), From 55023701f6064d2d8b5a7c54b5182c6a3878dc22 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 22 Jul 2024 13:34:23 +0530 Subject: [PATCH 225/582] powershell shell update to all blocks --- .github/workflows/est-workflow-dev.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/est-workflow-dev.yml b/.github/workflows/est-workflow-dev.yml index 1619be50..05a4aefa 100644 --- a/.github/workflows/est-workflow-dev.yml +++ b/.github/workflows/est-workflow-dev.yml @@ -52,7 +52,7 @@ jobs: } - name: Remove all containers except the specified ones - shell: pwsh + shell: powershell run: | $containers_to_keep = @( $(docker ps -q --filter "name=users_db"), @@ -67,7 +67,7 @@ jobs: } - name: Remove all images except the specified ones - shell: pwsh + shell: powershell run: | $images_to_keep = "authentication-layer|tim|data-mapper|resql|ruuter|cron-manager" $images_to_remove = docker images --format "{{.Repository}}:{{.Tag}}" | Where-Object {$_ -notmatch $images_to_keep} @@ -76,11 +76,11 @@ jobs: } - name: Prune unused volumes - shell: pwsh + shell: powershell run: docker volume prune -f - name: Prune unused networks - shell: pwsh + shell: powershell run: docker network prune -f - name: Update constants.ini with GitHub secret From caa4ada0ef811b77e17fb56ac3771126566d8aea Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 22 Jul 2024 13:35:53 +0530 Subject: [PATCH 226/582] adding force command to image removal --- .github/workflows/est-workflow-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/est-workflow-dev.yml b/.github/workflows/est-workflow-dev.yml index 05a4aefa..ef2583d6 100644 --- a/.github/workflows/est-workflow-dev.yml +++ b/.github/workflows/est-workflow-dev.yml @@ -72,7 +72,7 @@ jobs: $images_to_keep = "authentication-layer|tim|data-mapper|resql|ruuter|cron-manager" $images_to_remove = docker images --format "{{.Repository}}:{{.Tag}}" | Where-Object {$_ -notmatch $images_to_keep} if ($images_to_remove) { - $images_to_remove | ForEach-Object { docker rmi $_ } + $images_to_remove | ForEach-Object { docker rmi $_ --force} } - name: Prune unused volumes From f8dff36c8844265a1c9d9c8dd090805c76e70194 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 22 Jul 2024 13:46:59 +0530 Subject: [PATCH 227/582] removing the slack larger than marks and commenting docker --- .github/workflows/est-workflow-dev.yml | 117 ++++++++++++------------- 1 file changed, 54 insertions(+), 63 deletions(-) diff --git a/.github/workflows/est-workflow-dev.yml b/.github/workflows/est-workflow-dev.yml index ef2583d6..fcadc3f7 100644 --- a/.github/workflows/est-workflow-dev.yml +++ b/.github/workflows/est-workflow-dev.yml @@ -27,69 +27,60 @@ jobs: # run: | # sudo chmod +x src/unitTesting.sh - # - name: Remove all running containers, images, and prune Docker system + # - name: Stop all containers except the specified ones + # shell: powershell # run: | - # docker stop $(docker ps -a -q | grep -v -E "($(docker ps -q --filter "name=users_db")|$(docker ps -q --filter "name=tim")|$(docker ps -q --filter "name=authentication-layer")|$(docker ps -q --filter "name=tim-postgresql"))") || true - # docker rm $(docker ps -a -q | grep -v -E "($(docker ps -q --filter "name=users_db")|$(docker ps -q --filter "name=tim")|$(docker ps -q --filter "name=authentication-layer")|$(docker ps -q --filter "name=tim-postgresql"))") || true - # images_to_keep="authentication-layer|tim|data-mapper|resql|ruuter|cron-manager" - # docker images --format "{{.Repository}}:{{.Tag}}" | grep -Ev "$images_to_keep" | xargs -r docker rmi || true - # docker volume prune -f - # docker network prune -f - - - name: Stop all containers except the specified ones - shell: powershell - run: | - $containers_to_keep = @( - $(docker ps -q --filter "name=users_db"), - $(docker ps -q --filter "name=tim"), - $(docker ps -q --filter "name=authentication-layer"), - $(docker ps -q --filter "name=tim-postgresql") - ) -join "|" - - $containers_to_stop = docker ps -a -q | Where-Object {$_ -notmatch $containers_to_keep} - if ($containers_to_stop) { - docker stop $containers_to_stop - } - - - name: Remove all containers except the specified ones - shell: powershell - run: | - $containers_to_keep = @( - $(docker ps -q --filter "name=users_db"), - $(docker ps -q --filter "name=tim"), - $(docker ps -q --filter "name=authentication-layer"), - $(docker ps -q --filter "name=tim-postgresql") - ) -join "|" - - $containers_to_remove = docker ps -a -q | Where-Object {$_ -notmatch $containers_to_keep} - if ($containers_to_remove) { - docker rm $containers_to_remove - } - - - name: Remove all images except the specified ones - shell: powershell - run: | - $images_to_keep = "authentication-layer|tim|data-mapper|resql|ruuter|cron-manager" - $images_to_remove = docker images --format "{{.Repository}}:{{.Tag}}" | Where-Object {$_ -notmatch $images_to_keep} - if ($images_to_remove) { - $images_to_remove | ForEach-Object { docker rmi $_ --force} - } + # $containers_to_keep = @( + # $(docker ps -q --filter "name=users_db"), + # $(docker ps -q --filter "name=tim"), + # $(docker ps -q --filter "name=authentication-layer"), + # $(docker ps -q --filter "name=tim-postgresql") + # ) -join "|" + + # $containers_to_stop = docker ps -a -q | Where-Object {$_ -notmatch $containers_to_keep} + # if ($containers_to_stop) { + # docker stop $containers_to_stop + # } + + # - name: Remove all containers except the specified ones + # shell: powershell + # run: | + # $containers_to_keep = @( + # $(docker ps -q --filter "name=users_db"), + # $(docker ps -q --filter "name=tim"), + # $(docker ps -q --filter "name=authentication-layer"), + # $(docker ps -q --filter "name=tim-postgresql") + # ) -join "|" + + # $containers_to_remove = docker ps -a -q | Where-Object {$_ -notmatch $containers_to_keep} + # if ($containers_to_remove) { + # docker rm $containers_to_remove + # } + + # - name: Remove all images except the specified ones + # shell: powershell + # run: | + # $images_to_keep = "authentication-layer|tim|data-mapper|resql|ruuter|cron-manager" + # $images_to_remove = docker images --format "{{.Repository}}:{{.Tag}}" | Where-Object {$_ -notmatch $images_to_keep} + # if ($images_to_remove) { + # $images_to_remove | ForEach-Object { docker rmi $_ --force} + # } - - name: Prune unused volumes - shell: powershell - run: docker volume prune -f + # - name: Prune unused volumes + # shell: powershell + # run: docker volume prune -f - - name: Prune unused networks - shell: powershell - run: docker network prune -f + # - name: Prune unused networks + # shell: powershell + # run: docker network prune -f - - name: Update constants.ini with GitHub secret - run: | - powershell -Command "(Get-Content constants.ini) -replace 'DB_PASSWORD=value', 'DB_PASSWORD=${{ secrets.DB_PASSWORD }}' | Set-Content constants.ini" + # - name: Update constants.ini with GitHub secret + # run: | + # powershell -Command "(Get-Content constants.ini) -replace 'DB_PASSWORD=value', 'DB_PASSWORD=${{ secrets.DB_PASSWORD }}' | Set-Content constants.ini" - - name: Build and run Docker Compose - run: | - docker compose -f docker-compose.development.yml up --build -d + # - name: Build and run Docker Compose + # run: | + # docker compose -f docker-compose.development.yml up --build -d # - name: Check for db_update label # id: check_label @@ -104,10 +95,10 @@ jobs: # const labels = pr.data.labels.map(label => label.name); # return labels.includes('db_update'); - - name: Run migration script - if: steps.check_label.outputs.result == 'true' - run: | - wsl sh migrate.sh + # - name: Run migration script + # if: steps.check_label.outputs.result == 'true' + # run: | + # wsl sh migrate.sh # - name: Run unitTesting.sh # id: unittesting @@ -133,5 +124,5 @@ jobs: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} run: | curl -X POST -H 'Content-type: application/json' --data "{ - \"text\": \"The build is complete and the development environment is now available. Please click the following link to access it: \" + \"text\": \"The build is complete and the development environment is now available. Please click the following link to access it: https://esclassifier-dev.rootcode.software/classifier\" }" $SLACK_WEBHOOK_URL From c90d4660c0a1bbb3e98380060292f554d279d480 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 22 Jul 2024 13:49:38 +0530 Subject: [PATCH 228/582] update slack success block --- .github/workflows/est-workflow-dev.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/est-workflow-dev.yml b/.github/workflows/est-workflow-dev.yml index fcadc3f7..433e756d 100644 --- a/.github/workflows/est-workflow-dev.yml +++ b/.github/workflows/est-workflow-dev.yml @@ -122,7 +122,10 @@ jobs: if: success() env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + shell: powershell run: | - curl -X POST -H 'Content-type: application/json' --data "{ - \"text\": \"The build is complete and the development environment is now available. Please click the following link to access it: https://esclassifier-dev.rootcode.software/classifier\" - }" $SLACK_WEBHOOK_URL + $payload = @{ + text = "The build is complete and the development environment is now available. Please click the following link to access it: https://esclassifier-dev.rootcode.software/classifier" + } + $payloadJson = $payload | ConvertTo-Json + Invoke-RestMethod -Uri $env:SLACK_WEBHOOK_URL -Method Post -ContentType 'application/json' -Body $payloadJson From fbdf6599046fbe5d7c8a29c6ae5159eb08371dbc Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 22 Jul 2024 13:51:46 +0530 Subject: [PATCH 229/582] slack fail notification update. --- .github/workflows/est-workflow-dev.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/est-workflow-dev.yml b/.github/workflows/est-workflow-dev.yml index 433e756d..963272a9 100644 --- a/.github/workflows/est-workflow-dev.yml +++ b/.github/workflows/est-workflow-dev.yml @@ -113,10 +113,13 @@ jobs: if: failure() env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + shell: powershell run: | - curl -X POST -H 'Content-type: application/json' --data "{ - \"text\": \"The Development environment deployment failed during one of the steps. Please check the output for details.\" - }" $SLACK_WEBHOOK_URL + $payload = @{ + text = "The Development environment deployment failed during one of the steps. Please check the output for details." + } + $payloadJson = $payload | ConvertTo-Json + Invoke-RestMethod -Uri $env:SLACK_WEBHOOK_URL -Method Post -ContentType 'application/json' -Body $payloadJson - name: Send success Slack notification if: success() From 780fcb5ef617f4313514f5d632e63660ed76e46a Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 22 Jul 2024 13:52:21 +0530 Subject: [PATCH 230/582] uncomment docker blocks --- .github/workflows/est-workflow-dev.yml | 100 ++++++++++++------------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/.github/workflows/est-workflow-dev.yml b/.github/workflows/est-workflow-dev.yml index 963272a9..9c389c92 100644 --- a/.github/workflows/est-workflow-dev.yml +++ b/.github/workflows/est-workflow-dev.yml @@ -27,60 +27,60 @@ jobs: # run: | # sudo chmod +x src/unitTesting.sh - # - name: Stop all containers except the specified ones - # shell: powershell - # run: | - # $containers_to_keep = @( - # $(docker ps -q --filter "name=users_db"), - # $(docker ps -q --filter "name=tim"), - # $(docker ps -q --filter "name=authentication-layer"), - # $(docker ps -q --filter "name=tim-postgresql") - # ) -join "|" - - # $containers_to_stop = docker ps -a -q | Where-Object {$_ -notmatch $containers_to_keep} - # if ($containers_to_stop) { - # docker stop $containers_to_stop - # } - - # - name: Remove all containers except the specified ones - # shell: powershell - # run: | - # $containers_to_keep = @( - # $(docker ps -q --filter "name=users_db"), - # $(docker ps -q --filter "name=tim"), - # $(docker ps -q --filter "name=authentication-layer"), - # $(docker ps -q --filter "name=tim-postgresql") - # ) -join "|" - - # $containers_to_remove = docker ps -a -q | Where-Object {$_ -notmatch $containers_to_keep} - # if ($containers_to_remove) { - # docker rm $containers_to_remove - # } - - # - name: Remove all images except the specified ones - # shell: powershell - # run: | - # $images_to_keep = "authentication-layer|tim|data-mapper|resql|ruuter|cron-manager" - # $images_to_remove = docker images --format "{{.Repository}}:{{.Tag}}" | Where-Object {$_ -notmatch $images_to_keep} - # if ($images_to_remove) { - # $images_to_remove | ForEach-Object { docker rmi $_ --force} - # } + - name: Stop all containers except the specified ones + shell: powershell + run: | + $containers_to_keep = @( + $(docker ps -q --filter "name=users_db"), + $(docker ps -q --filter "name=tim"), + $(docker ps -q --filter "name=authentication-layer"), + $(docker ps -q --filter "name=tim-postgresql") + ) -join "|" + + $containers_to_stop = docker ps -a -q | Where-Object {$_ -notmatch $containers_to_keep} + if ($containers_to_stop) { + docker stop $containers_to_stop + } + + - name: Remove all containers except the specified ones + shell: powershell + run: | + $containers_to_keep = @( + $(docker ps -q --filter "name=users_db"), + $(docker ps -q --filter "name=tim"), + $(docker ps -q --filter "name=authentication-layer"), + $(docker ps -q --filter "name=tim-postgresql") + ) -join "|" + + $containers_to_remove = docker ps -a -q | Where-Object {$_ -notmatch $containers_to_keep} + if ($containers_to_remove) { + docker rm $containers_to_remove + } - # - name: Prune unused volumes - # shell: powershell - # run: docker volume prune -f + - name: Remove all images except the specified ones + shell: powershell + run: | + $images_to_keep = "authentication-layer|tim|data-mapper|resql|ruuter|cron-manager" + $images_to_remove = docker images --format "{{.Repository}}:{{.Tag}}" | Where-Object {$_ -notmatch $images_to_keep} + if ($images_to_remove) { + $images_to_remove | ForEach-Object { docker rmi $_ --force} + } - # - name: Prune unused networks - # shell: powershell - # run: docker network prune -f + - name: Prune unused volumes + shell: powershell + run: docker volume prune -f - # - name: Update constants.ini with GitHub secret - # run: | - # powershell -Command "(Get-Content constants.ini) -replace 'DB_PASSWORD=value', 'DB_PASSWORD=${{ secrets.DB_PASSWORD }}' | Set-Content constants.ini" + - name: Prune unused networks + shell: powershell + run: docker network prune -f - # - name: Build and run Docker Compose - # run: | - # docker compose -f docker-compose.development.yml up --build -d + - name: Update constants.ini with GitHub secret + run: | + powershell -Command "(Get-Content constants.ini) -replace 'DB_PASSWORD=value', 'DB_PASSWORD=${{ secrets.DB_PASSWORD }}' | Set-Content constants.ini" + + - name: Build and run Docker Compose + run: | + docker compose -f docker-compose.development.yml up --build -d # - name: Check for db_update label # id: check_label From 6d80d694b28fb55ab172798c1e7f3e9c6b94076b Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 22 Jul 2024 13:53:09 +0530 Subject: [PATCH 231/582] testing migration.sh --- .github/workflows/est-workflow-dev.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/est-workflow-dev.yml b/.github/workflows/est-workflow-dev.yml index 9c389c92..05b6fe30 100644 --- a/.github/workflows/est-workflow-dev.yml +++ b/.github/workflows/est-workflow-dev.yml @@ -95,10 +95,10 @@ jobs: # const labels = pr.data.labels.map(label => label.name); # return labels.includes('db_update'); - # - name: Run migration script - # if: steps.check_label.outputs.result == 'true' - # run: | - # wsl sh migrate.sh + - name: Run migration script + # if: steps.check_label.outputs.result == 'true' + run: | + wsl sh migrate.sh # - name: Run unitTesting.sh # id: unittesting From 73b012bb1e186b3cd0e9299cd3b3f9ae5c822ce8 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 22 Jul 2024 14:05:32 +0530 Subject: [PATCH 232/582] adapting migrate .sh to windows --- .github/workflows/est-workflow-dev.yml | 2 +- migrate_win.sh | 37 ++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 migrate_win.sh diff --git a/.github/workflows/est-workflow-dev.yml b/.github/workflows/est-workflow-dev.yml index 05b6fe30..63663ce3 100644 --- a/.github/workflows/est-workflow-dev.yml +++ b/.github/workflows/est-workflow-dev.yml @@ -98,7 +98,7 @@ jobs: - name: Run migration script # if: steps.check_label.outputs.result == 'true' run: | - wsl sh migrate.sh + wsl sh migrate_win.sh # - name: Run unitTesting.sh # id: unittesting diff --git a/migrate_win.sh b/migrate_win.sh new file mode 100644 index 00000000..a2185b7e --- /dev/null +++ b/migrate_win.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +# Define the path where the SQL file will be generated +SQL_FILE="DSL/Liquibase/data/update_refresh_token.sql" + +# Read the OUTLOOK_REFRESH_KEY value from the INI file +OUTLOOK_REFRESH_KEY=$(awk -F '=' '/OUTLOOK_REFRESH_KEY/ {print $2}' constants.ini | xargs) + +# Generate a SQL script with the extracted value +cat << EOF > "$SQL_FILE" +-- Update the refresh token in the database +UPDATE integration_status +SET token = '$OUTLOOK_REFRESH_KEY' +WHERE platform='OUTLOOK'; +EOF + +# Function to parse ini file and extract the value for a given key +get_ini_value() { + local file=$1 + local key=$2 + awk -F '=' -v key="$key" '$1 == key { gsub(/^[ \t]+|[ \t]+$/, "", $2); print $2; exit }' "$file" +} + +# Get the values from constants.ini +INI_FILE="constants.ini" +DB_PASSWORD=$(get_ini_value "$INI_FILE" "DB_PASSWORD") + +# Run the Liquibase update command using Docker +docker run --rm --network bykstack \ + -v "$(pwd)/DSL/Liquibase/changelog:/liquibase/changelog" \ + -v "$(pwd)/DSL/Liquibase/master.yml:/liquibase/master.yml" \ + -v "$(pwd)/DSL/Liquibase/data:/liquibase/data" \ + liquibase/liquibase \ + --defaultsFile=/liquibase/changelog/liquibase.properties \ + --changelog-file=master.yml \ + --url=jdbc:postgresql://users_db:5432/classifier?user=postgres \ + --password="$DB_PASSWORD" update From 1e99d063b19bbe056a4a460669485c2b34261a42 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 22 Jul 2024 14:14:34 +0530 Subject: [PATCH 233/582] workflow cleaning --- .github/workflows/est-workflow-dev.yml | 31 -------------------------- 1 file changed, 31 deletions(-) diff --git a/.github/workflows/est-workflow-dev.yml b/.github/workflows/est-workflow-dev.yml index 63663ce3..5a28acb5 100644 --- a/.github/workflows/est-workflow-dev.yml +++ b/.github/workflows/est-workflow-dev.yml @@ -10,10 +10,6 @@ jobs: runs-on: [self-hosted, dev-dell] steps: - # - name: Set PowerShell execution policy and clean up workspace - # run: | - # powershell.exe -Command "Set-ExecutionPolicy RemoteSigned -Scope Process -Force; del /Q /F /S 'C:\Users\pamod\Desktop\CICD\actions-runner\_work\classifier\classifier\*'" - - name: Clean up workspace run: | Remove-Item -Path 'C:\Users\pamod\Desktop\CICD\actions-runner\_work\classifier\classifier\*' -Recurse -Force @@ -23,10 +19,6 @@ jobs: with: clean: true - # - name: Give execute permissions to testScript.sh - # run: | - # sudo chmod +x src/unitTesting.sh - - name: Stop all containers except the specified ones shell: powershell run: | @@ -82,33 +74,10 @@ jobs: run: | docker compose -f docker-compose.development.yml up --build -d - # - name: Check for db_update label - # id: check_label - # uses: actions/github-script@v6 - # with: - # script: | - # const pr = await github.pulls.get({ - # owner: context.repo.owner, - # repo: context.repo.repo, - # pull_number: context.payload.number - # }); - # const labels = pr.data.labels.map(label => label.name); - # return labels.includes('db_update'); - - name: Run migration script - # if: steps.check_label.outputs.result == 'true' run: | wsl sh migrate_win.sh - # - name: Run unitTesting.sh - # id: unittesting - # run: | - # output=$(bash src/unitTesting.sh) - # if [ "$output" != "True" ]; then - # echo "unitTesting.sh failed with output: $output" - # exit 1 - # fi - - name: Send failure Slack notification if: failure() env: From 5fccdc1eaca04e051b9dc5ee0e6b256fda3a2819 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 22 Jul 2024 14:15:02 +0530 Subject: [PATCH 234/582] changing running branch --- .github/workflows/est-workflow-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/est-workflow-dev.yml b/.github/workflows/est-workflow-dev.yml index 5a28acb5..958f4fea 100644 --- a/.github/workflows/est-workflow-dev.yml +++ b/.github/workflows/est-workflow-dev.yml @@ -3,7 +3,7 @@ name: Deploy EST Frontend and Backend to development on: push: branches: - - classifier-142 + - dev jobs: deploy: From 8ea2bc3727df02e7f8b0a47a4b073a061dfdb708 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Mon, 22 Jul 2024 18:43:30 +0530 Subject: [PATCH 235/582] ESCLASS-118-implement gat dataset group meta data by id --- ...get-dataset-group-allowed-status-by-id.sql | 2 + .../get-dataset-group-metadata-by-id.sql | 5 +- .../datasetgroup/group/metadata.yml | 85 +++++++++++++++++++ .../classifier/datasetgroup/update/status.yml | 2 +- 4 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 DSL/Resql/get-dataset-group-allowed-status-by-id.sql create mode 100644 DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/group/metadata.yml diff --git a/DSL/Resql/get-dataset-group-allowed-status-by-id.sql b/DSL/Resql/get-dataset-group-allowed-status-by-id.sql new file mode 100644 index 00000000..78a35388 --- /dev/null +++ b/DSL/Resql/get-dataset-group-allowed-status-by-id.sql @@ -0,0 +1,2 @@ +SELECT enable_allowed +FROM dataset_group_metadata WHERE id =:id; \ No newline at end of file diff --git a/DSL/Resql/get-dataset-group-metadata-by-id.sql b/DSL/Resql/get-dataset-group-metadata-by-id.sql index 78a35388..c6fc13af 100644 --- a/DSL/Resql/get-dataset-group-metadata-by-id.sql +++ b/DSL/Resql/get-dataset-group-metadata-by-id.sql @@ -1,2 +1,3 @@ -SELECT enable_allowed -FROM dataset_group_metadata WHERE id =:id; \ No newline at end of file +SELECT id, group_name, major_version, minor_version, patch_version, latest, is_enabled, num_samples, enable_allowed, + validation_status, validation_errors, connected_models, validation_criteria, class_hierarchy +FROM dataset_group_metadata WHERE id = :id; diff --git a/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/group/metadata.yml b/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/group/metadata.yml new file mode 100644 index 00000000..5f7f52b9 --- /dev/null +++ b/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/group/metadata.yml @@ -0,0 +1,85 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'METADATA'" + method: get + accepts: json + returns: json + namespace: classifier + allowlist: + params: + - field: groupId + type: number + description: "Parameter 'groupId'" + +extract_data: + assign: + group_id: ${Number(incoming.params.groupId)} + next: get_dataset_meta_data_by_id + +get_dataset_meta_data_by_id: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-dataset-group-metadata-by-id" + body: + id: ${group_id} + result: res_dataset + next: check_status + +check_status: + switch: + - condition: ${200 <= res_dataset.response.statusCodeValue && res_dataset.response.statusCodeValue < 300} + next: check_data_exist + next: assign_fail_response + +check_data_exist: + switch: + - condition: ${res_dataset.response.body.length>0} + next: assign_formated_response + next: assign_fail_response + +assign_formated_response: + assign: + val: [{ + dgId: '${res_dataset.response.body[0].id}', + name: '${res_dataset.response.body[0].groupName}', + majorVersion: '${res_dataset.response.body[0].majorVersion}', + minorVersion: '${res_dataset.response.body[0].minorVersion}', + patchVersion: '${res_dataset.response.body[0].patchVersion}', + latest: '${res_dataset.response.body[0].latest}', + isEnabled: '${res_dataset.response.body[0].isEnabled}', + numSamples: '${res_dataset.response.body[0].numSamples}', + enableAllowed: '${res_dataset.response.body[0].enableAllowed}', + validationStatus: '${res_dataset.response.body[0].validationStatus}', + validationErrors: '${res_dataset.response.body[0].validationErrors === null ? [] :JSON.parse(res_dataset.response.body[0].validationErrors.value)}', + linkedModels: '${res_dataset.response.body[0].connectedModels === null ? [] :JSON.parse(res_dataset.response.body[0].connectedModels.value)}', + validationCriteria: '${res_dataset.response.body[0].validationCriteria === null ? [] :JSON.parse(res_dataset.response.body[0].validationCriteria.value)}', + classHierarchy: '${res_dataset.response.body[0].classHierarchy === null ? [] :JSON.parse(res_dataset.response.body[0].classHierarchy.value)}' + }] + next: assign_success_response + +assign_success_response: + assign: + format_res: { + operationSuccessful: true, + data: '${val}' + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + operationSuccessful: false, + data: '${[]}' + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/status.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/status.yml index 2fd5c114..5c9a95ed 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/status.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/status.yml @@ -24,7 +24,7 @@ extract_request_data: get_dataset_group: call: http.post args: - url: "[#CLASSIFIER_RESQL]/get-dataset-group-metadata-by-id" + url: "[#CLASSIFIER_RESQL]/get-dataset-group-allowed-status-by-id" body: id: ${id} result: res From 1a41242ba7e5cc6979a50c9733cb43a74ba1c660 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Mon, 22 Jul 2024 19:47:04 +0530 Subject: [PATCH 236/582] ESCLASS-118-implement gat dataset group data by python API,python API was mocked --- DSL/Resql/get-dataset-group-fields-by-id.sql | 2 + .../classifier/datasetgroup/group/data.yml | 102 ++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 DSL/Resql/get-dataset-group-fields-by-id.sql create mode 100644 DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/group/data.yml diff --git a/DSL/Resql/get-dataset-group-fields-by-id.sql b/DSL/Resql/get-dataset-group-fields-by-id.sql new file mode 100644 index 00000000..337679fb --- /dev/null +++ b/DSL/Resql/get-dataset-group-fields-by-id.sql @@ -0,0 +1,2 @@ +SELECT validation_criteria +FROM dataset_group_metadata WHERE id =:id; \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/group/data.yml b/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/group/data.yml new file mode 100644 index 00000000..0802fbeb --- /dev/null +++ b/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/group/data.yml @@ -0,0 +1,102 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'DATA'" + method: get + accepts: json + returns: json + namespace: classifier + allowlist: + params: + - field: groupId + type: number + description: "Parameter 'groupId'" + - field: pageNum + type: number + description: "Parameter 'pageNum'" + +extract_data: + assign: + group_id: ${Number(incoming.params.groupId)} + page_num: ${Number(incoming.params.pageNum)} + next: get_dataset_group_fields_by_id + +get_dataset_group_fields_by_id: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-dataset-group-fields-by-id" + body: + id: ${group_id} + result: res_dataset + next: check_fields_status + +check_fields_status: + switch: + - condition: ${200 <= res_dataset.response.statusCodeValue && res_dataset.response.statusCodeValue < 300} + next: check_fields_data_exist + next: assign_fail_response + +check_fields_data_exist: + switch: + - condition: ${res_dataset.response.body.length>0} + next: get_dataset_group_data_by_id + next: assign_fail_response + +get_dataset_group_data_by_id: + call: reflect.mock + args: + url: "[#CLASSIFIER_RESQL]/get-dataset-group-metadata-by-id" + body: + group_id: ${group_id} + page_num: ${page_num} + response: + statusCodeValue: 200 + dataPayload: [] + result: res_data + next: check_data_status + +check_data_status: + switch: + - condition: ${200 <= res_data.response.statusCodeValue && res_data.response.statusCodeValue < 300} + next: assign_fields_response + next: assign_fail_response + +assign_fields_response: + assign: + val: ${res_dataset.response.body[0].validationCriteria === null ? [] :JSON.parse(res_dataset.response.body[0].validationCriteria.value)} + next: assign_formated_response + +assign_formated_response: + assign: + val: [{ + dgId: '${group_id}', + fields: '${val === [] ? [] :val.fields}', + dataPayload: '[]' + }] + next: assign_success_response + +assign_success_response: + assign: + format_res: { + operationSuccessful: true, + data: '${val}' + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + operationSuccessful: false, + data: '${[]}' + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end \ No newline at end of file From 362b79d6e5b259edfa03c585f5c8bc21df2a5e81 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Tue, 23 Jul 2024 00:17:25 +0530 Subject: [PATCH 237/582] Background task update and bug fix --- s3-ferry/Dockerfile | 2 ++ s3-ferry/file_api.py | 17 +++++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/s3-ferry/Dockerfile b/s3-ferry/Dockerfile index 3843c7a5..c4cd7fe9 100644 --- a/s3-ferry/Dockerfile +++ b/s3-ferry/Dockerfile @@ -4,6 +4,8 @@ COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY file_api.py . COPY file_converter.py . +COPY constants.py . +COPY s3_ferry.py . EXPOSE 8000 RUN mkdir -p /shared && chmod -R 777 /shared CMD ["uvicorn", "file_api:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/s3-ferry/file_api.py b/s3-ferry/file_api.py index a78310b3..dbcc8f8a 100644 --- a/s3-ferry/file_api.py +++ b/s3-ferry/file_api.py @@ -1,4 +1,4 @@ -from fastapi import FastAPI, File, UploadFile, HTTPException, Form, Request +from fastapi import FastAPI, File, UploadFile, HTTPException, Form, Request, BackgroundTasks from fastapi.responses import FileResponse, JSONResponse import os import json @@ -44,13 +44,15 @@ async def authenticate_user(request: Request): @app.post("/datasetgroup/data/import") async def upload_and_copy(request: Request, dgId: int = Form(...), dataFile: UploadFile = File(...)): await authenticate_user(request) - fileName = f"{uuid.uuid4()}" + + fileConverter = FileConverter() + file_type = fileConverter._detect_file_type(dataFile.filename) + fileName = f"{uuid.uuid4()}.{file_type}" fileLocation = os.path.join(UPLOAD_DIRECTORY, fileName) with open(fileLocation, "wb") as f: f.write(dataFile.file.read()) - fileConverter = FileConverter() success, convertedData = fileConverter.convert_to_json(fileLocation) if not success: upload_failed = UPLOAD_FAILED.copy() @@ -75,9 +77,8 @@ async def upload_and_copy(request: Request, dgId: int = Form(...), dataFile: Upl else: raise HTTPException(status_code=500, detail=S3_UPLOAD_FAILED) - @app.post("/datasetgroup/data/download") -async def download_and_convert(request: Request, exportData: ExportFile): +async def download_and_convert(request: Request, exportData: ExportFile, background_tasks: BackgroundTasks): await authenticate_user(request) dgId = exportData.dgId version = exportData.version @@ -116,4 +117,8 @@ async def download_and_convert(request: Request, exportData: ExportFile): else: raise HTTPException(status_code=500, detail=EXPORT_TYPE_ERROR) - return FileResponse(outputFile, filename=os.path.basename(outputFile)) + background_tasks.add_task(os.remove, jsonFilePath) + if outputFile != jsonFilePath: + background_tasks.add_task(os.remove, outputFile) + + return FileResponse(outputFile, filename=os.path.basename(outputFile)) \ No newline at end of file From 9673e2ac337670bac7d9f700c5ac919e2d57d792 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Tue, 23 Jul 2024 00:29:50 +0530 Subject: [PATCH 238/582] Security fixes --- s3-ferry/Dockerfile | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/s3-ferry/Dockerfile b/s3-ferry/Dockerfile index c4cd7fe9..489e0f74 100644 --- a/s3-ferry/Dockerfile +++ b/s3-ferry/Dockerfile @@ -1,11 +1,17 @@ FROM python:3.9-slim +RUN addgroup --system appuser && adduser --system --ingroup appuser appuser WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt + COPY file_api.py . COPY file_converter.py . COPY constants.py . COPY s3_ferry.py . + +RUN mkdir -p /shared && chown appuser:appuser /shared && chmod 770 /shared +RUN chown -R appuser:appuser /app EXPOSE 8000 -RUN mkdir -p /shared && chmod -R 777 /shared +USER appuser + CMD ["uvicorn", "file_api:app", "--host", "0.0.0.0", "--port", "8000"] From ffc1281dbddf46f5347ee86ae2c03f453579f6c7 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Tue, 23 Jul 2024 14:38:22 +0530 Subject: [PATCH 239/582] add download chunks --- s3-ferry/file_api.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/s3-ferry/file_api.py b/s3-ferry/file_api.py index dbcc8f8a..ebab0a9e 100644 --- a/s3-ferry/file_api.py +++ b/s3-ferry/file_api.py @@ -121,4 +121,26 @@ async def download_and_convert(request: Request, exportData: ExportFile, backgro if outputFile != jsonFilePath: background_tasks.add_task(os.remove, outputFile) - return FileResponse(outputFile, filename=os.path.basename(outputFile)) \ No newline at end of file + return FileResponse(outputFile, filename=os.path.basename(outputFile)) + +@app.get("/datasetgroup/data/download/chunk") +async def download_and_convert(request: Request, dgId: int, pageId:int, background_tasks: BackgroundTasks): + await authenticate_user(request) + saveLocation = f"/dataset/{dgId}/chunks/{pageId}{JSON_EXT}" + localFileName = f"group_{dgId}_chunk_{pageId}" + + response = s3_ferry.transfer_file(f"{localFileName}{JSON_EXT}", "FS", saveLocation, "S3") + if response.status_code != 201: + raise HTTPException(status_code=500, detail=S3_DOWNLOAD_FAILED) + + jsonFilePath = os.path.join('..', 'shared', f"{localFileName}{JSON_EXT}") + + with open(f"{jsonFilePath}", 'r') as jsonFile: + jsonData = json.load(jsonFile) + + for index, item in enumerate(jsonData, start=1): + item['rowID'] = index + + background_tasks.add_task(os.remove, jsonFilePath) + + return jsonData \ No newline at end of file From fe6a3871d7b0c4eb5c37a11a3f4400606338b893 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Tue, 23 Jul 2024 14:40:47 +0530 Subject: [PATCH 240/582] docker compose env updates --- docker-compose.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index ac1af2d8..41b3ecfc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -204,11 +204,11 @@ services: environment: - API_CORS_ORIGIN=* - API_DOCUMENTATION_ENABLED=true - - S3_REGION=${S3_REGION} - - S3_ENDPOINT_URL=${S3_ENDPOINT_URL} + - S3_REGION=eu-west-1 + - S3_ENDPOINT_URL=https://s3.eu-west-1.amazonaws.com - S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID} - S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY} - - S3_DATA_BUCKET_NAME=${S3_DATA_BUCKET_NAME} + - S3_DATA_BUCKET_NAME=esclassifier-test - S3_DATA_BUCKET_PATH=data/ - FS_DATA_DIRECTORY_PATH=/shared ports: From 532e707c572a97af598a177a167b681511b7f37e Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Tue, 23 Jul 2024 14:54:47 +0530 Subject: [PATCH 241/582] file name changes --- docker-compose.yml | 8 ++++---- {s3-ferry => file-handler}/Dockerfile | 4 ++-- {s3-ferry => file-handler}/constants.py | 0 {s3-ferry => file-handler}/docker-compose.yml | 4 ++-- {s3-ferry => file-handler}/file_converter.py | 0 s3-ferry/file_api.py => file-handler/file_handler_api.py | 0 {s3-ferry => file-handler}/requirements.txt | 0 {s3-ferry => file-handler}/s3_ferry.py | 0 8 files changed, 8 insertions(+), 8 deletions(-) rename {s3-ferry => file-handler}/Dockerfile (78%) rename {s3-ferry => file-handler}/constants.py (100%) rename {s3-ferry => file-handler}/docker-compose.yml (90%) rename {s3-ferry => file-handler}/file_converter.py (100%) rename s3-ferry/file_api.py => file-handler/file_handler_api.py (100%) rename {s3-ferry => file-handler}/requirements.txt (100%) rename {s3-ferry => file-handler}/s3_ferry.py (100%) diff --git a/docker-compose.yml b/docker-compose.yml index 41b3ecfc..60667324 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -178,11 +178,11 @@ services: volumes: - shared-volume:/shared - python_file_api: + file_handler: build: - context: ./s3-ferry + context: ./file-handler dockerfile: Dockerfile - container_name: python_file_api + container_name: file_handler volumes: - shared-volume:/shared environment: @@ -214,7 +214,7 @@ services: ports: - "3002:3000" depends_on: - - python_file_api + - file_handler - init networks: - bykstack diff --git a/s3-ferry/Dockerfile b/file-handler/Dockerfile similarity index 78% rename from s3-ferry/Dockerfile rename to file-handler/Dockerfile index 489e0f74..11a36499 100644 --- a/s3-ferry/Dockerfile +++ b/file-handler/Dockerfile @@ -4,7 +4,7 @@ WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt -COPY file_api.py . +COPY file_handler_api.py . COPY file_converter.py . COPY constants.py . COPY s3_ferry.py . @@ -14,4 +14,4 @@ RUN chown -R appuser:appuser /app EXPOSE 8000 USER appuser -CMD ["uvicorn", "file_api:app", "--host", "0.0.0.0", "--port", "8000"] +CMD ["uvicorn", "file_handler_api:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/s3-ferry/constants.py b/file-handler/constants.py similarity index 100% rename from s3-ferry/constants.py rename to file-handler/constants.py diff --git a/s3-ferry/docker-compose.yml b/file-handler/docker-compose.yml similarity index 90% rename from s3-ferry/docker-compose.yml rename to file-handler/docker-compose.yml index ee0772dc..eb3582d0 100644 --- a/s3-ferry/docker-compose.yml +++ b/file-handler/docker-compose.yml @@ -31,8 +31,8 @@ services: - API_DOCUMENTATION_ENABLED=true - S3_REGION=eu-west-1 - S3_ENDPOINT_URL=https://s3.eu-west-1.amazonaws.com - - S3_ACCESS_KEY_ID= - - S3_SECRET_ACCESS_KEY= + - S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID} + - S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY} - S3_DATA_BUCKET_NAME=esclassifier-test - S3_DATA_BUCKET_PATH=data/ - FS_DATA_DIRECTORY_PATH=/shared diff --git a/s3-ferry/file_converter.py b/file-handler/file_converter.py similarity index 100% rename from s3-ferry/file_converter.py rename to file-handler/file_converter.py diff --git a/s3-ferry/file_api.py b/file-handler/file_handler_api.py similarity index 100% rename from s3-ferry/file_api.py rename to file-handler/file_handler_api.py diff --git a/s3-ferry/requirements.txt b/file-handler/requirements.txt similarity index 100% rename from s3-ferry/requirements.txt rename to file-handler/requirements.txt diff --git a/s3-ferry/s3_ferry.py b/file-handler/s3_ferry.py similarity index 100% rename from s3-ferry/s3_ferry.py rename to file-handler/s3_ferry.py From fe882c02eabff5251811dc28fabb341f28beeedd Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Tue, 23 Jul 2024 15:03:32 +0530 Subject: [PATCH 242/582] updated config file and compose --- config.env | 7 +++++++ docker-compose.yml | 9 ++------- file-handler/docker-compose.yml | 9 ++------- 3 files changed, 11 insertions(+), 14 deletions(-) create mode 100644 config.env diff --git a/config.env b/config.env new file mode 100644 index 00000000..675c35cf --- /dev/null +++ b/config.env @@ -0,0 +1,7 @@ +API_CORS_ORIGIN=* +API_DOCUMENTATION_ENABLED=true +S3_REGION=eu-west-1 +S3_ENDPOINT_URL=https://s3.eu-west-1.amazonaws.com +S3_DATA_BUCKET_NAME=esclassifier-test +S3_DATA_BUCKET_PATH=data/ +FS_DATA_DIRECTORY_PATH=/shared diff --git a/docker-compose.yml b/docker-compose.yml index 60667324..746bf480 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -201,16 +201,11 @@ services: container_name: s3-ferry volumes: - shared-volume:/shared + env_file: + - config.env environment: - - API_CORS_ORIGIN=* - - API_DOCUMENTATION_ENABLED=true - - S3_REGION=eu-west-1 - - S3_ENDPOINT_URL=https://s3.eu-west-1.amazonaws.com - S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID} - S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY} - - S3_DATA_BUCKET_NAME=esclassifier-test - - S3_DATA_BUCKET_PATH=data/ - - FS_DATA_DIRECTORY_PATH=/shared ports: - "3002:3000" depends_on: diff --git a/file-handler/docker-compose.yml b/file-handler/docker-compose.yml index eb3582d0..1d9e897c 100644 --- a/file-handler/docker-compose.yml +++ b/file-handler/docker-compose.yml @@ -26,16 +26,11 @@ services: container_name: s3-ferry volumes: - shared-volume:/shared + env_file: + - config.env environment: - - API_CORS_ORIGIN=* - - API_DOCUMENTATION_ENABLED=true - - S3_REGION=eu-west-1 - - S3_ENDPOINT_URL=https://s3.eu-west-1.amazonaws.com - S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID} - S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY} - - S3_DATA_BUCKET_NAME=esclassifier-test - - S3_DATA_BUCKET_PATH=data/ - - FS_DATA_DIRECTORY_PATH=/shared ports: - "3000:3000" depends_on: From 997c5224f529df2eac6a343b879ae53baea26a71 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Tue, 23 Jul 2024 16:33:46 +0530 Subject: [PATCH 243/582] initial datasetprocessor update --- .../data_enrichment/Dockerfile | 0 .../config_files/paraphraser_config.json | 0 .../config_files/translator_config.json | 0 .../data_enrichment/data_enrichment.py | 5 +- .../data_enrichment/data_enrichment_api.py | 2 +- .../data_enrichment/paraphraser.py | 2 +- .../data_enrichment/requirements.txt | 0 .../data_enrichment/test_data_enrichment.py | 0 .../data_enrichment/translator.py | 2 +- dataset-processor/dataset_processor.py | 124 ++++++++++++++++++ 10 files changed, 130 insertions(+), 5 deletions(-) rename {src => dataset-processor}/data_enrichment/Dockerfile (100%) rename {src => dataset-processor}/data_enrichment/config_files/paraphraser_config.json (100%) rename {src => dataset-processor}/data_enrichment/config_files/translator_config.json (100%) rename {src => dataset-processor}/data_enrichment/data_enrichment.py (92%) rename {src => dataset-processor}/data_enrichment/data_enrichment_api.py (94%) rename {src => dataset-processor}/data_enrichment/paraphraser.py (94%) rename {src => dataset-processor}/data_enrichment/requirements.txt (100%) rename {src => dataset-processor}/data_enrichment/test_data_enrichment.py (100%) rename {src => dataset-processor}/data_enrichment/translator.py (95%) create mode 100644 dataset-processor/dataset_processor.py diff --git a/src/data_enrichment/Dockerfile b/dataset-processor/data_enrichment/Dockerfile similarity index 100% rename from src/data_enrichment/Dockerfile rename to dataset-processor/data_enrichment/Dockerfile diff --git a/src/data_enrichment/config_files/paraphraser_config.json b/dataset-processor/data_enrichment/config_files/paraphraser_config.json similarity index 100% rename from src/data_enrichment/config_files/paraphraser_config.json rename to dataset-processor/data_enrichment/config_files/paraphraser_config.json diff --git a/src/data_enrichment/config_files/translator_config.json b/dataset-processor/data_enrichment/config_files/translator_config.json similarity index 100% rename from src/data_enrichment/config_files/translator_config.json rename to dataset-processor/data_enrichment/config_files/translator_config.json diff --git a/src/data_enrichment/data_enrichment.py b/dataset-processor/data_enrichment/data_enrichment.py similarity index 92% rename from src/data_enrichment/data_enrichment.py rename to dataset-processor/data_enrichment/data_enrichment.py index df61facc..a9ff0d8f 100644 --- a/src/data_enrichment/data_enrichment.py +++ b/dataset-processor/data_enrichment/data_enrichment.py @@ -1,5 +1,5 @@ -from translator import Translator -from paraphraser import Paraphraser +from data_enrichment.translator import Translator +from data_enrichment.paraphraser import Paraphraser from langdetect import detect from typing import List, Optional @@ -37,4 +37,5 @@ def enrich_data(self, text: str, num_return_sequences: int = None, language_id: translated_paraphrases.append(translated_paraphrase) return translated_paraphrases + print("*") return paraphrases diff --git a/src/data_enrichment/data_enrichment_api.py b/dataset-processor/data_enrichment/data_enrichment_api.py similarity index 94% rename from src/data_enrichment/data_enrichment_api.py rename to dataset-processor/data_enrichment/data_enrichment_api.py index ea291dec..1e3a91ce 100644 --- a/src/data_enrichment/data_enrichment_api.py +++ b/dataset-processor/data_enrichment/data_enrichment_api.py @@ -1,6 +1,6 @@ from fastapi import FastAPI, HTTPException from pydantic import BaseModel -from data_enrichment import DataEnrichment +from data_enrichment.data_enrichment import DataEnrichment from typing import List, Optional app = FastAPI() diff --git a/src/data_enrichment/paraphraser.py b/dataset-processor/data_enrichment/paraphraser.py similarity index 94% rename from src/data_enrichment/paraphraser.py rename to dataset-processor/data_enrichment/paraphraser.py index 5dc15bcc..b671ebef 100644 --- a/src/data_enrichment/paraphraser.py +++ b/dataset-processor/data_enrichment/paraphraser.py @@ -3,7 +3,7 @@ from typing import List class Paraphraser: - def __init__(self, config_path: str = "config_files/paraphraser_config.json"): + def __init__(self, config_path: str = "data_enrichment/config_files/paraphraser_config.json"): with open(config_path, 'r') as file: config = json.load(file) diff --git a/src/data_enrichment/requirements.txt b/dataset-processor/data_enrichment/requirements.txt similarity index 100% rename from src/data_enrichment/requirements.txt rename to dataset-processor/data_enrichment/requirements.txt diff --git a/src/data_enrichment/test_data_enrichment.py b/dataset-processor/data_enrichment/test_data_enrichment.py similarity index 100% rename from src/data_enrichment/test_data_enrichment.py rename to dataset-processor/data_enrichment/test_data_enrichment.py diff --git a/src/data_enrichment/translator.py b/dataset-processor/data_enrichment/translator.py similarity index 95% rename from src/data_enrichment/translator.py rename to dataset-processor/data_enrichment/translator.py index dd8470e7..a010571e 100644 --- a/src/data_enrichment/translator.py +++ b/dataset-processor/data_enrichment/translator.py @@ -3,7 +3,7 @@ from typing import Dict, Tuple class Translator: - def __init__(self, config_path: str = "config_files/translator_config.json"): + def __init__(self, config_path: str = "data_enrichment/config_files/translator_config.json"): with open(config_path, 'r') as file: config = json.load(file) diff --git a/dataset-processor/dataset_processor.py b/dataset-processor/dataset_processor.py new file mode 100644 index 00000000..7db7e64f --- /dev/null +++ b/dataset-processor/dataset_processor.py @@ -0,0 +1,124 @@ +import re +import os +import json +from data_enrichment.data_enrichment import DataEnrichment + +class DatasetProcessor: + def __init__(self): + self.data_enricher = DataEnrichment() + + def check_and_convert(self, data): + if self._is_multple_sheet_structure(data): + return self._convert_to_single_sheet_structure(data) + elif self._is_single_sheet_structure(data): + return data + else: + raise ValueError("The provided dictionary does not match the expected structures.") + + def _is_multple_sheet_structure(self, data): + if isinstance(data, dict): + for key, value in data.items(): + if not isinstance(key, str) or not isinstance(value, list): + return False + for item in value: + if not isinstance(item, dict): + return False + return True + return False + + def _is_single_sheet_structure(self, data): + if isinstance(data, list): + for item in data: + if not isinstance(item, dict) or len(item) != 1: + return False + return True + return False + + def _convert_to_single_sheet_structure(self, data): + result = [] + for value in data.values(): + result.extend(value) + return result + + def remove_stop_words(self, data, stop_words): + stop_words_set = set(stop_words) + stop_words_pattern = re.compile(r'\b(' + r'|'.join(re.escape(word) for word in stop_words_set) + r')\b', re.IGNORECASE) + + def clean_text(text): + return stop_words_pattern.sub('', text).strip() + + cleaned_data = [] + for entry in data: + cleaned_entry = {key: clean_text(value) if isinstance(value, str) else value for key, value in entry.items()} + cleaned_data.append(cleaned_entry) + + return cleaned_data + + def enrich_data(self, data): + enriched_data = [] + for entry in data: + enriched_entry = {} + for key, value in entry.items(): + if isinstance(value, str): + enriched_value = self.data_enricher.enrich_data(value, num_return_sequences=1, language_id='en') + enriched_entry[key] = enriched_value[0] if enriched_value else value + else: + enriched_entry[key] = value + enriched_data.append(enriched_entry) + return enriched_data + + def chunk_data(self, data, chunk_size=5): + return [data[i:i + chunk_size] for i in range(0, len(data), chunk_size)] + + def save_data_to_local(self, chunked_data, output_folder='output'): + + if not os.path.exists(output_folder): + os.makedirs(output_folder) + + for i, chunk in enumerate(chunked_data, start=1): + file_path = os.path.join(output_folder, f'{i}.json') + with open(file_path, 'w') as f: + json.dump(chunk, f, indent=4) + print(f'Saved: {file_path}') + +if __name__ == "__main__": + data1 = { + "Sheet1": [ + {"from": "alice@example.com", "to": "bob@example.com", "subject": "Meeting Reminder", "body": "Don't forget our meeting tomorrow at 10 AM."}, + {"from": "carol@example.com", "to": "dave@example.com", "subject": "Project Update", "body": "The project is on track for completion next week."}, + {"from": "eve@example.com", "to": "frank@example.com", "subject": "Happy Birthday!", "body": "Wishing you a very happy birthday!"}, + {"from": "grace@example.com", "to": "heidi@example.com", "subject": "Team Lunch", "body": "Let's have lunch together this Friday."}, + {"from": "ivy@example.com", "to": "jack@example.com", "subject": "New Opportunity", "body": "We have a new opportunity that I think you'll be interested in."}, + {"from": "ken@example.com", "to": "laura@example.com", "subject": "Meeting Follow-up", "body": "Following up on our meeting last week."}, + {"from": "mike@example.com", "to": "nancy@example.com", "subject": "Question about report", "body": "Could you clarify the numbers in section 3 of the report?"}, + {"from": "oliver@example.com", "to": "pam@example.com", "subject": "Vacation Plans", "body": "I'll be out of office next week for vacation."}, + {"from": "quinn@example.com", "to": "rachel@example.com", "subject": "Conference Call", "body": "Can we schedule a conference call for Thursday?"}, + {"from": "steve@example.com", "to": "tina@example.com", "subject": "Document Review", "body": "Please review the attached document."}, + ], + # "Sheet2": [ + # {"from": "ursula@example.com", "to": "victor@example.com", "subject": "Sales Report", "body": "The sales report for Q2 is ready."}, + # {"from": "wendy@example.com", "to": "xander@example.com", "subject": "Job Application", "body": "I am interested in the open position at your company."}, + # {"from": "yara@example.com", "to": "zane@example.com", "subject": "Invoice", "body": "Attached is the invoice for the recent purchase."}, + # {"from": "adam@example.com", "to": "betty@example.com", "subject": "Networking Event", "body": "Join us for a networking event next month."}, + # {"from": "charlie@example.com", "to": "diana@example.com", "subject": "Product Feedback", "body": "We'd love to hear your feedback on our new product."}, + # {"from": "ed@example.com", "to": "fay@example.com", "subject": "Workshop Invitation", "body": "You are invited to attend our upcoming workshop."}, + # {"from": "george@example.com", "to": "hannah@example.com", "subject": "Performance Review", "body": "Your performance review is scheduled for next week."}, + # {"from": "ian@example.com", "to": "jane@example.com", "subject": "Event Reminder", "body": "Reminder: The event is on Saturday at 5 PM."}, + # {"from": "kevin@example.com", "to": "lisa@example.com", "subject": "Thank You", "body": "Thank you for your assistance with the project."}, + # {"from": "mark@example.com", "to": "nina@example.com", "subject": "New Policy", "body": "Please review the new company policy on remote work."}, + # ] + } + converter1 = DatasetProcessor() + structured_data = converter1.check_and_convert(data1) + + enriched_data = converter1.enrich_data(structured_data) + + stop_words = ["to", "New", "remote", "Work"] + cleaned_data = converter1.remove_stop_words(enriched_data, stop_words) + + chunked_data = converter1.chunk_data(cleaned_data) + + converter1.save_data_to_local(chunked_data) + + for x in cleaned_data: + print(x) From aad7283ee9fac7a1067ca1e08acc28c5abc2b029 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Tue, 23 Jul 2024 16:46:03 +0530 Subject: [PATCH 244/582] file location convertion into consta --- file-handler/constants.py | 12 +++++++++++- file-handler/file_handler_api.py | 26 ++++++++++++++++---------- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/file-handler/constants.py b/file-handler/constants.py index 74000d9b..3b2ca0a3 100644 --- a/file-handler/constants.py +++ b/file-handler/constants.py @@ -44,4 +44,14 @@ JSON_EXT = ".json" YAML_EXT = ".yaml" YML_EXT = ".yml" -XLSX_EXT = ".xlsx" \ No newline at end of file +XLSX_EXT = ".xlsx" + +SAVE_LOCATION_MINOR_UPDATE = "/dataset/{dgId}/minor_update_temp/minor_update_{}" +LOCAL_FILE_NAME_MINOR_UPDATE = "group_{dgId}minor_update" + +SAVE_LOCATION_MAJOR_UPDATE = "/dataset/{dgId}/primary_dataset/dataset_{dgId}_aggregated{}" +LOCAL_FILE_NAME_MAJOR_UPDATE = "group_{dgId}_aggregated" + +SAVE_LOCATION_AGGREGATED = "/dataset/{dgId}/primary_dataset/dataset_{dgId}_aggregated{}" +SAVE_LOCATION_CHUNK = "/dataset/{dgId}/chunks/{}{}" +LOCAL_FILE_NAME_CHUNK = "group_{dgId}_chunk_{}" diff --git a/file-handler/file_handler_api.py b/file-handler/file_handler_api.py index ebab0a9e..241709d1 100644 --- a/file-handler/file_handler_api.py +++ b/file-handler/file_handler_api.py @@ -6,7 +6,13 @@ import requests from pydantic import BaseModel from file_converter import FileConverter -from constants import UPLOAD_FAILED, UPLOAD_SUCCESS, EXPORT_TYPE_ERROR, IMPORT_TYPE_ERROR, S3_UPLOAD_FAILED, S3_DOWNLOAD_FAILED, JSON_EXT, YAML_EXT, YML_EXT, XLSX_EXT +from constants import ( + UPLOAD_FAILED, UPLOAD_SUCCESS, EXPORT_TYPE_ERROR, IMPORT_TYPE_ERROR, + S3_UPLOAD_FAILED, S3_DOWNLOAD_FAILED, JSON_EXT, YAML_EXT, YML_EXT, XLSX_EXT, + SAVE_LOCATION_MINOR_UPDATE, LOCAL_FILE_NAME_MINOR_UPDATE, + SAVE_LOCATION_MAJOR_UPDATE, LOCAL_FILE_NAME_MAJOR_UPDATE, + SAVE_LOCATION_AGGREGATED, SAVE_LOCATION_CHUNK, LOCAL_FILE_NAME_CHUNK +) from s3_ferry import S3Ferry app = FastAPI() @@ -63,7 +69,7 @@ async def upload_and_copy(request: Request, dgId: int = Form(...), dataFile: Upl with open(jsonLocalFilePath, 'w') as jsonFile: json.dump(convertedData, jsonFile, indent=4) - saveLocation = f"/dataset/{dgId}/primary_dataset/dataset_{dgId}_aggregated{JSON_EXT}" + saveLocation = SAVE_LOCATION_AGGREGATED.format(dgId, JSON_EXT) sourceFilePath = fileName.replace(YML_EXT, JSON_EXT).replace(XLSX_EXT, JSON_EXT) response = s3_ferry.transfer_file(saveLocation, "S3", sourceFilePath, "FS") @@ -88,11 +94,11 @@ async def download_and_convert(request: Request, exportData: ExportFile, backgro raise HTTPException(status_code=500, detail=EXPORT_TYPE_ERROR) if version == "minor": - saveLocation = f"/dataset/{dgId}/minor_update_temp/minor_update_{JSON_EXT}" - localFileName = f"group_{dgId}minor_update" + saveLocation = SAVE_LOCATION_MINOR_UPDATE.format(dgId, JSON_EXT) + localFileName = LOCAL_FILE_NAME_MINOR_UPDATE.format(dgId) elif version == "major": - saveLocation = f"/dataset/{dgId}/primary_dataset/dataset_{dgId}_aggregated{JSON_EXT}" - localFileName = f"group_{dgId}_aggregated" + saveLocation = SAVE_LOCATION_MAJOR_UPDATE.format(dgId, JSON_EXT) + localFileName = LOCAL_FILE_NAME_MAJOR_UPDATE.format(dgId) else: raise HTTPException(status_code=500, detail=IMPORT_TYPE_ERROR) @@ -124,10 +130,10 @@ async def download_and_convert(request: Request, exportData: ExportFile, backgro return FileResponse(outputFile, filename=os.path.basename(outputFile)) @app.get("/datasetgroup/data/download/chunk") -async def download_and_convert(request: Request, dgId: int, pageId:int, background_tasks: BackgroundTasks): +async def download_and_convert(request: Request, dgId: int, pageId: int, background_tasks: BackgroundTasks): await authenticate_user(request) - saveLocation = f"/dataset/{dgId}/chunks/{pageId}{JSON_EXT}" - localFileName = f"group_{dgId}_chunk_{pageId}" + saveLocation = SAVE_LOCATION_CHUNK.format(dgId, pageId, JSON_EXT) + localFileName = LOCAL_FILE_NAME_CHUNK.format(dgId, pageId) response = s3_ferry.transfer_file(f"{localFileName}{JSON_EXT}", "FS", saveLocation, "S3") if response.status_code != 201: @@ -143,4 +149,4 @@ async def download_and_convert(request: Request, dgId: int, pageId:int, backgrou background_tasks.add_task(os.remove, jsonFilePath) - return jsonData \ No newline at end of file + return jsonData From 1f20d27f38d2e343182fcab810e10408393f401a Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Tue, 23 Jul 2024 17:30:18 +0530 Subject: [PATCH 245/582] fixing issues and adding constant to s3 payload --- file-handler/constants.py | 15 ++++++++------- file-handler/file_handler_api.py | 19 ++++++++----------- file-handler/s3_ferry.py | 8 ++------ 3 files changed, 18 insertions(+), 24 deletions(-) diff --git a/file-handler/constants.py b/file-handler/constants.py index 3b2ca0a3..aa5d1445 100644 --- a/file-handler/constants.py +++ b/file-handler/constants.py @@ -46,12 +46,13 @@ YML_EXT = ".yml" XLSX_EXT = ".xlsx" -SAVE_LOCATION_MINOR_UPDATE = "/dataset/{dgId}/minor_update_temp/minor_update_{}" -LOCAL_FILE_NAME_MINOR_UPDATE = "group_{dgId}minor_update" +def GET_S3_FERRY_PAYLOAD(destinationFilePath:str, destinationStorageType:str, sourceFilePath:str, sourceStorageType:str): + S3_FERRY_PAYLOAD = { + "destinationFilePath": destinationFilePath, + "destinationStorageType": destinationStorageType, + "sourceFilePath": sourceFilePath, + "sourceStorageType": sourceStorageType + } + return S3_FERRY_PAYLOAD -SAVE_LOCATION_MAJOR_UPDATE = "/dataset/{dgId}/primary_dataset/dataset_{dgId}_aggregated{}" -LOCAL_FILE_NAME_MAJOR_UPDATE = "group_{dgId}_aggregated" -SAVE_LOCATION_AGGREGATED = "/dataset/{dgId}/primary_dataset/dataset_{dgId}_aggregated{}" -SAVE_LOCATION_CHUNK = "/dataset/{dgId}/chunks/{}{}" -LOCAL_FILE_NAME_CHUNK = "group_{dgId}_chunk_{}" diff --git a/file-handler/file_handler_api.py b/file-handler/file_handler_api.py index 241709d1..fb508285 100644 --- a/file-handler/file_handler_api.py +++ b/file-handler/file_handler_api.py @@ -8,10 +8,7 @@ from file_converter import FileConverter from constants import ( UPLOAD_FAILED, UPLOAD_SUCCESS, EXPORT_TYPE_ERROR, IMPORT_TYPE_ERROR, - S3_UPLOAD_FAILED, S3_DOWNLOAD_FAILED, JSON_EXT, YAML_EXT, YML_EXT, XLSX_EXT, - SAVE_LOCATION_MINOR_UPDATE, LOCAL_FILE_NAME_MINOR_UPDATE, - SAVE_LOCATION_MAJOR_UPDATE, LOCAL_FILE_NAME_MAJOR_UPDATE, - SAVE_LOCATION_AGGREGATED, SAVE_LOCATION_CHUNK, LOCAL_FILE_NAME_CHUNK + S3_UPLOAD_FAILED, S3_DOWNLOAD_FAILED, JSON_EXT, YAML_EXT, YML_EXT, XLSX_EXT ) from s3_ferry import S3Ferry @@ -69,7 +66,7 @@ async def upload_and_copy(request: Request, dgId: int = Form(...), dataFile: Upl with open(jsonLocalFilePath, 'w') as jsonFile: json.dump(convertedData, jsonFile, indent=4) - saveLocation = SAVE_LOCATION_AGGREGATED.format(dgId, JSON_EXT) + saveLocation = f"/dataset/{dgId}/primary_dataset/dataset_{dgId}_aggregated{JSON_EXT}" sourceFilePath = fileName.replace(YML_EXT, JSON_EXT).replace(XLSX_EXT, JSON_EXT) response = s3_ferry.transfer_file(saveLocation, "S3", sourceFilePath, "FS") @@ -94,11 +91,11 @@ async def download_and_convert(request: Request, exportData: ExportFile, backgro raise HTTPException(status_code=500, detail=EXPORT_TYPE_ERROR) if version == "minor": - saveLocation = SAVE_LOCATION_MINOR_UPDATE.format(dgId, JSON_EXT) - localFileName = LOCAL_FILE_NAME_MINOR_UPDATE.format(dgId) + saveLocation = f"/dataset/{dgId}/minor_update_temp/minor_update_{JSON_EXT}" + localFileName = f"group_{dgId}minor_update" elif version == "major": - saveLocation = SAVE_LOCATION_MAJOR_UPDATE.format(dgId, JSON_EXT) - localFileName = LOCAL_FILE_NAME_MAJOR_UPDATE.format(dgId) + saveLocation = f"/dataset/{dgId}/primary_dataset/dataset_{dgId}_aggregated{JSON_EXT}" + localFileName = f"group_{dgId}_aggregated" else: raise HTTPException(status_code=500, detail=IMPORT_TYPE_ERROR) @@ -132,8 +129,8 @@ async def download_and_convert(request: Request, exportData: ExportFile, backgro @app.get("/datasetgroup/data/download/chunk") async def download_and_convert(request: Request, dgId: int, pageId: int, background_tasks: BackgroundTasks): await authenticate_user(request) - saveLocation = SAVE_LOCATION_CHUNK.format(dgId, pageId, JSON_EXT) - localFileName = LOCAL_FILE_NAME_CHUNK.format(dgId, pageId) + saveLocation = f"/dataset/{dgId}/chunks/{pageId}{JSON_EXT}" + localFileName = f"group_{dgId}_chunk_{pageId}" response = s3_ferry.transfer_file(f"{localFileName}{JSON_EXT}", "FS", saveLocation, "S3") if response.status_code != 201: diff --git a/file-handler/s3_ferry.py b/file-handler/s3_ferry.py index 7585ada8..a291e3ab 100644 --- a/file-handler/s3_ferry.py +++ b/file-handler/s3_ferry.py @@ -1,16 +1,12 @@ import requests +from constants import GET_S3_FERRY_PAYLOAD class S3Ferry: def __init__(self, url): self.url = url def transfer_file(self, destinationFilePath, destinationStorageType, sourceFilePath, sourceStorageType): - payload = { - "destinationFilePath": destinationFilePath, - "destinationStorageType": destinationStorageType, - "sourceFilePath": sourceFilePath, - "sourceStorageType": sourceStorageType - } + payload = GET_S3_FERRY_PAYLOAD(destinationFilePath, destinationStorageType, sourceFilePath, sourceStorageType) response = requests.post(self.url, json=payload) return response From db9b5d9d89f20c56f35e61eed4f855834facebb7 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Tue, 23 Jul 2024 18:14:08 +0530 Subject: [PATCH 246/582] data type update --- dataset-processor/dataset_processor.py | 31 +++++++++++++++++--------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/dataset-processor/dataset_processor.py b/dataset-processor/dataset_processor.py index 7db7e64f..4cad7679 100644 --- a/dataset-processor/dataset_processor.py +++ b/dataset-processor/dataset_processor.py @@ -54,12 +54,20 @@ def clean_text(text): return cleaned_data - def enrich_data(self, data): + def select_data_to_enrich(self, data_types): + selected_fields = [] + for data_feild, type in data_types.items(): + if type == "text": + selected_fields.append(data_feild) + return selected_fields + + def enrich_data(self, data, data_types): + selected_fields = self.select_data_to_enrich(data_types) enriched_data = [] for entry in data: enriched_entry = {} for key, value in entry.items(): - if isinstance(value, str): + if isinstance(value, str) and (key in selected_fields): enriched_value = self.data_enricher.enrich_data(value, num_return_sequences=1, language_id='en') enriched_entry[key] = enriched_value[0] if enriched_value else value else: @@ -86,14 +94,14 @@ def save_data_to_local(self, chunked_data, output_folder='output'): "Sheet1": [ {"from": "alice@example.com", "to": "bob@example.com", "subject": "Meeting Reminder", "body": "Don't forget our meeting tomorrow at 10 AM."}, {"from": "carol@example.com", "to": "dave@example.com", "subject": "Project Update", "body": "The project is on track for completion next week."}, - {"from": "eve@example.com", "to": "frank@example.com", "subject": "Happy Birthday!", "body": "Wishing you a very happy birthday!"}, - {"from": "grace@example.com", "to": "heidi@example.com", "subject": "Team Lunch", "body": "Let's have lunch together this Friday."}, - {"from": "ivy@example.com", "to": "jack@example.com", "subject": "New Opportunity", "body": "We have a new opportunity that I think you'll be interested in."}, - {"from": "ken@example.com", "to": "laura@example.com", "subject": "Meeting Follow-up", "body": "Following up on our meeting last week."}, - {"from": "mike@example.com", "to": "nancy@example.com", "subject": "Question about report", "body": "Could you clarify the numbers in section 3 of the report?"}, - {"from": "oliver@example.com", "to": "pam@example.com", "subject": "Vacation Plans", "body": "I'll be out of office next week for vacation."}, - {"from": "quinn@example.com", "to": "rachel@example.com", "subject": "Conference Call", "body": "Can we schedule a conference call for Thursday?"}, - {"from": "steve@example.com", "to": "tina@example.com", "subject": "Document Review", "body": "Please review the attached document."}, + # {"from": "eve@example.com", "to": "frank@example.com", "subject": "Happy Birthday!", "body": "Wishing you a very happy birthday!"}, + # {"from": "grace@example.com", "to": "heidi@example.com", "subject": "Team Lunch", "body": "Let's have lunch together this Friday."}, + # {"from": "ivy@example.com", "to": "jack@example.com", "subject": "New Opportunity", "body": "We have a new opportunity that I think you'll be interested in."}, + # {"from": "ken@example.com", "to": "laura@example.com", "subject": "Meeting Follow-up", "body": "Following up on our meeting last week."}, + # {"from": "mike@example.com", "to": "nancy@example.com", "subject": "Question about report", "body": "Could you clarify the numbers in section 3 of the report?"}, + # {"from": "oliver@example.com", "to": "pam@example.com", "subject": "Vacation Plans", "body": "I'll be out of office next week for vacation."}, + # {"from": "quinn@example.com", "to": "rachel@example.com", "subject": "Conference Call", "body": "Can we schedule a conference call for Thursday?"}, + # {"from": "steve@example.com", "to": "tina@example.com", "subject": "Document Review", "body": "Please review the attached document."}, ], # "Sheet2": [ # {"from": "ursula@example.com", "to": "victor@example.com", "subject": "Sales Report", "body": "The sales report for Q2 is ready."}, @@ -111,7 +119,8 @@ def save_data_to_local(self, chunked_data, output_folder='output'): converter1 = DatasetProcessor() structured_data = converter1.check_and_convert(data1) - enriched_data = converter1.enrich_data(structured_data) + data_types = {"to":"email", "from":"email", "subject":"text", "body":"text"} + enriched_data = converter1.enrich_data(structured_data, data_types) stop_words = ["to", "New", "remote", "Work"] cleaned_data = converter1.remove_stop_words(enriched_data, stop_words) From d842702a7f9259a545b1ed12667c5b8860b479c3 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Tue, 23 Jul 2024 18:28:32 +0530 Subject: [PATCH 247/582] ESCLASS-123-implement update stop words endpoint and get stop words endpoint --- .../return_stop_words_duplicates.handlebars | 3 + DSL/DMapper/lib/helpers.js | 9 +- .../classifier-script-v7-stop-words.sql | 9 ++ DSL/Resql/get-stop-words.sql | 2 + DSL/Resql/insert-stop-words.sql | 3 + .../classifier/datasetgroup/stop-words.yml | 48 +++++++ .../datasetgroup/update/stop-words.yml | 118 ++++++++++++++++++ constants.ini | 4 +- 8 files changed, 193 insertions(+), 3 deletions(-) create mode 100644 DSL/DMapper/hbs/return_stop_words_duplicates.handlebars create mode 100644 DSL/Liquibase/changelog/classifier-script-v7-stop-words.sql create mode 100644 DSL/Resql/get-stop-words.sql create mode 100644 DSL/Resql/insert-stop-words.sql create mode 100644 DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/stop-words.yml create mode 100644 DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/stop-words.yml diff --git a/DSL/DMapper/hbs/return_stop_words_duplicates.handlebars b/DSL/DMapper/hbs/return_stop_words_duplicates.handlebars new file mode 100644 index 00000000..9c90fc1b --- /dev/null +++ b/DSL/DMapper/hbs/return_stop_words_duplicates.handlebars @@ -0,0 +1,3 @@ +{ + "duplicates": {{{findDuplicates inputArray existingArray}}} +} diff --git a/DSL/DMapper/lib/helpers.js b/DSL/DMapper/lib/helpers.js index e34d6283..87f1586e 100644 --- a/DSL/DMapper/lib/helpers.js +++ b/DSL/DMapper/lib/helpers.js @@ -54,4 +54,11 @@ export function getOutlookExpirationDateTime() { currentDate.setDate(currentDate.getDate() + 3); const updatedDateISOString = currentDate.toISOString(); return updatedDateISOString; -} \ No newline at end of file +} + +export function findDuplicates(inputArray, existingArray) { + const set1 = new Set(inputArray); + const duplicates = existingArray.filter((item) => set1.has(item)); + const value = JSON.stringify(duplicates); + return value; +} diff --git a/DSL/Liquibase/changelog/classifier-script-v7-stop-words.sql b/DSL/Liquibase/changelog/classifier-script-v7-stop-words.sql new file mode 100644 index 00000000..ab86dcf9 --- /dev/null +++ b/DSL/Liquibase/changelog/classifier-script-v7-stop-words.sql @@ -0,0 +1,9 @@ +-- liquibase formatted sql + +-- changeset kalsara Magamage:classifier-script-v7-changeset1 +CREATE TABLE stop_words ( + id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY, + stop_word TEXT NOT NULL, + CONSTRAINT stop_words_pkey PRIMARY KEY (id), + CONSTRAINT stop_words_unique UNIQUE (stop_word) +); \ No newline at end of file diff --git a/DSL/Resql/get-stop-words.sql b/DSL/Resql/get-stop-words.sql new file mode 100644 index 00000000..7fd17820 --- /dev/null +++ b/DSL/Resql/get-stop-words.sql @@ -0,0 +1,2 @@ +SELECT array_agg(stop_word) AS stop_words_array +FROM stop_words; \ No newline at end of file diff --git a/DSL/Resql/insert-stop-words.sql b/DSL/Resql/insert-stop-words.sql new file mode 100644 index 00000000..d7b64fe4 --- /dev/null +++ b/DSL/Resql/insert-stop-words.sql @@ -0,0 +1,3 @@ +INSERT INTO stop_words (stop_word) +SELECT unnest(ARRAY[:stop_words]) +ON CONFLICT (stop_word) DO NOTHING; \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/stop-words.yml b/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/stop-words.yml new file mode 100644 index 00000000..0b96b509 --- /dev/null +++ b/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/stop-words.yml @@ -0,0 +1,48 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'STOP-WORDS'" + method: get + accepts: json + returns: json + namespace: classifier + +get_stop_words: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-stop-words" + result: res + next: check_status + +check_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: assign_success_response + next: assign_fail_response + +assign_success_response: + assign: + format_res: { + operationSuccessful: true, + stopWords: '${res.response.body[0].stopWordsArray}' + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + operationSuccessful: true, + stopWords: '${[]}' + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + + +return_bad_request: + status: 400 + return: ${format_res} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/stop-words.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/stop-words.yml new file mode 100644 index 00000000..6e909826 --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/stop-words.yml @@ -0,0 +1,118 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'STOP-WORDS'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: stopWords + type: array + description: "Body field 'stopWords'" + +extract_request_data: + assign: + stop_words: ${incoming.body.stopWords} + next: check_for_request_data + +check_for_request_data: + switch: + - condition: ${stop_words !== null || stop_words.length > 0 } + next: get_stop_words + next: return_incorrect_request + +get_stop_words: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-stop-words" + result: res_stop_words + next: check_snapshot_status + +check_snapshot_status: + switch: + - condition: ${200 <= res_stop_words.response.statusCodeValue && res_stop_words.response.statusCodeValue < 300} + next: get_duplicate_stop_words + next: assign_fail_response + +get_duplicate_stop_words: + call: http.post + args: + url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/return_stop_words_duplicates" + headers: + type: json + body: + inputArray: ${stop_words} + existingArray: ${res_stop_words.response.body[0].stopWordsArray} + result: res_duplicates + next: check_duplicates_status + +check_duplicates_status: + switch: + - condition: ${200 <= res_duplicates.response.statusCodeValue && res_duplicates.response.statusCodeValue < 300} + next: check_for_duplicates + next: return_not_found + +check_for_duplicates: + switch: + - condition: ${res_duplicates.response.body.duplicates.length > 0 } + next: assign_duplicate_response + next: insert_stop_words + +insert_stop_words: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/insert-stop-words" + body: + stop_words: ${stop_words} + result: res_stop_words + next: check_insert_status + +check_insert_status: + switch: + - condition: ${200 <= res_stop_words.response.statusCodeValue && res_stop_words.response.statusCodeValue < 300} + next: assign_success_response + next: assign_fail_response + +assign_success_response: + assign: + format_res: { + operationSuccessful: true, + duplicate: false, + duplicateItems: '${[]}' + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + operationSuccessful: false, + duplicate: false, + duplicateItems: '${[]}' + } + next: return_bad_request + +assign_duplicate_response: + assign: + format_res: { + operationSuccessful: false, + duplicate: true, + duplicateItems: '${res_duplicates.response.body.duplicates}' + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_incorrect_request: + status: 400 + return: 'Missing Required Fields' + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end \ No newline at end of file diff --git a/constants.ini b/constants.ini index 05af8045..6ed6e787 100644 --- a/constants.ini +++ b/constants.ini @@ -7,7 +7,7 @@ CLASSIFIER_RESQL=http://resql:8082 CLASSIFIER_TIM=http://tim:8085 CLASSIFIER_CRON_MANAGER=http://cron-manager:9010 CLASSIFIER_RUUTER_PUBLIC_FRONTEND_URL=value -DOMAIN=rootcode.software +DOMAIN=localhost JIRA_API_TOKEN= value JIRA_USERNAME= value JIRA_CLOUD_DOMAIN= value @@ -17,4 +17,4 @@ OUTLOOK_CLIENT_ID=value OUTLOOK_SECRET_KEY=value OUTLOOK_REFRESH_KEY=value OUTLOOK_SCOPE=User.Read Mail.ReadWrite MailboxSettings.ReadWrite offline_access -DB_PASSWORD=value +DB_PASSWORD=rootcode From a697b2d075f25b4d20f74c3e1e785e25564d4345 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Tue, 23 Jul 2024 23:56:11 +0530 Subject: [PATCH 248/582] ESCLASS-123-implement stop words delete endpoint and refactor update and get stop words endpoints --- .../return_stop_words_duplicates.handlebars | 2 +- .../return_stop_words_not_existing.handlebars | 3 + DSL/DMapper/lib/helpers.js | 14 ++- DSL/Resql/delete-stop-words.sql | 2 + .../get-paginated-dataset-group-metadata.sql | 1 + .../datasetgroup/delete/stop-words.yml | 118 ++++++++++++++++++ .../datasetgroup/update/stop-words.yml | 4 +- 7 files changed, 138 insertions(+), 6 deletions(-) create mode 100644 DSL/DMapper/hbs/return_stop_words_not_existing.handlebars create mode 100644 DSL/Resql/delete-stop-words.sql create mode 100644 DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/delete/stop-words.yml diff --git a/DSL/DMapper/hbs/return_stop_words_duplicates.handlebars b/DSL/DMapper/hbs/return_stop_words_duplicates.handlebars index 9c90fc1b..c5b86935 100644 --- a/DSL/DMapper/hbs/return_stop_words_duplicates.handlebars +++ b/DSL/DMapper/hbs/return_stop_words_duplicates.handlebars @@ -1,3 +1,3 @@ { - "duplicates": {{{findDuplicates inputArray existingArray}}} + "duplicates": {{{findDuplicateStopWords inputArray existingArray}}} } diff --git a/DSL/DMapper/hbs/return_stop_words_not_existing.handlebars b/DSL/DMapper/hbs/return_stop_words_not_existing.handlebars new file mode 100644 index 00000000..0d5f1453 --- /dev/null +++ b/DSL/DMapper/hbs/return_stop_words_not_existing.handlebars @@ -0,0 +1,3 @@ +{ + "notExisting": {{{findNotExistingStopWords inputArray existingArray}}} +} diff --git a/DSL/DMapper/lib/helpers.js b/DSL/DMapper/lib/helpers.js index 87f1586e..b94ce2c6 100644 --- a/DSL/DMapper/lib/helpers.js +++ b/DSL/DMapper/lib/helpers.js @@ -56,9 +56,17 @@ export function getOutlookExpirationDateTime() { return updatedDateISOString; } -export function findDuplicates(inputArray, existingArray) { - const set1 = new Set(inputArray); - const duplicates = existingArray.filter((item) => set1.has(item)); +export function findDuplicateStopWords(inputArray, existingArray) { + const set1 = new Set(existingArray); + const duplicates = inputArray.filter((item) => set1.has(item)); const value = JSON.stringify(duplicates); return value; } + +export function findNotExistingStopWords(inputArray, existingArray) { + const set1 = new Set(existingArray); + const notExisting = inputArray.filter((item) => !set1.has(item)); + const value = JSON.stringify(notExisting); + return value; +} + diff --git a/DSL/Resql/delete-stop-words.sql b/DSL/Resql/delete-stop-words.sql new file mode 100644 index 00000000..ece9beb8 --- /dev/null +++ b/DSL/Resql/delete-stop-words.sql @@ -0,0 +1,2 @@ +DELETE FROM stop_words +WHERE stop_word = ANY (ARRAY[:stop_words]); \ No newline at end of file diff --git a/DSL/Resql/get-paginated-dataset-group-metadata.sql b/DSL/Resql/get-paginated-dataset-group-metadata.sql index 3d1246de..e8d9270c 100644 --- a/DSL/Resql/get-paginated-dataset-group-metadata.sql +++ b/DSL/Resql/get-paginated-dataset-group-metadata.sql @@ -5,6 +5,7 @@ SELECT dt.id, dt.patch_version, dt.latest, dt.is_enabled, + dt.enable_allowed, dt.created_timestamp, dt.last_updated_timestamp, dt.last_trained_timestamp, diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/delete/stop-words.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/delete/stop-words.yml new file mode 100644 index 00000000..c73e7582 --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/delete/stop-words.yml @@ -0,0 +1,118 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'STOP-WORDS'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: stopWords + type: array + description: "Body field 'stopWords'" + +extract_request_data: + assign: + stop_words: ${incoming.body.stopWords} + next: check_for_request_data + +check_for_request_data: + switch: + - condition: ${stop_words !== null || stop_words.length > 0 } + next: get_stop_words + next: return_incorrect_request + +get_stop_words: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-stop-words" + result: res_stop_words + next: check_stop-words_status + +check_stop-words_status: + switch: + - condition: ${200 <= res_stop_words.response.statusCodeValue && res_stop_words.response.statusCodeValue < 300} + next: get_not_existing_stop_words + next: assign_fail_response + +get_not_existing_stop_words: + call: http.post + args: + url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/return_stop_words_not_existing" + headers: + type: json + body: + inputArray: ${stop_words} + existingArray: ${res_stop_words.response.body[0].stopWordsArray} + result: res_not_existing + next: check_not_existing_status + +check_not_existing_status: + switch: + - condition: ${200 <= res_not_existing.response.statusCodeValue && res_not_existing.response.statusCodeValue < 300} + next: check_for_not_existing + next: return_not_found + +check_for_not_existing: + switch: + - condition: ${res_not_existing.response.body.notExisting.length > 0 } + next: assign_not_existing_response + next: delete_stop_words + +delete_stop_words: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/delete-stop-words" + body: + stop_words: ${stop_words} + result: res_stop_words + next: check_delete_status + +check_delete_status: + switch: + - condition: ${200 <= res_stop_words.response.statusCodeValue && res_stop_words.response.statusCodeValue < 300} + next: assign_success_response + next: assign_fail_response + +assign_success_response: + assign: + format_res: { + operationSuccessful: true, + nonexistent: false, + nonexistentItems: '${[]}' + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + operationSuccessful: false, + nonexistent: false, + nonexistentItems: '${[]}' + } + next: return_bad_request + +assign_not_existing_response: + assign: + format_res: { + operationSuccessful: false, + nonexistent: true, + nonexistentItems: '${res_not_existing.response.body.notExisting}' + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_incorrect_request: + status: 400 + return: 'Missing Required Fields' + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/stop-words.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/stop-words.yml index 6e909826..c84ce30b 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/stop-words.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/stop-words.yml @@ -28,9 +28,9 @@ get_stop_words: args: url: "[#CLASSIFIER_RESQL]/get-stop-words" result: res_stop_words - next: check_snapshot_status + next: check_stop-words_status -check_snapshot_status: +check_stop-words_status: switch: - condition: ${200 <= res_stop_words.response.statusCodeValue && res_stop_words.response.statusCodeValue < 300} next: get_duplicate_stop_words From 5a5f89ff31a1b12123fa4f14feb14e5585b0c2db Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 24 Jul 2024 01:00:40 +0530 Subject: [PATCH 249/582] data processor background task before testing --- dataset-processor/dataset_processor.py | 191 +++++++++++++++---------- file-handler/file_handler_api.py | 47 ++++++ 2 files changed, 164 insertions(+), 74 deletions(-) diff --git a/dataset-processor/dataset_processor.py b/dataset-processor/dataset_processor.py index 4cad7679..928586a1 100644 --- a/dataset-processor/dataset_processor.py +++ b/dataset-processor/dataset_processor.py @@ -1,8 +1,15 @@ +from msilib.schema import _Validation import re import os import json +import requests from data_enrichment.data_enrichment import DataEnrichment +RUUTER_URL = os.getenv("RUUTER_URL") +FILE_HANDLER_DOWNLOAD_JSON_URL = os.getenv("FILE_HANDLER_DOWNLOAD_JSON_URL") +FILE_HANDLER_STOPWORDS_URL = os.getenv("FILE_HANDLER_STOPWORDS_URL") +FILE_HANDLER_IMPORT_CHUNKS_URL = os.getenv("FILE_HANDLER_IMPORT_CHUNKS_URL") + class DatasetProcessor: def __init__(self): self.data_enricher = DataEnrichment() @@ -13,7 +20,8 @@ def check_and_convert(self, data): elif self._is_single_sheet_structure(data): return data else: - raise ValueError("The provided dictionary does not match the expected structures.") + print("The provided dictionary does not match the expected structures.") + return None def _is_multple_sheet_structure(self, data): if isinstance(data, dict): @@ -41,28 +49,24 @@ def _convert_to_single_sheet_structure(self, data): return result def remove_stop_words(self, data, stop_words): - stop_words_set = set(stop_words) - stop_words_pattern = re.compile(r'\b(' + r'|'.join(re.escape(word) for word in stop_words_set) + r')\b', re.IGNORECASE) - - def clean_text(text): - return stop_words_pattern.sub('', text).strip() - - cleaned_data = [] - for entry in data: - cleaned_entry = {key: clean_text(value) if isinstance(value, str) else value for key, value in entry.items()} - cleaned_data.append(cleaned_entry) - - return cleaned_data - - def select_data_to_enrich(self, data_types): - selected_fields = [] - for data_feild, type in data_types.items(): - if type == "text": - selected_fields.append(data_feild) - return selected_fields + try: + stop_words_set = set(stop_words) + stop_words_pattern = re.compile(r'\b(' + r'|'.join(re.escape(word) for word in stop_words_set) + r')\b', re.IGNORECASE) + + def clean_text(text): + return stop_words_pattern.sub('', text).strip() + + cleaned_data = [] + for entry in data: + cleaned_entry = {key: clean_text(value) if isinstance(value, str) else value for key, value in entry.items()} + cleaned_data.append(cleaned_entry) + + return cleaned_data + except Exception as e: + print("Error while removing Stop Words") + return None - def enrich_data(self, data, data_types): - selected_fields = self.select_data_to_enrich(data_types) + def enrich_data(self, data, selected_fields): enriched_data = [] for entry in data: enriched_entry = {} @@ -76,58 +80,97 @@ def enrich_data(self, data, data_types): return enriched_data def chunk_data(self, data, chunk_size=5): - return [data[i:i + chunk_size] for i in range(0, len(data), chunk_size)] + try: + return [data[i:i + chunk_size] for i in range(0, len(data), chunk_size)] + except Exception as e: + print("Error while splitting data into chunks") + return None - def save_data_to_local(self, chunked_data, output_folder='output'): + def save_chunked_data(self, chunked_data, authCookie, dgID): + headers = { + 'cookie': f'customJwtCookie={authCookie}', + 'Content-Type': 'application/json' + } + + for chunk in chunked_data: + payload = { + "dg_id": dgID, + "chunks": chunk + } + try: + response = requests.post(FILE_HANDLER_IMPORT_CHUNKS_URL, json=payload, headers=headers) + response.raise_for_status() + except requests.exceptions.RequestException as e: + print(f"An error occurred while uploading chunk: {e}") + return False + + return True + + def get_selected_data_fields(self, dgID:int): - if not os.path.exists(output_folder): - os.makedirs(output_folder) + data_dict = self.get_validation_data(dgID) + + validation_rules = data_dict.get("response", {}).get("validationCriteria", {}).get("validationRules", {}) + + text_fields = [] + + for field, rules in validation_rules.items(): + if rules.get("type") == "text": + text_fields.append(field) + + return text_fields + + def get_validation_data(self, dgID): + try: + params = {'dgId': dgID} + response = requests.get(RUUTER_URL, params=params) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + print(f"An error occurred: {e}") + return None + + def get_dataset(self, dg_id, custom_jwt_cookie): + params = {'dgId': dg_id} + headers = { + 'cookie': f'customJwtCookie={custom_jwt_cookie}' + } + + try: + response = requests.get(FILE_HANDLER_DOWNLOAD_JSON_URL, params=params, headers=headers) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + print(f"An error occurred: {e}") + return None + + def get_stopwords(self, dg_id, custom_jwt_cookie): + params = {'dgId': dg_id} + headers = { + 'cookie': f'customJwtCookie={custom_jwt_cookie}' + } + + try: + response = requests.get(FILE_HANDLER_STOPWORDS_URL, params=params, headers=headers) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + print(f"An error occurred: {e}") + return None - for i, chunk in enumerate(chunked_data, start=1): - file_path = os.path.join(output_folder, f'{i}.json') - with open(file_path, 'w') as f: - json.dump(chunk, f, indent=4) - print(f'Saved: {file_path}') - -if __name__ == "__main__": - data1 = { - "Sheet1": [ - {"from": "alice@example.com", "to": "bob@example.com", "subject": "Meeting Reminder", "body": "Don't forget our meeting tomorrow at 10 AM."}, - {"from": "carol@example.com", "to": "dave@example.com", "subject": "Project Update", "body": "The project is on track for completion next week."}, - # {"from": "eve@example.com", "to": "frank@example.com", "subject": "Happy Birthday!", "body": "Wishing you a very happy birthday!"}, - # {"from": "grace@example.com", "to": "heidi@example.com", "subject": "Team Lunch", "body": "Let's have lunch together this Friday."}, - # {"from": "ivy@example.com", "to": "jack@example.com", "subject": "New Opportunity", "body": "We have a new opportunity that I think you'll be interested in."}, - # {"from": "ken@example.com", "to": "laura@example.com", "subject": "Meeting Follow-up", "body": "Following up on our meeting last week."}, - # {"from": "mike@example.com", "to": "nancy@example.com", "subject": "Question about report", "body": "Could you clarify the numbers in section 3 of the report?"}, - # {"from": "oliver@example.com", "to": "pam@example.com", "subject": "Vacation Plans", "body": "I'll be out of office next week for vacation."}, - # {"from": "quinn@example.com", "to": "rachel@example.com", "subject": "Conference Call", "body": "Can we schedule a conference call for Thursday?"}, - # {"from": "steve@example.com", "to": "tina@example.com", "subject": "Document Review", "body": "Please review the attached document."}, - ], - # "Sheet2": [ - # {"from": "ursula@example.com", "to": "victor@example.com", "subject": "Sales Report", "body": "The sales report for Q2 is ready."}, - # {"from": "wendy@example.com", "to": "xander@example.com", "subject": "Job Application", "body": "I am interested in the open position at your company."}, - # {"from": "yara@example.com", "to": "zane@example.com", "subject": "Invoice", "body": "Attached is the invoice for the recent purchase."}, - # {"from": "adam@example.com", "to": "betty@example.com", "subject": "Networking Event", "body": "Join us for a networking event next month."}, - # {"from": "charlie@example.com", "to": "diana@example.com", "subject": "Product Feedback", "body": "We'd love to hear your feedback on our new product."}, - # {"from": "ed@example.com", "to": "fay@example.com", "subject": "Workshop Invitation", "body": "You are invited to attend our upcoming workshop."}, - # {"from": "george@example.com", "to": "hannah@example.com", "subject": "Performance Review", "body": "Your performance review is scheduled for next week."}, - # {"from": "ian@example.com", "to": "jane@example.com", "subject": "Event Reminder", "body": "Reminder: The event is on Saturday at 5 PM."}, - # {"from": "kevin@example.com", "to": "lisa@example.com", "subject": "Thank You", "body": "Thank you for your assistance with the project."}, - # {"from": "mark@example.com", "to": "nina@example.com", "subject": "New Policy", "body": "Please review the new company policy on remote work."}, - # ] - } - converter1 = DatasetProcessor() - structured_data = converter1.check_and_convert(data1) - - data_types = {"to":"email", "from":"email", "subject":"text", "body":"text"} - enriched_data = converter1.enrich_data(structured_data, data_types) - - stop_words = ["to", "New", "remote", "Work"] - cleaned_data = converter1.remove_stop_words(enriched_data, stop_words) - - chunked_data = converter1.chunk_data(cleaned_data) - - converter1.save_data_to_local(chunked_data) - - for x in cleaned_data: - print(x) + def process_handler(self, dgID, authCookie): + dataset = self.get_dataset(dgID, authCookie) + if dataset != None: + structured_data = self.check_and_convert(dataset) + if structured_data != None: + selected_data_fields_to_enrich = self.get_selected_data_fields(dgID) + if selected_data_fields_to_enrich != None: + enriched_data = self.enrich_data(structured_data, selected_data_fields_to_enrich) + if enriched_data != None: + stop_words = self.get_stopwords(dgID, authCookie) + if stop_words != None: + cleaned_data = self.remove_stop_words(enriched_data, stop_words) + if cleaned_data != None: + chunked_data = self.chunk_data(cleaned_data) + if chunked_data != None: + self.save_chunked_data(chunked_data, authCookie, dgID) \ No newline at end of file diff --git a/file-handler/file_handler_api.py b/file-handler/file_handler_api.py index fb508285..81796552 100644 --- a/file-handler/file_handler_api.py +++ b/file-handler/file_handler_api.py @@ -15,6 +15,7 @@ app = FastAPI() UPLOAD_DIRECTORY = os.getenv("UPLOAD_DIRECTORY", "/shared") +CHUNK_UPLOAD_DIRECTORY = os.getenv("CHUNK_UPLOAD_DIRECTORY", "/shared/chunks") RUUTER_PRIVATE_URL = os.getenv("RUUTER_PRIVATE_URL") S3_FERRY_URL = os.getenv("S3_FERRY_URL") s3_ferry = S3Ferry(S3_FERRY_URL) @@ -24,6 +25,10 @@ class ExportFile(BaseModel): version: str exportType: str +class ImportChunks(BaseModel): + dg_id: int + chunks: list + if not os.path.exists(UPLOAD_DIRECTORY): os.makedirs(UPLOAD_DIRECTORY) @@ -126,6 +131,48 @@ async def download_and_convert(request: Request, exportData: ExportFile, backgro return FileResponse(outputFile, filename=os.path.basename(outputFile)) +@app.get("/datasetgroup/data/download/json") +async def download_and_convert(request: Request, dgId: int, background_tasks: BackgroundTasks): + await authenticate_user(request) + + saveLocation = f"/dataset/{dgId}/primary_dataset/dataset_{dgId}_aggregated{JSON_EXT}" + localFileName = f"group_{dgId}_aggregated" + + response = s3_ferry.transfer_file(f"{localFileName}{JSON_EXT}", "FS", saveLocation, "S3") + if response.status_code != 201: + raise HTTPException(status_code=500, detail=S3_DOWNLOAD_FAILED) + + jsonFilePath = os.path.join('..', 'shared', f"{localFileName}{JSON_EXT}") + + with open(f"{jsonFilePath}", 'r') as jsonFile: + jsonData = json.load(jsonFile) + + background_tasks.add_task(os.remove, jsonFilePath) + + return jsonData + +@app.post("/datasetgroup/data/import/chunk") +async def upload_and_copy(request: Request, import_chunks: ImportChunks): + await authenticate_user(request) + + dgID = import_chunks.dg_id + chunks = import_chunks.chunks + + for index, chunk in enumerate(chunks, start=1): + fileLocation = os.path.join(CHUNK_UPLOAD_DIRECTORY, f"{index}.json") + with open(fileLocation, 'w') as jsonFile: + json.dump(chunk, jsonFile, indent=4) + + saveLocation = f"/dataset/{dgID}/chunks/{index}{JSON_EXT}" + + response = s3_ferry.transfer_file(saveLocation, "S3", fileLocation, "FS") + if response.status_code == 201: + os.remove(fileLocation) + else: + raise HTTPException(status_code=500, detail=S3_UPLOAD_FAILED) + else: + return True + @app.get("/datasetgroup/data/download/chunk") async def download_and_convert(request: Request, dgId: int, pageId: int, background_tasks: BackgroundTasks): await authenticate_user(request) From 731d611ae2cc8c83e1ccdb1f7c568d9d10198099 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 24 Jul 2024 01:13:38 +0530 Subject: [PATCH 250/582] proper return outputs --- dataset-processor/constants.py | 53 +++++++++++++++ dataset-processor/dataset_processor.py | 89 ++++++++++++++++---------- 2 files changed, 108 insertions(+), 34 deletions(-) create mode 100644 dataset-processor/constants.py diff --git a/dataset-processor/constants.py b/dataset-processor/constants.py new file mode 100644 index 00000000..9dda4910 --- /dev/null +++ b/dataset-processor/constants.py @@ -0,0 +1,53 @@ +# Constants for return payloads +SUCCESSFUL_OPERATION = { + "operation_status": 200, + "operation_successful": True +} + +FAILED_TO_SAVE_CHUNKED_DATA = { + "operation_status": 500, + "operation_successful": False, + "reason": "Failed to save chunked data into S3" +} + +FAILED_TO_CHUNK_CLEANED_DATA = { + "operation_status": 500, + "operation_successful": False, + "reason": "Failed to chunk the cleaned data" +} + +FAILED_TO_REMOVE_STOP_WORDS = { + "operation_status": 500, + "operation_successful": False, + "reason": "Failed to remove stop words from enriched data" +} + +FAILED_TO_GET_STOP_WORDS = { + "operation_status": 500, + "operation_successful": False, + "reason": "Failed to get stop words" +} + +FAILED_TO_ENRICH_DATA = { + "operation_status": 500, + "operation_successful": False, + "reason": "Failed to enrich data" +} + +FAILED_TO_GET_SELECTED_FIELDS = { + "operation_status": 500, + "operation_successful": False, + "reason": "Failed to get selected data fields to enrich" +} + +FAILED_TO_CHECK_AND_CONVERT = { + "operation_status": 500, + "operation_successful": False, + "reason": "Failed to check and convert dataset structure" +} + +FAILED_TO_GET_DATASET = { + "operation_status": 500, + "operation_successful": False, + "reason": "Failed to get dataset" +} diff --git a/dataset-processor/dataset_processor.py b/dataset-processor/dataset_processor.py index 928586a1..4bdc58bc 100644 --- a/dataset-processor/dataset_processor.py +++ b/dataset-processor/dataset_processor.py @@ -1,9 +1,9 @@ -from msilib.schema import _Validation import re import os import json import requests from data_enrichment.data_enrichment import DataEnrichment +from constants import * RUUTER_URL = os.getenv("RUUTER_URL") FILE_HANDLER_DOWNLOAD_JSON_URL = os.getenv("FILE_HANDLER_DOWNLOAD_JSON_URL") @@ -67,17 +67,21 @@ def clean_text(text): return None def enrich_data(self, data, selected_fields): - enriched_data = [] - for entry in data: - enriched_entry = {} - for key, value in entry.items(): - if isinstance(value, str) and (key in selected_fields): - enriched_value = self.data_enricher.enrich_data(value, num_return_sequences=1, language_id='en') - enriched_entry[key] = enriched_value[0] if enriched_value else value - else: - enriched_entry[key] = value - enriched_data.append(enriched_entry) - return enriched_data + try: + enriched_data = [] + for entry in data: + enriched_entry = {} + for key, value in entry.items(): + if isinstance(value, str) and (key in selected_fields): + enriched_value = self.data_enricher.enrich_data(value, num_return_sequences=1, language_id='en') + enriched_entry[key] = enriched_value[0] if enriched_value else value + else: + enriched_entry[key] = value + enriched_data.append(enriched_entry) + return enriched_data + except Exception as e: + print(f"Internal Error occured while data enrichment : {e}") + return None def chunk_data(self, data, chunk_size=5): try: @@ -102,23 +106,22 @@ def save_chunked_data(self, chunked_data, authCookie, dgID): response.raise_for_status() except requests.exceptions.RequestException as e: print(f"An error occurred while uploading chunk: {e}") - return False + return None return True def get_selected_data_fields(self, dgID:int): - - data_dict = self.get_validation_data(dgID) - - validation_rules = data_dict.get("response", {}).get("validationCriteria", {}).get("validationRules", {}) - - text_fields = [] - - for field, rules in validation_rules.items(): - if rules.get("type") == "text": - text_fields.append(field) - - return text_fields + try: + data_dict = self.get_validation_data(dgID) + validation_rules = data_dict.get("response", {}).get("validationCriteria", {}).get("validationRules", {}) + text_fields = [] + for field, rules in validation_rules.items(): + if rules.get("type") == "text": + text_fields.append(field) + return text_fields + except Exception as e: + print(e) + return None def get_validation_data(self, dgID): try: @@ -160,17 +163,35 @@ def get_stopwords(self, dg_id, custom_jwt_cookie): def process_handler(self, dgID, authCookie): dataset = self.get_dataset(dgID, authCookie) - if dataset != None: + if dataset is not None: structured_data = self.check_and_convert(dataset) - if structured_data != None: + if structured_data is not None: selected_data_fields_to_enrich = self.get_selected_data_fields(dgID) - if selected_data_fields_to_enrich != None: + if selected_data_fields_to_enrich is not None: enriched_data = self.enrich_data(structured_data, selected_data_fields_to_enrich) - if enriched_data != None: + if enriched_data is not None: stop_words = self.get_stopwords(dgID, authCookie) - if stop_words != None: + if stop_words is not None: cleaned_data = self.remove_stop_words(enriched_data, stop_words) - if cleaned_data != None: - chunked_data = self.chunk_data(cleaned_data) - if chunked_data != None: - self.save_chunked_data(chunked_data, authCookie, dgID) \ No newline at end of file + if cleaned_data is not None: + chunked_data = self.chunk_data(cleaned_data) + if chunked_data is not None: + operation_result = self.save_chunked_data(chunked_data, authCookie, dgID) + if operation_result: + return SUCCESSFUL_OPERATION + else: + return FAILED_TO_SAVE_CHUNKED_DATA + else: + return FAILED_TO_CHUNK_CLEANED_DATA + else: + return FAILED_TO_REMOVE_STOP_WORDS + else: + return FAILED_TO_GET_STOP_WORDS + else: + return FAILED_TO_ENRICH_DATA + else: + return FAILED_TO_GET_SELECTED_FIELDS + else: + return FAILED_TO_CHECK_AND_CONVERT + else: + return FAILED_TO_GET_DATASET From 2fd1191f1990448eb54a53adf36456ecac613ac6 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 24 Jul 2024 01:44:35 +0530 Subject: [PATCH 251/582] Dataset processor API --- dataset-processor/dataset_processor_api.py | 37 ++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 dataset-processor/dataset_processor_api.py diff --git a/dataset-processor/dataset_processor_api.py b/dataset-processor/dataset_processor_api.py new file mode 100644 index 00000000..5688b1e3 --- /dev/null +++ b/dataset-processor/dataset_processor_api.py @@ -0,0 +1,37 @@ +from fastapi import FastAPI, HTTPException, Request +from pydantic import BaseModel +from main import DatasetProcessor +import requests +import os + +app = FastAPI() +processor = DatasetProcessor() +RUUTER_PRIVATE_URL = os.getenv("RUUTER_PRIVATE_URL") + +class ProcessHandlerRequest(BaseModel): + dgID: int + authCookie: str + +async def authenticate_user(request: Request): + cookie = request.cookies.get("customJwtCookie") + if not cookie: + raise HTTPException(status_code=401, detail="No cookie found in the request") + + url = f"{RUUTER_PRIVATE_URL}/auth/jwt/userinfo" + headers = { + 'cookie': f'customJwtCookie={cookie}' + } + + response = requests.get(url, headers=headers) + if response.status_code != 200: + raise HTTPException(status_code=response.status_code, detail="Authentication failed") + +@app.post("/init-dataset-process") +async def process_handler_endpoint(request: Request, process_request: ProcessHandlerRequest): + await authenticate_user(request) + authCookie = request.cookies.get("customJwtCookie") + result = processor.process_handler(process_request.dgID, authCookie) + if result: + return result + else: + raise HTTPException(status_code=500, detail="An unknown error occurred") From aa534f9f33c3c9d89d0db21acff59bbc5ec133a3 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 24 Jul 2024 01:44:45 +0530 Subject: [PATCH 252/582] dataset processor updates --- dataset-processor/dataset_processor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dataset-processor/dataset_processor.py b/dataset-processor/dataset_processor.py index 4bdc58bc..40f5b1b2 100644 --- a/dataset-processor/dataset_processor.py +++ b/dataset-processor/dataset_processor.py @@ -5,7 +5,7 @@ from data_enrichment.data_enrichment import DataEnrichment from constants import * -RUUTER_URL = os.getenv("RUUTER_URL") +RUUTER_PRIVATE_URL = os.getenv("RUUTER_PRIVATE_URL") FILE_HANDLER_DOWNLOAD_JSON_URL = os.getenv("FILE_HANDLER_DOWNLOAD_JSON_URL") FILE_HANDLER_STOPWORDS_URL = os.getenv("FILE_HANDLER_STOPWORDS_URL") FILE_HANDLER_IMPORT_CHUNKS_URL = os.getenv("FILE_HANDLER_IMPORT_CHUNKS_URL") From 4bb135ed3e559a66d91bfcb23bbadfaf5473fe87 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 24 Jul 2024 01:44:57 +0530 Subject: [PATCH 253/582] docker file for the processor --- dataset-processor/Dockerfile | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 dataset-processor/Dockerfile diff --git a/dataset-processor/Dockerfile b/dataset-processor/Dockerfile new file mode 100644 index 00000000..05d9a435 --- /dev/null +++ b/dataset-processor/Dockerfile @@ -0,0 +1,8 @@ +FROM python:3.9-slim +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY . . +EXPOSE 8001 +ENV RUUTER_PRIVATE_URL= +CMD ["uvicorn", "dataset_processor_api:app", "--host", "0.0.0.0", "--port", "8001"] From 31283ac33307b7e37629818bd85ac9a6b37d241e Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 24 Jul 2024 01:45:11 +0530 Subject: [PATCH 254/582] requirenments to processor and dataset enrichment --- dataset-processor/requirements.txt | 77 ++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 dataset-processor/requirements.txt diff --git a/dataset-processor/requirements.txt b/dataset-processor/requirements.txt new file mode 100644 index 00000000..95087b4b --- /dev/null +++ b/dataset-processor/requirements.txt @@ -0,0 +1,77 @@ +accelerate==0.31.0 +annotated-types==0.7.0 +anyio==4.4.0 +certifi==2024.6.2 +charset-normalizer==3.3.2 +click==8.1.7 +colorama==0.4.6 +dnspython==2.6.1 +email_validator==2.2.0 +et-xmlfile==1.1.0 +exceptiongroup==1.2.1 +fastapi==0.111.0 +fastapi-cli==0.0.4 +filelock==3.13.1 +fsspec==2024.2.0 +h11==0.14.0 +httpcore==1.0.5 +httptools==0.6.1 +httpx==0.27.0 +huggingface-hub==0.23.3 +idna==3.7 +install==1.3.5 +intel-openmp==2021.4.0 +Jinja2==3.1.3 +joblib==1.4.2 +langdetect==1.0.9 +markdown-it-py==3.0.0 +MarkupSafe==2.1.5 +mdurl==0.1.2 +mkl==2021.4.0 +mpmath==1.3.0 +networkx==3.2.1 +numpy==1.26.3 +openpyxl==3.1.3 +orjson==3.10.5 +packaging==24.1 +pandas==2.2.2 +pillow==10.2.0 +protobuf==5.27.1 +psutil==5.9.8 +pydantic==2.7.4 +pydantic_core==2.18.4 +Pygments==2.18.0 +python-dateutil==2.9.0.post0 +python-dotenv==1.0.1 +python-multipart==0.0.9 +pytz==2024.1 +PyYAML==6.0.1 +regex==2024.5.15 +requests==2.32.3 +rich==13.7.1 +sacremoses==0.1.1 +safetensors==0.4.3 +scikit-learn==1.5.0 +scipy==1.13.1 +sentencepiece==0.2.0 +shellingham==1.5.4 +six==1.16.0 +sniffio==1.3.1 +starlette==0.37.2 +sympy==1.12 +tbb==2021.11.0 +threadpoolctl==3.5.0 +tokenizers==0.19.1 +torch==2.3.1+cu121 +torchaudio==2.3.1+cu121 +torchvision==0.18.1+cu121 +tqdm==4.66.4 +transformers==4.41.2 +typer==0.12.3 +typing_extensions==4.9.0 +tzdata==2024.1 +ujson==5.10.0 +urllib3==2.2.1 +uvicorn==0.30.1 +watchfiles==0.22.0 +websockets==12.0 From f8b04318a271596cb6f40b61337b734c17a1f95c Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 24 Jul 2024 01:45:24 +0530 Subject: [PATCH 255/582] docker comopse file updates --- docker-compose.yml | 46 +++++++++++++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 746bf480..1bfe74f1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -178,6 +178,23 @@ services: volumes: - shared-volume:/shared + s3-ferry: + image: s3-ferry:latest + container_name: s3-ferry + volumes: + - shared-volume:/shared + env_file: + - config.env + environment: + - S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID} + - S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY} + ports: + - "3002:3000" + depends_on: + - init + networks: + - bykstack + file_handler: build: context: ./file-handler @@ -187,6 +204,7 @@ services: - shared-volume:/shared environment: - UPLOAD_DIRECTORY=/shared + - CHUNK_UPLOAD_DIRECTORY=/shared/chunks - RUUTER_PRIVATE_URL=http://ruuter-private:8088 - S3_FERRY_URL=http://s3-ferry:3000/v1/files/copy ports: @@ -195,24 +213,26 @@ services: - bykstack depends_on: - init + - s3-ferry - s3-ferry: - image: s3-ferry:latest - container_name: s3-ferry - volumes: - - shared-volume:/shared - env_file: - - config.env + dataset-processor: + build: + context: ./dataset-processor + dockerfile: Dockerfile + container_name: dataset-processor environment: - - S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID} - - S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY} + - RUUTER_PRIVATE_URL=http://ruuter-private:8088 + - FILE_HANDLER_DOWNLOAD_JSON_URL=http://file_handler:8000/datasetgroup/data/download/json + - FILE_HANDLER_STOPWORDS_URL=http://file_handler:8000/datasetgroup/data/download/json/stopwords + - FILE_HANDLER_IMPORT_CHUNKS_URL=http://file_handler:8000/datasetgroup/data/import/chunk ports: - - "3002:3000" - depends_on: - - file_handler - - init + - "8001:8001" networks: - bykstack + depends_on: + - init + - s3-ferry + - file_handler volumes: shared-volume: From b66b5f5142192ddcc995d501320f85498af93aac Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 24 Jul 2024 01:53:24 +0530 Subject: [PATCH 256/582] initial cron job files --- DSL/CronManager/DSL/data_processor.yml | 5 +++++ DSL/CronManager/config/config.ini | 1 + DSL/CronManager/script/data_processor_exec.sh | 22 +++++++++++++++++++ 3 files changed, 28 insertions(+) create mode 100644 DSL/CronManager/DSL/data_processor.yml create mode 100644 DSL/CronManager/script/data_processor_exec.sh diff --git a/DSL/CronManager/DSL/data_processor.yml b/DSL/CronManager/DSL/data_processor.yml new file mode 100644 index 00000000..21452d0b --- /dev/null +++ b/DSL/CronManager/DSL/data_processor.yml @@ -0,0 +1,5 @@ +init_data_processor: + trigger: off + type: exec + command: "../app/scripts/data_processor_exec.sh" + allowedEnvs: ["dgID", "customJwtCookie"] \ No newline at end of file diff --git a/DSL/CronManager/config/config.ini b/DSL/CronManager/config/config.ini index b8a253ef..79cb7622 100644 --- a/DSL/CronManager/config/config.ini +++ b/DSL/CronManager/config/config.ini @@ -4,3 +4,4 @@ CLASSIFIER_RESQL=http://resql:8082 OUTLOOK_CLIENT_ID=value OUTLOOK_SECRET_KEY=value OUTLOOK_SCOPE=User.Read Mail.ReadWrite MailboxSettings.ReadWrite offline_access +INIT_DATESET_PROCESSOR_API=http://dataset-processor:8001/init-dataset-process \ No newline at end of file diff --git a/DSL/CronManager/script/data_processor_exec.sh b/DSL/CronManager/script/data_processor_exec.sh new file mode 100644 index 00000000..79618b06 --- /dev/null +++ b/DSL/CronManager/script/data_processor_exec.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# Set the working directory to the location of the script +cd "$(dirname "$0")" + +# Source the constants from the ini file +source ../config/config.ini + +# Create JSON payload +json_payload=$(jq -n \ + --arg dgID "$dgId" \ + --arg authCookie "$customJwtCookie" \ + '{dgID: $dgID|tonumber, authCookie: $authCookie}') + +# Send POST request +response=$(curl -s -X POST "$INIT_DATESET_PROCESSOR_API" \ + -H "Content-Type: application/json" \ + -b "customJwtCookie=$customJwtCookie" \ + -d "$json_payload") + +# Print response +echo "Response from API: $response" From ab85c99e915051170ad24639b79787142a98d557 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 24 Jul 2024 02:10:08 +0530 Subject: [PATCH 257/582] initial ruuter test --- .../classifier/datasetprocessor/initiate.yml | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 DSL/Ruuter.private/DSL/POST/classifier/datasetprocessor/initiate.yml diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetprocessor/initiate.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetprocessor/initiate.yml new file mode 100644 index 00000000..6f4dc6b2 --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetprocessor/initiate.yml @@ -0,0 +1,25 @@ +description: Initiate dataset processing with dgID and cookie, then call the cron manager. + +request: + method: POST + url: "/classifier/datasetprocessor/initiate" + headers: + - field: cookie + type: string + description: "Cookie field" + body: + - field: dgId + type: number + description: "Body field 'dgId'" + +response: + status: 200 + body: + message: "Dataset processing initiated successfully." + after: + - call: /data_processor + args: + url: "[#CLASSIFIER_CRON_MANAGER]/execute//" + body: + cookie: ${incoming.header.cookie} + dgId: ${dg_id} From 501c15c1952eb095bb737b2f287a142f1e6b33ec Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 24 Jul 2024 10:33:29 +0530 Subject: [PATCH 258/582] update --- file-handler/file_handler_api.py | 83 ++++++++++++++++---------------- 1 file changed, 41 insertions(+), 42 deletions(-) diff --git a/file-handler/file_handler_api.py b/file-handler/file_handler_api.py index fb508285..bf2e38cf 100644 --- a/file-handler/file_handler_api.py +++ b/file-handler/file_handler_api.py @@ -21,7 +21,6 @@ class ExportFile(BaseModel): dgId: int - version: str exportType: str if not os.path.exists(UPLOAD_DIRECTORY): @@ -46,58 +45,58 @@ async def authenticate_user(request: Request): @app.post("/datasetgroup/data/import") async def upload_and_copy(request: Request, dgId: int = Form(...), dataFile: UploadFile = File(...)): - await authenticate_user(request) - - fileConverter = FileConverter() - file_type = fileConverter._detect_file_type(dataFile.filename) - fileName = f"{uuid.uuid4()}.{file_type}" - fileLocation = os.path.join(UPLOAD_DIRECTORY, fileName) - - with open(fileLocation, "wb") as f: - f.write(dataFile.file.read()) - - success, convertedData = fileConverter.convert_to_json(fileLocation) - if not success: - upload_failed = UPLOAD_FAILED.copy() - upload_failed["reason"] = "Json file convert failed." - raise HTTPException(status_code=500, detail=upload_failed) - - jsonLocalFilePath = fileLocation.replace(YAML_EXT, JSON_EXT).replace(YML_EXT, JSON_EXT).replace(XLSX_EXT, JSON_EXT) - with open(jsonLocalFilePath, 'w') as jsonFile: - json.dump(convertedData, jsonFile, indent=4) + try: + await authenticate_user(request) + + print(f"Received dgId: {dgId}") + print(f"Received filename: {dataFile.filename}") + + fileConverter = FileConverter() + file_type = fileConverter._detect_file_type(dataFile.filename) + fileName = f"{uuid.uuid4()}.{file_type}" + fileLocation = os.path.join(UPLOAD_DIRECTORY, fileName) + + with open(fileLocation, "wb") as f: + f.write(dataFile.file.read()) + + success, convertedData = fileConverter.convert_to_json(fileLocation) + if not success: + upload_failed = UPLOAD_FAILED.copy() + upload_failed["reason"] = "Json file convert failed." + raise HTTPException(status_code=500, detail=upload_failed) + + jsonLocalFilePath = fileLocation.replace(YAML_EXT, JSON_EXT).replace(YML_EXT, JSON_EXT).replace(XLSX_EXT, JSON_EXT) + with open(jsonLocalFilePath, 'w') as jsonFile: + json.dump(convertedData, jsonFile, indent=4) - saveLocation = f"/dataset/{dgId}/primary_dataset/dataset_{dgId}_aggregated{JSON_EXT}" - sourceFilePath = fileName.replace(YML_EXT, JSON_EXT).replace(XLSX_EXT, JSON_EXT) - - response = s3_ferry.transfer_file(saveLocation, "S3", sourceFilePath, "FS") - if response.status_code == 201: - os.remove(fileLocation) - if fileLocation != jsonLocalFilePath: - os.remove(jsonLocalFilePath) - upload_success = UPLOAD_SUCCESS.copy() - upload_success["saved_file_path"] = saveLocation - return JSONResponse(status_code=200, content=upload_success) - else: - raise HTTPException(status_code=500, detail=S3_UPLOAD_FAILED) + saveLocation = f"/dataset/{dgId}/primary_dataset/dataset_{dgId}_aggregated{JSON_EXT}" + sourceFilePath = fileName.replace(YML_EXT, JSON_EXT).replace(XLSX_EXT, JSON_EXT) + + response = s3_ferry.transfer_file(saveLocation, "S3", sourceFilePath, "FS") + if response.status_code == 201: + os.remove(fileLocation) + if fileLocation != jsonLocalFilePath: + os.remove(jsonLocalFilePath) + upload_success = UPLOAD_SUCCESS.copy() + upload_success["saved_file_path"] = saveLocation + return JSONResponse(status_code=200, content=upload_success) + else: + raise HTTPException(status_code=500, detail=S3_UPLOAD_FAILED) + except Exception as e: + print(f"Exception in data/import : {e}") + raise HTTPException(status_code=500, detail=str(e)) @app.post("/datasetgroup/data/download") async def download_and_convert(request: Request, exportData: ExportFile, background_tasks: BackgroundTasks): await authenticate_user(request) dgId = exportData.dgId - version = exportData.version exportType = exportData.exportType if exportType not in ["xlsx", "yaml", "json"]: raise HTTPException(status_code=500, detail=EXPORT_TYPE_ERROR) - if version == "minor": - saveLocation = f"/dataset/{dgId}/minor_update_temp/minor_update_{JSON_EXT}" - localFileName = f"group_{dgId}minor_update" - elif version == "major": - saveLocation = f"/dataset/{dgId}/primary_dataset/dataset_{dgId}_aggregated{JSON_EXT}" - localFileName = f"group_{dgId}_aggregated" - else: - raise HTTPException(status_code=500, detail=IMPORT_TYPE_ERROR) + saveLocation = f"/dataset/{dgId}/primary_dataset/dataset_{dgId}_aggregated{JSON_EXT}" + localFileName = f"group_{dgId}_aggregated" response = s3_ferry.transfer_file(f"{localFileName}{JSON_EXT}", "FS", saveLocation, "S3") if response.status_code != 201: From 3518e62d5c1c2d0a489339b30f9be1a60f41cb2e Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 24 Jul 2024 11:53:00 +0530 Subject: [PATCH 259/582] compose updates --- docker-compose.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 746bf480..e8098c90 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -23,7 +23,7 @@ services: container_name: ruuter-private image: ruuter environment: - - application.cors.allowedOrigins=http://localhost:3001,http://localhost:8088,http://localhost:3002,http://localhost:3004 + - application.cors.allowedOrigins=http://localhost:3001,http://localhost:8088,http://localhost:3002,http://localhost:3004,http://localhost:8000 - application.httpCodesAllowList=200,201,202,204,400,401,403,404,500 - application.internalRequests.allowedIPs=127.0.0.1 - application.logging.displayRequestContent=true @@ -178,11 +178,11 @@ services: volumes: - shared-volume:/shared - file_handler: + file-handler: build: context: ./file-handler dockerfile: Dockerfile - container_name: file_handler + container_name: file-handler volumes: - shared-volume:/shared environment: From 56c4ce72cee6278c33153ccc84b93eab6cbf6ef4 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 24 Jul 2024 11:53:27 +0530 Subject: [PATCH 260/582] cors settings and snake case removal --- file-handler/file_handler_api.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/file-handler/file_handler_api.py b/file-handler/file_handler_api.py index bf2e38cf..4b3bf666 100644 --- a/file-handler/file_handler_api.py +++ b/file-handler/file_handler_api.py @@ -1,4 +1,5 @@ from fastapi import FastAPI, File, UploadFile, HTTPException, Form, Request, BackgroundTasks +from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse, JSONResponse import os import json @@ -14,6 +15,15 @@ app = FastAPI() +app.add_middleware( + CORSMiddleware, + allow_origins=["http://localhost:3001", "http://localhost:3002"], + allow_credentials=True, + allow_methods=["GET", "POST"], + allow_headers=["*"], +) + + UPLOAD_DIRECTORY = os.getenv("UPLOAD_DIRECTORY", "/shared") RUUTER_PRIVATE_URL = os.getenv("RUUTER_PRIVATE_URL") S3_FERRY_URL = os.getenv("S3_FERRY_URL") @@ -87,7 +97,7 @@ async def upload_and_copy(request: Request, dgId: int = Form(...), dataFile: Upl raise HTTPException(status_code=500, detail=str(e)) @app.post("/datasetgroup/data/download") -async def download_and_convert(request: Request, exportData: ExportFile, background_tasks: BackgroundTasks): +async def download_and_convert(request: Request, exportData: ExportFile, backgroundTasks: BackgroundTasks): await authenticate_user(request) dgId = exportData.dgId exportType = exportData.exportType @@ -119,14 +129,14 @@ async def download_and_convert(request: Request, exportData: ExportFile, backgro else: raise HTTPException(status_code=500, detail=EXPORT_TYPE_ERROR) - background_tasks.add_task(os.remove, jsonFilePath) + backgroundTasks.add_task(os.remove, jsonFilePath) if outputFile != jsonFilePath: - background_tasks.add_task(os.remove, outputFile) + backgroundTasks.add_task(os.remove, outputFile) return FileResponse(outputFile, filename=os.path.basename(outputFile)) @app.get("/datasetgroup/data/download/chunk") -async def download_and_convert(request: Request, dgId: int, pageId: int, background_tasks: BackgroundTasks): +async def download_and_convert(request: Request, dgId: int, pageId: int, backgroundTasks: BackgroundTasks): await authenticate_user(request) saveLocation = f"/dataset/{dgId}/chunks/{pageId}{JSON_EXT}" localFileName = f"group_{dgId}_chunk_{pageId}" @@ -143,6 +153,6 @@ async def download_and_convert(request: Request, dgId: int, pageId: int, backgro for index, item in enumerate(jsonData, start=1): item['rowID'] = index - background_tasks.add_task(os.remove, jsonFilePath) + backgroundTasks.add_task(os.remove, jsonFilePath) return jsonData From a07dfc3ce4a2586350d505773f992a5c005a7596 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Wed, 24 Jul 2024 11:56:13 +0530 Subject: [PATCH 261/582] ESCLASS-119-payload-integrate with file-handler to receive payload from s3 ferry --- .../classifier/datasetgroup/group/data.yml | 23 +++++++++++-------- constants.ini | 3 ++- docker-compose.yml | 10 ++++---- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/group/data.yml b/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/group/data.yml index 0802fbeb..5c77bb8a 100644 --- a/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/group/data.yml +++ b/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/group/data.yml @@ -14,11 +14,16 @@ declaration: - field: pageNum type: number description: "Parameter 'pageNum'" + headers: + - field: cookie + type: string + description: "Cookie field" extract_data: assign: group_id: ${Number(incoming.params.groupId)} page_num: ${Number(incoming.params.pageNum)} + cookie: ${incoming.headers.cookie} next: get_dataset_group_fields_by_id get_dataset_group_fields_by_id: @@ -43,15 +48,15 @@ check_fields_data_exist: next: assign_fail_response get_dataset_group_data_by_id: - call: reflect.mock + call: http.get args: - url: "[#CLASSIFIER_RESQL]/get-dataset-group-metadata-by-id" - body: - group_id: ${group_id} - page_num: ${page_num} - response: - statusCodeValue: 200 - dataPayload: [] + url: "[#CLASSIFIER_FILE_HANDLER]/datasetgroup/data/download/chunk" + headers: + type: json + cookie: ${cookie} + query: + dgId: ${group_id} + pageId: ${page_num} result: res_data next: check_data_status @@ -71,7 +76,7 @@ assign_formated_response: val: [{ dgId: '${group_id}', fields: '${val === [] ? [] :val.fields}', - dataPayload: '[]' + dataPayload: '${res_data.response.body}' }] next: assign_success_response diff --git a/constants.ini b/constants.ini index 05af8045..8701cb86 100644 --- a/constants.ini +++ b/constants.ini @@ -6,8 +6,9 @@ CLASSIFIER_DMAPPER=http://data-mapper:3000 CLASSIFIER_RESQL=http://resql:8082 CLASSIFIER_TIM=http://tim:8085 CLASSIFIER_CRON_MANAGER=http://cron-manager:9010 +CLASSIFIER_FILE_HANDLER=http://file-handler:8000 CLASSIFIER_RUUTER_PUBLIC_FRONTEND_URL=value -DOMAIN=rootcode.software +DOMAIN=localhost JIRA_API_TOKEN= value JIRA_USERNAME= value JIRA_CLOUD_DOMAIN= value diff --git a/docker-compose.yml b/docker-compose.yml index 746bf480..e6f8ebc1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ services: container_name: ruuter-public image: ruuter environment: - - application.cors.allowedOrigins=http://localhost:8086,http://localhost:3001,http://localhost:3003,http://localhost:3004,http://localhost:8080 + - application.cors.allowedOrigins=http://localhost:8086,http://localhost:3001,http://localhost:3003,http://localhost:3004,http://localhost:8080,http://localhost:8000 - application.httpCodesAllowList=200,201,202,400,401,403,500 - application.internalRequests.allowedIPs=127.0.0.1 - application.logging.displayRequestContent=true @@ -23,7 +23,7 @@ services: container_name: ruuter-private image: ruuter environment: - - application.cors.allowedOrigins=http://localhost:3001,http://localhost:8088,http://localhost:3002,http://localhost:3004 + - application.cors.allowedOrigins=http://localhost:3001,http://localhost:8088,http://localhost:3002,http://localhost:3004,http://localhost:8000 - application.httpCodesAllowList=200,201,202,204,400,401,403,404,500 - application.internalRequests.allowedIPs=127.0.0.1 - application.logging.displayRequestContent=true @@ -178,11 +178,11 @@ services: volumes: - shared-volume:/shared - file_handler: + file-handler: build: context: ./file-handler dockerfile: Dockerfile - container_name: file_handler + container_name: file-handler volumes: - shared-volume:/shared environment: @@ -209,7 +209,7 @@ services: ports: - "3002:3000" depends_on: - - file_handler + - file-handler - init networks: - bykstack From 571d985dac31bfae5a28aa3515be960bedfd691e Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 24 Jul 2024 11:58:41 +0530 Subject: [PATCH 262/582] snake case fix --- file-handler/file_handler_api.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/file-handler/file_handler_api.py b/file-handler/file_handler_api.py index 4b3bf666..b8477564 100644 --- a/file-handler/file_handler_api.py +++ b/file-handler/file_handler_api.py @@ -62,8 +62,8 @@ async def upload_and_copy(request: Request, dgId: int = Form(...), dataFile: Upl print(f"Received filename: {dataFile.filename}") fileConverter = FileConverter() - file_type = fileConverter._detect_file_type(dataFile.filename) - fileName = f"{uuid.uuid4()}.{file_type}" + fileType = fileConverter._detect_file_type(dataFile.filename) + fileName = f"{uuid.uuid4()}.{fileType}" fileLocation = os.path.join(UPLOAD_DIRECTORY, fileName) with open(fileLocation, "wb") as f: @@ -71,9 +71,9 @@ async def upload_and_copy(request: Request, dgId: int = Form(...), dataFile: Upl success, convertedData = fileConverter.convert_to_json(fileLocation) if not success: - upload_failed = UPLOAD_FAILED.copy() - upload_failed["reason"] = "Json file convert failed." - raise HTTPException(status_code=500, detail=upload_failed) + uploadFailed = UPLOAD_FAILED.copy() + uploadFailed["reason"] = "Json file convert failed." + raise HTTPException(status_code=500, detail=uploadFailed) jsonLocalFilePath = fileLocation.replace(YAML_EXT, JSON_EXT).replace(YML_EXT, JSON_EXT).replace(XLSX_EXT, JSON_EXT) with open(jsonLocalFilePath, 'w') as jsonFile: @@ -87,9 +87,9 @@ async def upload_and_copy(request: Request, dgId: int = Form(...), dataFile: Upl os.remove(fileLocation) if fileLocation != jsonLocalFilePath: os.remove(jsonLocalFilePath) - upload_success = UPLOAD_SUCCESS.copy() - upload_success["saved_file_path"] = saveLocation - return JSONResponse(status_code=200, content=upload_success) + uploadSuccess = UPLOAD_SUCCESS.copy() + uploadSuccess["saved_file_path"] = saveLocation + return JSONResponse(status_code=200, content=uploadSuccess) else: raise HTTPException(status_code=500, detail=S3_UPLOAD_FAILED) except Exception as e: From a09162c03d306ded4683fd4cc723cef599c831e0 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Wed, 24 Jul 2024 12:21:12 +0530 Subject: [PATCH 263/582] classifier-132- implement data process initiation API --- .../classifier/datasetprocessor/initiate.yml | 79 +++++++++++++------ 1 file changed, 57 insertions(+), 22 deletions(-) diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetprocessor/initiate.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetprocessor/initiate.yml index 6f4dc6b2..4f7a2ad1 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetprocessor/initiate.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetprocessor/initiate.yml @@ -1,25 +1,60 @@ -description: Initiate dataset processing with dgID and cookie, then call the cron manager. +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'INITIATE'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: dgId + type: number + description: "Body field 'dgId'" + headers: + - field: cookie + type: string + description: "Cookie field" -request: - method: POST - url: "/classifier/datasetprocessor/initiate" - headers: - - field: cookie - type: string - description: "Cookie field" - body: - - field: dgId - type: number - description: "Body field 'dgId'" +extract_request_data: + assign: + dg_id: ${incoming.body.dgId} + cookie: ${incoming.headers.cookie} + next: execute_cron_manager -response: +execute_cron_manager: + call: http.post + args: + url: "[#CLASSIFIER_CRON_MANAGER]/execute/data_processor/init_data_processor" + body: + cookie: ${incoming.header.cookie} + dgId: ${dg_id} + result: res + next: assign_success_response + +assign_success_response: + assign: + format_res: { + dgId: '${dg_id}', + operationSuccessful: true, + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + dgId: '${dg_id}', + operationSuccessful: false, + } + next: return_bad_request + +return_ok: status: 200 - body: - message: "Dataset processing initiated successfully." - after: - - call: /data_processor - args: - url: "[#CLASSIFIER_CRON_MANAGER]/execute//" - body: - cookie: ${incoming.header.cookie} - dgId: ${dg_id} + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end + From cc2229b4a7a7df98038580cf9abe48344d7da6f9 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 24 Jul 2024 12:38:03 +0530 Subject: [PATCH 264/582] data processor diable --- docker-compose.yml | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 1bfe74f1..7b9e6486 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -215,24 +215,24 @@ services: - init - s3-ferry - dataset-processor: - build: - context: ./dataset-processor - dockerfile: Dockerfile - container_name: dataset-processor - environment: - - RUUTER_PRIVATE_URL=http://ruuter-private:8088 - - FILE_HANDLER_DOWNLOAD_JSON_URL=http://file_handler:8000/datasetgroup/data/download/json - - FILE_HANDLER_STOPWORDS_URL=http://file_handler:8000/datasetgroup/data/download/json/stopwords - - FILE_HANDLER_IMPORT_CHUNKS_URL=http://file_handler:8000/datasetgroup/data/import/chunk - ports: - - "8001:8001" - networks: - - bykstack - depends_on: - - init - - s3-ferry - - file_handler + # dataset-processor: + # build: + # context: ./dataset-processor + # dockerfile: Dockerfile + # container_name: dataset-processor + # environment: + # - RUUTER_PRIVATE_URL=http://ruuter-private:8088 + # - FILE_HANDLER_DOWNLOAD_JSON_URL=http://file_handler:8000/datasetgroup/data/download/json + # - FILE_HANDLER_STOPWORDS_URL=http://file_handler:8000/datasetgroup/data/download/json/stopwords + # - FILE_HANDLER_IMPORT_CHUNKS_URL=http://file_handler:8000/datasetgroup/data/import/chunk + # ports: + # - "8001:8001" + # networks: + # - bykstack + # depends_on: + # - init + # - s3-ferry + # - file_handler volumes: shared-volume: From 780398e8ac401d71fa1b677ad176b3c1a2a84015 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 24 Jul 2024 12:55:50 +0530 Subject: [PATCH 265/582] cron --- DSL/CronManager/DSL/data_processor.yml | 2 +- DSL/CronManager/script/data_processor_exec.sh | 17 +++-------------- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/DSL/CronManager/DSL/data_processor.yml b/DSL/CronManager/DSL/data_processor.yml index 21452d0b..1ce18558 100644 --- a/DSL/CronManager/DSL/data_processor.yml +++ b/DSL/CronManager/DSL/data_processor.yml @@ -2,4 +2,4 @@ init_data_processor: trigger: off type: exec command: "../app/scripts/data_processor_exec.sh" - allowedEnvs: ["dgID", "customJwtCookie"] \ No newline at end of file + allowedEnvs: ["dgId", "cookie"] \ No newline at end of file diff --git a/DSL/CronManager/script/data_processor_exec.sh b/DSL/CronManager/script/data_processor_exec.sh index 79618b06..391e9b6e 100644 --- a/DSL/CronManager/script/data_processor_exec.sh +++ b/DSL/CronManager/script/data_processor_exec.sh @@ -6,17 +6,6 @@ cd "$(dirname "$0")" # Source the constants from the ini file source ../config/config.ini -# Create JSON payload -json_payload=$(jq -n \ - --arg dgID "$dgId" \ - --arg authCookie "$customJwtCookie" \ - '{dgID: $dgID|tonumber, authCookie: $authCookie}') - -# Send POST request -response=$(curl -s -X POST "$INIT_DATESET_PROCESSOR_API" \ - -H "Content-Type: application/json" \ - -b "customJwtCookie=$customJwtCookie" \ - -d "$json_payload") - -# Print response -echo "Response from API: $response" +echo "dgID $dgId" +echo "cookie $cookie" +echo "API $INIT_DATESET_PROCESSOR_API" From 2059abc42717d7bd7741884c34c017c64043e5cc Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Wed, 24 Jul 2024 13:20:03 +0530 Subject: [PATCH 266/582] ESCLASS-123-change local db configs --- constants.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/constants.ini b/constants.ini index 6ed6e787..98f0d4ae 100644 --- a/constants.ini +++ b/constants.ini @@ -17,4 +17,4 @@ OUTLOOK_CLIENT_ID=value OUTLOOK_SECRET_KEY=value OUTLOOK_REFRESH_KEY=value OUTLOOK_SCOPE=User.Read Mail.ReadWrite MailboxSettings.ReadWrite offline_access -DB_PASSWORD=rootcode +DB_PASSWORD=value From fa7a909bb6c03292314eae52f99f3544e770feee Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 24 Jul 2024 14:12:23 +0530 Subject: [PATCH 267/582] changes --- DSL/CronManager/DSL/data_processor.yml | 2 +- DSL/CronManager/script/data_processor_exec.sh | 2 +- .../DSL/POST/classifier/datasetprocessor/initiate.yml | 10 +++------- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/DSL/CronManager/DSL/data_processor.yml b/DSL/CronManager/DSL/data_processor.yml index 1ce18558..53a93ace 100644 --- a/DSL/CronManager/DSL/data_processor.yml +++ b/DSL/CronManager/DSL/data_processor.yml @@ -2,4 +2,4 @@ init_data_processor: trigger: off type: exec command: "../app/scripts/data_processor_exec.sh" - allowedEnvs: ["dgId", "cookie"] \ No newline at end of file + allowedEnvs: ["dgId"] \ No newline at end of file diff --git a/DSL/CronManager/script/data_processor_exec.sh b/DSL/CronManager/script/data_processor_exec.sh index 391e9b6e..a27149d1 100644 --- a/DSL/CronManager/script/data_processor_exec.sh +++ b/DSL/CronManager/script/data_processor_exec.sh @@ -7,5 +7,5 @@ cd "$(dirname "$0")" source ../config/config.ini echo "dgID $dgId" -echo "cookie $cookie" +# echo "cookie $cookie" echo "API $INIT_DATESET_PROCESSOR_API" diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetprocessor/initiate.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetprocessor/initiate.yml index 4f7a2ad1..28ea8893 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetprocessor/initiate.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetprocessor/initiate.yml @@ -11,23 +11,19 @@ declaration: - field: dgId type: number description: "Body field 'dgId'" - headers: - - field: cookie - type: string - description: "Cookie field" extract_request_data: assign: dg_id: ${incoming.body.dgId} - cookie: ${incoming.headers.cookie} + # cookie: ${incoming.headers.cookie} next: execute_cron_manager execute_cron_manager: call: http.post args: url: "[#CLASSIFIER_CRON_MANAGER]/execute/data_processor/init_data_processor" - body: - cookie: ${incoming.header.cookie} + query: + # cookie: ${incoming.header.cookie} dgId: ${dg_id} result: res next: assign_success_response From a35581423de717edfa957fa47bb7c241b45627e9 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 24 Jul 2024 15:17:57 +0530 Subject: [PATCH 268/582] making all local variables snake case --- file-handler/file_handler_api.py | 105 +++++++++++++++---------------- 1 file changed, 52 insertions(+), 53 deletions(-) diff --git a/file-handler/file_handler_api.py b/file-handler/file_handler_api.py index b8477564..0cea766c 100644 --- a/file-handler/file_handler_api.py +++ b/file-handler/file_handler_api.py @@ -23,7 +23,6 @@ allow_headers=["*"], ) - UPLOAD_DIRECTORY = os.getenv("UPLOAD_DIRECTORY", "/shared") RUUTER_PRIVATE_URL = os.getenv("RUUTER_PRIVATE_URL") S3_FERRY_URL = os.getenv("S3_FERRY_URL") @@ -61,35 +60,35 @@ async def upload_and_copy(request: Request, dgId: int = Form(...), dataFile: Upl print(f"Received dgId: {dgId}") print(f"Received filename: {dataFile.filename}") - fileConverter = FileConverter() - fileType = fileConverter._detect_file_type(dataFile.filename) - fileName = f"{uuid.uuid4()}.{fileType}" - fileLocation = os.path.join(UPLOAD_DIRECTORY, fileName) + file_converter = FileConverter() + file_type = file_converter._detect_file_type(dataFile.filename) + file_name = f"{uuid.uuid4()}.{file_type}" + file_location = os.path.join(UPLOAD_DIRECTORY, file_name) - with open(fileLocation, "wb") as f: + with open(file_location, "wb") as f: f.write(dataFile.file.read()) - success, convertedData = fileConverter.convert_to_json(fileLocation) + success, converted_data = file_converter.convert_to_json(file_location) if not success: - uploadFailed = UPLOAD_FAILED.copy() - uploadFailed["reason"] = "Json file convert failed." - raise HTTPException(status_code=500, detail=uploadFailed) + upload_failed = UPLOAD_FAILED.copy() + upload_failed["reason"] = "Json file convert failed." + raise HTTPException(status_code=500, detail=upload_failed) - jsonLocalFilePath = fileLocation.replace(YAML_EXT, JSON_EXT).replace(YML_EXT, JSON_EXT).replace(XLSX_EXT, JSON_EXT) - with open(jsonLocalFilePath, 'w') as jsonFile: - json.dump(convertedData, jsonFile, indent=4) + json_local_file_path = file_location.replace(YAML_EXT, JSON_EXT).replace(YML_EXT, JSON_EXT).replace(XLSX_EXT, JSON_EXT) + with open(json_local_file_path, 'w') as json_file: + json.dump(converted_data, json_file, indent=4) - saveLocation = f"/dataset/{dgId}/primary_dataset/dataset_{dgId}_aggregated{JSON_EXT}" - sourceFilePath = fileName.replace(YML_EXT, JSON_EXT).replace(XLSX_EXT, JSON_EXT) + save_location = f"/dataset/{dgId}/primary_dataset/dataset_{dgId}_aggregated{JSON_EXT}" + source_file_path = file_name.replace(YML_EXT, JSON_EXT).replace(XLSX_EXT, JSON_EXT) - response = s3_ferry.transfer_file(saveLocation, "S3", sourceFilePath, "FS") + response = s3_ferry.transfer_file(save_location, "S3", source_file_path, "FS") if response.status_code == 201: - os.remove(fileLocation) - if fileLocation != jsonLocalFilePath: - os.remove(jsonLocalFilePath) - uploadSuccess = UPLOAD_SUCCESS.copy() - uploadSuccess["saved_file_path"] = saveLocation - return JSONResponse(status_code=200, content=uploadSuccess) + os.remove(file_location) + if file_location != json_local_file_path: + os.remove(json_local_file_path) + upload_success = UPLOAD_SUCCESS.copy() + upload_success["saved_file_path"] = save_location + return JSONResponse(status_code=200, content=upload_success) else: raise HTTPException(status_code=500, detail=S3_UPLOAD_FAILED) except Exception as e: @@ -99,60 +98,60 @@ async def upload_and_copy(request: Request, dgId: int = Form(...), dataFile: Upl @app.post("/datasetgroup/data/download") async def download_and_convert(request: Request, exportData: ExportFile, backgroundTasks: BackgroundTasks): await authenticate_user(request) - dgId = exportData.dgId - exportType = exportData.exportType + dg_id = exportData.dgId + export_type = exportData.exportType - if exportType not in ["xlsx", "yaml", "json"]: + if export_type not in ["xlsx", "yaml", "json"]: raise HTTPException(status_code=500, detail=EXPORT_TYPE_ERROR) - saveLocation = f"/dataset/{dgId}/primary_dataset/dataset_{dgId}_aggregated{JSON_EXT}" - localFileName = f"group_{dgId}_aggregated" + save_location = f"/dataset/{dg_id}/primary_dataset/dataset_{dg_id}_aggregated{JSON_EXT}" + local_file_name = f"group_{dg_id}_aggregated" - response = s3_ferry.transfer_file(f"{localFileName}{JSON_EXT}", "FS", saveLocation, "S3") + response = s3_ferry.transfer_file(f"{local_file_name}{JSON_EXT}", "FS", save_location, "S3") if response.status_code != 201: raise HTTPException(status_code=500, detail=S3_DOWNLOAD_FAILED) - jsonFilePath = os.path.join('..', 'shared', f"{localFileName}{JSON_EXT}") + json_file_path = os.path.join('..', 'shared', f"{local_file_name}{JSON_EXT}") - fileConverter = FileConverter() - with open(f"{jsonFilePath}", 'r') as jsonFile: - jsonData = json.load(jsonFile) + file_converter = FileConverter() + with open(f"{json_file_path}", 'r') as json_file: + json_data = json.load(json_file) - if exportType == "xlsx": - outputFile = f"{localFileName}{XLSX_EXT}" - fileConverter.convert_json_to_xlsx(jsonData, outputFile) - elif exportType == "yaml": - outputFile = f"{localFileName}{YAML_EXT}" - fileConverter.convert_json_to_yaml(jsonData, outputFile) - elif exportType == "json": - outputFile = f"{jsonFilePath}" + if export_type == "xlsx": + output_file = f"{local_file_name}{XLSX_EXT}" + file_converter.convert_json_to_xlsx(json_data, output_file) + elif export_type == "yaml": + output_file = f"{local_file_name}{YAML_EXT}" + file_converter.convert_json_to_yaml(json_data, output_file) + elif export_type == "json": + output_file = f"{json_file_path}" else: raise HTTPException(status_code=500, detail=EXPORT_TYPE_ERROR) - backgroundTasks.add_task(os.remove, jsonFilePath) - if outputFile != jsonFilePath: - backgroundTasks.add_task(os.remove, outputFile) + backgroundTasks.add_task(os.remove, json_file_path) + if output_file != json_file_path: + backgroundTasks.add_task(os.remove, output_file) - return FileResponse(outputFile, filename=os.path.basename(outputFile)) + return FileResponse(output_file, filename=os.path.basename(output_file)) @app.get("/datasetgroup/data/download/chunk") async def download_and_convert(request: Request, dgId: int, pageId: int, backgroundTasks: BackgroundTasks): await authenticate_user(request) - saveLocation = f"/dataset/{dgId}/chunks/{pageId}{JSON_EXT}" - localFileName = f"group_{dgId}_chunk_{pageId}" + save_location = f"/dataset/{dgId}/chunks/{pageId}{JSON_EXT}" + local_file_name = f"group_{dgId}_chunk_{pageId}" - response = s3_ferry.transfer_file(f"{localFileName}{JSON_EXT}", "FS", saveLocation, "S3") + response = s3_ferry.transfer_file(f"{local_file_name}{JSON_EXT}", "FS", save_location, "S3") if response.status_code != 201: raise HTTPException(status_code=500, detail=S3_DOWNLOAD_FAILED) - jsonFilePath = os.path.join('..', 'shared', f"{localFileName}{JSON_EXT}") + json_file_path = os.path.join('..', 'shared', f"{local_file_name}{JSON_EXT}") - with open(f"{jsonFilePath}", 'r') as jsonFile: - jsonData = json.load(jsonFile) + with open(f"{json_file_path}", 'r') as json_file: + json_data = json.load(json_file) - for index, item in enumerate(jsonData, start=1): + for index, item in enumerate(json_data, start=1): item['rowID'] = index - backgroundTasks.add_task(os.remove, jsonFilePath) + backgroundTasks.add_task(os.remove, json_file_path) - return jsonData + return json_data From 0a5eb39dabdf03a06f24e30490f73698c1588b67 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Wed, 24 Jul 2024 18:10:20 +0530 Subject: [PATCH 269/582] ESCLASS-129-implemnt validation status API and implment code to execute preprocess cron-manger --- DSL/Resql/get-dataset-group-fields-by-id.sql | 2 +- .../update-dataset-group-validation-data.sql | 5 + .../classifier/datasetgroup/group/data.yml | 2 + .../classifier/datasetgroup/update/minor.yml | 6 +- .../classifier/datasetgroup/update/patch.yml | 6 +- .../datasetgroup/update/validation/status.yml | 100 ++++++++++++++++++ 6 files changed, 114 insertions(+), 7 deletions(-) create mode 100644 DSL/Resql/update-dataset-group-validation-data.sql create mode 100644 DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/validation/status.yml diff --git a/DSL/Resql/get-dataset-group-fields-by-id.sql b/DSL/Resql/get-dataset-group-fields-by-id.sql index 337679fb..86ed75d3 100644 --- a/DSL/Resql/get-dataset-group-fields-by-id.sql +++ b/DSL/Resql/get-dataset-group-fields-by-id.sql @@ -1,2 +1,2 @@ -SELECT validation_criteria +SELECT validation_criteria,num_pages FROM dataset_group_metadata WHERE id =:id; \ No newline at end of file diff --git a/DSL/Resql/update-dataset-group-validation-data.sql b/DSL/Resql/update-dataset-group-validation-data.sql new file mode 100644 index 00000000..3f99f601 --- /dev/null +++ b/DSL/Resql/update-dataset-group-validation-data.sql @@ -0,0 +1,5 @@ +UPDATE dataset_group_metadata +SET + validation_status = :validation_status::Validation_Status, + validation_errors = :validation_errors::jsonb +WHERE id = :id; diff --git a/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/group/data.yml b/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/group/data.yml index 5c77bb8a..f0097799 100644 --- a/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/group/data.yml +++ b/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/group/data.yml @@ -69,6 +69,7 @@ check_data_status: assign_fields_response: assign: val: ${res_dataset.response.body[0].validationCriteria === null ? [] :JSON.parse(res_dataset.response.body[0].validationCriteria.value)} + num_pages: ${res_dataset.response.body[0].numPages} next: assign_formated_response assign_formated_response: @@ -76,6 +77,7 @@ assign_formated_response: val: [{ dgId: '${group_id}', fields: '${val === [] ? [] :val.fields}', + numPages: '${num_pages}', dataPayload: '${res_data.response.body}' }] next: assign_success_response diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/minor.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/minor.yml index 44aeb221..ca856269 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/minor.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/minor.yml @@ -84,10 +84,10 @@ check_old_dataset_status: next: assign_fail_response execute_cron_manager: - call: reflect.mock + call: http.post args: - url: "[#CLASSIFIER_CRON_MANAGER]/execute//" - body: + url: "[#CLASSIFIER_CRON_MANAGER]/execute/dataset_processing/data_validation" + query: cookie: ${incoming.header.cookie} dgId: ${dg_id} updateType: 'minor' diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/patch.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/patch.yml index e8475fcc..7ff8c45b 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/patch.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/patch.yml @@ -70,10 +70,10 @@ check_old_dataset_status: next: assign_fail_response execute_cron_manager: - call: reflect.mock + call: http.post args: - url: "[#CLASSIFIER_CRON_MANAGER]/execute//" - body: + url: "[#CLASSIFIER_CRON_MANAGER]/execute/dataset_processing/data_validation" + query: cookie: ${incoming.header.cookie} dgId: ${dg_id} updateType: 'patch' diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/validation/status.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/validation/status.yml new file mode 100644 index 00000000..e382d072 --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/validation/status.yml @@ -0,0 +1,100 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'STATUS'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: dgId + type: string + description: "Body field 'dgId'" + - field: updateType + type: string + description: "Body field 'updateType'" + - field: patchPayload + type: json + description: "Body field 'patchPayload'" + - field: savedFilePath + type: string + description: "Body field 'savedFilePath'" + - field: validationStatus + type: string + description: "Body field 'validationStatus'" + - field: validationErrors + type: array + description: "Body field 'validationErrors'" + +extract_request_data: + assign: + dg_id: ${incoming.body.dgId} + update_type: ${incoming.body.updateType} + patch_payload: ${incoming.body.patchPayload} + save_file_path: ${incoming.body.savedFilePath} + validation_status: ${incoming.body.validationStatus} + validation_errors: ${incoming.body.validationErrors} + next: update_dataset_group_validation + +update_dataset_group_validation: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/update-dataset-group-validation-data" + body: + id: ${dg_id} + validation_status: ${validation_status} + validation_errors: ${JSON.stringify(validation_errors)} + result: res + next: check_status + +check_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: check_validation_status_type + next: assign_fail_response + +check_validation_status_type: + switch: + - condition: ${validation_status === 'success'} + next: execute_cron_manager + next: assign_success_response + +execute_cron_manager: + call: reflect.mock + args: + url: "[#CLASSIFIER_CRON_MANAGER]/execute/dataset_processing/dataset_processor" + query: + cookie: ${incoming.header.cookie} + dgId: ${dg_id} + updateType: ${update_type} + savedFilePath: ${save_file_path} + patchPayload: ${patchPayload} + result: res + next: assign_success_response + +assign_success_response: + assign: + format_res: { + dgId: '${dg_id}', + operationSuccessful: true, + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + dgId: '${dg_id}', + operationSuccessful: false, + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end From 1b44c110eb36c9d4dc3f6a8badaf185c51368ae4 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Wed, 24 Jul 2024 18:11:32 +0530 Subject: [PATCH 270/582] ESCLASS-129-mock executing initiate --- .../DSL/POST/classifier/datasetgroup/update/minor.yml | 2 +- .../DSL/POST/classifier/datasetgroup/update/patch.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/minor.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/minor.yml index ca856269..dfdc5103 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/minor.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/minor.yml @@ -84,7 +84,7 @@ check_old_dataset_status: next: assign_fail_response execute_cron_manager: - call: http.post + call: reflect.mock args: url: "[#CLASSIFIER_CRON_MANAGER]/execute/dataset_processing/data_validation" query: diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/patch.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/patch.yml index 7ff8c45b..cd26c82f 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/patch.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/patch.yml @@ -70,7 +70,7 @@ check_old_dataset_status: next: assign_fail_response execute_cron_manager: - call: http.post + call: reflect.mock args: url: "[#CLASSIFIER_CRON_MANAGER]/execute/dataset_processing/data_validation" query: From b9292d4c4f3d77d33f955f23a5a3e121d90b37ce Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Thu, 25 Jul 2024 12:31:46 +0530 Subject: [PATCH 271/582] fixing merge conflict --- docker-compose.yml | 64 ++++++++++++++++------------------------------ 1 file changed, 22 insertions(+), 42 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 7b9e6486..8dac67d1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ services: container_name: ruuter-public image: ruuter environment: - - application.cors.allowedOrigins=http://localhost:8086,http://localhost:3001,http://localhost:3003,http://localhost:3004,http://localhost:8080 + - application.cors.allowedOrigins=http://localhost:8086,http://localhost:3001,http://localhost:3003,http://localhost:3004,http://localhost:8080,http://localhost:8000 - application.httpCodesAllowList=200,201,202,400,401,403,500 - application.internalRequests.allowedIPs=127.0.0.1 - application.logging.displayRequestContent=true @@ -23,7 +23,7 @@ services: container_name: ruuter-private image: ruuter environment: - - application.cors.allowedOrigins=http://localhost:3001,http://localhost:8088,http://localhost:3002,http://localhost:3004 + - application.cors.allowedOrigins=http://localhost:3001,http://localhost:8088,http://localhost:3002,http://localhost:3004,http://localhost:8000 - application.httpCodesAllowList=200,201,202,204,400,401,403,404,500 - application.internalRequests.allowedIPs=127.0.0.1 - application.logging.displayRequestContent=true @@ -178,33 +178,15 @@ services: volumes: - shared-volume:/shared - s3-ferry: - image: s3-ferry:latest - container_name: s3-ferry - volumes: - - shared-volume:/shared - env_file: - - config.env - environment: - - S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID} - - S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY} - ports: - - "3002:3000" - depends_on: - - init - networks: - - bykstack - - file_handler: + file-handler: build: context: ./file-handler dockerfile: Dockerfile - container_name: file_handler + container_name: file-handler volumes: - shared-volume:/shared environment: - UPLOAD_DIRECTORY=/shared - - CHUNK_UPLOAD_DIRECTORY=/shared/chunks - RUUTER_PRIVATE_URL=http://ruuter-private:8088 - S3_FERRY_URL=http://s3-ferry:3000/v1/files/copy ports: @@ -213,26 +195,24 @@ services: - bykstack depends_on: - init - - s3-ferry - # dataset-processor: - # build: - # context: ./dataset-processor - # dockerfile: Dockerfile - # container_name: dataset-processor - # environment: - # - RUUTER_PRIVATE_URL=http://ruuter-private:8088 - # - FILE_HANDLER_DOWNLOAD_JSON_URL=http://file_handler:8000/datasetgroup/data/download/json - # - FILE_HANDLER_STOPWORDS_URL=http://file_handler:8000/datasetgroup/data/download/json/stopwords - # - FILE_HANDLER_IMPORT_CHUNKS_URL=http://file_handler:8000/datasetgroup/data/import/chunk - # ports: - # - "8001:8001" - # networks: - # - bykstack - # depends_on: - # - init - # - s3-ferry - # - file_handler + s3-ferry: + image: s3-ferry:latest + container_name: s3-ferry + volumes: + - shared-volume:/shared + env_file: + - config.env + environment: + - S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID} + - S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY} + ports: + - "3002:3000" + depends_on: + - file-handler + - init + networks: + - bykstack volumes: shared-volume: @@ -242,4 +222,4 @@ networks: name: bykstack driver: bridge driver_opts: - com.docker.network.driver.mtu: 1400 + com.docker.network.driver.mtu: 1400 \ No newline at end of file From b41ea7a71f1c0a580a5a8d2d995868070ffa0d95 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Thu, 25 Jul 2024 12:34:34 +0530 Subject: [PATCH 272/582] updates after git pull --- ...a_processor.yml => dataset_processing.yml} | 2 +- docker-compose.yml | 22 ++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) rename DSL/CronManager/DSL/{data_processor.yml => dataset_processing.yml} (82%) diff --git a/DSL/CronManager/DSL/data_processor.yml b/DSL/CronManager/DSL/dataset_processing.yml similarity index 82% rename from DSL/CronManager/DSL/data_processor.yml rename to DSL/CronManager/DSL/dataset_processing.yml index 53a93ace..bfc9a0ba 100644 --- a/DSL/CronManager/DSL/data_processor.yml +++ b/DSL/CronManager/DSL/dataset_processing.yml @@ -1,4 +1,4 @@ -init_data_processor: +dataset_processor: trigger: off type: exec command: "../app/scripts/data_processor_exec.sh" diff --git a/docker-compose.yml b/docker-compose.yml index 8dac67d1..ff52e309 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -222,4 +222,24 @@ networks: name: bykstack driver: bridge driver_opts: - com.docker.network.driver.mtu: 1400 \ No newline at end of file + com.docker.network.driver.mtu: 1400 + + + # dataset-processor: + # build: + # context: ./dataset-processor + # dockerfile: Dockerfile + # container_name: dataset-processor + # environment: + # - RUUTER_PRIVATE_URL=http://ruuter-private:8088 + # - FILE_HANDLER_DOWNLOAD_JSON_URL=http://file_handler:8000/datasetgroup/data/download/json + # - FILE_HANDLER_STOPWORDS_URL=http://file_handler:8000/datasetgroup/data/download/json/stopwords + # - FILE_HANDLER_IMPORT_CHUNKS_URL=http://file_handler:8000/datasetgroup/data/import/chunk + # ports: + # - "8001:8001" + # networks: + # - bykstack + # depends_on: + # - init + # - s3-ferry + # - file_handler \ No newline at end of file From e9950a2ffc395af25ce9b84702ec8a4ebe11b4ba Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Thu, 25 Jul 2024 20:58:03 +0530 Subject: [PATCH 273/582] minor flow completed --- dataset-processor/dataset_processor.py | 145 +++++++++++++++++++------ 1 file changed, 113 insertions(+), 32 deletions(-) diff --git a/dataset-processor/dataset_processor.py b/dataset-processor/dataset_processor.py index 40f5b1b2..202555c5 100644 --- a/dataset-processor/dataset_processor.py +++ b/dataset-processor/dataset_processor.py @@ -9,6 +9,9 @@ FILE_HANDLER_DOWNLOAD_JSON_URL = os.getenv("FILE_HANDLER_DOWNLOAD_JSON_URL") FILE_HANDLER_STOPWORDS_URL = os.getenv("FILE_HANDLER_STOPWORDS_URL") FILE_HANDLER_IMPORT_CHUNKS_URL = os.getenv("FILE_HANDLER_IMPORT_CHUNKS_URL") +FILE_HANDLER_DOWNLOAD_LOCATION_JSON_URL = os.getenv("FILE_HANDLER_DOWNLOAD_LOCATION_JSON_URL") +GET_PAGE_COUNT_URL = os.getenv("GET_PAGE_COUNT_URL") +SAVE_JSON_AGGREGRATED_DATA_URL = os.getenv("SAVE_JSON_AGGREGRATED_DATA_URL") class DatasetProcessor: def __init__(self): @@ -90,16 +93,17 @@ def chunk_data(self, data, chunk_size=5): print("Error while splitting data into chunks") return None - def save_chunked_data(self, chunked_data, authCookie, dgID): + def save_chunked_data(self, chunked_data, cookie, dgID, exsistingChunks=0): headers = { - 'cookie': f'customJwtCookie={authCookie}', + 'cookie': f'customJwtCookie={cookie}', 'Content-Type': 'application/json' } for chunk in chunked_data: payload = { "dg_id": dgID, - "chunks": chunk + "chunks": chunk, + "exsistingChunks": exsistingChunks } try: response = requests.post(FILE_HANDLER_IMPORT_CHUNKS_URL, json=payload, headers=headers) @@ -116,7 +120,7 @@ def get_selected_data_fields(self, dgID:int): validation_rules = data_dict.get("response", {}).get("validationCriteria", {}).get("validationRules", {}) text_fields = [] for field, rules in validation_rules.items(): - if rules.get("type") == "text": + if rules.get("type") == "text" and rules.get("isDataClass")!=True: text_fields.append(field) return text_fields except Exception as e: @@ -126,7 +130,7 @@ def get_selected_data_fields(self, dgID:int): def get_validation_data(self, dgID): try: params = {'dgId': dgID} - response = requests.get(RUUTER_URL, params=params) + response = requests.get(RUUTER_PRIVATE_URL, params=params) response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: @@ -147,6 +151,20 @@ def get_dataset(self, dg_id, custom_jwt_cookie): print(f"An error occurred: {e}") return None + def get_dataset_by_location(self, fileLocation, custom_jwt_cookie): + params = {'saveLocation': fileLocation} + headers = { + 'cookie': f'customJwtCookie={custom_jwt_cookie}' + } + + try: + response = requests.get(FILE_HANDLER_DOWNLOAD_LOCATION_JSON_URL, params=params, headers=headers) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + print(f"An error occurred: {e}") + return None + def get_stopwords(self, dg_id, custom_jwt_cookie): params = {'dgId': dg_id} headers = { @@ -161,37 +179,100 @@ def get_stopwords(self, dg_id, custom_jwt_cookie): print(f"An error occurred: {e}") return None - def process_handler(self, dgID, authCookie): - dataset = self.get_dataset(dgID, authCookie) - if dataset is not None: - structured_data = self.check_and_convert(dataset) - if structured_data is not None: - selected_data_fields_to_enrich = self.get_selected_data_fields(dgID) - if selected_data_fields_to_enrich is not None: - enriched_data = self.enrich_data(structured_data, selected_data_fields_to_enrich) - if enriched_data is not None: - stop_words = self.get_stopwords(dgID, authCookie) - if stop_words is not None: - cleaned_data = self.remove_stop_words(enriched_data, stop_words) - if cleaned_data is not None: - chunked_data = self.chunk_data(cleaned_data) - if chunked_data is not None: - operation_result = self.save_chunked_data(chunked_data, authCookie, dgID) - if operation_result: - return SUCCESSFUL_OPERATION + def get_page_count(self, dg_id, custom_jwt_cookie): + params = {'dgId': dg_id} + headers = { + 'cookie': f'customJwtCookie={custom_jwt_cookie}' + } + + try: + page_count_url = GET_PAGE_COUNT_URL.replace("{dgif}",str(dg_id)) + response = requests.get(page_count_url, headers=headers) + response.raise_for_status() + data = response.json() + page_count = data["numpages"] + return page_count + except requests.exceptions.RequestException as e: + print(f"An error occurred: {e}") + return None + + def save_aggregrated_data(self, dgID, cookie, aggregratedData): + headers = { + 'cookie': f'customJwtCookie={cookie}', + 'Content-Type': 'application/json' + } + + payload = { + "dgId": dgID, + "dataset": aggregratedData + } + try: + response = requests.post(SAVE_JSON_AGGREGRATED_DATA_URL, json=payload, headers=headers) + response.raise_for_status() + except requests.exceptions.RequestException as e: + print(f"An error occurred while uploading aggregrated dataset: {e}") + return None + + return True + + def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload): + if updateType == "Major": + dataset = self.get_dataset(dgID, cookie) + if 4 is not None: + structured_data = self.check_and_convert(dataset) + if structured_data is not None: + selected_data_fields_to_enrich = self.get_selected_data_fields(dgID) + if selected_data_fields_to_enrich is not None: + enriched_data = self.enrich_data(structured_data, selected_data_fields_to_enrich) + if enriched_data is not None: + stop_words = self.get_stopwords(dgID, cookie) + if stop_words is not None: + cleaned_data = self.remove_stop_words(enriched_data, stop_words) + if cleaned_data is not None: + chunked_data = self.chunk_data(cleaned_data) + if chunked_data is not None: + operation_result = self.save_chunked_data(chunked_data, cookie, dgID, 0) + if operation_result: + return SUCCESSFUL_OPERATION + else: + return FAILED_TO_SAVE_CHUNKED_DATA else: - return FAILED_TO_SAVE_CHUNKED_DATA + return FAILED_TO_CHUNK_CLEANED_DATA else: - return FAILED_TO_CHUNK_CLEANED_DATA + return FAILED_TO_REMOVE_STOP_WORDS else: - return FAILED_TO_REMOVE_STOP_WORDS + return FAILED_TO_GET_STOP_WORDS else: - return FAILED_TO_GET_STOP_WORDS + return FAILED_TO_ENRICH_DATA else: - return FAILED_TO_ENRICH_DATA + return FAILED_TO_GET_SELECTED_FIELDS else: - return FAILED_TO_GET_SELECTED_FIELDS + return FAILED_TO_CHECK_AND_CONVERT else: - return FAILED_TO_CHECK_AND_CONVERT - else: - return FAILED_TO_GET_DATASET + return FAILED_TO_GET_DATASET + elif updateType == "Minor": + agregated_dataset = self.get_dataset(dgID, cookie) + minor_update_dataset = self.get_dataset_by_location(savedFilePath, cookie) + if minor_update_dataset is not None: + structured_data = self.check_and_convert(minor_update_dataset) + if structured_data is not None: + selected_data_fields_to_enrich = self.get_selected_data_fields(dgID) + if selected_data_fields_to_enrich is not None: + enriched_data = self.enrich_data(structured_data, selected_data_fields_to_enrich) + if enriched_data is not None: + stop_words = self.get_stopwords(dgID, cookie) + if stop_words is not None: + cleaned_data = self.remove_stop_words(enriched_data, stop_words) + if cleaned_data is not None: + chunked_data = self.chunk_data(cleaned_data) + if chunked_data is not None: + page_count = self.get_page_count(dgID) + operation_result = self.save_chunked_data(chunked_data, cookie, dgID, page_count) + if operation_result is not None: + agregated_dataset = agregated_dataset + cleaned_data + agregated_dataset_operation = self.save_aggregrated_data(dgID, cookie, agregated_dataset) + if agregated_dataset_operation: + return SUCCESSFUL_OPERATION + + + From d6d84720cea6a33b402a3c9dfa0064963cb72d7b Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Thu, 25 Jul 2024 21:15:53 +0530 Subject: [PATCH 274/582] else conditions for minor --- dataset-processor/constants.py | 24 +++++++++ dataset-processor/dataset_processor.py | 70 +++++++++++++++++--------- 2 files changed, 70 insertions(+), 24 deletions(-) diff --git a/dataset-processor/constants.py b/dataset-processor/constants.py index 9dda4910..cfd97a9a 100644 --- a/dataset-processor/constants.py +++ b/dataset-processor/constants.py @@ -51,3 +51,27 @@ "operation_successful": False, "reason": "Failed to get dataset" } + +FAILED_TO_GET_MINOR_UPDATE_DATASET = { + "operation_status": 500, + "operation_successful": False, + "reason": "Failed to get minor update dataset" +} + +FAILED_TO_GET_AGGREGATED_DATASET = { + "operation_status": 500, + "operation_successful": False, + "reason": "Failed to get aggregated dataset" +} + +FAILED_TO_GET_PAGE_COUNT = { + "operation_status": 500, + "operation_successful": False, + "reason": "Failed to get page count" +} + +FAILED_TO_SAVE_AGGREGATED_DATA = { + "operation_status": 500, + "operation_successful": False, + "reason": "Failed to save aggregated dataset" +} diff --git a/dataset-processor/dataset_processor.py b/dataset-processor/dataset_processor.py index 202555c5..2f7bad6e 100644 --- a/dataset-processor/dataset_processor.py +++ b/dataset-processor/dataset_processor.py @@ -218,7 +218,7 @@ def save_aggregrated_data(self, dgID, cookie, aggregratedData): def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload): if updateType == "Major": dataset = self.get_dataset(dgID, cookie) - if 4 is not None: + if dataset is not None: structured_data = self.check_and_convert(dataset) if structured_data is not None: selected_data_fields_to_enrich = self.get_selected_data_fields(dgID) @@ -252,27 +252,49 @@ def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload) return FAILED_TO_GET_DATASET elif updateType == "Minor": agregated_dataset = self.get_dataset(dgID, cookie) - minor_update_dataset = self.get_dataset_by_location(savedFilePath, cookie) - if minor_update_dataset is not None: - structured_data = self.check_and_convert(minor_update_dataset) - if structured_data is not None: - selected_data_fields_to_enrich = self.get_selected_data_fields(dgID) - if selected_data_fields_to_enrich is not None: - enriched_data = self.enrich_data(structured_data, selected_data_fields_to_enrich) - if enriched_data is not None: - stop_words = self.get_stopwords(dgID, cookie) - if stop_words is not None: - cleaned_data = self.remove_stop_words(enriched_data, stop_words) - if cleaned_data is not None: - chunked_data = self.chunk_data(cleaned_data) - if chunked_data is not None: - page_count = self.get_page_count(dgID) - operation_result = self.save_chunked_data(chunked_data, cookie, dgID, page_count) - if operation_result is not None: - agregated_dataset = agregated_dataset + cleaned_data - agregated_dataset_operation = self.save_aggregrated_data(dgID, cookie, agregated_dataset) - if agregated_dataset_operation: - return SUCCESSFUL_OPERATION - - + if agregated_dataset is not None: + minor_update_dataset = self.get_dataset_by_location(savedFilePath, cookie) + if minor_update_dataset is not None: + structured_data = self.check_and_convert(minor_update_dataset) + if structured_data is not None: + selected_data_fields_to_enrich = self.get_selected_data_fields(dgID) + if selected_data_fields_to_enrich is not None: + enriched_data = self.enrich_data(structured_data, selected_data_fields_to_enrich) + if enriched_data is not None: + stop_words = self.get_stopwords(dgID, cookie) + if stop_words is not None: + cleaned_data = self.remove_stop_words(enriched_data, stop_words) + if cleaned_data is not None: + chunked_data = self.chunk_data(cleaned_data) + if chunked_data is not None: + page_count = self.get_page_count(dgID, cookie) + if page_count is not None: + operation_result = self.save_chunked_data(chunked_data, cookie, dgID, page_count) + if operation_result is not None: + agregated_dataset += cleaned_data + agregated_dataset_operation = self.save_aggregrated_data(dgID, cookie, agregated_dataset) + if agregated_dataset_operation: + return SUCCESSFUL_OPERATION + else: + return FAILED_TO_SAVE_AGGREGATED_DATA + else: + return FAILED_TO_SAVE_CHUNKED_DATA + else: + return FAILED_TO_GET_PAGE_COUNT + else: + return FAILED_TO_CHUNK_CLEANED_DATA + else: + return FAILED_TO_REMOVE_STOP_WORDS + else: + return FAILED_TO_GET_STOP_WORDS + else: + return FAILED_TO_ENRICH_DATA + else: + return FAILED_TO_GET_SELECTED_FIELDS + else: + return FAILED_TO_CHECK_AND_CONVERT + else: + return FAILED_TO_GET_MINOR_UPDATE_DATASET + else: + return FAILED_TO_GET_AGGREGATED_DATASET From 80f4723569ef9f82ea5ae1a8c7612b093e859d33 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Thu, 25 Jul 2024 21:57:46 +0530 Subject: [PATCH 275/582] docker compose update --- docker-compose.yml | 45 ++++++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index ff52e309..ecec65bc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -214,6 +214,29 @@ services: networks: - bykstack + dataset-processor: + build: + context: ./dataset-processor + dockerfile: Dockerfile + container_name: dataset-processor + environment: + - RUUTER_PRIVATE_URL=http://ruuter-private:8088 + - FILE_HANDLER_DOWNLOAD_JSON_URL=http://file_handler:8000/datasetgroup/data/download/json + - FILE_HANDLER_DOWNLOAD_LOCATION_JSON_URL=http://file_handler:8000/datasetgroup/data/download/json/location + - FILE_HANDLER_STOPWORDS_URL=http://file_handler:8000/datasetgroup/data/download/json/stopwords + - FILE_HANDLER_IMPORT_CHUNKS_URL=http://file_handler:8000/datasetgroup/data/import/chunk + - GET_PAGE_COUNT_URL=http://ruuter-private:8088/datasetgroup/group/data?groupd_id={dgid}&page_num=1 + - SAVE_JSON_AGGREGRATED_DATA_URL=http://file_handler:8000/datasetgroup/data/import/json + - DOWNLOAD_CHUNK=http://file_handler:8000/datasetgroup/data/download/chunk + ports: + - "8001:8001" + networks: + - bykstack + depends_on: + - init + - s3-ferry + - file_handler + volumes: shared-volume: @@ -222,24 +245,4 @@ networks: name: bykstack driver: bridge driver_opts: - com.docker.network.driver.mtu: 1400 - - - # dataset-processor: - # build: - # context: ./dataset-processor - # dockerfile: Dockerfile - # container_name: dataset-processor - # environment: - # - RUUTER_PRIVATE_URL=http://ruuter-private:8088 - # - FILE_HANDLER_DOWNLOAD_JSON_URL=http://file_handler:8000/datasetgroup/data/download/json - # - FILE_HANDLER_STOPWORDS_URL=http://file_handler:8000/datasetgroup/data/download/json/stopwords - # - FILE_HANDLER_IMPORT_CHUNKS_URL=http://file_handler:8000/datasetgroup/data/import/chunk - # ports: - # - "8001:8001" - # networks: - # - bykstack - # depends_on: - # - init - # - s3-ferry - # - file_handler \ No newline at end of file + com.docker.network.driver.mtu: 1400 \ No newline at end of file From 4c3a6feb98e945f50ed8c7c2ad091ec11a1c3da6 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Thu, 25 Jul 2024 21:57:58 +0530 Subject: [PATCH 276/582] constants.py update --- dataset-processor/constants.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dataset-processor/constants.py b/dataset-processor/constants.py index cfd97a9a..53b17f56 100644 --- a/dataset-processor/constants.py +++ b/dataset-processor/constants.py @@ -75,3 +75,9 @@ "operation_successful": False, "reason": "Failed to save aggregated dataset" } + +FAILED_TO_DOWNLOAD_CHUNK = { + "operation_status": 500, + "operation_successful": False, + "reason": "Failed to download chunk" +} From 7db46697ac473b914a4d9eabcbd53536b42d7a5b Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Thu, 25 Jul 2024 21:58:13 +0530 Subject: [PATCH 277/582] dataset processor update --- dataset-processor/dataset_processor.py | 65 ++++++++++++++++++++++ dataset-processor/dataset_processor_api.py | 15 +++-- 2 files changed, 74 insertions(+), 6 deletions(-) diff --git a/dataset-processor/dataset_processor.py b/dataset-processor/dataset_processor.py index 2f7bad6e..1fda22a1 100644 --- a/dataset-processor/dataset_processor.py +++ b/dataset-processor/dataset_processor.py @@ -12,6 +12,7 @@ FILE_HANDLER_DOWNLOAD_LOCATION_JSON_URL = os.getenv("FILE_HANDLER_DOWNLOAD_LOCATION_JSON_URL") GET_PAGE_COUNT_URL = os.getenv("GET_PAGE_COUNT_URL") SAVE_JSON_AGGREGRATED_DATA_URL = os.getenv("SAVE_JSON_AGGREGRATED_DATA_URL") +DOWNLOAD_CHUNK_URL = os.getenv("DOWNLOAD_CHUNK_URL") class DatasetProcessor: def __init__(self): @@ -214,6 +215,20 @@ def save_aggregrated_data(self, dgID, cookie, aggregratedData): return None return True + + def download_chunk(self, dgID, cookie, pageId): + params = {'dgId': dgID, 'pageId': pageId} + headers = { + 'cookie': f'customJwtCookie={cookie}' + } + + try: + response = requests.get(DOWNLOAD_CHUNK_URL, params=params, headers=headers) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + print(f"An error occurred while downloading chunk: {e}") + return None def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload): if updateType == "Major": @@ -297,4 +312,54 @@ def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload) return FAILED_TO_GET_MINOR_UPDATE_DATASET else: return FAILED_TO_GET_AGGREGATED_DATASET + elif updateType == "Patch": + stop_words = self.get_stopwords(dgID, cookie) + if stop_words is not None: + cleaned_patch_payload = self.remove_stop_words(patchPayload, stop_words) + if cleaned_patch_payload is not None: + page_count = self.get_page_count(dgID, cookie) + if page_count is not None: + chunk_updates = {} + for entry in cleaned_patch_payload: + rowID = entry.get("rowID") + chunkNum = (rowID - 1) // 5 + 1 + if chunkNum not in chunk_updates: + chunk_updates[chunkNum] = [] + chunk_updates[chunkNum].append(entry) + for chunkNum, entries in chunk_updates.items(): + chunk_data = self.download_chunk(dgID, cookie, chunkNum) + if chunk_data is not None: + for entry in entries: + rowID = entry.get("rowID") + for idx, chunk_entry in enumerate(chunk_data): + if chunk_entry.get("rowID") == rowID: + chunk_data[idx] = entry + break + + chunk_save_operation = self.save_chunked_data([chunk_data], cookie, dgID, chunkNum - 1) + if chunk_save_operation == None: + return FAILED_TO_SAVE_CHUNKED_DATA + else: + return FAILED_TO_DOWNLOAD_CHUNK + agregated_dataset = self.get_dataset(dgID, cookie) + if agregated_dataset is not None: + for entry in cleaned_patch_payload: + rowID = entry.get("rowID") + for index, item in enumerate(agregated_dataset): + if item.get("rowID") == rowID: + agregated_dataset[index] = entry + break + save_result = self.save_aggregrated_data(dgID, cookie, agregated_dataset) + if save_result: + return SUCCESSFUL_OPERATION + else: + return FAILED_TO_SAVE_AGGREGATED_DATA + else: + return FAILED_TO_GET_AGGREGATED_DATASET + else: + return FAILED_TO_GET_PAGE_COUNT + else: + return FAILED_TO_REMOVE_STOP_WORDS + else: + return FAILED_TO_GET_STOP_WORDS diff --git a/dataset-processor/dataset_processor_api.py b/dataset-processor/dataset_processor_api.py index 5688b1e3..174b5608 100644 --- a/dataset-processor/dataset_processor_api.py +++ b/dataset-processor/dataset_processor_api.py @@ -1,6 +1,6 @@ from fastapi import FastAPI, HTTPException, Request from pydantic import BaseModel -from main import DatasetProcessor +from dataset_processor import DatasetProcessor import requests import os @@ -10,7 +10,10 @@ class ProcessHandlerRequest(BaseModel): dgID: int - authCookie: str + cookie: str + updateType: str + savedFilePath: str + patchPayload: list async def authenticate_user(request: Request): cookie = request.cookies.get("customJwtCookie") @@ -27,10 +30,10 @@ async def authenticate_user(request: Request): raise HTTPException(status_code=response.status_code, detail="Authentication failed") @app.post("/init-dataset-process") -async def process_handler_endpoint(request: Request, process_request: ProcessHandlerRequest): - await authenticate_user(request) - authCookie = request.cookies.get("customJwtCookie") - result = processor.process_handler(process_request.dgID, authCookie) +async def process_handler_endpoint(process_request: ProcessHandlerRequest): + # await authenticate_user(request) + # authCookie = request.cookies.get("customJwtCookie") + result = processor.process_handler(process_request.dgID, process_request.cookie, process_request.updateType, process_request.savedFilePath, process_request.patchPayload) if result: return result else: From 54d6b98fe317aefe2e845afb74110649fe026e96 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Thu, 25 Jul 2024 21:59:43 +0530 Subject: [PATCH 278/582] new req file --- .../data_enrichment/requirements.txt | 82 ++----------------- 1 file changed, 5 insertions(+), 77 deletions(-) diff --git a/dataset-processor/data_enrichment/requirements.txt b/dataset-processor/data_enrichment/requirements.txt index 95087b4b..ad22e047 100644 --- a/dataset-processor/data_enrichment/requirements.txt +++ b/dataset-processor/data_enrichment/requirements.txt @@ -1,77 +1,5 @@ -accelerate==0.31.0 -annotated-types==0.7.0 -anyio==4.4.0 -certifi==2024.6.2 -charset-normalizer==3.3.2 -click==8.1.7 -colorama==0.4.6 -dnspython==2.6.1 -email_validator==2.2.0 -et-xmlfile==1.1.0 -exceptiongroup==1.2.1 -fastapi==0.111.0 -fastapi-cli==0.0.4 -filelock==3.13.1 -fsspec==2024.2.0 -h11==0.14.0 -httpcore==1.0.5 -httptools==0.6.1 -httpx==0.27.0 -huggingface-hub==0.23.3 -idna==3.7 -install==1.3.5 -intel-openmp==2021.4.0 -Jinja2==3.1.3 -joblib==1.4.2 -langdetect==1.0.9 -markdown-it-py==3.0.0 -MarkupSafe==2.1.5 -mdurl==0.1.2 -mkl==2021.4.0 -mpmath==1.3.0 -networkx==3.2.1 -numpy==1.26.3 -openpyxl==3.1.3 -orjson==3.10.5 -packaging==24.1 -pandas==2.2.2 -pillow==10.2.0 -protobuf==5.27.1 -psutil==5.9.8 -pydantic==2.7.4 -pydantic_core==2.18.4 -Pygments==2.18.0 -python-dateutil==2.9.0.post0 -python-dotenv==1.0.1 -python-multipart==0.0.9 -pytz==2024.1 -PyYAML==6.0.1 -regex==2024.5.15 -requests==2.32.3 -rich==13.7.1 -sacremoses==0.1.1 -safetensors==0.4.3 -scikit-learn==1.5.0 -scipy==1.13.1 -sentencepiece==0.2.0 -shellingham==1.5.4 -six==1.16.0 -sniffio==1.3.1 -starlette==0.37.2 -sympy==1.12 -tbb==2021.11.0 -threadpoolctl==3.5.0 -tokenizers==0.19.1 -torch==2.3.1+cu121 -torchaudio==2.3.1+cu121 -torchvision==0.18.1+cu121 -tqdm==4.66.4 -transformers==4.41.2 -typer==0.12.3 -typing_extensions==4.9.0 -tzdata==2024.1 -ujson==5.10.0 -urllib3==2.2.1 -uvicorn==0.30.1 -watchfiles==0.22.0 -websockets==12.0 +fastapi +pydantic +requests +langdetect +transformers \ No newline at end of file From a7f7f1e843cdffbfd0b432575bf329a518aeade1 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Thu, 25 Jul 2024 21:59:56 +0530 Subject: [PATCH 279/582] cron job update --- DSL/CronManager/DSL/dataset_processing.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DSL/CronManager/DSL/dataset_processing.yml b/DSL/CronManager/DSL/dataset_processing.yml index bfc9a0ba..04e438bf 100644 --- a/DSL/CronManager/DSL/dataset_processing.yml +++ b/DSL/CronManager/DSL/dataset_processing.yml @@ -2,4 +2,4 @@ dataset_processor: trigger: off type: exec command: "../app/scripts/data_processor_exec.sh" - allowedEnvs: ["dgId"] \ No newline at end of file + allowedEnvs: ["dgId","cookie","updateType","savedFilePath","patchPayload"] \ No newline at end of file From ce03b9969c0fdb6ca17b6be128271c6542780e03 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Thu, 25 Jul 2024 22:00:07 +0530 Subject: [PATCH 280/582] file handler update --- file-handler/file_handler_api.py | 51 ++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/file-handler/file_handler_api.py b/file-handler/file_handler_api.py index 81796552..e3e59797 100644 --- a/file-handler/file_handler_api.py +++ b/file-handler/file_handler_api.py @@ -28,6 +28,11 @@ class ExportFile(BaseModel): class ImportChunks(BaseModel): dg_id: int chunks: list + exsistingChunks: int + +class ImportJsonMajor(BaseModel): + dgId: int + dataset: dict if not os.path.exists(UPLOAD_DIRECTORY): os.makedirs(UPLOAD_DIRECTORY) @@ -151,19 +156,39 @@ async def download_and_convert(request: Request, dgId: int, background_tasks: Ba return jsonData +@app.get("/datasetgroup/data/download/json/location") +async def download_and_convert(request: Request, saveLocation:str, background_tasks: BackgroundTasks): + await authenticate_user(request) + + localFileName = saveLocation.split("/")["-1"] + + response = s3_ferry.transfer_file(f"{localFileName}", "FS", saveLocation, "S3") + if response.status_code != 201: + raise HTTPException(status_code=500, detail=S3_DOWNLOAD_FAILED) + + jsonFilePath = os.path.join('..', 'shared', f"{localFileName}") + + with open(f"{jsonFilePath}", 'r') as jsonFile: + jsonData = json.load(jsonFile) + + background_tasks.add_task(os.remove, jsonFilePath) + + return jsonData + @app.post("/datasetgroup/data/import/chunk") async def upload_and_copy(request: Request, import_chunks: ImportChunks): await authenticate_user(request) dgID = import_chunks.dg_id chunks = import_chunks.chunks + exsisting_chunks = import_chunks.exsistingChunks for index, chunk in enumerate(chunks, start=1): - fileLocation = os.path.join(CHUNK_UPLOAD_DIRECTORY, f"{index}.json") + fileLocation = os.path.join(CHUNK_UPLOAD_DIRECTORY, f"{exsisting_chunks+index}.json") with open(fileLocation, 'w') as jsonFile: json.dump(chunk, jsonFile, indent=4) - saveLocation = f"/dataset/{dgID}/chunks/{index}{JSON_EXT}" + saveLocation = f"/dataset/{dgID}/chunks/{exsisting_chunks+index}{JSON_EXT}" response = s3_ferry.transfer_file(saveLocation, "S3", fileLocation, "FS") if response.status_code == 201: @@ -194,3 +219,25 @@ async def download_and_convert(request: Request, dgId: int, pageId: int, backgro background_tasks.add_task(os.remove, jsonFilePath) return jsonData + +@app.post("/datasetgroup/data/import/json") +async def upload_and_copy(request: Request, importData: ImportJsonMajor): + await authenticate_user(request) + + fileName = f"{uuid.uuid4()}.{JSON_EXT}" + fileLocation = os.path.join(UPLOAD_DIRECTORY, fileName) + + with open(fileLocation, 'w') as jsonFile: + json.dump(importData.dataset, jsonFile, indent=4) + + saveLocation = f"/dataset/{importData.dgId}/primary_dataset/dataset_{importData.dgId}_aggregated{JSON_EXT}" + sourceFilePath = fileName.replace(YML_EXT, JSON_EXT).replace(XLSX_EXT, JSON_EXT) + + response = s3_ferry.transfer_file(saveLocation, "S3", sourceFilePath, "FS") + if response.status_code == 201: + os.remove(fileLocation) + upload_success = UPLOAD_SUCCESS.copy() + upload_success["saved_file_path"] = saveLocation + return JSONResponse(status_code=200, content=upload_success) + else: + raise HTTPException(status_code=500, detail=S3_UPLOAD_FAILED) \ No newline at end of file From 89f9ae97885897fc5ad9d305d67bb7bfdee5b693 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Thu, 25 Jul 2024 22:03:24 +0530 Subject: [PATCH 281/582] gitignore update --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 3525077b..e47aea08 100644 --- a/.gitignore +++ b/.gitignore @@ -398,4 +398,5 @@ FodyWeavers.xsd *.sln.iml /tim-db -/data \ No newline at end of file +/data +/DSL/Liquibase/ \ No newline at end of file From a91ad0fc82631ae5a534507eb1f9e14acaf51a3d Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Thu, 25 Jul 2024 22:06:26 +0530 Subject: [PATCH 282/582] json_data fix --- file-handler/file_handler_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/file-handler/file_handler_api.py b/file-handler/file_handler_api.py index 0a258144..083d4a69 100644 --- a/file-handler/file_handler_api.py +++ b/file-handler/file_handler_api.py @@ -226,7 +226,7 @@ async def download_and_convert(request: Request, dgId: int, pageId: int, backgro backgroundTasks.add_task(os.remove, json_file_path) - return jsonData + return json_data @app.post("/datasetgroup/data/import/json") async def upload_and_copy(request: Request, importData: ImportJsonMajor): From 324f909aa6e01c86a9ae1c0edf71d14d5344123b Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Thu, 25 Jul 2024 22:45:56 +0530 Subject: [PATCH 283/582] file handler container update --- docker-compose.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index ecec65bc..c47499b1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -221,13 +221,13 @@ services: container_name: dataset-processor environment: - RUUTER_PRIVATE_URL=http://ruuter-private:8088 - - FILE_HANDLER_DOWNLOAD_JSON_URL=http://file_handler:8000/datasetgroup/data/download/json - - FILE_HANDLER_DOWNLOAD_LOCATION_JSON_URL=http://file_handler:8000/datasetgroup/data/download/json/location - - FILE_HANDLER_STOPWORDS_URL=http://file_handler:8000/datasetgroup/data/download/json/stopwords - - FILE_HANDLER_IMPORT_CHUNKS_URL=http://file_handler:8000/datasetgroup/data/import/chunk + - FILE_HANDLER_DOWNLOAD_JSON_URL=http://file-handler:8000/datasetgroup/data/download/json + - FILE_HANDLER_DOWNLOAD_LOCATION_JSON_URL=http://file-handler:8000/datasetgroup/data/download/json/location + - FILE_HANDLER_STOPWORDS_URL=http://file-handler:8000/datasetgroup/data/download/json/stopwords + - FILE_HANDLER_IMPORT_CHUNKS_URL=http://file-handler:8000/datasetgroup/data/import/chunk - GET_PAGE_COUNT_URL=http://ruuter-private:8088/datasetgroup/group/data?groupd_id={dgid}&page_num=1 - - SAVE_JSON_AGGREGRATED_DATA_URL=http://file_handler:8000/datasetgroup/data/import/json - - DOWNLOAD_CHUNK=http://file_handler:8000/datasetgroup/data/download/chunk + - SAVE_JSON_AGGREGRATED_DATA_URL=http://file-handler:8000/datasetgroup/data/import/json + - DOWNLOAD_CHUNK=http://file-handler:8000/datasetgroup/data/download/chunk ports: - "8001:8001" networks: @@ -235,7 +235,7 @@ services: depends_on: - init - s3-ferry - - file_handler + - file-handler volumes: shared-volume: From 09f473752597ebc1d93fccdf8179745eb77155b5 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Thu, 25 Jul 2024 22:46:04 +0530 Subject: [PATCH 284/582] new requirenments --- dataset-processor/requirements.txt | 82 ++---------------------------- 1 file changed, 5 insertions(+), 77 deletions(-) diff --git a/dataset-processor/requirements.txt b/dataset-processor/requirements.txt index 95087b4b..ad22e047 100644 --- a/dataset-processor/requirements.txt +++ b/dataset-processor/requirements.txt @@ -1,77 +1,5 @@ -accelerate==0.31.0 -annotated-types==0.7.0 -anyio==4.4.0 -certifi==2024.6.2 -charset-normalizer==3.3.2 -click==8.1.7 -colorama==0.4.6 -dnspython==2.6.1 -email_validator==2.2.0 -et-xmlfile==1.1.0 -exceptiongroup==1.2.1 -fastapi==0.111.0 -fastapi-cli==0.0.4 -filelock==3.13.1 -fsspec==2024.2.0 -h11==0.14.0 -httpcore==1.0.5 -httptools==0.6.1 -httpx==0.27.0 -huggingface-hub==0.23.3 -idna==3.7 -install==1.3.5 -intel-openmp==2021.4.0 -Jinja2==3.1.3 -joblib==1.4.2 -langdetect==1.0.9 -markdown-it-py==3.0.0 -MarkupSafe==2.1.5 -mdurl==0.1.2 -mkl==2021.4.0 -mpmath==1.3.0 -networkx==3.2.1 -numpy==1.26.3 -openpyxl==3.1.3 -orjson==3.10.5 -packaging==24.1 -pandas==2.2.2 -pillow==10.2.0 -protobuf==5.27.1 -psutil==5.9.8 -pydantic==2.7.4 -pydantic_core==2.18.4 -Pygments==2.18.0 -python-dateutil==2.9.0.post0 -python-dotenv==1.0.1 -python-multipart==0.0.9 -pytz==2024.1 -PyYAML==6.0.1 -regex==2024.5.15 -requests==2.32.3 -rich==13.7.1 -sacremoses==0.1.1 -safetensors==0.4.3 -scikit-learn==1.5.0 -scipy==1.13.1 -sentencepiece==0.2.0 -shellingham==1.5.4 -six==1.16.0 -sniffio==1.3.1 -starlette==0.37.2 -sympy==1.12 -tbb==2021.11.0 -threadpoolctl==3.5.0 -tokenizers==0.19.1 -torch==2.3.1+cu121 -torchaudio==2.3.1+cu121 -torchvision==0.18.1+cu121 -tqdm==4.66.4 -transformers==4.41.2 -typer==0.12.3 -typing_extensions==4.9.0 -tzdata==2024.1 -ujson==5.10.0 -urllib3==2.2.1 -uvicorn==0.30.1 -watchfiles==0.22.0 -websockets==12.0 +fastapi +pydantic +requests +langdetect +transformers \ No newline at end of file From 1377f1783df24b7af8de4e3d3ddb589a583a2d34 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Thu, 25 Jul 2024 22:46:24 +0530 Subject: [PATCH 285/582] ignoring multiple sheets --- file-handler/file_converter.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/file-handler/file_converter.py b/file-handler/file_converter.py index 9a057429..69059168 100644 --- a/file-handler/file_converter.py +++ b/file-handler/file_converter.py @@ -50,14 +50,25 @@ def _convert_yaml_to_json(self, filePath): print(f"Error converting YAML file '{filePath}' to JSON: {e}") return (False, {}) + # def _convert_xlsx_to_json(self, filePath): + # try: + # data = pd.read_excel(filePath, sheet_name=None) + # jsonData = {sheet: data[sheet].to_dict(orient='records') for sheet in data} + # return (True, jsonData) + # except Exception as e: + # print(f"Error converting XLSX file '{filePath}' to JSON: {e}") + # return (False, {}) + def _convert_xlsx_to_json(self, filePath): try: data = pd.read_excel(filePath, sheet_name=None) - jsonData = {sheet: data[sheet].to_dict(orient='records') for sheet in data} - return (True, jsonData) + combined_data = [] + for sheet in data: + combined_data.extend(data[sheet].to_dict(orient='records')) + return (True, combined_data) except Exception as e: print(f"Error converting XLSX file '{filePath}' to JSON: {e}") - return (False, {}) + return (False, []) def convert_json_to_xlsx(self, jsonData, outputPath): try: From 69a3cbb4b57fa5462d2ddcfdfaaaa850a2b9b4dc Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Thu, 25 Jul 2024 23:41:24 +0530 Subject: [PATCH 286/582] update to change computer --- dataset-processor/dataset_processor.py | 266 +++++++++++++++---------- dataset-processor/requirements.txt | 6 +- file-handler/file_handler_api.py | 9 +- 3 files changed, 174 insertions(+), 107 deletions(-) diff --git a/dataset-processor/dataset_processor.py b/dataset-processor/dataset_processor.py index 1fda22a1..e70724be 100644 --- a/dataset-processor/dataset_processor.py +++ b/dataset-processor/dataset_processor.py @@ -2,7 +2,7 @@ import os import json import requests -from data_enrichment.data_enrichment import DataEnrichment +# from data_enrichment.data_enrichment import DataEnrichment from constants import * RUUTER_PRIVATE_URL = os.getenv("RUUTER_PRIVATE_URL") @@ -16,7 +16,8 @@ class DatasetProcessor: def __init__(self): - self.data_enricher = DataEnrichment() + pass + # self.data_enricher = DataEnrichment() def check_and_convert(self, data): if self._is_multple_sheet_structure(data): @@ -77,7 +78,8 @@ def enrich_data(self, data, selected_fields): enriched_entry = {} for key, value in entry.items(): if isinstance(value, str) and (key in selected_fields): - enriched_value = self.data_enricher.enrich_data(value, num_return_sequences=1, language_id='en') + # enriched_value = self.data_enricher.enrich_data(value, num_return_sequences=1, language_id='en') + enriched_value = "enrichupdate" enriched_entry[key] = enriched_value[0] if enriched_value else value else: enriched_entry[key] = value @@ -230,136 +232,194 @@ def download_chunk(self, dgID, cookie, pageId): print(f"An error occurred while downloading chunk: {e}") return None - def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload): - if updateType == "Major": - dataset = self.get_dataset(dgID, cookie) - if dataset is not None: - structured_data = self.check_and_convert(dataset) +def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload): + print(f"Process handler started with updateType: {updateType}") + + if updateType == "Major": + print("Handling Major update") + dataset = self.get_dataset(dgID, cookie) + if dataset is not None: + print("Dataset retrieved successfully") + structured_data = self.check_and_convert(dataset) + if structured_data is not None: + print("Dataset converted successfully") + selected_data_fields_to_enrich = self.get_selected_data_fields(dgID) + if selected_data_fields_to_enrich is not None: + print("Selected data fields to enrich retrieved successfully") + enriched_data = self.enrich_data(structured_data, selected_data_fields_to_enrich) + if enriched_data is not None: + print("Data enrichment successful") + stop_words = self.get_stopwords(dgID, cookie) + if stop_words is not None: + print("Stop words retrieved successfully") + cleaned_data = self.remove_stop_words(enriched_data, stop_words) + if cleaned_data is not None: + print("Stop words removed successfully") + chunked_data = self.chunk_data(cleaned_data) + if chunked_data is not None: + print("Data chunking successful") + operation_result = self.save_chunked_data(chunked_data, cookie, dgID, 0) + if operation_result: + print("Chunked data saved successfully") + return SUCCESSFUL_OPERATION + else: + print("Failed to save chunked data") + return FAILED_TO_SAVE_CHUNKED_DATA + else: + print("Failed to chunk cleaned data") + return FAILED_TO_CHUNK_CLEANED_DATA + else: + print("Failed to remove stop words") + return FAILED_TO_REMOVE_STOP_WORDS + else: + print("Failed to retrieve stop words") + return FAILED_TO_GET_STOP_WORDS + else: + print("Failed to enrich data") + return FAILED_TO_ENRICH_DATA + else: + print("Failed to get selected data fields to enrich") + return FAILED_TO_GET_SELECTED_FIELDS + else: + print("Failed to convert dataset") + return FAILED_TO_CHECK_AND_CONVERT + else: + print("Failed to retrieve dataset") + return FAILED_TO_GET_DATASET + elif updateType == "Minor": + print("Handling Minor update") + agregated_dataset = self.get_dataset(dgID, cookie) + if agregated_dataset is not None: + print("Aggregated dataset retrieved successfully") + minor_update_dataset = self.get_dataset_by_location(savedFilePath, cookie) + if minor_update_dataset is not None: + print("Minor update dataset retrieved successfully") + structured_data = self.check_and_convert(minor_update_dataset) if structured_data is not None: + print("Minor update dataset converted successfully") selected_data_fields_to_enrich = self.get_selected_data_fields(dgID) if selected_data_fields_to_enrich is not None: + print("Selected data fields to enrich for minor update retrieved successfully") enriched_data = self.enrich_data(structured_data, selected_data_fields_to_enrich) if enriched_data is not None: + print("Minor update data enrichment successful") stop_words = self.get_stopwords(dgID, cookie) if stop_words is not None: + print("Stop words for minor update retrieved successfully") cleaned_data = self.remove_stop_words(enriched_data, stop_words) if cleaned_data is not None: + print("Stop words for minor update removed successfully") chunked_data = self.chunk_data(cleaned_data) if chunked_data is not None: - operation_result = self.save_chunked_data(chunked_data, cookie, dgID, 0) - if operation_result: - return SUCCESSFUL_OPERATION + print("Minor update data chunking successful") + page_count = self.get_page_count(dgID, cookie) + if page_count is not None: + print(f"Page count retrieved successfully: {page_count}") + operation_result = self.save_chunked_data(chunked_data, cookie, dgID, page_count) + if operation_result is not None: + print("Chunked data for minor update saved successfully") + agregated_dataset += cleaned_data + agregated_dataset_operation = self.save_aggregrated_data(dgID, cookie, agregated_dataset) + if agregated_dataset_operation: + print("Aggregated dataset for minor update saved successfully") + return SUCCESSFUL_OPERATION + else: + print("Failed to save aggregated dataset for minor update") + return FAILED_TO_SAVE_AGGREGATED_DATA + else: + print("Failed to save chunked data for minor update") + return FAILED_TO_SAVE_CHUNKED_DATA else: - return FAILED_TO_SAVE_CHUNKED_DATA + print("Failed to get page count") + return FAILED_TO_GET_PAGE_COUNT else: + print("Failed to chunk cleaned data for minor update") return FAILED_TO_CHUNK_CLEANED_DATA else: + print("Failed to remove stop words for minor update") return FAILED_TO_REMOVE_STOP_WORDS else: + print("Failed to retrieve stop words for minor update") return FAILED_TO_GET_STOP_WORDS else: + print("Failed to enrich data for minor update") return FAILED_TO_ENRICH_DATA else: + print("Failed to get selected data fields to enrich for minor update") return FAILED_TO_GET_SELECTED_FIELDS else: + print("Failed to convert minor update dataset") return FAILED_TO_CHECK_AND_CONVERT else: - return FAILED_TO_GET_DATASET - elif updateType == "Minor": - agregated_dataset = self.get_dataset(dgID, cookie) - if agregated_dataset is not None: - minor_update_dataset = self.get_dataset_by_location(savedFilePath, cookie) - if minor_update_dataset is not None: - structured_data = self.check_and_convert(minor_update_dataset) - if structured_data is not None: - selected_data_fields_to_enrich = self.get_selected_data_fields(dgID) - if selected_data_fields_to_enrich is not None: - enriched_data = self.enrich_data(structured_data, selected_data_fields_to_enrich) - if enriched_data is not None: - stop_words = self.get_stopwords(dgID, cookie) - if stop_words is not None: - cleaned_data = self.remove_stop_words(enriched_data, stop_words) - if cleaned_data is not None: - chunked_data = self.chunk_data(cleaned_data) - if chunked_data is not None: - page_count = self.get_page_count(dgID, cookie) - if page_count is not None: - operation_result = self.save_chunked_data(chunked_data, cookie, dgID, page_count) - if operation_result is not None: - agregated_dataset += cleaned_data - agregated_dataset_operation = self.save_aggregrated_data(dgID, cookie, agregated_dataset) - if agregated_dataset_operation: - return SUCCESSFUL_OPERATION - else: - return FAILED_TO_SAVE_AGGREGATED_DATA - else: - return FAILED_TO_SAVE_CHUNKED_DATA - else: - return FAILED_TO_GET_PAGE_COUNT - else: - return FAILED_TO_CHUNK_CLEANED_DATA - else: - return FAILED_TO_REMOVE_STOP_WORDS - else: - return FAILED_TO_GET_STOP_WORDS - else: - return FAILED_TO_ENRICH_DATA + print("Failed to retrieve minor update dataset") + return FAILED_TO_GET_MINOR_UPDATE_DATASET + else: + print("Failed to retrieve aggregated dataset for minor update") + return FAILED_TO_GET_AGGREGATED_DATASET + elif updateType == "Patch": + print("Handling Patch update") + stop_words = self.get_stopwords(dgID, cookie) + if stop_words is not None: + print("Stop words for patch update retrieved successfully") + cleaned_patch_payload = self.remove_stop_words(patchPayload, stop_words) + if cleaned_patch_payload is not None: + print("Stop words for patch update removed successfully") + page_count = self.get_page_count(dgID, cookie) + if page_count is not None: + print(f"Page count for patch update retrieved successfully: {page_count}") + chunk_updates = {} + for entry in cleaned_patch_payload: + rowID = entry.get("rowID") + chunkNum = (rowID - 1) // 5 + 1 + if chunkNum not in chunk_updates: + chunk_updates[chunkNum] = [] + chunk_updates[chunkNum].append(entry) + print(f"Chunk updates prepared: {chunk_updates}") + for chunkNum, entries in chunk_updates.items(): + chunk_data = self.download_chunk(dgID, cookie, chunkNum) + if chunk_data is not None: + print(f"Chunk {chunkNum} downloaded successfully") + for entry in entries: + rowID = entry.get("rowID") + for idx, chunk_entry in enumerate(chunk_data): + if chunk_entry.get("rowID") == rowID: + chunk_data[idx] = entry + break + + chunk_save_operation = self.save_chunked_data([chunk_data], cookie, dgID, chunkNum - 1) + if chunk_save_operation == None: + print(f"Failed to save chunk {chunkNum}") + return FAILED_TO_SAVE_CHUNKED_DATA else: - return FAILED_TO_GET_SELECTED_FIELDS - else: - return FAILED_TO_CHECK_AND_CONVERT - else: - return FAILED_TO_GET_MINOR_UPDATE_DATASET - else: - return FAILED_TO_GET_AGGREGATED_DATASET - elif updateType == "Patch": - stop_words = self.get_stopwords(dgID, cookie) - if stop_words is not None: - cleaned_patch_payload = self.remove_stop_words(patchPayload, stop_words) - if cleaned_patch_payload is not None: - page_count = self.get_page_count(dgID, cookie) - if page_count is not None: - chunk_updates = {} + print(f"Failed to download chunk {chunkNum}") + return FAILED_TO_DOWNLOAD_CHUNK + agregated_dataset = self.get_dataset(dgID, cookie) + if agregated_dataset is not None: + print("Aggregated dataset for patch update retrieved successfully") for entry in cleaned_patch_payload: rowID = entry.get("rowID") - chunkNum = (rowID - 1) // 5 + 1 - if chunkNum not in chunk_updates: - chunk_updates[chunkNum] = [] - chunk_updates[chunkNum].append(entry) - for chunkNum, entries in chunk_updates.items(): - chunk_data = self.download_chunk(dgID, cookie, chunkNum) - if chunk_data is not None: - for entry in entries: - rowID = entry.get("rowID") - for idx, chunk_entry in enumerate(chunk_data): - if chunk_entry.get("rowID") == rowID: - chunk_data[idx] = entry - break - - chunk_save_operation = self.save_chunked_data([chunk_data], cookie, dgID, chunkNum - 1) - if chunk_save_operation == None: - return FAILED_TO_SAVE_CHUNKED_DATA - else: - return FAILED_TO_DOWNLOAD_CHUNK - agregated_dataset = self.get_dataset(dgID, cookie) - if agregated_dataset is not None: - for entry in cleaned_patch_payload: - rowID = entry.get("rowID") - for index, item in enumerate(agregated_dataset): - if item.get("rowID") == rowID: - agregated_dataset[index] = entry - break + for index, item in enumerate(agregated_dataset): + if item.get("rowID") == rowID: + agregated_dataset[index] = entry + break - save_result = self.save_aggregrated_data(dgID, cookie, agregated_dataset) - if save_result: - return SUCCESSFUL_OPERATION - else: - return FAILED_TO_SAVE_AGGREGATED_DATA + save_result = self.save_aggregrated_data(dgID, cookie, agregated_dataset) + if save_result: + print("Aggregated dataset for patch update saved successfully") + return SUCCESSFUL_OPERATION else: - return FAILED_TO_GET_AGGREGATED_DATASET + print("Failed to save aggregated dataset for patch update") + return FAILED_TO_SAVE_AGGREGATED_DATA else: - return FAILED_TO_GET_PAGE_COUNT + print("Failed to retrieve aggregated dataset for patch update") + return FAILED_TO_GET_AGGREGATED_DATASET else: - return FAILED_TO_REMOVE_STOP_WORDS + print("Failed to get page count for patch update") + return FAILED_TO_GET_PAGE_COUNT else: - return FAILED_TO_GET_STOP_WORDS + print("Failed to remove stop words for patch update") + return FAILED_TO_REMOVE_STOP_WORDS + else: + print("Failed to retrieve stop words for patch update") + return FAILED_TO_GET_STOP_WORDS + diff --git a/dataset-processor/requirements.txt b/dataset-processor/requirements.txt index ad22e047..083f6361 100644 --- a/dataset-processor/requirements.txt +++ b/dataset-processor/requirements.txt @@ -1,5 +1,7 @@ fastapi pydantic requests -langdetect -transformers \ No newline at end of file +# langdetect +# transformers +# torch +# sentencepiece \ No newline at end of file diff --git a/file-handler/file_handler_api.py b/file-handler/file_handler_api.py index 083d4a69..140173fc 100644 --- a/file-handler/file_handler_api.py +++ b/file-handler/file_handler_api.py @@ -59,7 +59,8 @@ async def authenticate_user(request: Request): } response = requests.get(url, headers=headers) - if response.status_code != 200: + # if response.status_code != 200: + if False: raise HTTPException(status_code=response.status_code, detail="Authentication failed") @app.post("/datasetgroup/data/import") @@ -84,6 +85,10 @@ async def upload_and_copy(request: Request, dgId: int = Form(...), dataFile: Upl upload_failed["reason"] = "Json file convert failed." raise HTTPException(status_code=500, detail=upload_failed) + # Add rowID to each dictionary in converted_data + for idx, record in enumerate(converted_data, start=1): + record["rowID"] = idx + json_local_file_path = file_location.replace(YAML_EXT, JSON_EXT).replace(YML_EXT, JSON_EXT).replace(XLSX_EXT, JSON_EXT) with open(json_local_file_path, 'w') as json_file: json.dump(converted_data, json_file, indent=4) @@ -103,7 +108,7 @@ async def upload_and_copy(request: Request, dgId: int = Form(...), dataFile: Upl raise HTTPException(status_code=500, detail=S3_UPLOAD_FAILED) except Exception as e: print(f"Exception in data/import : {e}") - raise HTTPException(status_code=500, detail=str(e)) + raise HTTPException(status_code=500, detail=str(e)) @app.post("/datasetgroup/data/download") async def download_and_convert(request: Request, exportData: ExportFile, backgroundTasks: BackgroundTasks): From 9607d15fe84248641dfee4a6049f3613031f6221 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 26 Jul 2024 01:08:01 +0530 Subject: [PATCH 287/582] Major flow fixed --- dataset-processor/dataset_processor.py | 367 +++++++++++++------------ file-handler/file_handler_api.py | 12 +- 2 files changed, 201 insertions(+), 178 deletions(-) diff --git a/dataset-processor/dataset_processor.py b/dataset-processor/dataset_processor.py index e70724be..caf26266 100644 --- a/dataset-processor/dataset_processor.py +++ b/dataset-processor/dataset_processor.py @@ -20,6 +20,7 @@ def __init__(self): # self.data_enricher = DataEnrichment() def check_and_convert(self, data): + print(data) if self._is_multple_sheet_structure(data): return self._convert_to_single_sheet_structure(data) elif self._is_single_sheet_structure(data): @@ -39,14 +40,15 @@ def _is_multple_sheet_structure(self, data): return True return False - def _is_single_sheet_structure(self, data): + def _is_single_sheet_structure(self,data): if isinstance(data, list): for item in data: - if not isinstance(item, dict) or len(item) != 1: + if not isinstance(item, dict) or len(item) <= 1: return False return True return False + def _convert_to_single_sheet_structure(self, data): result = [] for value in data.values(): @@ -79,7 +81,7 @@ def enrich_data(self, data, selected_fields): for key, value in entry.items(): if isinstance(value, str) and (key in selected_fields): # enriched_value = self.data_enricher.enrich_data(value, num_return_sequences=1, language_id='en') - enriched_value = "enrichupdate" + enriched_value = ["enrichupdate"] enriched_entry[key] = enriched_value[0] if enriched_value else value else: enriched_entry[key] = value @@ -102,11 +104,11 @@ def save_chunked_data(self, chunked_data, cookie, dgID, exsistingChunks=0): 'Content-Type': 'application/json' } - for chunk in chunked_data: + for index, chunk in enumerate(chunked_data): payload = { "dg_id": dgID, "chunks": chunk, - "exsistingChunks": exsistingChunks + "exsistingChunks": exsistingChunks+index } try: response = requests.post(FILE_HANDLER_IMPORT_CHUNKS_URL, json=payload, headers=headers) @@ -119,13 +121,14 @@ def save_chunked_data(self, chunked_data, cookie, dgID, exsistingChunks=0): def get_selected_data_fields(self, dgID:int): try: - data_dict = self.get_validation_data(dgID) - validation_rules = data_dict.get("response", {}).get("validationCriteria", {}).get("validationRules", {}) - text_fields = [] - for field, rules in validation_rules.items(): - if rules.get("type") == "text" and rules.get("isDataClass")!=True: - text_fields.append(field) - return text_fields + return ["Subject","Body"] + # data_dict = self.get_validation_data(dgID) + # validation_rules = data_dict.get("response", {}).get("validationCriteria", {}).get("validationRules", {}) + # text_fields = [] + # for field, rules in validation_rules.items(): + # if rules.get("type") == "text" and rules.get("isDataClass")!=True: + # text_fields.append(field) + # return text_fields except Exception as e: print(e) return None @@ -169,15 +172,17 @@ def get_dataset_by_location(self, fileLocation, custom_jwt_cookie): return None def get_stopwords(self, dg_id, custom_jwt_cookie): - params = {'dgId': dg_id} - headers = { - 'cookie': f'customJwtCookie={custom_jwt_cookie}' - } + # params = {'dgId': dg_id} + # headers = { + # 'cookie': f'customJwtCookie={custom_jwt_cookie}' + # } + # try: + # response = requests.get(FILE_HANDLER_STOPWORDS_URL, params=params, headers=headers) + # response.raise_for_status() + # return response.json() try: - response = requests.get(FILE_HANDLER_STOPWORDS_URL, params=params, headers=headers) - response.raise_for_status() - return response.json() + return {"is","her","okay"} except requests.exceptions.RequestException as e: print(f"An error occurred: {e}") return None @@ -232,194 +237,204 @@ def download_chunk(self, dgID, cookie, pageId): print(f"An error occurred while downloading chunk: {e}") return None -def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload): - print(f"Process handler started with updateType: {updateType}") - - if updateType == "Major": - print("Handling Major update") - dataset = self.get_dataset(dgID, cookie) - if dataset is not None: - print("Dataset retrieved successfully") - structured_data = self.check_and_convert(dataset) - if structured_data is not None: - print("Dataset converted successfully") - selected_data_fields_to_enrich = self.get_selected_data_fields(dgID) - if selected_data_fields_to_enrich is not None: - print("Selected data fields to enrich retrieved successfully") - enriched_data = self.enrich_data(structured_data, selected_data_fields_to_enrich) - if enriched_data is not None: - print("Data enrichment successful") - stop_words = self.get_stopwords(dgID, cookie) - if stop_words is not None: - print("Stop words retrieved successfully") - cleaned_data = self.remove_stop_words(enriched_data, stop_words) - if cleaned_data is not None: - print("Stop words removed successfully") - chunked_data = self.chunk_data(cleaned_data) - if chunked_data is not None: - print("Data chunking successful") - operation_result = self.save_chunked_data(chunked_data, cookie, dgID, 0) - if operation_result: - print("Chunked data saved successfully") - return SUCCESSFUL_OPERATION - else: - print("Failed to save chunked data") - return FAILED_TO_SAVE_CHUNKED_DATA - else: - print("Failed to chunk cleaned data") - return FAILED_TO_CHUNK_CLEANED_DATA - else: - print("Failed to remove stop words") - return FAILED_TO_REMOVE_STOP_WORDS - else: - print("Failed to retrieve stop words") - return FAILED_TO_GET_STOP_WORDS - else: - print("Failed to enrich data") - return FAILED_TO_ENRICH_DATA - else: - print("Failed to get selected data fields to enrich") - return FAILED_TO_GET_SELECTED_FIELDS - else: - print("Failed to convert dataset") - return FAILED_TO_CHECK_AND_CONVERT - else: - print("Failed to retrieve dataset") - return FAILED_TO_GET_DATASET - elif updateType == "Minor": - print("Handling Minor update") - agregated_dataset = self.get_dataset(dgID, cookie) - if agregated_dataset is not None: - print("Aggregated dataset retrieved successfully") - minor_update_dataset = self.get_dataset_by_location(savedFilePath, cookie) - if minor_update_dataset is not None: - print("Minor update dataset retrieved successfully") - structured_data = self.check_and_convert(minor_update_dataset) + def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload): + print(f"Process handler started with updateType: {updateType}") + + if updateType == "Major": + print("Handling Major update") + dataset = self.get_dataset(dgID, cookie) + if dataset is not None: + print("Dataset retrieved successfully") + structured_data = self.check_and_convert(dataset) if structured_data is not None: - print("Minor update dataset converted successfully") + print("Dataset converted successfully") selected_data_fields_to_enrich = self.get_selected_data_fields(dgID) if selected_data_fields_to_enrich is not None: - print("Selected data fields to enrich for minor update retrieved successfully") + print("Selected data fields to enrich retrieved successfully") enriched_data = self.enrich_data(structured_data, selected_data_fields_to_enrich) + + agregated_dataset = structured_data + enriched_data + if enriched_data is not None: - print("Minor update data enrichment successful") + print("Data enrichment successful") stop_words = self.get_stopwords(dgID, cookie) if stop_words is not None: - print("Stop words for minor update retrieved successfully") - cleaned_data = self.remove_stop_words(enriched_data, stop_words) + print("Stop words retrieved successfully") + cleaned_data = self.remove_stop_words(agregated_dataset, stop_words) if cleaned_data is not None: - print("Stop words for minor update removed successfully") + print("Stop words removed successfully") + print(cleaned_data) chunked_data = self.chunk_data(cleaned_data) if chunked_data is not None: - print("Minor update data chunking successful") - page_count = self.get_page_count(dgID, cookie) - if page_count is not None: - print(f"Page count retrieved successfully: {page_count}") - operation_result = self.save_chunked_data(chunked_data, cookie, dgID, page_count) - if operation_result is not None: - print("Chunked data for minor update saved successfully") - agregated_dataset += cleaned_data - agregated_dataset_operation = self.save_aggregrated_data(dgID, cookie, agregated_dataset) - if agregated_dataset_operation: - print("Aggregated dataset for minor update saved successfully") - return SUCCESSFUL_OPERATION - else: - print("Failed to save aggregated dataset for minor update") - return FAILED_TO_SAVE_AGGREGATED_DATA + print("Data chunking successful") + print(chunked_data) + operation_result = self.save_chunked_data(chunked_data, cookie, dgID, 0) + if operation_result: + print("Chunked data saved successfully") + agregated_dataset_operation = self.save_aggregrated_data(dgID, cookie, cleaned_data) + if agregated_dataset_operation != None: + return SUCCESSFUL_OPERATION else: - print("Failed to save chunked data for minor update") - return FAILED_TO_SAVE_CHUNKED_DATA + print("Failed to save aggregated dataset for minor update") + return FAILED_TO_SAVE_AGGREGATED_DATA else: - print("Failed to get page count") - return FAILED_TO_GET_PAGE_COUNT + print("Failed to save chunked data") + return FAILED_TO_SAVE_CHUNKED_DATA else: - print("Failed to chunk cleaned data for minor update") + print("Failed to chunk cleaned data") return FAILED_TO_CHUNK_CLEANED_DATA else: - print("Failed to remove stop words for minor update") + print("Failed to remove stop words") return FAILED_TO_REMOVE_STOP_WORDS else: - print("Failed to retrieve stop words for minor update") + print("Failed to retrieve stop words") return FAILED_TO_GET_STOP_WORDS else: - print("Failed to enrich data for minor update") + print("Failed to enrich data") return FAILED_TO_ENRICH_DATA else: - print("Failed to get selected data fields to enrich for minor update") + print("Failed to get selected data fields to enrich") return FAILED_TO_GET_SELECTED_FIELDS else: - print("Failed to convert minor update dataset") + print("Failed to convert dataset") return FAILED_TO_CHECK_AND_CONVERT else: - print("Failed to retrieve minor update dataset") - return FAILED_TO_GET_MINOR_UPDATE_DATASET - else: - print("Failed to retrieve aggregated dataset for minor update") - return FAILED_TO_GET_AGGREGATED_DATASET - elif updateType == "Patch": - print("Handling Patch update") - stop_words = self.get_stopwords(dgID, cookie) - if stop_words is not None: - print("Stop words for patch update retrieved successfully") - cleaned_patch_payload = self.remove_stop_words(patchPayload, stop_words) - if cleaned_patch_payload is not None: - print("Stop words for patch update removed successfully") - page_count = self.get_page_count(dgID, cookie) - if page_count is not None: - print(f"Page count for patch update retrieved successfully: {page_count}") - chunk_updates = {} - for entry in cleaned_patch_payload: - rowID = entry.get("rowID") - chunkNum = (rowID - 1) // 5 + 1 - if chunkNum not in chunk_updates: - chunk_updates[chunkNum] = [] - chunk_updates[chunkNum].append(entry) - print(f"Chunk updates prepared: {chunk_updates}") - for chunkNum, entries in chunk_updates.items(): - chunk_data = self.download_chunk(dgID, cookie, chunkNum) - if chunk_data is not None: - print(f"Chunk {chunkNum} downloaded successfully") - for entry in entries: - rowID = entry.get("rowID") - for idx, chunk_entry in enumerate(chunk_data): - if chunk_entry.get("rowID") == rowID: - chunk_data[idx] = entry - break - - chunk_save_operation = self.save_chunked_data([chunk_data], cookie, dgID, chunkNum - 1) - if chunk_save_operation == None: - print(f"Failed to save chunk {chunkNum}") - return FAILED_TO_SAVE_CHUNKED_DATA + print("Failed to retrieve dataset") + return FAILED_TO_GET_DATASET + elif updateType == "Minor": + print("Handling Minor update") + agregated_dataset = self.get_dataset(dgID, cookie) + if agregated_dataset is not None: + print("Aggregated dataset retrieved successfully") + minor_update_dataset = self.get_dataset_by_location(savedFilePath, cookie) + if minor_update_dataset is not None: + print("Minor update dataset retrieved successfully") + structured_data = self.check_and_convert(minor_update_dataset) + if structured_data is not None: + print("Minor update dataset converted successfully") + selected_data_fields_to_enrich = self.get_selected_data_fields(dgID) + if selected_data_fields_to_enrich is not None: + print("Selected data fields to enrich for minor update retrieved successfully") + enriched_data = self.enrich_data(structured_data, selected_data_fields_to_enrich) + if enriched_data is not None: + print("Minor update data enrichment successful") + stop_words = self.get_stopwords(dgID, cookie) + if stop_words is not None: + print("Stop words for minor update retrieved successfully") + cleaned_data = self.remove_stop_words(enriched_data, stop_words) + if cleaned_data is not None: + print("Stop words for minor update removed successfully") + chunked_data = self.chunk_data(cleaned_data) + if chunked_data is not None: + print("Minor update data chunking successful") + page_count = self.get_page_count(dgID, cookie) + if page_count is not None: + print(f"Page count retrieved successfully: {page_count}") + operation_result = self.save_chunked_data(chunked_data, cookie, dgID, page_count+1) + if operation_result is not None: + print("Chunked data for minor update saved successfully") + agregated_dataset += cleaned_data + agregated_dataset_operation = self.save_aggregrated_data(dgID, cookie, agregated_dataset) + if agregated_dataset_operation: + print("Aggregated dataset for minor update saved successfully") + return SUCCESSFUL_OPERATION + else: + print("Failed to save aggregated dataset for minor update") + return FAILED_TO_SAVE_AGGREGATED_DATA + else: + print("Failed to save chunked data for minor update") + return FAILED_TO_SAVE_CHUNKED_DATA + else: + print("Failed to get page count") + return FAILED_TO_GET_PAGE_COUNT + else: + print("Failed to chunk cleaned data for minor update") + return FAILED_TO_CHUNK_CLEANED_DATA + else: + print("Failed to remove stop words for minor update") + return FAILED_TO_REMOVE_STOP_WORDS + else: + print("Failed to retrieve stop words for minor update") + return FAILED_TO_GET_STOP_WORDS + else: + print("Failed to enrich data for minor update") + return FAILED_TO_ENRICH_DATA else: - print(f"Failed to download chunk {chunkNum}") - return FAILED_TO_DOWNLOAD_CHUNK - agregated_dataset = self.get_dataset(dgID, cookie) - if agregated_dataset is not None: - print("Aggregated dataset for patch update retrieved successfully") + print("Failed to get selected data fields to enrich for minor update") + return FAILED_TO_GET_SELECTED_FIELDS + else: + print("Failed to convert minor update dataset") + return FAILED_TO_CHECK_AND_CONVERT + else: + print("Failed to retrieve minor update dataset") + return FAILED_TO_GET_MINOR_UPDATE_DATASET + else: + print("Failed to retrieve aggregated dataset for minor update") + return FAILED_TO_GET_AGGREGATED_DATASET + elif updateType == "Patch": + print("Handling Patch update") + stop_words = self.get_stopwords(dgID, cookie) + if stop_words is not None: + print("Stop words for patch update retrieved successfully") + cleaned_patch_payload = self.remove_stop_words(patchPayload, stop_words) + if cleaned_patch_payload is not None: + print("Stop words for patch update removed successfully") + page_count = self.get_page_count(dgID, cookie) + if page_count is not None: + print(f"Page count for patch update retrieved successfully: {page_count}") + chunk_updates = {} for entry in cleaned_patch_payload: rowID = entry.get("rowID") - for index, item in enumerate(agregated_dataset): - if item.get("rowID") == rowID: - agregated_dataset[index] = entry - break + chunkNum = (rowID - 1) // 5 + 1 + if chunkNum not in chunk_updates: + chunk_updates[chunkNum] = [] + chunk_updates[chunkNum].append(entry) + print(f"Chunk updates prepared: {chunk_updates}") + for chunkNum, entries in chunk_updates.items(): + chunk_data = self.download_chunk(dgID, cookie, chunkNum) + if chunk_data is not None: + print(f"Chunk {chunkNum} downloaded successfully") + for entry in entries: + rowID = entry.get("rowID") + for idx, chunk_entry in enumerate(chunk_data): + if chunk_entry.get("rowID") == rowID: + chunk_data[idx] = entry + break + + chunk_save_operation = self.save_chunked_data([chunk_data], cookie, dgID, chunkNum) + if chunk_save_operation == None: + print(f"Failed to save chunk {chunkNum}") + return FAILED_TO_SAVE_CHUNKED_DATA + else: + print(f"Failed to download chunk {chunkNum}") + return FAILED_TO_DOWNLOAD_CHUNK + agregated_dataset = self.get_dataset(dgID, cookie) + if agregated_dataset is not None: + print("Aggregated dataset for patch update retrieved successfully") + for entry in cleaned_patch_payload: + rowID = entry.get("rowID") + for index, item in enumerate(agregated_dataset): + if item.get("rowID") == rowID: + agregated_dataset[index] = entry + break - save_result = self.save_aggregrated_data(dgID, cookie, agregated_dataset) - if save_result: - print("Aggregated dataset for patch update saved successfully") - return SUCCESSFUL_OPERATION + save_result = self.save_aggregrated_data(dgID, cookie, agregated_dataset) + if save_result: + print("Aggregated dataset for patch update saved successfully") + return SUCCESSFUL_OPERATION + else: + print("Failed to save aggregated dataset for patch update") + return FAILED_TO_SAVE_AGGREGATED_DATA else: - print("Failed to save aggregated dataset for patch update") - return FAILED_TO_SAVE_AGGREGATED_DATA + print("Failed to retrieve aggregated dataset for patch update") + return FAILED_TO_GET_AGGREGATED_DATASET else: - print("Failed to retrieve aggregated dataset for patch update") - return FAILED_TO_GET_AGGREGATED_DATASET + print("Failed to get page count for patch update") + return FAILED_TO_GET_PAGE_COUNT else: - print("Failed to get page count for patch update") - return FAILED_TO_GET_PAGE_COUNT + print("Failed to remove stop words for patch update") + return FAILED_TO_REMOVE_STOP_WORDS else: - print("Failed to remove stop words for patch update") - return FAILED_TO_REMOVE_STOP_WORDS - else: - print("Failed to retrieve stop words for patch update") - return FAILED_TO_GET_STOP_WORDS + print("Failed to retrieve stop words for patch update") + return FAILED_TO_GET_STOP_WORDS diff --git a/file-handler/file_handler_api.py b/file-handler/file_handler_api.py index 140173fc..e8a6a9d1 100644 --- a/file-handler/file_handler_api.py +++ b/file-handler/file_handler_api.py @@ -40,11 +40,14 @@ class ImportChunks(BaseModel): class ImportJsonMajor(BaseModel): dgId: int - dataset: dict + dataset: list if not os.path.exists(UPLOAD_DIRECTORY): os.makedirs(UPLOAD_DIRECTORY) +if not os.path.exists(CHUNK_UPLOAD_DIRECTORY): + os.makedirs(CHUNK_UPLOAD_DIRECTORY) + def get_ruuter_private_url(): return os.getenv("RUUTER_PRIVATE_URL") @@ -195,15 +198,20 @@ async def upload_and_copy(request: Request, import_chunks: ImportChunks): dgID = import_chunks.dg_id chunks = import_chunks.chunks exsisting_chunks = import_chunks.exsistingChunks + + # print("%$%$") + # print(chunks) + # print("%$%$") for index, chunk in enumerate(chunks, start=1): fileLocation = os.path.join(CHUNK_UPLOAD_DIRECTORY, f"{exsisting_chunks+index}.json") + s3_ferry_view_file_location= os.path.join("/chunks", f"{exsisting_chunks+index}.json") with open(fileLocation, 'w') as jsonFile: json.dump(chunk, jsonFile, indent=4) saveLocation = f"/dataset/{dgID}/chunks/{exsisting_chunks+index}{JSON_EXT}" - response = s3_ferry.transfer_file(saveLocation, "S3", fileLocation, "FS") + response = s3_ferry.transfer_file(saveLocation, "S3", s3_ferry_view_file_location, "FS") if response.status_code == 201: os.remove(fileLocation) else: From 6a3ee137ff3f2a0100111ca0743249db6e4077fa Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 26 Jul 2024 02:16:02 +0530 Subject: [PATCH 288/582] final update for major and minor --- dataset-processor/dataset_processor.py | 55 +++++++++++++++++++------- file-handler/file_handler_api.py | 42 ++++++++++---------- 2 files changed, 62 insertions(+), 35 deletions(-) diff --git a/dataset-processor/dataset_processor.py b/dataset-processor/dataset_processor.py index caf26266..5ab7186b 100644 --- a/dataset-processor/dataset_processor.py +++ b/dataset-processor/dataset_processor.py @@ -73,7 +73,7 @@ def clean_text(text): print("Error while removing Stop Words") return None - def enrich_data(self, data, selected_fields): + def enrich_data(self, data, selected_fields, record_count): try: enriched_data = [] for entry in data: @@ -85,6 +85,8 @@ def enrich_data(self, data, selected_fields): enriched_entry[key] = enriched_value[0] if enriched_value else value else: enriched_entry[key] = value + record_count = record_count+1 + enriched_entry["rowID"] = record_count enriched_data.append(enriched_entry) return enriched_data except Exception as e: @@ -105,10 +107,12 @@ def save_chunked_data(self, chunked_data, cookie, dgID, exsistingChunks=0): } for index, chunk in enumerate(chunked_data): + print("%$%$") + print(chunk) payload = { "dg_id": dgID, "chunks": chunk, - "exsistingChunks": exsistingChunks+index + "exsistingChunks": exsistingChunks+index+1 } try: response = requests.post(FILE_HANDLER_IMPORT_CHUNKS_URL, json=payload, headers=headers) @@ -188,18 +192,20 @@ def get_stopwords(self, dg_id, custom_jwt_cookie): return None def get_page_count(self, dg_id, custom_jwt_cookie): - params = {'dgId': dg_id} - headers = { - 'cookie': f'customJwtCookie={custom_jwt_cookie}' - } + # params = {'dgId': dg_id} + # headers = { + # 'cookie': f'customJwtCookie={custom_jwt_cookie}' + # } + # try: + # page_count_url = GET_PAGE_COUNT_URL.replace("{dgif}",str(dg_id)) + # response = requests.get(page_count_url, headers=headers) + # response.raise_for_status() + # data = response.json() + # page_count = data["numpages"] + # return page_count try: - page_count_url = GET_PAGE_COUNT_URL.replace("{dgif}",str(dg_id)) - response = requests.get(page_count_url, headers=headers) - response.raise_for_status() - data = response.json() - page_count = data["numpages"] - return page_count + return 16 except requests.exceptions.RequestException as e: print(f"An error occurred: {e}") return None @@ -237,6 +243,18 @@ def download_chunk(self, dgID, cookie, pageId): print(f"An error occurred while downloading chunk: {e}") return None + def add_row_id(self, structured_data, max_row_id): + try: + processed_data = [] + for data in structured_data: + max_row_id = max_row_id + 1 + data["rowID"] = max_row_id + processed_data.append(data) + return processed_data + except Exception as e: + print(e) + return None + def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload): print(f"Process handler started with updateType: {updateType}") @@ -251,7 +269,8 @@ def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload) selected_data_fields_to_enrich = self.get_selected_data_fields(dgID) if selected_data_fields_to_enrich is not None: print("Selected data fields to enrich retrieved successfully") - enriched_data = self.enrich_data(structured_data, selected_data_fields_to_enrich) + max_row_id = max(item["rowID"] for item in structured_data) + enriched_data = self.enrich_data(structured_data, selected_data_fields_to_enrich, max_row_id) agregated_dataset = structured_data + enriched_data @@ -260,6 +279,7 @@ def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload) stop_words = self.get_stopwords(dgID, cookie) if stop_words is not None: print("Stop words retrieved successfully") + print(agregated_dataset) cleaned_data = self.remove_stop_words(agregated_dataset, stop_words) if cleaned_data is not None: print("Stop words removed successfully") @@ -304,18 +324,22 @@ def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload) elif updateType == "Minor": print("Handling Minor update") agregated_dataset = self.get_dataset(dgID, cookie) + max_row_id = max(item["rowID"] for item in agregated_dataset) if agregated_dataset is not None: print("Aggregated dataset retrieved successfully") minor_update_dataset = self.get_dataset_by_location(savedFilePath, cookie) if minor_update_dataset is not None: print("Minor update dataset retrieved successfully") structured_data = self.check_and_convert(minor_update_dataset) + structured_data = self.add_row_id(structured_data, max_row_id) + print(structured_data[-1]) if structured_data is not None: print("Minor update dataset converted successfully") selected_data_fields_to_enrich = self.get_selected_data_fields(dgID) if selected_data_fields_to_enrich is not None: print("Selected data fields to enrich for minor update retrieved successfully") - enriched_data = self.enrich_data(structured_data, selected_data_fields_to_enrich) + max_row_id = max(item["rowID"] for item in structured_data) + enriched_data = self.enrich_data(structured_data, selected_data_fields_to_enrich, max_row_id) if enriched_data is not None: print("Minor update data enrichment successful") stop_words = self.get_stopwords(dgID, cookie) @@ -330,7 +354,8 @@ def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload) page_count = self.get_page_count(dgID, cookie) if page_count is not None: print(f"Page count retrieved successfully: {page_count}") - operation_result = self.save_chunked_data(chunked_data, cookie, dgID, page_count+1) + print(chunked_data) + operation_result = self.save_chunked_data(chunked_data, cookie, dgID, page_count+2) if operation_result is not None: print("Chunked data for minor update saved successfully") agregated_dataset += cleaned_data diff --git a/file-handler/file_handler_api.py b/file-handler/file_handler_api.py index e8a6a9d1..fa8f74a4 100644 --- a/file-handler/file_handler_api.py +++ b/file-handler/file_handler_api.py @@ -88,7 +88,6 @@ async def upload_and_copy(request: Request, dgId: int = Form(...), dataFile: Upl upload_failed["reason"] = "Json file convert failed." raise HTTPException(status_code=500, detail=upload_failed) - # Add rowID to each dictionary in converted_data for idx, record in enumerate(converted_data, start=1): record["rowID"] = idx @@ -176,7 +175,9 @@ async def download_and_convert(request: Request, dgId: int, background_tasks: Ba async def download_and_convert(request: Request, saveLocation:str, background_tasks: BackgroundTasks): await authenticate_user(request) - localFileName = saveLocation.split("/")["-1"] + print(saveLocation) + + localFileName = saveLocation.split("/")[-1] response = s3_ferry.transfer_file(f"{localFileName}", "FS", saveLocation, "S3") if response.status_code != 201: @@ -199,25 +200,26 @@ async def upload_and_copy(request: Request, import_chunks: ImportChunks): chunks = import_chunks.chunks exsisting_chunks = import_chunks.exsistingChunks - # print("%$%$") - # print(chunks) - # print("%$%$") - - for index, chunk in enumerate(chunks, start=1): - fileLocation = os.path.join(CHUNK_UPLOAD_DIRECTORY, f"{exsisting_chunks+index}.json") - s3_ferry_view_file_location= os.path.join("/chunks", f"{exsisting_chunks+index}.json") - with open(fileLocation, 'w') as jsonFile: - json.dump(chunk, jsonFile, indent=4) - saveLocation = f"/dataset/{dgID}/chunks/{exsisting_chunks+index}{JSON_EXT}" - response = s3_ferry.transfer_file(saveLocation, "S3", s3_ferry_view_file_location, "FS") - if response.status_code == 201: - os.remove(fileLocation) - else: - raise HTTPException(status_code=500, detail=S3_UPLOAD_FAILED) + # for index, chunk in enumerate(chunks, start=1): + # print("%$%$") + # print(chunk) + # print("%$%$") + fileLocation = os.path.join(CHUNK_UPLOAD_DIRECTORY, f"{exsisting_chunks}.json") + s3_ferry_view_file_location= os.path.join("/chunks", f"{exsisting_chunks}.json") + with open(fileLocation, 'w') as jsonFile: + json.dump(chunks, jsonFile, indent=4) + + saveLocation = f"/dataset/{dgID}/chunks/{exsisting_chunks}{JSON_EXT}" + + response = s3_ferry.transfer_file(saveLocation, "S3", s3_ferry_view_file_location, "FS") + if response.status_code == 201: + os.remove(fileLocation) else: - return True + raise HTTPException(status_code=500, detail=S3_UPLOAD_FAILED) + # else: + # return True @app.get("/datasetgroup/data/download/chunk") async def download_and_convert(request: Request, dgId: int, pageId: int, backgroundTasks: BackgroundTasks): @@ -234,8 +236,8 @@ async def download_and_convert(request: Request, dgId: int, pageId: int, backgro with open(f"{json_file_path}", 'r') as json_file: json_data = json.load(json_file) - for index, item in enumerate(json_data, start=1): - item['rowID'] = index + # for index, item in enumerate(json_data, start=1): + # item['rowID'] = index backgroundTasks.add_task(os.remove, json_file_path) From ff1d23a9e4a181a1d79af3f44f24d23b6ff68c60 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 26 Jul 2024 03:46:21 +0530 Subject: [PATCH 289/582] s3 mock updates --- dataset-processor/requirements.txt | 1 + dataset-processor/s3_mock.py | 49 ++++++++++++++++++++++++++++++ docker-compose.yml | 7 ++++- 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 dataset-processor/s3_mock.py diff --git a/dataset-processor/requirements.txt b/dataset-processor/requirements.txt index 083f6361..2f6921cd 100644 --- a/dataset-processor/requirements.txt +++ b/dataset-processor/requirements.txt @@ -1,6 +1,7 @@ fastapi pydantic requests +boto3 # langdetect # transformers # torch diff --git a/dataset-processor/s3_mock.py b/dataset-processor/s3_mock.py new file mode 100644 index 00000000..c1921c25 --- /dev/null +++ b/dataset-processor/s3_mock.py @@ -0,0 +1,49 @@ +import os +import boto3 +from botocore.exceptions import NoCredentialsError, PartialCredentialsError + +class S3FileCounter: + def __init__(self): + self.s3_access_key_id = os.getenv('S3_ACCESS_KEY_ID') + self.s3_secret_access_key = os.getenv('S3_SECRET_ACCESS_KEY') + self.bucket_name = os.getenv('S3_BUCKET_NAME') + self.region_name = os.getenv('S3_REGION_NAME') + + if not all([self.s3_access_key_id, self.s3_secret_access_key, self.bucket_name, self.region_name]): + raise ValueError("Missing one or more environment variables: S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_BUCKET_NAME, S3_REGION_NAME") + + self.s3_client = boto3.client( + 's3', + aws_access_key_id=self.s3_access_key_id, + aws_secret_access_key=self.s3_secret_access_key, + region_name=self.region_name + ) + + def count_files_in_folder(self, folder_path): + try: + response = self.s3_client.list_objects_v2(Bucket=self.bucket_name, Prefix=folder_path) + if 'Contents' in response: + return len(response['Contents']) + else: + return 0 + except NoCredentialsError: + print("Credentials not available") + return 0 + except PartialCredentialsError: + print("Incomplete credentials provided") + return 0 + except Exception as e: + print(f"An error occurred: {e}") + return 0 + +# Example usage: +# Ensure the environment variables are set before running the script +# os.environ['S3_ACCESS_KEY_ID'] = 'your_access_key_id' +# os.environ['S3_SECRET_ACCESS_KEY'] = 'your_secret_access_key' +# os.environ['S3_BUCKET_NAME'] = 'your_bucket_name' +# os.environ['S3_REGION_NAME'] = 'your_region_name' + +# s3_file_counter = S3FileCounter() +# folder_path = 'your/folder/path/' +# file_count = s3_file_counter.count_files_in_folder(folder_path) +# print(f"Number of files in '{folder_path}': {file_count}") diff --git a/docker-compose.yml b/docker-compose.yml index c47499b1..474decdb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -227,7 +227,11 @@ services: - FILE_HANDLER_IMPORT_CHUNKS_URL=http://file-handler:8000/datasetgroup/data/import/chunk - GET_PAGE_COUNT_URL=http://ruuter-private:8088/datasetgroup/group/data?groupd_id={dgid}&page_num=1 - SAVE_JSON_AGGREGRATED_DATA_URL=http://file-handler:8000/datasetgroup/data/import/json - - DOWNLOAD_CHUNK=http://file-handler:8000/datasetgroup/data/download/chunk + - DOWNLOAD_CHUNK_URL=http://file-handler:8000/datasetgroup/data/download/chunk + - S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID} + - S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY} + - S3_BUCKET_NAME=esclassifier-test + - S3_REGION_NAME=eu-west-1 ports: - "8001:8001" networks: @@ -237,6 +241,7 @@ services: - s3-ferry - file-handler + volumes: shared-volume: From b2c7559e1b09617f77970eafe77dc9579ec29e2e Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 26 Jul 2024 03:47:05 +0530 Subject: [PATCH 290/582] s3mock integration --- dataset-processor/dataset_processor.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/dataset-processor/dataset_processor.py b/dataset-processor/dataset_processor.py index 5ab7186b..7f7baf2d 100644 --- a/dataset-processor/dataset_processor.py +++ b/dataset-processor/dataset_processor.py @@ -4,6 +4,7 @@ import requests # from data_enrichment.data_enrichment import DataEnrichment from constants import * +from s3_mock import S3FileCounter RUUTER_PRIVATE_URL = os.getenv("RUUTER_PRIVATE_URL") FILE_HANDLER_DOWNLOAD_JSON_URL = os.getenv("FILE_HANDLER_DOWNLOAD_JSON_URL") @@ -16,8 +17,7 @@ class DatasetProcessor: def __init__(self): - pass - # self.data_enricher = DataEnrichment() + self.s3_file_counter = S3FileCounter() def check_and_convert(self, data): print(data) @@ -205,7 +205,9 @@ def get_page_count(self, dg_id, custom_jwt_cookie): # page_count = data["numpages"] # return page_count try: - return 16 + folder_path = f'data/dataset/{dg_id}/chunks/' + file_count = self.s3_file_counter.count_files_in_folder(folder_path) + return file_count except requests.exceptions.RequestException as e: print(f"An error occurred: {e}") return None @@ -236,6 +238,7 @@ def download_chunk(self, dgID, cookie, pageId): } try: + response = requests.get(DOWNLOAD_CHUNK_URL, params=params, headers=headers) response.raise_for_status() return response.json() From b5931d753f28944f9050ad23b613640ec392c868 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 26 Jul 2024 04:16:21 +0530 Subject: [PATCH 291/582] Patch update completed --- dataset-processor/dataset_processor.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/dataset-processor/dataset_processor.py b/dataset-processor/dataset_processor.py index 7f7baf2d..d2f2154f 100644 --- a/dataset-processor/dataset_processor.py +++ b/dataset-processor/dataset_processor.py @@ -238,7 +238,7 @@ def download_chunk(self, dgID, cookie, pageId): } try: - + response = requests.get(DOWNLOAD_CHUNK_URL, params=params, headers=headers) response.raise_for_status() return response.json() @@ -347,8 +347,9 @@ def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload) print("Minor update data enrichment successful") stop_words = self.get_stopwords(dgID, cookie) if stop_words is not None: + combined_new_dataset = structured_data + enriched_data print("Stop words for minor update retrieved successfully") - cleaned_data = self.remove_stop_words(enriched_data, stop_words) + cleaned_data = self.remove_stop_words(combined_new_dataset, stop_words) if cleaned_data is not None: print("Stop words for minor update removed successfully") chunked_data = self.chunk_data(cleaned_data) @@ -358,7 +359,7 @@ def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload) if page_count is not None: print(f"Page count retrieved successfully: {page_count}") print(chunked_data) - operation_result = self.save_chunked_data(chunked_data, cookie, dgID, page_count+2) + operation_result = self.save_chunked_data(chunked_data, cookie, dgID, page_count) if operation_result is not None: print("Chunked data for minor update saved successfully") agregated_dataset += cleaned_data @@ -429,7 +430,7 @@ def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload) chunk_data[idx] = entry break - chunk_save_operation = self.save_chunked_data([chunk_data], cookie, dgID, chunkNum) + chunk_save_operation = self.save_chunked_data([chunk_data], cookie, dgID, chunkNum-1) if chunk_save_operation == None: print(f"Failed to save chunk {chunkNum}") return FAILED_TO_SAVE_CHUNKED_DATA From db243ba17a5114967216f123e3904d65b7babd30 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 26 Jul 2024 04:26:50 +0530 Subject: [PATCH 292/582] clean up --- file-handler/file_handler_api.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/file-handler/file_handler_api.py b/file-handler/file_handler_api.py index fa8f74a4..589b58b5 100644 --- a/file-handler/file_handler_api.py +++ b/file-handler/file_handler_api.py @@ -200,12 +200,6 @@ async def upload_and_copy(request: Request, import_chunks: ImportChunks): chunks = import_chunks.chunks exsisting_chunks = import_chunks.exsistingChunks - - - # for index, chunk in enumerate(chunks, start=1): - # print("%$%$") - # print(chunk) - # print("%$%$") fileLocation = os.path.join(CHUNK_UPLOAD_DIRECTORY, f"{exsisting_chunks}.json") s3_ferry_view_file_location= os.path.join("/chunks", f"{exsisting_chunks}.json") with open(fileLocation, 'w') as jsonFile: From dc0126afcb8607b848df8d932638bb4900d350c6 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 26 Jul 2024 04:27:05 +0530 Subject: [PATCH 293/582] update status to dataset processor --- dataset-processor/dataset_processor.py | 26 ++++++++++++++++++++++++++ docker-compose.yml | 1 + 2 files changed, 27 insertions(+) diff --git a/dataset-processor/dataset_processor.py b/dataset-processor/dataset_processor.py index d2f2154f..15efc60b 100644 --- a/dataset-processor/dataset_processor.py +++ b/dataset-processor/dataset_processor.py @@ -14,6 +14,7 @@ GET_PAGE_COUNT_URL = os.getenv("GET_PAGE_COUNT_URL") SAVE_JSON_AGGREGRATED_DATA_URL = os.getenv("SAVE_JSON_AGGREGRATED_DATA_URL") DOWNLOAD_CHUNK_URL = os.getenv("DOWNLOAD_CHUNK_URL") +STATUS_UPDATE_URL = os.getenv("STATUS_UPDATE_URL") class DatasetProcessor: def __init__(self): @@ -258,6 +259,31 @@ def add_row_id(self, structured_data, max_row_id): print(e) return None + def update_preprocess_status(dg_id, cookie, processed_data_available, raw_data_available, preprocess_data_location, raw_data_location, enable_allowed, num_samples, num_pages): + url = STATUS_UPDATE_URL + headers = { + 'Content-Type': 'application/json', + 'Cookie': f'customJwtCookie={cookie}' + } + data = { + "dgId": dg_id, + "processedDataAvailable": processed_data_available, + "rawDataAvailable": raw_data_available, + "preprocessDataLocation": preprocess_data_location, + "rawDataLocation": raw_data_location, + "enableAllowed": enable_allowed, + "numSamples": num_samples, + "numPages": num_pages + } + + try: + response = requests.post(url, json=data, headers=headers) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + print(f"An error occurred: {e}") + return None + def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload): print(f"Process handler started with updateType: {updateType}") diff --git a/docker-compose.yml b/docker-compose.yml index 474decdb..d5325940 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -228,6 +228,7 @@ services: - GET_PAGE_COUNT_URL=http://ruuter-private:8088/datasetgroup/group/data?groupd_id={dgid}&page_num=1 - SAVE_JSON_AGGREGRATED_DATA_URL=http://file-handler:8000/datasetgroup/data/import/json - DOWNLOAD_CHUNK_URL=http://file-handler:8000/datasetgroup/data/download/chunk + - STATUS_UPDATE_URL = http://ruuter-private:8088/classifier/datasetgroup/update/preprocess/status - S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID} - S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY} - S3_BUCKET_NAME=esclassifier-test From 99c2d493f55800b6bb2e9fce5ab2fa995524c2fa Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 26 Jul 2024 04:31:37 +0530 Subject: [PATCH 294/582] renabling auth --- dataset-processor/dataset_processor_api.py | 6 +++--- file-handler/file_handler_api.py | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/dataset-processor/dataset_processor_api.py b/dataset-processor/dataset_processor_api.py index 174b5608..d41494fb 100644 --- a/dataset-processor/dataset_processor_api.py +++ b/dataset-processor/dataset_processor_api.py @@ -31,9 +31,9 @@ async def authenticate_user(request: Request): @app.post("/init-dataset-process") async def process_handler_endpoint(process_request: ProcessHandlerRequest): - # await authenticate_user(request) - # authCookie = request.cookies.get("customJwtCookie") - result = processor.process_handler(process_request.dgID, process_request.cookie, process_request.updateType, process_request.savedFilePath, process_request.patchPayload) + await authenticate_user(process_request) + authCookie = process_request.cookies.get("customJwtCookie") + result = processor.process_handler(process_request.dgID, authCookie, process_request.updateType, process_request.savedFilePath, process_request.patchPayload) if result: return result else: diff --git a/file-handler/file_handler_api.py b/file-handler/file_handler_api.py index 589b58b5..62b96fd3 100644 --- a/file-handler/file_handler_api.py +++ b/file-handler/file_handler_api.py @@ -62,8 +62,7 @@ async def authenticate_user(request: Request): } response = requests.get(url, headers=headers) - # if response.status_code != 200: - if False: + if response.status_code != 200: raise HTTPException(status_code=response.status_code, detail="Authentication failed") @app.post("/datasetgroup/data/import") From 9b427f99c63352ce3bc1864df10c6f6601836b1d Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 26 Jul 2024 09:21:30 +0530 Subject: [PATCH 295/582] enrichment updates --- dataset-processor/Dockerfile | 2 +- dataset-processor/data_enrichment/requirements.txt | 5 ++++- dataset-processor/dataset_processor.py | 7 ++++--- dataset-processor/requirements.txt | 8 ++++---- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/dataset-processor/Dockerfile b/dataset-processor/Dockerfile index 05d9a435..7187e2e5 100644 --- a/dataset-processor/Dockerfile +++ b/dataset-processor/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9-slim +FROM python:3.10-alpine WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt diff --git a/dataset-processor/data_enrichment/requirements.txt b/dataset-processor/data_enrichment/requirements.txt index ad22e047..690d16ef 100644 --- a/dataset-processor/data_enrichment/requirements.txt +++ b/dataset-processor/data_enrichment/requirements.txt @@ -1,5 +1,8 @@ fastapi pydantic requests +boto3 langdetect -transformers \ No newline at end of file +transformers +torch +sentencepiece \ No newline at end of file diff --git a/dataset-processor/dataset_processor.py b/dataset-processor/dataset_processor.py index 15efc60b..f0ba288b 100644 --- a/dataset-processor/dataset_processor.py +++ b/dataset-processor/dataset_processor.py @@ -2,7 +2,7 @@ import os import json import requests -# from data_enrichment.data_enrichment import DataEnrichment +from data_enrichment.data_enrichment import DataEnrichment from constants import * from s3_mock import S3FileCounter @@ -18,6 +18,7 @@ class DatasetProcessor: def __init__(self): + self.data_enricher = DataEnrichment() self.s3_file_counter = S3FileCounter() def check_and_convert(self, data): @@ -81,8 +82,8 @@ def enrich_data(self, data, selected_fields, record_count): enriched_entry = {} for key, value in entry.items(): if isinstance(value, str) and (key in selected_fields): - # enriched_value = self.data_enricher.enrich_data(value, num_return_sequences=1, language_id='en') - enriched_value = ["enrichupdate"] + enriched_value = self.data_enricher.enrich_data(value, num_return_sequences=1, language_id='en') + # enriched_value = ["enrichupdate"] enriched_entry[key] = enriched_value[0] if enriched_value else value else: enriched_entry[key] = value diff --git a/dataset-processor/requirements.txt b/dataset-processor/requirements.txt index 2f6921cd..690d16ef 100644 --- a/dataset-processor/requirements.txt +++ b/dataset-processor/requirements.txt @@ -2,7 +2,7 @@ fastapi pydantic requests boto3 -# langdetect -# transformers -# torch -# sentencepiece \ No newline at end of file +langdetect +transformers +torch +sentencepiece \ No newline at end of file From b6df67ae3f90f8692549784d0ff05297511b7ef1 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 26 Jul 2024 09:36:57 +0530 Subject: [PATCH 296/582] disable data enrichment --- dataset-processor/dataset_processor.py | 8 ++++---- dataset-processor/requirements.txt | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/dataset-processor/dataset_processor.py b/dataset-processor/dataset_processor.py index f0ba288b..52289c9d 100644 --- a/dataset-processor/dataset_processor.py +++ b/dataset-processor/dataset_processor.py @@ -2,7 +2,7 @@ import os import json import requests -from data_enrichment.data_enrichment import DataEnrichment +# from data_enrichment.data_enrichment import DataEnrichment from constants import * from s3_mock import S3FileCounter @@ -18,7 +18,7 @@ class DatasetProcessor: def __init__(self): - self.data_enricher = DataEnrichment() + # self.data_enricher = DataEnrichment() self.s3_file_counter = S3FileCounter() def check_and_convert(self, data): @@ -82,8 +82,8 @@ def enrich_data(self, data, selected_fields, record_count): enriched_entry = {} for key, value in entry.items(): if isinstance(value, str) and (key in selected_fields): - enriched_value = self.data_enricher.enrich_data(value, num_return_sequences=1, language_id='en') - # enriched_value = ["enrichupdate"] + # enriched_value = self.data_enricher.enrich_data(value, num_return_sequences=1, language_id='en') + enriched_value = ["enrichupdate"] enriched_entry[key] = enriched_value[0] if enriched_value else value else: enriched_entry[key] = value diff --git a/dataset-processor/requirements.txt b/dataset-processor/requirements.txt index 690d16ef..2f6921cd 100644 --- a/dataset-processor/requirements.txt +++ b/dataset-processor/requirements.txt @@ -2,7 +2,7 @@ fastapi pydantic requests boto3 -langdetect -transformers -torch -sentencepiece \ No newline at end of file +# langdetect +# transformers +# torch +# sentencepiece \ No newline at end of file From 5cd0a25ca737e0f7eac324ab320c544b26616148 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 26 Jul 2024 09:51:59 +0530 Subject: [PATCH 297/582] gitignore --- .gitignore | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index e47aea08..3525077b 100644 --- a/.gitignore +++ b/.gitignore @@ -398,5 +398,4 @@ FodyWeavers.xsd *.sln.iml /tim-db -/data -/DSL/Liquibase/ \ No newline at end of file +/data \ No newline at end of file From 3c222e7f78526e7f8460143cab31d317f225e5e0 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 26 Jul 2024 10:02:00 +0530 Subject: [PATCH 298/582] update --- dataset-processor/dataset_processor.py | 28 +++++++++++++++----------- docker-compose.yml | 1 + 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/dataset-processor/dataset_processor.py b/dataset-processor/dataset_processor.py index 52289c9d..5605f535 100644 --- a/dataset-processor/dataset_processor.py +++ b/dataset-processor/dataset_processor.py @@ -7,6 +7,7 @@ from s3_mock import S3FileCounter RUUTER_PRIVATE_URL = os.getenv("RUUTER_PRIVATE_URL") +GET_VALIDATION_SCHEMA = os.getenv("GET_VALIDATION_SCHEMA") FILE_HANDLER_DOWNLOAD_JSON_URL = os.getenv("FILE_HANDLER_DOWNLOAD_JSON_URL") FILE_HANDLER_STOPWORDS_URL = os.getenv("FILE_HANDLER_STOPWORDS_URL") FILE_HANDLER_IMPORT_CHUNKS_URL = os.getenv("FILE_HANDLER_IMPORT_CHUNKS_URL") @@ -125,24 +126,27 @@ def save_chunked_data(self, chunked_data, cookie, dgID, exsistingChunks=0): return True - def get_selected_data_fields(self, dgID:int): + def get_selected_data_fields(self, dgID:int, cookie:str): try: - return ["Subject","Body"] - # data_dict = self.get_validation_data(dgID) - # validation_rules = data_dict.get("response", {}).get("validationCriteria", {}).get("validationRules", {}) - # text_fields = [] - # for field, rules in validation_rules.items(): - # if rules.get("type") == "text" and rules.get("isDataClass")!=True: - # text_fields.append(field) - # return text_fields + # return ["Subject","Body"] + data_dict = self.get_validation_data(dgID, cookie) + validation_rules = data_dict.get("response", {}).get("validationCriteria", {}).get("validationRules", {}) + text_fields = [] + for field, rules in validation_rules.items(): + if rules.get("type") == "text" and rules.get("isDataClass")!=True: + text_fields.append(field) + return text_fields except Exception as e: print(e) return None - def get_validation_data(self, dgID): + def get_validation_data(self, dgID, custom_jwt_cookie): try: params = {'dgId': dgID} - response = requests.get(RUUTER_PRIVATE_URL, params=params) + headers = { + 'cookie': f'customJwtCookie={custom_jwt_cookie}' + } + response = requests.get(GET_VALIDATION_SCHEMA, params=params, headers=headers) response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: @@ -296,7 +300,7 @@ def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload) structured_data = self.check_and_convert(dataset) if structured_data is not None: print("Dataset converted successfully") - selected_data_fields_to_enrich = self.get_selected_data_fields(dgID) + selected_data_fields_to_enrich = self.get_selected_data_fields(dgID, cookie) if selected_data_fields_to_enrich is not None: print("Selected data fields to enrich retrieved successfully") max_row_id = max(item["rowID"] for item in structured_data) diff --git a/docker-compose.yml b/docker-compose.yml index d5325940..dfd24826 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -221,6 +221,7 @@ services: container_name: dataset-processor environment: - RUUTER_PRIVATE_URL=http://ruuter-private:8088 + - GET_VALIDATION_SCHEMA=http://ruuter-private:8088/classifier/datasetgroup/schema - FILE_HANDLER_DOWNLOAD_JSON_URL=http://file-handler:8000/datasetgroup/data/download/json - FILE_HANDLER_DOWNLOAD_LOCATION_JSON_URL=http://file-handler:8000/datasetgroup/data/download/json/location - FILE_HANDLER_STOPWORDS_URL=http://file-handler:8000/datasetgroup/data/download/json/stopwords From 13549414a31548c91a9a5a589ddaf12958ac66ed Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 26 Jul 2024 10:09:58 +0530 Subject: [PATCH 299/582] update --- dataset-processor/dataset_processor_api.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/dataset-processor/dataset_processor_api.py b/dataset-processor/dataset_processor_api.py index d41494fb..111495ed 100644 --- a/dataset-processor/dataset_processor_api.py +++ b/dataset-processor/dataset_processor_api.py @@ -1,4 +1,5 @@ from fastapi import FastAPI, HTTPException, Request +from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel from dataset_processor import DatasetProcessor import requests @@ -8,6 +9,14 @@ processor = DatasetProcessor() RUUTER_PRIVATE_URL = os.getenv("RUUTER_PRIVATE_URL") +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + class ProcessHandlerRequest(BaseModel): dgID: int cookie: str From c58e151226e4394134a8bcfeb5369d61ce7b7c12 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Fri, 26 Jul 2024 10:44:16 +0530 Subject: [PATCH 300/582] dataset groups integration --- GUI/package-lock.json | 91 ++ GUI/package.json | 1 + GUI/src/App.tsx | 5 +- GUI/src/components/Button/Button.scss | 1 + GUI/src/components/DataTable/index.tsx | 4 +- GUI/src/components/FileUpload/index.tsx | 63 ++ .../FormElements/DynamicForm/index.tsx | 52 ++ .../FormCheckbox/FormCheckbox.scss | 2 +- .../FormElements/FormCheckbox/index.tsx | 6 +- .../FormElements/FormRadios/FormRadios.scss | 5 +- .../FormElements/FormRadios/index.tsx | 5 +- GUI/src/components/LabelChip/index.scss | 20 + GUI/src/components/LabelChip/index.tsx | 25 + GUI/src/components/ProgressBar/index.scss | 28 + GUI/src/components/ProgressBar/index.tsx | 26 + .../molecules/ClassHeirarchy/index.tsx | 33 +- .../molecules/DatasetGroupCard/index.tsx | 65 +- .../{index.tsx => CardsView.tsx} | 26 +- .../molecules/ValidationCriteria/RowsView.tsx | 149 ++++ GUI/src/config/dataTypesConfig.json | 10 +- GUI/src/config/formatsConfig.json | 6 + GUI/src/config/importOptionsConfig.json | 6 + .../DatasetGroups/CreateDatasetGroup.tsx | 85 +- .../pages/DatasetGroups/ViewDatasetGroup.tsx | 820 +++++++++++++++--- GUI/src/pages/DatasetGroups/index.tsx | 51 +- GUI/src/pages/Integrations/index.tsx | 9 +- GUI/src/pages/StopWords/index.tsx | 140 +++ GUI/src/pages/ValidationSessions/index.tsx | 22 + GUI/src/services/api-dev.ts | 2 - GUI/src/services/api-external.ts | 36 + GUI/src/services/api-mock.ts | 4 +- GUI/src/services/api.ts | 2 - GUI/src/services/datasets.ts | 114 ++- GUI/src/styles/generic/_base.scss | 14 + GUI/src/types/common.ts | 6 + GUI/src/types/datasetGroups.ts | 54 +- GUI/src/utils/datasetGroupsUtils.ts | 101 ++- GUI/translations/en/common.json | 2 +- 38 files changed, 1802 insertions(+), 289 deletions(-) create mode 100644 GUI/src/components/FileUpload/index.tsx create mode 100644 GUI/src/components/FormElements/DynamicForm/index.tsx create mode 100644 GUI/src/components/LabelChip/index.scss create mode 100644 GUI/src/components/LabelChip/index.tsx create mode 100644 GUI/src/components/ProgressBar/index.scss create mode 100644 GUI/src/components/ProgressBar/index.tsx rename GUI/src/components/molecules/ValidationCriteria/{index.tsx => CardsView.tsx} (85%) create mode 100644 GUI/src/components/molecules/ValidationCriteria/RowsView.tsx create mode 100644 GUI/src/config/formatsConfig.json create mode 100644 GUI/src/config/importOptionsConfig.json create mode 100644 GUI/src/pages/StopWords/index.tsx create mode 100644 GUI/src/pages/ValidationSessions/index.tsx create mode 100644 GUI/src/services/api-external.ts create mode 100644 GUI/src/types/common.ts diff --git a/GUI/package-lock.json b/GUI/package-lock.json index 815d8a8f..1453e606 100644 --- a/GUI/package-lock.json +++ b/GUI/package-lock.json @@ -16,6 +16,7 @@ "@radix-ui/react-collapsible": "^1.0.1", "@radix-ui/react-dialog": "^1.0.2", "@radix-ui/react-popover": "^1.0.2", + "@radix-ui/react-progress": "^1.1.0", "@radix-ui/react-select": "^1.1.2", "@radix-ui/react-switch": "^1.0.1", "@radix-ui/react-tabs": "^1.0.1", @@ -5354,6 +5355,96 @@ } } }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.0.tgz", + "integrity": "sha512-aSzvnYpP725CROcxAOEBVZZSIQVQdHgBr2QQFKySsaD14u8dNT0batuXI+AAGDdAHfXH8rbnHmjYFqVJ21KkRg==", + "dependencies": { + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-primitive": "2.0.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", + "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-context": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz", + "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", + "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "dependencies": { + "@radix-ui/react-slot": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz", diff --git a/GUI/package.json b/GUI/package.json index 646b1064..1016bbc9 100644 --- a/GUI/package.json +++ b/GUI/package.json @@ -19,6 +19,7 @@ "@radix-ui/react-collapsible": "^1.0.1", "@radix-ui/react-dialog": "^1.0.2", "@radix-ui/react-popover": "^1.0.2", + "@radix-ui/react-progress": "^1.1.0", "@radix-ui/react-select": "^1.1.2", "@radix-ui/react-switch": "^1.0.1", "@radix-ui/react-tabs": "^1.0.1", diff --git a/GUI/src/App.tsx b/GUI/src/App.tsx index eff08272..c82550fe 100644 --- a/GUI/src/App.tsx +++ b/GUI/src/App.tsx @@ -10,6 +10,8 @@ import { useQuery } from '@tanstack/react-query'; import { UserInfo } from 'types/userInfo'; import CreateDatasetGroup from 'pages/DatasetGroups/CreateDatasetGroup'; import ViewDatasetGroup from 'pages/DatasetGroups/ViewDatasetGroup'; +import StopWords from 'pages/StopWords'; +import ValidationSessions from 'pages/ValidationSessions'; const App: FC = () => { @@ -31,7 +33,8 @@ const App: FC = () => { } /> } /> } /> - } /> + } /> + } /> diff --git a/GUI/src/components/Button/Button.scss b/GUI/src/components/Button/Button.scss index fa0b5b11..fc21cab1 100644 --- a/GUI/src/components/Button/Button.scss +++ b/GUI/src/components/Button/Button.scss @@ -21,6 +21,7 @@ line-height: 24px; border-radius: 20px; white-space: nowrap; + height: fit-content; &:focus { outline: none; diff --git a/GUI/src/components/DataTable/index.tsx b/GUI/src/components/DataTable/index.tsx index 54727a7f..32445976 100644 --- a/GUI/src/components/DataTable/index.tsx +++ b/GUI/src/components/DataTable/index.tsx @@ -233,7 +233,7 @@ const DataTable: FC = (
    )} -
    + {/*
    -
    +
    */}
    )} diff --git a/GUI/src/components/FileUpload/index.tsx b/GUI/src/components/FileUpload/index.tsx new file mode 100644 index 00000000..ba0ae071 --- /dev/null +++ b/GUI/src/components/FileUpload/index.tsx @@ -0,0 +1,63 @@ +import { FormInput } from 'components/FormElements'; +import React, { + useState, + ChangeEvent, + forwardRef, + useImperativeHandle, + Ref, +} from 'react'; + +type FileUploadProps = { + label?: string; + onFileSelect: (file: File | null) => void; + accept?: string; + disabled?: boolean; +}; + +export type FileUploadHandle = { + clearFile: () => void; +}; + +const FileUpload = forwardRef( + (props: FileUploadProps, ref: Ref) => { + const { onFileSelect, accept,disabled } = props; + const [selectedFile, setSelectedFile] = useState(null); + + useImperativeHandle(ref, () => ({ + clearFile() { + setSelectedFile(null); + onFileSelect(null); + }, + })); + + const handleFileChange = (e: ChangeEvent) => { + const file = e.target.files ? e.target.files[0] : null; + setSelectedFile(file); + onFileSelect(file); + }; + + const restrictFormat = (accept: string) => { + if (accept === 'json') return '.json'; + else if (accept === 'xlsx') return '.xlsx'; + else if (accept === 'yaml') return '.yaml, .yml'; + }; + + return ( +
    + + + + +
    + ); + } +); + +export default FileUpload; diff --git a/GUI/src/components/FormElements/DynamicForm/index.tsx b/GUI/src/components/FormElements/DynamicForm/index.tsx new file mode 100644 index 00000000..b1adff6f --- /dev/null +++ b/GUI/src/components/FormElements/DynamicForm/index.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { useForm } from 'react-hook-form'; +import FormInput from '../FormInput'; +import Button from 'components/Button'; +import Track from 'components/Track'; + +type DynamicFormProps = { + formData: { [key: string]: string }; + onSubmit: (data: any) => void; + setPatchUpdateModalOpen: React.Dispatch> +}; + +const DynamicForm: React.FC = ({ formData, onSubmit,setPatchUpdateModalOpen }) => { + const { register, handleSubmit } = useForm(); + + const renderInput = (key: string, type: string) => { + + + return ( + + ); + }; + + return ( +
    + {Object.keys(formData).map((key) => ( +
    + {key.toLowerCase() !== 'rowid' && ( +
    + + {renderInput(key, formData[key])} +
    + )} +
    + ))} + +
    + + +
    + +
    + ); +}; + +export default DynamicForm; diff --git a/GUI/src/components/FormElements/FormCheckbox/FormCheckbox.scss b/GUI/src/components/FormElements/FormCheckbox/FormCheckbox.scss index 2cb0046d..613f4e6a 100644 --- a/GUI/src/components/FormElements/FormCheckbox/FormCheckbox.scss +++ b/GUI/src/components/FormElements/FormCheckbox/FormCheckbox.scss @@ -6,7 +6,7 @@ .checkbox { width: 100%; display: flex; - align-items: flex-start; + align-items: center; gap: get-spacing(paldiski); &__label { diff --git a/GUI/src/components/FormElements/FormCheckbox/index.tsx b/GUI/src/components/FormElements/FormCheckbox/index.tsx index 0ce1ba33..66645255 100644 --- a/GUI/src/components/FormElements/FormCheckbox/index.tsx +++ b/GUI/src/components/FormElements/FormCheckbox/index.tsx @@ -26,11 +26,11 @@ const FormCheckbox = forwardRef(( const uid = useId(); return ( -
    +
    {label && !hideLabel && }
    - - + +
    ); diff --git a/GUI/src/components/FormElements/FormRadios/FormRadios.scss b/GUI/src/components/FormElements/FormRadios/FormRadios.scss index ac5f24c6..72862bc1 100644 --- a/GUI/src/components/FormElements/FormRadios/FormRadios.scss +++ b/GUI/src/components/FormElements/FormRadios/FormRadios.scss @@ -18,10 +18,13 @@ &__wrapper { display: flex; - flex-direction: column; gap: 8px; } + &__stack { + gap: 8px; + } + &__item { input[type=radio] { display: none; diff --git a/GUI/src/components/FormElements/FormRadios/index.tsx b/GUI/src/components/FormElements/FormRadios/index.tsx index e9357a78..619a961a 100644 --- a/GUI/src/components/FormElements/FormRadios/index.tsx +++ b/GUI/src/components/FormElements/FormRadios/index.tsx @@ -11,15 +11,16 @@ type FormRadiosType = { value: string; }[]; onChange: (selectedValue: string) => void; + isStack?: boolean; } -const FormRadios: FC = ({ label, name, hideLabel, items, onChange }) => { +const FormRadios: FC = ({ label, name, hideLabel, items, onChange,isStack=false }) => { const id = useId(); return (
    {label && !hideLabel && } -
    +
    {items.map((item, index) => (
    { diff --git a/GUI/src/components/LabelChip/index.scss b/GUI/src/components/LabelChip/index.scss new file mode 100644 index 00000000..9e49c645 --- /dev/null +++ b/GUI/src/components/LabelChip/index.scss @@ -0,0 +1,20 @@ +.label-chip { + display: inline-flex; + align-items: center; + padding: 4px 15px; + border-radius: 16px; + background-color: #e0e0e0; + margin: 4px; + } + + .label-chip .label { + margin-right: 8px; + } + + .label-chip .button { + background: none; + border: none; + cursor: pointer; + display: flex; + align-items: center; + } \ No newline at end of file diff --git a/GUI/src/components/LabelChip/index.tsx b/GUI/src/components/LabelChip/index.tsx new file mode 100644 index 00000000..146e80c1 --- /dev/null +++ b/GUI/src/components/LabelChip/index.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import './index.scss'; +import { MdClose } from 'react-icons/md'; + +type LabelChipProps = { + label: string; + onRemove: () => void; +}; + +const LabelChip: React.FC = ({ label, onRemove }) => { + return ( +
    + {label} + +
    + ); +}; + +export default LabelChip; diff --git a/GUI/src/components/ProgressBar/index.scss b/GUI/src/components/ProgressBar/index.scss new file mode 100644 index 00000000..bc4f3a53 --- /dev/null +++ b/GUI/src/components/ProgressBar/index.scss @@ -0,0 +1,28 @@ +.progress-bar-container { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + } + + .progress-bar-label { + margin-bottom: 4px; + font-size: 14px; + } + + .progress-bar-root { + position: relative; + overflow: hidden; + background-color: #e0e0e0; + border-radius: 4px; + width: 100%; + height: 10px; + } + + .progress-bar-indicator { + background-color: #07478d; + height: 100%; + transition: width 0.3s; + border-radius: 20px; + } + \ No newline at end of file diff --git a/GUI/src/components/ProgressBar/index.tsx b/GUI/src/components/ProgressBar/index.tsx new file mode 100644 index 00000000..20f3b328 --- /dev/null +++ b/GUI/src/components/ProgressBar/index.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import * as Progress from '@radix-ui/react-progress'; +import './index.scss'; // Import your CSS file for custom styles + +type ProgressBarProps = { + value: number; + max: number; + label?: string; +}; + +const ProgressBar: React.FC = ({ value, max, label }) => { + return ( +
    + + + + {label && } + +
    + ); +}; + +export default ProgressBar; diff --git a/GUI/src/components/molecules/ClassHeirarchy/index.tsx b/GUI/src/components/molecules/ClassHeirarchy/index.tsx index 2601ad44..8fb09dfe 100644 --- a/GUI/src/components/molecules/ClassHeirarchy/index.tsx +++ b/GUI/src/components/molecules/ClassHeirarchy/index.tsx @@ -7,6 +7,7 @@ import { v4 as uuidv4 } from 'uuid'; import './index.css'; import Dialog from 'components/Dialog'; import { Class } from 'types/datasetGroups'; +import { isClassHierarchyDuplicated } from 'utils/datasetGroupsUtils'; type ClassHierarchyProps = { nodes?: Class[]; @@ -30,6 +31,10 @@ const ClassHierarchy: FC> = ({ const handleChange = (e) => { setFieldName(e.target.value); node.fieldName = e.target.value; + if(isClassHierarchyDuplicated(nodes,e.target.value)) + setNodesError(true) + else + setNodesError(false) }; return ( @@ -48,7 +53,13 @@ const ClassHierarchy: FC> = ({ placeholder="Enter Field Name" value={fieldName} onChange={handleChange} - error={nodesError && !fieldName ? 'Enter a field name' : ''} + error={ + nodesError && !fieldName + ? 'Enter a field name' + : fieldName && isClassHierarchyDuplicated(nodes, fieldName) + ? 'Class name already exists' + : '' + } />
    > = ({ const addSubClass = (parentId) => { const addSubClassRecursive = (nodes) => { - return nodes.map((node) => { + return nodes?.map((node) => { if (node.id === parentId) { const newNode = { id: uuidv4(), @@ -136,7 +147,7 @@ const ClassHierarchy: FC> = ({ const confirmDeleteNode = () => { const deleteNodeRecursive = (nodes) => { - return nodes.filter((node) => { + return nodes?.filter((node) => { if (node.id === currentNode.id) { return false; // Remove this node } @@ -154,11 +165,10 @@ const ClassHierarchy: FC> = ({ return (
    -
    Class Hierarchy
    - +
    - {nodes.map((node) => ( + {nodes?.map((node) => ( > = ({ /> ))}
    - +
    - + @@ -181,7 +196,7 @@ const ClassHierarchy: FC> = ({ } onClose={() => setIsModalOpen(false)} > - Confirm that you are wish to delete the following record + Confirm that you are wish to delete the following record. This will delete the current class and all subclasses of it
    ); diff --git a/GUI/src/components/molecules/DatasetGroupCard/index.tsx b/GUI/src/components/molecules/DatasetGroupCard/index.tsx index 4bd488b7..e4a576d4 100644 --- a/GUI/src/components/molecules/DatasetGroupCard/index.tsx +++ b/GUI/src/components/molecules/DatasetGroupCard/index.tsx @@ -6,9 +6,12 @@ import Label from 'components/Label'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { enableDataset } from 'services/datasets'; import { useDialog } from 'hooks/useDialog'; +import { createSearchParams, URLSearchParamsInit, useNavigate } from 'react-router-dom'; +import { Operation } from 'types/datasetGroups'; +import { AxiosError } from 'axios'; type DatasetGroupCardProps = { - datasetGroupId?: number; + datasetGroupId?: number|string|undefined; datasetName?: string; version?: string; isLatest?: boolean; @@ -18,6 +21,9 @@ type DatasetGroupCardProps = { lastUsed?: string; validationStatus?: string; lastModelTrained?: string; + setId?: React.Dispatch> + setView?: React.Dispatch> + }; const DatasetGroupCard: FC> = ({ @@ -31,49 +37,46 @@ const DatasetGroupCard: FC> = ({ lastUsed, validationStatus, lastModelTrained, + setId, + setView }) => { const queryClient = useQueryClient(); const { open } = useDialog(); - - const renderValidationStatus = (status:string) => { - if (status === 'successful') { + + const renderValidationStatus = (status:string|undefined) => { + if (status === 'success') { return ; - } else if (status === 'failed') { + } else if (status === 'fail') { return ; - } else if (status === 'pending') { - return ; - } else if (status === 'in_progress') { + } else if (status === 'unvalidated') { + return ; + } else if (status === 'in-progress') { return ; } }; const datasetEnableMutation = useMutation({ - mutationFn: (data) => enableDataset(data), + mutationFn: (data:Operation) => enableDataset(data), onSuccess: async (response) => { - await queryClient.invalidateQueries(['GET/datasetgroup/overview', 1]); - if (response?.operationSuccessful) - open({ - title: 'Cannot Enable Dataset Group', - content: ( -

    - The dataset group cannot be enabled until data is added. Please - add datasets to this group and try again. -

    - ), - }); + await queryClient.invalidateQueries(['datasetgroup/overview', 1]); }, - onError: () => { + onError: (error) => { open({ - title: 'Operation Unsuccessful', - content:

    Something went wrong. Please try again.

    , + title: 'Cannot Enable Dataset Group', + content: ( +

    + The dataset group cannot be enabled until data is added. Please + add datasets to this group and try again. +

    + ), }); }, }); const datasetDisableMutation = useMutation({ - mutationFn: (data) => enableDataset(data), + mutationFn: (data:Operation) => enableDataset(data), onSuccess: async (response) => { - await queryClient.invalidateQueries(['GET/datasetgroup/overview', 1]); + await queryClient.invalidateQueries(['datasetgroup/overview', 1]); if (response?.operationSuccessful) open({ title: 'Cannot Enable Dataset Group', @@ -85,7 +88,7 @@ const DatasetGroupCard: FC> = ({ ), }); }, - onError: (error: AxiosError) => { + onError: () => { open({ title: 'Operation Unsuccessful', content:

    Something went wrong. Please try again.

    , @@ -110,7 +113,7 @@ const DatasetGroupCard: FC> = ({
    -

    {datasetName}

    +
    {datasetName}
    > = ({
    {renderValidationStatus(validationStatus)}
    -

    +

    {'Last Model Trained:'} {lastModelTrained}

    -

    +

    {'Last Used For Training:'} {lastUsed}

    -

    +

    {'Last Updated:'} {lastUpdated}

    @@ -138,7 +141,7 @@ const DatasetGroupCard: FC> = ({
    -
    diff --git a/GUI/src/components/molecules/ValidationCriteria/index.tsx b/GUI/src/components/molecules/ValidationCriteria/CardsView.tsx similarity index 85% rename from GUI/src/components/molecules/ValidationCriteria/index.tsx rename to GUI/src/components/molecules/ValidationCriteria/CardsView.tsx index 6101ca92..2d7f5fca 100644 --- a/GUI/src/components/molecules/ValidationCriteria/index.tsx +++ b/GUI/src/components/molecules/ValidationCriteria/CardsView.tsx @@ -8,6 +8,7 @@ import { FormCheckbox, FormInput, FormSelect } from 'components/FormElements'; import Button from 'components/Button'; import { ValidationRule } from 'types/datasetGroups'; import { Link } from 'react-router-dom'; +import { isFieldNameExisting } from 'utils/datasetGroupsUtils'; const ItemTypes = { ITEM: 'item', @@ -19,14 +20,18 @@ type ValidationRulesProps = { validationRuleError?: boolean; setValidationRuleError: React.Dispatch>; }; -const ValidationCriteria: FC> = ({ +const ValidationCriteriaCardsView: FC< + PropsWithChildren +> = ({ validationRules, setValidationRules, setValidationRuleError, validationRuleError, }) => { + + const setIsDataClass = (id, isDataClass) => { - const updatedItems = validationRules.map((item) => + const updatedItems = validationRules?.map((item) => item.id === id ? { ...item, isDataClass: !isDataClass } : item ); setValidationRules(updatedItems); @@ -76,6 +81,11 @@ const ValidationCriteria: FC> = ({ error={ validationRuleError && !item.fieldName ? 'Enter a field name' + : validationRuleError && + item.fieldName && + item?.fieldName.toString().toLocaleLowerCase() === 'rowid' + ? `${item?.fieldName} cannot be used as a field name` + : item.fieldName && isFieldNameExisting(validationRules,item?.fieldName)?`${item?.fieldName} alreday exist as field name` : '' } /> @@ -97,10 +107,10 @@ const ValidationCriteria: FC> = ({ style={{ display: 'flex', justifyContent: 'end', gap: '10px' }} > deleteItem(item.id)} - className='link' + className="link" > Delete @@ -114,8 +124,9 @@ const ValidationCriteria: FC> = ({ name="dataClass" checked={item.isDataClass} onChange={() => setIsDataClass(item.id, item.isDataClass)} + style={{width:"150px"}} /> - +
    @@ -131,7 +142,7 @@ const ValidationCriteria: FC> = ({ }; const addNewClass = () => { - setValidationRuleError(false) + setValidationRuleError(false); const newId = validationRules[validationRules?.length - 1]?.id + 1; const updatedItems = [ ...validationRules, @@ -161,9 +172,8 @@ const ValidationCriteria: FC> = ({
    - ); }; -export default ValidationCriteria; +export default ValidationCriteriaCardsView; diff --git a/GUI/src/components/molecules/ValidationCriteria/RowsView.tsx b/GUI/src/components/molecules/ValidationCriteria/RowsView.tsx new file mode 100644 index 00000000..d76cc228 --- /dev/null +++ b/GUI/src/components/molecules/ValidationCriteria/RowsView.tsx @@ -0,0 +1,149 @@ +import React, { FC, PropsWithChildren, useCallback } from 'react'; +import { DndProvider, useDrag, useDrop } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; +import dataTypes from '../../../config/dataTypesConfig.json'; +import { MdAdd, MdDehaze, MdDelete } from 'react-icons/md'; +import Card from 'components/Card'; +import { FormCheckbox, FormInput, FormSelect } from 'components/FormElements'; +import Button from 'components/Button'; +import { ValidationRule } from 'types/datasetGroups'; +import { Link } from 'react-router-dom'; +import { isFieldNameExisting } from 'utils/datasetGroupsUtils'; + +type ValidationRulesProps = { + validationRules?: ValidationRule[]; + setValidationRules: React.Dispatch>; + validationRuleError?: boolean; + setValidationRuleError: React.Dispatch>; +}; +const ValidationCriteriaRowsView: FC> = ({ + validationRules, + setValidationRules, + setValidationRuleError, + validationRuleError, +}) => { + const setIsDataClass = (id, isDataClass) => { + const updatedItems = validationRules.map((item) => + item.id === id ? { ...item, isDataClass: !isDataClass } : item + ); + setValidationRules(updatedItems); + }; + + const changeName = (id, newValue) => { + setValidationRules((prevData) => + prevData.map((item) => + item.id === id ? { ...item, fieldName: newValue } : item + ) + ); + + if(isFieldNameExisting(validationRuleError,newValue)) + setValidationRuleError(true); + else + setValidationRuleError(false) + } + + const changeDataType = (id, value) => { + const updatedItems = validationRules.map((item) => + item.id === id ? { ...item, dataType: value } : item + ); + setValidationRules(updatedItems); + }; + + const addNewClass = () => { + setValidationRuleError(false) + const newId = validationRules[validationRules?.length - 1]?.id + 1; + + const updatedItems = [ + ...validationRules, + { id: newId, fieldName: '', dataType: '', isDataClass: false }, + ]; + + + setValidationRules(updatedItems); + }; + + const deleteItem = (idToDelete) => { + const updatedItems = validationRules.filter( + (item) => item.id !== idToDelete + ); + setValidationRules(updatedItems); + }; + + return ( +
    + {validationRules?.map((item, index) => ( +
    + changeName(item.id, e.target.value)} + error={ + validationRuleError && !item.fieldName + ? 'Enter a field name' + : validationRuleError && + item.fieldName && + item?.fieldName.toString().toLocaleLowerCase() === 'rowid' + ? `${item?.fieldName} cannot be used as a field name` + : item.fieldName && isFieldNameExisting(validationRules,item?.fieldName)?`${item?.fieldName} alreday exist as field name` + : '' + } + /> + + changeDataType(item.id, selection?.value) + } + error={ + validationRuleError && !item.dataType + ? 'Select a data type' + : '' + } + /> +
    + addNewClass()} + className='link' + > + + Add + + deleteItem(item.id)} + className='link' + > + + Delete + + setIsDataClass(item.id, item.isDataClass)} + style={{width:"150px"}} + /> +
    +
    + ))} + + +
    + ); +}; + +export default ValidationCriteriaRowsView; diff --git a/GUI/src/config/dataTypesConfig.json b/GUI/src/config/dataTypesConfig.json index 6bb681b2..fa993ac3 100644 --- a/GUI/src/config/dataTypesConfig.json +++ b/GUI/src/config/dataTypesConfig.json @@ -1,6 +1,10 @@ [ - { "label": "Text", "value": "TEXT" }, - { "label": "Numbers", "value": "NUMBER" }, - { "label": "Date Time", "value": "DATETIME" } + { "label": "Text", "value": "text" }, + { "label": "Numbers", "value": "numbers" }, + { "label": "Date Time", "value": "datetime" }, + { "label": "Email", "value": "email" }, + { "label": "File Attachments", "value": "file_attachments" } + + ] \ No newline at end of file diff --git a/GUI/src/config/formatsConfig.json b/GUI/src/config/formatsConfig.json new file mode 100644 index 00000000..1c0a3ed9 --- /dev/null +++ b/GUI/src/config/formatsConfig.json @@ -0,0 +1,6 @@ +[ + { "label": "XLSX", "value": "xlsx" }, + { "label": "JSON", "value": "json" }, + { "label": "YAML", "value": "yaml" } + + ] \ No newline at end of file diff --git a/GUI/src/config/importOptionsConfig.json b/GUI/src/config/importOptionsConfig.json new file mode 100644 index 00000000..280cd15b --- /dev/null +++ b/GUI/src/config/importOptionsConfig.json @@ -0,0 +1,6 @@ +[ + { "label": "Import to add", "value": "add" }, + { "label": "Import to update", "value": "update" }, + { "label": "Import to delete", "value": "delete" } + + ] \ No newline at end of file diff --git a/GUI/src/pages/DatasetGroups/CreateDatasetGroup.tsx b/GUI/src/pages/DatasetGroups/CreateDatasetGroup.tsx index 88fc01d0..7a352fbe 100644 --- a/GUI/src/pages/DatasetGroups/CreateDatasetGroup.tsx +++ b/GUI/src/pages/DatasetGroups/CreateDatasetGroup.tsx @@ -5,19 +5,22 @@ import { Button, Card, Dialog, FormInput } from 'components'; import { v4 as uuidv4 } from 'uuid'; import ClassHierarchy from 'components/molecules/ClassHeirarchy'; import { - getTimestampNow, isValidationRulesSatisfied, transformClassHierarchy, transformValidationRules, validateClassHierarchy, validateValidationRules, } from 'utils/datasetGroupsUtils'; -import ValidationCriteria from 'components/molecules/ValidationCriteria'; -import { ValidationRule } from 'types/datasetGroups'; +import { DatasetGroup, ValidationRule } from 'types/datasetGroups'; import { useNavigate } from 'react-router-dom'; +import ValidationCriteriaCardsView from 'components/molecules/ValidationCriteria/CardsView'; +import { useMutation } from '@tanstack/react-query'; +import { createDatasetGroup } from 'services/datasets'; +import { useDialog } from 'hooks/useDialog'; const CreateDatasetGroup: FC = () => { const { t } = useTranslation(); + const { open } = useDialog(); const [isModalOpen, setIsModalOpen] = useState(false); const [modalType, setModalType] = useState(''); const navigate = useNavigate(); @@ -44,38 +47,41 @@ const CreateDatasetGroup: FC = () => { const validateData = useCallback(() => { setNodesError(validateClassHierarchy(nodes)); - setDatasetNameError(!datasetName && true); + setDatasetNameError(!datasetName); setValidationRuleError(validateValidationRules(validationRules)); if ( !validateClassHierarchy(nodes) && datasetName && - true && !validateValidationRules(validationRules) ) { if (!isValidationRulesSatisfied(validationRules)) { setIsModalOpen(true); setModalType('VALIDATION_ERROR'); - }else{ - const payload = { - name: datasetName, - major_version: 1, - minor_version: 0, - patch_version: 0, - created_timestamp: getTimestampNow(), - last_updated_timestamp: getTimestampNow(), - ...transformValidationRules(validationRules), + } else { + const payload: DatasetGroup = { + groupName: datasetName, + validationCriteria: { ...transformValidationRules(validationRules) }, ...transformClassHierarchy(nodes), }; - - console.log(payload); - setIsModalOpen(true); - setModalType('SUCCESS'); + createDatasetGroupMutation.mutate(payload); } - } - }, [datasetName, nodes, validationRules]); + const createDatasetGroupMutation = useMutation({ + mutationFn: (data: DatasetGroup) => createDatasetGroup(data), + onSuccess: async (response) => { + setIsModalOpen(true); + setModalType('SUCCESS'); + }, + onError: () => { + open({ + title: 'Dataset Group Creation Unsuccessful', + content:

    Something went wrong. Please try again.

    , + }); + }, + }); + return (
    @@ -96,18 +102,24 @@ const CreateDatasetGroup: FC = () => { />
    - - + +
    Class Hierarchy
    + + {' '} + +
    {modalType === 'VALIDATION_ERROR' && ( { isOpen={isModalOpen} title={'Dataset Group Created Successfully'} footer={ -
    +
    -
    @@ -156,12 +168,21 @@ const CreateDatasetGroup: FC = () => { )}
    - +
    -
    ); diff --git a/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx b/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx index 93ea31cd..1c285522 100644 --- a/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx +++ b/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx @@ -1,164 +1,730 @@ -import { FC, useCallback, useState } from 'react'; +import { + FC, + PropsWithChildren, + SetStateAction, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; import './DatasetGroups.scss'; import { useTranslation } from 'react-i18next'; -import { Button, Card, Dialog, FormInput, FormSelect } from 'components'; -import DatasetGroupCard from 'components/molecules/DatasetGroupCard'; -import Pagination from 'components/molecules/Pagination'; -import { getDatasetsOverview } from 'services/datasets'; -import { useQuery } from '@tanstack/react-query'; -import DraggableRows from 'components/molecules/ValidationCriteria'; -import { v4 as uuidv4 } from 'uuid'; +import { + Button, + Card, + DataTable, + Dialog, + FormRadios, + FormSelect, + FormTextarea, + Icon, + Label, + Switch, +} from 'components'; import ClassHierarchy from 'components/molecules/ClassHeirarchy'; +import { createColumnHelper, PaginationState } from '@tanstack/react-table'; // Adjust based on your table library +import { + Dataset, + DatasetGroup, + ImportDataset, + ValidationRule, +} from 'types/datasetGroups'; +import BackArrowButton from 'assets/BackArrowButton'; +import { Link, useNavigate, useSearchParams } from 'react-router-dom'; +import ValidationCriteriaRowsView from 'components/molecules/ValidationCriteria/RowsView'; +import { MdOutlineDeleteOutline, MdOutlineEdit } from 'react-icons/md'; +import { + exportDataset, + getDatasets, + getMetadata, + importDataset, + majorUpdate, + minorUpdate, + patchUpdate, +} from 'services/datasets'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useDialog } from 'hooks/useDialog'; +import { useForm } from 'react-hook-form'; import { - getTimestampNow, - isValidationRulesSatisfied, + handleDownload, + reverseTransformClassHierarchy, transformClassHierarchy, + transformObjectToArray, transformValidationRules, validateClassHierarchy, validateValidationRules, } from 'utils/datasetGroupsUtils'; -import ValidationCriteria from 'components/molecules/ValidationCriteria'; -import { ValidationRule } from 'types/datasetGroups'; +import formats from '../../config/formatsConfig.json'; +import FileUpload, { FileUploadHandle } from 'components/FileUpload'; +import DynamicForm from 'components/FormElements/DynamicForm'; -const ViewDatasetGroup: FC = () => { +type Props = { + dgId: number; + setView: React.Dispatch>; +}; +const ViewDatasetGroup: FC> = ({ dgId, setView }) => { const { t } = useTranslation(); - const [isModalOpen, setIsModalOpen] = useState(false); - const [modalType, setModalType] = useState(''); + const [searchParams] = useSearchParams(); + const { open, close } = useDialog(); + const { register } = useForm(); + const queryClient = useQueryClient(); - const initialValidationRules = [ - { id: 1, fieldName: '', dataType: '', isDataClass: false }, - { id: 2, fieldName: '', dataType: '', isDataClass: true }, - ]; + const [validationRuleError, setValidationRuleError] = useState(false); + const [nodesError, setNodesError] = useState(false); + const [updatedData, setUpdatedData] = useState(''); + const [importFormat, setImportFormat] = useState(''); + const [exportFormat, setExportFormat] = useState(''); + const [importStatus, setImportStatus] = useState(''); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + const [isImportModalOpen, setIsImportModalOpen] = useState(false); + const [isExportModalOpen, setIsExportModalOpen] = useState(false); + const [patchUpdateModalOpen, setPatchUpdateModalOpen] = useState(false); + const fileUploadRef = useRef(null); + const [fetchEnabled, setFetchEnabled] = useState(true); + const [file, setFile] = useState(''); + const [selectedRow, setSelectedRow] = useState({}); - const initialClass = [ - { id: uuidv4(), fieldName: '', level: 0, children: [] }, - ]; + const navigate = useNavigate(); - const [datasetName, setDatasetName] = useState(''); - const [datasetNameError, setDatasetNameError] = useState(false); + useEffect(() => { + setFetchEnabled(false); + }, []); - const [validationRules, setValidationRules] = useState( - initialValidationRules + const { data: datasets, isLoading } = useQuery( + ['datasets/groups/data', pagination, dgId], + () => getDatasets(pagination, dgId), + { + keepPreviousData: true, + } ); - const [validationRuleError, setValidationRuleError] = useState(false); + // dgId: 1, + // operationSuccessful: true, + // fields: [ + // 'rowId', + // 'emailAddress', + // 'emailBody', + // 'emailSendTime', + // 'departmentCode', + // 'ministry', + // 'division', + // ], + // dataPayload: [ + // { + // rowId: 1, + // emailAddress: 'thiru.dinesh@rootcodelabs.com', + // emailBody: + // 'A generated email body that is interenstingly sophisticated and long enought to fill the screen. This body should also simulate the real life emails recieved by the government authorities in Estonia. Will be very interesting', + // emailSendTime: 'Sun, 07 Jul 2024 08:35:54 GMT', + // departmentCode: '05ABC', + // ministry: 'police and border guard', + // division: 'complaints processsing', + // }, + // { + // rowId: 2, + // emailAddress: 'thiru.dinesh@rootcodelabs.com', + // emailBody: + // 'A generated email body that is interenstingly sophisticated and long enought to fill the screen. This body should also simulate the real life emails recieved by the government authorities in Estonia. Will be very interesting', + // emailSendTime: 'Sun, 07 Jul 2024 08:35:54 GMT', + // departmentCode: '05ABC', + // ministry: 'police and border guard', + // division: 'complaints processsing', + // }, + // { + // rowId: 3, + // emailAddress: 'thiru.dinesh@rootcodelabs.com', + // emailBody: + // 'A generated email body that is interenstingly sophisticated and long enought to fill the screen. This body should also simulate the real life emails recieved by the government authorities in Estonia. Will be very interesting', + // emailSendTime: 'Sun, 07 Jul 2024 08:35:54 GMT', + // departmentCode: '05ABC', + // ministry: 'police and border guard', + // division: 'complaints processsing', + // }, + // { + // rowId: 4, + // emailAddress: 'thiru.dinesh@rootcodelabs.com', + // emailBody: + // 'A generated email body that is interenstingly sophisticated and long enought to fill the screen. This body should also simulate the real life emails recieved by the government authorities in Estonia. Will be very interesting', + // emailSendTime: 'Sun, 07 Jul 2024 08:35:54 GMT', + // departmentCode: '05ABC', + // ministry: 'police and border guard', + // division: 'complaints processsing', + // }, + // { + // rowId: 5, + // emailAddress: 'thiru.dinesh@rootcodelabs.com', + // emailBody: + // 'A generated email body that is interenstingly sophisticated and long enought to fill the screen. This body should also simulate the real life emails recieved by the government authorities in Estonia. Will be very interesting', + // emailSendTime: 'Sun, 07 Jul 2024 08:35:54 GMT', + // departmentCode: '05ABC', + // ministry: 'police and border guard', + // division: 'complaints processsing', + // }, + // ], + // }; + // const isLoading = false; - const [nodes, setNodes] = useState(initialClass); - const [nodesError, setNodesError] = useState(false); + const { data: metadata, isLoading: isMetadataLoading } = useQuery( + ['datasets/groups/metadata', dgId], + () => getMetadata(dgId), + { enabled: fetchEnabled } + ); + + const [nodes, setNodes] = useState( + reverseTransformClassHierarchy( + metadata?.response?.data?.[0]?.classHierarchy + ) + ); + const [validationRules, setValidationRules] = useState< + ValidationRule[] | undefined + >( + transformObjectToArray( + metadata?.response?.data?.[0]?.validationCriteria?.validationRules + ) + ); + + useEffect(() => { + setNodes( + reverseTransformClassHierarchy( + metadata?.response?.data?.[0]?.classHierarchy + ) + ); + }, [metadata]); + + useEffect(() => { + setValidationRules( + transformObjectToArray( + metadata?.response?.data?.[0]?.validationCriteria?.validationRules + ) + ); + }, [metadata]); + + const patchDataUpdate = (dataset) => { + const payload = { + dgId, + updateDataPayload: { + rowID: selectedRow?.rowID, + ...dataset, + }, + }; + patchUpdateMutation.mutate(payload); + }; + + const patchUpdateMutation = useMutation({ + mutationFn: (data) => patchUpdate(data), + onSuccess: async () => { + await queryClient.invalidateQueries(['datasets/groups/data']); + setPatchUpdateModalOpen(false); + }, + onError: () => { + open({ + title: 'Patch Data Update Unsuccessful', + content:

    Something went wrong. Please try again.

    , + }); + }, + }); + + const generateDynamicColumns = (columnsData, editView, deleteView) => { + const columnHelper = createColumnHelper(); + const dynamicColumns = columnsData?.map((col) => { + return columnHelper.accessor(col, { + header: col ?? '', + id: col, + }); + }); + + const staticColumns = [ + columnHelper.display({ + id: 'edit', + cell: editView, + meta: { + size: '1%', + }, + }), + columnHelper.display({ + id: 'delete', + cell: deleteView, + meta: { + size: '1%', + }, + }), + ]; + if (dynamicColumns) return [...dynamicColumns, ...staticColumns]; + else return []; + }; + + const editView = (props) => { + return ( + + ); + }; + + const deleteView = (props: any) => ( + + +
    + ), + }) + } + > + } /> + {'Delete'} + + ); + + const dataColumns = useMemo( + () => generateDynamicColumns(datasets?.fields, editView, deleteView), + [datasets?.fields] + ); + + const handleExport = () => { + exportDataMutation.mutate({ dgId, exportType: exportFormat }); + }; - const validateData = useCallback(() => { + const exportDataMutation = useMutation({ + mutationFn: (data) => exportDataset(data?.dgId, data?.exportType), + onSuccess: async (response) => { + handleDownload(response, exportFormat); + open({ + title: 'Data export was successful', + content:

    Your data has been successfully exported.

    , + }); + setIsExportModalOpen(false); + }, + onError: () => { + open({ + title: 'Dataset Export Unsuccessful', + content:

    Something went wrong. Please try again.

    , + }); + }, + }); + + const handleFileSelect = (file: File) => { + setFile(file); + }; + + const handleImport = () => { + setImportStatus('STARTED'); + const payload = { + dgId, + dataFile: file, + }; + + importDataMutation.mutate(payload); + }; + + const importDataMutation = useMutation({ + mutationFn: (data: ImportDataset) => + importDataset(data?.dataFile, data?.dgId), + onSuccess: async (response) => { + const payload = { + dgId, + s3FilePath: response?.saved_file_path, + }; + minorUpdateMutation.mutate(payload); + }, + onError: () => { + open({ + title: 'Dataset Import Unsuccessful', + content:

    Something went wrong. Please try again.

    , + }); + }, + }); + + const minorUpdateMutation = useMutation({ + mutationFn: (data) => minorUpdate(data), + onSuccess: async (response) => { + open({ + title: 'Dataset uploaded and validation initiated', + content: ( +

    + The dataset file was successfully uploaded. The validation and + preprocessing is now initiated +

    + ), + footer: ( +
    + + +
    + ), + }); + setIsImportModalOpen(false); + }, + onError: () => { + open({ + title: 'Dataset Import Unsuccessful', + content:

    Something went wrong. Please try again.

    , + }); + }, + }); + + const renderValidationStatus = (status: string | undefined) => { + if (status === 'success') { + return ; + } else if (status === 'fail') { + return ; + } else if (status === 'unvalidated') { + return ; + } else if (status === 'in-progress') { + return ; + } + }; + + const datasetGroupMajorUpdate = () => { setNodesError(validateClassHierarchy(nodes)); - setDatasetNameError(!datasetName && true); setValidationRuleError(validateValidationRules(validationRules)); if ( !validateClassHierarchy(nodes) && - datasetName && - true && - !validateValidationRules(validationRules) + !validateValidationRules(validationRules) && + !nodesError && + !validationRuleError ) { - const payload = { - name: datasetName, - major_version: 1, - minor_version: 0, - patch_version: 0, - created_timestamp: getTimestampNow(), - last_updated_timestamp: getTimestampNow(), - ...transformValidationRules(validationRules), + const payload: DatasetGroup = { + dgId, + validationCriteria: { ...transformValidationRules(validationRules) }, ...transformClassHierarchy(nodes), }; - - console.log(payload); - setIsModalOpen(true); - setModalType('SUCCESS'); - } - if (!isValidationRulesSatisfied(validationRules)) { - setIsModalOpen(true); - setModalType('VALIDATION_ERROR'); + majorUpdateDatasetGroupMutation.mutate(payload); } - }, [datasetName, nodes, validationRules]); + }; + + const majorUpdateDatasetGroupMutation = useMutation({ + mutationFn: (data: DatasetGroup) => majorUpdate(data), + onSuccess: async (response) => { + await queryClient.invalidateQueries(['datasetgroup/overview']); + setView('list'); + }, + onError: () => { + open({ + title: 'Dataset Group Update Unsuccessful', + content:

    Something went wrong. Please try again.

    , + }); + }, + }); return ( - <> +
    -
    -
    Create Dataset Group
    -
    - - <> - setDatasetName(e.target.value)} - error={ - !datasetName && datasetNameError ? 'Enter dataset name' : '' + {metadata && ( +
    + {' '} + +
    + navigate(0)}> + + +
    + {metadata?.response?.data?.[0]?.name} +
    + {metadata && ( + + )} + {metadata?.response?.data?.[0]?.latest ? ( + + ) : null} + {renderValidationStatus( + metadata?.response?.data?.[0]?.validationStatus + )} +
    + +
    } - /> - -
    - - -
    - - {modalType === 'VALIDATION_ERROR' && ( - - + +
    +
    + + +
    - Cancel - - - - } - onClose={() => setIsModalOpen(false)} - > - The dataset must have at least 2 columns. Additionally, there needs - to be at least one column designated as a data class and one column - that is not a data class. Please adjust your dataset accordingly. - - )} - {modalType === 'SUCCESS' && ( - - - - - } - onClose={() => setIsModalOpen(false)} + {!datasets&& ( +
    +
    + No Data Available +
    +

    + You have created the dataset group, but there are no + datasets available to show here. You can upload a + dataset to view it in this space. Once added, you can + edit or delete the data as needed. +

    + +
    + )} + {datasets && + datasets?.length < 10 && ( +
    +

    + Insufficient examples - at least 10 examples are + needed to activate the dataset group. +

    + +
    + )} +
    +
    +
    + )} +
    + {!isLoading && datasets && ( + { + if ( + state.pageIndex === pagination.pageIndex && + state.pageSize === pagination.pageSize + ) + return; + setPagination(state); + getDatasets(state, dgId); + }} + pagesCount={10} + isClientSide={false} + /> + )} +
    + {metadata && ( +
    + + + + + + {!isMetadataLoading && ( + + )} + +
    + )} +
    - You have successfully created the dataset group. In the detailed view, you can now see and edit the dataset as needed. - - )} + + +
    + ), + }) + } + > + Delete Dataset + + +
    + - + {isImportModalOpen && ( + + + + + } + onClose={() => { + setIsImportModalOpen(false); + setImportStatus('ABORTED'); + }} + > +
    +

    Select the file format

    +
    + +
    +

    Attachments

    + + {importStatus === 'STARTED' && ( +
    +
    + Upload in Progress... +
    +

    + Uploading dataset. Please wait until the upload finishes. If + you cancel midway, the data and progress will be lost. +

    +
    + )} +
    +
    + )} + {isExportModalOpen && ( + + + + + } + onClose={() => { + setIsExportModalOpen(false); + setImportStatus('ABORTED'); + }} + > +
    +

    Select the file format

    +
    + +
    +
    +
    + )} + {patchUpdateModalOpen && ( + setPatchUpdateModalOpen(false)} + isOpen={patchUpdateModalOpen} + > + + + )} + ); }; diff --git a/GUI/src/pages/DatasetGroups/index.tsx b/GUI/src/pages/DatasetGroups/index.tsx index c754e7d5..43e55e49 100644 --- a/GUI/src/pages/DatasetGroups/index.tsx +++ b/GUI/src/pages/DatasetGroups/index.tsx @@ -1,4 +1,4 @@ -import { FC, useState } from 'react'; +import { FC, useEffect, useState } from 'react'; import './DatasetGroups.scss'; import { useTranslation } from 'react-i18next'; import { Button, FormInput, FormSelect } from 'components'; @@ -13,13 +13,20 @@ import { parseVersionString, } from 'utils/commonUtilts'; import { DatasetGroup } from 'types/datasetGroups'; +import ViewDatasetGroup from './ViewDatasetGroup'; const DatasetGroups: FC = () => { const { t } = useTranslation(); const navigate = useNavigate(); const [pageIndex, setPageIndex] = useState(1); + const [id, setId] = useState(0); const [enableFetch, setEnableFetch] = useState(true); + const [view, setView] = useState("list"); + +useEffect(()=>{ + setEnableFetch(true) +},[view]); const [filters, setFilters] = useState({ datasetGroupName: 'all', @@ -31,10 +38,9 @@ const DatasetGroups: FC = () => { const { data: datasetGroupsData, isLoading, - refetch, } = useQuery( [ - 'datasets/groups', + 'datasetgroup/overview', pageIndex, filters.datasetGroupName, parseVersionString(filters?.version)?.major, @@ -61,9 +67,8 @@ const DatasetGroups: FC = () => { const { data: filterData } = useQuery(['datasets/filters'], () => getFilterData() ); - const pageCount = datasetGroupsData?.totalPages || 5; + const pageCount = datasetGroupsData?.response?.data?.[0]?.totalPages || 1; - // Handler for updating filters state const handleFilterChange = (name: string, value: string) => { setEnableFetch(false); setFilters((prevFilters) => ({ @@ -72,9 +77,10 @@ const DatasetGroups: FC = () => { })); }; + return (
    -
    + {view==="list" &&(
    Dataset Groups
    -
    +
    )} + {view==="individual" && ( + + )}
    ); }; diff --git a/GUI/src/pages/Integrations/index.tsx b/GUI/src/pages/Integrations/index.tsx index fc4bcdbd..a74a5bfc 100644 --- a/GUI/src/pages/Integrations/index.tsx +++ b/GUI/src/pages/Integrations/index.tsx @@ -1,23 +1,21 @@ import { FC } from 'react'; import './Integrations.scss'; import { useTranslation } from 'react-i18next'; -import { Button, Card, Switch } from 'components'; import IntegrationCard from 'components/molecules/IntegrationCard'; import Outlook from 'assets/Outlook'; import Pinal from 'assets/Pinal'; import Jira from 'assets/Jira'; -import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; import { getIntegrationStatus } from 'services/integration'; const Integrations: FC = () => { const { t } = useTranslation(); - const queryClient = useQueryClient(); - const { data: integrationStatus, isLoading } = useQuery( + const { data: integrationStatus } = useQuery( ['classifier/integration/platform-status'], () => getIntegrationStatus() ); -console.log(integrationStatus); + return (
    @@ -41,7 +39,6 @@ console.log(integrationStatus); logo={} channel={"Outlook+Pinal"} channelDescription={t('integration.pinalDesc')??""} - user={"Rickey Walker - Admin"} isActive={integrationStatus?.pinal_connection_status} />
    diff --git a/GUI/src/pages/StopWords/index.tsx b/GUI/src/pages/StopWords/index.tsx new file mode 100644 index 00000000..7477cdbb --- /dev/null +++ b/GUI/src/pages/StopWords/index.tsx @@ -0,0 +1,140 @@ +import { FC, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Button, Card, Dialog, FormInput, FormRadios } from 'components'; +import LabelChip from 'components/LabelChip'; +import FileUpload from 'components/FileUpload'; +import { useForm } from 'react-hook-form'; +import importOptions from '../../config/importOptionsConfig.json'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { addStopWord, deleteStopWord, getStopWords } from 'services/datasets'; + +const StopWords: FC = () => { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + + const [isModalOpen, setIsModalOpen] = useState(false); + const [importOption, setImportOption] = useState(''); + const [file, setFile] = useState(''); + + const [addedStopWord, setAddedStopWord] = useState(''); + + const { register, setValue, watch } = useForm({ + defaultValues: { + stopWord: addedStopWord, + }, + }); + + const { data: stopWordsData } = useQuery(['datasetgroups/stopwords'], () => + getStopWords() + ); + + const watchedStopWord = watch('stopWord'); + + const removeStopWord = (wordToRemove: string) => { + deleteStopWordMutation.mutate({ stopWords: [wordToRemove] }); + }; + + + const addStopWordMutation = useMutation({ + mutationFn: (data) => addStopWord(data), + onSuccess: async (res) => { + await queryClient.invalidateQueries(['datasetgroups/stopwords']); + + }, + onError: () => {}, + }); + + const deleteStopWordMutation = useMutation({ + mutationFn: (data) => deleteStopWord(data), + onSuccess: async (res) => { + await queryClient.invalidateQueries(['datasetgroups/stopwords']); + + }, + onError: () => {}, + }); + + return ( +
    +
    +
    +
    Stop Words
    + +
    + + {stopWordsData?.map((word) => ( + removeStopWord(word)} + /> + ))} +
    + + +
    +
    + {isModalOpen && ( + { + setIsModalOpen(false); + setImportOption(''); + }} + title={'Import stop words'} + footer={ +
    + + +
    + } + > +
    +

    Select the option below

    + +
    +

    Attachments (TXT, XLSX, YAML, JSON)

    + + setFile(selectedFile?.name ?? '') + } + /> +
    +
    + )} +
    +
    + ); +}; + +export default StopWords; diff --git a/GUI/src/pages/ValidationSessions/index.tsx b/GUI/src/pages/ValidationSessions/index.tsx new file mode 100644 index 00000000..4134030e --- /dev/null +++ b/GUI/src/pages/ValidationSessions/index.tsx @@ -0,0 +1,22 @@ +import { FC, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import ProgressBar from 'components/ProgressBar'; + +const ValidationSessions: FC = () => { + const { t } = useTranslation(); + const [progress, setProgress] = useState(40); + + + return ( +
    +
    +
    Validation Sessions
    +
    + + +
    +
    + ); +}; + +export default ValidationSessions; diff --git a/GUI/src/services/api-dev.ts b/GUI/src/services/api-dev.ts index 83623d26..c3e746f8 100644 --- a/GUI/src/services/api-dev.ts +++ b/GUI/src/services/api-dev.ts @@ -14,7 +14,6 @@ instance.interceptors.response.use( return axiosResponse; }, (error: AxiosError) => { - console.log(error); return Promise.reject(new Error(error.message)); } ); @@ -24,7 +23,6 @@ instance.interceptors.request.use( return axiosRequest; }, (error: AxiosError) => { - console.log(error); if (error.response?.status === 401) { // To be added: handle unauthorized requests } diff --git a/GUI/src/services/api-external.ts b/GUI/src/services/api-external.ts new file mode 100644 index 00000000..b55ddb35 --- /dev/null +++ b/GUI/src/services/api-external.ts @@ -0,0 +1,36 @@ +import axios, { AxiosError } from 'axios'; + +const instance = axios.create({ + baseURL: import.meta.env.REACT_APP_EXTERNAL_API_URL, + headers: { + Accept: 'application/json', + 'Content-Type': 'multipart/form-data', + }, + withCredentials: true, +}); + +instance.interceptors.response.use( + (axiosResponse) => { + return axiosResponse; + }, + (error: AxiosError) => { + return Promise.reject(new Error(error.message)); + } +); + +instance.interceptors.request.use( + (axiosRequest) => { + return axiosRequest; + }, + (error: AxiosError) => { + if (error.response?.status === 401) { + // To be added: handle unauthorized requests + } + if (error.response?.status === 403) { + // To be added: handle unauthorized requests + } + return Promise.reject(new Error(error.message)); + } +); + +export default instance; diff --git a/GUI/src/services/api-mock.ts b/GUI/src/services/api-mock.ts index 52b762d5..4932793d 100644 --- a/GUI/src/services/api-mock.ts +++ b/GUI/src/services/api-mock.ts @@ -1,7 +1,7 @@ import axios, { AxiosError } from 'axios'; const instance = axios.create({ - baseURL: "https://d5e7cde0-f9b1-4425-8a16-c5f93f503e2e.mock.pstmn.io", + baseURL: 'https://d5e7cde0-f9b1-4425-8a16-c5f93f503e2e.mock.pstmn.io', headers: { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded', @@ -13,7 +13,6 @@ instance.interceptors.response.use( return axiosResponse; }, (error: AxiosError) => { - console.log(error); return Promise.reject(new Error(error.message)); } ); @@ -23,7 +22,6 @@ instance.interceptors.request.use( return axiosRequest; }, (error: AxiosError) => { - console.log(error); if (error.response?.status === 401) { // To be added: handle unauthorized requests } diff --git a/GUI/src/services/api.ts b/GUI/src/services/api.ts index 885d36dd..3ce245ae 100644 --- a/GUI/src/services/api.ts +++ b/GUI/src/services/api.ts @@ -14,7 +14,6 @@ instance.interceptors.response.use( return axiosResponse; }, (error: AxiosError) => { - console.log(error); return Promise.reject(new Error(error.message)); } ); @@ -24,7 +23,6 @@ instance.interceptors.request.use( return axiosRequest; }, (error: AxiosError) => { - console.log(error); if (error.response?.status === 401) { // To be added: handle unauthorized requests } diff --git a/GUI/src/services/datasets.ts b/GUI/src/services/datasets.ts index e688581f..b78c403c 100644 --- a/GUI/src/services/datasets.ts +++ b/GUI/src/services/datasets.ts @@ -1,5 +1,9 @@ -import apiMock from './api-mock'; +import apiDev from './api-dev'; +import apiExternal from './api-external'; + +import { PaginationState } from '@tanstack/react-table'; +import { DatasetGroup, Operation } from 'types/datasetGroups'; export async function getDatasetsOverview( pageNum: number, name: string, @@ -9,22 +13,23 @@ export async function getDatasetsOverview( validationStatus: string, sort: string ) { - const { data } = await apiMock.get('GET/datasetgroup/overview', { + const { data } = await apiDev.get('classifier/datasetgroup/overview', { params: { - pageNum: pageNum, - name, + page: pageNum, + groupName:name, majorVersion, minorVersion, patchVersion, validationStatus, - sort, + sortType:sort, + pageSize:12 }, }); return data; } -export async function enableDataset(enableData) { - const { data } = await apiMock.post('POST/datasetgroup/update/status', { +export async function enableDataset(enableData: Operation) { + const { data } = await apiDev.post('classifier/datasetgroup/update/status', { dgId: enableData.dgId, operationType: enableData.operationType, }); @@ -32,6 +37,99 @@ export async function enableDataset(enableData) { } export async function getFilterData() { - const { data } = await apiMock.get('GET/datasetgroup/overview/filters'); + const { data } = await apiDev.get('classifier/datasetgroup/overview/filters'); + return data; +} + +export async function getDatasets( + pagination: PaginationState, + groupId: string | number | null +) { + const { data } = await apiDev.get('classifier/datasetgroup/group/data', { + params: { + pageNum: pagination.pageIndex+1, + groupId, + }, + }); + + + return data?.response?.data[0]; +} + +export async function getMetadata(groupId: string | number | null) { + const { data } = await apiDev.get('classifier/datasetgroup/group/metadata', { + params: { + groupId + }, + }); + return data; +} + +export async function createDatasetGroup(datasetGroup: DatasetGroup) { + + const { data } = await apiDev.post('classifier/datasetgroup/create', { + ...datasetGroup, + }); + return data; +} + +export async function importDataset(file: File, id: string|number) { + + + const { data } = await apiExternal.post('datasetgroup/data/import', { + dataFile:file, + dgId:id + }); + return data; +} + +export async function exportDataset(id: string, type: string) { + const headers = { + 'Content-Type': 'application/json', + } + const { data } = await apiExternal.post('datasetgroup/data/download', { + dgId: id, + exportType: type, + },{headers,responseType: 'blob'}); + return data; +} + +export async function patchUpdate(updatedData: DatasetGroup) { + const { data } = await apiDev.post('classifier/datasetgroup/update/patch', { + ...updatedData, + }); return data; } + +export async function minorUpdate(updatedData) { + const { data } = await apiDev.post('classifier/datasetgroup/update/minor', { + ...updatedData, + }); + return data; +} + +export async function majorUpdate(updatedData: DatasetGroup) { + const { data } = await apiDev.post('classifier/datasetgroup/update/major', { + ...updatedData, + }); + return data; +} + +export async function getStopWords() { + const { data } = await apiDev.get('classifier/datasetgroup/stop-words'); + return data?.response?.stopWords; +} + +export async function addStopWord(stopWordData) { + const { data } = await apiDev.post('classifier/datasetgroup/update/stop-words',{ +...stopWordData + }); + return data; +} + +export async function deleteStopWord(stopWordData) { + const { data } = await apiDev.post('classifier/datasetgroup/delete/stop-words',{ +...stopWordData + }); + return data; +} \ No newline at end of file diff --git a/GUI/src/styles/generic/_base.scss b/GUI/src/styles/generic/_base.scss index 67693e6f..0641e8b1 100644 --- a/GUI/src/styles/generic/_base.scss +++ b/GUI/src/styles/generic/_base.scss @@ -57,6 +57,16 @@ body { margin: 20px 0px; } +.flex-between { + display: flex; + justify-content: space-between; +} + +.flex-grid { + display: flex; + gap: 10px; +} + a, input, select, @@ -66,6 +76,10 @@ button { transition: background-color 0.25s, color 0.25s, border-color 0.25s, box-shadow 0.25s; } +p { +margin-bottom: 10px; +} + a { color: get-color(sapphire-blue-10); text-decoration: none; diff --git a/GUI/src/types/common.ts b/GUI/src/types/common.ts new file mode 100644 index 00000000..dce11644 --- /dev/null +++ b/GUI/src/types/common.ts @@ -0,0 +1,6 @@ +export interface Columns { + accessorKey?: string; + header?: string; + id?: string; + meta?: {} +} diff --git a/GUI/src/types/datasetGroups.ts b/GUI/src/types/datasetGroups.ts index 6b0f89a9..1c7a8ea6 100644 --- a/GUI/src/types/datasetGroups.ts +++ b/GUI/src/types/datasetGroups.ts @@ -9,7 +9,7 @@ export interface Class { id: string; fieldName: string; level: number; - children: Class[]|any; + children: Class[] | any; } export interface LinkedModel { @@ -17,18 +17,46 @@ export interface LinkedModel { modelName: string; modelVersion: string; trainingTimestamp: number; +} + +export interface Dataset { + rowId: number; + emailAddress: string; + emailBody: string; + emailSendTime: string; + departmentCode: string; + ministry: string; + division: string; +} + +export interface Operation { + dgId: number|undefined; + operationType: 'enable' | 'disable'; +} + +export interface ValidationRuleResponse { + type: string; + isDataClass: boolean; +}; + +export interface ValidationCriteria { + fields: string[]; + validationRules: Record; +}; + +export interface ClassNode { + class: string; + subclasses: ClassNode[]; }; export interface DatasetGroup { - dgId: number; - name: string; - majorVersion: number; - minorVersion: number; - patchVersion: number; - latest: boolean; - isEnabled: boolean; - enableAllowed: boolean; - lastUpdated: string; - linkedModels: LinkedModel[]; - validationStatus: string; -}; \ No newline at end of file + dgId?: number; + groupName?:string; + validationCriteria: ValidationCriteria; + classHierarchy: ClassNode[]; +}; + +export interface ImportDataset { + dgId: number|string; + dataFile: File; +} \ No newline at end of file diff --git a/GUI/src/utils/datasetGroupsUtils.ts b/GUI/src/utils/datasetGroupsUtils.ts index c0e0cff4..0c6a6dca 100644 --- a/GUI/src/utils/datasetGroupsUtils.ts +++ b/GUI/src/utils/datasetGroupsUtils.ts @@ -1,21 +1,20 @@ import { Class, ValidationRule } from 'types/datasetGroups'; +import { v4 as uuidv4 } from 'uuid'; -export const transformValidationRules = (data: ValidationRule[]) => { +export const transformValidationRules = ( + data: ValidationRule[] | undefined +) => { const validationCriteria = { fields: [], - validation_rules: {}, + validationRules: {}, }; - data.forEach((item) => { - const fieldNameKey: string = item.fieldName - .toLowerCase() - .replace(/\s+/g, '_'); + data?.forEach((item) => { + validationCriteria.fields.push(item.fieldName); - validationCriteria.fields.push(fieldNameKey); - - validationCriteria.validation_rules[fieldNameKey] = { + validationCriteria.validationRules[item.fieldName] = { type: item.dataType.toLowerCase(), - is_data_class: item.isDataClass, + isDataClass: item.isDataClass, }; }); @@ -31,10 +30,40 @@ export const transformClassHierarchy = (data: Class[]) => { }; return { - class_hierarchy: data.map(transformNode), + classHierarchy: data.map(transformNode), }; }; +export const reverseTransformClassHierarchy = (data) => { + const traverse = (node, level: number) => { + const flatNode = { + id: uuidv4(), + fieldName: node.class, + level, + children: node?.subclasses.map((subclass) => + traverse(subclass, level + 1) + ), + }; + + return flatNode; + }; + + return data?.map((item) => traverse(item, 0)); +}; + +export const transformObjectToArray = (data) => { + if (data) { + const output = Object.entries(data).map(([fieldName, details], index) => ({ + id: index + 1, + fieldName, + dataType: details?.type, + isDataClass: details?.isDataClass, + })); + + return output; + } +}; + export const validateClassHierarchy = (data: Class[]) => { for (let item of data) { if (item.fieldName === '') { @@ -49,7 +78,7 @@ export const validateClassHierarchy = (data: Class[]) => { return false; }; -export const validateValidationRules = (data: ValidationRule[]) => { +export const validateValidationRules = (data: ValidationRule[]|undefined) => { for (let item of data) { if (item.fieldName === '' || item.dataType === '') { return true; @@ -85,3 +114,51 @@ export const isValidationRulesSatisfied = (data: ValidationRule[]) => { return false; }; + +export const isFieldNameExisting = (dataArray, fieldNameToCheck) => { + + const count = dataArray.reduce((acc, item) => { + return item?.fieldName?.toLowerCase() === fieldNameToCheck?.toLowerCase() ? acc + 1 : acc; + }, 0); + + return count === 2; +}; + +export const countFieldNameOccurrences=(dataArray, fieldNameToCheck)=> { + let count = 0; + + function countOccurrences(node) { + if (node?.fieldName?.toLowerCase() === fieldNameToCheck?.toLowerCase()) { + count += 1; + } + + if (node.children) { + node.children.forEach(child => countOccurrences(child)); + } + } + + dataArray.forEach(node => countOccurrences(node)); + + return count; +} + +export const isClassHierarchyDuplicated=(dataArray, fieldNameToCheck)=> { + const count = countFieldNameOccurrences(dataArray, fieldNameToCheck); + return count === 2; +} + +export const handleDownload = (response,format) =>{ + try { + // Create a URL for the Blob + const url = window.URL.createObjectURL(new Blob([response.data])); + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', `export.${format}`); // Specify the file name and extension + document.body.appendChild(link); + link.click(); + link.parentNode.removeChild(link); + } catch (error) { + console.error('Error downloading the file', error); + } +} + diff --git a/GUI/translations/en/common.json b/GUI/translations/en/common.json index 6f0ee734..d76d2d1e 100644 --- a/GUI/translations/en/common.json +++ b/GUI/translations/en/common.json @@ -94,7 +94,7 @@ "integrationErrorTitle": "Integration Unsuccessful", "integrationErrorDesc": "Failed to connect with {{channel}}. Please check your settings and try again. If the problem persists, contact support for assistance.", "integrationSuccessTitle": "Integration Successful", - "integrationSuccessDesc": "You have successfully connected with {{channel}}! Your integration is now complete, and you can start working with {{Jira}} seamlessly.", + "integrationSuccessDesc": "You have successfully connected with {{channel}}! Your integration is now complete, and you can start working with {{channel}} seamlessly.", "confirmationModalTitle": "Are you sure?", "disconnectConfirmationModalDesc": "Are you sure you want to disconnect the {{channel}} integration? This action cannot be undone and may affect your workflow and linked issues.", "connectConfirmationModalDesc": "Are you sure you want to connect the {{channel}} integration?", From a756419d58e15f48140837ac6767dff8b7efa29d Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Fri, 26 Jul 2024 10:47:53 +0530 Subject: [PATCH 301/582] changes in docker compose and env file --- GUI/.env.development | 11 +++++------ docker-compose.yml | 32 ++++++++++++++++---------------- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/GUI/.env.development b/GUI/.env.development index 6297da2a..3310ca76 100644 --- a/GUI/.env.development +++ b/GUI/.env.development @@ -1,9 +1,8 @@ -REACT_APP_RUUTER_API_URL=https://esclassifier-dev-ruuter.rootcode.software -REACT_APP_RUUTER_PRIVATE_API_URL=https://esclassifier-dev-ruuter-private.rootcode.software -REACT_APP_CUSTOMER_SERVICE_LOGIN=https://login.esclassifier-dev.rootcode.software +REACT_APP_RUUTER_API_URL=http://localhost:8086 +REACT_APP_RUUTER_PRIVATE_API_URL=http://localhost:8088 +REACT_APP_EXTERNAL_API_URL=http://localhost:8000 +REACT_APP_CUSTOMER_SERVICE_LOGIN=http://localhost:3004/et/dev-auth REACT_APP_SERVICE_ID=conversations,settings,monitoring REACT_APP_NOTIFICATION_NODE_URL=http://localhost:4040 -REACT_APP_CSP=upgrade-insecure-requests; default-src 'self'; font-src 'self' data:; img-src 'self' data:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self' http://localhost:8086 http://localhost:8088 http://localhost:8085 http://localhost:4040; +REACT_APP_CSP=upgrade-insecure-requests; default-src 'self'; font-src 'self' data:; img-src 'self' data:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self' http://localhost:8086 http://localhost:8088 http://localhost:8085 http://localhost:4040 http://localhost:3002/menu.json; REACT_APP_ENABLE_HIDDEN_FEATURES=TRUE - - diff --git a/docker-compose.yml b/docker-compose.yml index e6f8ebc1..469a3458 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ services: container_name: ruuter-public image: ruuter environment: - - application.cors.allowedOrigins=http://localhost:8086,http://localhost:3001,http://localhost:3003,http://localhost:3004,http://localhost:8080,http://localhost:8000 + - application.cors.allowedOrigins=http://localhost:8086,http://localhost:3001,http://localhost:3003,http://localhost:3004,http://localhost:8080 - application.httpCodesAllowList=200,201,202,400,401,403,500 - application.internalRequests.allowedIPs=127.0.0.1 - application.logging.displayRequestContent=true @@ -23,7 +23,7 @@ services: container_name: ruuter-private image: ruuter environment: - - application.cors.allowedOrigins=http://localhost:3001,http://localhost:8088,http://localhost:3002,http://localhost:3004,http://localhost:8000 + - application.cors.allowedOrigins=http://localhost:3001,http://localhost:8000,http://localhost:3006,http://localhost:8088,http://localhost:3002,http://localhost:3004 - application.httpCodesAllowList=200,201,202,204,400,401,403,404,500 - application.internalRequests.allowedIPs=127.0.0.1 - application.logging.displayRequestContent=true @@ -158,19 +158,19 @@ services: - bykstack restart: always - cron-manager: - container_name: cron-manager - image: cron-manager - volumes: - - ./DSL/CronManager/DSL:/DSL - - ./DSL/CronManager/script:/app/scripts - - ./DSL/CronManager/config:/app/config - environment: - - server.port=9010 - ports: - - 9010:8080 - networks: - - bykstack + # cron-manager: + # container_name: cron-manager + # image: cron-manager + # volumes: + # - ./DSL/CronManager/DSL:/DSL + # - ./DSL/CronManager/script:/app/scripts + # - ./DSL/CronManager/config:/app/config + # environment: + # - server.port=9010 + # ports: + # - 9010:8080 + # networks: + # - bykstack init: image: busybox @@ -207,7 +207,7 @@ services: - S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID} - S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY} ports: - - "3002:3000" + - "3006:3000" depends_on: - file-handler - init From 8827ec7a731cf85b424289d45cec3a5129a6c275 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 26 Jul 2024 13:26:07 +0530 Subject: [PATCH 302/582] Completed updates for minor flow --- DSL/CronManager/DSL/dataset_processing.yml | 8 ++- DSL/CronManager/script/data_processor_exec.sh | 47 +++++++++++--- DSL/CronManager/script/data_validator_exec.sh | 45 ++++++++++++++ .../classifier/datasetgroup/update/minor.yml | 4 +- .../classifier/datasetgroup/update/patch.yml | 4 +- .../datasetgroup/update/validation/status.yml | 6 +- constants.ini | 2 +- dataset-processor/dataset_processor.py | 16 +++-- dataset-processor/dataset_processor_api.py | 62 ++++++++++++++++--- docker-compose.yml | 2 +- 10 files changed, 165 insertions(+), 31 deletions(-) create mode 100644 DSL/CronManager/script/data_validator_exec.sh diff --git a/DSL/CronManager/DSL/dataset_processing.yml b/DSL/CronManager/DSL/dataset_processing.yml index 04e438bf..23fe2743 100644 --- a/DSL/CronManager/DSL/dataset_processing.yml +++ b/DSL/CronManager/DSL/dataset_processing.yml @@ -2,4 +2,10 @@ dataset_processor: trigger: off type: exec command: "../app/scripts/data_processor_exec.sh" - allowedEnvs: ["dgId","cookie","updateType","savedFilePath","patchPayload"] \ No newline at end of file + allowedEnvs: ["cookie","dgId","updateType","savedFilePath","patchPayload"] + +data_validation: + trigger: off + type: exec + command: "../app/scripts/data_validator_exec.sh" + allowedEnvs: ["cookie","dgId","updateType","savedFilePath","patchPayload"] \ No newline at end of file diff --git a/DSL/CronManager/script/data_processor_exec.sh b/DSL/CronManager/script/data_processor_exec.sh index a27149d1..35f2c40b 100644 --- a/DSL/CronManager/script/data_processor_exec.sh +++ b/DSL/CronManager/script/data_processor_exec.sh @@ -1,11 +1,44 @@ #!/bin/bash -# Set the working directory to the location of the script -cd "$(dirname "$0")" +# Ensure required environment variables are set +if [ -z "$dgId" ] || [ -z "$cookie" ] || [ -z "$updateType" ] || [ -z "$savedFilePath" ] || [ -z "$patchPayload" ]; then + echo "One or more environment variables are missing." + echo "Please set dgId, cookie, updateType, savedFilePath, and patchPayload." + exit 1 +fi -# Source the constants from the ini file -source ../config/config.ini +# Construct the payload using grep +payload=$(cat < Date: Fri, 26 Jul 2024 13:32:55 +0530 Subject: [PATCH 303/582] constatnt updarte --- constants.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/constants.ini b/constants.ini index 158c62c1..bc34bf45 100644 --- a/constants.ini +++ b/constants.ini @@ -18,4 +18,4 @@ OUTLOOK_CLIENT_ID=value OUTLOOK_SECRET_KEY=value OUTLOOK_REFRESH_KEY=value OUTLOOK_SCOPE=User.Read Mail.ReadWrite MailboxSettings.ReadWrite offline_access -DB_PASSWORD=rootcode \ No newline at end of file +DB_PASSWORD=value \ No newline at end of file From e4a84db7b842739e79b90835e560193f6cb65440 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 26 Jul 2024 14:45:49 +0530 Subject: [PATCH 304/582] update --- dataset-processor/dataset_processor_api.py | 16 ++++++++++------ file-handler/file_handler_api.py | 4 ++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/dataset-processor/dataset_processor_api.py b/dataset-processor/dataset_processor_api.py index fc276ec1..7c5a55ed 100644 --- a/dataset-processor/dataset_processor_api.py +++ b/dataset-processor/dataset_processor_api.py @@ -63,7 +63,7 @@ async def forward_request(request: Request, response: Response): headers = { - 'cookie': f'customJwtCookie={payload["cookie"]}', + 'cookie': f'{payload["cookie"]}', 'Content-Type': 'application/json' } @@ -82,14 +82,18 @@ async def forward_request(request: Request, response: Response): try: print("8") - forward_response = requests.post(forward_url, json=payload2, headers=headers) - print("8") + forward_response = requests.post(forward_url, json=payload2, headers= + ) + print() + print("9") forward_response.raise_for_status() - print("8") + print("10") return JSONResponse(content=forward_response.json(), status_code=forward_response.status_code) except requests.HTTPError as e: - print("9") + print("11") + print(e) raise HTTPException(status_code=e.response.status_code, detail=e.response.text) except Exception as e: - print("9") + print("12") + print(e) raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file diff --git a/file-handler/file_handler_api.py b/file-handler/file_handler_api.py index 62b96fd3..fdc85d42 100644 --- a/file-handler/file_handler_api.py +++ b/file-handler/file_handler_api.py @@ -62,8 +62,8 @@ async def authenticate_user(request: Request): } response = requests.get(url, headers=headers) - if response.status_code != 200: - raise HTTPException(status_code=response.status_code, detail="Authentication failed") + # if response.status_code != 200: + # raise HTTPException(status_code=response.status_code, detail="Authentication failed") @app.post("/datasetgroup/data/import") async def upload_and_copy(request: Request, dgId: int = Form(...), dataFile: UploadFile = File(...)): From 305bf42f03aec3646e9b930a4a2baa80065c492d Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Mon, 29 Jul 2024 13:04:58 +0530 Subject: [PATCH 305/582] ESCLASS-145-node-server and opensearch implementation --- DSL/OpenSearch/deploy-opensearch.sh | 15 + .../fieldMappings/dataset_group_progress.json | 21 + .../mock/dataset_group_progress.json | 8 + docker-compose.yml | 93 +- notification-server/.env | 9 + notification-server/Dockerfile | 13 + notification-server/index.js | 16 + notification-server/package-lock.json | 1354 +++++++++++++++++ notification-server/package.json | 21 + notification-server/src/addOns.js | 24 + notification-server/src/config.js | 24 + notification-server/src/openSearch.js | 72 + notification-server/src/server.js | 54 + notification-server/src/sseUtil.js | 53 + 14 files changed, 1761 insertions(+), 16 deletions(-) create mode 100644 DSL/OpenSearch/deploy-opensearch.sh create mode 100644 DSL/OpenSearch/fieldMappings/dataset_group_progress.json create mode 100644 DSL/OpenSearch/mock/dataset_group_progress.json create mode 100644 notification-server/.env create mode 100644 notification-server/Dockerfile create mode 100644 notification-server/index.js create mode 100644 notification-server/package-lock.json create mode 100644 notification-server/package.json create mode 100644 notification-server/src/addOns.js create mode 100644 notification-server/src/config.js create mode 100644 notification-server/src/openSearch.js create mode 100644 notification-server/src/server.js create mode 100644 notification-server/src/sseUtil.js diff --git a/DSL/OpenSearch/deploy-opensearch.sh b/DSL/OpenSearch/deploy-opensearch.sh new file mode 100644 index 00000000..7dd376a4 --- /dev/null +++ b/DSL/OpenSearch/deploy-opensearch.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +URL=$1 +AUTH=$2 +MOCK_ALLOWED=${3:-false} + +if [[ -z $URL || -z $AUTH ]]; then + echo "Url and Auth are required" + exit 1 +fi + +# dataset_group_progress +curl -XDELETE "$URL/dataset_group_progress?ignore_unavailable=true" -u "$AUTH" --insecure +curl -H "Content-Type: application/x-ndjson" -X PUT "$URL/dataset_group_progress" -ku "$AUTH" --data-binary "@fieldMappings/dataset_group_progress.json" +if $MOCK_ALLOWED; then curl -H "Content-Type: application/x-ndjson" -X PUT "$URL/dataset_group_progress/_bulk" -ku "$AUTH" --data-binary "@mock/dataset_group_progress.json"; fi \ No newline at end of file diff --git a/DSL/OpenSearch/fieldMappings/dataset_group_progress.json b/DSL/OpenSearch/fieldMappings/dataset_group_progress.json new file mode 100644 index 00000000..d1ec86eb --- /dev/null +++ b/DSL/OpenSearch/fieldMappings/dataset_group_progress.json @@ -0,0 +1,21 @@ +{ + "mappings": { + "properties": { + "sessionId": { + "type": "keyword" + }, + "validationStatus": { + "type": "keyword" + }, + "progress": { + "type": "integer" + }, + "timestamp": { + "type": "keyword" + }, + "sentTo": { + "type": "keyword" + } + } + } +} diff --git a/DSL/OpenSearch/mock/dataset_group_progress.json b/DSL/OpenSearch/mock/dataset_group_progress.json new file mode 100644 index 00000000..461392f8 --- /dev/null +++ b/DSL/OpenSearch/mock/dataset_group_progress.json @@ -0,0 +1,8 @@ +{"index":{"_id":"1"}} +{"sessionId": "1","validationStatus": "in-progress","progress": 1,"timestamp": "1801371325497", "sentTo": []} +{"index":{"_id":"2"}} +{"sessionId": "2","validationStatus": "in-progress","progress": 10,"timestamp": "1801371325597", "sentTo": []} +{"index":{"_id":"3"}} +{"sessionId": "3","validationStatus": "in-progress","progress": 52,"timestamp": "1801371325697", "sentTo": []} +{"index":{"_id":"4"}} +{"sessionId": "4","validationStatus": "in-progress","progress": 97,"timestamp": "1801371325797", "sentTo": []} diff --git a/docker-compose.yml b/docker-compose.yml index 32271afb..329c1a78 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ services: container_name: ruuter-public image: ruuter environment: - - application.cors.allowedOrigins=http://localhost:8086,http://localhost:3001,http://localhost:3003,http://localhost:3004,http://localhost:8080 + - application.cors.allowedOrigins=http://localhost:8086,http://localhost:3001,http://localhost:3003,http://localhost:3004,http://localhost:8080,http://localhost:8000 - application.httpCodesAllowList=200,201,202,400,401,403,500 - application.internalRequests.allowedIPs=127.0.0.1 - application.logging.displayRequestContent=true @@ -23,7 +23,7 @@ services: container_name: ruuter-private image: ruuter environment: - - application.cors.allowedOrigins=http://localhost:3001,http://localhost:8000,http://localhost:3006,http://localhost:8088,http://localhost:3002,http://localhost:3004 + - application.cors.allowedOrigins=http://localhost:3001,http://localhost:8088,http://localhost:3002,http://localhost:3004,http://localhost:8000 - application.httpCodesAllowList=200,201,202,204,400,401,403,404,500 - application.internalRequests.allowedIPs=127.0.0.1 - application.logging.displayRequestContent=true @@ -158,19 +158,19 @@ services: - bykstack restart: always - # cron-manager: - # container_name: cron-manager - # image: cron-manager - # volumes: - # - ./DSL/CronManager/DSL:/DSL - # - ./DSL/CronManager/script:/app/scripts - # - ./DSL/CronManager/config:/app/config - # environment: - # - server.port=9010 - # ports: - # - 9010:8080 - # networks: - # - bykstack + cron-manager: + container_name: cron-manager + image: cron-manager + volumes: + - ./DSL/CronManager/DSL:/DSL + - ./DSL/CronManager/script:/app/scripts + - ./DSL/CronManager/config:/app/config + environment: + - server.port=9010 + ports: + - 9010:8080 + networks: + - bykstack init: image: busybox @@ -207,7 +207,7 @@ services: - S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID} - S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY} ports: - - "3006:3000" + - "3002:3000" depends_on: - file-handler - init @@ -243,9 +243,70 @@ services: - s3-ferry - file-handler + opensearch-node: + image: opensearchproject/opensearch:2.11.1 + container_name: opensearch-node + environment: + - node.name=opensearch-node + - discovery.seed_hosts=opensearch + - discovery.type=single-node + - bootstrap.memory_lock=true + - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m" + - plugins.security.disabled=true + ulimits: + memlock: + soft: -1 + hard: -1 + nofile: + soft: 65536 + hard: 65536 + volumes: + - opensearch-data:/usr/share/opensearch/data + ports: + - 9200:9200 + - 9600:9600 + networks: + - bykstack + + opensearch-dashboards: + image: opensearchproject/opensearch-dashboards:2.11.1 + container_name: opensearch-dashboards + environment: + - OPENSEARCH_HOSTS=http://opensearch-node:9200 + - DISABLE_SECURITY_DASHBOARDS_PLUGIN=true + ports: + - 5601:5601 + networks: + - bykstack + + notifications-node: + container_name: notifications-node + build: + context: ./notification-server + dockerfile: Dockerfile + ports: + - 4040:4040 + depends_on: + - opensearch-node + environment: + OPENSEARCH_PROTOCOL: http + OPENSEARCH_HOST: opensearch-node + OPENSEARCH_PORT: 9200 + OPENSEARCH_USERNAME: admin + OPENSEARCH_PASSWORD: admin + PORT: 4040 + REFRESH_INTERVAL: 1000 + CORS_WHITELIST_ORIGINS: http://localhost:3001,http://localhost:3003,http://localhost:3004,http://localhost:8080 + RUUTER_URL: http://ruuter-public:8086 + volumes: + - /app/node_modules + - ./notification-server:/app + networks: + - bykstack volumes: shared-volume: + opensearch-data: networks: bykstack: diff --git a/notification-server/.env b/notification-server/.env new file mode 100644 index 00000000..fc3e9f18 --- /dev/null +++ b/notification-server/.env @@ -0,0 +1,9 @@ +OPENSEARCH_PROTOCOL=http +OPENSEARCH_HOST=opensearch-node +OPENSEARCH_PORT=9200 +OPENSEARCH_USERNAME=admin +OPENSEARCH_PASSWORD=admin +PORT=4040 +REFRESH_INTERVAL=1000 +CORS_WHITELIST_ORIGINS=http://localhost:3001,http://localhost:3003,http://localhost:3004,http://localhost:8080 +RUUTER_URL=http://localhost:8086 diff --git a/notification-server/Dockerfile b/notification-server/Dockerfile new file mode 100644 index 00000000..6769c0ce --- /dev/null +++ b/notification-server/Dockerfile @@ -0,0 +1,13 @@ +FROM node:22.0.0-alpine + +WORKDIR /app + +COPY package.json package-lock.json /app/ + +RUN npm install + +COPY . /app/ + +EXPOSE 4040 + +CMD ["npm", "run", "dev"] diff --git a/notification-server/index.js b/notification-server/index.js new file mode 100644 index 00000000..5db0807e --- /dev/null +++ b/notification-server/index.js @@ -0,0 +1,16 @@ +require('dotenv').config(); +const { client } = require('./src/openSearch'); + +(async () => { + try { + await client.indices.putSettings({ + index: 'dataset_group_progress', + body: { + refresh_interval: '5s', + }, + }); + require('./src/server'); + } catch (error) { + console.error('Error:', error); + } +})(); diff --git a/notification-server/package-lock.json b/notification-server/package-lock.json new file mode 100644 index 00000000..21905e70 --- /dev/null +++ b/notification-server/package-lock.json @@ -0,0 +1,1354 @@ +{ + "name": "notification-service", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "notification-service", + "version": "1.0.0", + "dependencies": { + "@opensearch-project/opensearch": "^2.4.0", + "cookie-parser": "^1.4.6", + "cors": "^2.8.5", + "csurf": "^1.11.0", + "dotenv": "^16.3.1", + "express": "^4.19.2", + "helmet": "^7.1.0", + "uuid": "^9.0.1" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } + }, + "node_modules/@opensearch-project/opensearch": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opensearch-project/opensearch/-/opensearch-2.4.0.tgz", + "integrity": "sha512-r0ZNIlDxAua1ZecOBJ8qOXshf2ZQhNKmfly7o0aNuACf0pDa6Et/8mWMZuaFOu7xlNEeRNB7IjDQUYFy2SPElw==", + "dependencies": { + "aws4": "^1.11.0", + "debug": "^4.3.1", + "hpagent": "^1.2.0", + "ms": "^2.1.3", + "secure-json-parse": "^2.4.0" + }, + "engines": { + "node": ">=10", + "yarn": "^1.22.10" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/aws4": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", + "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" + }, + "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 + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "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, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "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 + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", + "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", + "dependencies": { + "cookie": "0.4.1", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/csrf": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz", + "integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==", + "dependencies": { + "rndm": "1.2.0", + "tsscmp": "1.0.6", + "uid-safe": "2.1.5" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csurf": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/csurf/-/csurf-1.11.0.tgz", + "integrity": "sha512-UCtehyEExKTxgiu8UHdGvHj4tnpE/Qctue03Giq5gPgMQ9cg/ciod5blZQ5a4uCEenNQjxyGuzygLdKUmee/bQ==", + "deprecated": "Please use another csrf package", + "dependencies": { + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "csrf": "3.1.0", + "http-errors": "~1.7.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/csurf/node_modules/cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/csurf/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/csurf/node_modules/http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/csurf/node_modules/setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "node_modules/csurf/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/csurf/node_modules/toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "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==", + "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/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "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==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "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, + "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==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "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==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "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==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.1.0.tgz", + "integrity": "sha512-g+HZqgfbpXdCkme/Cd/mZkV0aV3BZZZSugecH03kl38m/Kmdx8jKjBikpDj2cr+Iynv4KpYEviojNdTJActJAg==", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/hpagent": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hpagent/-/hpagent-1.2.0.tgz", + "integrity": "sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==", + "engines": { + "node": ">=14" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.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, + "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==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nodemon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.1.tgz", + "integrity": "sha512-g9AZ7HmkhQkqXkRc20w+ZfQ73cHLbE8hnPbtaFbFtCumZsjyMhKk9LajQ07U5Ux28lvFjZ5X7HvWR1xzU8jHVw==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^3.2.7", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "dev": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rndm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz", + "integrity": "sha512-fJhQQI5tLrQvYIYFpOnFinzv9dwmR7hRnUz1XqP3OJ1jIweTNOd6aTO4jwQSgcBSFUB+/KHJxuGneime+FdzOw==" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==" + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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==", + "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/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "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, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "dependencies": { + "nopt": "~1.0.10" + }, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "engines": { + "node": ">=0.6.x" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } +} diff --git a/notification-server/package.json b/notification-server/package.json new file mode 100644 index 00000000..4dc73421 --- /dev/null +++ b/notification-server/package.json @@ -0,0 +1,21 @@ +{ + "name": "notification-service", + "version": "1.0.0", + "scripts": { + "start": "node ./src/server.js", + "dev": "nodemon ./src/server.js" + }, + "dependencies": { + "@opensearch-project/opensearch": "^2.4.0", + "cookie-parser": "^1.4.6", + "cors": "^2.8.5", + "csurf": "^1.11.0", + "dotenv": "^16.3.1", + "express": "^4.19.2", + "helmet": "^7.1.0", + "uuid": "^9.0.1" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } +} diff --git a/notification-server/src/addOns.js b/notification-server/src/addOns.js new file mode 100644 index 00000000..aa3a531c --- /dev/null +++ b/notification-server/src/addOns.js @@ -0,0 +1,24 @@ +const { searchNotification } = require("./openSearch"); +const { serverConfig } = require("./config"); + +function buildNotificationSearchInterval({ + sessionId, + interval = serverConfig.refreshInterval, +}) { + return ({ connectionId, sender }) => { + const intervalHandle = setInterval( + () => + searchNotification({ + connectionId, + sessionId, + sender, + }), + interval + ); + return () => clearInterval(intervalHandle); + }; +} + +module.exports = { + buildNotificationSearchInterval, +}; diff --git a/notification-server/src/config.js b/notification-server/src/config.js new file mode 100644 index 00000000..86b3d3f1 --- /dev/null +++ b/notification-server/src/config.js @@ -0,0 +1,24 @@ +require("dotenv").config(); + +module.exports = { + openSearchConfig: { + datasetGroupProgress: "dataset_group_progress", + ssl: { + rejectUnauthorized: false, + }, + getUrl: () => { + const protocol = process.env.OPENSEARCH_PROTOCOL || "https"; + const username = process.env.OPENSEARCH_USERNAME || "admin"; + const password = process.env.OPENSEARCH_PASSWORD || "admin"; + const host = process.env.OPENSEARCH_HOST || "host.docker.internal"; + const port = process.env.OPENSEARCH_PORT || "9200"; + + return `${protocol}://${username}:${password}@${host}:${port}`; + }, + retry_on_conflict: 6, + }, + serverConfig: { + port: process.env.PORT || 4040, + refreshInterval: process.env.REFRESH_INTERVAL || 1000, + }, +}; diff --git a/notification-server/src/openSearch.js b/notification-server/src/openSearch.js new file mode 100644 index 00000000..fb16aab1 --- /dev/null +++ b/notification-server/src/openSearch.js @@ -0,0 +1,72 @@ +const { Client } = require("@opensearch-project/opensearch"); +const { openSearchConfig } = require("./config"); + +const client = new Client({ + node: openSearchConfig.getUrl(), + ssl: openSearchConfig.ssl, +}); + +async function searchNotification({ sessionId, connectionId, sender }) { + try { + const response = await client.search({ + index: openSearchConfig.datasetGroupProgress, + body: { + query: { + bool: { + must: { match: { sessionId } }, + must_not: { match: { sentTo: connectionId } }, + }, + }, + sort: { timestamp: { order: "asc" } }, + }, + }); + + for (const hit of response.body.hits.hits) { + console.log(`hit: ${JSON.stringify(hit)}`); + const jsonObject = { + sessionId: hit._source.sessionId, + progress: hit._source.progress, + }; + await sender(jsonObject); + await markAsSent(hit, connectionId); + } + } catch (e) { + console.error(e); + await sender({}); + } +} + +async function markAsSent({ _index, _id }, connectionId) { + await client.update({ + index: _index, + id: _id, + retry_on_conflict: openSearchConfig.retry_on_conflict, + body: { + script: { + source: `if (ctx._source.sentTo == null) { + ctx._source.sentTo = [params.connectionId]; + } else { + ctx._source.sentTo.add(params.connectionId); + }`, + lang: "painless", + params: { connectionId }, + }, + }, + }); +} + +async function updateProgress(sessionId, progress) { + await client.index({ + index: "dataset_group_progress", + body: { + sessionId, + progress, + timestamp: new Date(), + }, + }); +} + +module.exports = { + searchNotification, + updateProgress, +}; diff --git a/notification-server/src/server.js b/notification-server/src/server.js new file mode 100644 index 00000000..4c75fc0d --- /dev/null +++ b/notification-server/src/server.js @@ -0,0 +1,54 @@ +const express = require("express"); +const cors = require("cors"); +const { buildSSEResponse } = require("./sseUtil"); +const { serverConfig } = require("./config"); +const { buildNotificationSearchInterval } = require("./addOns"); +const { updateProgress } = require("./openSearch"); +const helmet = require("helmet"); +const cookieParser = require("cookie-parser"); +const csurf = require("csurf"); + +const app = express(); + +app.use(cors()); +app.use(helmet.hidePoweredBy()); +app.use(express.json({ extended: false })); +app.use(cookieParser()); +app.use(csurf({ cookie: true })); + +app.get("/sse/notifications/:sessionId", (req, res) => { + const { sessionId } = req.params; + console.log(`session id: ${sessionId}`); + buildSSEResponse({ + req, + res, + buildCallbackFunction: buildNotificationSearchInterval({ sessionId }), + }); +}); + +app.get("/csrf-token", (req, res) => { + res.json({ csrfToken: req.csrfToken() }); +}); + +// Endpoint to update the dataset_group_progress index +app.post("/dataset-group/update-progress", async (req, res) => { + const { sessionId, progress } = req.body; + + if (!sessionId || progress === undefined) { + return res.status(400).json({ error: "Missing required fields" }); + } + + try { + await updateProgress(sessionId, progress); + res.status(201).json({ message: "Document created successfully" }); + } catch (error) { + console.error("Error creating document:", error); + res.status(500).json({ error: "Failed to create document" }); + } +}); + +const server = app.listen(serverConfig.port, () => { + console.log(`Server running on port ${serverConfig.port}`); +}); + +module.exports = server; diff --git a/notification-server/src/sseUtil.js b/notification-server/src/sseUtil.js new file mode 100644 index 00000000..a25569cb --- /dev/null +++ b/notification-server/src/sseUtil.js @@ -0,0 +1,53 @@ +const { v4: uuidv4 } = require("uuid"); + +function buildSSEResponse({ res, req, buildCallbackFunction }) { + addSSEHeader(req, res); + keepStreamAlive(res); + const connectionId = generateConnectionID(); + const sender = buildSender(res); + + const cleanUp = buildCallbackFunction({ connectionId, sender }); + + req.on("close", () => { + console.log("Client disconnected from SSE"); + cleanUp?.(); + }); +} + +function addSSEHeader(req, res) { + const origin = extractOrigin(req.headers.origin); + + res.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + "Access-Control-Allow-Origin": origin, + "Access-Control-Allow-Credentials": true, + "Access-Control-Expose-Headers": + "Origin, X-Requested-With, Content-Type, Cache-Control, Connection, Accept", + }); +} + +function extractOrigin(reqOrigin) { + const corsWhitelist = process.env.CORS_WHITELIST_ORIGINS.split(","); + const whitelisted = corsWhitelist.indexOf(reqOrigin) !== -1; + return whitelisted ? reqOrigin : "*"; +} + +function keepStreamAlive(res) { + res.write(""); +} + +function generateConnectionID() { + const connectionId = uuidv4(); + console.log(`New client connected with connectionId: ${connectionId}`); + return connectionId; +} + +function buildSender(res) { + return (data) => res.write(`data: ${JSON.stringify(data)}\n\n`); +} + +module.exports = { + buildSSEResponse, +}; From 4c571ae040f27d099e36fd6448e7395369e62579 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Mon, 29 Jul 2024 17:46:19 +0530 Subject: [PATCH 306/582] demo fixes --- GUI/.env.development | 2 +- .../FormElements/DynamicForm/index.tsx | 7 ++- .../ValidationCriteria/CardsView.tsx | 4 +- .../molecules/ValidationCriteria/RowsView.tsx | 6 +- .../molecules/ValidationSessionCard/index.tsx | 52 ++++++++++++++++ .../DatasetGroups/CreateDatasetGroup.tsx | 4 +- .../pages/DatasetGroups/ViewDatasetGroup.tsx | 21 ++++--- GUI/src/pages/UserManagement/index.tsx | 2 +- GUI/src/pages/ValidationSessions/index.tsx | 60 +++++++++++++++++-- GUI/src/styles/generic/_base.scss | 4 ++ GUI/translations/en/common.json | 24 +------- dataset-processor/dataset_processor.py | 8 +-- dataset-processor/dataset_processor_api.py | 13 ++-- docker-compose.yml | 26 ++++---- file-handler/file_handler_api.py | 4 +- 15 files changed, 164 insertions(+), 73 deletions(-) create mode 100644 GUI/src/components/molecules/ValidationSessionCard/index.tsx diff --git a/GUI/.env.development b/GUI/.env.development index 3310ca76..efa8ea86 100644 --- a/GUI/.env.development +++ b/GUI/.env.development @@ -5,4 +5,4 @@ REACT_APP_CUSTOMER_SERVICE_LOGIN=http://localhost:3004/et/dev-auth REACT_APP_SERVICE_ID=conversations,settings,monitoring REACT_APP_NOTIFICATION_NODE_URL=http://localhost:4040 REACT_APP_CSP=upgrade-insecure-requests; default-src 'self'; font-src 'self' data:; img-src 'self' data:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self' http://localhost:8086 http://localhost:8088 http://localhost:8085 http://localhost:4040 http://localhost:3002/menu.json; -REACT_APP_ENABLE_HIDDEN_FEATURES=TRUE +REACT_APP_ENABLE_HIDDEN_FEATURES=TRUE \ No newline at end of file diff --git a/GUI/src/components/FormElements/DynamicForm/index.tsx b/GUI/src/components/FormElements/DynamicForm/index.tsx index b1adff6f..90f938b8 100644 --- a/GUI/src/components/FormElements/DynamicForm/index.tsx +++ b/GUI/src/components/FormElements/DynamicForm/index.tsx @@ -17,13 +17,17 @@ const DynamicForm: React.FC = ({ formData, onSubmit,setPatchUp return ( +
    + +
    ); }; @@ -31,12 +35,9 @@ const DynamicForm: React.FC = ({ formData, onSubmit,setPatchUp
    {Object.keys(formData).map((key) => (
    - {key.toLowerCase() !== 'rowid' && (
    - {renderInput(key, formData[key])}
    - )}
    ))} diff --git a/GUI/src/components/molecules/ValidationCriteria/CardsView.tsx b/GUI/src/components/molecules/ValidationCriteria/CardsView.tsx index 2d7f5fca..feeea79c 100644 --- a/GUI/src/components/molecules/ValidationCriteria/CardsView.tsx +++ b/GUI/src/components/molecules/ValidationCriteria/CardsView.tsx @@ -9,6 +9,7 @@ import Button from 'components/Button'; import { ValidationRule } from 'types/datasetGroups'; import { Link } from 'react-router-dom'; import { isFieldNameExisting } from 'utils/datasetGroupsUtils'; +import { v4 as uuidv4 } from 'uuid'; const ItemTypes = { ITEM: 'item', @@ -143,10 +144,9 @@ const ValidationCriteriaCardsView: FC< const addNewClass = () => { setValidationRuleError(false); - const newId = validationRules[validationRules?.length - 1]?.id + 1; const updatedItems = [ ...validationRules, - { id: newId, fieldName: '', dataType: '', isDataClass: false }, + { id: uuidv4(), fieldName: '', dataType: '', isDataClass: false }, ]; setValidationRules(updatedItems); }; diff --git a/GUI/src/components/molecules/ValidationCriteria/RowsView.tsx b/GUI/src/components/molecules/ValidationCriteria/RowsView.tsx index d76cc228..e548dc1b 100644 --- a/GUI/src/components/molecules/ValidationCriteria/RowsView.tsx +++ b/GUI/src/components/molecules/ValidationCriteria/RowsView.tsx @@ -50,12 +50,10 @@ const ValidationCriteriaRowsView: FC> = }; const addNewClass = () => { - setValidationRuleError(false) - const newId = validationRules[validationRules?.length - 1]?.id + 1; - + setValidationRuleError(false) const updatedItems = [ ...validationRules, - { id: newId, fieldName: '', dataType: '', isDataClass: false }, + { id: uuidv4(), fieldName: '', dataType: '', isDataClass: false }, ]; diff --git a/GUI/src/components/molecules/ValidationSessionCard/index.tsx b/GUI/src/components/molecules/ValidationSessionCard/index.tsx new file mode 100644 index 00000000..fac09a84 --- /dev/null +++ b/GUI/src/components/molecules/ValidationSessionCard/index.tsx @@ -0,0 +1,52 @@ +import { FC, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import ProgressBar from 'components/ProgressBar'; +import { Card, Label } from 'components'; + +type ValidationSessionCardProps = { + dgName:string; + version:string; + isLatest:boolean; + status?:string; + errorMessage?: string; + progress: number; + }; + +const ValidationSessionCard: React.FC = ({dgName,version,isLatest,status,errorMessage,progress}) => { + const { t } = useTranslation(); + + return ( + + {dgName} + {isLatest &&( + + )} + {status==="failed" &&( + + )} + + } + > +
    + {status==="failed" ? ( +
    + {errorMessage} +
    + ) : ( +
    +
    Validation In-Progress
    + +
    + )} +
    +
    + ); +}; + +export default ValidationSessionCard; diff --git a/GUI/src/pages/DatasetGroups/CreateDatasetGroup.tsx b/GUI/src/pages/DatasetGroups/CreateDatasetGroup.tsx index 7a352fbe..5ab886d5 100644 --- a/GUI/src/pages/DatasetGroups/CreateDatasetGroup.tsx +++ b/GUI/src/pages/DatasetGroups/CreateDatasetGroup.tsx @@ -26,8 +26,8 @@ const CreateDatasetGroup: FC = () => { const navigate = useNavigate(); const initialValidationRules = [ - { id: 1, fieldName: '', dataType: '', isDataClass: false }, - { id: 2, fieldName: '', dataType: '', isDataClass: true }, + { id: uuidv4(), fieldName: '', dataType: '', isDataClass: false }, + { id: uuidv4(), fieldName: '', dataType: '', isDataClass: true }, ]; const initialClass = [ diff --git a/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx b/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx index 1c285522..f9d1ecb8 100644 --- a/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx +++ b/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx @@ -71,7 +71,6 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { const [validationRuleError, setValidationRuleError] = useState(false); const [nodesError, setNodesError] = useState(false); - const [updatedData, setUpdatedData] = useState(''); const [importFormat, setImportFormat] = useState(''); const [exportFormat, setExportFormat] = useState(''); const [importStatus, setImportStatus] = useState(''); @@ -202,14 +201,18 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { }, [metadata]); const patchDataUpdate = (dataset) => { - const payload = { + const payload= datasets?.dataPayload?.map((row) => + row.rowID === selectedRow?.rowID ? dataset : row + ); + + const updatedPayload = { dgId, - updateDataPayload: { - rowID: selectedRow?.rowID, - ...dataset, - }, + updateDataPayload: payload }; - patchUpdateMutation.mutate(payload); + patchUpdateMutation.mutate(updatedPayload); + + console.log(updatedPayload); + }; const patchUpdateMutation = useMutation({ @@ -219,6 +222,7 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { setPatchUpdateModalOpen(false); }, onError: () => { + setPatchUpdateModalOpen(false); open({ title: 'Patch Data Update Unsuccessful', content:

    Something went wrong. Please try again.

    , @@ -261,7 +265,6 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { appearance="text" onClick={() => { setSelectedRow(props.row.original); - setUpdatedData(props.row.original?.Country); setPatchUpdateModalOpen(true); }} > @@ -554,7 +557,7 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { setPagination(state); getDatasets(state, dgId); }} - pagesCount={10} + pagesCount={datasets?.numPages} isClientSide={false} /> )} diff --git a/GUI/src/pages/UserManagement/index.tsx b/GUI/src/pages/UserManagement/index.tsx index 074e523c..19d09091 100644 --- a/GUI/src/pages/UserManagement/index.tsx +++ b/GUI/src/pages/UserManagement/index.tsx @@ -94,7 +94,7 @@ const UserManagement: FC = () => { } ), columnHelper.accessor('useridcode', { - header: t('settings.users.idCode') ?? '', + header: t('global.idCode') ?? '', }), columnHelper.accessor( (data: { authorities: ROLES[] }) => { diff --git a/GUI/src/pages/ValidationSessions/index.tsx b/GUI/src/pages/ValidationSessions/index.tsx index 4134030e..c5ce3b63 100644 --- a/GUI/src/pages/ValidationSessions/index.tsx +++ b/GUI/src/pages/ValidationSessions/index.tsx @@ -1,20 +1,68 @@ import { FC, useState } from 'react'; import { useTranslation } from 'react-i18next'; import ProgressBar from 'components/ProgressBar'; +import { Card, Label } from 'components'; +import ValidationSessionCard from 'components/molecules/ValidationSessionCard'; const ValidationSessions: FC = () => { const { t } = useTranslation(); const [progress, setProgress] = useState(40); + const data = [ + { + dgName: 'Dataset Group Alpha', + version: 'V5.3.1', + isLatest: true, + status: '', + errorMessage: '', + progress: 30, + }, + { + dgName: 'Dataset Group 1', + version: 'V5.3.1', + isLatest: true, + status: '', + errorMessage: '', + progress: 50, + }, + { + dgName: 'Dataset Group 2', + version: 'V5.3.1', + isLatest: true, + status: 'failed', + errorMessage: + 'Validation failed because “complaints” class found in the “department” column does not exist in hierarchy', + progress: 30, + }, + { + dgName: 'Dataset Group 3', + version: 'V5.3.1', + isLatest: false, + status: '', + errorMessage: '', + progress: 80, + }, + ]; return ( -
    -
    -
    Validation Sessions
    +
    +
    +
    +
    Validation Sessions
    +
    + {data?.map((session) => { + return ( + + ); + })}
    - - -
    ); }; diff --git a/GUI/src/styles/generic/_base.scss b/GUI/src/styles/generic/_base.scss index 0641e8b1..dd22d40f 100644 --- a/GUI/src/styles/generic/_base.scss +++ b/GUI/src/styles/generic/_base.scss @@ -67,6 +67,10 @@ body { gap: 10px; } +.text-center { + text-align: center; +} + a, input, select, diff --git a/GUI/translations/en/common.json b/GUI/translations/en/common.json index d76d2d1e..31e0257c 100644 --- a/GUI/translations/en/common.json +++ b/GUI/translations/en/common.json @@ -8,9 +8,6 @@ "confirm": "Confirm", "modifiedAt": "Last modified at", "addNew": "Add new", - "dependencies": "Dependencies", - "language": "Language", - "choose": "Choose", "search": "Search", "notification": "Notification", "notificationError": "Error", @@ -31,7 +28,6 @@ "name": "Name", "idCode": "ID code", "status": "Status", - "statusChangeQuestion": "Would you like to change your status to \"present\"?", "yes": "Yes", "no": "No", "removeValidation": "Are you sure?", @@ -39,21 +35,10 @@ "endDate": "End date", "preview": "Preview", "logout": "Logout", - "anonymous": "Anonymous", - "csaStatus": "Customer support status", - "present": "Present", - "away": "Away", - "today": "Today", - "forward": "Forward", - "chosen": "Chosen", - "read": "Read" - }, - "mainMenu": { - "menuLabel": "Main navigation", - "closeMenu": "Close menu", - "openIcon": "Open menu icon", - "closeIcon": "Close menu icon" + "choose": "Choose" + }, + "menu": { "userManagement": "User Management", "integration": "Integration", @@ -122,9 +107,6 @@ "toast": { "success": { "updateSuccess": "Updated Successfully", - "messageToUserEmail": "Message sent to user email", - "chatStatusChanged": "Chat status changed", - "chatCommentChanged": "Chat comment changed", "copied": "Copied", "userDeleted": "User deleted", "newUserAdded": "New user added", diff --git a/dataset-processor/dataset_processor.py b/dataset-processor/dataset_processor.py index 035af0d4..db05580a 100644 --- a/dataset-processor/dataset_processor.py +++ b/dataset-processor/dataset_processor.py @@ -144,7 +144,7 @@ def get_validation_data(self, dgID, custom_jwt_cookie): try: params = {'dgId': dgID} headers = { - 'cookie': f'customJwtCookie={custom_jwt_cookie}' + 'cookie': custom_jwt_cookie } response = requests.get(GET_VALIDATION_SCHEMA, params=params, headers=headers) response.raise_for_status() @@ -156,7 +156,7 @@ def get_validation_data(self, dgID, custom_jwt_cookie): def get_dataset(self, dg_id, custom_jwt_cookie): params = {'dgId': dg_id} headers = { - 'cookie': f'customJwtCookie={custom_jwt_cookie}' + 'cookie': custom_jwt_cookie } try: @@ -170,7 +170,7 @@ def get_dataset(self, dg_id, custom_jwt_cookie): def get_dataset_by_location(self, fileLocation, custom_jwt_cookie): params = {'saveLocation': fileLocation} headers = { - 'cookie': f'customJwtCookie={custom_jwt_cookie}' + 'cookie': custom_jwt_cookie } try: @@ -269,7 +269,7 @@ def update_preprocess_status(self,dg_id, cookie, processed_data_available, raw_d print(url) headers = { 'Content-Type': 'application/json', - 'Cookie': f'customJwtCookie={cookie}' + 'Cookie': cookie } data = { "dgId": dg_id, diff --git a/dataset-processor/dataset_processor_api.py b/dataset-processor/dataset_processor_api.py index fc276ec1..5171d3c1 100644 --- a/dataset-processor/dataset_processor_api.py +++ b/dataset-processor/dataset_processor_api.py @@ -63,7 +63,7 @@ async def forward_request(request: Request, response: Response): headers = { - 'cookie': f'customJwtCookie={payload["cookie"]}', + 'cookie': payload["cookie"], 'Content-Type': 'application/json' } @@ -83,13 +83,16 @@ async def forward_request(request: Request, response: Response): try: print("8") forward_response = requests.post(forward_url, json=payload2, headers=headers) - print("8") + print(headers) + print("9") forward_response.raise_for_status() - print("8") + print("10") return JSONResponse(content=forward_response.json(), status_code=forward_response.status_code) except requests.HTTPError as e: - print("9") + print("11") + print(e) raise HTTPException(status_code=e.response.status_code, detail=e.response.text) except Exception as e: - print("9") + print("12") + print(e) raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 32271afb..26224b76 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -158,19 +158,19 @@ services: - bykstack restart: always - # cron-manager: - # container_name: cron-manager - # image: cron-manager - # volumes: - # - ./DSL/CronManager/DSL:/DSL - # - ./DSL/CronManager/script:/app/scripts - # - ./DSL/CronManager/config:/app/config - # environment: - # - server.port=9010 - # ports: - # - 9010:8080 - # networks: - # - bykstack + cron-manager: + container_name: cron-manager + image: cron-manager + volumes: + - ./DSL/CronManager/DSL:/DSL + - ./DSL/CronManager/script:/app/scripts + - ./DSL/CronManager/config:/app/config + environment: + - server.port=9010 + ports: + - 9010:8080 + networks: + - bykstack init: image: busybox diff --git a/file-handler/file_handler_api.py b/file-handler/file_handler_api.py index 62b96fd3..fdc85d42 100644 --- a/file-handler/file_handler_api.py +++ b/file-handler/file_handler_api.py @@ -62,8 +62,8 @@ async def authenticate_user(request: Request): } response = requests.get(url, headers=headers) - if response.status_code != 200: - raise HTTPException(status_code=response.status_code, detail="Authentication failed") + # if response.status_code != 200: + # raise HTTPException(status_code=response.status_code, detail="Authentication failed") @app.post("/datasetgroup/data/import") async def upload_and_copy(request: Request, dgId: int = Form(...), dataFile: UploadFile = File(...)): From 0236bccdbd57ff25d9cd10041dbb234105096409 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 29 Jul 2024 17:50:09 +0530 Subject: [PATCH 307/582] updates for the data processor and bug fixes --- .../classifier/datasetgroup/update/minor.yml | 5 ++ dataset-processor/Dockerfile | 2 +- dataset-processor/data_enrichment/Dockerfile | 10 +-- .../enrichment_requirements.txt | 57 ++++++++++++++++ .../data_enrichment/requirements.txt | 8 --- dataset-processor/dataset_processor.py | 41 ++++++------ dataset-processor/dataset_processor_api.py | 14 ++-- dataset-processor/requirements.txt | 67 ++++++++++++++++--- dataset-processor/s3_mock.py | 30 ++++----- docker-compose.yml | 26 +++---- file-handler/file_handler_api.py | 7 +- migrate_win.sh | 37 ---------- 12 files changed, 185 insertions(+), 119 deletions(-) create mode 100644 dataset-processor/data_enrichment/enrichment_requirements.txt delete mode 100644 dataset-processor/data_enrichment/requirements.txt delete mode 100644 migrate_win.sh diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/minor.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/minor.yml index 982d222d..9322f45d 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/minor.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/minor.yml @@ -119,6 +119,11 @@ return_ok: return: ${format_res} next: end +return_not_found: + status: 400 + return: "Data Group Not Found" + next: end + return_bad_request: status: 400 return: ${format_res} diff --git a/dataset-processor/Dockerfile b/dataset-processor/Dockerfile index 7187e2e5..49264ae6 100644 --- a/dataset-processor/Dockerfile +++ b/dataset-processor/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.10-alpine +FROM python:3.12.4-bookworm WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt diff --git a/dataset-processor/data_enrichment/Dockerfile b/dataset-processor/data_enrichment/Dockerfile index 84d7db2a..47b9a5e9 100644 --- a/dataset-processor/data_enrichment/Dockerfile +++ b/dataset-processor/data_enrichment/Dockerfile @@ -1,13 +1,13 @@ -FROM python:3.10-slim +FROM python:3.12.4-bookworm WORKDIR /app -COPY requirements.txt . +COPY enrichment_requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt +RUN pip install --no-cache-dir -r enrichment_requirements.txt COPY . . -EXPOSE 8500 +EXPOSE 8002 -CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8500"] +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8002"] diff --git a/dataset-processor/data_enrichment/enrichment_requirements.txt b/dataset-processor/data_enrichment/enrichment_requirements.txt new file mode 100644 index 00000000..dfe35b0d --- /dev/null +++ b/dataset-processor/data_enrichment/enrichment_requirements.txt @@ -0,0 +1,57 @@ +annotated-types==0.7.0 +anyio==4.4.0 +certifi==2024.7.4 +charset-normalizer==3.3.2 +click==8.1.7 +colorama==0.4.6 +dnspython==2.6.1 +email_validator==2.2.0 +exceptiongroup==1.2.2 +fastapi==0.111.1 +fastapi-cli==0.0.4 +filelock==3.15.4 +fsspec==2024.6.1 +h11==0.14.0 +httpcore==1.0.5 +httptools==0.6.1 +httpx==0.27.0 +huggingface-hub==0.24.2 +idna==3.7 +Jinja2==3.1.4 +langdetect==1.0.9 +markdown-it-py==3.0.0 +MarkupSafe==2.1.5 +mdurl==0.1.2 +mpmath==1.3.0 +networkx==3.2.1 +numpy==2.0.1 +packaging==24.1 +pillow==10.2.0 +pydantic==2.8.2 +pydantic_core==2.20.1 +Pygments==2.18.0 +python-dotenv==1.0.1 +python-multipart==0.0.9 +PyYAML==6.0.1 +regex==2024.7.24 +requests==2.32.3 +rich==13.7.1 +safetensors==0.4.3 +sentencepiece==0.2.0 +shellingham==1.5.4 +six==1.16.0 +sniffio==1.3.1 +starlette==0.37.2 +sympy==1.12 +tokenizers==0.19.1 +torch==2.4.0 +torchaudio==2.4.0 +torchvision==0.19.0 +tqdm==4.66.4 +transformers==4.43.3 +typer==0.12.3 +typing_extensions==4.12.2 +urllib3==2.2.2 +uvicorn==0.30.3 +watchfiles==0.22.0 +websockets==12.0 diff --git a/dataset-processor/data_enrichment/requirements.txt b/dataset-processor/data_enrichment/requirements.txt deleted file mode 100644 index 690d16ef..00000000 --- a/dataset-processor/data_enrichment/requirements.txt +++ /dev/null @@ -1,8 +0,0 @@ -fastapi -pydantic -requests -boto3 -langdetect -transformers -torch -sentencepiece \ No newline at end of file diff --git a/dataset-processor/dataset_processor.py b/dataset-processor/dataset_processor.py index 035af0d4..113de3b6 100644 --- a/dataset-processor/dataset_processor.py +++ b/dataset-processor/dataset_processor.py @@ -2,7 +2,7 @@ import os import json import requests -# from data_enrichment.data_enrichment import DataEnrichment +from data_enrichment.data_enrichment import DataEnrichment from constants import * from s3_mock import S3FileCounter @@ -19,7 +19,7 @@ class DatasetProcessor: def __init__(self): - # self.data_enricher = DataEnrichment() + self.data_enricher = DataEnrichment() self.s3_file_counter = S3FileCounter() def check_and_convert(self, data): @@ -83,8 +83,8 @@ def enrich_data(self, data, selected_fields, record_count): enriched_entry = {} for key, value in entry.items(): if isinstance(value, str) and (key in selected_fields): - # enriched_value = self.data_enricher.enrich_data(value, num_return_sequences=1, language_id='en') - enriched_value = ["enrichupdate"] + enriched_value = self.data_enricher.enrich_data(value, num_return_sequences=1, language_id='en') + # enriched_value = ["enrichupdate"] enriched_entry[key] = enriched_value[0] if enriched_value else value else: enriched_entry[key] = value @@ -128,7 +128,6 @@ def save_chunked_data(self, chunked_data, cookie, dgID, exsistingChunks=0): def get_selected_data_fields(self, dgID:int, cookie:str): try: - # return ["Subject","Body"] data_dict = self.get_validation_data(dgID, cookie) validation_rules = data_dict.get("response", {}).get("validationCriteria", {}).get("validationRules", {}) text_fields = [] @@ -144,7 +143,7 @@ def get_validation_data(self, dgID, custom_jwt_cookie): try: params = {'dgId': dgID} headers = { - 'cookie': f'customJwtCookie={custom_jwt_cookie}' + 'cookie': custom_jwt_cookie } response = requests.get(GET_VALIDATION_SCHEMA, params=params, headers=headers) response.raise_for_status() @@ -198,22 +197,22 @@ def get_stopwords(self, dg_id, custom_jwt_cookie): return None def get_page_count(self, dg_id, custom_jwt_cookie): - # params = {'dgId': dg_id} - # headers = { - # 'cookie': f'customJwtCookie={custom_jwt_cookie}' - # } + params = {'dgId': dg_id} + headers = { + 'cookie': f'customJwtCookie={custom_jwt_cookie}' + } - # try: - # page_count_url = GET_PAGE_COUNT_URL.replace("{dgif}",str(dg_id)) - # response = requests.get(page_count_url, headers=headers) - # response.raise_for_status() - # data = response.json() - # page_count = data["numpages"] - # return page_count try: - folder_path = f'data/dataset/{dg_id}/chunks/' - file_count = self.s3_file_counter.count_files_in_folder(folder_path) - return file_count + page_count_url = GET_PAGE_COUNT_URL.replace("{dgid}",str(dg_id)) + response = requests.get(page_count_url, headers=headers) + response.raise_for_status() + data = response.json() + page_count = data["numpages"] + return page_count + # try: + # folder_path = f'data/dataset/{dg_id}/chunks/' + # file_count = self.s3_file_counter.count_files_in_folder(folder_path) + # return file_count except requests.exceptions.RequestException as e: print(f"An error occurred: {e}") return None @@ -269,7 +268,7 @@ def update_preprocess_status(self,dg_id, cookie, processed_data_available, raw_d print(url) headers = { 'Content-Type': 'application/json', - 'Cookie': f'customJwtCookie={cookie}' + 'Cookie': cookie } data = { "dgId": dg_id, diff --git a/dataset-processor/dataset_processor_api.py b/dataset-processor/dataset_processor_api.py index 7c5a55ed..4edb2094 100644 --- a/dataset-processor/dataset_processor_api.py +++ b/dataset-processor/dataset_processor_api.py @@ -5,7 +5,6 @@ from dataset_processor import DatasetProcessor import requests import os -import httpx app = FastAPI() processor = DatasetProcessor() @@ -37,15 +36,15 @@ async def authenticate_user(request: Request): } response = requests.get(url, headers=headers) - # if response.status_code != 200: - # raise HTTPException(status_code=response.status_code, detail="Authentication failed") + if response.status_code != 200: + raise HTTPException(status_code=response.status_code, detail="Authentication failed") @app.post("/init-dataset-process") async def process_handler_endpoint(request: Request): print("in init dataset") payload = await request.json() print(payload) - # await authenticate_user(process_request) + await authenticate_user(request) authCookie = payload["cookie"] result = processor.process_handler(int(payload["dgID"]), authCookie, payload["updateType"], payload["savedFilePath"], payload["patchPayload"]) @@ -63,7 +62,7 @@ async def forward_request(request: Request, response: Response): headers = { - 'cookie': f'{payload["cookie"]}', + 'cookie': payload["cookie"], 'Content-Type': 'application/json' } @@ -82,9 +81,8 @@ async def forward_request(request: Request, response: Response): try: print("8") - forward_response = requests.post(forward_url, json=payload2, headers= - ) - print() + forward_response = requests.post(forward_url, json=payload2, headers=headers) + print(headers) print("9") forward_response.raise_for_status() print("10") diff --git a/dataset-processor/requirements.txt b/dataset-processor/requirements.txt index 2f6921cd..5911b077 100644 --- a/dataset-processor/requirements.txt +++ b/dataset-processor/requirements.txt @@ -1,8 +1,59 @@ -fastapi -pydantic -requests -boto3 -# langdetect -# transformers -# torch -# sentencepiece \ No newline at end of file +annotated-types==0.7.0 +anyio==4.4.0 +certifi==2024.7.4 +charset-normalizer==3.3.2 +click==8.1.7 +colorama==0.4.6 +dnspython==2.6.1 +email_validator==2.2.0 +fastapi==0.111.1 +fastapi-cli==0.0.4 +filelock==3.15.4 +fsspec==2024.6.1 +h11==0.14.0 +httpcore==1.0.5 +httptools==0.6.1 +httpx==0.27.0 +huggingface-hub==0.24.2 +idna==3.7 +Jinja2==3.1.4 +joblib==1.4.2 +sacremoses==0.1.1 +langdetect==1.0.9 +markdown-it-py==3.0.0 +MarkupSafe==2.1.5 +mdurl==0.1.2 +mpmath==1.3.0 +networkx==3.3 +numpy==1.26.4 +packaging==24.1 +pillow==10.4.0 +pydantic==2.8.2 +pydantic_core==2.20.1 +Pygments==2.18.0 +python-dotenv==1.0.1 +python-multipart==0.0.9 +PyYAML==6.0.1 +regex==2024.7.24 +requests==2.32.3 +rich==13.7.1 +safetensors==0.4.3 +sentencepiece==0.2.0 +setuptools==69.5.1 +shellingham==1.5.4 +sniffio==1.3.1 +starlette==0.37.2 +sympy==1.13.1 +tokenizers==0.19.1 +torch==2.4.0 +torchaudio==2.4.0 +torchvision==0.19.0 +tqdm==4.66.4 +transformers==4.43.3 +typer==0.12.3 +typing_extensions==4.12.2 +urllib3==2.2.2 +uvicorn==0.30.3 +watchfiles==0.22.0 +websockets==12.0 +wheel==0.43.0 diff --git a/dataset-processor/s3_mock.py b/dataset-processor/s3_mock.py index c1921c25..8d3af534 100644 --- a/dataset-processor/s3_mock.py +++ b/dataset-processor/s3_mock.py @@ -1,6 +1,6 @@ import os -import boto3 -from botocore.exceptions import NoCredentialsError, PartialCredentialsError +# import boto3 +# from botocore.exceptions import NoCredentialsError, PartialCredentialsError class S3FileCounter: def __init__(self): @@ -12,12 +12,12 @@ def __init__(self): if not all([self.s3_access_key_id, self.s3_secret_access_key, self.bucket_name, self.region_name]): raise ValueError("Missing one or more environment variables: S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_BUCKET_NAME, S3_REGION_NAME") - self.s3_client = boto3.client( - 's3', - aws_access_key_id=self.s3_access_key_id, - aws_secret_access_key=self.s3_secret_access_key, - region_name=self.region_name - ) + # self.s3_client = boto3.client( + # 's3', + # aws_access_key_id=self.s3_access_key_id, + # aws_secret_access_key=self.s3_secret_access_key, + # region_name=self.region_name + # ) def count_files_in_folder(self, folder_path): try: @@ -26,15 +26,15 @@ def count_files_in_folder(self, folder_path): return len(response['Contents']) else: return 0 - except NoCredentialsError: - print("Credentials not available") - return 0 - except PartialCredentialsError: - print("Incomplete credentials provided") - return 0 + # except NoCredentialsError: + # print("Credentials not available") + # return 0 + # except PartialCredentialsError: + # print("Incomplete credentials provided") + # return 0 except Exception as e: print(f"An error occurred: {e}") - return 0 + return 20 # Example usage: # Ensure the environment variables are set before running the script diff --git a/docker-compose.yml b/docker-compose.yml index 32271afb..26224b76 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -158,19 +158,19 @@ services: - bykstack restart: always - # cron-manager: - # container_name: cron-manager - # image: cron-manager - # volumes: - # - ./DSL/CronManager/DSL:/DSL - # - ./DSL/CronManager/script:/app/scripts - # - ./DSL/CronManager/config:/app/config - # environment: - # - server.port=9010 - # ports: - # - 9010:8080 - # networks: - # - bykstack + cron-manager: + container_name: cron-manager + image: cron-manager + volumes: + - ./DSL/CronManager/DSL:/DSL + - ./DSL/CronManager/script:/app/scripts + - ./DSL/CronManager/config:/app/config + environment: + - server.port=9010 + ports: + - 9010:8080 + networks: + - bykstack init: image: busybox diff --git a/file-handler/file_handler_api.py b/file-handler/file_handler_api.py index fdc85d42..aceab837 100644 --- a/file-handler/file_handler_api.py +++ b/file-handler/file_handler_api.py @@ -17,7 +17,7 @@ app.add_middleware( CORSMiddleware, - allow_origins=["http://localhost:3001", "http://localhost:3002"], + allow_origins=["*"], allow_credentials=True, allow_methods=["GET", "POST"], allow_headers=["*"], @@ -62,8 +62,8 @@ async def authenticate_user(request: Request): } response = requests.get(url, headers=headers) - # if response.status_code != 200: - # raise HTTPException(status_code=response.status_code, detail="Authentication failed") + if response.status_code != 200: + raise HTTPException(status_code=response.status_code, detail="Authentication failed") @app.post("/datasetgroup/data/import") async def upload_and_copy(request: Request, dgId: int = Form(...), dataFile: UploadFile = File(...)): @@ -172,6 +172,7 @@ async def download_and_convert(request: Request, dgId: int, background_tasks: Ba @app.get("/datasetgroup/data/download/json/location") async def download_and_convert(request: Request, saveLocation:str, background_tasks: BackgroundTasks): + print(request) await authenticate_user(request) print(saveLocation) diff --git a/migrate_win.sh b/migrate_win.sh deleted file mode 100644 index a2185b7e..00000000 --- a/migrate_win.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash - -# Define the path where the SQL file will be generated -SQL_FILE="DSL/Liquibase/data/update_refresh_token.sql" - -# Read the OUTLOOK_REFRESH_KEY value from the INI file -OUTLOOK_REFRESH_KEY=$(awk -F '=' '/OUTLOOK_REFRESH_KEY/ {print $2}' constants.ini | xargs) - -# Generate a SQL script with the extracted value -cat << EOF > "$SQL_FILE" --- Update the refresh token in the database -UPDATE integration_status -SET token = '$OUTLOOK_REFRESH_KEY' -WHERE platform='OUTLOOK'; -EOF - -# Function to parse ini file and extract the value for a given key -get_ini_value() { - local file=$1 - local key=$2 - awk -F '=' -v key="$key" '$1 == key { gsub(/^[ \t]+|[ \t]+$/, "", $2); print $2; exit }' "$file" -} - -# Get the values from constants.ini -INI_FILE="constants.ini" -DB_PASSWORD=$(get_ini_value "$INI_FILE" "DB_PASSWORD") - -# Run the Liquibase update command using Docker -docker run --rm --network bykstack \ - -v "$(pwd)/DSL/Liquibase/changelog:/liquibase/changelog" \ - -v "$(pwd)/DSL/Liquibase/master.yml:/liquibase/master.yml" \ - -v "$(pwd)/DSL/Liquibase/data:/liquibase/data" \ - liquibase/liquibase \ - --defaultsFile=/liquibase/changelog/liquibase.properties \ - --changelog-file=master.yml \ - --url=jdbc:postgresql://users_db:5432/classifier?user=postgres \ - --password="$DB_PASSWORD" update From ecee7f43eacf994a26163a05a881e1152d723707 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 29 Jul 2024 19:07:12 +0530 Subject: [PATCH 308/582] disable data synthasis --- dataset-processor/dataset_processor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dataset-processor/dataset_processor.py b/dataset-processor/dataset_processor.py index 113de3b6..090dff46 100644 --- a/dataset-processor/dataset_processor.py +++ b/dataset-processor/dataset_processor.py @@ -2,7 +2,7 @@ import os import json import requests -from data_enrichment.data_enrichment import DataEnrichment +# from data_enrichment.data_enrichment import DataEnrichment from constants import * from s3_mock import S3FileCounter @@ -19,7 +19,7 @@ class DatasetProcessor: def __init__(self): - self.data_enricher = DataEnrichment() + # self.data_enricher = DataEnrichment() self.s3_file_counter = S3FileCounter() def check_and_convert(self, data): @@ -83,8 +83,8 @@ def enrich_data(self, data, selected_fields, record_count): enriched_entry = {} for key, value in entry.items(): if isinstance(value, str) and (key in selected_fields): - enriched_value = self.data_enricher.enrich_data(value, num_return_sequences=1, language_id='en') - # enriched_value = ["enrichupdate"] + # enriched_value = self.data_enricher.enrich_data(value, num_return_sequences=1, language_id='en') + enriched_value = ["enrichupdate"] enriched_entry[key] = enriched_value[0] if enriched_value else value else: enriched_entry[key] = value From 955fc03f9ee809684505dc224d9562e1251f7e18 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Mon, 29 Jul 2024 22:05:57 +0530 Subject: [PATCH 309/582] ESCLASS-145- dataset progress sessions Ruuter API's --- ...er-script-v8-dataset-progress-sessions.sql | 20 +++ DSL/OpenSearch/deploy-opensearch.sh | 8 +- ...ss.json => dataset_progress_sessions.json} | 5 +- .../mock/dataset_group_progress.json | 8 - .../mock/dataset_progress_sessions.json | 13 ++ .../delete-all-dataset-progress-sessions.sql | 1 + DSL/Resql/get-dataset-progress-sessions.sql | 13 ++ DSL/Resql/insert-dataset-progress-session.sql | 19 +++ DSL/Resql/update-dataset-progress-session.sql | 6 + .../GET/classifier/datasetgroup/progress.yml | 48 ++++++ .../datasetgroup/progress/create.yml | 147 ++++++++++++++++++ .../datasetgroup/progress/delete.yml | 46 ++++++ .../datasetgroup/progress/update.yml | 126 +++++++++++++++ constants.ini | 1 + docker-compose.yml | 2 +- notification-server/.env | 2 +- notification-server/index.js | 2 +- notification-server/src/config.js | 2 +- notification-server/src/openSearch.js | 16 +- notification-server/src/server.js | 19 ++- 20 files changed, 475 insertions(+), 29 deletions(-) create mode 100644 DSL/Liquibase/changelog/classifier-script-v8-dataset-progress-sessions.sql rename DSL/OpenSearch/fieldMappings/{dataset_group_progress.json => dataset_progress_sessions.json} (76%) delete mode 100644 DSL/OpenSearch/mock/dataset_group_progress.json create mode 100644 DSL/OpenSearch/mock/dataset_progress_sessions.json create mode 100644 DSL/Resql/delete-all-dataset-progress-sessions.sql create mode 100644 DSL/Resql/get-dataset-progress-sessions.sql create mode 100644 DSL/Resql/insert-dataset-progress-session.sql create mode 100644 DSL/Resql/update-dataset-progress-session.sql create mode 100644 DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/progress.yml create mode 100644 DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/progress/create.yml create mode 100644 DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/progress/delete.yml create mode 100644 DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/progress/update.yml diff --git a/DSL/Liquibase/changelog/classifier-script-v8-dataset-progress-sessions.sql b/DSL/Liquibase/changelog/classifier-script-v8-dataset-progress-sessions.sql new file mode 100644 index 00000000..e3bdf3e1 --- /dev/null +++ b/DSL/Liquibase/changelog/classifier-script-v8-dataset-progress-sessions.sql @@ -0,0 +1,20 @@ +-- liquibase formatted sql + +-- changeset kalsara Magamage:classifier-script-v8-changeset1 +CREATE TYPE Validation_Progress_Status AS ENUM ('Initiating Validation', 'Validation In-Progress', 'Cleaning Dataset', 'Generating Data', 'Success', 'Fail'); + +-- changeset kalsara Magamage:classifier-script-v8-changeset2 +CREATE TABLE dataset_progress_sessions ( + id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY, + dg_id BIGINT NOT NULL, + group_name TEXT NOT NULL, + major_version INT NOT NULL DEFAULT 0, + minor_version INT NOT NULL DEFAULT 0, + patch_version INT NOT NULL DEFAULT 0, + latest BOOLEAN DEFAULT false, + process_complete BOOLEAN DEFAULT false, + progress_percentage INT, + validation_status Validation_Progress_Status, + validation_message TEXT , + CONSTRAINT dataset_progress_sessions_pkey PRIMARY KEY (id) +); \ No newline at end of file diff --git a/DSL/OpenSearch/deploy-opensearch.sh b/DSL/OpenSearch/deploy-opensearch.sh index 7dd376a4..aebf6959 100644 --- a/DSL/OpenSearch/deploy-opensearch.sh +++ b/DSL/OpenSearch/deploy-opensearch.sh @@ -9,7 +9,7 @@ if [[ -z $URL || -z $AUTH ]]; then exit 1 fi -# dataset_group_progress -curl -XDELETE "$URL/dataset_group_progress?ignore_unavailable=true" -u "$AUTH" --insecure -curl -H "Content-Type: application/x-ndjson" -X PUT "$URL/dataset_group_progress" -ku "$AUTH" --data-binary "@fieldMappings/dataset_group_progress.json" -if $MOCK_ALLOWED; then curl -H "Content-Type: application/x-ndjson" -X PUT "$URL/dataset_group_progress/_bulk" -ku "$AUTH" --data-binary "@mock/dataset_group_progress.json"; fi \ No newline at end of file +# ddataset_progress_sessions +curl -XDELETE "$URL/dataset_progress_sessions?ignore_unavailable=true" -u "$AUTH" --insecure +curl -H "Content-Type: application/x-ndjson" -X PUT "$URL/dataset_progress_sessions" -ku "$AUTH" --data-binary "@fieldMappings/dataset_progress_sessions.json" +if $MOCK_ALLOWED; then curl -H "Content-Type: application/x-ndjson" -X PUT "$URL/dataset_progress_sessions/_bulk" -ku "$AUTH" --data-binary "@mock/dataset_progress_sessions.json"; fi \ No newline at end of file diff --git a/DSL/OpenSearch/fieldMappings/dataset_group_progress.json b/DSL/OpenSearch/fieldMappings/dataset_progress_sessions.json similarity index 76% rename from DSL/OpenSearch/fieldMappings/dataset_group_progress.json rename to DSL/OpenSearch/fieldMappings/dataset_progress_sessions.json index d1ec86eb..18b1c2fc 100644 --- a/DSL/OpenSearch/fieldMappings/dataset_group_progress.json +++ b/DSL/OpenSearch/fieldMappings/dataset_progress_sessions.json @@ -7,7 +7,10 @@ "validationStatus": { "type": "keyword" }, - "progress": { + "validationMessage": { + "type": "keyword" + }, + "progressPercentage": { "type": "integer" }, "timestamp": { diff --git a/DSL/OpenSearch/mock/dataset_group_progress.json b/DSL/OpenSearch/mock/dataset_group_progress.json deleted file mode 100644 index 461392f8..00000000 --- a/DSL/OpenSearch/mock/dataset_group_progress.json +++ /dev/null @@ -1,8 +0,0 @@ -{"index":{"_id":"1"}} -{"sessionId": "1","validationStatus": "in-progress","progress": 1,"timestamp": "1801371325497", "sentTo": []} -{"index":{"_id":"2"}} -{"sessionId": "2","validationStatus": "in-progress","progress": 10,"timestamp": "1801371325597", "sentTo": []} -{"index":{"_id":"3"}} -{"sessionId": "3","validationStatus": "in-progress","progress": 52,"timestamp": "1801371325697", "sentTo": []} -{"index":{"_id":"4"}} -{"sessionId": "4","validationStatus": "in-progress","progress": 97,"timestamp": "1801371325797", "sentTo": []} diff --git a/DSL/OpenSearch/mock/dataset_progress_sessions.json b/DSL/OpenSearch/mock/dataset_progress_sessions.json new file mode 100644 index 00000000..c0ed6e30 --- /dev/null +++ b/DSL/OpenSearch/mock/dataset_progress_sessions.json @@ -0,0 +1,13 @@ +{"index":{"_id":"1"}} +{"sessionId": "1","validationStatus": "Initiating Validation","validationMessage": "","progressPercentage": 1,"timestamp": "1801371325497", "sentTo": []} +{"index":{"_id":"2"}} +{"sessionId": "2","validationStatus": "Validation In-Progress","validationMessage": "","progressPercentage": 26,"timestamp": "1801371325597", "sentTo": []} +{"index":{"_id":"3"}} +{"sessionId": "3","validationStatus": "Cleaning Dataset","validationMessage": "","progressPercentage": 52,"timestamp": "1801371325697", "sentTo": []} +{"index":{"_id":"4"}} +{"sessionId": "4","validationStatus": "Generating Data","validationMessage": "","progressPercentage": 97,"timestamp": "1801371325797", "sentTo": []} +{"index":{"_id":"5"}} +{"sessionId": "5","validationStatus": "Success","validationMessage": "","progressPercentage": 100,"timestamp": "1801371325797", "sentTo": []} +{"index":{"_id":"6"}} +{"sessionId": "6","validationStatus": "Fail","validationMessage": "Validation failed because class called 'complaints' in 'police' column doesn't exist in the dataset`","progressPercentage": 100,"timestamp": "1801371325797", "sentTo": []} + diff --git a/DSL/Resql/delete-all-dataset-progress-sessions.sql b/DSL/Resql/delete-all-dataset-progress-sessions.sql new file mode 100644 index 00000000..fe017a6f --- /dev/null +++ b/DSL/Resql/delete-all-dataset-progress-sessions.sql @@ -0,0 +1 @@ +DELETE FROM dataset_progress_sessions; diff --git a/DSL/Resql/get-dataset-progress-sessions.sql b/DSL/Resql/get-dataset-progress-sessions.sql new file mode 100644 index 00000000..d76a32b7 --- /dev/null +++ b/DSL/Resql/get-dataset-progress-sessions.sql @@ -0,0 +1,13 @@ +SELECT + id, + dg_id, + group_name, + major_version, + minor_version, + patch_version, + latest, + process_complete, + progress_percentage, + validation_status, + validation_message +FROM dataset_progress_sessions; diff --git a/DSL/Resql/insert-dataset-progress-session.sql b/DSL/Resql/insert-dataset-progress-session.sql new file mode 100644 index 00000000..dd65f7ee --- /dev/null +++ b/DSL/Resql/insert-dataset-progress-session.sql @@ -0,0 +1,19 @@ +INSERT INTO "dataset_progress_sessions" ( + dg_id, + group_name, + major_version, + minor_version, + patch_version, + latest, + progress_percentage, + validation_status +) VALUES ( + :dg_id, + :group_name, + :major_version, + :minor_version, + :patch_version, + :latest, + :progressPercentage, + :validation_status::Validation_Progress_Status +)RETURNING id; \ No newline at end of file diff --git a/DSL/Resql/update-dataset-progress-session.sql b/DSL/Resql/update-dataset-progress-session.sql new file mode 100644 index 00000000..72d501b5 --- /dev/null +++ b/DSL/Resql/update-dataset-progress-session.sql @@ -0,0 +1,6 @@ +UPDATE dataset_progress_sessions +SET + validation_status = :validation_status::Validation_Progress_Status, + validation_message = :validation_message, + progress_percentage = :progress_percentage +WHERE id = :id; \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/progress.yml b/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/progress.yml new file mode 100644 index 00000000..8d8613c0 --- /dev/null +++ b/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/progress.yml @@ -0,0 +1,48 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'PROGRESS'" + method: get + accepts: json + returns: json + namespace: classifier + +get_dataset_progress_sessions: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-dataset-progress-sessions" + result: res_sessions + next: check_status + +check_status: + switch: + - condition: ${200 <= res_sessions.response.statusCodeValue && res_sessions.response.statusCodeValue < 300} + next: assign_success_response + next: assign_fail_response + +assign_success_response: + assign: + format_res: { + operationSuccessful: true, + data: '${res_sessions.response.body}' + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + operationSuccessful: false, + data: '${[]}' + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end + diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/progress/create.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/progress/create.yml new file mode 100644 index 00000000..ba3dc248 --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/progress/create.yml @@ -0,0 +1,147 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'CREATE'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: dgId + type: number + description: "Body field 'dgId'" + - field: groupName + type: number + description: "Body field 'groupName'" + - field: majorVersion + type: number + description: "Body field 'majorVersion'" + - field: minorVersion + type: number + description: "Body field 'minorVersion'" + - field: patchVersion + type: number + description: "Body field 'patchVersion'" + - field: latest + type: boolean + description: "Body field 'latest'" + headers: + - field: cookie + type: string + description: "Cookie field" + +extract_request_data: + assign: + dg_id: ${incoming.body.dgId} + group_name: ${incoming.body.groupName} + major_version: ${incoming.body.majorVersion} + minor_version: ${incoming.body.minorVersion} + patch_version: ${incoming.body.patchVersion} + latest: ${incoming.body.latest} + next: check_for_request_data + +check_for_request_data: + switch: + - condition: ${dg_id !== null || major_version !=null || minor_version !==null || patch_version !==null } + next: create_dataset_progress_session + next: return_incorrect_request + +create_dataset_progress_session: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/insert-dataset-progress-session" + body: + dg_id: ${dg_id} + group_name: ${group_name} + major_version: ${major_version} + minor_version: ${minor_version} + patch_version: ${patch_version} + latest: ${latest} + progressPercentage: 0 + validation_status: 'Initiating Validation' + result: res + next: check_status + +check_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: assign_session_id + next: assign_fail_response + +assign_session_id: + assign: + session_id: ${res.response.body[0].id} + next: get_csrf_token + +get_csrf_token: + call: http.get + args: + url: "[#CLASSIFIER_NOTIFICATIONS]/csrf-token" + headers: + cookie: '_csrf=1rLs4xdR24bTnazFl-jMmghjs-g;' + result: res_token + next: check_token_status + +check_token_status: + switch: + - condition: ${200 <= res_token.response.statusCodeValue && res_token.response.statusCodeValue < 300} + next: assign_csrf_token + next: assign_fail_response + +assign_csrf_token: + assign: + token: ${res_token.response.body.csrfToken} + next: update_progress + +update_progress: + call: http.post + args: + url: "[#CLASSIFIER_NOTIFICATIONS]/dataset/progress" + headers: + X-CSRF-Token: ${token} + cookie: '_csrf=1rLs4xdR24bTnadddddzFl-jMmghjs-g;' + body: + sessionId: ${session_id} + progressPercentage: 0 + validationStatus: 'Initiating Validation' + validationMessage: '' + result: res_node + next: check_node_server_status + +check_node_server_status: + switch: + - condition: ${200 <= res_node.response.statusCodeValue && res_node.response.statusCodeValue < 300} + next: assign_success_response + next: assign_fail_response + +assign_success_response: + assign: + format_res: { + sessionId: '${session_id}', + operationSuccessful: true, + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + sessionId: '', + operationSuccessful: false, + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end + +return_incorrect_request: + status: 400 + return: 'Missing Required Fields' + next: end diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/progress/delete.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/progress/delete.yml new file mode 100644 index 00000000..780e1605 --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/progress/delete.yml @@ -0,0 +1,46 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'DELETE'" + method: post + accepts: json + returns: json + namespace: classifier + +delete_dataset_progress_sessions: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/delete-all-dataset-progress-sessions" + result: res_sessions + next: check_status + +check_status: + switch: + - condition: ${200 <= res_sessions.response.statusCodeValue && res_sessions.response.statusCodeValue < 300} + next: assign_success_response + next: assign_fail_response + +assign_success_response: + assign: + format_res: { + operationSuccessful: true, + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + operationSuccessful: false, + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end + diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/progress/update.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/progress/update.yml new file mode 100644 index 00000000..5a4b1cf8 --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/progress/update.yml @@ -0,0 +1,126 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'UPDATE'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: sessionId + type: number + description: "Body field 'sessionId'" + - field: validationStatus + type: string + description: "Body field 'validationStatus'" + - field: validationMessage + type: string + description: "Body field 'validationMessage'" + - field: progressPercentage + type: number + description: "Body field 'progressPercentage'" + +extract_request_data: + assign: + session_id: ${incoming.body.sessionId} + validation_status: ${incoming.body.validationStatus} + validation_message: ${incoming.body.validationMessage} + progress_percentage: ${incoming.body.progressPercentage} + next: check_for_request_data + +check_for_request_data: + switch: + - condition: ${session_id !== null || !validation_status || progress_percentage !==null} + next: update_dataset_progress_session + next: return_incorrect_request + +update_dataset_progress_session: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/update-dataset-progress-session" + body: + id: ${session_id} + validation_status: ${validation_status} + progress_percentage: ${progress_percentage} + validation_message: ${validation_message} + result: res + next: check_status + +check_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: get_csrf_token + next: assign_fail_response + +get_csrf_token: + call: http.get + args: + url: "[#CLASSIFIER_NOTIFICATIONS]/csrf-token" + headers: + cookie: '_csrf=1rLs4xdR24bTnazFl-jMmghjs-g;' + result: res_token + next: check_token_status + +check_token_status: + switch: + - condition: ${200 <= res_token.response.statusCodeValue && res_token.response.statusCodeValue < 300} + next: assign_csrf_token + next: assign_fail_response + +assign_csrf_token: + assign: + token: ${res_token.response.body.csrfToken} + next: update_progress + +update_progress: + call: http.post + args: + url: "[#CLASSIFIER_NOTIFICATIONS]/dataset/progress" + headers: + X-CSRF-Token: ${token} + cookie: '_csrf=1rLs4xdR24bTnazFl-jMmghjs-g;' + body: + sessionId: ${session_id} + progressPercentage: ${progress_percentage} + validationStatus: ${validation_status} + validationMessage: ${validation_message} + result: res_node + next: check_node_server_status + +check_node_server_status: + switch: + - condition: ${200 <= res_node.response.statusCodeValue && res_node.response.statusCodeValue < 300} + next: assign_success_response + next: assign_fail_response + +assign_success_response: + assign: + format_res: { + sessionId: '${session_id}', + operationSuccessful: true, + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + sessionId: '', + operationSuccessful: false, + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end + +return_incorrect_request: + status: 400 + return: 'Missing Required Fields' + next: end diff --git a/constants.ini b/constants.ini index bc34bf45..23b5dca2 100644 --- a/constants.ini +++ b/constants.ini @@ -7,6 +7,7 @@ CLASSIFIER_RESQL=http://resql:8082 CLASSIFIER_TIM=http://tim:8085 CLASSIFIER_CRON_MANAGER=http://cron-manager:9010 CLASSIFIER_FILE_HANDLER=http://file-handler:8000 +CLASSIFIER_NOTIFICATIONS=http://notifications-node:4040 CLASSIFIER_RUUTER_PUBLIC_FRONTEND_URL=value DOMAIN=localhost JIRA_API_TOKEN= value diff --git a/docker-compose.yml b/docker-compose.yml index 329c1a78..e525ec52 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -296,7 +296,7 @@ services: OPENSEARCH_PASSWORD: admin PORT: 4040 REFRESH_INTERVAL: 1000 - CORS_WHITELIST_ORIGINS: http://localhost:3001,http://localhost:3003,http://localhost:3004,http://localhost:8080 + CORS_WHITELIST_ORIGINS: http://localhost:3001,http://localhost:3003,http://localhost:3004,http://localhost:8080,http://localhost:8088 RUUTER_URL: http://ruuter-public:8086 volumes: - /app/node_modules diff --git a/notification-server/.env b/notification-server/.env index fc3e9f18..fd0d88cb 100644 --- a/notification-server/.env +++ b/notification-server/.env @@ -5,5 +5,5 @@ OPENSEARCH_USERNAME=admin OPENSEARCH_PASSWORD=admin PORT=4040 REFRESH_INTERVAL=1000 -CORS_WHITELIST_ORIGINS=http://localhost:3001,http://localhost:3003,http://localhost:3004,http://localhost:8080 +CORS_WHITELIST_ORIGINS=http://localhost:3001,http://localhost:3003,http://localhost:3004,http://localhost:8080,http://localhost:8088 RUUTER_URL=http://localhost:8086 diff --git a/notification-server/index.js b/notification-server/index.js index 5db0807e..2952b006 100644 --- a/notification-server/index.js +++ b/notification-server/index.js @@ -4,7 +4,7 @@ const { client } = require('./src/openSearch'); (async () => { try { await client.indices.putSettings({ - index: 'dataset_group_progress', + index: 'dataset_progress_sessions', body: { refresh_interval: '5s', }, diff --git a/notification-server/src/config.js b/notification-server/src/config.js index 86b3d3f1..04ce118f 100644 --- a/notification-server/src/config.js +++ b/notification-server/src/config.js @@ -2,7 +2,7 @@ require("dotenv").config(); module.exports = { openSearchConfig: { - datasetGroupProgress: "dataset_group_progress", + datasetGroupProgress: "dataset_progress_sessions", ssl: { rejectUnauthorized: false, }, diff --git a/notification-server/src/openSearch.js b/notification-server/src/openSearch.js index fb16aab1..9c7bf4d6 100644 --- a/notification-server/src/openSearch.js +++ b/notification-server/src/openSearch.js @@ -23,11 +23,13 @@ async function searchNotification({ sessionId, connectionId, sender }) { for (const hit of response.body.hits.hits) { console.log(`hit: ${JSON.stringify(hit)}`); - const jsonObject = { + const sessionJson = { sessionId: hit._source.sessionId, - progress: hit._source.progress, + progressPercentage: hit._source.progressPercentage, + validationStatus: hit._source.validationStatus, + validationMessage: hit._source.validationMessage }; - await sender(jsonObject); + await sender(sessionJson); await markAsSent(hit, connectionId); } } catch (e) { @@ -55,12 +57,14 @@ async function markAsSent({ _index, _id }, connectionId) { }); } -async function updateProgress(sessionId, progress) { +async function updateProgress(sessionId, progressPercentage, validationStatus, validationMessage) { await client.index({ - index: "dataset_group_progress", + index: "dataset_progress_sessions", body: { sessionId, - progress, + validationStatus, + progressPercentage, + validationMessage, timestamp: new Date(), }, }); diff --git a/notification-server/src/server.js b/notification-server/src/server.js index 4c75fc0d..196c6e9d 100644 --- a/notification-server/src/server.js +++ b/notification-server/src/server.js @@ -27,19 +27,26 @@ app.get("/sse/notifications/:sessionId", (req, res) => { }); app.get("/csrf-token", (req, res) => { - res.json({ csrfToken: req.csrfToken() }); + console.log(`Cookies: ${JSON.stringify(req.cookies)}`); + res.json({ csrfToken: req.csrfToken(), }); }); -// Endpoint to update the dataset_group_progress index -app.post("/dataset-group/update-progress", async (req, res) => { - const { sessionId, progress } = req.body; +// Endpoint to update the dataset_progress_sessions index +app.post("/dataset/progress", async (req, res) => { + const { sessionId, progressPercentage, validationStatus, validationMessage } = + req.body; - if (!sessionId || progress === undefined) { + if (!sessionId || progressPercentage === undefined || !validationStatus) { return res.status(400).json({ error: "Missing required fields" }); } try { - await updateProgress(sessionId, progress); + await updateProgress( + sessionId, + progressPercentage, + validationStatus, + validationMessage + ); res.status(201).json({ message: "Document created successfully" }); } catch (error) { console.error("Error creating document:", error); From 3f1d6c209c64dddfeb1f9d44c46de78b98ff9537 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Tue, 30 Jul 2024 13:30:07 +0530 Subject: [PATCH 310/582] ESCLASS-145- add new random string generating DMapper method and add cron job to auto delete completed progression sessions in datasets --- .../DSL/dataset_progress_session.yml | 4 +++ ...ete_completed_dataset_progress_sessions.sh | 26 +++++++++++++++++++ .../hbs/return_random_string.handlebars | 3 +++ DSL/DMapper/lib/helpers.js | 6 ++++- .../delete-all-dataset-progress-sessions.sql | 1 - ...te-completed-dataset-progress-sessions.sql | 2 ++ .../datasetgroup/progress/create.yml | 26 ++++++++++++++++--- .../datasetgroup/progress/delete.yml | 2 +- .../datasetgroup/progress/update.yml | 26 ++++++++++++++++--- 9 files changed, 87 insertions(+), 9 deletions(-) create mode 100644 DSL/CronManager/DSL/dataset_progress_session.yml create mode 100644 DSL/CronManager/script/delete_completed_dataset_progress_sessions.sh create mode 100644 DSL/DMapper/hbs/return_random_string.handlebars delete mode 100644 DSL/Resql/delete-all-dataset-progress-sessions.sql create mode 100644 DSL/Resql/delete-completed-dataset-progress-sessions.sql diff --git a/DSL/CronManager/DSL/dataset_progress_session.yml b/DSL/CronManager/DSL/dataset_progress_session.yml new file mode 100644 index 00000000..f11405a3 --- /dev/null +++ b/DSL/CronManager/DSL/dataset_progress_session.yml @@ -0,0 +1,4 @@ +token_refresh: + trigger: "0 0 0 * * ?" + type: exec + command: "../app/scripts/delete_completed_dataset_progress_sessions.sh" \ No newline at end of file diff --git a/DSL/CronManager/script/delete_completed_dataset_progress_sessions.sh b/DSL/CronManager/script/delete_completed_dataset_progress_sessions.sh new file mode 100644 index 00000000..e32f382d --- /dev/null +++ b/DSL/CronManager/script/delete_completed_dataset_progress_sessions.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# Set the working directory to the location of the script +cd "$(dirname "$0")" + +# Source the constants from the ini file +source ../config/config.ini + +script_name=$(basename $0) +pwd + +echo $(date -u +"%Y-%m-%d %H:%M:%S.%3NZ") - $script_name started + +delete_dataset_progress_sessions() { + delete_response=$(curl -s -X DELETE "$CLASSIFIER_RESQL/delete-completed-dataset-progress-sessions") + if echo "$delete_response" | grep -q '"success":true'; then + echo "Data deletion successful" + else + echo "Data deletion failed: $delete_response" + exit 1 + fi +} + +delete_dataset_progress_sessions + +echo $(date -u +"%Y-%m-%d %H:%M:%S.%3NZ") - $script_name finished diff --git a/DSL/DMapper/hbs/return_random_string.handlebars b/DSL/DMapper/hbs/return_random_string.handlebars new file mode 100644 index 00000000..ecefc239 --- /dev/null +++ b/DSL/DMapper/hbs/return_random_string.handlebars @@ -0,0 +1,3 @@ +{ + "randomHexString": "{{{getRandomString}}}" +} diff --git a/DSL/DMapper/lib/helpers.js b/DSL/DMapper/lib/helpers.js index b94ce2c6..fc73bb69 100644 --- a/DSL/DMapper/lib/helpers.js +++ b/DSL/DMapper/lib/helpers.js @@ -1,4 +1,4 @@ -import { createHmac, timingSafeEqual } from "crypto"; +import { createHmac, timingSafeEqual, randomBytes } from "crypto"; export function verifySignature(payload, headers, secret) { const signature = headers["x-hub-signature"]; @@ -70,3 +70,7 @@ export function findNotExistingStopWords(inputArray, existingArray) { return value; } +export function getRandomString() { + const randomHexString = randomBytes(32).toString("hex"); + return randomHexString; +} diff --git a/DSL/Resql/delete-all-dataset-progress-sessions.sql b/DSL/Resql/delete-all-dataset-progress-sessions.sql deleted file mode 100644 index fe017a6f..00000000 --- a/DSL/Resql/delete-all-dataset-progress-sessions.sql +++ /dev/null @@ -1 +0,0 @@ -DELETE FROM dataset_progress_sessions; diff --git a/DSL/Resql/delete-completed-dataset-progress-sessions.sql b/DSL/Resql/delete-completed-dataset-progress-sessions.sql new file mode 100644 index 00000000..4ea006b9 --- /dev/null +++ b/DSL/Resql/delete-completed-dataset-progress-sessions.sql @@ -0,0 +1,2 @@ +DELETE FROM dataset_progress_sessions +WHERE process_complete = true; diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/progress/create.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/progress/create.yml index ba3dc248..e8f53322 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/progress/create.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/progress/create.yml @@ -44,9 +44,29 @@ extract_request_data: check_for_request_data: switch: - condition: ${dg_id !== null || major_version !=null || minor_version !==null || patch_version !==null } - next: create_dataset_progress_session + next: get_random_string_cookie next: return_incorrect_request +get_random_string_cookie: + call: http.post + args: + url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/return_random_string" + headers: + type: json + result: res_cookie + next: check_random_string_status + +check_random_string_status: + switch: + - condition: ${200 <= res_cookie.response.statusCodeValue && res_cookie.response.statusCodeValue < 300} + next: assign_csrf_cookie + next: assign_fail_response + +assign_csrf_cookie: + assign: + csrf_cookie: ${'_csrf=' + res_cookie.response.body.randomHexString+';'} + next: create_dataset_progress_session + create_dataset_progress_session: call: http.post args: @@ -79,7 +99,7 @@ get_csrf_token: args: url: "[#CLASSIFIER_NOTIFICATIONS]/csrf-token" headers: - cookie: '_csrf=1rLs4xdR24bTnazFl-jMmghjs-g;' + cookie: ${csrf_cookie} result: res_token next: check_token_status @@ -100,7 +120,7 @@ update_progress: url: "[#CLASSIFIER_NOTIFICATIONS]/dataset/progress" headers: X-CSRF-Token: ${token} - cookie: '_csrf=1rLs4xdR24bTnadddddzFl-jMmghjs-g;' + cookie: ${csrf_cookie} body: sessionId: ${session_id} progressPercentage: 0 diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/progress/delete.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/progress/delete.yml index 780e1605..b29f34c6 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/progress/delete.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/progress/delete.yml @@ -10,7 +10,7 @@ declaration: delete_dataset_progress_sessions: call: http.post args: - url: "[#CLASSIFIER_RESQL]/delete-all-dataset-progress-sessions" + url: "[#CLASSIFIER_RESQL]/delete-completed-dataset-progress-sessions" result: res_sessions next: check_status diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/progress/update.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/progress/update.yml index 5a4b1cf8..56798d97 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/progress/update.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/progress/update.yml @@ -32,9 +32,29 @@ extract_request_data: check_for_request_data: switch: - condition: ${session_id !== null || !validation_status || progress_percentage !==null} - next: update_dataset_progress_session + next: get_random_string_cookie next: return_incorrect_request +get_random_string_cookie: + call: http.post + args: + url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/return_random_string" + headers: + type: json + result: res_cookie + next: check_random_string_status + +check_random_string_status: + switch: + - condition: ${200 <= res_cookie.response.statusCodeValue && res_cookie.response.statusCodeValue < 300} + next: assign_csrf_cookie + next: assign_fail_response + +assign_csrf_cookie: + assign: + csrf_cookie: ${'_csrf=' + res_cookie.response.body.randomHexString+';'} + next: update_dataset_progress_session + update_dataset_progress_session: call: http.post args: @@ -58,7 +78,7 @@ get_csrf_token: args: url: "[#CLASSIFIER_NOTIFICATIONS]/csrf-token" headers: - cookie: '_csrf=1rLs4xdR24bTnazFl-jMmghjs-g;' + cookie: ${csrf_cookie} result: res_token next: check_token_status @@ -79,7 +99,7 @@ update_progress: url: "[#CLASSIFIER_NOTIFICATIONS]/dataset/progress" headers: X-CSRF-Token: ${token} - cookie: '_csrf=1rLs4xdR24bTnazFl-jMmghjs-g;' + cookie: ${csrf_cookie} body: sessionId: ${session_id} progressPercentage: ${progress_percentage} From fb443d217ac078a79d7490e513af844fe98fa58c Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Tue, 30 Jul 2024 13:47:44 +0530 Subject: [PATCH 311/582] data models ui --- GUI/src/App.tsx | 2 + GUI/src/pages/DataModels/index.tsx | 200 +++++++++++++++++++++++++++++ GUI/src/services/api-mock.ts | 2 +- GUI/src/services/data-models.ts | 58 +++++++++ 4 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 GUI/src/pages/DataModels/index.tsx create mode 100644 GUI/src/services/data-models.ts diff --git a/GUI/src/App.tsx b/GUI/src/App.tsx index c82550fe..50533212 100644 --- a/GUI/src/App.tsx +++ b/GUI/src/App.tsx @@ -12,6 +12,7 @@ import CreateDatasetGroup from 'pages/DatasetGroups/CreateDatasetGroup'; import ViewDatasetGroup from 'pages/DatasetGroups/ViewDatasetGroup'; import StopWords from 'pages/StopWords'; import ValidationSessions from 'pages/ValidationSessions'; +import DataModels from 'pages/DataModels'; const App: FC = () => { @@ -35,6 +36,7 @@ const App: FC = () => { } /> } /> } /> + } /> diff --git a/GUI/src/pages/DataModels/index.tsx b/GUI/src/pages/DataModels/index.tsx new file mode 100644 index 00000000..63a913ae --- /dev/null +++ b/GUI/src/pages/DataModels/index.tsx @@ -0,0 +1,200 @@ +import { FC, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Button, FormInput, FormSelect } from 'components'; +import DatasetGroupCard from 'components/molecules/DatasetGroupCard'; +import Pagination from 'components/molecules/Pagination'; +import { getDatasetsOverview, getFilterData } from 'services/datasets'; +import { useQuery } from '@tanstack/react-query'; +import { useNavigate } from 'react-router-dom'; +import { + convertTimestampToDateTime, + formattedArray, + parseVersionString, +} from 'utils/commonUtilts'; + +const DataModels: FC = () => { + const { t } = useTranslation(); + const navigate = useNavigate(); + + const [pageIndex, setPageIndex] = useState(1); + const [id, setId] = useState(0); + const [enableFetch, setEnableFetch] = useState(true); + const [view, setView] = useState("list"); + +useEffect(()=>{ + setEnableFetch(true) +},[view]); + + const [filters, setFilters] = useState({ + datasetGroupName: 'all', + version: 'x.x.x', + validationStatus: 'all', + sort: 'asc', + }); + + const { + data: datasetGroupsData, + isLoading, + } = useQuery( + [ + 'datasetgroup/overview', + pageIndex, + filters.datasetGroupName, + parseVersionString(filters?.version)?.major, + parseVersionString(filters?.version)?.minor, + parseVersionString(filters?.version)?.patch, + filters.validationStatus, + filters.sort, + ], + () => + getDatasetsOverview( + pageIndex, + filters.datasetGroupName, + parseVersionString(filters?.version)?.major, + parseVersionString(filters?.version)?.minor, + parseVersionString(filters?.version)?.patch, + filters.validationStatus, + filters.sort + ), + { + keepPreviousData: true, + enabled: enableFetch, + } + ); + const { data: filterData } = useQuery(['datasets/filters'], () => + getFilterData() + ); + const pageCount = datasetGroupsData?.response?.data?.[0]?.totalPages || 1; + + const handleFilterChange = (name: string, value: string) => { + setEnableFetch(false); + setFilters((prevFilters) => ({ + ...prevFilters, + [name]: value, + })); + }; + + + return ( +
    +
    +
    +
    Data Models
    + +
    +
    +
    + + handleFilterChange('datasetGroupName', selection?.value ?? '') + } + /> + + handleFilterChange('version', selection?.value ?? '') + } + /> + + handleFilterChange('validationStatus', selection?.value ?? '') + } + /> + + handleFilterChange('validationStatus', selection?.value ?? '') + } + /> + + handleFilterChange('validationStatus', selection?.value ?? '') + } + /> + + handleFilterChange('sort', selection?.value ?? '') + } + /> + + +
    +
    + {isLoading &&
    Loading...
    } + {datasetGroupsData?.response?.data?.map( + (dataset, index: number) => { + return ( + + ); + } + )} +
    + 1} + canNextPage={pageIndex < pageCount} + onPageChange={setPageIndex} + /> +
    +
    + +
    + ); +}; + +export default DataModels; diff --git a/GUI/src/services/api-mock.ts b/GUI/src/services/api-mock.ts index 4932793d..040891fa 100644 --- a/GUI/src/services/api-mock.ts +++ b/GUI/src/services/api-mock.ts @@ -1,7 +1,7 @@ import axios, { AxiosError } from 'axios'; const instance = axios.create({ - baseURL: 'https://d5e7cde0-f9b1-4425-8a16-c5f93f503e2e.mock.pstmn.io', + baseURL: 'https://cd16ef7b-5bb2-419b-9785-af03380d0b9d.mock.pstmn.io', headers: { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded', diff --git a/GUI/src/services/data-models.ts b/GUI/src/services/data-models.ts new file mode 100644 index 00000000..00e8e63b --- /dev/null +++ b/GUI/src/services/data-models.ts @@ -0,0 +1,58 @@ +import apiDev from './api-dev'; +import apiExternal from './api-external'; +import apiMock from './api-mock'; +import { PaginationState } from '@tanstack/react-table'; +import { DatasetGroup, Operation } from 'types/datasetGroups'; + +export async function getDatasetsOverview( + pageNum: number, + modelGroup: string, + majorVersion: number, + minorVersion: number, + patchVersion: number, + platform: string, + sort: string, + datasetGroup:string, + trainingStatus:string, + deploymentMaturity:string +) { + const { data } = await apiMock.get('classifier/datamodel/overview', { + params: { + page: pageNum, + modelGroup, + majorVersion, + minorVersion, + patchVersion, + platform, + sortType:sort, + datasetGroup, + trainingStatus, + deploymentMaturity, + pageSize:5 + }, + }); + return data; +} + +export async function getFilterData() { + const { data } = await apiDev.get('classifier/datasetgroup/overview/filters'); + return data; +} + +export async function getMetadata(groupId: string | number | null) { + const { data } = await apiDev.get('classifier/datasetgroup/group/metadata', { + params: { + groupId + }, + }); + return data; +} + +export async function createDatasetGroup(datasetGroup: DatasetGroup) { + + const { data } = await apiDev.post('classifier/datasetgroup/create', { + ...datasetGroup, + }); + return data; +} + From 686576d0dab279c02a0fa9a9c2eeeaea1f872c3a Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 31 Jul 2024 03:59:06 +0530 Subject: [PATCH 312/582] docker compose bug fix --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 26224b76..50ac3eca 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -226,7 +226,7 @@ services: - FILE_HANDLER_DOWNLOAD_LOCATION_JSON_URL=http://file-handler:8000/datasetgroup/data/download/json/location - FILE_HANDLER_STOPWORDS_URL=http://file-handler:8000/datasetgroup/data/download/json/stopwords - FILE_HANDLER_IMPORT_CHUNKS_URL=http://file-handler:8000/datasetgroup/data/import/chunk - - GET_PAGE_COUNT_URL=http://ruuter-private:8088/datasetgroup/group/data?groupd_id={dgid}&page_num=1 + - GET_PAGE_COUNT_URL=http://ruuter-private:8088/classifier/datasetgroup/group/data?groupId=dgID&pageNum=1 - SAVE_JSON_AGGREGRATED_DATA_URL=http://file-handler:8000/datasetgroup/data/import/json - DOWNLOAD_CHUNK_URL=http://file-handler:8000/datasetgroup/data/download/chunk - STATUS_UPDATE_URL=http://ruuter-private:8088/classifier/datasetgroup/update/preprocess/status From db82bd36cc3ade1359f4ebc663e7a73757e17efa Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 31 Jul 2024 03:59:28 +0530 Subject: [PATCH 313/582] completed woring minor both instances, patch and delete --- dataset-processor/dataset_processor.py | 190 ++++++++++++++++--------- 1 file changed, 124 insertions(+), 66 deletions(-) diff --git a/dataset-processor/dataset_processor.py b/dataset-processor/dataset_processor.py index 090dff46..c66978fd 100644 --- a/dataset-processor/dataset_processor.py +++ b/dataset-processor/dataset_processor.py @@ -1,6 +1,7 @@ import re import os import json +import urllib.parse import requests # from data_enrichment.data_enrichment import DataEnrichment from constants import * @@ -73,7 +74,7 @@ def clean_text(text): return cleaned_data except Exception as e: - print("Error while removing Stop Words") + print(f"Error while removing Stop Words : {e}") return None def enrich_data(self, data, selected_fields, record_count): @@ -199,15 +200,16 @@ def get_stopwords(self, dg_id, custom_jwt_cookie): def get_page_count(self, dg_id, custom_jwt_cookie): params = {'dgId': dg_id} headers = { - 'cookie': f'customJwtCookie={custom_jwt_cookie}' + 'cookie': custom_jwt_cookie } try: - page_count_url = GET_PAGE_COUNT_URL.replace("{dgid}",str(dg_id)) + page_count_url = GET_PAGE_COUNT_URL.replace("dgID",str(dg_id)) response = requests.get(page_count_url, headers=headers) response.raise_for_status() data = response.json() - page_count = data["numpages"] + + page_count = data["response"]["data"][0]["numPages"] return page_count # try: # folder_path = f'data/dataset/{dg_id}/chunks/' @@ -291,8 +293,17 @@ def update_preprocess_status(self,dg_id, cookie, processed_data_available, raw_d def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload): print(f"Process handler started with updateType: {updateType}") + page_count = self.get_page_count(dgID, cookie) + print(f"Page Count : {page_count}") + + if updateType == "minor" and page_count>0: + updateType = "minor_append_update" + elif updateType == "patch": + pass + else: + updateType = "minor_initial_update" - if updateType == "minor": + if updateType == "minor_initial_update": print("Handling Minor update") # dataset = self.get_dataset(dgID, cookie) dataset = self.get_dataset_by_location(savedFilePath, cookie) @@ -328,7 +339,7 @@ def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload) print("Chunked data saved successfully") agregated_dataset_operation = self.save_aggregrated_data(dgID, cookie, cleaned_data) if agregated_dataset_operation != None: - return_data = self.update_preprocess_status(dgID, cookie, True, False, f"/dataset/{dgID}/chunks/", "", True, 100, len(chunked_data)) + return_data = self.update_preprocess_status(dgID, cookie, True, False, f"/dataset/{dgID}/chunks/", "", True, len(cleaned_data), len(chunked_data)) print(return_data) return SUCCESSFUL_OPERATION else: @@ -358,7 +369,7 @@ def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload) else: print("Failed to retrieve dataset") return FAILED_TO_GET_DATASET - elif updateType == "Minor2": + elif updateType == "minor_append_update": print("Handling Minor update") agregated_dataset = self.get_dataset(dgID, cookie) max_row_id = max(item["rowID"] for item in agregated_dataset) @@ -372,7 +383,7 @@ def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload) print(structured_data[-1]) if structured_data is not None: print("Minor update dataset converted successfully") - selected_data_fields_to_enrich = self.get_selected_data_fields(dgID) + selected_data_fields_to_enrich = self.get_selected_data_fields(dgID, cookie) if selected_data_fields_to_enrich is not None: print("Selected data fields to enrich for minor update retrieved successfully") max_row_id = max(item["rowID"] for item in structured_data) @@ -400,6 +411,8 @@ def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload) agregated_dataset_operation = self.save_aggregrated_data(dgID, cookie, agregated_dataset) if agregated_dataset_operation: print("Aggregated dataset for minor update saved successfully") + return_data = self.update_preprocess_status(dgID, cookie, True, False, f"/dataset/{dgID}/chunks/", "", True, len(cleaned_data), len(chunked_data)) + print(return_data) return SUCCESSFUL_OPERATION else: print("Failed to save aggregated dataset for minor update") @@ -435,69 +448,114 @@ def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload) print("Failed to retrieve aggregated dataset for minor update") return FAILED_TO_GET_AGGREGATED_DATASET elif updateType == "patch": - print("Handling Patch update") - stop_words = self.get_stopwords(dgID, cookie) - if stop_words is not None: - print("Stop words for patch update retrieved successfully") - cleaned_patch_payload = self.remove_stop_words(patchPayload, stop_words) - if cleaned_patch_payload is not None: - print("Stop words for patch update removed successfully") - page_count = self.get_page_count(dgID, cookie) - if page_count is not None: - print(f"Page count for patch update retrieved successfully: {page_count}") - chunk_updates = {} - for entry in cleaned_patch_payload: - rowID = entry.get("rowID") - chunkNum = (rowID - 1) // 5 + 1 - if chunkNum not in chunk_updates: - chunk_updates[chunkNum] = [] - chunk_updates[chunkNum].append(entry) - print(f"Chunk updates prepared: {chunk_updates}") - for chunkNum, entries in chunk_updates.items(): - chunk_data = self.download_chunk(dgID, cookie, chunkNum) - if chunk_data is not None: - print(f"Chunk {chunkNum} downloaded successfully") - for entry in entries: - rowID = entry.get("rowID") - for idx, chunk_entry in enumerate(chunk_data): - if chunk_entry.get("rowID") == rowID: - chunk_data[idx] = entry - break - - chunk_save_operation = self.save_chunked_data([chunk_data], cookie, dgID, chunkNum-1) - if chunk_save_operation == None: - print(f"Failed to save chunk {chunkNum}") - return FAILED_TO_SAVE_CHUNKED_DATA - else: - print(f"Failed to download chunk {chunkNum}") - return FAILED_TO_DOWNLOAD_CHUNK - agregated_dataset = self.get_dataset(dgID, cookie) - if agregated_dataset is not None: - print("Aggregated dataset for patch update retrieved successfully") + decoded_string = urllib.parse.unquote(patchPayload) + data_payload = json.loads(decoded_string) + print(data_payload) + print("*************") + if (data_payload["editedData"]!=[]): + print("Handling Patch update") + stop_words = self.get_stopwords(dgID, cookie) + if stop_words is not None: + print("Stop words for patch update retrieved successfully") + cleaned_patch_payload = self.remove_stop_words(data_payload["editedData"], stop_words) + if cleaned_patch_payload is not None: + print("Stop words for patch update removed successfully") + page_count = self.get_page_count(dgID, cookie) + if page_count is not None: + print(f"Page count for patch update retrieved successfully: {page_count}") + print(cleaned_patch_payload) + chunk_updates = {} for entry in cleaned_patch_payload: - rowID = entry.get("rowID") - for index, item in enumerate(agregated_dataset): - if item.get("rowID") == rowID: - agregated_dataset[index] = entry - break + rowID = entry.get("rowId") + chunkNum = (rowID - 1) // 5 + 1 + if chunkNum not in chunk_updates: + chunk_updates[chunkNum] = [] + chunk_updates[chunkNum].append(entry) + print(f"Chunk updates prepared: {chunk_updates}") + for chunkNum, entries in chunk_updates.items(): + chunk_data = self.download_chunk(dgID, cookie, chunkNum) + if chunk_data is not None: + print(f"Chunk {chunkNum} downloaded successfully") + for entry in entries: + rowID = entry.get("rowId") + for idx, chunk_entry in enumerate(chunk_data): + if chunk_entry.get("rowID") == rowID: + chunk_data[idx] = entry + break + chunk_save_operation = self.save_chunked_data([chunk_data], cookie, dgID, chunkNum-1) + if chunk_save_operation == None: + print(f"Failed to save chunk {chunkNum}") + return FAILED_TO_SAVE_CHUNKED_DATA + else: + print(f"Failed to download chunk {chunkNum}") + return FAILED_TO_DOWNLOAD_CHUNK + agregated_dataset = self.get_dataset(dgID, cookie) + if agregated_dataset is not None: + print("Aggregated dataset for patch update retrieved successfully") + for entry in cleaned_patch_payload: + rowID = entry.get("rowId") + for index, item in enumerate(agregated_dataset): + if item.get("rowID") == rowID: + entry["rowID"] = rowID + del entry["rowId"] + agregated_dataset[index] = entry + break - save_result = self.save_aggregrated_data(dgID, cookie, agregated_dataset) - if save_result: - print("Aggregated dataset for patch update saved successfully") - return SUCCESSFUL_OPERATION + save_result_update = self.save_aggregrated_data(dgID, cookie, agregated_dataset) + if save_result_update: + print("Aggregated dataset for patch update saved successfully") + # return SUCCESSFUL_OPERATION + else: + print("Failed to save aggregated dataset for patch update") + return FAILED_TO_SAVE_AGGREGATED_DATA else: - print("Failed to save aggregated dataset for patch update") - return FAILED_TO_SAVE_AGGREGATED_DATA + print("Failed to retrieve aggregated dataset for patch update") + return FAILED_TO_GET_AGGREGATED_DATASET else: - print("Failed to retrieve aggregated dataset for patch update") - return FAILED_TO_GET_AGGREGATED_DATASET + print("Failed to get page count for patch update") + return FAILED_TO_GET_PAGE_COUNT else: - print("Failed to get page count for patch update") - return FAILED_TO_GET_PAGE_COUNT + print("Failed to remove stop words for patch update") + return FAILED_TO_REMOVE_STOP_WORDS else: - print("Failed to remove stop words for patch update") - return FAILED_TO_REMOVE_STOP_WORDS + print("Failed to retrieve stop words for patch update") + return FAILED_TO_GET_STOP_WORDS + + print(data_payload["deletedDataRows"]) + if (data_payload["deletedDataRows"]!=[]): + deleted_rows = data_payload["deletedDataRows"] + aggregated_dataset = self.get_dataset(dgID, cookie) + updated_dataset = [row for row in aggregated_dataset if row.get('rowID') not in deleted_rows] + for idx, row in enumerate(updated_dataset, start=1): + row['rowID'] = idx + if updated_dataset is not None: + chunked_data = self.chunk_data(updated_dataset) + if chunked_data is not None: + print("Data chunking successful") + print(chunked_data) + operation_result = self.save_chunked_data(chunked_data, cookie, dgID, 0) + save_result_delete = self.save_aggregrated_data(dgID, cookie, updated_dataset) + + if data_payload["editedData"]==[] and data_payload["deletedDataRows"]==[]: + return SUCCESSFUL_OPERATION + elif data_payload["editedData"]!=[] and data_payload["deletedDataRows"]==[]: + if save_result_update: + return SUCCESSFUL_OPERATION else: - print("Failed to retrieve stop words for patch update") - return FAILED_TO_GET_STOP_WORDS + return FAILED_TO_SAVE_AGGREGATED_DATA + elif data_payload["editedData"]==[] and data_payload["deletedDataRows"]!=[]: + if save_result_delete: + return_data = self.update_preprocess_status(dgID, cookie, True, False, f"/dataset/{dgID}/chunks/", "", True, len(updated_dataset), len(chunked_data)) + return SUCCESSFUL_OPERATION + else: + return FAILED_TO_SAVE_AGGREGATED_DATA + elif data_payload["editedData"]!=[] and data_payload["deletedDataRows"]!=[]: + if save_result_update and save_result_delete: + return_data = self.update_preprocess_status(dgID, cookie, True, False, f"/dataset/{dgID}/chunks/", "", True, len(updated_dataset), len(chunked_data)) + return SUCCESSFUL_OPERATION + else: + return FAILED_TO_SAVE_AGGREGATED_DATA + + + From 7850d0e02bcf9f35f86fe6228ae7d575d2cf61f2 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 31 Jul 2024 04:00:22 +0530 Subject: [PATCH 314/582] fixing ruuter patch update bug --- .../DSL/POST/classifier/datasetgroup/update/patch.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/patch.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/patch.yml index 1dff7d81..49867a3d 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/patch.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/patch.yml @@ -77,8 +77,8 @@ execute_cron_manager: cookie: ${incoming.headers.cookie} dgId: ${dg_id} updateType: 'patch' - savedFilePath: null - patchPayload: ${update_data_payload} + savedFilePath: 'None' + patchPayload: ${encodeURIComponent(JSON.stringify(update_data_payload))} result: res next: assign_success_response From 63b55269e83b90c5d7196493066a2f14ac40ce72 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 31 Jul 2024 04:00:46 +0530 Subject: [PATCH 315/582] fixing file handler bugs --- file-handler/file_handler_api.py | 88 ++++++++++++++++++++------------ 1 file changed, 56 insertions(+), 32 deletions(-) diff --git a/file-handler/file_handler_api.py b/file-handler/file_handler_api.py index aceab837..98abe065 100644 --- a/file-handler/file_handler_api.py +++ b/file-handler/file_handler_api.py @@ -51,24 +51,34 @@ class ImportJsonMajor(BaseModel): def get_ruuter_private_url(): return os.getenv("RUUTER_PRIVATE_URL") -async def authenticate_user(request: Request): - cookie = request.cookies.get("customJwtCookie") - if not cookie: - raise HTTPException(status_code=401, detail="No cookie found in the request") +async def authenticate_user(cookie: str): + try: + # cookie = request.cookies.get("customJwtCookie") + # cookie = f'customJwtCookie={cookie}' - url = f"{RUUTER_PRIVATE_URL}/auth/jwt/userinfo" - headers = { - 'cookie': f'customJwtCookie={cookie}' - } + if not cookie: + raise HTTPException(status_code=401, detail="No cookie found in the request") + + print("@#!@#!@#!2") + print(cookie) - response = requests.get(url, headers=headers) - if response.status_code != 200: - raise HTTPException(status_code=response.status_code, detail="Authentication failed") + url = f"{RUUTER_PRIVATE_URL}/auth/jwt/userinfo" + headers = { + 'cookie': cookie + } + + response = requests.get(url, headers=headers) + + if response.status_code != 200: + raise HTTPException(status_code=response.status_code, detail="Authentication failed") + except Exception as e: + print(f"Error in file handler authentication : {e}") @app.post("/datasetgroup/data/import") async def upload_and_copy(request: Request, dgId: int = Form(...), dataFile: UploadFile = File(...)): try: - await authenticate_user(request) + cookie = request.cookies.get("customJwtCookie") + await authenticate_user(f'customJwtCookie={cookie}') print(f"Received dgId: {dgId}") print(f"Received filename: {dataFile.filename}") @@ -94,7 +104,7 @@ async def upload_and_copy(request: Request, dgId: int = Form(...), dataFile: Upl with open(json_local_file_path, 'w') as json_file: json.dump(converted_data, json_file, indent=4) - save_location = f"/dataset/{dgId}/primary_dataset/dataset_{dgId}_aggregated{JSON_EXT}" + save_location = f"/dataset/{dgId}/temp/temp_dataset{JSON_EXT}" source_file_path = file_name.replace(YML_EXT, JSON_EXT).replace(XLSX_EXT, JSON_EXT) response = s3_ferry.transfer_file(save_location, "S3", source_file_path, "FS") @@ -113,7 +123,8 @@ async def upload_and_copy(request: Request, dgId: int = Form(...), dataFile: Upl @app.post("/datasetgroup/data/download") async def download_and_convert(request: Request, exportData: ExportFile, backgroundTasks: BackgroundTasks): - await authenticate_user(request) + cookie = request.cookies.get("customJwtCookie") + await authenticate_user(f'customJwtCookie={cookie}') dg_id = exportData.dgId export_type = exportData.exportType @@ -152,7 +163,8 @@ async def download_and_convert(request: Request, exportData: ExportFile, backgro @app.get("/datasetgroup/data/download/json") async def download_and_convert(request: Request, dgId: int, background_tasks: BackgroundTasks): - await authenticate_user(request) + cookie = request.cookies.get("customJwtCookie") + await authenticate_user(f'customJwtCookie={cookie}') saveLocation = f"/dataset/{dgId}/primary_dataset/dataset_{dgId}_aggregated{JSON_EXT}" localFileName = f"group_{dgId}_aggregated" @@ -172,8 +184,8 @@ async def download_and_convert(request: Request, dgId: int, background_tasks: Ba @app.get("/datasetgroup/data/download/json/location") async def download_and_convert(request: Request, saveLocation:str, background_tasks: BackgroundTasks): - print(request) - await authenticate_user(request) + cookie = request.cookies.get("customJwtCookie") + await authenticate_user(f'customJwtCookie={cookie}') print(saveLocation) @@ -194,7 +206,8 @@ async def download_and_convert(request: Request, saveLocation:str, background_ta @app.post("/datasetgroup/data/import/chunk") async def upload_and_copy(request: Request, import_chunks: ImportChunks): - await authenticate_user(request) + cookie = request.cookies.get("customJwtCookie") + await authenticate_user(f'customJwtCookie={cookie}') dgID = import_chunks.dg_id chunks = import_chunks.chunks @@ -217,29 +230,40 @@ async def upload_and_copy(request: Request, import_chunks: ImportChunks): @app.get("/datasetgroup/data/download/chunk") async def download_and_convert(request: Request, dgId: int, pageId: int, backgroundTasks: BackgroundTasks): - await authenticate_user(request) - save_location = f"/dataset/{dgId}/chunks/{pageId}{JSON_EXT}" - local_file_name = f"group_{dgId}_chunk_{pageId}" + try: + cookie = request.cookies.get("customJwtCookie") + await authenticate_user(f'customJwtCookie={cookie}') + print("$#@$@#$@#$@#$") + print(request) + # cookie = request.cookies.get("cookie") + # cookie = f'customJwtCookie={cookie}' + # await authenticate_user(cookie) + save_location = f"/dataset/{dgId}/chunks/{pageId}{JSON_EXT}" + local_file_name = f"group_{dgId}_chunk_{pageId}" - response = s3_ferry.transfer_file(f"{local_file_name}{JSON_EXT}", "FS", save_location, "S3") - if response.status_code != 201: - raise HTTPException(status_code=500, detail=S3_DOWNLOAD_FAILED) + response = s3_ferry.transfer_file(f"{local_file_name}{JSON_EXT}", "FS", save_location, "S3") + if response.status_code != 201: + print("S3 Download Failed") + return {} - json_file_path = os.path.join('..', 'shared', f"{local_file_name}{JSON_EXT}") + json_file_path = os.path.join('..', 'shared', f"{local_file_name}{JSON_EXT}") - with open(f"{json_file_path}", 'r') as json_file: - json_data = json.load(json_file) + with open(f"{json_file_path}", 'r') as json_file: + json_data = json.load(json_file) - # for index, item in enumerate(json_data, start=1): - # item['rowID'] = index + # for index, item in enumerate(json_data, start=1): + # item['rowID'] = index - backgroundTasks.add_task(os.remove, json_file_path) + backgroundTasks.add_task(os.remove, json_file_path) - return json_data + return json_data + except Exception as e: + print(f"Error in download/chunk : {e}") @app.post("/datasetgroup/data/import/json") async def upload_and_copy(request: Request, importData: ImportJsonMajor): - await authenticate_user(request) + cookie = request.cookies.get("customJwtCookie") + await authenticate_user(f'customJwtCookie={cookie}') fileName = f"{uuid.uuid4()}.{JSON_EXT}" fileLocation = os.path.join(UPLOAD_DIRECTORY, fileName) From a60ea4c2bb0c2b678889ca4f426a82df7e2e812f Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Wed, 31 Jul 2024 08:34:56 +0530 Subject: [PATCH 316/582] ESCLASS-145-delete-script: implement shell script to delete completed data from opensearch --- .../DSL/dataset_progress_session.yml | 2 +- ...ete_completed_dataset_progress_sessions.sh | 35 +++++++++++++++---- ...te-completed-dataset-progress-sessions.sql | 3 +- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/DSL/CronManager/DSL/dataset_progress_session.yml b/DSL/CronManager/DSL/dataset_progress_session.yml index f11405a3..e60b8ba9 100644 --- a/DSL/CronManager/DSL/dataset_progress_session.yml +++ b/DSL/CronManager/DSL/dataset_progress_session.yml @@ -1,4 +1,4 @@ -token_refresh: +delete_completed_sessions: trigger: "0 0 0 * * ?" type: exec command: "../app/scripts/delete_completed_dataset_progress_sessions.sh" \ No newline at end of file diff --git a/DSL/CronManager/script/delete_completed_dataset_progress_sessions.sh b/DSL/CronManager/script/delete_completed_dataset_progress_sessions.sh index e32f382d..2ca3b7af 100644 --- a/DSL/CronManager/script/delete_completed_dataset_progress_sessions.sh +++ b/DSL/CronManager/script/delete_completed_dataset_progress_sessions.sh @@ -1,9 +1,7 @@ #!/bin/bash -# Set the working directory to the location of the script cd "$(dirname "$0")" -# Source the constants from the ini file source ../config/config.ini script_name=$(basename $0) @@ -12,15 +10,38 @@ pwd echo $(date -u +"%Y-%m-%d %H:%M:%S.%3NZ") - $script_name started delete_dataset_progress_sessions() { - delete_response=$(curl -s -X DELETE "$CLASSIFIER_RESQL/delete-completed-dataset-progress-sessions") - if echo "$delete_response" | grep -q '"success":true'; then - echo "Data deletion successful" + delete_response=$(curl -s -X POST -H "Content-Type: application/json" "http://resql:8082/delete-completed-dataset-progress-sessions") + + echo "Response from delete request: $delete_response" + + session_ids=$(echo "$delete_response" | grep -oP '"id":\K\d+' | tr '\n' ' ' | sed 's/ $//') # Remove trailing space + + echo "Session IDs: $session_ids" + + if [ -n "$session_ids" ]; then + delete_from_opensearch "$session_ids" else - echo "Data deletion failed: $delete_response" - exit 1 + echo "No session IDs were returned in the response." fi } +delete_from_opensearch() { + local session_ids="$1" + + delete_query="{\"query\": {\"terms\": {\"sessionId\": [" + for id in $session_ids; do + delete_query+="\"$id\"," + done + delete_query=$(echo "$delete_query" | sed 's/,$//') # Remove trailing comma + delete_query+="]}}}" + + echo "delete query: $delete_query" + + opensearch_response=$(curl -s -X POST -H "Content-Type: application/json" -d "$delete_query" "http://opensearch-node:9200/dataset_progress_sessions/_delete_by_query") + + echo "Response from OpenSearch delete request: $opensearch_response" +} + delete_dataset_progress_sessions echo $(date -u +"%Y-%m-%d %H:%M:%S.%3NZ") - $script_name finished diff --git a/DSL/Resql/delete-completed-dataset-progress-sessions.sql b/DSL/Resql/delete-completed-dataset-progress-sessions.sql index 4ea006b9..eb0b5728 100644 --- a/DSL/Resql/delete-completed-dataset-progress-sessions.sql +++ b/DSL/Resql/delete-completed-dataset-progress-sessions.sql @@ -1,2 +1,3 @@ DELETE FROM dataset_progress_sessions -WHERE process_complete = true; +WHERE process_complete = true +RETURNING id; From d3518468a5a3c215019024d891c1b858c9a3249d Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 31 Jul 2024 08:56:43 +0530 Subject: [PATCH 317/582] removing s3 mock --- dataset-processor/dataset_processor.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/dataset-processor/dataset_processor.py b/dataset-processor/dataset_processor.py index c66978fd..b2630720 100644 --- a/dataset-processor/dataset_processor.py +++ b/dataset-processor/dataset_processor.py @@ -5,7 +5,6 @@ import requests # from data_enrichment.data_enrichment import DataEnrichment from constants import * -from s3_mock import S3FileCounter RUUTER_PRIVATE_URL = os.getenv("RUUTER_PRIVATE_URL") GET_VALIDATION_SCHEMA = os.getenv("GET_VALIDATION_SCHEMA") @@ -21,7 +20,7 @@ class DatasetProcessor: def __init__(self): # self.data_enricher = DataEnrichment() - self.s3_file_counter = S3FileCounter() + pass def check_and_convert(self, data): print(data) @@ -211,10 +210,6 @@ def get_page_count(self, dg_id, custom_jwt_cookie): page_count = data["response"]["data"][0]["numPages"] return page_count - # try: - # folder_path = f'data/dataset/{dg_id}/chunks/' - # file_count = self.s3_file_counter.count_files_in_folder(folder_path) - # return file_count except requests.exceptions.RequestException as e: print(f"An error occurred: {e}") return None From ce5893569d1ebbee274cced46bd25f132f90a2f5 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 31 Jul 2024 09:15:57 +0530 Subject: [PATCH 318/582] polising the code --- dataset-processor/dataset_processor.py | 90 +++++++++++++++++--------- 1 file changed, 60 insertions(+), 30 deletions(-) diff --git a/dataset-processor/dataset_processor.py b/dataset-processor/dataset_processor.py index b2630720..180bcf7c 100644 --- a/dataset-processor/dataset_processor.py +++ b/dataset-processor/dataset_processor.py @@ -518,38 +518,68 @@ def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload) print(data_payload["deletedDataRows"]) if (data_payload["deletedDataRows"]!=[]): - deleted_rows = data_payload["deletedDataRows"] - aggregated_dataset = self.get_dataset(dgID, cookie) - updated_dataset = [row for row in aggregated_dataset if row.get('rowID') not in deleted_rows] - for idx, row in enumerate(updated_dataset, start=1): - row['rowID'] = idx - if updated_dataset is not None: - chunked_data = self.chunk_data(updated_dataset) - if chunked_data is not None: - print("Data chunking successful") - print(chunked_data) - operation_result = self.save_chunked_data(chunked_data, cookie, dgID, 0) - save_result_delete = self.save_aggregrated_data(dgID, cookie, updated_dataset) + try: + print("Handling deleted data rows") + deleted_rows = data_payload["deletedDataRows"] + aggregated_dataset = self.get_dataset(dgID, cookie) + if aggregated_dataset is not None: + print("Aggregated dataset for delete operation retrieved successfully") + updated_dataset = [row for row in aggregated_dataset if row.get('rowID') not in deleted_rows] + for idx, row in enumerate(updated_dataset, start=1): + row['rowID'] = idx + if updated_dataset is not None: + print("Deleted rows removed and dataset updated successfully") + chunked_data = self.chunk_data(updated_dataset) + if chunked_data is not None: + print("Data chunking after delete operation successful") + print(chunked_data) + operation_result = self.save_chunked_data(chunked_data, cookie, dgID, 0) + if operation_result: + print("Chunked data after delete operation saved successfully") + save_result_delete = self.save_aggregrated_data(dgID, cookie, updated_dataset) + if save_result_delete: + print("Aggregated dataset after delete operation saved successfully") + else: + print("Failed to save aggregated dataset after delete operation") + return FAILED_TO_SAVE_AGGREGATED_DATA + else: + print("Failed to save chunked data after delete operation") + return FAILED_TO_SAVE_CHUNKED_DATA + else: + print("Failed to chunk data after delete operation") + return FAILED_TO_CHUNK_CLEANED_DATA + else: + print("Failed to update dataset after deleting rows") + return FAILED_TO_UPDATE_DATASET + else: + print("Failed to retrieve aggregated dataset for delete operation") + return FAILED_TO_GET_AGGREGATED_DATASET + except Exception as e: + print(f"An error occurred while handling deleted data rows: {e}") + return FAILED_TO_HANDLE_DELETED_ROWS - if data_payload["editedData"]==[] and data_payload["deletedDataRows"]==[]: - return SUCCESSFUL_OPERATION - elif data_payload["editedData"]!=[] and data_payload["deletedDataRows"]==[]: - if save_result_update: - return SUCCESSFUL_OPERATION - else: - return FAILED_TO_SAVE_AGGREGATED_DATA - elif data_payload["editedData"]==[] and data_payload["deletedDataRows"]!=[]: - if save_result_delete: - return_data = self.update_preprocess_status(dgID, cookie, True, False, f"/dataset/{dgID}/chunks/", "", True, len(updated_dataset), len(chunked_data)) + if data_payload["editedData"]==[] and data_payload["deletedDataRows"]==[]: return SUCCESSFUL_OPERATION - else: - return FAILED_TO_SAVE_AGGREGATED_DATA - elif data_payload["editedData"]!=[] and data_payload["deletedDataRows"]!=[]: - if save_result_update and save_result_delete: - return_data = self.update_preprocess_status(dgID, cookie, True, False, f"/dataset/{dgID}/chunks/", "", True, len(updated_dataset), len(chunked_data)) - return SUCCESSFUL_OPERATION - else: - return FAILED_TO_SAVE_AGGREGATED_DATA + elif data_payload["editedData"]!=[] and data_payload["deletedDataRows"]==[]: + if save_result_update: + return SUCCESSFUL_OPERATION + else: + return FAILED_TO_SAVE_AGGREGATED_DATA + elif data_payload["editedData"]==[] and data_payload["deletedDataRows"]!=[]: + if save_result_delete: + return_data = self.update_preprocess_status(dgID, cookie, True, False, f"/dataset/{dgID}/chunks/", "", True, len(updated_dataset), len(chunked_data)) + print(return_data) + return SUCCESSFUL_OPERATION + else: + return FAILED_TO_SAVE_AGGREGATED_DATA + elif data_payload["editedData"]!=[] and data_payload["deletedDataRows"]!=[]: + if save_result_update and save_result_delete: + return_data = self.update_preprocess_status(dgID, cookie, True, False, f"/dataset/{dgID}/chunks/", "", True, len(updated_dataset), len(chunked_data)) + print(return_data) + return SUCCESSFUL_OPERATION + else: + return FAILED_TO_SAVE_AGGREGATED_DATA + From 10fd26e37ac9b58e9f27334f2d0cea25cd0f8a36 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 31 Jul 2024 09:45:44 +0530 Subject: [PATCH 319/582] enabling dataset synthasis --- dataset-processor/dataset_processor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dataset-processor/dataset_processor.py b/dataset-processor/dataset_processor.py index 180bcf7c..73ac820b 100644 --- a/dataset-processor/dataset_processor.py +++ b/dataset-processor/dataset_processor.py @@ -3,7 +3,7 @@ import json import urllib.parse import requests -# from data_enrichment.data_enrichment import DataEnrichment +from data_enrichment.data_enrichment import DataEnrichment from constants import * RUUTER_PRIVATE_URL = os.getenv("RUUTER_PRIVATE_URL") @@ -19,7 +19,7 @@ class DatasetProcessor: def __init__(self): - # self.data_enricher = DataEnrichment() + self.data_enricher = DataEnrichment() pass def check_and_convert(self, data): @@ -83,7 +83,7 @@ def enrich_data(self, data, selected_fields, record_count): enriched_entry = {} for key, value in entry.items(): if isinstance(value, str) and (key in selected_fields): - # enriched_value = self.data_enricher.enrich_data(value, num_return_sequences=1, language_id='en') + enriched_value = self.data_enricher.enrich_data(value, num_return_sequences=1, language_id='en') enriched_value = ["enrichupdate"] enriched_entry[key] = enriched_value[0] if enriched_value else value else: From 80243556abeec444715e4113d8c74a8365697983 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 31 Jul 2024 09:46:33 +0530 Subject: [PATCH 320/582] uncomment --- dataset-processor/dataset_processor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dataset-processor/dataset_processor.py b/dataset-processor/dataset_processor.py index 73ac820b..75befaa9 100644 --- a/dataset-processor/dataset_processor.py +++ b/dataset-processor/dataset_processor.py @@ -84,7 +84,7 @@ def enrich_data(self, data, selected_fields, record_count): for key, value in entry.items(): if isinstance(value, str) and (key in selected_fields): enriched_value = self.data_enricher.enrich_data(value, num_return_sequences=1, language_id='en') - enriched_value = ["enrichupdate"] + # enriched_value = ["enrichupdate"] enriched_entry[key] = enriched_value[0] if enriched_value else value else: enriched_entry[key] = value From c3de590938eedfd108836d5d1a06881f4de161f1 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Wed, 31 Jul 2024 11:20:11 +0530 Subject: [PATCH 321/582] dataset groups edit flow --- .../molecules/ClassHeirarchy/index.tsx | 4 +- .../molecules/ValidationCriteria/RowsView.tsx | 11 +- GUI/src/enums/commonEnums.ts | 11 + GUI/src/enums/integrationEnums.ts | 21 +- .../pages/DatasetGroups/DatasetGroups.scss | 13 + .../pages/DatasetGroups/ViewDatasetGroup.tsx | 321 +++++++++++------- GUI/src/utils/commonUtilts.ts | 46 ++- GUI/src/utils/datasetGroupsUtils.ts | 8 +- GUI/src/utils/endpoints.ts | 14 + GUI/src/utils/queryKeys.ts | 17 + 10 files changed, 300 insertions(+), 166 deletions(-) create mode 100644 GUI/src/enums/commonEnums.ts create mode 100644 GUI/src/utils/endpoints.ts create mode 100644 GUI/src/utils/queryKeys.ts diff --git a/GUI/src/components/molecules/ClassHeirarchy/index.tsx b/GUI/src/components/molecules/ClassHeirarchy/index.tsx index 8fb09dfe..63cc2000 100644 --- a/GUI/src/components/molecules/ClassHeirarchy/index.tsx +++ b/GUI/src/components/molecules/ClassHeirarchy/index.tsx @@ -1,6 +1,5 @@ import React, { FC, PropsWithChildren, useState } from 'react'; import { MdDeleteOutline } from 'react-icons/md'; -import Card from 'components/Card'; import { FormInput } from 'components/FormElements'; import Button from 'components/Button'; import { v4 as uuidv4 } from 'uuid'; @@ -14,6 +13,8 @@ type ClassHierarchyProps = { setNodes: React.Dispatch>; nodesError?: boolean; setNodesError: React.Dispatch>; + setBannerMessage:React.Dispatch>; + }; const ClassHierarchy: FC> = ({ @@ -21,6 +22,7 @@ const ClassHierarchy: FC> = ({ setNodes, nodesError, setNodesError, + setBannerMessage }) => { const [currentNode, setCurrentNode] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); diff --git a/GUI/src/components/molecules/ValidationCriteria/RowsView.tsx b/GUI/src/components/molecules/ValidationCriteria/RowsView.tsx index e548dc1b..25261c99 100644 --- a/GUI/src/components/molecules/ValidationCriteria/RowsView.tsx +++ b/GUI/src/components/molecules/ValidationCriteria/RowsView.tsx @@ -1,26 +1,25 @@ -import React, { FC, PropsWithChildren, useCallback } from 'react'; -import { DndProvider, useDrag, useDrop } from 'react-dnd'; -import { HTML5Backend } from 'react-dnd-html5-backend'; +import React, { FC, PropsWithChildren } from 'react'; import dataTypes from '../../../config/dataTypesConfig.json'; -import { MdAdd, MdDehaze, MdDelete } from 'react-icons/md'; -import Card from 'components/Card'; +import { MdAdd, MdDelete } from 'react-icons/md'; import { FormCheckbox, FormInput, FormSelect } from 'components/FormElements'; -import Button from 'components/Button'; import { ValidationRule } from 'types/datasetGroups'; import { Link } from 'react-router-dom'; import { isFieldNameExisting } from 'utils/datasetGroupsUtils'; +import { v4 as uuidv4 } from 'uuid'; type ValidationRulesProps = { validationRules?: ValidationRule[]; setValidationRules: React.Dispatch>; validationRuleError?: boolean; setValidationRuleError: React.Dispatch>; + setBannerMessage:React.Dispatch>; }; const ValidationCriteriaRowsView: FC> = ({ validationRules, setValidationRules, setValidationRuleError, validationRuleError, + setBannerMessage }) => { const setIsDataClass = (id, isDataClass) => { const updatedItems = validationRules.map((item) => diff --git a/GUI/src/enums/commonEnums.ts b/GUI/src/enums/commonEnums.ts new file mode 100644 index 00000000..25f7cd63 --- /dev/null +++ b/GUI/src/enums/commonEnums.ts @@ -0,0 +1,11 @@ +export enum ToastTypes { + SUCCESS = 'success', + ERROR = 'error', +} + +export enum ButtonAppearanceTypes { + PRIMARY = 'primary', + SECONDARY = 'secondary', + ERROR = 'error', + TEXT = 'text', +} diff --git a/GUI/src/enums/integrationEnums.ts b/GUI/src/enums/integrationEnums.ts index 4bf901d2..4d4a521d 100644 --- a/GUI/src/enums/integrationEnums.ts +++ b/GUI/src/enums/integrationEnums.ts @@ -1,12 +1,13 @@ export enum INTEGRATION_MODALS { - INTEGRATION_SUCCESS = 'INTEGRATION_SUCCESS', - INTEGRATION_ERROR='INTEGRATION_ERROR', - DISCONNECT_CONFIRMATION='DISCONNECT_CONFIRMATION', - CONNECT_CONFIRMATION='CONNECT_CONFIRMATION', - DISCONNECT_ERROR='DISCONNECT_ERROR' - } + INTEGRATION_SUCCESS = 'INTEGRATION_SUCCESS', + INTEGRATION_ERROR = 'INTEGRATION_ERROR', + DISCONNECT_CONFIRMATION = 'DISCONNECT_CONFIRMATION', + CONNECT_CONFIRMATION = 'CONNECT_CONFIRMATION', + DISCONNECT_ERROR = 'DISCONNECT_ERROR', + NULL = 'NULL', +} - export enum INTEGRATION_OPERATIONS { - ENABLE = 'enable', - DISABLE='disable' - } \ No newline at end of file +export enum INTEGRATION_OPERATIONS { + ENABLE = 'enable', + DISABLE = 'disable', +} diff --git a/GUI/src/pages/DatasetGroups/DatasetGroups.scss b/GUI/src/pages/DatasetGroups/DatasetGroups.scss index c2aa95bc..1e5a7af8 100644 --- a/GUI/src/pages/DatasetGroups/DatasetGroups.scss +++ b/GUI/src/pages/DatasetGroups/DatasetGroups.scss @@ -1,4 +1,6 @@ @import 'src/styles/tools/color'; +@import 'src/styles/tools/spacing'; + .search-panel { background-color: white; @@ -27,3 +29,14 @@ padding: 16px; width: 100%; } + +.banner{ + background-color: #fff3f3; + border: 1px solid #cf0000; + border-radius: 5px; + margin-bottom: 10px; + padding: 20px 200px; + text-align: center; + color: #cf0000; + +} diff --git a/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx b/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx index f9d1ecb8..ac9a1f7b 100644 --- a/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx +++ b/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx @@ -47,6 +47,7 @@ import { useDialog } from 'hooks/useDialog'; import { useForm } from 'react-hook-form'; import { handleDownload, + isMajorUpdate, reverseTransformClassHierarchy, transformClassHierarchy, transformObjectToArray, @@ -64,9 +65,7 @@ type Props = { }; const ViewDatasetGroup: FC> = ({ dgId, setView }) => { const { t } = useTranslation(); - const [searchParams] = useSearchParams(); const { open, close } = useDialog(); - const { register } = useForm(); const queryClient = useQueryClient(); const [validationRuleError, setValidationRuleError] = useState(false); @@ -81,10 +80,17 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { const [isImportModalOpen, setIsImportModalOpen] = useState(false); const [isExportModalOpen, setIsExportModalOpen] = useState(false); const [patchUpdateModalOpen, setPatchUpdateModalOpen] = useState(false); + const [deleteRowModalOpen, setDeleteRowModalOpen] = useState(false); const fileUploadRef = useRef(null); const [fetchEnabled, setFetchEnabled] = useState(true); const [file, setFile] = useState(''); const [selectedRow, setSelectedRow] = useState({}); + const [isDataChanged, setIsDataChanged] = useState(false); + const [bannerMessage, setBannerMessage] = useState(''); + const [minorPayload, setMinorPayload] = useState(""); + const [patchPayload, setPatchPayload] = useState(""); + const [deletedDataRows, setDeletedDataRows] = useState([]); + const [updatePriority, setUpdatePriority] = useState(''); const navigate = useNavigate(); @@ -99,71 +105,6 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { keepPreviousData: true, } ); - // dgId: 1, - // operationSuccessful: true, - // fields: [ - // 'rowId', - // 'emailAddress', - // 'emailBody', - // 'emailSendTime', - // 'departmentCode', - // 'ministry', - // 'division', - // ], - // dataPayload: [ - // { - // rowId: 1, - // emailAddress: 'thiru.dinesh@rootcodelabs.com', - // emailBody: - // 'A generated email body that is interenstingly sophisticated and long enought to fill the screen. This body should also simulate the real life emails recieved by the government authorities in Estonia. Will be very interesting', - // emailSendTime: 'Sun, 07 Jul 2024 08:35:54 GMT', - // departmentCode: '05ABC', - // ministry: 'police and border guard', - // division: 'complaints processsing', - // }, - // { - // rowId: 2, - // emailAddress: 'thiru.dinesh@rootcodelabs.com', - // emailBody: - // 'A generated email body that is interenstingly sophisticated and long enought to fill the screen. This body should also simulate the real life emails recieved by the government authorities in Estonia. Will be very interesting', - // emailSendTime: 'Sun, 07 Jul 2024 08:35:54 GMT', - // departmentCode: '05ABC', - // ministry: 'police and border guard', - // division: 'complaints processsing', - // }, - // { - // rowId: 3, - // emailAddress: 'thiru.dinesh@rootcodelabs.com', - // emailBody: - // 'A generated email body that is interenstingly sophisticated and long enought to fill the screen. This body should also simulate the real life emails recieved by the government authorities in Estonia. Will be very interesting', - // emailSendTime: 'Sun, 07 Jul 2024 08:35:54 GMT', - // departmentCode: '05ABC', - // ministry: 'police and border guard', - // division: 'complaints processsing', - // }, - // { - // rowId: 4, - // emailAddress: 'thiru.dinesh@rootcodelabs.com', - // emailBody: - // 'A generated email body that is interenstingly sophisticated and long enought to fill the screen. This body should also simulate the real life emails recieved by the government authorities in Estonia. Will be very interesting', - // emailSendTime: 'Sun, 07 Jul 2024 08:35:54 GMT', - // departmentCode: '05ABC', - // ministry: 'police and border guard', - // division: 'complaints processsing', - // }, - // { - // rowId: 5, - // emailAddress: 'thiru.dinesh@rootcodelabs.com', - // emailBody: - // 'A generated email body that is interenstingly sophisticated and long enought to fill the screen. This body should also simulate the real life emails recieved by the government authorities in Estonia. Will be very interesting', - // emailSendTime: 'Sun, 07 Jul 2024 08:35:54 GMT', - // departmentCode: '05ABC', - // ministry: 'police and border guard', - // division: 'complaints processsing', - // }, - // ], - // }; - // const isLoading = false; const { data: metadata, isLoading: isMetadataLoading } = useQuery( ['datasets/groups/metadata', dgId], @@ -171,6 +112,11 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { { enabled: fetchEnabled } ); + const [updatedDataset, setUpdatedDataset] = useState(datasets?.dataPayload); + useEffect(() => { + setUpdatedDataset(datasets?.dataPayload); + }, [datasets]); + const [nodes, setNodes] = useState( reverseTransformClassHierarchy( metadata?.response?.data?.[0]?.classHierarchy @@ -200,26 +146,58 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { ); }, [metadata]); - const patchDataUpdate = (dataset) => { - const payload= datasets?.dataPayload?.map((row) => - row.rowID === selectedRow?.rowID ? dataset : row + const deleteRow = (dataRow) => { + setDeletedDataRows((prevDeletedDataRows) => [ + ...prevDeletedDataRows, + dataRow?.rowID, + ]); + const payload = updatedDataset?.filter((row) => { + if (row.rowID !== selectedRow?.rowID) return row; + }); + setUpdatedDataset(payload); + + const updatedPayload = { + dgId, + updateDataPayload: { + deletedDataRows: [...deletedDataRows, dataRow?.rowID], + editedData: payload, + }, + }; + setPatchPayload(updatedPayload); + setDeleteRowModalOpen(false); + // setIsDataChanged(true); + setBannerMessage( + 'You have edited individual items in the dataset which are not saved. Please save the changes to apply' + ); + }; + + const patchDataUpdate = (dataRow) => { + const payload = updatedDataset?.map((row) => + row.rowID === selectedRow?.rowID ? dataRow : row ); + setUpdatedDataset(payload); const updatedPayload = { dgId, - updateDataPayload: payload + updateDataPayload: { + deletedDataRows, + editedData: payload, + }, }; - patchUpdateMutation.mutate(updatedPayload); + setPatchPayload(updatedPayload); + setPatchUpdateModalOpen(false); + // setIsDataChanged(true); + setBannerMessage( + 'You have edited individual items in the dataset which are not saved. Please save the changes to apply' + ); - console.log(updatedPayload); - + // patchUpdateMutation.mutate(updatedPayload); }; const patchUpdateMutation = useMutation({ mutationFn: (data) => patchUpdate(data), onSuccess: async () => { await queryClient.invalidateQueries(['datasets/groups/data']); - setPatchUpdateModalOpen(false); }, onError: () => { setPatchUpdateModalOpen(false); @@ -277,22 +255,10 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { const deleteView = (props: any) => ( - -
    - ), - }) - } + onClick={() => { + setSelectedRow(props.row.original); + setDeleteRowModalOpen(true); + }} > } /> {'Delete'} @@ -344,11 +310,20 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { mutationFn: (data: ImportDataset) => importDataset(data?.dataFile, data?.dgId), onSuccess: async (response) => { - const payload = { + setMinorPayload({ dgId, s3FilePath: response?.saved_file_path, - }; - minorUpdateMutation.mutate(payload); + }); + setBannerMessage( + 'You have imported new data into the dataset, please save the changes to apply. Any changes you made to the individual data items will be discarded after changes are applied ' + ); + // const payload = { + // dgId, + // s3FilePath: response?.saved_file_path, + // }; + // setIsDataChanged(true); + setIsImportModalOpen(false); + // minorUpdateMutation.mutate(payload); }, onError: () => { open({ @@ -406,7 +381,17 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { } }; - const datasetGroupMajorUpdate = () => { + const handleMajorUpdate = () => { + const payload: DatasetGroup = { + dgId, + validationCriteria: { ...transformValidationRules(validationRules) }, + ...transformClassHierarchy(nodes), + }; + majorUpdateDatasetGroupMutation.mutate(payload); + + }; + + const datasetGroupUpdate = () => { setNodesError(validateClassHierarchy(nodes)); setValidationRuleError(validateValidationRules(validationRules)); if ( @@ -415,12 +400,60 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { !nodesError && !validationRuleError ) { - const payload: DatasetGroup = { - dgId, - validationCriteria: { ...transformValidationRules(validationRules) }, - ...transformClassHierarchy(nodes), - }; - majorUpdateDatasetGroupMutation.mutate(payload); + if ( + isMajorUpdate( + { + validationRules: + metadata?.response?.data?.[0]?.validationCriteria?.validationRules, + classHierarchy: metadata?.response?.data?.[0]?.classHierarchy, + }, + { + validationRules: + transformValidationRules(validationRules)?.validationRules, + ...transformClassHierarchy(nodes), + } + ) + ) { + open({ + content: + 'Any files imported or edits made to the existing data will be discarded after changes are applied', + title: 'Confirm major update', + footer: ( +
    + + +
    + ), + }); + } + } + else if (minorPayload) { + open({ + content: + 'Any changes you made to the individual data items (patch update) will be discarded after changes are applied', + title: 'Confirm minor update', + footer: ( +
    + + +
    + ), + }); + } else if (patchPayload) { + open({ + content: 'Changed data rows will be updated in the dataset', + title: 'Confirm patch update', + footer: ( +
    + + +
    + ), + }); } }; @@ -429,6 +462,7 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { onSuccess: async (response) => { await queryClient.invalidateQueries(['datasetgroup/overview']); setView('list'); + close(); }, onError: () => { open({ @@ -506,46 +540,52 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => {
    - -
    - {!datasets&& ( -
    -
    - No Data Available + {bannerMessage &&
    {bannerMessage}
    } + {(!datasets || (datasets && datasets?.length < 10)) && ( + +
    + {!datasets && ( +
    +
    + No Data Available +
    +

    + You have created the dataset group, but there are no + datasets available to show here. You can upload a + dataset to view it in this space. Once added, you can + edit or delete the data as needed. +

    +
    -

    - You have created the dataset group, but there are no - datasets available to show here. You can upload a - dataset to view it in this space. Once added, you can - edit or delete the data as needed. -

    - -
    - )} - {datasets && - datasets?.length < 10 && ( + )} + {datasets && datasets?.length < 10 && (

    Insufficient examples - at least 10 examples are needed to activate the dataset group.

    - +
    )} -
    - +
    + + )}
    )}
    - {!isLoading && datasets && ( + {!isLoading && updatedDataset && ( { @@ -570,6 +610,7 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { setValidationRules={setValidationRules} validationRuleError={validationRuleError} setValidationRuleError={setValidationRuleError} + setBannerMessage={setBannerMessage} /> @@ -580,6 +621,7 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { setNodes={setNodes} nodesError={nodesError} setNodesError={setNodesError} + setBannerMessage={setBannerMessage} /> )} @@ -621,7 +663,7 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { > Delete Dataset - +
    @@ -727,6 +769,25 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { /> )} + {deleteRowModalOpen && ( + setDeleteRowModalOpen(false)} + title="Are you sure?" + footer={ +
    + + +
    + } + > + Confirm that you are wish to delete the following record +
    + )} ); }; diff --git a/GUI/src/utils/commonUtilts.ts b/GUI/src/utils/commonUtilts.ts index 5cc6950f..a72a6976 100644 --- a/GUI/src/utils/commonUtilts.ts +++ b/GUI/src/utils/commonUtilts.ts @@ -1,22 +1,32 @@ +import { rankItem } from '@tanstack/match-sorter-utils'; +import { FilterFn } from '@tanstack/react-table'; import moment from 'moment'; -export const formattedArray = (data:string[])=>{ - return data?.map(name => ({ - label: name, - value: name - })); - }; +export const formattedArray = (data: string[]) => { + return data?.map((name) => ({ + label: name, + value: name, + })); +}; + +export const convertTimestampToDateTime = (timestamp: number) => { + return moment.unix(timestamp).format('YYYY-MM-DD HH:mm:ss'); +}; - export const convertTimestampToDateTime=(timestamp:number)=> { - return moment.unix(timestamp).format('YYYY-MM-DD HH:mm:ss'); - } +export const parseVersionString = (version: string) => { + const parts = version.split('.'); + + return { + major: parts[0] !== 'x' ? parseInt(parts[0], 10) : -1, + minor: parts[1] !== 'x' ? parseInt(parts[1], 10) : -1, + patch: parts[2] !== 'x' ? parseInt(parts[2], 10) : -1, + }; +}; - export const parseVersionString=(version: string)=> { - const parts = version.split('.'); - - return { - major: parts[0] !== 'x' ? parseInt(parts[0], 10) : -1, - minor: parts[1] !== 'x' ? parseInt(parts[1], 10) : -1, - patch: parts[2] !== 'x' ? parseInt(parts[2], 10) : -1, - }; - } \ No newline at end of file +export const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { + const itemRank = rankItem(row.getValue(columnId), value); + addMeta({ + itemRank, + }); + return itemRank.passed; +}; diff --git a/GUI/src/utils/datasetGroupsUtils.ts b/GUI/src/utils/datasetGroupsUtils.ts index 0c6a6dca..2c325fbc 100644 --- a/GUI/src/utils/datasetGroupsUtils.ts +++ b/GUI/src/utils/datasetGroupsUtils.ts @@ -1,5 +1,6 @@ import { Class, ValidationRule } from 'types/datasetGroups'; import { v4 as uuidv4 } from 'uuid'; +import isEqual from 'lodash/isEqual'; export const transformValidationRules = ( data: ValidationRule[] | undefined @@ -117,7 +118,7 @@ export const isValidationRulesSatisfied = (data: ValidationRule[]) => { export const isFieldNameExisting = (dataArray, fieldNameToCheck) => { - const count = dataArray.reduce((acc, item) => { + const count = dataArray?.reduce((acc, item) => { return item?.fieldName?.toLowerCase() === fieldNameToCheck?.toLowerCase() ? acc + 1 : acc; }, 0); @@ -162,3 +163,8 @@ export const handleDownload = (response,format) =>{ } } +export const isMajorUpdate = (initialData,updatedData) =>{ + return !isEqual(initialData, updatedData); + +} + diff --git a/GUI/src/utils/endpoints.ts b/GUI/src/utils/endpoints.ts new file mode 100644 index 00000000..8d0afdfe --- /dev/null +++ b/GUI/src/utils/endpoints.ts @@ -0,0 +1,14 @@ +export const userManagementEndpoints = { + FETCH_USERS: (): string => `/accounts/users`, + ADD_USER: (): string => `/accounts/add`, + CHECK_ACCOUNT_AVAILABILITY: (): string => `/accounts/exists`, + EDIT_USER: (): string => `/accounts/edit`, + DELETE_USER: (): string => `/accounts/delete`, + FETCH_USER_ROLES: (): string => `/accounts/user-role` +}; + +export const integrationsEndPoints = { + GET_INTEGRATION_STATUS: (): string => + `/classifier/integration/platform-status`, + TOGGLE_PLATFORM: (): string => `/classifier/integration/toggle-platform`, +}; diff --git a/GUI/src/utils/queryKeys.ts b/GUI/src/utils/queryKeys.ts new file mode 100644 index 00000000..7f54ad3a --- /dev/null +++ b/GUI/src/utils/queryKeys.ts @@ -0,0 +1,17 @@ +import { PaginationState, SortingState } from '@tanstack/react-table'; + +export const userManagementQueryKeys = { + getAllEmployees: function ( + pagination?: PaginationState, + sorting?: SortingState + ) { + return ['accounts/users', pagination, sorting]; + }, +}; + +export const integrationQueryKeys = { + INTEGRATION_STATUS: (): string[] => [ + `classifier/integration/platform-status`, + ], + USER_ROLES: (): string[] => ['/accounts/user-role', 'prod'], +}; From ca31de99164ace567c70f9c388c459de246677c9 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 31 Jul 2024 12:49:50 +0530 Subject: [PATCH 322/582] new upates --- dataset-processor/constants.py | 12 +++++++++++ dataset-processor/dataset_processor.py | 28 +++++++++++++------------- file-handler/file_handler_api.py | 4 ++-- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/dataset-processor/constants.py b/dataset-processor/constants.py index 53b17f56..3343b1fb 100644 --- a/dataset-processor/constants.py +++ b/dataset-processor/constants.py @@ -81,3 +81,15 @@ "operation_successful": False, "reason": "Failed to download chunk" } + +FAILED_TO_HANDLE_DELETED_ROWS = { + "operation_status": 500, + "operation_successful": False, + "reason": "Failed to handle deleted rows" +} + +FAILED_TO_UPDATE_DATASET = { + "operation_status": 500, + "operation_successful": False, + "reason": "Failed to update dataset after deleting rows" +} diff --git a/dataset-processor/dataset_processor.py b/dataset-processor/dataset_processor.py index 75befaa9..c612f983 100644 --- a/dataset-processor/dataset_processor.py +++ b/dataset-processor/dataset_processor.py @@ -89,7 +89,7 @@ def enrich_data(self, data, selected_fields, record_count): else: enriched_entry[key] = value record_count = record_count+1 - enriched_entry["rowID"] = record_count + enriched_entry["rowId"] = record_count enriched_data.append(enriched_entry) return enriched_data except Exception as e: @@ -253,7 +253,7 @@ def add_row_id(self, structured_data, max_row_id): processed_data = [] for data in structured_data: max_row_id = max_row_id + 1 - data["rowID"] = max_row_id + data["rowId"] = max_row_id processed_data.append(data) return processed_data except Exception as e: @@ -310,7 +310,7 @@ def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload) selected_data_fields_to_enrich = self.get_selected_data_fields(dgID, cookie) if selected_data_fields_to_enrich is not None: print("Selected data fields to enrich retrieved successfully") - max_row_id = max(item["rowID"] for item in structured_data) + max_row_id = max(item["rowId"] for item in structured_data) enriched_data = self.enrich_data(structured_data, selected_data_fields_to_enrich, max_row_id) agregated_dataset = structured_data + enriched_data @@ -367,7 +367,7 @@ def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload) elif updateType == "minor_append_update": print("Handling Minor update") agregated_dataset = self.get_dataset(dgID, cookie) - max_row_id = max(item["rowID"] for item in agregated_dataset) + max_row_id = max(item["rowId"] for item in agregated_dataset) if agregated_dataset is not None: print("Aggregated dataset retrieved successfully") minor_update_dataset = self.get_dataset_by_location(savedFilePath, cookie) @@ -381,7 +381,7 @@ def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload) selected_data_fields_to_enrich = self.get_selected_data_fields(dgID, cookie) if selected_data_fields_to_enrich is not None: print("Selected data fields to enrich for minor update retrieved successfully") - max_row_id = max(item["rowID"] for item in structured_data) + max_row_id = max(item["rowId"] for item in structured_data) enriched_data = self.enrich_data(structured_data, selected_data_fields_to_enrich, max_row_id) if enriched_data is not None: print("Minor update data enrichment successful") @@ -461,8 +461,8 @@ def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload) print(cleaned_patch_payload) chunk_updates = {} for entry in cleaned_patch_payload: - rowID = entry.get("rowId") - chunkNum = (rowID - 1) // 5 + 1 + rowId = entry.get("rowId") + chunkNum = (rowId - 1) // 5 + 1 if chunkNum not in chunk_updates: chunk_updates[chunkNum] = [] chunk_updates[chunkNum].append(entry) @@ -472,9 +472,9 @@ def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload) if chunk_data is not None: print(f"Chunk {chunkNum} downloaded successfully") for entry in entries: - rowID = entry.get("rowId") + rowId = entry.get("rowId") for idx, chunk_entry in enumerate(chunk_data): - if chunk_entry.get("rowID") == rowID: + if chunk_entry.get("rowId") == rowId: chunk_data[idx] = entry break chunk_save_operation = self.save_chunked_data([chunk_data], cookie, dgID, chunkNum-1) @@ -488,10 +488,10 @@ def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload) if agregated_dataset is not None: print("Aggregated dataset for patch update retrieved successfully") for entry in cleaned_patch_payload: - rowID = entry.get("rowId") + rowId = entry.get("rowId") for index, item in enumerate(agregated_dataset): - if item.get("rowID") == rowID: - entry["rowID"] = rowID + if item.get("rowId") == rowId: + entry["rowId"] = rowId del entry["rowId"] agregated_dataset[index] = entry break @@ -524,9 +524,9 @@ def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload) aggregated_dataset = self.get_dataset(dgID, cookie) if aggregated_dataset is not None: print("Aggregated dataset for delete operation retrieved successfully") - updated_dataset = [row for row in aggregated_dataset if row.get('rowID') not in deleted_rows] + updated_dataset = [row for row in aggregated_dataset if row.get('rowId') not in deleted_rows] for idx, row in enumerate(updated_dataset, start=1): - row['rowID'] = idx + row['rowId'] = idx if updated_dataset is not None: print("Deleted rows removed and dataset updated successfully") chunked_data = self.chunk_data(updated_dataset) diff --git a/file-handler/file_handler_api.py b/file-handler/file_handler_api.py index 98abe065..b443c418 100644 --- a/file-handler/file_handler_api.py +++ b/file-handler/file_handler_api.py @@ -98,7 +98,7 @@ async def upload_and_copy(request: Request, dgId: int = Form(...), dataFile: Upl raise HTTPException(status_code=500, detail=upload_failed) for idx, record in enumerate(converted_data, start=1): - record["rowID"] = idx + record["rowId"] = idx json_local_file_path = file_location.replace(YAML_EXT, JSON_EXT).replace(YML_EXT, JSON_EXT).replace(XLSX_EXT, JSON_EXT) with open(json_local_file_path, 'w') as json_file: @@ -252,7 +252,7 @@ async def download_and_convert(request: Request, dgId: int, pageId: int, backgro json_data = json.load(json_file) # for index, item in enumerate(json_data, start=1): - # item['rowID'] = index + # item['rowId'] = index backgroundTasks.add_task(os.remove, json_file_path) From 984a637f39cff95c47cb036251621209ff3a77f7 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Wed, 31 Jul 2024 12:51:58 +0530 Subject: [PATCH 323/582] dataset groups edit flow --- .../pages/DatasetGroups/ViewDatasetGroup.tsx | 64 +++++++++---------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx b/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx index ac9a1f7b..7b6386f6 100644 --- a/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx +++ b/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx @@ -149,17 +149,17 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { const deleteRow = (dataRow) => { setDeletedDataRows((prevDeletedDataRows) => [ ...prevDeletedDataRows, - dataRow?.rowID, + dataRow?.rowId, ]); const payload = updatedDataset?.filter((row) => { - if (row.rowID !== selectedRow?.rowID) return row; + if (row.rowId !== selectedRow?.rowId) return row; }); setUpdatedDataset(payload); const updatedPayload = { dgId, updateDataPayload: { - deletedDataRows: [...deletedDataRows, dataRow?.rowID], + deletedDataRows: [...deletedDataRows, dataRow?.rowId], editedData: payload, }, }; @@ -173,7 +173,7 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { const patchDataUpdate = (dataRow) => { const payload = updatedDataset?.map((row) => - row.rowID === selectedRow?.rowID ? dataRow : row + row.rowId === selectedRow?.rowId ? dataRow : row ); setUpdatedDataset(payload); @@ -425,36 +425,36 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { ), }); + }else if (minorPayload) { + open({ + content: + 'Any changes you made to the individual data items (patch update) will be discarded after changes are applied', + title: 'Confirm minor update', + footer: ( +
    + + +
    + ), + }); + } else if (patchPayload) { + open({ + content: 'Changed data rows will be updated in the dataset', + title: 'Confirm patch update', + footer: ( +
    + + +
    + ), + }); } } - else if (minorPayload) { - open({ - content: - 'Any changes you made to the individual data items (patch update) will be discarded after changes are applied', - title: 'Confirm minor update', - footer: ( -
    - - -
    - ), - }); - } else if (patchPayload) { - open({ - content: 'Changed data rows will be updated in the dataset', - title: 'Confirm patch update', - footer: ( -
    - - -
    - ), - }); - } + }; const majorUpdateDatasetGroupMutation = useMutation({ From 88547d157f51fa4923fd80b5b7dd2308e0bf4ea8 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 31 Jul 2024 12:58:40 +0530 Subject: [PATCH 324/582] s3ferry port update --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 59a3cf39..161bcd7e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -207,7 +207,7 @@ services: - S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID} - S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY} ports: - - "3002:3000" + - "3006:3000" depends_on: - file-handler - init From c0781378b9d20c1a3023dcee10b17a421310f8a3 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 31 Jul 2024 14:23:45 +0530 Subject: [PATCH 325/582] minor page update count bug fix --- dataset-processor/dataset_processor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dataset-processor/dataset_processor.py b/dataset-processor/dataset_processor.py index c612f983..499c546b 100644 --- a/dataset-processor/dataset_processor.py +++ b/dataset-processor/dataset_processor.py @@ -406,7 +406,7 @@ def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload) agregated_dataset_operation = self.save_aggregrated_data(dgID, cookie, agregated_dataset) if agregated_dataset_operation: print("Aggregated dataset for minor update saved successfully") - return_data = self.update_preprocess_status(dgID, cookie, True, False, f"/dataset/{dgID}/chunks/", "", True, len(cleaned_data), len(chunked_data)) + return_data = self.update_preprocess_status(dgID, cookie, True, False, f"/dataset/{dgID}/chunks/", "", True, len(cleaned_data), (len(chunked_data)+page_count)) print(return_data) return SUCCESSFUL_OPERATION else: From d4f6b5d48c2b60d9c72db4566c7d16120733492c Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Wed, 31 Jul 2024 21:05:12 +0530 Subject: [PATCH 326/582] ESCLASS-159- create model API implementation and get model overview and filters --- .../classifier-script-v9-models-metadata.sql | 35 ++++++ DSL/Resql/get-data-model-filters.sql | 23 ++++ .../get-paginated-data-model-metadata.sql | 34 ++++++ DSL/Resql/insert-model-metadata.sql | 27 +++++ .../DSL/GET/classifier/datamodel/overview.yml | 104 ++++++++++++++++ .../classifier/datamodel/overview/filters.yml | 36 ++++++ .../DSL/POST/classifier/datamodel/create.yml | 111 ++++++++++++++++++ .../classifier/datasetgroup/update/status.yml | 2 +- 8 files changed, 371 insertions(+), 1 deletion(-) create mode 100644 DSL/Liquibase/changelog/classifier-script-v9-models-metadata.sql create mode 100644 DSL/Resql/get-data-model-filters.sql create mode 100644 DSL/Resql/get-paginated-data-model-metadata.sql create mode 100644 DSL/Resql/insert-model-metadata.sql create mode 100644 DSL/Ruuter.private/DSL/GET/classifier/datamodel/overview.yml create mode 100644 DSL/Ruuter.private/DSL/GET/classifier/datamodel/overview/filters.yml create mode 100644 DSL/Ruuter.private/DSL/POST/classifier/datamodel/create.yml diff --git a/DSL/Liquibase/changelog/classifier-script-v9-models-metadata.sql b/DSL/Liquibase/changelog/classifier-script-v9-models-metadata.sql new file mode 100644 index 00000000..a56747df --- /dev/null +++ b/DSL/Liquibase/changelog/classifier-script-v9-models-metadata.sql @@ -0,0 +1,35 @@ +-- liquibase formatted sql + +-- changeset kalsara Magamage:classifier-script-v9-changeset1 +CREATE TYPE Maturity_Label AS ENUM ('development', 'staging', 'production-ready'); + +-- changeset kalsara Magamage:classifier-script-v9-changeset2 +CREATE TYPE Deployment_Env AS ENUM ('outlook', 'testing', 'inival', 'undeployed'); + +-- changeset kalsara Magamage:classifier-script-v9-changeset3 +CREATE TYPE Training_Status AS ENUM ('not trained', 'training in progress', 'trained', 'retraining needed', 'untrainable'); + +-- changeset kalsara Magamage:classifier-script-v9-changeset4 +CREATE TYPE Base_Models AS ENUM ('xlnet', 'bert', 'roberta'); + +-- changeset kalsara Magamage:classifier-script-v9-changeset5 +CREATE TABLE models_metadata ( + id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY, + model_group_key TEXT NOT NULL, + model_name TEXT NOT NULL, + major_version INT NOT NULL DEFAULT 0, + minor_version INT NOT NULL DEFAULT 0, + latest BOOLEAN DEFAULT false, + maturity_label Maturity_Label, + deployment_env Deployment_Env, + training_status Training_Status, + base_models Base_Models[], + last_trained_timestamp TIMESTAMP WITH TIME ZONE, + created_timestamp TIMESTAMP WITH TIME ZONE, + connected_dg_id INT, + connected_dg_name TEXT, + model_s3_location TEXT, + inference_routes JSONB, + training_results JSONB, + CONSTRAINT models_metadata_pkey PRIMARY KEY (id) +); \ No newline at end of file diff --git a/DSL/Resql/get-data-model-filters.sql b/DSL/Resql/get-data-model-filters.sql new file mode 100644 index 00000000..2d663837 --- /dev/null +++ b/DSL/Resql/get-data-model-filters.sql @@ -0,0 +1,23 @@ +SELECT json_build_object( + 'modelNames', modelNames, + 'modelVersions', modelVersions, + 'datasetGroups', datasetGroups, + 'deploymentsEnvs', deploymentsEnvs, + 'trainingStatuses', trainingStatuses, + 'maturityLabels', maturityLabels +) +FROM ( + SELECT + array_agg(DISTINCT model_name) AS modelNames, + array_agg(DISTINCT + major_version::TEXT || '.x' + ) FILTER (WHERE major_version > 0) || + array_agg(DISTINCT + 'x.' || minor_version::TEXT + ) FILTER (WHERE minor_version > 0) AS modelVersions, + array_agg(DISTINCT connected_dg_name) AS datasetGroups, + array_agg(DISTINCT deployment_env) AS deploymentsEnvs, + array_agg(DISTINCT training_status) AS trainingStatuses, + array_agg(DISTINCT maturity_label) AS maturityLabels + FROM models_metadata +) AS subquery; \ No newline at end of file diff --git a/DSL/Resql/get-paginated-data-model-metadata.sql b/DSL/Resql/get-paginated-data-model-metadata.sql new file mode 100644 index 00000000..7083ccc0 --- /dev/null +++ b/DSL/Resql/get-paginated-data-model-metadata.sql @@ -0,0 +1,34 @@ +SELECT + dt.id, + dt.model_group_key, + dt.model_name, + dt.major_version, + dt.minor_version, + dt.latest, + dt.maturity_label, + dt.deployment_env, + dt.training_status, + dt.base_models, + dt.last_trained_timestamp, + dt.created_timestamp, + dt.connected_dg_id, + dt.connected_dg_name, + dt.model_s3_location, + dt.inference_routes, + dt.training_results, + CEIL(COUNT(*) OVER() / :page_size::DECIMAL) AS total_pages +FROM + models_metadata dt +WHERE + (:major_version = -1 OR dt.major_version = :major_version) + AND (:minor_version = -1 OR dt.minor_version = :minor_version) + AND (:model_name = 'all' OR dt.model_name = :model_name) + AND (:deployment_maturity = 'all' OR dt.maturity_label = :deployment_maturity::Maturity_Label) + AND (:training_status = 'all' OR dt.training_status = :training_status::Training_Status) + AND (:platform = 'all' OR dt.deployment_env = :platform::Deployment_Env) + AND (:dataset_group = 'all' OR dt.connected_dg_name = :dataset_group) +ORDER BY + CASE WHEN :sort_type = 'asc' THEN dt.model_name END ASC, + CASE WHEN :sort_type = 'desc' THEN dt.model_name END DESC +OFFSET ((GREATEST(:page, 1) - 1) * :page_size) LIMIT :page_size; + diff --git a/DSL/Resql/insert-model-metadata.sql b/DSL/Resql/insert-model-metadata.sql new file mode 100644 index 00000000..93e66bb6 --- /dev/null +++ b/DSL/Resql/insert-model-metadata.sql @@ -0,0 +1,27 @@ +INSERT INTO models_metadata ( + model_group_key, + model_name, + major_version, + minor_version, + latest, + maturity_label, + deployment_env, + training_status, + base_models, + created_timestamp, + connected_dg_id, + connected_dg_name +) VALUES ( + :model_group_key, + :model_name, + :major_version, + :minor_version, + :latest, + :maturity_label::Maturity_Label, + :deployment_env::Deployment_Env, + :training_status::Training_Status, + ARRAY [:base_models]::Base_Models[], + :created_timestamp::timestamp with time zone, + :connected_dg_id, + :connected_dg_name +) RETURNING id; diff --git a/DSL/Ruuter.private/DSL/GET/classifier/datamodel/overview.yml b/DSL/Ruuter.private/DSL/GET/classifier/datamodel/overview.yml new file mode 100644 index 00000000..d529ec7b --- /dev/null +++ b/DSL/Ruuter.private/DSL/GET/classifier/datamodel/overview.yml @@ -0,0 +1,104 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'OVERVIEW'" + method: get + accepts: json + returns: json + namespace: classifier + allowlist: + params: + - field: page + type: number + description: "Parameter 'page'" + - field: pageSize + type: number + description: "Parameter 'pageSize'" + - field: sortType + type: string + description: "Parameter 'sortType'" + - field: modelName + type: string + description: "Parameter 'modelName'" + - field: majorVersion + type: string + description: "Parameter 'majorVersion'" + - field: minorVersion + type: string + description: "Parameter 'minorVersion'" + - field: platform + type: string + description: "Parameter 'platform'" + - field: datasetGroup + type: string + description: "Parameter 'datasetGroup'" + - field: trainingStatus + type: string + description: "Parameter 'trainingStatus'" + - field: deploymentMaturity + type: string + description: "Parameter 'deploymentMaturity'" + +extract_data: + assign: + page: ${Number(incoming.params.page)} + page_size: ${Number(incoming.params.pageSize)} + sort_type: ${incoming.params.sortType} + model_name: ${incoming.params.modelName} + major_version: ${Number(incoming.params.majorVersion)} + minor_version: ${Number(incoming.params.minorVersion)} + platform: ${incoming.params.platform} + dataset_group: ${incoming.params.datasetGroup} + training_status: ${incoming.params.trainingStatus} + deployment_maturity: ${incoming.params.deploymentMaturity} + next: get_dataset_meta_data_overview + +get_data_model_meta_data_overview: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-paginated-data-model-metadata" + body: + page: ${page} + page_size: ${page_size} + sorting: ${sort_type} + model_name: ${model_name} + major_version: ${major_version} + minor_version: ${minor_version} + platform: ${platform} + dataset_group: ${dataset_group} + training_status: ${training_status} + deployment_maturity: ${deployment_maturity} + result: res_model + next: check_status + +check_status: + switch: + - condition: ${200 <= res_model.response.statusCodeValue && res_model.response.statusCodeValue < 300} + next: assign_success_response + next: assign_fail_response + +assign_success_response: + assign: + format_res: { + operationSuccessful: true, + data: '${res_model.response.body}' + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + operationSuccessful: false, + data: '${[]}' + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/GET/classifier/datamodel/overview/filters.yml b/DSL/Ruuter.private/DSL/GET/classifier/datamodel/overview/filters.yml new file mode 100644 index 00000000..278e6bf2 --- /dev/null +++ b/DSL/Ruuter.private/DSL/GET/classifier/datamodel/overview/filters.yml @@ -0,0 +1,36 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'FILTERS'" + method: get + accepts: json + returns: json + namespace: classifier + +get_data_model_filters: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-data-model-filters" + result: res_filters + next: check_status + +check_status: + switch: + - condition: ${200 <= res_filters.response.statusCodeValue && res_filters.response.statusCodeValue < 300} + next: assign_Json_format + next: return_bad_request + +assign_Json_format: + assign: + data: ${JSON.parse(res_filters.response.body[0].jsonBuildObject.value)} + next: return_ok + +return_ok: + status: 200 + return: ${data} + next: end + +return_bad_request: + status: 400 + return: "Bad Request" + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/create.yml b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/create.yml new file mode 100644 index 00000000..4df5e8fa --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/create.yml @@ -0,0 +1,111 @@ +declaration: + call: declare + version: 0.1 + description: "Insert model metadata" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: modelName + type: string + description: "Body field 'modelName'" + - field: datasetGroupName + type: string + description: "Body field 'datasetGroupName'" + - field: dgId + type: number + description: "Body field 'dgId'" + - field: baseModels + type: array + description: "Body field 'baseModels'" + - field: deploymentPlatform + type: string + description: "Body field 'deploymentPlatform'" + - field: maturityLabel + type: string + description: "Body field 'maturityLabel'" + headers: + - field: cookie + type: string + description: "Cookie field" + +extract_request_data: + assign: + model_name: ${incoming.body.modelName} + dataset_group_name: ${incoming.body.datasetGroupName} + dg_id: ${incoming.body.dgId} + base_models: ${incoming.body.baseModels} + deployment_platform: ${incoming.body.deploymentPlatform} + maturity_label: ${incoming.body.maturityLabel} + next: check_for_request_data + +check_for_request_data: + switch: + - condition: ${model_name !== null && dataset_group_name !== null && dg_id !== null && base_models !== null && deployment_platform !== null && maturity_label !== null} + next: get_epoch_date + next: return_incorrect_request + +get_epoch_date: + assign: + current_epoch: ${Date.now()} + random_num: ${Math.floor(Math.random() * 100000)} + next: create_model_metadata + +create_model_metadata: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/insert-model-metadata" + body: + model_name: ${model_name} + model_group_key: "${random_num+ '_'+current_epoch}" + connected_dg_name: ${dataset_group_name} + connected_dg_id: ${dg_id} + base_models: ${base_models} + deployment_env: ${deployment_platform} + maturity_label: ${maturity_label} + training_status: not trained + major_version: 1 + minor_version: 0 + latest: true + created_timestamp: ${new Date(current_epoch).toISOString()} + result: res_model + next: check_status + +check_status: + switch: + - condition: ${200 <= res_model.response.statusCodeValue && res_model.response.statusCodeValue < 300} + next: assign_success_response + next: assign_fail_response + +assign_success_response: + assign: + format_res: { + modelId: '${res_model.response.body[0].id}', + operationSuccessful: true + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + modelId: '', + operationSuccessful: false + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end + +return_incorrect_request: + status: 400 + return: 'Missing Required Fields' + next: end diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/status.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/status.yml index 5c9a95ed..f487e49d 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/status.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/status.yml @@ -9,7 +9,7 @@ declaration: allowlist: body: - field: dgId - type: string + type: number description: "Body field 'dgId'" - field: operationType type: string From 8d5e233feffc74aed593a21e31e7c1c82cfe19bb Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Wed, 31 Jul 2024 22:34:54 +0530 Subject: [PATCH 327/582] datasetgroup-update-refactor- minor major patch version issue and update revamp --- DSL/Resql/get-dataset-group-key-by-id.sql | 2 +- DSL/Resql/snapshot-dataset-group.sql | 17 ------- DSL/Resql/snapshot-major-dataset-group.sql | 38 +++++++++++++++ DSL/Resql/snapshot-minor-dataset-group.sql | 24 ++++++++++ .../update-patch-version-dataset-group.sql | 2 +- .../classifier/datasetgroup/update/major.yml | 39 ++++++++-------- .../classifier/datasetgroup/update/minor.yml | 46 +++++++++---------- .../classifier/datasetgroup/update/patch.yml | 4 ++ 8 files changed, 109 insertions(+), 63 deletions(-) delete mode 100644 DSL/Resql/snapshot-dataset-group.sql create mode 100644 DSL/Resql/snapshot-major-dataset-group.sql create mode 100644 DSL/Resql/snapshot-minor-dataset-group.sql diff --git a/DSL/Resql/get-dataset-group-key-by-id.sql b/DSL/Resql/get-dataset-group-key-by-id.sql index d6d7ac48..3b5861dd 100644 --- a/DSL/Resql/get-dataset-group-key-by-id.sql +++ b/DSL/Resql/get-dataset-group-key-by-id.sql @@ -1,2 +1,2 @@ -SELECT group_key +SELECT group_key, major_version, minor_version FROM dataset_group_metadata WHERE id =:id; \ No newline at end of file diff --git a/DSL/Resql/snapshot-dataset-group.sql b/DSL/Resql/snapshot-dataset-group.sql deleted file mode 100644 index cc6e0b62..00000000 --- a/DSL/Resql/snapshot-dataset-group.sql +++ /dev/null @@ -1,17 +0,0 @@ -INSERT INTO dataset_group_metadata ( - group_name, group_key, major_version, minor_version, patch_version, latest, - is_enabled, enable_allowed, last_model_trained, created_timestamp, - last_updated_timestamp, last_trained_timestamp, validation_status, - validation_errors, processed_data_available, raw_data_available, - num_samples, num_pages, raw_data_location, preprocess_data_location, - validation_criteria, class_hierarchy, connected_models -) -SELECT - group_name, group_key, major_version, minor_version, patch_version, false AS latest, - is_enabled, enable_allowed, last_model_trained, created_timestamp, - last_updated_timestamp, last_trained_timestamp, validation_status, - validation_errors, processed_data_available, raw_data_available, - num_samples, num_pages, raw_data_location, preprocess_data_location, - validation_criteria, class_hierarchy, connected_models -FROM dataset_group_metadata -WHERE id = :id; \ No newline at end of file diff --git a/DSL/Resql/snapshot-major-dataset-group.sql b/DSL/Resql/snapshot-major-dataset-group.sql new file mode 100644 index 00000000..6fd9c837 --- /dev/null +++ b/DSL/Resql/snapshot-major-dataset-group.sql @@ -0,0 +1,38 @@ +INSERT INTO dataset_group_metadata ( + group_name, group_key, major_version, minor_version, patch_version, latest, + is_enabled, enable_allowed, last_model_trained, created_timestamp, + last_updated_timestamp, last_trained_timestamp, validation_status, + validation_errors, processed_data_available, raw_data_available, + num_samples, num_pages, raw_data_location, preprocess_data_location, + validation_criteria, class_hierarchy, connected_models +) +SELECT + group_name, group_key, + ( + SELECT COALESCE(MAX(major_version), 0) + 1 + FROM dataset_group_metadata + WHERE group_key = :group_key + ) AS major_version, + 0 AS minor_version, + 0 AS patch_version, + true AS latest, + false AS is_enabled, + false AS enable_allowed, + NULL AS last_model_trained, + created_timestamp, + CURRENT_TIMESTAMP AS last_updated_timestamp, + NULL AS last_trained_timestamp, + 'unvalidated'::Validation_Status AS validation_status, + NULL::JSONB AS validation_errors, + false AS processed_data_available, + false AS raw_data_available, + 0 AS num_samples, + 0 AS num_pages, + NULL AS raw_data_location, + NULL AS preprocess_data_location, + :validation_criteria::JSONB AS validation_criteria, + :class_hierarchy::JSONB AS class_hierarchy, + NULL::JSONB AS connected_models +FROM dataset_group_metadata +WHERE id = :id +RETURNING id; \ No newline at end of file diff --git a/DSL/Resql/snapshot-minor-dataset-group.sql b/DSL/Resql/snapshot-minor-dataset-group.sql new file mode 100644 index 00000000..fd72ba0a --- /dev/null +++ b/DSL/Resql/snapshot-minor-dataset-group.sql @@ -0,0 +1,24 @@ +INSERT INTO dataset_group_metadata ( + group_name, group_key, major_version, minor_version, patch_version, latest, + is_enabled, enable_allowed, last_model_trained, created_timestamp, + last_updated_timestamp, last_trained_timestamp, validation_status, + validation_errors, processed_data_available, raw_data_available, + num_samples, num_pages, raw_data_location, preprocess_data_location, + validation_criteria, class_hierarchy, connected_models +) +SELECT + group_name, group_key, major_version, + ( + SELECT COALESCE(MAX(minor_version), 0) + 1 + FROM dataset_group_metadata + WHERE group_key = :group_key AND major_version = :major_version + ) AS minor_version, + 0 AS patch_version, true AS latest, + false AS is_enabled, false AS enable_allowed, last_model_trained, created_timestamp, + CURRENT_TIMESTAMP AS last_updated_timestamp, last_trained_timestamp, + 'in-progress'::Validation_Status AS validation_status, validation_errors, processed_data_available, + raw_data_available, num_samples, num_pages, raw_data_location, preprocess_data_location, + validation_criteria, class_hierarchy, connected_models +FROM dataset_group_metadata +WHERE id = :id +RETURNING id; diff --git a/DSL/Resql/update-patch-version-dataset-group.sql b/DSL/Resql/update-patch-version-dataset-group.sql index 5f1bee12..2c589db8 100644 --- a/DSL/Resql/update-patch-version-dataset-group.sql +++ b/DSL/Resql/update-patch-version-dataset-group.sql @@ -10,7 +10,7 @@ update_specific AS ( patch_version = ( SELECT COALESCE(MAX(patch_version), 0) + 1 FROM dataset_group_metadata - WHERE group_key = :group_key + WHERE group_key = :group_key AND major_version = :major_version AND minor_version = :minor_version ), enable_allowed = false, validation_status = 'in-progress'::Validation_Status, diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/major.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/major.yml index cfaa0497..c36d17da 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/major.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/major.yml @@ -31,21 +31,6 @@ check_for_request_data: next: snapshot_dataset_group next: return_incorrect_request -snapshot_dataset_group: - call: http.post - args: - url: "[#CLASSIFIER_RESQL]/snapshot-dataset-group" - body: - id: ${dg_id} - result: res - next: check_snapshot_status - -check_snapshot_status: - switch: - - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} - next: get_dataset_group - next: assign_fail_response - get_dataset_group: call: http.post args: @@ -72,29 +57,40 @@ assign_group_key: group_key: ${res.response.body[0].groupKey} next: update_old_dataset_group -update_old_dataset_group: +snapshot_dataset_group: call: http.post args: - url: "[#CLASSIFIER_RESQL]/update-major-version-dataset-group" + url: "[#CLASSIFIER_RESQL]/snapshot-major-dataset-group" body: id: ${dg_id} group_key: ${group_key} - last_updated_timestamp: ${new Date().toISOString()} validation_criteria: ${JSON.stringify(validation_criteria)} class_hierarchy: ${JSON.stringify(class_hierarchy)} result: res - next: check_old_dataset_status + next: check_snapshot_status -check_old_dataset_status: +check_snapshot_status: switch: - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} - next: assign_success_response + next: check_updated_data_exist next: assign_fail_response +check_updated_data_exist: + switch: + - condition: ${res.response.body.length>0} + next: assign_new_dg_id + next: return_not_found + +assign_new_dg_id: + assign: + new_dg_id: ${res.response.body[0].id} + next: assign_success_response + assign_success_response: assign: format_res: { dgId: '${dg_id}', + newDgId: '${new_dg_id}', operationSuccessful: true, } next: return_ok @@ -103,6 +99,7 @@ assign_fail_response: assign: format_res: { dgId: '${dg_id}', + newDgId: '', operationSuccessful: false, } next: return_bad_request diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/minor.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/minor.yml index 9322f45d..62a4da11 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/minor.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/minor.yml @@ -23,22 +23,7 @@ extract_request_data: assign: dg_id: ${incoming.body.dgId} s3_file_path: ${incoming.body.s3FilePath} - next: snapshot_dataset_group - -snapshot_dataset_group: - call: http.post - args: - url: "[#CLASSIFIER_RESQL]/snapshot-dataset-group" - body: - id: ${dg_id} - result: res - next: check_snapshot_status - -check_snapshot_status: - switch: - - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} - next: get_dataset_group - next: assign_fail_response + next: get_dataset_group get_dataset_group: call: http.post @@ -64,25 +49,37 @@ check_data_exist: assign_group_key: assign: group_key: ${res.response.body[0].groupKey} - next: update_old_dataset_group + major_version: ${res.response.body[0].majorVersion} + next: snapshot_dataset_group -update_old_dataset_group: +snapshot_dataset_group: call: http.post args: - url: "[#CLASSIFIER_RESQL]/update-minor-version-dataset-group" + url: "[#CLASSIFIER_RESQL]/snapshot-minor-dataset-group" body: id: ${dg_id} group_key: ${group_key} - last_updated_timestamp: ${new Date().toISOString()} + major_version: ${major_version} result: res - next: check_old_dataset_status + next: check_snapshot_status -check_old_dataset_status: +check_snapshot_status: switch: - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} - next: execute_cron_manager + next: check_updated_data_exist next: assign_fail_response +check_updated_data_exist: + switch: + - condition: ${res.response.body.length>0} + next: assign_new_dg_id + next: return_not_found + +assign_new_dg_id: + assign: + new_dg_id: ${res.response.body[0].id} + next: execute_cron_manager + execute_cron_manager: call: http.post args: @@ -90,6 +87,7 @@ execute_cron_manager: query: cookie: ${incoming.headers.cookie} dgId: ${dg_id} + newDgId: ${new_dg_id} updateType: 'minor' savedFilePath: ${s3_file_path} patchPayload: ${[]} @@ -100,6 +98,7 @@ assign_success_response: assign: format_res: { dgId: '${dg_id}', + newDgId: '${new_dg_id}', validationStatus: 'in-progress', operationSuccessful: true, } @@ -109,6 +108,7 @@ assign_fail_response: assign: format_res: { dgId: '${dg_id}', + newDgId: '', validationStatus: '', operationSuccessful: false, } diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/patch.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/patch.yml index 49867a3d..319caa2f 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/patch.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/patch.yml @@ -50,6 +50,8 @@ check_data_exist: assign_group_key: assign: group_key: ${res.response.body[0].groupKey} + major_version: ${res.response.body[0].majorVersion} + minor_version: ${res.response.body[0].minorVersion} next: update_old_dataset_group update_old_dataset_group: @@ -59,6 +61,8 @@ update_old_dataset_group: body: id: ${dg_id} group_key: ${group_key} + major_version: ${major_version} + minor_version: ${minor_version} last_updated_timestamp: ${new Date().toISOString()} result: res next: check_old_dataset_status From 0f46b425005c110b52f697a02d1bf8323b8a1eb4 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Wed, 31 Jul 2024 23:08:09 +0530 Subject: [PATCH 328/582] datasetgroup-update-refactor- versioning bug fixed --- .../DSL/POST/classifier/datasetgroup/update/major.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/major.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/major.yml index c36d17da..5a455623 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/major.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/major.yml @@ -28,7 +28,7 @@ extract_request_data: check_for_request_data: switch: - condition: ${validation_criteria !== null && class_hierarchy !=null} - next: snapshot_dataset_group + next: get_dataset_group next: return_incorrect_request get_dataset_group: @@ -55,7 +55,7 @@ check_data_exist: assign_group_key: assign: group_key: ${res.response.body[0].groupKey} - next: update_old_dataset_group + next: snapshot_dataset_group snapshot_dataset_group: call: http.post From a6012ed5b00c10a88a80e50154fa1556a8a0e37c Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Thu, 1 Aug 2024 00:00:28 +0530 Subject: [PATCH 329/582] dataset groups edit fixes --- .../FormElements/DynamicForm/index.tsx | 45 +++++----- .../pages/DatasetGroups/ViewDatasetGroup.tsx | 83 +++++++++++++------ GUI/src/services/datasets.ts | 5 ++ GUI/src/utils/datasetGroupsUtils.ts | 35 ++++---- docker-compose.yml | 4 +- 5 files changed, 108 insertions(+), 64 deletions(-) diff --git a/GUI/src/components/FormElements/DynamicForm/index.tsx b/GUI/src/components/FormElements/DynamicForm/index.tsx index 90f938b8..2ce032b9 100644 --- a/GUI/src/components/FormElements/DynamicForm/index.tsx +++ b/GUI/src/components/FormElements/DynamicForm/index.tsx @@ -5,28 +5,32 @@ import Button from 'components/Button'; import Track from 'components/Track'; type DynamicFormProps = { - formData: { [key: string]: string }; + formData: { [key: string]: string | number }; // Adjust the type to include numbers onSubmit: (data: any) => void; - setPatchUpdateModalOpen: React.Dispatch> + setPatchUpdateModalOpen: React.Dispatch>; }; -const DynamicForm: React.FC = ({ formData, onSubmit,setPatchUpdateModalOpen }) => { +const DynamicForm: React.FC = ({ + formData, + onSubmit, + setPatchUpdateModalOpen, +}) => { const { register, handleSubmit } = useForm(); - const renderInput = (key: string, type: string) => { - + const renderInput = (key: string) => { + const isRowID = key.toLowerCase() === 'rowid'; + const inputType = isRowID ? 'number' : 'text'; return ( -
    - - +
    + +
    ); }; @@ -35,14 +39,17 @@ const DynamicForm: React.FC = ({ formData, onSubmit,setPatchUp {Object.keys(formData).map((key) => (
    -
    - {renderInput(key, formData[key])} -
    +
    {renderInput(key)}
    ))}
    - +
    diff --git a/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx b/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx index 7b6386f6..5d62290e 100644 --- a/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx +++ b/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx @@ -15,8 +15,6 @@ import { DataTable, Dialog, FormRadios, - FormSelect, - FormTextarea, Icon, Label, Switch, @@ -24,13 +22,12 @@ import { import ClassHierarchy from 'components/molecules/ClassHeirarchy'; import { createColumnHelper, PaginationState } from '@tanstack/react-table'; // Adjust based on your table library import { - Dataset, DatasetGroup, ImportDataset, ValidationRule, } from 'types/datasetGroups'; import BackArrowButton from 'assets/BackArrowButton'; -import { Link, useNavigate, useSearchParams } from 'react-router-dom'; +import { Link, useNavigate } from 'react-router-dom'; import ValidationCriteriaRowsView from 'components/molecules/ValidationCriteria/RowsView'; import { MdOutlineDeleteOutline, MdOutlineEdit } from 'react-icons/md'; import { @@ -44,7 +41,6 @@ import { } from 'services/datasets'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useDialog } from 'hooks/useDialog'; -import { useForm } from 'react-hook-form'; import { handleDownload, isMajorUpdate, @@ -85,10 +81,9 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { const [fetchEnabled, setFetchEnabled] = useState(true); const [file, setFile] = useState(''); const [selectedRow, setSelectedRow] = useState({}); - const [isDataChanged, setIsDataChanged] = useState(false); const [bannerMessage, setBannerMessage] = useState(''); - const [minorPayload, setMinorPayload] = useState(""); - const [patchPayload, setPatchPayload] = useState(""); + const [minorPayload, setMinorPayload] = useState(''); + const [patchPayload, setPatchPayload] = useState(''); const [deletedDataRows, setDeletedDataRows] = useState([]); const [updatePriority, setUpdatePriority] = useState(''); @@ -98,6 +93,22 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { setFetchEnabled(false); }, []); + useEffect(() => { + if (updatePriority === 'MAJOR') + setBannerMessage( + 'You have updated key configurations of the dataset schema which are not saved, please save to apply changes. Any files imported or edits made to the existing data will be discarded after changes are applied' + ); + else if (updatePriority === 'MINOR') + setBannerMessage( + 'You have imported new data into the dataset, please save the changes to apply. Any changes you made to the individual data items will be discarded after changes are applied' + ); + else if (updatePriority === 'PATCH') + setBannerMessage( + 'You have edited individual items in the dataset which are not saved. Please save the changes to apply' + ); + else setBannerMessage(''); + }, [updatePriority]); + const { data: datasets, isLoading } = useQuery( ['datasets/groups/data', pagination, dgId], () => getDatasets(pagination, dgId), @@ -146,6 +157,28 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { ); }, [metadata]); + useEffect(() => { + if ( + metadata && + isMajorUpdate( + { + validationRules: + metadata?.response?.data?.[0]?.validationCriteria?.validationRules, + classHierarchy: metadata?.response?.data?.[0]?.classHierarchy, + }, + { + validationRules: + transformValidationRules(validationRules)?.validationRules, + ...transformClassHierarchy(nodes), + } + ) + ) { + setUpdatePriority('MAJOR'); + } else { + setUpdatePriority(''); + } + }, [validationRules, nodes]); + const deleteRow = (dataRow) => { setDeletedDataRows((prevDeletedDataRows) => [ ...prevDeletedDataRows, @@ -165,10 +198,8 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { }; setPatchPayload(updatedPayload); setDeleteRowModalOpen(false); - // setIsDataChanged(true); - setBannerMessage( - 'You have edited individual items in the dataset which are not saved. Please save the changes to apply' - ); + if (updatePriority !== 'MAJOR' && updatePriority !== 'MINOR') + setUpdatePriority('PATCH'); }; const patchDataUpdate = (dataRow) => { @@ -187,9 +218,8 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { setPatchPayload(updatedPayload); setPatchUpdateModalOpen(false); // setIsDataChanged(true); - setBannerMessage( - 'You have edited individual items in the dataset which are not saved. Please save the changes to apply' - ); + if (updatePriority !== 'MAJOR' && updatePriority !== 'MINOR') + setUpdatePriority('PATCH'); // patchUpdateMutation.mutate(updatedPayload); }; @@ -198,6 +228,8 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { mutationFn: (data) => patchUpdate(data), onSuccess: async () => { await queryClient.invalidateQueries(['datasets/groups/data']); + close(); + setView('list'); }, onError: () => { setPatchUpdateModalOpen(false); @@ -314,9 +346,7 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { dgId, s3FilePath: response?.saved_file_path, }); - setBannerMessage( - 'You have imported new data into the dataset, please save the changes to apply. Any changes you made to the individual data items will be discarded after changes are applied ' - ); + if (updatePriority !== 'MAJOR') setUpdatePriority('MINOR'); // const payload = { // dgId, // s3FilePath: response?.saved_file_path, @@ -388,10 +418,9 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { ...transformClassHierarchy(nodes), }; majorUpdateDatasetGroupMutation.mutate(payload); - }; - const datasetGroupUpdate = () => { + const datasetGroupUpdate = () => { setNodesError(validateClassHierarchy(nodes)); setValidationRuleError(validateValidationRules(validationRules)); if ( @@ -404,7 +433,8 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { isMajorUpdate( { validationRules: - metadata?.response?.data?.[0]?.validationCriteria?.validationRules, + metadata?.response?.data?.[0]?.validationCriteria + ?.validationRules, classHierarchy: metadata?.response?.data?.[0]?.classHierarchy, }, { @@ -420,19 +450,23 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { title: 'Confirm major update', footer: (
    - +
    ), }); - }else if (minorPayload) { + } else if (minorPayload) { open({ content: 'Any changes you made to the individual data items (patch update) will be discarded after changes are applied', title: 'Confirm minor update', footer: (
    - + @@ -454,7 +488,6 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { }); } } - }; const majorUpdateDatasetGroupMutation = useMutation({ diff --git a/GUI/src/services/datasets.ts b/GUI/src/services/datasets.ts index b78c403c..218e4e15 100644 --- a/GUI/src/services/datasets.ts +++ b/GUI/src/services/datasets.ts @@ -132,4 +132,9 @@ export async function deleteStopWord(stopWordData) { ...stopWordData }); return data; +} + +export async function getDatasetGroupsProgress() { + const { data } = await apiDev.get('classifier/datasetgroup/progress'); + return data?.response?.data; } \ No newline at end of file diff --git a/GUI/src/utils/datasetGroupsUtils.ts b/GUI/src/utils/datasetGroupsUtils.ts index 2c325fbc..c99ce576 100644 --- a/GUI/src/utils/datasetGroupsUtils.ts +++ b/GUI/src/utils/datasetGroupsUtils.ts @@ -25,13 +25,13 @@ export const transformValidationRules = ( export const transformClassHierarchy = (data: Class[]) => { const transformNode = (node: Class) => { return { - class: node.fieldName, - subclasses: node.children.map(transformNode), + class: node?.fieldName, + subclasses: node?.children?.map(transformNode), }; }; return { - classHierarchy: data.map(transformNode), + classHierarchy: data?.map(transformNode), }; }; @@ -79,7 +79,7 @@ export const validateClassHierarchy = (data: Class[]) => { return false; }; -export const validateValidationRules = (data: ValidationRule[]|undefined) => { +export const validateValidationRules = (data: ValidationRule[] | undefined) => { for (let item of data) { if (item.fieldName === '' || item.dataType === '') { return true; @@ -117,15 +117,16 @@ export const isValidationRulesSatisfied = (data: ValidationRule[]) => { }; export const isFieldNameExisting = (dataArray, fieldNameToCheck) => { - const count = dataArray?.reduce((acc, item) => { - return item?.fieldName?.toLowerCase() === fieldNameToCheck?.toLowerCase() ? acc + 1 : acc; + return item?.fieldName?.toLowerCase() === fieldNameToCheck?.toLowerCase() + ? acc + 1 + : acc; }, 0); return count === 2; }; -export const countFieldNameOccurrences=(dataArray, fieldNameToCheck)=> { +export const countFieldNameOccurrences = (dataArray, fieldNameToCheck) => { let count = 0; function countOccurrences(node) { @@ -134,21 +135,21 @@ export const countFieldNameOccurrences=(dataArray, fieldNameToCheck)=> { } if (node.children) { - node.children.forEach(child => countOccurrences(child)); + node.children.forEach((child) => countOccurrences(child)); } } - dataArray.forEach(node => countOccurrences(node)); + dataArray.forEach((node) => countOccurrences(node)); return count; -} +}; -export const isClassHierarchyDuplicated=(dataArray, fieldNameToCheck)=> { +export const isClassHierarchyDuplicated = (dataArray, fieldNameToCheck) => { const count = countFieldNameOccurrences(dataArray, fieldNameToCheck); return count === 2; -} +}; -export const handleDownload = (response,format) =>{ +export const handleDownload = (response, format) => { try { // Create a URL for the Blob const url = window.URL.createObjectURL(new Blob([response.data])); @@ -161,10 +162,8 @@ export const handleDownload = (response,format) =>{ } catch (error) { console.error('Error downloading the file', error); } -} +}; -export const isMajorUpdate = (initialData,updatedData) =>{ +export const isMajorUpdate = (initialData, updatedData) => { return !isEqual(initialData, updatedData); - -} - +}; diff --git a/docker-compose.yml b/docker-compose.yml index 59a3cf39..69b10c7b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -207,7 +207,7 @@ services: - S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID} - S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY} ports: - - "3002:3000" + - "3006:3000" depends_on: - file-handler - init @@ -296,7 +296,7 @@ services: OPENSEARCH_PASSWORD: admin PORT: 4040 REFRESH_INTERVAL: 1000 - CORS_WHITELIST_ORIGINS: http://localhost:3001,http://localhost:3003,http://localhost:3004,http://localhost:8080,http://localhost:8088 + CORS_WHITELIST_ORIGINS: http://localhost:3001,http://localhost:3002,http://localhost:3003,http://localhost:3004,http://localhost:8080,http://localhost:8088 RUUTER_URL: http://ruuter-public:8086 volumes: - /app/node_modules From 1d21bb4dcbba951fa6130b614dac72f4649b60b3 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Thu, 1 Aug 2024 00:06:36 +0530 Subject: [PATCH 330/582] dataset groups edit fixes --- .../components/molecules/ClassHeirarchy/index.tsx | 13 +++++-------- .../molecules/ValidationCriteria/RowsView.tsx | 3 +-- .../molecules/ValidationSessionCard/index.tsx | 2 +- GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx | 14 ++------------ 4 files changed, 9 insertions(+), 23 deletions(-) diff --git a/GUI/src/components/molecules/ClassHeirarchy/index.tsx b/GUI/src/components/molecules/ClassHeirarchy/index.tsx index 63cc2000..402c33c5 100644 --- a/GUI/src/components/molecules/ClassHeirarchy/index.tsx +++ b/GUI/src/components/molecules/ClassHeirarchy/index.tsx @@ -13,8 +13,6 @@ type ClassHierarchyProps = { setNodes: React.Dispatch>; nodesError?: boolean; setNodesError: React.Dispatch>; - setBannerMessage:React.Dispatch>; - }; const ClassHierarchy: FC> = ({ @@ -22,7 +20,6 @@ const ClassHierarchy: FC> = ({ setNodes, nodesError, setNodesError, - setBannerMessage }) => { const [currentNode, setCurrentNode] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); @@ -33,10 +30,9 @@ const ClassHierarchy: FC> = ({ const handleChange = (e) => { setFieldName(e.target.value); node.fieldName = e.target.value; - if(isClassHierarchyDuplicated(nodes,e.target.value)) - setNodesError(true) - else - setNodesError(false) + if (isClassHierarchyDuplicated(nodes, e.target.value)) + setNodesError(true); + else setNodesError(false); }; return ( @@ -198,7 +194,8 @@ const ClassHierarchy: FC> = ({ } onClose={() => setIsModalOpen(false)} > - Confirm that you are wish to delete the following record. This will delete the current class and all subclasses of it + Confirm that you are wish to delete the following record. This will + delete the current class and all subclasses of it
    ); diff --git a/GUI/src/components/molecules/ValidationCriteria/RowsView.tsx b/GUI/src/components/molecules/ValidationCriteria/RowsView.tsx index 25261c99..5315bac0 100644 --- a/GUI/src/components/molecules/ValidationCriteria/RowsView.tsx +++ b/GUI/src/components/molecules/ValidationCriteria/RowsView.tsx @@ -12,14 +12,13 @@ type ValidationRulesProps = { setValidationRules: React.Dispatch>; validationRuleError?: boolean; setValidationRuleError: React.Dispatch>; - setBannerMessage:React.Dispatch>; }; const ValidationCriteriaRowsView: FC> = ({ validationRules, setValidationRules, setValidationRuleError, validationRuleError, - setBannerMessage + }) => { const setIsDataClass = (id, isDataClass) => { const updatedItems = validationRules.map((item) => diff --git a/GUI/src/components/molecules/ValidationSessionCard/index.tsx b/GUI/src/components/molecules/ValidationSessionCard/index.tsx index fac09a84..f2715d11 100644 --- a/GUI/src/components/molecules/ValidationSessionCard/index.tsx +++ b/GUI/src/components/molecules/ValidationSessionCard/index.tsx @@ -36,7 +36,7 @@ const ValidationSessionCard: React.FC = ({dgName,ver
    ) : (
    -
    Validation In-Progress
    +
    {status}
    > = ({ dgId, setView }) => { }; setPatchPayload(updatedPayload); setPatchUpdateModalOpen(false); - // setIsDataChanged(true); if (updatePriority !== 'MAJOR' && updatePriority !== 'MINOR') setUpdatePriority('PATCH'); - - // patchUpdateMutation.mutate(updatedPayload); }; const patchUpdateMutation = useMutation({ @@ -347,13 +344,8 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { s3FilePath: response?.saved_file_path, }); if (updatePriority !== 'MAJOR') setUpdatePriority('MINOR'); - // const payload = { - // dgId, - // s3FilePath: response?.saved_file_path, - // }; - // setIsDataChanged(true); + setIsImportModalOpen(false); - // minorUpdateMutation.mutate(payload); }, onError: () => { open({ @@ -643,7 +635,6 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { setValidationRules={setValidationRules} validationRuleError={validationRuleError} setValidationRuleError={setValidationRuleError} - setBannerMessage={setBannerMessage} /> @@ -654,7 +645,6 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { setNodes={setNodes} nodesError={nodesError} setNodesError={setNodesError} - setBannerMessage={setBannerMessage} /> )} From a39f7062bcaab41980599a488911798ee6cfc0c5 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Thu, 1 Aug 2024 13:47:06 +0530 Subject: [PATCH 331/582] sse integration --- .../molecules/ValidationSessionCard/index.tsx | 4 +- GUI/src/pages/ValidationSessions/index.tsx | 92 ++++++++++--------- GUI/src/services/sse-service.ts | 2 +- notification-server/.env | 2 +- 4 files changed, 51 insertions(+), 49 deletions(-) diff --git a/GUI/src/components/molecules/ValidationSessionCard/index.tsx b/GUI/src/components/molecules/ValidationSessionCard/index.tsx index f2715d11..4eb3dab0 100644 --- a/GUI/src/components/molecules/ValidationSessionCard/index.tsx +++ b/GUI/src/components/molecules/ValidationSessionCard/index.tsx @@ -23,14 +23,14 @@ const ValidationSessionCard: React.FC = ({dgName,ver {isLatest &&( )} - {status==="failed" &&( + {status==="Fail" &&( )}
    } >
    - {status==="failed" ? ( + {errorMessage? (
    {errorMessage}
    diff --git a/GUI/src/pages/ValidationSessions/index.tsx b/GUI/src/pages/ValidationSessions/index.tsx index c5ce3b63..8e05b271 100644 --- a/GUI/src/pages/ValidationSessions/index.tsx +++ b/GUI/src/pages/ValidationSessions/index.tsx @@ -1,48 +1,50 @@ -import { FC, useState } from 'react'; +import { FC, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import ProgressBar from 'components/ProgressBar'; -import { Card, Label } from 'components'; import ValidationSessionCard from 'components/molecules/ValidationSessionCard'; +import sse from 'services/sse-service'; +import { useQuery } from '@tanstack/react-query'; +import { getDatasetGroupsProgress } from 'services/datasets'; const ValidationSessions: FC = () => { const { t } = useTranslation(); - const [progress, setProgress] = useState(40); + const [progresses, setProgresses] = useState([]); - const data = [ + const { data: progressData } = useQuery( + ['datasetgroups/progress'], + () => getDatasetGroupsProgress(), { - dgName: 'Dataset Group Alpha', - version: 'V5.3.1', - isLatest: true, - status: '', - errorMessage: '', - progress: 30, - }, - { - dgName: 'Dataset Group 1', - version: 'V5.3.1', - isLatest: true, - status: '', - errorMessage: '', - progress: 50, - }, - { - dgName: 'Dataset Group 2', - version: 'V5.3.1', - isLatest: true, - status: 'failed', - errorMessage: - 'Validation failed because “complaints” class found in the “department” column does not exist in hierarchy', - progress: 30, - }, - { - dgName: 'Dataset Group 3', - version: 'V5.3.1', - isLatest: false, - status: '', - errorMessage: '', - progress: 80, - }, - ]; + onSuccess: (data) => { + setProgresses(data); + }, + } + ); + + useEffect(() => { + if (!progressData) return; + + // Function to update the state with data from each SSE + const handleUpdate = (sessionId, newData) => { + setProgresses((prevProgresses) => + prevProgresses.map((progress) => + progress.id === sessionId ? { ...progress, ...newData } : progress + ) + ); + }; + + // Iterate over each element and create an SSE connection for each + const eventSources = progressData.map((progress) => { + return sse(`/${progress.id}`, (data) => { + console.log(`New data for notification ${progress.id}:`, data); + handleUpdate(data.sessionId, data); + }); + }); + + // Clean up all event sources on component unmount + return () => { + eventSources.forEach((eventSource) => eventSource.close()); + console.log('SSE connections closed'); + }; + }, [progressData]); return (
    @@ -50,15 +52,15 @@ const ValidationSessions: FC = () => {
    Validation Sessions
    - {data?.map((session) => { + {progresses?.map((session) => { return ( ); })} diff --git a/GUI/src/services/sse-service.ts b/GUI/src/services/sse-service.ts index ee5389c8..ac913baf 100644 --- a/GUI/src/services/sse-service.ts +++ b/GUI/src/services/sse-service.ts @@ -13,7 +13,7 @@ const sse = (url: string, onMessage: (data: T) => void): EventSource => { if (event.data != undefined && event.data != 'undefined') { const response = JSON.parse(event.data); if (response != undefined) { - onMessage(Object.values(response)[0] as T); + onMessage(response as T); } } }; diff --git a/notification-server/.env b/notification-server/.env index fd0d88cb..1cf1a9bc 100644 --- a/notification-server/.env +++ b/notification-server/.env @@ -5,5 +5,5 @@ OPENSEARCH_USERNAME=admin OPENSEARCH_PASSWORD=admin PORT=4040 REFRESH_INTERVAL=1000 -CORS_WHITELIST_ORIGINS=http://localhost:3001,http://localhost:3003,http://localhost:3004,http://localhost:8080,http://localhost:8088 +CORS_WHITELIST_ORIGINS=http://localhost:3001,http://localhost:3002,http://localhost:3003,http://localhost:3004,http://localhost:8080,http://localhost:8088 RUUTER_URL=http://localhost:8086 From f4069a3354cb5df8663cee837a3c91a1e65171dd Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Thu, 1 Aug 2024 15:23:43 +0530 Subject: [PATCH 332/582] version update bug fix --- DSL/CronManager/DSL/dataset_processing.yml | 4 +- DSL/CronManager/script/data_processor_exec.sh | 13 +- DSL/CronManager/script/data_validator_exec.sh | 5 +- dataset-processor/dataset_processor.py | 117 +++++++++++------- dataset-processor/dataset_processor_api.py | 6 +- docker-compose.yml | 3 +- file-handler/file_handler_api.py | 38 ++++++ 7 files changed, 131 insertions(+), 55 deletions(-) diff --git a/DSL/CronManager/DSL/dataset_processing.yml b/DSL/CronManager/DSL/dataset_processing.yml index 23fe2743..c8a3b03b 100644 --- a/DSL/CronManager/DSL/dataset_processing.yml +++ b/DSL/CronManager/DSL/dataset_processing.yml @@ -2,10 +2,10 @@ dataset_processor: trigger: off type: exec command: "../app/scripts/data_processor_exec.sh" - allowedEnvs: ["cookie","dgId","updateType","savedFilePath","patchPayload"] + allowedEnvs: ["cookie","dgId", "newDgId","updateType","savedFilePath","patchPayload"] data_validation: trigger: off type: exec command: "../app/scripts/data_validator_exec.sh" - allowedEnvs: ["cookie","dgId","updateType","savedFilePath","patchPayload"] \ No newline at end of file + allowedEnvs: ["cookie","dgId", "newDgId","updateType","savedFilePath","patchPayload"] \ No newline at end of file diff --git a/DSL/CronManager/script/data_processor_exec.sh b/DSL/CronManager/script/data_processor_exec.sh index 35f2c40b..34d0cc43 100644 --- a/DSL/CronManager/script/data_processor_exec.sh +++ b/DSL/CronManager/script/data_processor_exec.sh @@ -1,20 +1,21 @@ #!/bin/bash # Ensure required environment variables are set -if [ -z "$dgId" ] || [ -z "$cookie" ] || [ -z "$updateType" ] || [ -z "$savedFilePath" ] || [ -z "$patchPayload" ]; then +if [ -z "$dgId" ] || [ -z "$newDgId" ] || [ -z "$cookie" ] || [ -z "$updateType" ] || [ -z "$savedFilePath" ] || [ -z "$patchPayload" ]; then echo "One or more environment variables are missing." - echo "Please set dgId, cookie, updateType, savedFilePath, and patchPayload." + echo "Please set dgId, newDgId, cookie, updateType, savedFilePath, and patchPayload." exit 1 fi -# Construct the payload using grep +# Construct the payload using here document payload=$(cat <0: @@ -300,14 +332,14 @@ def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload) if updateType == "minor_initial_update": print("Handling Minor update") - # dataset = self.get_dataset(dgID, cookie) + # dataset = self.get_dataset(dgId, cookie) dataset = self.get_dataset_by_location(savedFilePath, cookie) if dataset is not None: print("Dataset retrieved successfully") structured_data = self.check_and_convert(dataset) if structured_data is not None: print("Dataset converted successfully") - selected_data_fields_to_enrich = self.get_selected_data_fields(dgID, cookie) + selected_data_fields_to_enrich = self.get_selected_data_fields(newDgId, cookie) if selected_data_fields_to_enrich is not None: print("Selected data fields to enrich retrieved successfully") max_row_id = max(item["rowId"] for item in structured_data) @@ -317,7 +349,7 @@ def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload) if enriched_data is not None: print("Data enrichment successful") - stop_words = self.get_stopwords(dgID, cookie) + stop_words = self.get_stopwords(newDgId, cookie) if stop_words is not None: print("Stop words retrieved successfully") print(agregated_dataset) @@ -329,12 +361,12 @@ def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload) if chunked_data is not None: print("Data chunking successful") print(chunked_data) - operation_result = self.save_chunked_data(chunked_data, cookie, dgID, 0) + operation_result = self.save_chunked_data(chunked_data, cookie, newDgId, 0) if operation_result: print("Chunked data saved successfully") - agregated_dataset_operation = self.save_aggregrated_data(dgID, cookie, cleaned_data) + agregated_dataset_operation = self.save_aggregrated_data(newDgId, cookie, cleaned_data) if agregated_dataset_operation != None: - return_data = self.update_preprocess_status(dgID, cookie, True, False, f"/dataset/{dgID}/chunks/", "", True, len(cleaned_data), len(chunked_data)) + return_data = self.update_preprocess_status(newDgId, cookie, True, False, f"/dataset/{newDgId}/chunks/", "", True, len(cleaned_data), len(chunked_data)) print(return_data) return SUCCESSFUL_OPERATION else: @@ -366,7 +398,7 @@ def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload) return FAILED_TO_GET_DATASET elif updateType == "minor_append_update": print("Handling Minor update") - agregated_dataset = self.get_dataset(dgID, cookie) + agregated_dataset = self.get_dataset(dgId, cookie) max_row_id = max(item["rowId"] for item in agregated_dataset) if agregated_dataset is not None: print("Aggregated dataset retrieved successfully") @@ -378,14 +410,14 @@ def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload) print(structured_data[-1]) if structured_data is not None: print("Minor update dataset converted successfully") - selected_data_fields_to_enrich = self.get_selected_data_fields(dgID, cookie) + selected_data_fields_to_enrich = self.get_selected_data_fields(newDgId, cookie) if selected_data_fields_to_enrich is not None: print("Selected data fields to enrich for minor update retrieved successfully") max_row_id = max(item["rowId"] for item in structured_data) enriched_data = self.enrich_data(structured_data, selected_data_fields_to_enrich, max_row_id) if enriched_data is not None: print("Minor update data enrichment successful") - stop_words = self.get_stopwords(dgID, cookie) + stop_words = self.get_stopwords(newDgId, cookie) if stop_words is not None: combined_new_dataset = structured_data + enriched_data print("Stop words for minor update retrieved successfully") @@ -395,18 +427,19 @@ def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload) chunked_data = self.chunk_data(cleaned_data) if chunked_data is not None: print("Minor update data chunking successful") - page_count = self.get_page_count(dgID, cookie) + page_count = self.get_page_count(dgId, cookie) if page_count is not None: print(f"Page count retrieved successfully: {page_count}") print(chunked_data) - operation_result = self.save_chunked_data(chunked_data, cookie, dgID, page_count) + copy_exsisting_files = self.copy_chunked_datafiles(dgId, newDgId, cookie, page_count) + operation_result = self.save_chunked_data(chunked_data, cookie, newDgId, page_count) if operation_result is not None: print("Chunked data for minor update saved successfully") agregated_dataset += cleaned_data - agregated_dataset_operation = self.save_aggregrated_data(dgID, cookie, agregated_dataset) + agregated_dataset_operation = self.save_aggregrated_data(newDgId, cookie, agregated_dataset) if agregated_dataset_operation: print("Aggregated dataset for minor update saved successfully") - return_data = self.update_preprocess_status(dgID, cookie, True, False, f"/dataset/{dgID}/chunks/", "", True, len(cleaned_data), (len(chunked_data)+page_count)) + return_data = self.update_preprocess_status(newDgId, cookie, True, False, f"/dataset/{newDgId}/chunks/", "", True, len(agregated_dataset), (len(chunked_data)+page_count)) print(return_data) return SUCCESSFUL_OPERATION else: @@ -449,13 +482,13 @@ def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload) print("*************") if (data_payload["editedData"]!=[]): print("Handling Patch update") - stop_words = self.get_stopwords(dgID, cookie) + stop_words = self.get_stopwords(dgId, cookie) if stop_words is not None: print("Stop words for patch update retrieved successfully") cleaned_patch_payload = self.remove_stop_words(data_payload["editedData"], stop_words) if cleaned_patch_payload is not None: print("Stop words for patch update removed successfully") - page_count = self.get_page_count(dgID, cookie) + page_count = self.get_page_count(dgId, cookie) if page_count is not None: print(f"Page count for patch update retrieved successfully: {page_count}") print(cleaned_patch_payload) @@ -468,7 +501,7 @@ def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload) chunk_updates[chunkNum].append(entry) print(f"Chunk updates prepared: {chunk_updates}") for chunkNum, entries in chunk_updates.items(): - chunk_data = self.download_chunk(dgID, cookie, chunkNum) + chunk_data = self.download_chunk(dgId, cookie, chunkNum) if chunk_data is not None: print(f"Chunk {chunkNum} downloaded successfully") for entry in entries: @@ -477,14 +510,14 @@ def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload) if chunk_entry.get("rowId") == rowId: chunk_data[idx] = entry break - chunk_save_operation = self.save_chunked_data([chunk_data], cookie, dgID, chunkNum-1) + chunk_save_operation = self.save_chunked_data([chunk_data], cookie, dgId, chunkNum-1) if chunk_save_operation == None: print(f"Failed to save chunk {chunkNum}") return FAILED_TO_SAVE_CHUNKED_DATA else: print(f"Failed to download chunk {chunkNum}") return FAILED_TO_DOWNLOAD_CHUNK - agregated_dataset = self.get_dataset(dgID, cookie) + agregated_dataset = self.get_dataset(dgId, cookie) if agregated_dataset is not None: print("Aggregated dataset for patch update retrieved successfully") for entry in cleaned_patch_payload: @@ -496,7 +529,7 @@ def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload) agregated_dataset[index] = entry break - save_result_update = self.save_aggregrated_data(dgID, cookie, agregated_dataset) + save_result_update = self.save_aggregrated_data(dgId, cookie, agregated_dataset) if save_result_update: print("Aggregated dataset for patch update saved successfully") # return SUCCESSFUL_OPERATION @@ -521,7 +554,7 @@ def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload) try: print("Handling deleted data rows") deleted_rows = data_payload["deletedDataRows"] - aggregated_dataset = self.get_dataset(dgID, cookie) + aggregated_dataset = self.get_dataset(dgId, cookie) if aggregated_dataset is not None: print("Aggregated dataset for delete operation retrieved successfully") updated_dataset = [row for row in aggregated_dataset if row.get('rowId') not in deleted_rows] @@ -533,10 +566,10 @@ def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload) if chunked_data is not None: print("Data chunking after delete operation successful") print(chunked_data) - operation_result = self.save_chunked_data(chunked_data, cookie, dgID, 0) + operation_result = self.save_chunked_data(chunked_data, cookie, dgId, 0) if operation_result: print("Chunked data after delete operation saved successfully") - save_result_delete = self.save_aggregrated_data(dgID, cookie, updated_dataset) + save_result_delete = self.save_aggregrated_data(dgId, cookie, updated_dataset) if save_result_delete: print("Aggregated dataset after delete operation saved successfully") else: @@ -567,14 +600,14 @@ def process_handler(self, dgID, cookie, updateType, savedFilePath, patchPayload) return FAILED_TO_SAVE_AGGREGATED_DATA elif data_payload["editedData"]==[] and data_payload["deletedDataRows"]!=[]: if save_result_delete: - return_data = self.update_preprocess_status(dgID, cookie, True, False, f"/dataset/{dgID}/chunks/", "", True, len(updated_dataset), len(chunked_data)) + return_data = self.update_preprocess_status(dgId, cookie, True, False, f"/dataset/{dgId}/chunks/", "", True, len(updated_dataset), len(chunked_data)) print(return_data) return SUCCESSFUL_OPERATION else: return FAILED_TO_SAVE_AGGREGATED_DATA elif data_payload["editedData"]!=[] and data_payload["deletedDataRows"]!=[]: if save_result_update and save_result_delete: - return_data = self.update_preprocess_status(dgID, cookie, True, False, f"/dataset/{dgID}/chunks/", "", True, len(updated_dataset), len(chunked_data)) + return_data = self.update_preprocess_status(dgId, cookie, True, False, f"/dataset/{dgId}/chunks/", "", True, len(updated_dataset), len(chunked_data)) print(return_data) return SUCCESSFUL_OPERATION else: diff --git a/dataset-processor/dataset_processor_api.py b/dataset-processor/dataset_processor_api.py index 4edb2094..d2d831f4 100644 --- a/dataset-processor/dataset_processor_api.py +++ b/dataset-processor/dataset_processor_api.py @@ -19,7 +19,8 @@ ) class ProcessHandlerRequest(BaseModel): - dgID: int + dgId: int + newDgId: int cookie: str updateType: str savedFilePath: str @@ -47,7 +48,7 @@ async def process_handler_endpoint(request: Request): await authenticate_user(request) authCookie = payload["cookie"] - result = processor.process_handler(int(payload["dgID"]), authCookie, payload["updateType"], payload["savedFilePath"], payload["patchPayload"]) + result = processor.process_handler(int(payload["dgId"]), int(payload["newDgId"]), authCookie, payload["updateType"], payload["savedFilePath"], payload["patchPayload"]) if result: return result else: @@ -69,6 +70,7 @@ async def forward_request(request: Request, response: Response): print(payload) payload2 = {} payload2["dgId"] = int(payload["dgId"]) + payload2["newDgId"] = int(payload["newDgId"]) payload2["updateType"] = payload["updateType"] payload2["patchPayload"] = payload["patchPayload"] payload2["savedFilePath"] = payload["savedFilePath"] diff --git a/docker-compose.yml b/docker-compose.yml index 69b10c7b..130113de 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -226,7 +226,8 @@ services: - FILE_HANDLER_DOWNLOAD_LOCATION_JSON_URL=http://file-handler:8000/datasetgroup/data/download/json/location - FILE_HANDLER_STOPWORDS_URL=http://file-handler:8000/datasetgroup/data/download/json/stopwords - FILE_HANDLER_IMPORT_CHUNKS_URL=http://file-handler:8000/datasetgroup/data/import/chunk - - GET_PAGE_COUNT_URL=http://ruuter-private:8088/classifier/datasetgroup/group/data?groupId=dgID&pageNum=1 + - FILE_HANDLER_COPY_CHUNKS_URL=http://file-handler:8000/datasetgroup/data/copy + - GET_PAGE_COUNT_URL=http://ruuter-private:8088/classifier/datasetgroup/group/data?groupId=dgId&pageNum=1 - SAVE_JSON_AGGREGRATED_DATA_URL=http://file-handler:8000/datasetgroup/data/import/json - DOWNLOAD_CHUNK_URL=http://file-handler:8000/datasetgroup/data/download/chunk - STATUS_UPDATE_URL=http://ruuter-private:8088/classifier/datasetgroup/update/preprocess/status diff --git a/file-handler/file_handler_api.py b/file-handler/file_handler_api.py index b443c418..418a37cc 100644 --- a/file-handler/file_handler_api.py +++ b/file-handler/file_handler_api.py @@ -42,6 +42,11 @@ class ImportJsonMajor(BaseModel): dgId: int dataset: list +class CopyPayload(BaseModel): + dgId: int + newDgId: int + fileLocations: list + if not os.path.exists(UPLOAD_DIRECTORY): os.makedirs(UPLOAD_DIRECTORY) @@ -282,3 +287,36 @@ async def upload_and_copy(request: Request, importData: ImportJsonMajor): return JSONResponse(status_code=200, content=upload_success) else: raise HTTPException(status_code=500, detail=S3_UPLOAD_FAILED) + +@app.post("/datasetgroup/data/copy") +async def upload_and_copy(request: Request, copyPayload: CopyPayload): + cookie = request.cookies.get("customJwtCookie") + await authenticate_user(f'customJwtCookie={cookie}') + + dg_id = copyPayload.dgId + new_dg_id = copyPayload.newDgId + files = copyPayload.fileLocations + + if len(files)>0: + local_storage_location = "temp_copy.json" + else: + print("Abort copying since sent file list does not have any entry.") + upload_success = UPLOAD_SUCCESS.copy() + upload_success["saved_file_path"] = "" + return JSONResponse(status_code=200, content=upload_success) + for file in files: + old_location = f"/dataset/{dg_id}/{file}" + new_location = f"/dataset/{new_dg_id}/{file}" + response = s3_ferry.transfer_file(local_storage_location, "FS", old_location, "S3") + response = s3_ferry.transfer_file(new_location, "S3", local_storage_location, "FS") + + if response.status_code == 201: + print(f"Copying completed : {file}") + else: + print(f"Copying failed : {file}") + raise HTTPException(status_code=500, detail=S3_UPLOAD_FAILED) + else: + os.remove(local_storage_location) + upload_success = UPLOAD_SUCCESS.copy() + upload_success["saved_file_path"] = f"/dataset/{new_dg_id}/" + return JSONResponse(status_code=200, content=upload_success) From 5fed45af2aa41421d9f2eb51e0c61d079320b31e Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Thu, 1 Aug 2024 15:43:45 +0530 Subject: [PATCH 333/582] code cleanups --- GUI/src/pages/ValidationSessions/index.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/GUI/src/pages/ValidationSessions/index.tsx b/GUI/src/pages/ValidationSessions/index.tsx index 8e05b271..67062487 100644 --- a/GUI/src/pages/ValidationSessions/index.tsx +++ b/GUI/src/pages/ValidationSessions/index.tsx @@ -22,7 +22,6 @@ const ValidationSessions: FC = () => { useEffect(() => { if (!progressData) return; - // Function to update the state with data from each SSE const handleUpdate = (sessionId, newData) => { setProgresses((prevProgresses) => prevProgresses.map((progress) => @@ -31,7 +30,6 @@ const ValidationSessions: FC = () => { ); }; - // Iterate over each element and create an SSE connection for each const eventSources = progressData.map((progress) => { return sse(`/${progress.id}`, (data) => { console.log(`New data for notification ${progress.id}:`, data); @@ -39,7 +37,6 @@ const ValidationSessions: FC = () => { }); }); - // Clean up all event sources on component unmount return () => { eventSources.forEach((eventSource) => eventSource.close()); console.log('SSE connections closed'); From 9b8839342f9499fa08a4bd07955e9d1143350582 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Thu, 1 Aug 2024 17:16:56 +0530 Subject: [PATCH 334/582] ESCLASS-159- model implementation API's and validation status issue fixed --- .../classifier-script-v9-models-metadata.sql | 23 +++++- DSL/Resql/get-data-model-metadata-by-id.sql | 14 ++++ DSL/Resql/get-data-model-options.sql | 1 + .../get-paginated-data-model-metadata.sql | 4 +- .../get-validated-all-dataset-groups.sql | 3 + .../update-patch-version-dataset-group.sql | 6 +- .../classifier/datamodel/create/options.yml | 71 +++++++++++++++++++ .../DSL/GET/classifier/datamodel/metadata.yml | 65 +++++++++++++++++ .../DSL/GET/classifier/datamodel/overview.yml | 3 +- .../classifier/datasetgroup/update/patch.yml | 5 +- .../datasetgroup/update/validation/status.yml | 36 ++++++++-- 11 files changed, 212 insertions(+), 19 deletions(-) create mode 100644 DSL/Resql/get-data-model-metadata-by-id.sql create mode 100644 DSL/Resql/get-data-model-options.sql create mode 100644 DSL/Resql/get-validated-all-dataset-groups.sql create mode 100644 DSL/Ruuter.private/DSL/GET/classifier/datamodel/create/options.yml create mode 100644 DSL/Ruuter.private/DSL/GET/classifier/datamodel/metadata.yml diff --git a/DSL/Liquibase/changelog/classifier-script-v9-models-metadata.sql b/DSL/Liquibase/changelog/classifier-script-v9-models-metadata.sql index a56747df..e3a04054 100644 --- a/DSL/Liquibase/changelog/classifier-script-v9-models-metadata.sql +++ b/DSL/Liquibase/changelog/classifier-script-v9-models-metadata.sql @@ -1,16 +1,16 @@ -- liquibase formatted sql -- changeset kalsara Magamage:classifier-script-v9-changeset1 -CREATE TYPE Maturity_Label AS ENUM ('development', 'staging', 'production-ready'); +CREATE TYPE Maturity_Label AS ENUM ('development', 'staging', 'production'); -- changeset kalsara Magamage:classifier-script-v9-changeset2 -CREATE TYPE Deployment_Env AS ENUM ('outlook', 'testing', 'inival', 'undeployed'); +CREATE TYPE Deployment_Env AS ENUM ('jira', 'outlook', 'pinal', 'testing', 'undeployed'); -- changeset kalsara Magamage:classifier-script-v9-changeset3 CREATE TYPE Training_Status AS ENUM ('not trained', 'training in progress', 'trained', 'retraining needed', 'untrainable'); -- changeset kalsara Magamage:classifier-script-v9-changeset4 -CREATE TYPE Base_Models AS ENUM ('xlnet', 'bert', 'roberta'); +CREATE TYPE Base_Models AS ENUM ('xlnet', 'roberta', 'albert'); -- changeset kalsara Magamage:classifier-script-v9-changeset5 CREATE TABLE models_metadata ( @@ -32,4 +32,21 @@ CREATE TABLE models_metadata ( inference_routes JSONB, training_results JSONB, CONSTRAINT models_metadata_pkey PRIMARY KEY (id) +); + +-- changeset kalsara Magamage:classifier-script-v9-changeset6 +CREATE TABLE model_configurations ( + id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY, + base_models Base_Models[], + deployment_platforms Deployment_Env[], + maturity_labels Maturity_Label[], + CONSTRAINT model_configurations_pkey PRIMARY KEY (id) +); + +-- changeset kalsara Magamage:classifier-script-v9-changeset7 +INSERT INTO model_configurations (base_models, deployment_platforms, maturity_labels) VALUES +( + ARRAY['xlnet', 'roberta', 'albert']::Base_Models[], + ARRAY['jira', 'outlook', 'pinal', 'testing', 'undeployed']::Deployment_Env[], + ARRAY['development', 'staging', 'production']::Maturity_Label[] ); \ No newline at end of file diff --git a/DSL/Resql/get-data-model-metadata-by-id.sql b/DSL/Resql/get-data-model-metadata-by-id.sql new file mode 100644 index 00000000..2f88d651 --- /dev/null +++ b/DSL/Resql/get-data-model-metadata-by-id.sql @@ -0,0 +1,14 @@ +SELECT + id AS model_id, + model_name, + major_version, + minor_version, + latest, + maturity_label, + deployment_env, + training_status, + base_models, + connected_dg_id, + connected_dg_name +FROM models_metadata +WHERE id = :id; diff --git a/DSL/Resql/get-data-model-options.sql b/DSL/Resql/get-data-model-options.sql new file mode 100644 index 00000000..682c4629 --- /dev/null +++ b/DSL/Resql/get-data-model-options.sql @@ -0,0 +1 @@ +SELECT * from model_configurations; \ No newline at end of file diff --git a/DSL/Resql/get-paginated-data-model-metadata.sql b/DSL/Resql/get-paginated-data-model-metadata.sql index 7083ccc0..13d30b12 100644 --- a/DSL/Resql/get-paginated-data-model-metadata.sql +++ b/DSL/Resql/get-paginated-data-model-metadata.sql @@ -13,9 +13,7 @@ SELECT dt.created_timestamp, dt.connected_dg_id, dt.connected_dg_name, - dt.model_s3_location, - dt.inference_routes, - dt.training_results, + jsonb_pretty(dt.training_results) AS training_results, CEIL(COUNT(*) OVER() / :page_size::DECIMAL) AS total_pages FROM models_metadata dt diff --git a/DSL/Resql/get-validated-all-dataset-groups.sql b/DSL/Resql/get-validated-all-dataset-groups.sql new file mode 100644 index 00000000..f5f5854b --- /dev/null +++ b/DSL/Resql/get-validated-all-dataset-groups.sql @@ -0,0 +1,3 @@ +SELECT id as dg_id, group_name, major_version, minor_version, patch_version +FROM dataset_group_metadata +WHERE is_enabled = TRUE AND validation_status = 'success'; diff --git a/DSL/Resql/update-patch-version-dataset-group.sql b/DSL/Resql/update-patch-version-dataset-group.sql index 2c589db8..9d5f4784 100644 --- a/DSL/Resql/update-patch-version-dataset-group.sql +++ b/DSL/Resql/update-patch-version-dataset-group.sql @@ -7,11 +7,7 @@ WITH update_latest AS ( update_specific AS ( UPDATE dataset_group_metadata SET - patch_version = ( - SELECT COALESCE(MAX(patch_version), 0) + 1 - FROM dataset_group_metadata - WHERE group_key = :group_key AND major_version = :major_version AND minor_version = :minor_version - ), + patch_version = patch_version + 1, enable_allowed = false, validation_status = 'in-progress'::Validation_Status, is_enabled = false, diff --git a/DSL/Ruuter.private/DSL/GET/classifier/datamodel/create/options.yml b/DSL/Ruuter.private/DSL/GET/classifier/datamodel/create/options.yml new file mode 100644 index 00000000..60d23b8e --- /dev/null +++ b/DSL/Ruuter.private/DSL/GET/classifier/datamodel/create/options.yml @@ -0,0 +1,71 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'OPTIONS'" + method: get + accepts: json + returns: json + namespace: classifier + +get_data_model_options: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-data-model-options" + result: res_options + next: check_status + +check_status: + switch: + - condition: ${200 <= res_options.response.statusCodeValue && res_options.response.statusCodeValue < 300} + next: get_dataset_group_data + next: return_bad_request + +get_dataset_group_data: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-validated-all-dataset-groups" + result: res_dataset + next: check_dataset_group_status + +check_dataset_group_status: + switch: + - condition: ${200 <= res_dataset.response.statusCodeValue && res_dataset.response.statusCodeValue < 300} + next: check_data_exist + next: return_bad_request + +check_data_exist: + switch: + - condition: ${res_dataset.response.body.length>0} + next: assign_dataset + next: assign_empty + +assign_dataset: + assign: + dataset_group: ${res_dataset.response.body} + next: assign_success_response + +assign_empty: + assign: + dataset_group: [] + next: assign_success_response + +assign_success_response: + assign: + format_res: { + baseModels: '${res_options.response.body[0].baseModels}', + deploymentPlatforms: '${res_options.response.body[0].deploymentPlatforms}', + maturityLabels: '${res_options.response.body[0].maturityLabels}', + datasetGroups: '${dataset_group}' + } + next: return_ok + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: "Bad Request" + next: end + diff --git a/DSL/Ruuter.private/DSL/GET/classifier/datamodel/metadata.yml b/DSL/Ruuter.private/DSL/GET/classifier/datamodel/metadata.yml new file mode 100644 index 00000000..30780d05 --- /dev/null +++ b/DSL/Ruuter.private/DSL/GET/classifier/datamodel/metadata.yml @@ -0,0 +1,65 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'METADATA'" + method: get + accepts: json + returns: json + namespace: classifier + allowlist: + params: + - field: modelId + type: number + description: "Parameter 'modelId'" + +extract_data: + assign: + model_id: ${Number(incoming.params.modelId)} + next: get_data_model_meta_data_by_id + +get_data_model_meta_data_by_id: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-data-model-metadata-by-id" + body: + id: ${model_id} + result: res_model + next: check_status + +check_status: + switch: + - condition: ${200 <= res_model.response.statusCodeValue && res_model.response.statusCodeValue < 300} + next: check_data_exist + next: assign_fail_response + +check_data_exist: + switch: + - condition: ${res_model.response.body.length>0} + next: assign_success_response + next: assign_fail_response + +assign_success_response: + assign: + format_res: { + operationSuccessful: true, + data: '${res_model.response.body}' + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + operationSuccessful: false, + data: '${[]}' + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/GET/classifier/datamodel/overview.yml b/DSL/Ruuter.private/DSL/GET/classifier/datamodel/overview.yml index d529ec7b..e6acc0a0 100644 --- a/DSL/Ruuter.private/DSL/GET/classifier/datamodel/overview.yml +++ b/DSL/Ruuter.private/DSL/GET/classifier/datamodel/overview.yml @@ -51,7 +51,7 @@ extract_data: dataset_group: ${incoming.params.datasetGroup} training_status: ${incoming.params.trainingStatus} deployment_maturity: ${incoming.params.deploymentMaturity} - next: get_dataset_meta_data_overview + next: get_data_model_meta_data_overview get_data_model_meta_data_overview: call: http.post @@ -68,6 +68,7 @@ get_data_model_meta_data_overview: dataset_group: ${dataset_group} training_status: ${training_status} deployment_maturity: ${deployment_maturity} + sort_type: ${sort_type} result: res_model next: check_status diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/patch.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/patch.yml index 319caa2f..c7ac0b5d 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/patch.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/patch.yml @@ -60,10 +60,8 @@ update_old_dataset_group: url: "[#CLASSIFIER_RESQL]/update-patch-version-dataset-group" body: id: ${dg_id} - group_key: ${group_key} - major_version: ${major_version} - minor_version: ${minor_version} last_updated_timestamp: ${new Date().toISOString()} + group_key: ${group_key} result: res next: check_old_dataset_status @@ -80,6 +78,7 @@ execute_cron_manager: query: cookie: ${incoming.headers.cookie} dgId: ${dg_id} + newDgId: 0 updateType: 'patch' savedFilePath: 'None' patchPayload: ${encodeURIComponent(JSON.stringify(update_data_payload))} diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/validation/status.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/validation/status.yml index 8fc9a70e..e594350d 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/validation/status.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/validation/status.yml @@ -11,6 +11,9 @@ declaration: - field: dgId type: string description: "Body field 'dgId'" + - field: newDgId + type: string + description: "Body field 'newDgId'" - field: updateType type: string description: "Body field 'updateType'" @@ -30,11 +33,30 @@ declaration: extract_request_data: assign: dg_id: ${incoming.body.dgId} + new_dg_id: ${incoming.body.newDgId} update_type: ${incoming.body.updateType} patch_payload: ${incoming.body.patchPayload} save_file_path: ${incoming.body.savedFilePath} validation_status: ${incoming.body.validationStatus} validation_errors: ${incoming.body.validationErrors} + next: check_request_type + +check_request_type: + switch: + - condition: ${update_type == 'minor'} + next: assign_minor_type + - condition: ${update_type == 'patch'} + next: assign_patch_type + next: update_type_not_found + +assign_patch_type: + assign: + active_dg_id: ${incoming.body.dgId} + next: update_dataset_group_validation + +assign_minor_type: + assign: + active_dg_id: ${incoming.body.newDgId} next: update_dataset_group_validation update_dataset_group_validation: @@ -42,7 +64,7 @@ update_dataset_group_validation: args: url: "[#CLASSIFIER_RESQL]/update-dataset-group-validation-data" body: - id: ${dg_id} + id: ${active_dg_id} validation_status: ${validation_status} validation_errors: ${JSON.stringify(validation_errors)} result: res @@ -67,6 +89,7 @@ execute_cron_manager: query: cookie: ${incoming.headers.cookie} dgId: ${dg_id} + newDgId: ${new_dg_id} updateType: ${update_type} savedFilePath: ${save_file_path} patchPayload: ${patch_payload} @@ -76,7 +99,7 @@ execute_cron_manager: assign_success_response: assign: format_res: { - dgId: '${dg_id}', + dgId: '${active_dg_id}', operationSuccessful: true, } next: return_ok @@ -84,7 +107,7 @@ assign_success_response: assign_fail_response: assign: format_res: { - dgId: '${dg_id}', + dgId: '${active_dg_id}', operationSuccessful: false, } next: return_bad_request @@ -94,7 +117,12 @@ return_ok: return: ${format_res} next: end +update_type_not_found: + status: 400 + return: "Update type not found" + next: end + return_bad_request: status: 400 return: ${format_res} - next: end + next: end \ No newline at end of file From 1538305732dfa8b1ef2e9bd8e651b60839397aa8 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Thu, 1 Aug 2024 17:48:27 +0530 Subject: [PATCH 335/582] ESCLASS-159- sonar issues fixed --- DSL/Resql/get-data-model-options.sql | 3 ++- DSL/Resql/get-validated-all-dataset-groups.sql | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/DSL/Resql/get-data-model-options.sql b/DSL/Resql/get-data-model-options.sql index 682c4629..91df4715 100644 --- a/DSL/Resql/get-data-model-options.sql +++ b/DSL/Resql/get-data-model-options.sql @@ -1 +1,2 @@ -SELECT * from model_configurations; \ No newline at end of file +SELECT base_models, deployment_platforms, maturity_labels +FROM model_configurations; \ No newline at end of file diff --git a/DSL/Resql/get-validated-all-dataset-groups.sql b/DSL/Resql/get-validated-all-dataset-groups.sql index f5f5854b..2d0c0ccc 100644 --- a/DSL/Resql/get-validated-all-dataset-groups.sql +++ b/DSL/Resql/get-validated-all-dataset-groups.sql @@ -1,3 +1,3 @@ SELECT id as dg_id, group_name, major_version, minor_version, patch_version FROM dataset_group_metadata -WHERE is_enabled = TRUE AND validation_status = 'success'; +WHERE is_enabled = true AND validation_status = 'success'; From 3b4e77aeb4bfee4187cf71b93523da3e61cbd224 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Thu, 1 Aug 2024 18:19:18 +0530 Subject: [PATCH 336/582] data models uis --- GUI/src/pages/DataModels/index.tsx | 101 ++++++++++++++++------------- GUI/src/services/data-models.ts | 11 ++-- 2 files changed, 63 insertions(+), 49 deletions(-) diff --git a/GUI/src/pages/DataModels/index.tsx b/GUI/src/pages/DataModels/index.tsx index 63a913ae..9dce22eb 100644 --- a/GUI/src/pages/DataModels/index.tsx +++ b/GUI/src/pages/DataModels/index.tsx @@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next'; import { Button, FormInput, FormSelect } from 'components'; import DatasetGroupCard from 'components/molecules/DatasetGroupCard'; import Pagination from 'components/molecules/Pagination'; -import { getDatasetsOverview, getFilterData } from 'services/datasets'; import { useQuery } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; import { @@ -11,6 +10,7 @@ import { formattedArray, parseVersionString, } from 'utils/commonUtilts'; +import { getDataModelsOverview, getFilterData } from 'services/data-models'; const DataModels: FC = () => { const { t } = useTranslation(); @@ -19,41 +19,47 @@ const DataModels: FC = () => { const [pageIndex, setPageIndex] = useState(1); const [id, setId] = useState(0); const [enableFetch, setEnableFetch] = useState(true); - const [view, setView] = useState("list"); + const [view, setView] = useState('list'); -useEffect(()=>{ - setEnableFetch(true) -},[view]); + useEffect(() => { + setEnableFetch(true); + }, [view]); const [filters, setFilters] = useState({ - datasetGroupName: 'all', + modelName: 'all', version: 'x.x.x', - validationStatus: 'all', + platform: 'all', + datasetGroup: 'all', + trainingStatus: 'all', + maturity: 'all', sort: 'asc', }); - const { - data: datasetGroupsData, - isLoading, - } = useQuery( + const { data: dataModelsData, isLoading } = useQuery( [ - 'datasetgroup/overview', + 'datamodels/overview', pageIndex, - filters.datasetGroupName, + filters.modelName, parseVersionString(filters?.version)?.major, parseVersionString(filters?.version)?.minor, parseVersionString(filters?.version)?.patch, - filters.validationStatus, + filters.platform, + filters.datasetGroup, + filters.trainingStatus, + filters.maturity, filters.sort, ], () => - getDatasetsOverview( + getDataModelsOverview( pageIndex, - filters.datasetGroupName, + filters.modelName, parseVersionString(filters?.version)?.major, parseVersionString(filters?.version)?.minor, parseVersionString(filters?.version)?.patch, - filters.validationStatus, + filters.platform, + filters.datasetGroup, + filters.trainingStatus, + filters.maturity, filters.sort ), { @@ -61,10 +67,11 @@ useEffect(()=>{ enabled: enableFetch, } ); - const { data: filterData } = useQuery(['datasets/filters'], () => + const { data: filterData } = useQuery(['datamodels/filters'], () => getFilterData() ); - const pageCount = datasetGroupsData?.response?.data?.[0]?.totalPages || 1; + // const pageCount = datasetGroupsData?.response?.data?.[0]?.totalPages || 1; + console.log(dataModelsData); const handleFilterChange = (name: string, value: string) => { setEnableFetch(false); @@ -74,10 +81,9 @@ useEffect(()=>{ })); }; - return (
    -
    +
    Data Models
    - handleFilterChange('datasetGroupName', selection?.value ?? '') + handleFilterChange('modelName', selection?.value ?? '') } /> handleFilterChange('version', selection?.value ?? '') } /> - handleFilterChange('validationStatus', selection?.value ?? '') + handleFilterChange('platform', selection?.value ?? '') } /> - - handleFilterChange('validationStatus', selection?.value ?? '') + handleFilterChange('datasetGroup', selection?.value ?? '') } /> - - handleFilterChange('validationStatus', selection?.value ?? '') + handleFilterChange('trainingStatus', selection?.value ?? '') + } + /> + + handleFilterChange('maturity', selection?.value ?? '') } /> { style={{ padding: '20px', marginTop: '20px' }} > {isLoading &&
    Loading...
    } - {datasetGroupsData?.response?.data?.map( + {dataModelsData?.data?.map( (dataset, index: number) => { return ( { lastModelTrained={dataset?.lastModelTrained} setId={setId} setView={setView} - /> ); } )}
    1} - canNextPage={pageIndex < pageCount} + canNextPage={pageIndex < 10} onPageChange={setPageIndex} />
    -
    ); }; diff --git a/GUI/src/services/data-models.ts b/GUI/src/services/data-models.ts index 00e8e63b..7c37b307 100644 --- a/GUI/src/services/data-models.ts +++ b/GUI/src/services/data-models.ts @@ -4,17 +4,18 @@ import apiMock from './api-mock'; import { PaginationState } from '@tanstack/react-table'; import { DatasetGroup, Operation } from 'types/datasetGroups'; -export async function getDatasetsOverview( +export async function getDataModelsOverview( pageNum: number, modelGroup: string, majorVersion: number, minorVersion: number, patchVersion: number, platform: string, - sort: string, datasetGroup:string, trainingStatus:string, - deploymentMaturity:string + deploymentMaturity:string, + sort: string, + ) { const { data } = await apiMock.get('classifier/datamodel/overview', { params: { @@ -24,10 +25,10 @@ export async function getDatasetsOverview( minorVersion, patchVersion, platform, - sortType:sort, datasetGroup, trainingStatus, deploymentMaturity, + sortType:sort, pageSize:5 }, }); @@ -35,7 +36,7 @@ export async function getDatasetsOverview( } export async function getFilterData() { - const { data } = await apiDev.get('classifier/datasetgroup/overview/filters'); + const { data } = await apiMock.get('classifier/datamodel/overview/filters'); return data; } From 5f5907c32e0195e182891db5109cc1f6d007f07a Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Thu, 1 Aug 2024 19:00:02 +0530 Subject: [PATCH 337/582] rowId cast change --- dataset-processor/dataset_processor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dataset-processor/dataset_processor.py b/dataset-processor/dataset_processor.py index 456a0d81..3d9ecb86 100644 --- a/dataset-processor/dataset_processor.py +++ b/dataset-processor/dataset_processor.py @@ -495,6 +495,7 @@ def process_handler(self, dgId, newDgId, cookie, updateType, savedFilePath, patc chunk_updates = {} for entry in cleaned_patch_payload: rowId = entry.get("rowId") + rowId = int(rowId) chunkNum = (rowId - 1) // 5 + 1 if chunkNum not in chunk_updates: chunk_updates[chunkNum] = [] @@ -506,6 +507,7 @@ def process_handler(self, dgId, newDgId, cookie, updateType, savedFilePath, patc print(f"Chunk {chunkNum} downloaded successfully") for entry in entries: rowId = entry.get("rowId") + rowId = int(rowId) for idx, chunk_entry in enumerate(chunk_data): if chunk_entry.get("rowId") == rowId: chunk_data[idx] = entry @@ -522,6 +524,7 @@ def process_handler(self, dgId, newDgId, cookie, updateType, savedFilePath, patc print("Aggregated dataset for patch update retrieved successfully") for entry in cleaned_patch_payload: rowId = entry.get("rowId") + rowId = int(rowId) for index, item in enumerate(agregated_dataset): if item.get("rowId") == rowId: entry["rowId"] = rowId From f69ce6dddb30c50fedaf1629c525f198f43d6df4 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Thu, 1 Aug 2024 19:44:55 +0530 Subject: [PATCH 338/582] update-latest-issue: minor and major latest false issue fixed --- .../update-latest-version-dataset-group.sql | 3 +++ .../classifier/datasetgroup/update/major.yml | 17 ++++++++++++++++- .../classifier/datasetgroup/update/minor.yml | 17 ++++++++++++++++- 3 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 DSL/Resql/update-latest-version-dataset-group.sql diff --git a/DSL/Resql/update-latest-version-dataset-group.sql b/DSL/Resql/update-latest-version-dataset-group.sql new file mode 100644 index 00000000..ce2ed3fb --- /dev/null +++ b/DSL/Resql/update-latest-version-dataset-group.sql @@ -0,0 +1,3 @@ +UPDATE dataset_group_metadata +SET latest = false +WHERE group_key = :group_key \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/major.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/major.yml index 5a455623..11e81c49 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/major.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/major.yml @@ -55,7 +55,22 @@ check_data_exist: assign_group_key: assign: group_key: ${res.response.body[0].groupKey} - next: snapshot_dataset_group + next: update_latest_in_old_version + +update_latest_in_old_version: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/update-latest-version-dataset-group" + body: + group_key: ${group_key} + result: res + next: check_latest_status + +check_latest_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: snapshot_dataset_group + next: assign_fail_response snapshot_dataset_group: call: http.post diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/minor.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/minor.yml index 62a4da11..d06bc3e7 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/minor.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/minor.yml @@ -50,7 +50,22 @@ assign_group_key: assign: group_key: ${res.response.body[0].groupKey} major_version: ${res.response.body[0].majorVersion} - next: snapshot_dataset_group + next: update_latest_in_old_version + +update_latest_in_old_version: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/update-latest-version-dataset-group" + body: + group_key: ${group_key} + result: res + next: check_latest_status + +check_latest_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: snapshot_dataset_group + next: assign_fail_response snapshot_dataset_group: call: http.post From 30e9068a199038f1a4d808f0294bb489d4adcacc Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Thu, 1 Aug 2024 22:51:08 +0530 Subject: [PATCH 339/582] react app csp update --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 130113de..94497c6e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -96,7 +96,7 @@ services: - REACT_APP_RUUTER_PRIVATE_API_URL=http://localhost:8088 - REACT_APP_CUSTOMER_SERVICE_LOGIN=http://localhost:3004/et/dev-auth # - REACT_APP_NOTIFICATION_NODE_URL=http://localhost:4040 - - REACT_APP_CSP=upgrade-insecure-requests; default-src 'self'; font-src 'self' data:; img-src 'self' data:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self' http://localhost:8086 http://localhost:8088 http://localhost:8085 http://localhost:4040 http://localhost:3001; + - REACT_APP_CSP=upgrade-insecure-requests; default-src 'self'; font-src 'self' data:; img-src 'self' data:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self' http://localhost:8086 http://localhost:8088 http://localhost:8085 http://localhost:4040 http://localhost:3001 http://localhost:8000; - DEBUG_ENABLED=true - CHOKIDAR_USEPOLLING=true - PORT=3001 From 3864ca14f8e424ec55c6fd74bff91169b9df4095 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Thu, 1 Aug 2024 23:01:33 +0530 Subject: [PATCH 340/582] import stopwords --- file-handler/file_handler_api.py | 52 +++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/file-handler/file_handler_api.py b/file-handler/file_handler_api.py index 418a37cc..3c417ffb 100644 --- a/file-handler/file_handler_api.py +++ b/file-handler/file_handler_api.py @@ -58,14 +58,8 @@ def get_ruuter_private_url(): async def authenticate_user(cookie: str): try: - # cookie = request.cookies.get("customJwtCookie") - # cookie = f'customJwtCookie={cookie}' - if not cookie: raise HTTPException(status_code=401, detail="No cookie found in the request") - - print("@#!@#!@#!2") - print(cookie) url = f"{RUUTER_PRIVATE_URL}/auth/jwt/userinfo" headers = { @@ -73,11 +67,12 @@ async def authenticate_user(cookie: str): } response = requests.get(url, headers=headers) - + if response.status_code != 200: raise HTTPException(status_code=response.status_code, detail="Authentication failed") except Exception as e: print(f"Error in file handler authentication : {e}") + raise HTTPException(status_code=500, detail="Authentication failed") @app.post("/datasetgroup/data/import") async def upload_and_copy(request: Request, dgId: int = Form(...), dataFile: UploadFile = File(...)): @@ -230,8 +225,6 @@ async def upload_and_copy(request: Request, import_chunks: ImportChunks): os.remove(fileLocation) else: raise HTTPException(status_code=500, detail=S3_UPLOAD_FAILED) - # else: - # return True @app.get("/datasetgroup/data/download/chunk") async def download_and_convert(request: Request, dgId: int, pageId: int, backgroundTasks: BackgroundTasks): @@ -240,9 +233,6 @@ async def download_and_convert(request: Request, dgId: int, pageId: int, backgro await authenticate_user(f'customJwtCookie={cookie}') print("$#@$@#$@#$@#$") print(request) - # cookie = request.cookies.get("cookie") - # cookie = f'customJwtCookie={cookie}' - # await authenticate_user(cookie) save_location = f"/dataset/{dgId}/chunks/{pageId}{JSON_EXT}" local_file_name = f"group_{dgId}_chunk_{pageId}" @@ -256,9 +246,6 @@ async def download_and_convert(request: Request, dgId: int, pageId: int, backgro with open(f"{json_file_path}", 'r') as json_file: json_data = json.load(json_file) - # for index, item in enumerate(json_data, start=1): - # item['rowId'] = index - backgroundTasks.add_task(os.remove, json_file_path) return json_data @@ -320,3 +307,38 @@ async def upload_and_copy(request: Request, copyPayload: CopyPayload): upload_success = UPLOAD_SUCCESS.copy() upload_success["saved_file_path"] = f"/dataset/{new_dg_id}/" return JSONResponse(status_code=200, content=upload_success) + +@app.post("/datasetgroup/data/import/stop-words") +async def import_stop_words(request: Request, stopWordsFile: UploadFile = File(...)): + try: + cookie = request.cookies.get("customJwtCookie") + await authenticate_user(f'customJwtCookie={cookie}') + + file_content = await stopWordsFile.read() + words_list = file_content.decode('utf-8').split(',') + + url = 'http://localhost:8088/classifier/datasetgroup/update/stop-words' + headers = { + 'Content-Type': 'application/json', + 'Cookie': f'customJwtCookie={cookie}' + } + + response = requests.post(url, headers=headers, json={"stopWords": words_list}) + + if response.status_code == 200: + response_data = response.json() + if response_data['operationSuccessful']: + return response_data + elif response_data['duplicate']: + duplicate_items = response_data['duplicateItems'] + new_words_list = [word for word in words_list if word not in duplicate_items] + if new_words_list: + response = requests.post(url, headers=headers, json={"stopWords": new_words_list}) + return response.json() + else: + return response_data + else: + raise HTTPException(status_code=response.status_code, detail="Failed to update stop words") + except Exception as e: + print(f"Error in import/stop-words: {e}") + raise HTTPException(status_code=500, detail=str(e)) From c728797a86b06d8c9da376611d32bde39ad66ca8 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Thu, 1 Aug 2024 23:02:13 +0530 Subject: [PATCH 341/582] remove spaces --- file-handler/file_handler_api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/file-handler/file_handler_api.py b/file-handler/file_handler_api.py index 3c417ffb..6e540d7f 100644 --- a/file-handler/file_handler_api.py +++ b/file-handler/file_handler_api.py @@ -315,7 +315,8 @@ async def import_stop_words(request: Request, stopWordsFile: UploadFile = File(. await authenticate_user(f'customJwtCookie={cookie}') file_content = await stopWordsFile.read() - words_list = file_content.decode('utf-8').split(',') + + words_list = [word.strip() for word in file_content.decode('utf-8').split(',')] url = 'http://localhost:8088/classifier/datasetgroup/update/stop-words' headers = { From 5dd788f94dd85ed513f1a3dd192662bcbf85b922 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Thu, 1 Aug 2024 23:12:00 +0530 Subject: [PATCH 342/582] delete stopwords and docker compose update --- docker-compose.yml | 2 ++ file-handler/file_handler_api.py | 44 +++++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 94497c6e..73eea7f2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -189,6 +189,8 @@ services: - UPLOAD_DIRECTORY=/shared - RUUTER_PRIVATE_URL=http://ruuter-private:8088 - S3_FERRY_URL=http://s3-ferry:3000/v1/files/copy + - IMPORT_STOPWORDS_URL=http://ruuter-private:8088/classifier/datasetgroup/update/stop-words + - DELETE_STOPWORDS_URL=http://ruuter-private:8088/classifier/datasetgroup/delete/stop-words ports: - "8000:8000" networks: diff --git a/file-handler/file_handler_api.py b/file-handler/file_handler_api.py index 6e540d7f..0aa8c783 100644 --- a/file-handler/file_handler_api.py +++ b/file-handler/file_handler_api.py @@ -27,6 +27,8 @@ CHUNK_UPLOAD_DIRECTORY = os.getenv("CHUNK_UPLOAD_DIRECTORY", "/shared/chunks") RUUTER_PRIVATE_URL = os.getenv("RUUTER_PRIVATE_URL") S3_FERRY_URL = os.getenv("S3_FERRY_URL") +IMPORT_STOPWORDS_URL = os.getenv("IMPORT_STOPWORDS_URL") +DELETE_STOPWORDS_URL = os.getenv("DELETE_STOPWORDS_URL") s3_ferry = S3Ferry(S3_FERRY_URL) class ExportFile(BaseModel): @@ -318,7 +320,7 @@ async def import_stop_words(request: Request, stopWordsFile: UploadFile = File(. words_list = [word.strip() for word in file_content.decode('utf-8').split(',')] - url = 'http://localhost:8088/classifier/datasetgroup/update/stop-words' + url = IMPORT_STOPWORDS_URL headers = { 'Content-Type': 'application/json', 'Cookie': f'customJwtCookie={cookie}' @@ -343,3 +345,43 @@ async def import_stop_words(request: Request, stopWordsFile: UploadFile = File(. except Exception as e: print(f"Error in import/stop-words: {e}") raise HTTPException(status_code=500, detail=str(e)) + +@app.post("/datasetgroup/data/delete/stop-words") +async def delete_stop_words(request: Request, stopWordsFile: UploadFile = File(...)): + try: + cookie = request.cookies.get("customJwtCookie") + await authenticate_user(f'customJwtCookie={cookie}') + + file_content = await stopWordsFile.read() + words_list = [word.strip() for word in file_content.decode('utf-8').split(',')] + + url = DELETE_STOPWORDS_URL + headers = { + 'Content-Type': 'application/json', + 'Cookie': f'customJwtCookie={cookie}' + } + + response = requests.post(url, headers=headers, json={"stopWords": words_list}) + + if response.status_code == 200: + response_data = response.json() + if response_data['operationSuccessful']: + return response_data + elif response_data['nonexistent']: + nonexistent_items = response_data['nonexistentItems'] + new_words_list = [word for word in words_list if word not in nonexistent_items] + if new_words_list: + response = requests.post(url, headers=headers, json={"stopWords": new_words_list}) + return response.json() + else: + return JSONResponse( + status_code=400, + content={ + "message": f"The following words are not in the list and cannot be deleted: {', '.join(nonexistent_items)}" + } + ) + else: + raise HTTPException(status_code=response.status_code, detail="Failed to delete stop words") + except Exception as e: + print(f"Error in delete/stop-words: {e}") + raise HTTPException(status_code=500, detail=str(e)) From e4356c6971ce60cef7896d6810d7257a6c261ae6 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Thu, 1 Aug 2024 23:29:11 +0530 Subject: [PATCH 343/582] added if to check and report --- dataset-processor/dataset_processor.py | 35 +++++++++++++------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/dataset-processor/dataset_processor.py b/dataset-processor/dataset_processor.py index 3d9ecb86..718de2f9 100644 --- a/dataset-processor/dataset_processor.py +++ b/dataset-processor/dataset_processor.py @@ -432,22 +432,26 @@ def process_handler(self, dgId, newDgId, cookie, updateType, savedFilePath, patc print(f"Page count retrieved successfully: {page_count}") print(chunked_data) copy_exsisting_files = self.copy_chunked_datafiles(dgId, newDgId, cookie, page_count) - operation_result = self.save_chunked_data(chunked_data, cookie, newDgId, page_count) - if operation_result is not None: - print("Chunked data for minor update saved successfully") - agregated_dataset += cleaned_data - agregated_dataset_operation = self.save_aggregrated_data(newDgId, cookie, agregated_dataset) - if agregated_dataset_operation: - print("Aggregated dataset for minor update saved successfully") - return_data = self.update_preprocess_status(newDgId, cookie, True, False, f"/dataset/{newDgId}/chunks/", "", True, len(agregated_dataset), (len(chunked_data)+page_count)) - print(return_data) - return SUCCESSFUL_OPERATION + if copy_exsisting_files is not None: + operation_result = self.save_chunked_data(chunked_data, cookie, newDgId, page_count) + if operation_result is not None: + print("Chunked data for minor update saved successfully") + agregated_dataset += cleaned_data + agregated_dataset_operation = self.save_aggregrated_data(newDgId, cookie, agregated_dataset) + if agregated_dataset_operation: + print("Aggregated dataset for minor update saved successfully") + return_data = self.update_preprocess_status(newDgId, cookie, True, False, f"/dataset/{newDgId}/chunks/", "", True, len(agregated_dataset), (len(chunked_data)+page_count)) + print(return_data) + return SUCCESSFUL_OPERATION + else: + print("Failed to save aggregated dataset for minor update") + return FAILED_TO_SAVE_AGGREGATED_DATA else: - print("Failed to save aggregated dataset for minor update") - return FAILED_TO_SAVE_AGGREGATED_DATA + print("Failed to save chunked data for minor update") + return FAILED_TO_SAVE_CHUNKED_DATA else: - print("Failed to save chunked data for minor update") - return FAILED_TO_SAVE_CHUNKED_DATA + print("Failed to copy existing chunked data for minor update") + return FAILED_TO_COPY_CHUNKED_DATA else: print("Failed to get page count") return FAILED_TO_GET_PAGE_COUNT @@ -478,8 +482,6 @@ def process_handler(self, dgId, newDgId, cookie, updateType, savedFilePath, patc elif updateType == "patch": decoded_string = urllib.parse.unquote(patchPayload) data_payload = json.loads(decoded_string) - print(data_payload) - print("*************") if (data_payload["editedData"]!=[]): print("Handling Patch update") stop_words = self.get_stopwords(dgId, cookie) @@ -535,7 +537,6 @@ def process_handler(self, dgId, newDgId, cookie, updateType, savedFilePath, patc save_result_update = self.save_aggregrated_data(dgId, cookie, agregated_dataset) if save_result_update: print("Aggregated dataset for patch update saved successfully") - # return SUCCESSFUL_OPERATION else: print("Failed to save aggregated dataset for patch update") return FAILED_TO_SAVE_AGGREGATED_DATA From b788af8ca0ad0eabe211fcb7c86df5dd2be3425c Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Thu, 1 Aug 2024 23:30:33 +0530 Subject: [PATCH 344/582] remove unwanted s3mock file --- dataset-processor/s3_mock.py | 49 ------------------------------------ 1 file changed, 49 deletions(-) delete mode 100644 dataset-processor/s3_mock.py diff --git a/dataset-processor/s3_mock.py b/dataset-processor/s3_mock.py deleted file mode 100644 index 8d3af534..00000000 --- a/dataset-processor/s3_mock.py +++ /dev/null @@ -1,49 +0,0 @@ -import os -# import boto3 -# from botocore.exceptions import NoCredentialsError, PartialCredentialsError - -class S3FileCounter: - def __init__(self): - self.s3_access_key_id = os.getenv('S3_ACCESS_KEY_ID') - self.s3_secret_access_key = os.getenv('S3_SECRET_ACCESS_KEY') - self.bucket_name = os.getenv('S3_BUCKET_NAME') - self.region_name = os.getenv('S3_REGION_NAME') - - if not all([self.s3_access_key_id, self.s3_secret_access_key, self.bucket_name, self.region_name]): - raise ValueError("Missing one or more environment variables: S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_BUCKET_NAME, S3_REGION_NAME") - - # self.s3_client = boto3.client( - # 's3', - # aws_access_key_id=self.s3_access_key_id, - # aws_secret_access_key=self.s3_secret_access_key, - # region_name=self.region_name - # ) - - def count_files_in_folder(self, folder_path): - try: - response = self.s3_client.list_objects_v2(Bucket=self.bucket_name, Prefix=folder_path) - if 'Contents' in response: - return len(response['Contents']) - else: - return 0 - # except NoCredentialsError: - # print("Credentials not available") - # return 0 - # except PartialCredentialsError: - # print("Incomplete credentials provided") - # return 0 - except Exception as e: - print(f"An error occurred: {e}") - return 20 - -# Example usage: -# Ensure the environment variables are set before running the script -# os.environ['S3_ACCESS_KEY_ID'] = 'your_access_key_id' -# os.environ['S3_SECRET_ACCESS_KEY'] = 'your_secret_access_key' -# os.environ['S3_BUCKET_NAME'] = 'your_bucket_name' -# os.environ['S3_REGION_NAME'] = 'your_region_name' - -# s3_file_counter = S3FileCounter() -# folder_path = 'your/folder/path/' -# file_count = s3_file_counter.count_files_in_folder(folder_path) -# print(f"Number of files in '{folder_path}': {file_count}") From 1991019fb01d6918e6f415e40178556d84c9c39d Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Thu, 1 Aug 2024 23:37:03 +0530 Subject: [PATCH 345/582] ESCLASS-187- Delete data set group and minor and maturity update on data models --- DSL/Resql/delete-dataset-group.sql | 2 + DSL/Resql/get-data-model-group-key-by-id.sql | 2 + DSL/Resql/get-dataset-group-by-id.sql | 2 + DSL/Resql/snapshot-minor-data-model.sql | 42 ++++++ DSL/Resql/update-data-model-dataset-group.sql | 5 + .../update-data-model-maturity-label.sql | 4 + .../datamodel/update/dataset-group.yml | 70 +++++++++ .../datamodel/update/maturity-label.yml | 75 ++++++++++ .../classifier/datamodel/update/minor.yml | 133 ++++++++++++++++++ .../POST/classifier/datasetgroup/delete.yml | 86 +++++++++++ .../datasetgroup/metadata/delete.yml | 91 ++++++++++++ 11 files changed, 512 insertions(+) create mode 100644 DSL/Resql/delete-dataset-group.sql create mode 100644 DSL/Resql/get-data-model-group-key-by-id.sql create mode 100644 DSL/Resql/get-dataset-group-by-id.sql create mode 100644 DSL/Resql/snapshot-minor-data-model.sql create mode 100644 DSL/Resql/update-data-model-dataset-group.sql create mode 100644 DSL/Resql/update-data-model-maturity-label.sql create mode 100644 DSL/Ruuter.private/DSL/POST/classifier/datamodel/update/dataset-group.yml create mode 100644 DSL/Ruuter.private/DSL/POST/classifier/datamodel/update/maturity-label.yml create mode 100644 DSL/Ruuter.private/DSL/POST/classifier/datamodel/update/minor.yml create mode 100644 DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/delete.yml create mode 100644 DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/metadata/delete.yml diff --git a/DSL/Resql/delete-dataset-group.sql b/DSL/Resql/delete-dataset-group.sql new file mode 100644 index 00000000..c898530a --- /dev/null +++ b/DSL/Resql/delete-dataset-group.sql @@ -0,0 +1,2 @@ +DELETE FROM dataset_group_metadata +WHERE id =:id; \ No newline at end of file diff --git a/DSL/Resql/get-data-model-group-key-by-id.sql b/DSL/Resql/get-data-model-group-key-by-id.sql new file mode 100644 index 00000000..bff582c8 --- /dev/null +++ b/DSL/Resql/get-data-model-group-key-by-id.sql @@ -0,0 +1,2 @@ +SELECT model_group_key, major_version +FROM models_metadata WHERE id =:id; \ No newline at end of file diff --git a/DSL/Resql/get-dataset-group-by-id.sql b/DSL/Resql/get-dataset-group-by-id.sql new file mode 100644 index 00000000..514b9075 --- /dev/null +++ b/DSL/Resql/get-dataset-group-by-id.sql @@ -0,0 +1,2 @@ +SELECT id +FROM dataset_group_metadata WHERE id =:id; \ No newline at end of file diff --git a/DSL/Resql/snapshot-minor-data-model.sql b/DSL/Resql/snapshot-minor-data-model.sql new file mode 100644 index 00000000..bae2aea8 --- /dev/null +++ b/DSL/Resql/snapshot-minor-data-model.sql @@ -0,0 +1,42 @@ +INSERT INTO models_metadata ( + model_group_key, + model_name, + major_version, + minor_version, + latest, + maturity_label, + deployment_env, + training_status, + base_models, + last_trained_timestamp, + created_timestamp, + connected_dg_id, + connected_dg_name, + model_s3_location, + inference_routes, + training_results +) +SELECT + model_group_key, + model_name, + major_version, + ( + SELECT COALESCE(MAX(minor_version), 0) + 1 + FROM dataset_group_metadata + WHERE group_key = :group_key AND major_version = :major_version + ) AS minor_version, + true AS latest, + 'development'::Maturity_Label AS maturity_label, + :deployment_env::Deployment_Env, + 'not trained'::Training_Status AS training_status, + ARRAY [:base_models]::Base_Models[], + last_trained_timestamp, + created_timestamp, + connected_dg_id, + connected_dg_name, + model_s3_location, + NULL, + NULL +FROM models_metadata +WHERE id = :id +RETURNING id; diff --git a/DSL/Resql/update-data-model-dataset-group.sql b/DSL/Resql/update-data-model-dataset-group.sql new file mode 100644 index 00000000..b60ab387 --- /dev/null +++ b/DSL/Resql/update-data-model-dataset-group.sql @@ -0,0 +1,5 @@ +UPDATE models_metadata +SET + connected_dg_name = null, + connected_dg_id = null +WHERE connected_dg_id = :id; diff --git a/DSL/Resql/update-data-model-maturity-label.sql b/DSL/Resql/update-data-model-maturity-label.sql new file mode 100644 index 00000000..0e751b63 --- /dev/null +++ b/DSL/Resql/update-data-model-maturity-label.sql @@ -0,0 +1,4 @@ +UPDATE models_metadata +SET + maturity_label = :maturity_label::Maturity_Label +WHERE id = :id; diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update/dataset-group.yml b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update/dataset-group.yml new file mode 100644 index 00000000..f151807b --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update/dataset-group.yml @@ -0,0 +1,70 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'DATASET-GROUP'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: dgId + type: number + description: "Body field 'dgId'" + +extract_request_data: + assign: + dg_id: ${incoming.body.dgId} + next: check_for_request_data + +check_for_request_data: + switch: + - condition: ${dg_id !== null} + next: update_dataset_group_models + next: return_incorrect_request + +update_dataset_group_models: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/update-data-model-dataset-group" + body: + id: ${dg_id} + result: res_update + next: check_dataset_model_update_status + +check_dataset_model_update_status: + switch: + - condition: ${200 <= res_update.response.statusCodeValue && res_update.response.statusCodeValue < 300} + next: assign_success_response + next: assign_fail_response + +assign_success_response: + assign: + format_res: { + dgId: '${dg_id}', + operationSuccessful: true, + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + dgId: '', + operationSuccessful: false, + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_incorrect_request: + status: 400 + return: 'Missing Required Fields' + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update/maturity-label.yml b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update/maturity-label.yml new file mode 100644 index 00000000..cc280981 --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update/maturity-label.yml @@ -0,0 +1,75 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'MATURITY-LABEL'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: modelId + type: number + description: "Body field 'modelId'" + - field: maturityLabel + type: string + description: "Body field 'maturityLabel'" + +extract_request_data: + assign: + model_id: ${incoming.body.modelId} + maturity_label: ${incoming.body.maturityLabel} + next: check_for_request_data + +check_for_request_data: + switch: + - condition: ${model_id !== null && maturity_label !== null} + next: update_data_model + next: return_incorrect_request + +update_data_model: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/update-data-model-maturity-label" + body: + id: ${model_id} + maturity_label: ${maturity_label} + result: res_update + next: check_model_update_status + +check_model_update_status: + switch: + - condition: ${200 <= res_update.response.statusCodeValue && res_update.response.statusCodeValue < 300} + next: assign_success_response + next: assign_fail_response + +assign_success_response: + assign: + format_res: { + modelId: '${model_id}', + operationSuccessful: true, + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + modelId: '', + operationSuccessful: false, + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_incorrect_request: + status: 400 + return: 'Missing Required Fields' + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update/minor.yml b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update/minor.yml new file mode 100644 index 00000000..7fdb2e55 --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update/minor.yml @@ -0,0 +1,133 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'MINOR'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: modelId + type: number + description: "Body field 'modelId'" + - field: deploymentEnv + type: string + description: "Body field 'deploymentEnv'" + - field: baseModels + type: array + description: "Body field 'baseModels'" + headers: + - field: cookie + type: string + description: "Cookie field" + +extract_request_data: + assign: + model_id: ${incoming.body.modelId} + deployment_env: ${incoming.body.deploymentEnv} + base_models: ${incoming.body.baseModels} + next: get_data_model + +get_data_model: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-data-model-group-key-by-id" + body: + id: ${model_id} + result: res + next: check_data_model_status + +check_data_model_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: check_data_exist + next: return_not_found + +check_data_exist: + switch: + - condition: ${res.response.body.length>0} + next: assign_group_key + next: return_not_found + +assign_group_key: + assign: + group_key: ${res.response.body[0].modelGroupKey} + major_version: ${res.response.body[0].majorVersion} + next: snapshot_dataset_group + +snapshot_dataset_group: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/snapshot-minor-data-model" + body: + id: ${model_id} + group_key: ${group_key} + major_version: ${major_version} + deployment_env: ${deployment_env} + base_models: ${base_models} + result: res + next: check_snapshot_status + +check_snapshot_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: check_updated_data_exist + next: assign_fail_response + +check_updated_data_exist: + switch: + - condition: ${res.response.body.length>0} + next: assign_new_model_id + next: return_not_found + +assign_new_model_id: + assign: + new_model_id: ${res.response.body[0].id} + next: execute_cron_manager + +execute_cron_manager: + call: reflect.mock + args: + url: "[#CLASSIFIER_CRON_MANAGER]/execute/data_model/model_trainer" + query: + cookie: ${incoming.headers.cookie} + model_id: ${model_id} + new_model_id: ${new_model_id} + updateType: 'minor' + result: res + next: assign_success_response + +assign_success_response: + assign: + format_res: { + model_id: '${model_id}', + new_model_id: '${new_model_id}', + operationSuccessful: true, + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + model_id: '${model_id}', + new_model_id: '', + operationSuccessful: false, + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_not_found: + status: 400 + return: "Data Group Not Found" + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end + diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/delete.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/delete.yml new file mode 100644 index 00000000..92b1ab32 --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/delete.yml @@ -0,0 +1,86 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'DELETE'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: dgId + type: number + description: "Body field 'dgId'" + +extract_request_data: + assign: + dg_id: ${incoming.body.dgId} + next: check_for_request_data + +check_for_request_data: + switch: + - condition: ${dg_id !== null} + next: get_dataset_group_by_id + next: return_incorrect_request + +get_dataset_group_by_id: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-dataset-group-by-id" + body: + id: ${dg_id} + result: res_dataset + next: check_dataset_fields_status + +check_dataset_fields_status: + switch: + - condition: ${200 <= res_dataset.response.statusCodeValue && res_dataset.response.statusCodeValue < 300} + next: check_dataset_data_exist + next: assign_fail_response + +check_dataset_data_exist: + switch: + - condition: ${res_dataset.response.body.length>0} + next: execute_cron_manager + next: assign_fail_response + +execute_cron_manager: + call: reflect.mock + args: + url: "[#CLASSIFIER_CRON_MANAGER]/execute/dataset_processing/dataset_deletion" + query: + cookie: ${incoming.headers.cookie} + dgId: ${dg_id} + result: res + next: assign_success_response + +assign_success_response: + assign: + format_res: { + dgId: '${dg_id}', + operationSuccessful: true, + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + dgId: '', + operationSuccessful: false, + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_incorrect_request: + status: 400 + return: 'Missing Required Fields' + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/metadata/delete.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/metadata/delete.yml new file mode 100644 index 00000000..05714123 --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/metadata/delete.yml @@ -0,0 +1,91 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'DELETE'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: dgId + type: number + description: "Body field 'dgId'" + +extract_request_data: + assign: + dg_id: ${incoming.body.dgId} + next: check_for_request_data + +check_for_request_data: + switch: + - condition: ${dg_id !== null} + next: get_dataset_group_by_id + next: return_incorrect_request + +get_dataset_group_by_id: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-dataset-group-by-id" + body: + id: ${dg_id} + result: res_dataset + next: check_dataset_fields_status + +check_dataset_fields_status: + switch: + - condition: ${200 <= res_dataset.response.statusCodeValue && res_dataset.response.statusCodeValue < 300} + next: check_dataset_data_exist + next: assign_fail_response + +check_dataset_data_exist: + switch: + - condition: ${res_dataset.response.body.length>0} + next: delete_dataset_group + next: assign_fail_response + +delete_dataset_group: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/delete-dataset-group" + body: + id: ${dg_id} + result: res_delete + next: check_dataset_delete_status + +check_dataset_delete_status: + switch: + - condition: ${200 <= res_delete.response.statusCodeValue && res_delete.response.statusCodeValue < 300} + next: assign_success_response + next: assign_fail_response + +assign_success_response: + assign: + format_res: { + dgId: '${dg_id}', + operationSuccessful: true, + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + dgId: '', + operationSuccessful: false, + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_incorrect_request: + status: 400 + return: 'Missing Required Fields' + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end \ No newline at end of file From 11c6288aa69196074c6e36a149d51174a74e68e7 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Thu, 1 Aug 2024 23:37:19 +0530 Subject: [PATCH 346/582] constant update --- dataset-processor/constants.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dataset-processor/constants.py b/dataset-processor/constants.py index 3343b1fb..1a26f8fe 100644 --- a/dataset-processor/constants.py +++ b/dataset-processor/constants.py @@ -10,6 +10,12 @@ "reason": "Failed to save chunked data into S3" } +FAILED_TO_COPY_CHUNKED_DATA = { + "operation_status": 500, + "operation_successful": False, + "reason": "Failed to copy existing chunked data in S3" +} + FAILED_TO_CHUNK_CLEANED_DATA = { "operation_status": 500, "operation_successful": False, From 544a34e70019d5256be5b2a88c684615688c61b2 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 2 Aug 2024 10:09:23 +0530 Subject: [PATCH 347/582] data enrichment sperate container update --- .../Dockerfile | 6 +- .../config_files/paraphraser_config.json | 0 .../config_files/translator_config.json | 0 .../data_enrichment.py | 6 +- .../data_enrichment_api.py | 6 +- data_enrichment/download_models.py | 27 +++++ .../enrichment_requirements.txt | 0 .../paraphraser.py | 2 +- .../test_data_enrichment.py | 0 .../translator.py | 2 +- dataset-processor/dataset_processor.py | 111 +++++++++++------- docker-compose.yml | 14 ++- 12 files changed, 119 insertions(+), 55 deletions(-) rename {dataset-processor/data_enrichment => data_enrichment}/Dockerfile (55%) rename {dataset-processor/data_enrichment => data_enrichment}/config_files/paraphraser_config.json (100%) rename {dataset-processor/data_enrichment => data_enrichment}/config_files/translator_config.json (100%) rename {dataset-processor/data_enrichment => data_enrichment}/data_enrichment.py (90%) rename {dataset-processor/data_enrichment => data_enrichment}/data_enrichment_api.py (85%) create mode 100644 data_enrichment/download_models.py rename {dataset-processor/data_enrichment => data_enrichment}/enrichment_requirements.txt (100%) rename {dataset-processor/data_enrichment => data_enrichment}/paraphraser.py (94%) rename {dataset-processor/data_enrichment => data_enrichment}/test_data_enrichment.py (100%) rename {dataset-processor/data_enrichment => data_enrichment}/translator.py (95%) diff --git a/dataset-processor/data_enrichment/Dockerfile b/data_enrichment/Dockerfile similarity index 55% rename from dataset-processor/data_enrichment/Dockerfile rename to data_enrichment/Dockerfile index 47b9a5e9..5c349730 100644 --- a/dataset-processor/data_enrichment/Dockerfile +++ b/data_enrichment/Dockerfile @@ -8,6 +8,8 @@ RUN pip install --no-cache-dir -r enrichment_requirements.txt COPY . . -EXPOSE 8002 +RUN python download_models.py -CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8002"] +EXPOSE 8005 + +CMD ["uvicorn", "data_enrichment_api:app", "--host", "0.0.0.0", "--port", "8005"] diff --git a/dataset-processor/data_enrichment/config_files/paraphraser_config.json b/data_enrichment/config_files/paraphraser_config.json similarity index 100% rename from dataset-processor/data_enrichment/config_files/paraphraser_config.json rename to data_enrichment/config_files/paraphraser_config.json diff --git a/dataset-processor/data_enrichment/config_files/translator_config.json b/data_enrichment/config_files/translator_config.json similarity index 100% rename from dataset-processor/data_enrichment/config_files/translator_config.json rename to data_enrichment/config_files/translator_config.json diff --git a/dataset-processor/data_enrichment/data_enrichment.py b/data_enrichment/data_enrichment.py similarity index 90% rename from dataset-processor/data_enrichment/data_enrichment.py rename to data_enrichment/data_enrichment.py index a9ff0d8f..902edc85 100644 --- a/dataset-processor/data_enrichment/data_enrichment.py +++ b/data_enrichment/data_enrichment.py @@ -1,5 +1,5 @@ -from data_enrichment.translator import Translator -from data_enrichment.paraphraser import Paraphraser +from translator import Translator +from paraphraser import Paraphraser from langdetect import detect from typing import List, Optional @@ -8,7 +8,7 @@ def __init__(self): self.translator = Translator() self.paraphraser = Paraphraser() - def enrich_data(self, text: str, num_return_sequences: int = None, language_id: Optional[str] = None) -> List[str]: + def enrich_data(self, text: str, num_return_sequences: int = 1, language_id: Optional[str] = None) -> List[str]: supported_languages = ['en', 'et', 'ru', 'pl', 'fi'] if language_id: diff --git a/dataset-processor/data_enrichment/data_enrichment_api.py b/data_enrichment/data_enrichment_api.py similarity index 85% rename from dataset-processor/data_enrichment/data_enrichment_api.py rename to data_enrichment/data_enrichment_api.py index 1e3a91ce..1147aade 100644 --- a/dataset-processor/data_enrichment/data_enrichment_api.py +++ b/data_enrichment/data_enrichment_api.py @@ -1,6 +1,6 @@ from fastapi import FastAPI, HTTPException from pydantic import BaseModel -from data_enrichment.data_enrichment import DataEnrichment +from data_enrichment import DataEnrichment from typing import List, Optional app = FastAPI() @@ -8,7 +8,7 @@ class ParaphraseRequest(BaseModel): text: str - num_return_sequences: Optional[int] = None + num_return_sequences: Optional[int] = 1 language_id: Optional[str] = None class ParaphraseResponse(BaseModel): @@ -30,4 +30,4 @@ def paraphrase(request: ParaphraseRequest): if __name__ == "__main__": import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8500) + uvicorn.run(app, host="0.0.0.0", port=8005) diff --git a/data_enrichment/download_models.py b/data_enrichment/download_models.py new file mode 100644 index 00000000..bf710c10 --- /dev/null +++ b/data_enrichment/download_models.py @@ -0,0 +1,27 @@ +from transformers import MarianMTModel, MarianTokenizer, AutoTokenizer, AutoModelForSeq2SeqLM +import json + +def download_translator_models(config_path="config_files/translator_config.json"): + with open(config_path, 'r') as file: + config = json.load(file) + models = config["models"] + unsupported_model = config["unsupported-en-pl_model"] + + for key, (model_name, reverse_model_name) in models.items(): + MarianTokenizer.from_pretrained(model_name) + MarianMTModel.from_pretrained(model_name) + reverse_key = f"{key.split('-')[1]}-{key.split('-')[0]}" + if reverse_model_name != unsupported_model: + MarianTokenizer.from_pretrained(reverse_model_name) + MarianMTModel.from_pretrained(reverse_model_name) + +def download_paraphraser_model(config_path="config_files/paraphraser_config.json"): + with open(config_path, 'r') as file: + config = json.load(file) + model_name = config["model_name"] + AutoTokenizer.from_pretrained(model_name) + AutoModelForSeq2SeqLM.from_pretrained(model_name) + +if __name__ == "__main__": + download_translator_models() + download_paraphraser_model() diff --git a/dataset-processor/data_enrichment/enrichment_requirements.txt b/data_enrichment/enrichment_requirements.txt similarity index 100% rename from dataset-processor/data_enrichment/enrichment_requirements.txt rename to data_enrichment/enrichment_requirements.txt diff --git a/dataset-processor/data_enrichment/paraphraser.py b/data_enrichment/paraphraser.py similarity index 94% rename from dataset-processor/data_enrichment/paraphraser.py rename to data_enrichment/paraphraser.py index b671ebef..5dc15bcc 100644 --- a/dataset-processor/data_enrichment/paraphraser.py +++ b/data_enrichment/paraphraser.py @@ -3,7 +3,7 @@ from typing import List class Paraphraser: - def __init__(self, config_path: str = "data_enrichment/config_files/paraphraser_config.json"): + def __init__(self, config_path: str = "config_files/paraphraser_config.json"): with open(config_path, 'r') as file: config = json.load(file) diff --git a/dataset-processor/data_enrichment/test_data_enrichment.py b/data_enrichment/test_data_enrichment.py similarity index 100% rename from dataset-processor/data_enrichment/test_data_enrichment.py rename to data_enrichment/test_data_enrichment.py diff --git a/dataset-processor/data_enrichment/translator.py b/data_enrichment/translator.py similarity index 95% rename from dataset-processor/data_enrichment/translator.py rename to data_enrichment/translator.py index a010571e..dd8470e7 100644 --- a/dataset-processor/data_enrichment/translator.py +++ b/data_enrichment/translator.py @@ -3,7 +3,7 @@ from typing import Dict, Tuple class Translator: - def __init__(self, config_path: str = "data_enrichment/config_files/translator_config.json"): + def __init__(self, config_path: str = "config_files/translator_config.json"): with open(config_path, 'r') as file: config = json.load(file) diff --git a/dataset-processor/dataset_processor.py b/dataset-processor/dataset_processor.py index 718de2f9..554055eb 100644 --- a/dataset-processor/dataset_processor.py +++ b/dataset-processor/dataset_processor.py @@ -3,13 +3,12 @@ import json import urllib.parse import requests -# from data_enrichment.data_enrichment import DataEnrichment from constants import * RUUTER_PRIVATE_URL = os.getenv("RUUTER_PRIVATE_URL") GET_VALIDATION_SCHEMA = os.getenv("GET_VALIDATION_SCHEMA") FILE_HANDLER_DOWNLOAD_JSON_URL = os.getenv("FILE_HANDLER_DOWNLOAD_JSON_URL") -FILE_HANDLER_STOPWORDS_URL = os.getenv("FILE_HANDLER_STOPWORDS_URL") +GET_STOPWORDS_URL = os.getenv("GET_STOPWORDS_URL") FILE_HANDLER_IMPORT_CHUNKS_URL = os.getenv("FILE_HANDLER_IMPORT_CHUNKS_URL") FILE_HANDLER_DOWNLOAD_LOCATION_JSON_URL = os.getenv("FILE_HANDLER_DOWNLOAD_LOCATION_JSON_URL") GET_PAGE_COUNT_URL = os.getenv("GET_PAGE_COUNT_URL") @@ -17,10 +16,10 @@ DOWNLOAD_CHUNK_URL = os.getenv("DOWNLOAD_CHUNK_URL") STATUS_UPDATE_URL = os.getenv("STATUS_UPDATE_URL") FILE_HANDLER_COPY_CHUNKS_URL = os.getenv("FILE_HANDLER_COPY_CHUNKS_URL") +PARAPHRASE_API_URL = os.getenv("PARAPHRASE_API_URL") class DatasetProcessor: def __init__(self): - # self.data_enricher = DataEnrichment() pass def check_and_convert(self, data): @@ -44,7 +43,7 @@ def _is_multple_sheet_structure(self, data): return True return False - def _is_single_sheet_structure(self,data): + def _is_single_sheet_structure(self, data): if isinstance(data, list): for item in data: if not isinstance(item, dict) or len(item) <= 1: @@ -52,50 +51,54 @@ def _is_single_sheet_structure(self,data): return True return False - def _convert_to_single_sheet_structure(self, data): result = [] for value in data.values(): result.extend(value) return result - def remove_stop_words(self, data, stop_words): - try: - stop_words_set = set(stop_words) - stop_words_pattern = re.compile(r'\b(' + r'|'.join(re.escape(word) for word in stop_words_set) + r')\b', re.IGNORECASE) - - def clean_text(text): - return stop_words_pattern.sub('', text).strip() - - cleaned_data = [] - for entry in data: - cleaned_entry = {key: clean_text(value) if isinstance(value, str) else value for key, value in entry.items()} - cleaned_data.append(cleaned_entry) - - return cleaned_data - except Exception as e: - print(f"Error while removing Stop Words : {e}") - return None - def enrich_data(self, data, selected_fields, record_count): try: enriched_data = [] for entry in data: enriched_entry = {} + enrich_server = True for key, value in entry.items(): if isinstance(value, str) and (key in selected_fields): - # enriched_value = self.data_enricher.enrich_data(value, num_return_sequences=1, language_id='en') - enriched_value = ["enrichupdate"] - enriched_entry[key] = enriched_value[0] if enriched_value else value + enriched_value = self._get_paraphrases(value) + if enriched_value != []: + enriched_entry[key] = enriched_value[0] + else: + enrich_server = False + break else: enriched_entry[key] = value - record_count = record_count+1 - enriched_entry["rowId"] = record_count - enriched_data.append(enriched_entry) + if enrich_server: + record_count += 1 + enriched_entry["rowId"] = record_count + enriched_data.append(enriched_entry) return enriched_data except Exception as e: - print(f"Internal Error occured while data enrichment : {e}") + print(f"Internal Error occurred while data enrichment : {e}") return None + + def _get_paraphrases(self, text): + payload = { + "text": text, + "num_return_sequences": 1 + } + try: + response = requests.post(PARAPHRASE_API_URL, json=payload) + if response.status_code == 200: + paraphrases = response.json().get("paraphrases", []) + return paraphrases + else: + print(f"Failed to get paraphrases: {response.status_code}, {response.text}") + return [] + except Exception as e: + print(f"Error calling paraphrase API: {e}") + return [] + def chunk_data(self, data, chunk_size=5): try: @@ -213,20 +216,40 @@ def get_dataset_by_location(self, fileLocation, custom_jwt_cookie): print(f"An error occurred: {e}") return None - def get_stopwords(self, dg_id, custom_jwt_cookie): - # params = {'dgId': dg_id} - # headers = { - # 'cookie': f'customJwtCookie={custom_jwt_cookie}' - # } - - # try: - # response = requests.get(FILE_HANDLER_STOPWORDS_URL, params=params, headers=headers) - # response.raise_for_status() - # return response.json() + def get_stopwords(self, custom_jwt_cookie): + headers = { + 'Cookie': f'customJwtCookie={custom_jwt_cookie}' + } + try: - return {"is","her","okay"} + response = requests.get(self.GET_STOPWORDS_URL, headers=headers) + response.raise_for_status() + response_data = response.json() + if response_data.get("operationSuccessful", False): + return response_data.get("stopwords", []) + else: + return [] except requests.exceptions.RequestException as e: print(f"An error occurred: {e}") + return [] + + + def remove_stop_words(self, data, stop_words): + try: + stop_words_set = set(stop_words) + stop_words_pattern = re.compile(r'\b(' + r'|'.join(re.escape(word) for word in stop_words_set) + r')\b', re.IGNORECASE) + + def clean_text(text): + return stop_words_pattern.sub('', text).strip() + + cleaned_data = [] + for entry in data: + cleaned_entry = {key: clean_text(value) if isinstance(value, str) else value for key, value in entry.items()} + cleaned_data.append(cleaned_entry) + + return cleaned_data + except Exception as e: + print(f"Error while removing Stop Words : {e}") return None def get_page_count(self, dg_id, custom_jwt_cookie): @@ -349,7 +372,7 @@ def process_handler(self, dgId, newDgId, cookie, updateType, savedFilePath, patc if enriched_data is not None: print("Data enrichment successful") - stop_words = self.get_stopwords(newDgId, cookie) + stop_words = self.get_stopwords(cookie) if stop_words is not None: print("Stop words retrieved successfully") print(agregated_dataset) @@ -417,7 +440,7 @@ def process_handler(self, dgId, newDgId, cookie, updateType, savedFilePath, patc enriched_data = self.enrich_data(structured_data, selected_data_fields_to_enrich, max_row_id) if enriched_data is not None: print("Minor update data enrichment successful") - stop_words = self.get_stopwords(newDgId, cookie) + stop_words = self.get_stopwords(cookie) if stop_words is not None: combined_new_dataset = structured_data + enriched_data print("Stop words for minor update retrieved successfully") @@ -484,7 +507,7 @@ def process_handler(self, dgId, newDgId, cookie, updateType, savedFilePath, patc data_payload = json.loads(decoded_string) if (data_payload["editedData"]!=[]): print("Handling Patch update") - stop_words = self.get_stopwords(dgId, cookie) + stop_words = self.get_stopwords(cookie) if stop_words is not None: print("Stop words for patch update retrieved successfully") cleaned_patch_payload = self.remove_stop_words(data_payload["editedData"], stop_words) diff --git a/docker-compose.yml b/docker-compose.yml index 73eea7f2..bfe7bcd6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -226,7 +226,7 @@ services: - GET_VALIDATION_SCHEMA=http://ruuter-private:8088/classifier/datasetgroup/schema - FILE_HANDLER_DOWNLOAD_JSON_URL=http://file-handler:8000/datasetgroup/data/download/json - FILE_HANDLER_DOWNLOAD_LOCATION_JSON_URL=http://file-handler:8000/datasetgroup/data/download/json/location - - FILE_HANDLER_STOPWORDS_URL=http://file-handler:8000/datasetgroup/data/download/json/stopwords + - GET_STOPWORDS_URL=http://ruuter-private:8088/classifier/datasetgroup/stop-words - FILE_HANDLER_IMPORT_CHUNKS_URL=http://file-handler:8000/datasetgroup/data/import/chunk - FILE_HANDLER_COPY_CHUNKS_URL=http://file-handler:8000/datasetgroup/data/copy - GET_PAGE_COUNT_URL=http://ruuter-private:8088/classifier/datasetgroup/group/data?groupId=dgId&pageNum=1 @@ -237,6 +237,7 @@ services: - S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY} - S3_BUCKET_NAME=esclassifier-test - S3_REGION_NAME=eu-west-1 + - PARAPHRASE_API_URL=http://data-enrichment-api:8005/paraphrase ports: - "8001:8001" networks: @@ -307,6 +308,17 @@ services: networks: - bykstack + data-enrichment-api: + container_name: data-enrichment-api + image: data-enrichment-api + build: + context: ./data_enrichment + dockerfile: Dockerfile + ports: + - "8005:8005" + networks: + - bykstack + volumes: shared-volume: opensearch-data: From 2427fb310b6c07f7f3b1cd3de737c8bf21ec1cd4 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Fri, 2 Aug 2024 14:22:57 +0530 Subject: [PATCH 348/582] Stop-word-issue: stop word bug on data empty fixed --- .../DSL/GET/classifier/datasetgroup/stop-words.yml | 6 ++++++ .../POST/classifier/datasetgroup/delete/stop-words.yml | 6 ++++++ .../POST/classifier/datasetgroup/update/stop-words.yml | 8 +++++++- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/stop-words.yml b/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/stop-words.yml index 0b96b509..41538a7d 100644 --- a/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/stop-words.yml +++ b/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/stop-words.yml @@ -17,6 +17,12 @@ get_stop_words: check_status: switch: - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: check_data_exist + next: assign_fail_response + +check_data_exist: + switch: + - condition: ${res.response.body.length>0 && res.response.body[0].stopWordsArray !== null} next: assign_success_response next: assign_fail_response diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/delete/stop-words.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/delete/stop-words.yml index c73e7582..2dfb8ac8 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/delete/stop-words.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/delete/stop-words.yml @@ -33,6 +33,12 @@ get_stop_words: check_stop-words_status: switch: - condition: ${200 <= res_stop_words.response.statusCodeValue && res_stop_words.response.statusCodeValue < 300} + next: check_data_exist + next: assign_fail_response + +check_data_exist: + switch: + - condition: ${res_stop_words.response.body.length>0 && res_stop_words.response.body[0].stopWordsArray !== null} next: get_not_existing_stop_words next: assign_fail_response diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/stop-words.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/stop-words.yml index c84ce30b..ded16e0b 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/stop-words.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/stop-words.yml @@ -33,9 +33,15 @@ get_stop_words: check_stop-words_status: switch: - condition: ${200 <= res_stop_words.response.statusCodeValue && res_stop_words.response.statusCodeValue < 300} - next: get_duplicate_stop_words + next: check_data_exist next: assign_fail_response +check_data_exist: + switch: + - condition: ${res_stop_words.response.body.length>0 && res_stop_words.response.body[0].stopWordsArray !== null} + next: get_duplicate_stop_words + next: insert_stop_words + get_duplicate_stop_words: call: http.post args: From 07dd8a0d1babdec14a4230ba6c9b066a9f2dca9f Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 2 Aug 2024 16:15:28 +0530 Subject: [PATCH 349/582] Docker file update --- data_enrichment/Dockerfile | 2 -- 1 file changed, 2 deletions(-) diff --git a/data_enrichment/Dockerfile b/data_enrichment/Dockerfile index 5c349730..e8cb5688 100644 --- a/data_enrichment/Dockerfile +++ b/data_enrichment/Dockerfile @@ -8,8 +8,6 @@ RUN pip install --no-cache-dir -r enrichment_requirements.txt COPY . . -RUN python download_models.py - EXPOSE 8005 CMD ["uvicorn", "data_enrichment_api:app", "--host", "0.0.0.0", "--port", "8005"] From 623efbc443ff0d8ef298bdad6b371bcdf65cabbd Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Fri, 2 Aug 2024 22:31:44 +0530 Subject: [PATCH 350/582] ESCLASS-162: model create remain logic implementation adn model update API implementation --- .../get-data-model-basic-metadata-by-id.sql | 7 + DSL/Resql/get-data-model-major-data.sql | 2 + DSL/Resql/get-data-model-minor-data.sql | 2 + DSL/Resql/snapshot-major-data-model.sql | 42 ++ DSL/Resql/snapshot-minor-data-model.sql | 14 +- DSL/Resql/snapshot-minor-dataset-group.sql | 4 +- .../update-data-model-deployment-env.sql | 4 + .../update-dataset-group-connected-models.sql | 7 + .../update-latest-version-data-model.sql | 3 + .../DSL/POST/classifier/datamodel/create.yml | 63 ++- .../DSL/POST/classifier/datamodel/update.yml | 380 ++++++++++++++++++ 11 files changed, 517 insertions(+), 11 deletions(-) create mode 100644 DSL/Resql/get-data-model-basic-metadata-by-id.sql create mode 100644 DSL/Resql/get-data-model-major-data.sql create mode 100644 DSL/Resql/get-data-model-minor-data.sql create mode 100644 DSL/Resql/snapshot-major-data-model.sql create mode 100644 DSL/Resql/update-data-model-deployment-env.sql create mode 100644 DSL/Resql/update-dataset-group-connected-models.sql create mode 100644 DSL/Resql/update-latest-version-data-model.sql create mode 100644 DSL/Ruuter.private/DSL/POST/classifier/datamodel/update.yml diff --git a/DSL/Resql/get-data-model-basic-metadata-by-id.sql b/DSL/Resql/get-data-model-basic-metadata-by-id.sql new file mode 100644 index 00000000..ea3673de --- /dev/null +++ b/DSL/Resql/get-data-model-basic-metadata-by-id.sql @@ -0,0 +1,7 @@ +SELECT + id AS model_id, + model_name, + major_version, + minor_version +FROM models_metadata +WHERE id = :id; diff --git a/DSL/Resql/get-data-model-major-data.sql b/DSL/Resql/get-data-model-major-data.sql new file mode 100644 index 00000000..15ed959a --- /dev/null +++ b/DSL/Resql/get-data-model-major-data.sql @@ -0,0 +1,2 @@ +SELECT model_group_key, deployment_env, base_models, maturity_label +FROM models_metadata WHERE id =:id; \ No newline at end of file diff --git a/DSL/Resql/get-data-model-minor-data.sql b/DSL/Resql/get-data-model-minor-data.sql new file mode 100644 index 00000000..0c513ef4 --- /dev/null +++ b/DSL/Resql/get-data-model-minor-data.sql @@ -0,0 +1,2 @@ +SELECT model_group_key, major_version, deployment_env, base_models, maturity_label +FROM models_metadata WHERE id =:id; \ No newline at end of file diff --git a/DSL/Resql/snapshot-major-data-model.sql b/DSL/Resql/snapshot-major-data-model.sql new file mode 100644 index 00000000..058a6413 --- /dev/null +++ b/DSL/Resql/snapshot-major-data-model.sql @@ -0,0 +1,42 @@ +INSERT INTO models_metadata ( + model_group_key, + model_name, + major_version, + minor_version, + latest, + maturity_label, + deployment_env, + training_status, + base_models, + last_trained_timestamp, + created_timestamp, + connected_dg_id, + connected_dg_name, + model_s3_location, + inference_routes, + training_results +) +SELECT + model_group_key, + model_name, + ( + SELECT COALESCE(MAX(major_version), 0) + 1 + FROM models_metadata + WHERE model_group_key = :group_key + ) AS major_version, + 0 AS minor_version, + true AS latest, + :maturity_label::Maturity_Label, + :deployment_env::Deployment_Env, + 'not trained'::Training_Status AS training_status, + ARRAY [:base_models]::Base_Models[], + NULL AS last_trained_timestamp, + created_timestamp, + :connected_dg_id, + :connected_dg_name, + NULL AS model_s3_location, + NULL AS inference_routes, + NULL AS training_results +FROM models_metadata +WHERE id = :id +RETURNING id; diff --git a/DSL/Resql/snapshot-minor-data-model.sql b/DSL/Resql/snapshot-minor-data-model.sql index bae2aea8..90568453 100644 --- a/DSL/Resql/snapshot-minor-data-model.sql +++ b/DSL/Resql/snapshot-minor-data-model.sql @@ -22,21 +22,21 @@ SELECT major_version, ( SELECT COALESCE(MAX(minor_version), 0) + 1 - FROM dataset_group_metadata - WHERE group_key = :group_key AND major_version = :major_version + FROM models_metadata + WHERE model_group_key = :group_key AND major_version = :major_version ) AS minor_version, true AS latest, - 'development'::Maturity_Label AS maturity_label, + :maturity_label::Maturity_Label, :deployment_env::Deployment_Env, 'not trained'::Training_Status AS training_status, ARRAY [:base_models]::Base_Models[], - last_trained_timestamp, + NULL AS last_trained_timestamp, created_timestamp, connected_dg_id, connected_dg_name, - model_s3_location, - NULL, - NULL + NULL AS model_s3_location, + NULL AS inference_routes, + NULL AS training_results FROM models_metadata WHERE id = :id RETURNING id; diff --git a/DSL/Resql/snapshot-minor-dataset-group.sql b/DSL/Resql/snapshot-minor-dataset-group.sql index fd72ba0a..e6f8ae99 100644 --- a/DSL/Resql/snapshot-minor-dataset-group.sql +++ b/DSL/Resql/snapshot-minor-dataset-group.sql @@ -16,8 +16,8 @@ SELECT 0 AS patch_version, true AS latest, false AS is_enabled, false AS enable_allowed, last_model_trained, created_timestamp, CURRENT_TIMESTAMP AS last_updated_timestamp, last_trained_timestamp, - 'in-progress'::Validation_Status AS validation_status, validation_errors, processed_data_available, - raw_data_available, num_samples, num_pages, raw_data_location, preprocess_data_location, + 'in-progress'::Validation_Status AS validation_status, NULL AS validation_errors, false AS processed_data_available, + false AS raw_data_available, 0 AS num_samples, 0 AS num_pages, NULL AS raw_data_location, NULL AS preprocess_data_location, validation_criteria, class_hierarchy, connected_models FROM dataset_group_metadata WHERE id = :id diff --git a/DSL/Resql/update-data-model-deployment-env.sql b/DSL/Resql/update-data-model-deployment-env.sql new file mode 100644 index 00000000..ff27147f --- /dev/null +++ b/DSL/Resql/update-data-model-deployment-env.sql @@ -0,0 +1,4 @@ +UPDATE models_metadata +SET + deployment_env = :deployment_env::Deployment_Env +WHERE id = :id; diff --git a/DSL/Resql/update-dataset-group-connected-models.sql b/DSL/Resql/update-dataset-group-connected-models.sql new file mode 100644 index 00000000..3ef66eac --- /dev/null +++ b/DSL/Resql/update-dataset-group-connected-models.sql @@ -0,0 +1,7 @@ +UPDATE dataset_group_metadata +SET connected_models = + CASE + WHEN connected_models IS NULL THEN jsonb_build_array(:connected_model::jsonb) + ELSE connected_models || :connected_model::jsonb + END +WHERE id = :id; diff --git a/DSL/Resql/update-latest-version-data-model.sql b/DSL/Resql/update-latest-version-data-model.sql new file mode 100644 index 00000000..031a22a1 --- /dev/null +++ b/DSL/Resql/update-latest-version-data-model.sql @@ -0,0 +1,3 @@ +UPDATE models_metadata +SET latest = false +WHERE model_group_key = :group_key \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/create.yml b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/create.yml index 4df5e8fa..79f2c517 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/create.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/create.yml @@ -76,13 +76,67 @@ create_model_metadata: check_status: switch: - condition: ${200 <= res_model.response.statusCodeValue && res_model.response.statusCodeValue < 300} - next: assign_success_response + next: assign_model_id next: assign_fail_response +assign_model_id: + assign: + model_id: ${res_model.response.body[0].id} + next: get_model_data_by_id + +get_model_data_by_id: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-data-model-basic-metadata-by-id" + body: + id: ${model_id} + result: res_model_id + next: check_model_status + +check_model_status: + switch: + - condition: ${200 <= res_model_id.response.statusCodeValue && res_model_id.response.statusCodeValue < 300} + next: check_model_exist + next: assign_fail_response + +check_model_exist: + switch: + - condition: ${res_model_id.response.body.length>0} + next: update_dataset_group_connected_model + next: return_not_found + +update_dataset_group_connected_model: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/update-dataset-group-connected-models" + body: + id: ${dg_id} + connected_model: ${JSON.stringify(res_model_id.response.body[0])} + result: res_dataset + next: check_connected_model_updated_status + +check_connected_model_updated_status: + switch: + - condition: ${200 <= res_dataset.response.statusCodeValue && res_dataset.response.statusCodeValue < 300} + next: execute_cron_manager + next: assign_fail_response + +execute_cron_manager: + call: reflect.mock + args: + url: "[#CLASSIFIER_CRON_MANAGER]/execute/data_model/model_trainer" + query: + cookie: ${incoming.headers.cookie} + model_id: ${model_id} + new_model_id: ${model_id} + updateType: 'major' + result: res + next: assign_success_response + assign_success_response: assign: format_res: { - modelId: '${res_model.response.body[0].id}', + modelId: '${model_id}', operationSuccessful: true } next: return_ok @@ -109,3 +163,8 @@ return_incorrect_request: status: 400 return: 'Missing Required Fields' next: end + +return_not_found: + status: 404 + return: "Model Not Found" + next: end diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update.yml b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update.yml new file mode 100644 index 00000000..57feb53e --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update.yml @@ -0,0 +1,380 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'UPDATE'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: modelId + type: number + description: "Body field 'modelId'" + - field: connectedDgId + type: number + description: "Body field 'connectedDgId'" + - field: connectedDgName + type: string + description: "Body field 'connectedDgName'" + - field: deploymentEnv + type: string + description: "Body field 'deploymentEnv'" + - field: baseModels + type: array + description: "Body field 'baseModels'" + - field: maturityLabel + type: string + description: "Body field 'maturityLabel'" + - field: updateType + type: string + description: "Body field 'updateType'" + headers: + - field: cookie + type: string + description: "Cookie field" + +extract_request_data: + assign: + model_id: ${incoming.body.modelId} + connected_dg_id: ${incoming.body.connectedDgId} + connected_dg_name: ${incoming.body.connectedDgName} + deployment_env: ${incoming.body.deploymentEnv} + base_models: ${incoming.body.baseModels} + maturity_label: ${incoming.body.maturityLabel} + update_type: ${incoming.body.updateType} + next: check_event_type + +check_event_type: + switch: + - condition: ${update_type == 'major'} + next: get_data_model_major_data + - condition: ${update_type == 'minor'} + next: get_data_model_minor_data + - condition: ${update_type == 'maturityLabel'} + next: check_for_maturity_request_data + next: return_type_found + +get_data_model_major_data: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-data-model-major-data" + body: + id: ${model_id} + result: res + next: check_data_model_major_status + +check_data_model_major_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: check_data_model_major_exist + next: return_not_found + +check_data_model_major_exist: + switch: + - condition: ${res.response.body.length>0} + next: assign_major_data + next: return_not_found + +assign_major_data: + assign: + group_key: ${res.response.body[0].modelGroupKey} + deployment_env_prev: ${res.response.body[0].deploymentEnv} + base_models_prev: ${res.response.body[0].baseModels} + maturity_label_prev: ${res.response.body[0].maturityLabel} + next: update_latest_in_old_versions + +update_latest_in_old_versions: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/update-latest-version-data-model" + body: + group_key: ${group_key} + result: res + next: check_latest_status + +check_latest_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: check_event_type_for_latest_version_again + next: assign_fail_response + +check_event_type_for_latest_version_again: + switch: + - condition: ${update_type == 'major'} + next: snapshot_major_data_model + - condition: ${update_type == 'minor'} + next: snapshot_minor_data_model + next: return_type_found + +snapshot_major_data_model: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/snapshot-major-data-model" + body: + id: ${model_id} + group_key: ${group_key} + connected_dg_id: ${connected_dg_id} + connected_dg_name: ${connected_dg_name} + deployment_env: ${deployment_env == null ? deployment_env_prev :deployment_env } + base_models: ${base_models == null ? base_models_prev :base_models} + maturity_label: ${maturity_label == null ? 'development' :maturity_label} + result: res + next: check_snapshot_status + +check_major_previous_deployment_env: + switch: + - condition: ${deployment_env == deployment_env_prev && (deployment_env_prev == 'jira' || deployment_env_prev == 'outlook' || deployment_env_prev == 'pinal')} + next: update_data_model_deployment_env + next: get_major_model_data_by_id + +get_data_model_minor_data: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-data-model-minor-data" + body: + id: ${model_id} + result: res + next: check_data_model_status + +check_data_model_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: check_data_exist + next: return_not_found + +check_data_exist: + switch: + - condition: ${res.response.body.length>0} + next: assign_minor_day + next: return_not_found + +assign_minor_day: + assign: + group_key: ${res.response.body[0].modelGroupKey} + major_version: ${res.response.body[0].majorVersion} + deployment_env_prev: ${res.response.body[0].deploymentEnv} + base_models_prev: ${res.response.body[0].baseModels} + maturity_label_prev: ${res.response.body[0].maturityLabel} + next: check_minor_data_request_status + +check_minor_data_request_status: + switch: + - condition: ${deployment_env == null && (deployment_env_prev == 'jira' || deployment_env_prev == 'outlook' || deployment_env_prev == 'pinal')} + next: assign_previous_deployment_env_and_update_old + - condition: ${deployment_env == null && (deployment_env_prev == 'testing' || deployment_env_prev == 'undeployed')} + next: assign_previous_deployment_env + - condition: ${deployment_env !== null && deployment_env == deployment_env_prev && (deployment_env_prev == 'jira' || deployment_env_prev == 'outlook' || deployment_env_prev == 'pinal')} + next: assign_new_deployment_env_and_update_old + - condition: ${deployment_env !== null && deployment_env !== deployment_env_prev} + next: assign_new_deployment_env + next: assign_fail_response + +assign_new_deployment_env: + assign: + deployment_env_data: ${deployment_env} + next: update_latest_in_old_versions + +assign_previous_deployment_env_and_update_old: + assign: + deployment_env_data: ${deployment_env_prev} + next: update_data_model_deployment_env + +assign_new_deployment_env_and_update_old: + assign: + deployment_env_data: ${deployment_env} + next: update_data_model_deployment_env + +assign_previous_deployment_env: + assign: + deployment_env_data: ${deployment_env_prev} + next: update_latest_in_old_versions + +update_data_model_deployment_env: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/update-data-model-deployment-env" + body: + id: ${model_id} + deployment_env: testing + result: res + next: check_data_model_deployment_env_status + +check_data_model_deployment_env_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: check_event_type_again + next: assign_fail_response + +check_event_type_again: + switch: + - condition: ${update_type == 'major'} + next: get_major_model_data_by_id + - condition: ${update_type == 'minor'} + next: update_latest_in_old_versions + next: return_type_found + +get_major_model_data_by_id: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-data-model-basic-metadata-by-id" + body: + id: ${new_model_id} + result: res_model_id + next: check_model_status + +check_model_status: + switch: + - condition: ${200 <= res_model_id.response.statusCodeValue && res_model_id.response.statusCodeValue < 300} + next: check_model_exist + next: assign_fail_response + +check_model_exist: + switch: + - condition: ${res_model_id.response.body.length>0} + next: update_dataset_group_connected_model + next: return_not_found + +update_dataset_group_connected_model: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/update-dataset-group-connected-models" + body: + id: ${connected_dg_id} + connected_model: ${JSON.stringify(res_model_id.response.body[0])} + result: res_dataset + next: check_connected_model_updated_status + +check_connected_model_updated_status: + switch: + - condition: ${200 <= res_dataset.response.statusCodeValue && res_dataset.response.statusCodeValue < 300} + next: execute_cron_manager + next: assign_fail_response + +snapshot_minor_data_model: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/snapshot-minor-data-model" + body: + id: ${model_id} + group_key: ${group_key} + major_version: ${major_version} + deployment_env: ${deployment_env_data} + base_models: ${base_models == null ? base_models_prev :base_models} + maturity_label: ${maturity_label == null ? maturity_label_prev :maturity_label} + result: res + next: check_snapshot_status + +check_snapshot_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: check_updated_data_exist + next: assign_fail_response + +check_updated_data_exist: + switch: + - condition: ${res.response.body.length>0} + next: assign_new_model_id + next: return_not_found + +assign_new_model_id: + assign: + new_model_id: ${res.response.body[0].id} + next: check_event_type_again_again + +check_event_type_again_again: + switch: + - condition: ${update_type == 'major'} + next: check_major_previous_deployment_env + - condition: ${update_type == 'minor'} + next: execute_cron_manager + next: return_type_found + +execute_cron_manager: + call: reflect.mock + args: + url: "[#CLASSIFIER_CRON_MANAGER]/execute/data_model/model_trainer" + query: + cookie: ${incoming.headers.cookie} + model_id: ${model_id} + new_model_id: ${new_model_id} + updateType: ${update_type} + result: res + next: assign_success_response + +check_for_maturity_request_data: + switch: + - condition: ${model_id !== null && maturity_label !== null} + next: update_maturity_data_model + next: return_incorrect_request + +update_maturity_data_model: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/update-data-model-maturity-label" + body: + id: ${model_id} + maturity_label: ${maturity_label} + result: res_update + next: check_model_maturity_update_status + +check_model_maturity_update_status: + switch: + - condition: ${200 <= res_update.response.statusCodeValue && res_update.response.statusCodeValue < 300} + next: assign_maturity_success_response + next: assign_maturity_fail_response + +assign_maturity_success_response: + assign: + format_res: { + modelId: '${model_id}', + operationSuccessful: true, + } + next: return_ok + +assign_maturity_fail_response: + assign: + format_res: { + modelId: '', + operationSuccessful: false, + } + next: return_bad_request + +assign_success_response: + assign: + format_res: { + model_id: '${model_id}', + new_model_id: '${new_model_id}', + operationSuccessful: true, + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + model_id: '${model_id}', + new_model_id: '', + operationSuccessful: false, + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_not_found: + status: 400 + return: "Data Group Not Found" + next: end + +return_type_found: + status: 400 + return: "Update Type Not Found" + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end + From fa4808d0e329e397777ed528d1392b11cbe15cd3 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Sat, 3 Aug 2024 09:25:11 +0530 Subject: [PATCH 351/582] ESCLASS-162: change enum name --- .../changelog/classifier-script-v9-models-metadata.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DSL/Liquibase/changelog/classifier-script-v9-models-metadata.sql b/DSL/Liquibase/changelog/classifier-script-v9-models-metadata.sql index e3a04054..d01ec384 100644 --- a/DSL/Liquibase/changelog/classifier-script-v9-models-metadata.sql +++ b/DSL/Liquibase/changelog/classifier-script-v9-models-metadata.sql @@ -1,7 +1,7 @@ -- liquibase formatted sql -- changeset kalsara Magamage:classifier-script-v9-changeset1 -CREATE TYPE Maturity_Label AS ENUM ('development', 'staging', 'production'); +CREATE TYPE Maturity_Label AS ENUM ('development', 'staging', 'production ready'); -- changeset kalsara Magamage:classifier-script-v9-changeset2 CREATE TYPE Deployment_Env AS ENUM ('jira', 'outlook', 'pinal', 'testing', 'undeployed'); @@ -48,5 +48,5 @@ INSERT INTO model_configurations (base_models, deployment_platforms, maturity_la ( ARRAY['xlnet', 'roberta', 'albert']::Base_Models[], ARRAY['jira', 'outlook', 'pinal', 'testing', 'undeployed']::Deployment_Env[], - ARRAY['development', 'staging', 'production']::Maturity_Label[] + ARRAY['development', 'staging', 'production ready']::Maturity_Label[] ); \ No newline at end of file From 7c799b0875eeba3c95ba3c0ddc252fbebce4131a Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Sat, 3 Aug 2024 09:26:41 +0530 Subject: [PATCH 352/582] ESCLASS-162: change enum name --- .../changelog/classifier-script-v6-dataset-group-metadata.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DSL/Liquibase/changelog/classifier-script-v6-dataset-group-metadata.sql b/DSL/Liquibase/changelog/classifier-script-v6-dataset-group-metadata.sql index c39f3d30..0112fe82 100644 --- a/DSL/Liquibase/changelog/classifier-script-v6-dataset-group-metadata.sql +++ b/DSL/Liquibase/changelog/classifier-script-v6-dataset-group-metadata.sql @@ -3,7 +3,7 @@ -- changeset kalsara Magamage:classifier-script-v6-changeset1 CREATE TYPE Validation_Status AS ENUM ('success', 'fail', 'in-progress', 'unvalidated'); --- changeset kalsara Magamage:classifier-script-v6-changeset3 +-- changeset kalsara Magamage:classifier-script-v6-changeset2 CREATE TABLE dataset_group_metadata ( id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY, group_name TEXT NOT NULL, From c549500bd6ddc63ac7c7f4524f26076fe16a80a0 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Sun, 4 Aug 2024 23:01:31 +0530 Subject: [PATCH 353/582] removing the docker file model download --- dataset-processor/Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/dataset-processor/Dockerfile b/dataset-processor/Dockerfile index 49264ae6..90379f52 100644 --- a/dataset-processor/Dockerfile +++ b/dataset-processor/Dockerfile @@ -4,5 +4,4 @@ COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 8001 -ENV RUUTER_PRIVATE_URL= CMD ["uvicorn", "dataset_processor_api:app", "--host", "0.0.0.0", "--port", "8001"] From 281654da7d06a9110220c567ca58ec1102c015f6 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Sun, 4 Aug 2024 23:54:19 +0530 Subject: [PATCH 354/582] new cleaned requirenments --- dataset-processor/requirements.txt | 44 ++---------------------------- file-handler/requirements.txt | 33 ++++++++++++++++++---- 2 files changed, 29 insertions(+), 48 deletions(-) diff --git a/dataset-processor/requirements.txt b/dataset-processor/requirements.txt index 5911b077..b218991f 100644 --- a/dataset-processor/requirements.txt +++ b/dataset-processor/requirements.txt @@ -4,56 +4,16 @@ certifi==2024.7.4 charset-normalizer==3.3.2 click==8.1.7 colorama==0.4.6 -dnspython==2.6.1 -email_validator==2.2.0 -fastapi==0.111.1 -fastapi-cli==0.0.4 -filelock==3.15.4 -fsspec==2024.6.1 +fastapi==0.112.0 h11==0.14.0 -httpcore==1.0.5 -httptools==0.6.1 -httpx==0.27.0 -huggingface-hub==0.24.2 idna==3.7 -Jinja2==3.1.4 -joblib==1.4.2 -sacremoses==0.1.1 -langdetect==1.0.9 -markdown-it-py==3.0.0 -MarkupSafe==2.1.5 -mdurl==0.1.2 -mpmath==1.3.0 -networkx==3.3 -numpy==1.26.4 -packaging==24.1 -pillow==10.4.0 pydantic==2.8.2 pydantic_core==2.20.1 -Pygments==2.18.0 -python-dotenv==1.0.1 -python-multipart==0.0.9 -PyYAML==6.0.1 -regex==2024.7.24 requests==2.32.3 -rich==13.7.1 -safetensors==0.4.3 -sentencepiece==0.2.0 setuptools==69.5.1 -shellingham==1.5.4 sniffio==1.3.1 starlette==0.37.2 -sympy==1.13.1 -tokenizers==0.19.1 -torch==2.4.0 -torchaudio==2.4.0 -torchvision==0.19.0 -tqdm==4.66.4 -transformers==4.43.3 -typer==0.12.3 typing_extensions==4.12.2 urllib3==2.2.2 -uvicorn==0.30.3 -watchfiles==0.22.0 -websockets==12.0 +uvicorn==0.30.5 wheel==0.43.0 diff --git a/file-handler/requirements.txt b/file-handler/requirements.txt index 67f21bb0..c00f213e 100644 --- a/file-handler/requirements.txt +++ b/file-handler/requirements.txt @@ -1,6 +1,27 @@ -fastapi -uvicorn[standard] -aiofiles -requests -pandas -openpyxl +annotated-types==0.7.0 +anyio==4.4.0 +certifi==2024.7.4 +charset-normalizer==3.3.2 +click==8.1.7 +colorama==0.4.6 +fastapi==0.112.0 +h11==0.14.0 +idna==3.7 +numpy==2.0.1 +pandas==2.2.2 +pydantic==2.8.2 +pydantic_core==2.20.1 +python-dateutil==2.9.0.post0 +python-multipart==0.0.9 +pytz==2024.1 +PyYAML==6.0.1 +requests==2.32.3 +setuptools==69.5.1 +six==1.16.0 +sniffio==1.3.1 +starlette==0.37.2 +typing_extensions==4.12.2 +tzdata==2024.1 +urllib3==2.2.2 +uvicorn==0.30.5 +wheel==0.43.0 From d13987c78c6a3adf31d919a44e75208b91eee326 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 5 Aug 2024 00:19:54 +0530 Subject: [PATCH 355/582] updated requirenments --- file-handler/requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/file-handler/requirements.txt b/file-handler/requirements.txt index c00f213e..0b2b0421 100644 --- a/file-handler/requirements.txt +++ b/file-handler/requirements.txt @@ -4,10 +4,12 @@ certifi==2024.7.4 charset-normalizer==3.3.2 click==8.1.7 colorama==0.4.6 +et-xmlfile==1.1.0 fastapi==0.112.0 h11==0.14.0 idna==3.7 numpy==2.0.1 +openpyxl==3.1.5 pandas==2.2.2 pydantic==2.8.2 pydantic_core==2.20.1 From 57f5030fb2dfb5ebf21031bd810855b57170334b Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 5 Aug 2024 00:20:12 +0530 Subject: [PATCH 356/582] completed stopwords --- file-handler/file_converter.py | 2 ++ file-handler/file_handler_api.py | 49 +++++++++++++++++++++++++------- 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/file-handler/file_converter.py b/file-handler/file_converter.py index 69059168..d231fa71 100644 --- a/file-handler/file_converter.py +++ b/file-handler/file_converter.py @@ -12,6 +12,8 @@ def _detect_file_type(self, filePath): return 'yaml' elif filePath.endswith('.xlsx'): return 'xlsx' + elif filePath.endswith('.txt'): + return 'txt' else: return None diff --git a/file-handler/file_handler_api.py b/file-handler/file_handler_api.py index 0aa8c783..fc6d2cdf 100644 --- a/file-handler/file_handler_api.py +++ b/file-handler/file_handler_api.py @@ -12,6 +12,11 @@ S3_UPLOAD_FAILED, S3_DOWNLOAD_FAILED, JSON_EXT, YAML_EXT, YML_EXT, XLSX_EXT ) from s3_ferry import S3Ferry +import yaml +import pandas as pd +from typing import List +from io import BytesIO, TextIOWrapper + app = FastAPI() @@ -310,15 +315,38 @@ async def upload_and_copy(request: Request, copyPayload: CopyPayload): upload_success["saved_file_path"] = f"/dataset/{new_dg_id}/" return JSONResponse(status_code=200, content=upload_success) +def extract_stop_words(file: UploadFile) -> List[str]: + file_converter = FileConverter() + file_type = file_converter._detect_file_type(file.filename) + + if file_type == 'txt': + content = file.file.read().decode('utf-8') + return [word.strip() for word in content.split(',')] + elif file_type == 'json': + content = json.load(file.file) + return content if isinstance(content, list) else [] + elif file_type == 'yaml': + content = yaml.safe_load(file.file) + return content if isinstance(content, list) else [] + elif file_type == 'xlsx': + content = file.file.read() + excel_file = BytesIO(content) + data = pd.read_excel(excel_file, sheet_name=None) + stop_words = [] + for sheet in data: + stop_words.extend(data[sheet].stack().tolist()) + return stop_words + else: + raise HTTPException(status_code=400, detail="Unsupported file type") + + @app.post("/datasetgroup/data/import/stop-words") async def import_stop_words(request: Request, stopWordsFile: UploadFile = File(...)): try: cookie = request.cookies.get("customJwtCookie") await authenticate_user(f'customJwtCookie={cookie}') - file_content = await stopWordsFile.read() - - words_list = [word.strip() for word in file_content.decode('utf-8').split(',')] + words_list = extract_stop_words(stopWordsFile) url = IMPORT_STOPWORDS_URL headers = { @@ -330,10 +358,10 @@ async def import_stop_words(request: Request, stopWordsFile: UploadFile = File(. if response.status_code == 200: response_data = response.json() - if response_data['operationSuccessful']: + if response_data['response']['operationSuccessful']: return response_data - elif response_data['duplicate']: - duplicate_items = response_data['duplicateItems'] + elif response_data['response']['duplicate']: + duplicate_items = response_data['response']['duplicateItems'] new_words_list = [word for word in words_list if word not in duplicate_items] if new_words_list: response = requests.post(url, headers=headers, json={"stopWords": new_words_list}) @@ -352,8 +380,7 @@ async def delete_stop_words(request: Request, stopWordsFile: UploadFile = File(. cookie = request.cookies.get("customJwtCookie") await authenticate_user(f'customJwtCookie={cookie}') - file_content = await stopWordsFile.read() - words_list = [word.strip() for word in file_content.decode('utf-8').split(',')] + words_list = extract_stop_words(stopWordsFile) url = DELETE_STOPWORDS_URL headers = { @@ -365,10 +392,10 @@ async def delete_stop_words(request: Request, stopWordsFile: UploadFile = File(. if response.status_code == 200: response_data = response.json() - if response_data['operationSuccessful']: + if response_data['response']['operationSuccessful']: return response_data - elif response_data['nonexistent']: - nonexistent_items = response_data['nonexistentItems'] + elif response_data['response']['nonexistent']: + nonexistent_items = response_data['response']['nonexistentItems'] new_words_list = [word for word in words_list if word not in nonexistent_items] if new_words_list: response = requests.post(url, headers=headers, json={"stopWords": new_words_list}) From 892f1536787f87ebc17be925f1fbacd739036176 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 5 Aug 2024 00:20:25 +0530 Subject: [PATCH 357/582] example stop word files --- .../example_stop_word_files/stop_words.json | 7 +++++++ .../example_stop_word_files/stop_words.txt | 1 + .../example_stop_word_files/stop_words.xlsx | Bin 0 -> 5579 bytes .../example_stop_word_files/stop_words.yaml | 5 +++++ 4 files changed, 13 insertions(+) create mode 100644 file-handler/example_stop_word_files/stop_words.json create mode 100644 file-handler/example_stop_word_files/stop_words.txt create mode 100644 file-handler/example_stop_word_files/stop_words.xlsx create mode 100644 file-handler/example_stop_word_files/stop_words.yaml diff --git a/file-handler/example_stop_word_files/stop_words.json b/file-handler/example_stop_word_files/stop_words.json new file mode 100644 index 00000000..1cfd3a57 --- /dev/null +++ b/file-handler/example_stop_word_files/stop_words.json @@ -0,0 +1,7 @@ +[ + "stop", + "word", + "filter", + "remove", + "ignore" +] diff --git a/file-handler/example_stop_word_files/stop_words.txt b/file-handler/example_stop_word_files/stop_words.txt new file mode 100644 index 00000000..cfb2dc72 --- /dev/null +++ b/file-handler/example_stop_word_files/stop_words.txt @@ -0,0 +1 @@ +stop, word, filter, remove, ignore \ No newline at end of file diff --git a/file-handler/example_stop_word_files/stop_words.xlsx b/file-handler/example_stop_word_files/stop_words.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..6b1e0c852d043c85ed517b9c93ccbdaa4899f232 GIT binary patch literal 5579 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 Date: Mon, 5 Aug 2024 02:49:43 +0530 Subject: [PATCH 358/582] fixing issues in dataset processor --- dataset-processor/dataset_processor.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dataset-processor/dataset_processor.py b/dataset-processor/dataset_processor.py index 554055eb..04a5e3fe 100644 --- a/dataset-processor/dataset_processor.py +++ b/dataset-processor/dataset_processor.py @@ -110,7 +110,7 @@ def chunk_data(self, data, chunk_size=5): def copy_chunked_datafiles(self, dgId, newDgId, cookie, exsistingChunks=None): try: headers = { - 'cookie': f'customJwtCookie={cookie}', + 'cookie': cookie, 'Content-Type': 'application/json' } @@ -141,7 +141,7 @@ def copy_chunked_datafiles(self, dgId, newDgId, cookie, exsistingChunks=None): def save_chunked_data(self, chunked_data, cookie, dgId, exsistingChunks=0): headers = { - 'cookie': f'customJwtCookie={cookie}', + 'cookie': cookie, 'Content-Type': 'application/json' } @@ -218,11 +218,11 @@ def get_dataset_by_location(self, fileLocation, custom_jwt_cookie): def get_stopwords(self, custom_jwt_cookie): headers = { - 'Cookie': f'customJwtCookie={custom_jwt_cookie}' + 'Cookie': custom_jwt_cookie } try: - response = requests.get(self.GET_STOPWORDS_URL, headers=headers) + response = requests.get(GET_STOPWORDS_URL, headers=headers) response.raise_for_status() response_data = response.json() if response_data.get("operationSuccessful", False): @@ -271,7 +271,7 @@ def get_page_count(self, dg_id, custom_jwt_cookie): def save_aggregrated_data(self, dgId, cookie, aggregratedData): headers = { - 'cookie': f'customJwtCookie={cookie}', + 'cookie': cookie, 'Content-Type': 'application/json' } @@ -291,7 +291,7 @@ def save_aggregrated_data(self, dgId, cookie, aggregratedData): def download_chunk(self, dgId, cookie, pageId): params = {'dgId': dgId, 'pageId': pageId} headers = { - 'cookie': f'customJwtCookie={cookie}' + 'cookie': cookie } try: From 3440954cb963dcfb83c607cf9ece61cbd08dd413 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 5 Aug 2024 02:50:07 +0530 Subject: [PATCH 359/582] sh file echo to log sh started --- DSL/CronManager/script/data_processor_exec.sh | 2 +- DSL/CronManager/script/data_validator_exec.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DSL/CronManager/script/data_processor_exec.sh b/DSL/CronManager/script/data_processor_exec.sh index 34d0cc43..c7147496 100644 --- a/DSL/CronManager/script/data_processor_exec.sh +++ b/DSL/CronManager/script/data_processor_exec.sh @@ -1,5 +1,5 @@ #!/bin/bash - +echo "Started Shell Script to process" # Ensure required environment variables are set if [ -z "$dgId" ] || [ -z "$newDgId" ] || [ -z "$cookie" ] || [ -z "$updateType" ] || [ -z "$savedFilePath" ] || [ -z "$patchPayload" ]; then echo "One or more environment variables are missing." diff --git a/DSL/CronManager/script/data_validator_exec.sh b/DSL/CronManager/script/data_validator_exec.sh index 0e4ee7bf..143e57be 100644 --- a/DSL/CronManager/script/data_validator_exec.sh +++ b/DSL/CronManager/script/data_validator_exec.sh @@ -1,5 +1,5 @@ #!/bin/bash - +echo "Started Shell Script to validator" # Ensure required environment variables are set if [ -z "$dgId" ] || [ -z "$newDgId" ] || [ -z "$cookie" ] || [ -z "$updateType" ] || [ -z "$savedFilePath" ] || [ -z "$patchPayload" ]; then echo "One or more environment variables are missing." From 102d74cae2a1218026a2d9382fda11db7a1489de Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Mon, 5 Aug 2024 14:34:29 +0530 Subject: [PATCH 360/582] data models create and configure uis --- GUI/src/App.tsx | 2 + .../FormCheckboxes/FormCheckboxes.scss | 6 +- .../FormElements/FormCheckboxes/index.tsx | 71 +++++--- .../FormElements/FormRadios/index.tsx | 48 ++++-- GUI/src/components/Label/Label.scss | 11 ++ GUI/src/components/Label/index.tsx | 4 +- .../molecules/DataModelCard/DataModel.scss | 5 + .../molecules/DataModelCard/index.tsx | 160 ++++++++++++++++++ .../molecules/DataModelForm/index.tsx | 134 +++++++++++++++ .../DatasetGroupCard/DatasetGroupCard.scss | 1 + GUI/src/enums/dataModelsEnums.ts | 13 ++ .../pages/DataModels/ConfigureDataModel.tsx | 131 ++++++++++++++ GUI/src/pages/DataModels/CreateDataModel.tsx | 111 ++++++++++++ GUI/src/pages/DataModels/DataModels.scss | 10 ++ GUI/src/pages/DataModels/index.tsx | 42 ++--- GUI/src/services/data-models.ts | 11 +- GUI/src/utils/commonUtilts.ts | 8 + GUI/src/utils/dataModelsUtils.ts | 13 ++ dataset-processor/dataset_processor.py | 3 + 19 files changed, 726 insertions(+), 58 deletions(-) create mode 100644 GUI/src/components/molecules/DataModelCard/DataModel.scss create mode 100644 GUI/src/components/molecules/DataModelCard/index.tsx create mode 100644 GUI/src/components/molecules/DataModelForm/index.tsx create mode 100644 GUI/src/enums/dataModelsEnums.ts create mode 100644 GUI/src/pages/DataModels/ConfigureDataModel.tsx create mode 100644 GUI/src/pages/DataModels/CreateDataModel.tsx create mode 100644 GUI/src/pages/DataModels/DataModels.scss create mode 100644 GUI/src/utils/dataModelsUtils.ts diff --git a/GUI/src/App.tsx b/GUI/src/App.tsx index 50533212..f5486026 100644 --- a/GUI/src/App.tsx +++ b/GUI/src/App.tsx @@ -13,6 +13,7 @@ import ViewDatasetGroup from 'pages/DatasetGroups/ViewDatasetGroup'; import StopWords from 'pages/StopWords'; import ValidationSessions from 'pages/ValidationSessions'; import DataModels from 'pages/DataModels'; +import CreateDataModel from 'pages/DataModels/CreateDataModel'; const App: FC = () => { @@ -37,6 +38,7 @@ const App: FC = () => { } /> } /> } /> + } /> diff --git a/GUI/src/components/FormElements/FormCheckboxes/FormCheckboxes.scss b/GUI/src/components/FormElements/FormCheckboxes/FormCheckboxes.scss index b312ad91..02d57e7d 100644 --- a/GUI/src/components/FormElements/FormCheckboxes/FormCheckboxes.scss +++ b/GUI/src/components/FormElements/FormCheckboxes/FormCheckboxes.scss @@ -4,7 +4,6 @@ @import 'src/styles/settings/variables/typography'; .checkboxes { - width: 100%; display: flex; align-items: flex-start; gap: get-spacing(paldiski); @@ -22,6 +21,11 @@ gap: 8px; } + &__row { + display: flex; + gap: 20px; + } + &__item { input[type=checkbox] { display: none; diff --git a/GUI/src/components/FormElements/FormCheckboxes/index.tsx b/GUI/src/components/FormElements/FormCheckboxes/index.tsx index e3270315..a70561d2 100644 --- a/GUI/src/components/FormElements/FormCheckboxes/index.tsx +++ b/GUI/src/components/FormElements/FormCheckboxes/index.tsx @@ -6,38 +6,71 @@ type FormCheckboxesType = { label: string; name: string; hideLabel?: boolean; - onValuesChange?: (values: Record) => void; + onValuesChange?: (values: Record) => void; items: { label: string; value: string; }[]; -} + isStack?: boolean; + error?: string; +}; -const FormCheckboxes: FC = ({ label, name, hideLabel, onValuesChange, items }) => { +const FormCheckboxes: FC = ({ + label, + name, + hideLabel, + onValuesChange, + items, + isStack = true, + error, +}) => { const id = useId(); - const [selectedValues, setSelectedValues] = useState>({}); + const [selectedValues, setSelectedValues] = useState< + Record + >({ [name]: [] }); const handleValuesChange = (e: ChangeEvent) => { - setSelectedValues((prevState) => ({ - ...prevState, - [e.target.name]: [e.target.value], - })); - if (onValuesChange) onValuesChange(selectedValues); + const { checked, value } = e.target; + + setSelectedValues((prevState) => { + const newValues = checked + ? [...prevState[name], value] // Add the checked value to the array + : prevState[name].filter((v: string) => v !== value); // Remove the unchecked value from the array + + const updatedValues = { ...prevState, [name]: newValues }; + + if (onValuesChange) onValuesChange(updatedValues); + + return updatedValues; + }); }; return ( -
    - {label && !hideLabel && } -
    - {items.map((item, index) => ( -
    - - +
    +
    +
    + {label && !hideLabel && ( + + )} +
    + {items.map((item, index) => ( +
    + + +
    + ))}
    - ))} +
    -
    +
    {error &&

    {error}

    }
    +
    ); }; diff --git a/GUI/src/components/FormElements/FormRadios/index.tsx b/GUI/src/components/FormElements/FormRadios/index.tsx index 619a961a..6ee27e4e 100644 --- a/GUI/src/components/FormElements/FormRadios/index.tsx +++ b/GUI/src/components/FormElements/FormRadios/index.tsx @@ -12,25 +12,47 @@ type FormRadiosType = { }[]; onChange: (selectedValue: string) => void; isStack?: boolean; -} + error?:string +}; -const FormRadios: FC = ({ label, name, hideLabel, items, onChange,isStack=false }) => { +const FormRadios: FC = ({ + label, + name, + hideLabel, + items, + onChange, + isStack = false, + error +}) => { const id = useId(); return ( -
    - {label && !hideLabel && } -
    - {items.map((item, index) => ( -
    - { - onChange(event.target.value); - }} /> - +
    +
    +
    + {label && !hideLabel && ( + + )} +
    + {items.map((item, index) => ( +
    + { + onChange(event.target.value); + }} + /> + +
    + ))}
    - ))} +
    -
    +
    {error &&

    {error}

    }
    +
    ); }; diff --git a/GUI/src/components/Label/Label.scss b/GUI/src/components/Label/Label.scss index bff23787..da25afdd 100644 --- a/GUI/src/components/Label/Label.scss +++ b/GUI/src/components/Label/Label.scss @@ -50,6 +50,17 @@ } } + &--default { + color: get-color(black-coral-7); + border-color: get-color(black-coral-7); + + #{$self} { + &__icon { + border-color: get-color(black-coral-7); + } + } + } + &--success { color: get-color(sea-green-10); border-color: get-color(sea-green-10); diff --git a/GUI/src/components/Label/index.tsx b/GUI/src/components/Label/index.tsx index 0b450c24..e27d0d4f 100644 --- a/GUI/src/components/Label/index.tsx +++ b/GUI/src/components/Label/index.tsx @@ -6,13 +6,13 @@ import { Tooltip } from 'components'; import './Label.scss'; type LabelProps = { - type?: 'warning' | 'error' | 'info' | 'success'; + type?: 'warning' | 'error' | 'info' | 'success' | 'default'; tooltip?: ReactNode; } const Label = forwardRef>(( { - type = 'info', + type = 'default', tooltip, children, }, ref, diff --git a/GUI/src/components/molecules/DataModelCard/DataModel.scss b/GUI/src/components/molecules/DataModelCard/DataModel.scss new file mode 100644 index 00000000..5eed0e77 --- /dev/null +++ b/GUI/src/components/molecules/DataModelCard/DataModel.scss @@ -0,0 +1,5 @@ +.training-results-grid-container { + display: grid; + grid-template-columns: 3fr 1fr 1fr; + gap: 10px; +} \ No newline at end of file diff --git a/GUI/src/components/molecules/DataModelCard/index.tsx b/GUI/src/components/molecules/DataModelCard/index.tsx new file mode 100644 index 00000000..fc5d0fa9 --- /dev/null +++ b/GUI/src/components/molecules/DataModelCard/index.tsx @@ -0,0 +1,160 @@ +import { FC, PropsWithChildren } from 'react'; +import Button from 'components/Button'; +import Label from 'components/Label'; +import { useQueryClient } from '@tanstack/react-query'; +import { useDialog } from 'hooks/useDialog'; +import './DataModel.scss'; +import { Maturity, TrainingStatus } from 'enums/dataModelsEnums'; +import Card from 'components/Card'; + +type DataModelCardProps = { + modelId?:number; + dataModelName?: string | undefined; + datasetGroupName?: string; + version?: string; + isLatest?: boolean; + dgVersion?: string; + lastTrained?: string; + trainingStatus?: string; + platform?: string; + maturity?: string; + setId?: React.Dispatch>; + setView?: React.Dispatch>; + results?: any; +}; + +const DataModelCard: FC> = ({ + modelId, + dataModelName, + datasetGroupName, + version, + isLatest, + dgVersion, + lastTrained, + trainingStatus, + platform, + maturity, + results, + setId, + setView, +}) => { + const queryClient = useQueryClient(); + const { open,close } = useDialog(); + + const renderTrainingStatus = (status: string | undefined) => { + if (status === TrainingStatus.RETRAINING_NEEDED) { + return ; + } else if (status === TrainingStatus.TRAINED) { + return ; + } else if (status === TrainingStatus.TRAINING_INPROGRESS) { + return ; + } else if (status === TrainingStatus.UNTRAINABLE) { + return ; + } + }; + + const renderMaturityLabel = (status: string | undefined) => { + if (status === Maturity.DEVELOPMENT) { + return ; + } else if (status === Maturity.PRODUCTION) { + return ; + } else if (status === Maturity.STAGING) { + return ; + } else if (status === Maturity.TESTING) { + return ; + } + }; + + return ( +
    +
    +

    {dataModelName}

    +
    + + {isLatest ? : null} +
    + +
    +

    + {'Dataset Group:'} + {datasetGroupName} +

    +

    + {'Dataset Group Version:'} + {dgVersion} +

    +

    + {'Last Trained:'} + {lastTrained} +

    +
    +
    + {renderTrainingStatus(trainingStatus)} + + {renderMaturityLabel(maturity)} +
    + +
    + , + size: 'large', + content: ( +
    +
    Best Performing Model -
    {' '} + +
    Classes
    +
    Accuracy
    +
    F1 Score
    +
    + } + > +
    +
    + {results?.classes?.map((c) => { + return
    {c}
    ; + })} +
    +
    + {results?.accuracy?.map((c) => { + return
    {c}
    ; + })} +
    +
    + {results?.f1_score?.map((c) => { + return
    {c}
    ; + })} +
    +
    + +
    + ), + }); + }} + > + View Results + + +
    +
    +
    + ); +}; + +export default DataModelCard; diff --git a/GUI/src/components/molecules/DataModelForm/index.tsx b/GUI/src/components/molecules/DataModelForm/index.tsx new file mode 100644 index 00000000..de97f584 --- /dev/null +++ b/GUI/src/components/molecules/DataModelForm/index.tsx @@ -0,0 +1,134 @@ +import { FC, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { FormCheckboxes, FormInput, FormRadios, FormSelect, Label } from 'components'; +import { DatasetGroup } from 'types/datasetGroups'; +import { useNavigate } from 'react-router-dom'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import { createDatasetGroup } from 'services/datasets'; +import { useDialog } from 'hooks/useDialog'; +import { getCreateOptions } from 'services/data-models'; +import { customFormattedArray, formattedArray } from 'utils/commonUtilts'; +import { validateDataModel } from 'utils/dataModelsUtils'; + +type DataModelFormType = { + dataModel: any; + handleChange:any + +}; +const DataModelForm: FC = ({dataModel,handleChange}) => { + const { t } = useTranslation(); + const { open } = useDialog(); + const [isModalOpen, setIsModalOpen] = useState(false); + const [modalType, setModalType] = useState(''); + const navigate = useNavigate(); + + const { data: createOptions } = useQuery(['datamodels/create-options'], () => + getCreateOptions() + ); + + const [errors, setErrors] = useState({ + modelName: '', + dgName: '', + platform: '', + baseModels: '', + maturity: '', + }); + + const validateData = () => { + + setErrors(validateDataModel(dataModel)); + }; + + const createDatasetGroupMutation = useMutation({ + mutationFn: (data: DatasetGroup) => createDatasetGroup(data), + onSuccess: async (response) => { + setIsModalOpen(true); + setModalType('SUCCESS'); + }, + onError: () => { + open({ + title: 'Dataset Group Creation Unsuccessful', + content:

    Something went wrong. Please try again.

    , + }); + }, + }); + + return ( +
    +
    +
    + + handleChange('modelName', e.target.value) + } + error={errors.modelName} + > +
    +
    + Model Version +
    +
    + {createOptions && ( +
    +
    Select Dataset Group
    +
    + + handleChange('dgName', selection.value) + } + error={errors.dgName} + > +
    +
    Select Deployment Platform
    +
    + + handleChange('baseModels', values?.dataset) + } + error={errors.baseModels} + > +
    +
    Select Deployment Platform
    +
    + + handleChange('platform', value) + } + error={errors.platform} + > +
    +
    Select Maturity Label
    +
    + + handleChange('maturity', value) + } + error={errors.maturity} + > +
    +
    + )} +
    + ); +}; + +export default DataModelForm; diff --git a/GUI/src/components/molecules/DatasetGroupCard/DatasetGroupCard.scss b/GUI/src/components/molecules/DatasetGroupCard/DatasetGroupCard.scss index 901b47fc..a4f9432e 100644 --- a/GUI/src/components/molecules/DatasetGroupCard/DatasetGroupCard.scss +++ b/GUI/src/components/molecules/DatasetGroupCard/DatasetGroupCard.scss @@ -56,6 +56,7 @@ justify-content: flex-end; display: flex; align-items: center; + margin-top: 15px; } .left-label { diff --git a/GUI/src/enums/dataModelsEnums.ts b/GUI/src/enums/dataModelsEnums.ts new file mode 100644 index 00000000..c805ea02 --- /dev/null +++ b/GUI/src/enums/dataModelsEnums.ts @@ -0,0 +1,13 @@ +export enum TrainingStatus { + TRAINING_INPROGRESS = 'training in-progress', + TRAINED = 'trained', + RETRAINING_NEEDED = 'retraining needed', + UNTRAINABLE = 'untrainable', + } + + export enum Maturity { + PRODUCTION = 'production', + STAGING = 'staging', + DEVELOPMENT = 'development', + TESTING = 'testing', + } \ No newline at end of file diff --git a/GUI/src/pages/DataModels/ConfigureDataModel.tsx b/GUI/src/pages/DataModels/ConfigureDataModel.tsx new file mode 100644 index 00000000..da0732af --- /dev/null +++ b/GUI/src/pages/DataModels/ConfigureDataModel.tsx @@ -0,0 +1,131 @@ +import { FC, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Button, Card } from 'components'; +import { DatasetGroup } from 'types/datasetGroups'; +import { Link, useNavigate } from 'react-router-dom'; +import './DataModels.scss'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import { createDatasetGroup } from 'services/datasets'; +import { useDialog } from 'hooks/useDialog'; +import BackArrowButton from 'assets/BackArrowButton'; +import { getMetadata } from 'services/data-models'; +import { validateDataModel } from 'utils/dataModelsUtils'; +import DataModelForm from 'components/molecules/DataModelForm'; + +type ConfigureDataModelType = { + id: number; +}; +const ConfigureDataModel: FC = ({ id }) => { + const { t } = useTranslation(); + const { open } = useDialog(); + const [isModalOpen, setIsModalOpen] = useState(false); + const [modalType, setModalType] = useState(''); + const navigate = useNavigate(); + + + const { data: dataModelData } = useQuery(['datamodels/metadata',id], () => + getMetadata(id), + ); + const [dataModel, setDataModel] = useState({ + modelName: dataModelData?.modelName, + dgName: dataModelData?.connectedDgName, + platform: dataModelData?.deploymentEnv, + baseModels: dataModelData?.baseModels, + maturity: dataModelData?.maturityLabel, + }); + + const handleDataModelAttributesChange = (name: string, value: string) => { + + + setDataModel((prevFilters) => ({ + ...prevFilters, + [name]: value, + })); + }; + + const [errors, setErrors] = useState({ + modelName: '', + dgName: '', + platform: '', + baseModels: '', + maturity: '', + }); + + const validateData = () => { + + setErrors(validateDataModel(dataModel)); + }; + + const createDatasetGroupMutation = useMutation({ + mutationFn: (data: DatasetGroup) => createDatasetGroup(data), + onSuccess: async (response) => { + setIsModalOpen(true); + setModalType('SUCCESS'); + }, + onError: () => { + open({ + title: 'Dataset Group Creation Unsuccessful', + content:

    Something went wrong. Please try again.

    , + }); + }, + }); + + return ( +
    +
    +
    + navigate(0)}> + + +
    Configure Data Model
    +
    + + +
    +
    +
    + No Data Available +
    +

    + You have created the dataset group, but there are no datasets + available to show here. You can upload a dataset to view it in + this space. Once added, you can edit or delete the data as + needed. +

    + +
    +
    +
    + +
    +
    + + +
    +
    + ); +}; + +export default ConfigureDataModel; diff --git a/GUI/src/pages/DataModels/CreateDataModel.tsx b/GUI/src/pages/DataModels/CreateDataModel.tsx new file mode 100644 index 00000000..d8181cd9 --- /dev/null +++ b/GUI/src/pages/DataModels/CreateDataModel.tsx @@ -0,0 +1,111 @@ +import { FC, useCallback, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + Button, + FormCheckbox, + FormCheckboxes, + FormInput, + FormRadios, + FormSelect, + Label, +} from 'components'; +import { DatasetGroup } from 'types/datasetGroups'; +import { Link, useNavigate } from 'react-router-dom'; +import './DataModels.scss'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import { createDatasetGroup } from 'services/datasets'; +import { useDialog } from 'hooks/useDialog'; +import BackArrowButton from 'assets/BackArrowButton'; +import { getCreateOptions } from 'services/data-models'; +import { customFormattedArray, formattedArray } from 'utils/commonUtilts'; +import { validateDataModel } from 'utils/dataModelsUtils'; +import DataModelForm from 'components/molecules/DataModelForm'; + +const CreateDataModel: FC = () => { + const { t } = useTranslation(); + const { open } = useDialog(); + const [isModalOpen, setIsModalOpen] = useState(false); + const [modalType, setModalType] = useState(''); + const navigate = useNavigate(); + + const { data: createOptions } = useQuery(['datamodels/create-options'], () => + getCreateOptions() + ); + const [dataModel, setDataModel] = useState({ + modelName: '', + dgName: '', + platform: '', + baseModels: [], + maturity: '', + }); + + const handleDataModelAttributesChange = (name: string, value: string) => { + + + setDataModel((prevFilters) => ({ + ...prevFilters, + [name]: value, + })); + }; + + const [errors, setErrors] = useState({ + modelName: '', + dgName: '', + platform: '', + baseModels: '', + maturity: '', + }); + + const validateData = () => { + + setErrors(validateDataModel(dataModel)); + }; + + const createDatasetGroupMutation = useMutation({ + mutationFn: (data: DatasetGroup) => createDatasetGroup(data), + onSuccess: async (response) => { + setIsModalOpen(true); + setModalType('SUCCESS'); + }, + onError: () => { + open({ + title: 'Dataset Group Creation Unsuccessful', + content:

    Something went wrong. Please try again.

    , + }); + }, + }); + + return ( +
    +
    +
    +
    + navigate(0)}> + + +
    Create Data Model
    +
    +
    + +
    +
    + + +
    +
    + ); +}; + +export default CreateDataModel; diff --git a/GUI/src/pages/DataModels/DataModels.scss b/GUI/src/pages/DataModels/DataModels.scss new file mode 100644 index 00000000..67c1ded0 --- /dev/null +++ b/GUI/src/pages/DataModels/DataModels.scss @@ -0,0 +1,10 @@ +.grey-card { + border: 1px solid #A6A8B1; + border-radius: 5px; + margin-bottom: 10px; + display: flex; + gap: 20px; + align-items: center; + background-color: #F9F9F9; + padding: 25px; +} diff --git a/GUI/src/pages/DataModels/index.tsx b/GUI/src/pages/DataModels/index.tsx index 9dce22eb..39dcb2fb 100644 --- a/GUI/src/pages/DataModels/index.tsx +++ b/GUI/src/pages/DataModels/index.tsx @@ -1,16 +1,13 @@ import { FC, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Button, FormInput, FormSelect } from 'components'; -import DatasetGroupCard from 'components/molecules/DatasetGroupCard'; +import { Button, FormSelect } from 'components'; import Pagination from 'components/molecules/Pagination'; import { useQuery } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; -import { - convertTimestampToDateTime, - formattedArray, - parseVersionString, -} from 'utils/commonUtilts'; +import { formattedArray, parseVersionString } from 'utils/commonUtilts'; import { getDataModelsOverview, getFilterData } from 'services/data-models'; +import DataModelCard from 'components/molecules/DataModelCard'; +import ConfigureDataModel from './ConfigureDataModel'; const DataModels: FC = () => { const { t } = useTranslation(); @@ -71,7 +68,6 @@ const DataModels: FC = () => { getFilterData() ); // const pageCount = datasetGroupsData?.response?.data?.[0]?.totalPages || 1; - console.log(dataModelsData); const handleFilterChange = (name: string, value: string) => { setEnableFetch(false); @@ -83,13 +79,13 @@ const DataModels: FC = () => { return (
    -
    + {view==="list"&&(
    Data Models
    @@ -179,17 +175,19 @@ const DataModels: FC = () => { {dataModelsData?.data?.map( (dataset, index: number) => { return ( - @@ -205,7 +203,11 @@ const DataModels: FC = () => { onPageChange={setPageIndex} />
    -
    +
    )} + {view==="individual" &&( + + )} +
    ); }; diff --git a/GUI/src/services/data-models.ts b/GUI/src/services/data-models.ts index 7c37b307..d4e1aa09 100644 --- a/GUI/src/services/data-models.ts +++ b/GUI/src/services/data-models.ts @@ -40,10 +40,15 @@ export async function getFilterData() { return data; } -export async function getMetadata(groupId: string | number | null) { - const { data } = await apiDev.get('classifier/datasetgroup/group/metadata', { +export async function getCreateOptions() { + const { data } = await apiMock.get('classifier/datamodel/create/options'); + return data; +} + +export async function getMetadata(modelId: string | number | null) { + const { data } = await apiMock.get('classifier/datamodel/metadata', { params: { - groupId + modelId }, }); return data; diff --git a/GUI/src/utils/commonUtilts.ts b/GUI/src/utils/commonUtilts.ts index a72a6976..09ebef3f 100644 --- a/GUI/src/utils/commonUtilts.ts +++ b/GUI/src/utils/commonUtilts.ts @@ -9,6 +9,14 @@ export const formattedArray = (data: string[]) => { })); }; +export const customFormattedArray = >(data: T[], attributeName: keyof T) => { + return data?.map((item) => ({ + label: item[attributeName], + value: item[attributeName], + })); +}; + + export const convertTimestampToDateTime = (timestamp: number) => { return moment.unix(timestamp).format('YYYY-MM-DD HH:mm:ss'); }; diff --git a/GUI/src/utils/dataModelsUtils.ts b/GUI/src/utils/dataModelsUtils.ts new file mode 100644 index 00000000..6419bfe9 --- /dev/null +++ b/GUI/src/utils/dataModelsUtils.ts @@ -0,0 +1,13 @@ +export const validateDataModel = (dataModel) => { + const { modelName, dgName, platform, baseModels, maturity } = dataModel; + const newErrors: any = {}; + + if (!modelName.trim()) newErrors.modelName = 'Model Name is required'; + if (!dgName.trim()) newErrors.dgName = 'Dataset Group Name is required'; + if (!platform.trim()) newErrors.platform = 'Platform is required'; + if (baseModels.length === 0) + newErrors.baseModels = 'At least one Base Model is required'; + if (!maturity.trim()) newErrors.maturity = 'Maturity is required'; + + return newErrors; +}; diff --git a/dataset-processor/dataset_processor.py b/dataset-processor/dataset_processor.py index 456a0d81..3d9ecb86 100644 --- a/dataset-processor/dataset_processor.py +++ b/dataset-processor/dataset_processor.py @@ -495,6 +495,7 @@ def process_handler(self, dgId, newDgId, cookie, updateType, savedFilePath, patc chunk_updates = {} for entry in cleaned_patch_payload: rowId = entry.get("rowId") + rowId = int(rowId) chunkNum = (rowId - 1) // 5 + 1 if chunkNum not in chunk_updates: chunk_updates[chunkNum] = [] @@ -506,6 +507,7 @@ def process_handler(self, dgId, newDgId, cookie, updateType, savedFilePath, patc print(f"Chunk {chunkNum} downloaded successfully") for entry in entries: rowId = entry.get("rowId") + rowId = int(rowId) for idx, chunk_entry in enumerate(chunk_data): if chunk_entry.get("rowId") == rowId: chunk_data[idx] = entry @@ -522,6 +524,7 @@ def process_handler(self, dgId, newDgId, cookie, updateType, savedFilePath, patc print("Aggregated dataset for patch update retrieved successfully") for entry in cleaned_patch_payload: rowId = entry.get("rowId") + rowId = int(rowId) for index, item in enumerate(agregated_dataset): if item.get("rowId") == rowId: entry["rowId"] = rowId From b090c22a87dfa9d9cc2718e03221aca0af898331 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Mon, 5 Aug 2024 15:02:32 +0530 Subject: [PATCH 361/582] ESCLASS-162: Re-training API and model train status API updated --- DSL/Resql/get-data-model-by-id.sql | 2 + DSL/Resql/update-data-model-training-data.sql | 7 ++ .../POST/classifier/datamodel/re-train.yml | 91 ++++++++++++++ .../datamodel/update/training/status.yml | 113 ++++++++++++++++++ .../datasetgroup/update/validation/status.yml | 4 +- 5 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 DSL/Resql/get-data-model-by-id.sql create mode 100644 DSL/Resql/update-data-model-training-data.sql create mode 100644 DSL/Ruuter.private/DSL/POST/classifier/datamodel/re-train.yml create mode 100644 DSL/Ruuter.private/DSL/POST/classifier/datamodel/update/training/status.yml diff --git a/DSL/Resql/get-data-model-by-id.sql b/DSL/Resql/get-data-model-by-id.sql new file mode 100644 index 00000000..4d5f6dbc --- /dev/null +++ b/DSL/Resql/get-data-model-by-id.sql @@ -0,0 +1,2 @@ +SELECT id +FROM models_metadata WHERE id =:id; \ No newline at end of file diff --git a/DSL/Resql/update-data-model-training-data.sql b/DSL/Resql/update-data-model-training-data.sql new file mode 100644 index 00000000..9fad3c72 --- /dev/null +++ b/DSL/Resql/update-data-model-training-data.sql @@ -0,0 +1,7 @@ +UPDATE models_metadata +SET + training_status = :training_status::Training_Status, + model_s3_location = :model_s3_location, + last_trained_timestamp = :last_trained_timestamp::timestamp with time zone, + training_results = :training_results::jsonb +WHERE id = :id; diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/re-train.yml b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/re-train.yml new file mode 100644 index 00000000..78a43453 --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/re-train.yml @@ -0,0 +1,91 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'RE-TRAIN'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: modelId + type: number + description: "Body field 'modelId'" + headers: + - field: cookie + type: string + description: "Cookie field" + +extract_request_data: + assign: + model_id: ${incoming.body.modelId} + cookie: ${incoming.headers.cookie} + next: check_for_request_data + +check_for_request_data: + switch: + - condition: ${model_id !== null} + next: get_data_model_by_id + next: return_incorrect_request + +get_data_model_by_id: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-data-model-by-id" + body: + id: ${model_id} + result: res_model + next: check_data_model_status + +check_data_model_status: + switch: + - condition: ${200 <= res_model.response.statusCodeValue && res_model.response.statusCodeValue < 300} + next: check_data_model_exist + next: assign_fail_response + +check_data_model_exist: + switch: + - condition: ${res_model.response.body.length>0} + next: execute_cron_manager + next: assign_fail_response + +execute_cron_manager: + call: reflect.mock + args: + url: "[#CLASSIFIER_CRON_MANAGER]/execute/data_model/model_trainer" + query: + cookie: ${cookie} + modelId: ${model_id} + result: res + next: assign_success_response + +assign_success_response: + assign: + format_res: { + modelId: '${model_id}', + operationSuccessful: true, + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + modelId: '${model_id}', + operationSuccessful: false, + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_incorrect_request: + status: 400 + return: 'Missing Required Fields' + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update/training/status.yml b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update/training/status.yml new file mode 100644 index 00000000..70430734 --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update/training/status.yml @@ -0,0 +1,113 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'STATUS'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: modelId + type: number + description: "Body field 'modelId'" + - field: trainingStatus + type: string + description: "Body field 'trainingStatus'" + - field: modelS3Location + type: string + description: "Body field 'modelS3Location'" + - field: lastTrainedTimestamp + type: number + description: "Body field 'lastTrainedTimeStamp'" + - field: trainingResults + type: json + description: "Body field 'trainingResults'" + +extract_request_data: + assign: + model_id: ${incoming.body.modelId} + training_status: ${incoming.body.trainingStatus} + model_s3_location: ${incoming.body.modelS3Location} + last_trained_timestamp: ${incoming.body.lastTrainedTimestamp} + training_results: ${incoming.body.trainingResults} + next: check_for_request_data + +check_for_request_data: + switch: + - condition: ${model_id !== null && training_status !== null && model_s3_location !== null} + next: get_data_model_by_id + next: return_incorrect_request + +get_data_model_by_id: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-data-model-by-id" + body: + id: ${model_id} + result: res_model + next: check_data_model_status + +check_data_model_status: + switch: + - condition: ${200 <= res_model.response.statusCodeValue && res_model.response.statusCodeValue < 300} + next: check_data_model_exist + next: assign_fail_response + +check_data_model_exist: + switch: + - condition: ${res_model.response.body.length>0} + next: update_training_data + next: assign_fail_response + +update_training_data: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/update-data-model-training-data" + body: + id: ${model_id} + training_status: ${training_status} + model_s3_location: ${model_s3_location} + last_trained_timestamp: ${new Date(last_trained_timestamp).toISOString()} + training_results: ${JSON.stringify(training_results)} + result: res_update + next: check_data_model_update_status + +check_data_model_update_status: + switch: + - condition: ${200 <= res_update.response.statusCodeValue && res_update.response.statusCodeValue < 300} + next: assign_success_response + next: assign_fail_response + +assign_success_response: + assign: + format_res: { + modelId: '${model_id}', + trainingStatus: '${training_status}', + operationSuccessful: true, + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + modelId: '${model_id}', + trainingStatus: '', + operationSuccessful: false, + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_incorrect_request: + status: 400 + return: 'Missing Required Fields' + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/validation/status.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/validation/status.yml index e594350d..0dd27f2e 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/validation/status.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/validation/status.yml @@ -9,10 +9,10 @@ declaration: allowlist: body: - field: dgId - type: string + type: number description: "Body field 'dgId'" - field: newDgId - type: string + type: number description: "Body field 'newDgId'" - field: updateType type: string From 41312d5537700ef8de74c8ce3c6887f45ccd4d0e Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Mon, 5 Aug 2024 15:44:30 +0530 Subject: [PATCH 362/582] data models create and configure uis --- .../FormElements/FormCheckboxes/index.tsx | 28 ++-- .../FormElements/FormRadios/index.tsx | 8 +- .../molecules/DataModelForm/index.tsx | 110 +++++---------- .../pages/DataModels/ConfigureDataModel.tsx | 133 +++++++++--------- GUI/src/pages/DataModels/CreateDataModel.tsx | 1 + 5 files changed, 123 insertions(+), 157 deletions(-) diff --git a/GUI/src/components/FormElements/FormCheckboxes/index.tsx b/GUI/src/components/FormElements/FormCheckboxes/index.tsx index a70561d2..ab3df8ef 100644 --- a/GUI/src/components/FormElements/FormCheckboxes/index.tsx +++ b/GUI/src/components/FormElements/FormCheckboxes/index.tsx @@ -1,4 +1,4 @@ -import { ChangeEvent, FC, useId, useState } from 'react'; +import { ChangeEvent, FC, useId, useState, useEffect } from 'react'; import './FormCheckboxes.scss'; @@ -13,6 +13,7 @@ type FormCheckboxesType = { }[]; isStack?: boolean; error?: string; + selectedValues?: string[]; // New prop for selected values }; const FormCheckboxes: FC = ({ @@ -23,26 +24,25 @@ const FormCheckboxes: FC = ({ items, isStack = true, error, + selectedValues = [], // Default to an empty array if not provided }) => { const id = useId(); - const [selectedValues, setSelectedValues] = useState< - Record - >({ [name]: [] }); + const [internalSelectedValues, setInternalSelectedValues] = useState(selectedValues); + + useEffect(() => { + setInternalSelectedValues(selectedValues); + }, [selectedValues]); const handleValuesChange = (e: ChangeEvent) => { const { checked, value } = e.target; - setSelectedValues((prevState) => { - const newValues = checked - ? [...prevState[name], value] // Add the checked value to the array - : prevState[name].filter((v: string) => v !== value); // Remove the unchecked value from the array - - const updatedValues = { ...prevState, [name]: newValues }; + const newValues = checked + ? [...internalSelectedValues, value] // Add the checked value to the array + : internalSelectedValues.filter((v: string) => v !== value); // Remove the unchecked value from the array - if (onValuesChange) onValuesChange(updatedValues); + setInternalSelectedValues(newValues); - return updatedValues; - }); + if (onValuesChange) onValuesChange({ [name]: newValues }); }; return ( @@ -61,7 +61,7 @@ const FormCheckboxes: FC = ({ id={`${id}-${item.value}`} value={item.value} onChange={handleValuesChange} - checked={selectedValues[name].includes(item.value)} // Manage checkbox state + checked={internalSelectedValues.includes(item.value)} // Manage checkbox state /> diff --git a/GUI/src/components/FormElements/FormRadios/index.tsx b/GUI/src/components/FormElements/FormRadios/index.tsx index 6ee27e4e..301f941b 100644 --- a/GUI/src/components/FormElements/FormRadios/index.tsx +++ b/GUI/src/components/FormElements/FormRadios/index.tsx @@ -1,5 +1,4 @@ import { FC, useId } from 'react'; - import './FormRadios.scss'; type FormRadiosType = { @@ -11,8 +10,9 @@ type FormRadiosType = { value: string; }[]; onChange: (selectedValue: string) => void; + selectedValue?: string; // New prop for the selected value isStack?: boolean; - error?:string + error?: string; }; const FormRadios: FC = ({ @@ -21,8 +21,9 @@ const FormRadios: FC = ({ hideLabel, items, onChange, + selectedValue, // Use selectedValue to control the selected radio button isStack = false, - error + error, }) => { const id = useId(); @@ -41,6 +42,7 @@ const FormRadios: FC = ({ name={name} id={`${id}-${item.value}`} value={item.value} + checked={selectedValue === item.value} // Check if the radio button should be selected onChange={(event) => { onChange(event.target.value); }} diff --git a/GUI/src/components/molecules/DataModelForm/index.tsx b/GUI/src/components/molecules/DataModelForm/index.tsx index de97f584..65f6e5fe 100644 --- a/GUI/src/components/molecules/DataModelForm/index.tsx +++ b/GUI/src/components/molecules/DataModelForm/index.tsx @@ -1,58 +1,23 @@ -import { FC, useState } from 'react'; +import { FC } from 'react'; import { useTranslation } from 'react-i18next'; import { FormCheckboxes, FormInput, FormRadios, FormSelect, Label } from 'components'; -import { DatasetGroup } from 'types/datasetGroups'; -import { useNavigate } from 'react-router-dom'; -import { useMutation, useQuery } from '@tanstack/react-query'; -import { createDatasetGroup } from 'services/datasets'; -import { useDialog } from 'hooks/useDialog'; -import { getCreateOptions } from 'services/data-models'; import { customFormattedArray, formattedArray } from 'utils/commonUtilts'; -import { validateDataModel } from 'utils/dataModelsUtils'; +import { useQuery } from '@tanstack/react-query'; +import { getCreateOptions } from 'services/data-models'; type DataModelFormType = { dataModel: any; - handleChange:any - + handleChange: (name: string, value: any) => void; + errors: Record; }; -const DataModelForm: FC = ({dataModel,handleChange}) => { - const { t } = useTranslation(); - const { open } = useDialog(); - const [isModalOpen, setIsModalOpen] = useState(false); - const [modalType, setModalType] = useState(''); - const navigate = useNavigate(); +const DataModelForm: FC = ({ dataModel, handleChange, errors }) => { + const { t } = useTranslation(); + const { data: createOptions } = useQuery(['datamodels/create-options'], () => getCreateOptions() ); - const [errors, setErrors] = useState({ - modelName: '', - dgName: '', - platform: '', - baseModels: '', - maturity: '', - }); - - const validateData = () => { - - setErrors(validateDataModel(dataModel)); - }; - - const createDatasetGroupMutation = useMutation({ - mutationFn: (data: DatasetGroup) => createDatasetGroup(data), - onSuccess: async (response) => { - setIsModalOpen(true); - setModalType('SUCCESS'); - }, - onError: () => { - open({ - title: 'Dataset Group Creation Unsuccessful', - content:

    Something went wrong. Please try again.

    , - }); - }, - }); - return (
    @@ -60,70 +25,65 @@ const DataModelForm: FC = ({dataModel,handleChange}) => { - handleChange('modelName', e.target.value) - } - error={errors.modelName} - > + value={dataModel.modelName} + onChange={(e) => handleChange('modelName', e.target.value)} + error={errors?.modelName} + />
    Model Version
    + {createOptions && (
    Select Dataset Group
    - handleChange('dgName', selection.value) - } - error={errors.dgName} - > + onSelectionChange={(selection) => handleChange('dgName', selection.value)} + error={errors?.dgName} + value={dataModel?.dgName} + />
    -
    Select Deployment Platform
    + +
    Select Base Models
    - handleChange('baseModels', values?.dataset) - } - error={errors.baseModels} - > + onValuesChange={(values) => handleChange('baseModels', values.baseModels)} + error={errors?.baseModels} + selectedValues={dataModel?.baseModels} + />
    +
    Select Deployment Platform
    - handleChange('platform', value) - } - error={errors.platform} - > + onChange={(value) => handleChange('platform', value)} + error={errors?.platform} + selectedValue={dataModel?.platform} + />
    +
    Select Maturity Label
    - handleChange('maturity', value) - } - error={errors.maturity} - > + onChange={(value) => handleChange('maturity', value)} + error={errors?.maturity} + selectedValue={dataModel?.maturity} + />
    )} diff --git a/GUI/src/pages/DataModels/ConfigureDataModel.tsx b/GUI/src/pages/DataModels/ConfigureDataModel.tsx index da0732af..b75dd97b 100644 --- a/GUI/src/pages/DataModels/ConfigureDataModel.tsx +++ b/GUI/src/pages/DataModels/ConfigureDataModel.tsx @@ -1,44 +1,46 @@ import { FC, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Button, Card } from 'components'; -import { DatasetGroup } from 'types/datasetGroups'; +import { useQuery } from '@tanstack/react-query'; import { Link, useNavigate } from 'react-router-dom'; -import './DataModels.scss'; -import { useMutation, useQuery } from '@tanstack/react-query'; -import { createDatasetGroup } from 'services/datasets'; +import { Button, Card } from 'components'; import { useDialog } from 'hooks/useDialog'; import BackArrowButton from 'assets/BackArrowButton'; import { getMetadata } from 'services/data-models'; -import { validateDataModel } from 'utils/dataModelsUtils'; import DataModelForm from 'components/molecules/DataModelForm'; +import { validateDataModel } from 'utils/dataModelsUtils'; type ConfigureDataModelType = { id: number; }; + const ConfigureDataModel: FC = ({ id }) => { - const { t } = useTranslation(); const { open } = useDialog(); - const [isModalOpen, setIsModalOpen] = useState(false); - const [modalType, setModalType] = useState(''); const navigate = useNavigate(); - - - const { data: dataModelData } = useQuery(['datamodels/metadata',id], () => - getMetadata(id), - ); const [dataModel, setDataModel] = useState({ - modelName: dataModelData?.modelName, - dgName: dataModelData?.connectedDgName, - platform: dataModelData?.deploymentEnv, - baseModels: dataModelData?.baseModels, - maturity: dataModelData?.maturityLabel, + modelName: '', + dgName: '', + platform: '', + baseModels: [], + maturity: '', }); - const handleDataModelAttributesChange = (name: string, value: string) => { - + const { data: dataModelData } = useQuery(['datamodels/metadata', id], () => + getMetadata(id), + { + onSuccess: (data) => { + setDataModel({ + modelName: data?.modelName || '', + dgName: data?.connectedDgName || '', + platform: data?.deploymentEnv || '', + baseModels: data?.baseModels || [], + maturity: data?.maturityLabel || '', + }); + }, + } + ); - setDataModel((prevFilters) => ({ - ...prevFilters, + const handleDataModelAttributesChange = (name: string, value: any) => { + setDataModel((prevDataModel) => ({ + ...prevDataModel, [name]: value, })); }; @@ -52,60 +54,61 @@ const ConfigureDataModel: FC = ({ id }) => { }); const validateData = () => { - - setErrors(validateDataModel(dataModel)); + const validationErrors = validateDataModel(dataModel); + setErrors(validationErrors); + return Object.keys(validationErrors).length === 0; }; - const createDatasetGroupMutation = useMutation({ - mutationFn: (data: DatasetGroup) => createDatasetGroup(data), - onSuccess: async (response) => { - setIsModalOpen(true); - setModalType('SUCCESS'); - }, - onError: () => { + const handleSave = () => { + console.log(dataModel); + + if (validateData()) { + // Trigger the mutation or save action + } else { open({ - title: 'Dataset Group Creation Unsuccessful', - content:

    Something went wrong. Please try again.

    , + title: 'Validation Error', + content:

    Please correct the errors and try again.

    , }); - }, - }); + } + }; return (
    -
    - navigate(0)}> - - -
    Configure Data Model
    -
    +
    + navigate(0)}> + + +
    Configure Data Model
    +
    - -
    -
    -
    - No Data Available -
    -

    - You have created the dataset group, but there are no datasets - available to show here. You can upload a dataset to view it in - this space. Once added, you can edit or delete the data as - needed. -

    - + +
    +
    +
    + No Data Available
    +

    + You have created the dataset group, but there are no datasets + available to show here. You can upload a dataset to view it in + this space. Once added, you can edit or delete the data as + needed. +

    +
    - +
    +
    +
    = ({ id }) => { background: 'white', }} > - + diff --git a/GUI/src/pages/DataModels/CreateDataModel.tsx b/GUI/src/pages/DataModels/CreateDataModel.tsx index d8181cd9..022253e6 100644 --- a/GUI/src/pages/DataModels/CreateDataModel.tsx +++ b/GUI/src/pages/DataModels/CreateDataModel.tsx @@ -57,6 +57,7 @@ const CreateDataModel: FC = () => { }); const validateData = () => { + console.log(dataModel); setErrors(validateDataModel(dataModel)); }; From 7f33d53582549e90cf18c3ccc220c33cb1551720 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Mon, 5 Aug 2024 16:15:08 +0530 Subject: [PATCH 363/582] ui refactoring --- GUI/src/components/MainNavigation/index.tsx | 98 +-- .../TreeNode/ClassHeirarchyTreeNode.tsx | 87 ++ .../ClassHeirarchy/TreeNode/index.css | 9 + .../molecules/ClassHeirarchy/index.tsx | 141 +-- .../CreateDatasetGroupModal.tsx | 94 ++ .../DatasetDetailedViewTable.tsx | 214 +++++ .../molecules/DatasetGroupCard/index.tsx | 112 ++- .../molecules/IntegrationCard/index.tsx | 161 +--- .../IntegrationModals/IntegrationModals.tsx | 157 ++++ .../ValidationAndHierarchyCards.tsx | 69 ++ .../ValidationCriteria/CardsView.tsx | 154 +--- .../DraggableItem/DraggableItem.tsx | 139 +++ .../molecules/ValidationCriteria/RowsView.tsx | 29 +- .../ValidationStatus/ValidationStatus.tsx | 43 + .../ViewDatasetGroupModalController.tsx | 227 +++++ .../styles.scss | 13 + GUI/src/enums/commonEnums.ts | 7 + GUI/src/enums/datasetEnums.ts | 38 + GUI/src/enums/roles.ts | 6 +- .../DatasetGroups/CreateDatasetGroup.tsx | 106 +-- .../pages/DatasetGroups/DatasetGroups.scss | 9 +- .../pages/DatasetGroups/ViewDatasetGroup.tsx | 807 ++++++------------ GUI/src/pages/DatasetGroups/index.tsx | 234 ++--- GUI/src/pages/Integrations/index.tsx | 54 +- GUI/src/pages/UserManagement/UserModal.tsx | 86 +- GUI/src/pages/UserManagement/index.tsx | 209 +++-- GUI/src/services/datasets.ts | 107 +-- GUI/src/types/datasetGroups.ts | 89 +- GUI/src/types/integration.ts | 8 +- GUI/src/types/mainNavigation.ts | 4 +- GUI/src/utils/commonUtilts.ts | 4 + GUI/src/utils/dataTableUtils.ts | 34 + GUI/src/utils/datasetGroupsUtils.ts | 32 +- GUI/src/utils/endpoints.ts | 20 +- GUI/src/utils/queryKeys.ts | 34 + GUI/translations/en/common.json | 187 +++- GUI/translations/et/common.json | 561 ++++++------ 37 files changed, 2570 insertions(+), 1813 deletions(-) create mode 100644 GUI/src/components/molecules/ClassHeirarchy/TreeNode/ClassHeirarchyTreeNode.tsx create mode 100644 GUI/src/components/molecules/ClassHeirarchy/TreeNode/index.css create mode 100644 GUI/src/components/molecules/CreateDatasetGroupModals/CreateDatasetGroupModal.tsx create mode 100644 GUI/src/components/molecules/DatasetDetailedViewTable/DatasetDetailedViewTable.tsx create mode 100644 GUI/src/components/molecules/IntegrationModals/IntegrationModals.tsx create mode 100644 GUI/src/components/molecules/ValidationAndHierarchyCards/ValidationAndHierarchyCards.tsx create mode 100644 GUI/src/components/molecules/ValidationCriteria/DraggableItem/DraggableItem.tsx create mode 100644 GUI/src/components/molecules/ValidationStatus/ValidationStatus.tsx create mode 100644 GUI/src/components/molecules/ViewDatasetGroupModalController/ViewDatasetGroupModalController.tsx create mode 100644 GUI/src/components/molecules/ViewDatasetGroupModalController/styles.scss create mode 100644 GUI/src/enums/datasetEnums.ts create mode 100644 GUI/src/utils/dataTableUtils.ts diff --git a/GUI/src/components/MainNavigation/index.tsx b/GUI/src/components/MainNavigation/index.tsx index 2a2e432f..c6906827 100644 --- a/GUI/src/components/MainNavigation/index.tsx +++ b/GUI/src/components/MainNavigation/index.tsx @@ -1,4 +1,4 @@ -import { FC, MouseEvent, useEffect, useState } from 'react'; +import { FC, MouseEvent, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { NavLink, useLocation } from 'react-router-dom'; import { @@ -16,6 +16,9 @@ import { Icon } from 'components'; import type { MenuItem } from 'types/mainNavigation'; import './MainNavigation.scss'; import apiDev from 'services/api-dev'; +import { userManagementEndpoints } from 'utils/endpoints'; +import { integrationQueryKeys } from 'utils/queryKeys'; +import { ROLES } from 'enums/roles'; const MainNavigation: FC = () => { const { t } = useTranslation(); @@ -74,99 +77,76 @@ const MainNavigation: FC = () => { }, ]; - const getUserRole = () => { - apiDev - .get(`/accounts/user-role`) - .then((res: any) => { - const filteredItems = - items.filter((item) => { - const role = res?.data?.response[0]; - - switch (role) { - case 'ROLE_ADMINISTRATOR': - return item.id; - case 'ROLE_MODEL_TRAINER': - return ( - item.id !== 'userManagement' && item.id !== 'integration' - ); - case 'ROLE_UNAUTHENTICATED': - return null; - } - }) ?? []; - setMenuItems(filteredItems); - }) - .catch((error: any) => console.log(error)); + const filterItemsByRole = (role: string, items: MenuItem[]) => { + return items?.filter((item) => { + switch (role) { + case ROLES.ROLE_ADMINISTRATOR: + return item?.id; + case ROLES.ROLE_MODEL_TRAINER: + return item?.id !== 'userManagement' && item?.id !== 'integration'; + case 'ROLE_UNAUTHENTICATED': + return false; + default: + return false; + } + }); }; - useEffect(() => { - getUserRole(); - }, []); - - useQuery({ - queryKey: ['/accounts/user-role', 'prod'], - onSuccess: (res: any) => { - const filteredItems = - items.filter((item) => { - const role = res?.response[0]; - - switch (role) { - case 'ROLE_ADMINISTRATOR': - return item.id; - case 'ROLE_MODEL_TRAINER': - return ( - item.id !== 'userManagement' && item.id !== 'integration' - ); - case 'ROLE_UNAUTHENTICATED': - return null; - } - }) ?? []; - setMenuItems(filteredItems); + useQuery(integrationQueryKeys.USER_ROLES(), { + queryFn: async () => { + const res = await apiDev.get(userManagementEndpoints.FETCH_USER_ROLES()); + return res?.data?.response; + }, + onSuccess: (res) => { + const role = res[0]; + const filteredItems = filterItemsByRole(role, items); + setMenuItems(filteredItems); + }, + onError: (error) => { + console.error('Error fetching user roles:', error); }, }); - - const location = useLocation(); const [navCollapsed, setNavCollapsed] = useState(false); const handleNavToggle = (event: MouseEvent) => { const isExpanded = - event.currentTarget.getAttribute('aria-expanded') === 'true'; - event.currentTarget.setAttribute( + event?.currentTarget?.getAttribute('aria-expanded') === 'true'; + event?.currentTarget?.setAttribute( 'aria-expanded', isExpanded ? 'false' : 'true' ); }; const renderMenuTree = (menuItems: MenuItem[]) => { - return menuItems.map((menuItem) => ( -
  • - {menuItem.children ? ( + return menuItems?.map((menuItem) => ( +
  • + {menuItem?.children ? (
      - {renderMenuTree(menuItem.children)} + {renderMenuTree(menuItem?.children)}
    ) : ( - + {' '} - {menuItem.label} + {menuItem?.label} )}
  • diff --git a/GUI/src/components/molecules/ClassHeirarchy/TreeNode/ClassHeirarchyTreeNode.tsx b/GUI/src/components/molecules/ClassHeirarchy/TreeNode/ClassHeirarchyTreeNode.tsx new file mode 100644 index 00000000..702aeecc --- /dev/null +++ b/GUI/src/components/molecules/ClassHeirarchy/TreeNode/ClassHeirarchyTreeNode.tsx @@ -0,0 +1,87 @@ +import { FormInput } from 'components/FormElements'; +import React, { ChangeEvent, useState } from 'react'; +import { TreeNode } from 'types/datasetGroups'; +import { isClassHierarchyDuplicated } from 'utils/datasetGroupsUtils'; +import { MdDeleteOutline } from 'react-icons/md'; +import { useTranslation } from 'react-i18next'; +import './index.css'; + +const ClassHeirarchyTreeNode = ({ + node, + onAddSubClass, + onDelete, + setNodesError, + nodesError, + nodes, +}: { + onAddSubClass: (parentId: number | string) => void; + onDelete: (nodeId: number | string) => void; + setNodesError: React.Dispatch>; + nodes?: TreeNode[]; + node: TreeNode; + nodesError?: boolean; +}) => { + const { t } = useTranslation(); + + const [fieldName, setFieldName] = useState(node.fieldName); + + const handleChange = (e: ChangeEvent) => { + setFieldName(e.target.value); + node.fieldName = e.target.value; + if (isClassHierarchyDuplicated(nodes, e.target.value)) setNodesError(true); + else setNodesError(false); + }; + + return ( +
    +
    +
    + +
    +
    onAddSubClass(node?.id)} + className="link" + > + {t('datasetGroups.classHierarchy.addSubClass')} +
    +
    onDelete(node?.id)} + className="link" + > + {t('global.delete')} +
    +
    + {node?.children && + node?.children?.map((child) => ( + + ))} +
    + ); +}; + +export default ClassHeirarchyTreeNode; diff --git a/GUI/src/components/molecules/ClassHeirarchy/TreeNode/index.css b/GUI/src/components/molecules/ClassHeirarchy/TreeNode/index.css new file mode 100644 index 00000000..3cf9a08b --- /dev/null +++ b/GUI/src/components/molecules/ClassHeirarchy/TreeNode/index.css @@ -0,0 +1,9 @@ +.container-wrapper { + width: 450px; + margin-top: 10px; +} + +.link { + display: flex; + align-items: center; +} \ No newline at end of file diff --git a/GUI/src/components/molecules/ClassHeirarchy/index.tsx b/GUI/src/components/molecules/ClassHeirarchy/index.tsx index 402c33c5..001ab51c 100644 --- a/GUI/src/components/molecules/ClassHeirarchy/index.tsx +++ b/GUI/src/components/molecules/ClassHeirarchy/index.tsx @@ -1,15 +1,15 @@ import React, { FC, PropsWithChildren, useState } from 'react'; -import { MdDeleteOutline } from 'react-icons/md'; -import { FormInput } from 'components/FormElements'; import Button from 'components/Button'; import { v4 as uuidv4 } from 'uuid'; import './index.css'; import Dialog from 'components/Dialog'; -import { Class } from 'types/datasetGroups'; -import { isClassHierarchyDuplicated } from 'utils/datasetGroupsUtils'; +import { Class, TreeNode } from 'types/datasetGroups'; +import { useTranslation } from 'react-i18next'; +import { ButtonAppearanceTypes } from 'enums/commonEnums'; +import ClassHeirarchyTreeNode from './TreeNode/ClassHeirarchyTreeNode'; type ClassHierarchyProps = { - nodes?: Class[]; + nodes: TreeNode[]; setNodes: React.Dispatch>; nodesError?: boolean; setNodesError: React.Dispatch>; @@ -21,71 +21,10 @@ const ClassHierarchy: FC> = ({ nodesError, setNodesError, }) => { - const [currentNode, setCurrentNode] = useState(null); + const { t } = useTranslation(); + const [currentNode, setCurrentNode] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); - const TreeNode = ({ node, onAddSubClass, onDelete }) => { - const [fieldName, setFieldName] = useState(node.fieldName); - - const handleChange = (e) => { - setFieldName(e.target.value); - node.fieldName = e.target.value; - if (isClassHierarchyDuplicated(nodes, e.target.value)) - setNodesError(true); - else setNodesError(false); - }; - - return ( -
    - - {node.children && - node.children.map((child) => ( - - ))} -
    - ); - }; - const addMainClass = () => { setNodes([ ...nodes, @@ -93,9 +32,9 @@ const ClassHierarchy: FC> = ({ ]); }; - const addSubClass = (parentId) => { - const addSubClassRecursive = (nodes) => { - return nodes?.map((node) => { + const addSubClass = (parentId: number | string) => { + const addSubClassRecursive = (nodes: TreeNode[]): TreeNode[] => { + return nodes?.map((node: TreeNode) => { if (node.id === parentId) { const newNode = { id: uuidv4(), @@ -111,43 +50,43 @@ const ClassHierarchy: FC> = ({ return node; }); }; - setNodes(addSubClassRecursive(nodes)); + if (nodes) setNodes(addSubClassRecursive(nodes)); setNodesError(false); }; - const deleteNode = (nodeId) => { - const deleteNodeRecursive = (nodes) => { + const deleteNode = (nodeId: number | string) => { + const deleteNodeRecursive = (nodes: TreeNode[]): TreeNode[] => { return nodes - .map((node) => { - if (node.children.length > 0) { - return { ...node, children: deleteNodeRecursive(node.children) }; + ?.map((node: TreeNode) => { + if (node?.children?.length > 0) { + return { ...node, children: deleteNodeRecursive(node?.children) }; } return node; }) - .filter((node) => { - if (node.id === nodeId) { - if (node.children.length > 0 || node.fieldName) { + ?.filter((node: TreeNode) => { + if (node?.id === nodeId) { + if (node?.children?.length > 0 || node?.fieldName) { setCurrentNode(node); setIsModalOpen(true); - return true; // Keep the node for now, until user confirms deletion + return true; } } return !( - node.id === nodeId && - node.children.length === 0 && - !node.fieldName + node?.id === nodeId && + node?.children?.length === 0 && + !node?.fieldName ); }); }; - setNodes(deleteNodeRecursive(nodes)); + if (nodes) setNodes(deleteNodeRecursive(nodes)); }; const confirmDeleteNode = () => { - const deleteNodeRecursive = (nodes) => { - return nodes?.filter((node) => { - if (node.id === currentNode.id) { - return false; // Remove this node + const deleteNodeRecursive = (nodes: TreeNode[]) => { + return nodes?.filter((node: TreeNode) => { + if (currentNode && node.id === currentNode.id) { + return false; } if (node.children.length > 0) { node.children = deleteNodeRecursive(node.children); @@ -164,38 +103,44 @@ const ClassHierarchy: FC> = ({ return (
    - +
    {nodes?.map((node) => ( - ))}
    -
    } onClose={() => setIsModalOpen(false)} > - Confirm that you are wish to delete the following record. This will - delete the current class and all subclasses of it + {t('datasetGroups.modals.deleteClaassDesc') ?? ''}
    ); diff --git a/GUI/src/components/molecules/CreateDatasetGroupModals/CreateDatasetGroupModal.tsx b/GUI/src/components/molecules/CreateDatasetGroupModals/CreateDatasetGroupModal.tsx new file mode 100644 index 00000000..bb63eaa7 --- /dev/null +++ b/GUI/src/components/molecules/CreateDatasetGroupModals/CreateDatasetGroupModal.tsx @@ -0,0 +1,94 @@ +import { CreateDatasetGroupModals } from 'enums/datasetEnums'; +import { Button, Dialog } from 'components'; +import { ButtonAppearanceTypes } from 'enums/commonEnums'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; +import { useDialog } from 'hooks/useDialog'; + +const CreateDatasetGroupModalController = ({ + modalType, + isModalOpen, + setIsModalOpen, +}: { + modalType: CreateDatasetGroupModals; + isModalOpen: boolean; + setIsModalOpen: React.Dispatch>; +}) => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const { open, close } = useDialog(); + + const opneValidationErrorModal = (modalType: CreateDatasetGroupModals) => { + open({ + title: t('datasetGroups.modals.columnInsufficientHeader') ?? "", + content: ( +

    + {t('datasetGroups.modals.columnInsufficientDescription')} +

    + ), + footer: ( +
    + + +
    + ) + }) + } + return ( + <> + {modalType === CreateDatasetGroupModals.VALIDATION_ERROR && ( + + + +
    + } + onClose={() => setIsModalOpen(false)} + > + {t('datasetGroups.modals.columnInsufficientDescription')} + + )} + {modalType === CreateDatasetGroupModals.SUCCESS && ( + + + +
    + } + onClose={() => setIsModalOpen(false)} + > + {t('datasetGroups.modals.createDatasetSucceessDesc')} + + )} + + ); +}; + +export default CreateDatasetGroupModalController; diff --git a/GUI/src/components/molecules/DatasetDetailedViewTable/DatasetDetailedViewTable.tsx b/GUI/src/components/molecules/DatasetDetailedViewTable/DatasetDetailedViewTable.tsx new file mode 100644 index 00000000..a403c57b --- /dev/null +++ b/GUI/src/components/molecules/DatasetDetailedViewTable/DatasetDetailedViewTable.tsx @@ -0,0 +1,214 @@ +import BackArrowButton from 'assets/BackArrowButton'; +import { Button, Card, DataTable, Icon, Label, Switch } from 'components'; +import { ButtonAppearanceTypes, LabelType } from 'enums/commonEnums'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Link, useNavigate } from 'react-router-dom'; +import DatasetValidationStatus from '../ValidationStatus/ValidationStatus'; +import { ViewDatasetGroupModalContexts } from 'enums/datasetEnums'; +import { generateDynamicColumns } from 'utils/dataTableUtils'; +import { MdOutlineDeleteOutline, MdOutlineEdit } from 'react-icons/md'; +import { CellContext, ColumnDef, PaginationState } from '@tanstack/react-table'; +import { getDatasets } from 'services/datasets'; +import { DatasetDetails, MetaData, SelectedRowPayload } from 'types/datasetGroups'; + +const DatasetDetailedViewTable = ({ + metadata, + handleOpenModals, + bannerMessage, + datasets, + isLoading, + updatedDataset, + setSelectedRow, + pagination, + setPagination, + dgId, +}: { + metadata: MetaData[]; + handleOpenModals: (context: ViewDatasetGroupModalContexts) => void; + bannerMessage: string; + datasets: DatasetDetails | undefined; + isLoading: boolean; + updatedDataset: any; + setSelectedRow: React.Dispatch>; + pagination: PaginationState; + setPagination: React.Dispatch>; + dgId: number; +}) => { + const { t } = useTranslation(); + const navigate = useNavigate(); + + const editView = (props: CellContext) => { + return ( + + ); + }; + + const deleteView = (props: CellContext) => ( + + ); + + const dataColumns = useMemo( + () => generateDynamicColumns(datasets?.fields ?? [], editView, deleteView), + [datasets?.fields] + ); + + return ( + <> + {metadata && ( +
    + +
    + navigate(0)}> + + +
    {metadata?.[0]?.name}
    + {metadata && ( + + )} + {metadata?.[0]?.latest ? ( + + ) : null} + +
    + +
    + } + > +
    +
    +

    + {t('datasetGroups.detailedView.connectedModels') ?? ''} : + {metadata?.[0]?.linkedModels?.map((model, index: number) => { + return index === metadata?.[0]?.linkedModels?.length - 1 + ? ` ${model?.modelName}` + : ` ${model?.modelName}, `; + })} +

    +

    + {t('datasetGroups.detailedView.noOfItems') ?? ''} : + {` ${metadata?.[0]?.numSamples}`} +

    +
    +
    + + +
    +
    + + {bannerMessage &&
    {bannerMessage}
    } + {(!datasets || (datasets && datasets?.numPages < 2)) && ( + +
    + {(!datasets || datasets?.dataPayload?.length === 0) && ( +
    +
    + {t('datasetGroups.detailedView.noData') ?? ''} +
    +

    {t('datasetGroups.detailedView.noDataDesc') ?? ''}

    + +
    + )} + {datasets && datasets?.numPages <= 2 && ( +
    +

    + {t( + 'datasetGroups.detailedView.insufficientExamplesDesc' + ) ?? ''} +

    + +
    + )} +
    +
    + )} +
    + )} +
    + {!isLoading && updatedDataset && ( + []} + pagination={pagination} + setPagination={(state: PaginationState) => { + if ( + state.pageIndex === pagination.pageIndex && + state.pageSize === pagination.pageSize + ) + return; + setPagination(state); + getDatasets(state, dgId); + }} + pagesCount={datasets?.numPages} + isClientSide={false} + /> + )} +
    + + ); +}; + +export default DatasetDetailedViewTable; diff --git a/GUI/src/components/molecules/DatasetGroupCard/index.tsx b/GUI/src/components/molecules/DatasetGroupCard/index.tsx index e4a576d4..ce5561fa 100644 --- a/GUI/src/components/molecules/DatasetGroupCard/index.tsx +++ b/GUI/src/components/molecules/DatasetGroupCard/index.tsx @@ -6,24 +6,27 @@ import Label from 'components/Label'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { enableDataset } from 'services/datasets'; import { useDialog } from 'hooks/useDialog'; -import { createSearchParams, URLSearchParamsInit, useNavigate } from 'react-router-dom'; import { Operation } from 'types/datasetGroups'; -import { AxiosError } from 'axios'; +import { datasetQueryKeys } from 'utils/queryKeys'; +import { DatasetViewEnum, ValidationStatus } from 'enums/datasetEnums'; +import { ButtonAppearanceTypes, LabelType } from 'enums/commonEnums'; +import { useTranslation } from 'react-i18next'; +import { formatDate } from 'utils/commonUtilts'; +import DatasetValidationStatus from '../ValidationStatus/ValidationStatus'; type DatasetGroupCardProps = { - datasetGroupId?: number|string|undefined; + datasetGroupId: number; datasetName?: string; version?: string; isLatest?: boolean; isEnabled?: boolean; enableAllowed?: boolean; - lastUpdated?: string; - lastUsed?: string; + lastUpdated?: Date | null; + lastUsed?: Date | null; validationStatus?: string; - lastModelTrained?: string; - setId?: React.Dispatch> - setView?: React.Dispatch> - + lastModelTrained?: Date | null; + setId?: React.Dispatch>; + setView?: React.Dispatch>; }; const DatasetGroupCard: FC> = ({ @@ -32,66 +35,44 @@ const DatasetGroupCard: FC> = ({ version, isLatest, isEnabled, - enableAllowed, lastUpdated, lastUsed, validationStatus, lastModelTrained, setId, - setView + setView, }) => { const queryClient = useQueryClient(); const { open } = useDialog(); - - const renderValidationStatus = (status:string|undefined) => { - if (status === 'success') { - return ; - } else if (status === 'fail') { - return ; - } else if (status === 'unvalidated') { - return ; - } else if (status === 'in-progress') { - return ; - } - }; + const { t } = useTranslation(); const datasetEnableMutation = useMutation({ - mutationFn: (data:Operation) => enableDataset(data), - onSuccess: async (response) => { - await queryClient.invalidateQueries(['datasetgroup/overview', 1]); + mutationFn: (data: Operation) => enableDataset(data), + onSuccess: async () => { + await queryClient.invalidateQueries(datasetQueryKeys.DATASET_OVERVIEW(1)); }, - onError: (error) => { + onError: () => { open({ - title: 'Cannot Enable Dataset Group', - content: ( -

    - The dataset group cannot be enabled until data is added. Please - add datasets to this group and try again. -

    - ), + title: t('datasetGroups.modals.enableDatasetTitle'), + content:

    {t('datasetGroups.modals.enableDatasetDesc')}

    , }); }, }); const datasetDisableMutation = useMutation({ - mutationFn: (data:Operation) => enableDataset(data), + mutationFn: (data: Operation) => enableDataset(data), onSuccess: async (response) => { - await queryClient.invalidateQueries(['datasetgroup/overview', 1]); + await queryClient.invalidateQueries(datasetQueryKeys.DATASET_OVERVIEW(1)); if (response?.operationSuccessful) open({ - title: 'Cannot Enable Dataset Group', - content: ( -

    - The dataset group cannot be enabled until data is added. Please - add datasets to this group and try again. -

    - ), + title: t('datasetGroups.modals.enableDatasetTitle'), + content:

    {t('datasetGroups.modals.enableDatasetDesc')}

    , }); }, onError: () => { open({ - title: 'Operation Unsuccessful', - content:

    Something went wrong. Please try again.

    , + title: t('datasetGroups.modals.errorTitle'), + content:

    {t('datasetGroups.modals.errorDesc')}

    , }); }, }); @@ -113,36 +94,47 @@ const DatasetGroupCard: FC> = ({
    -
    {datasetName}
    +
    {datasetName}
    handleCheck()} />
    - {renderValidationStatus(validationStatus)} +
    -

    - {'Last Model Trained:'} - {lastModelTrained} +

    + {t('datasetGroups.datasetCard.lastModelTrained')}:{' '} + {lastModelTrained && formatDate(lastModelTrained, 'D.M.yy-H:m')}

    -

    - {'Last Used For Training:'} - {lastUsed} +

    + {t('datasetGroups.datasetCard.lastUsedForTraining')}:{' '} + {lastUsed && formatDate(lastUsed, 'D.M.yy-H:m')}

    -

    - {'Last Updated:'} - {lastUpdated} +

    + {t('datasetGroups.datasetCard.lastUpdate')}:{' '} + {lastUpdated && formatDate(lastUpdated, 'D.M.yy-H:m')}

    - - {isLatest ? : null} + + {isLatest ? ( + + ) : null}
    -
    diff --git a/GUI/src/components/molecules/IntegrationCard/index.tsx b/GUI/src/components/molecules/IntegrationCard/index.tsx index d0cd1e0f..563f01a7 100644 --- a/GUI/src/components/molecules/IntegrationCard/index.tsx +++ b/GUI/src/components/molecules/IntegrationCard/index.tsx @@ -1,12 +1,16 @@ -import { FC, PropsWithChildren, ReactNode, useState } from 'react'; +import { + FC, + PropsWithChildren, + ReactNode, + useState, +} from 'react'; import './IntegrationCard.scss'; import { useTranslation } from 'react-i18next'; -import { Button, Card, Dialog, Switch } from 'components'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { OperationConfig } from 'types/integration'; -import { togglePlatform } from 'services/integration'; -import { AxiosError } from 'axios'; -import { INTEGRATION_MODALS, INTEGRATION_OPERATIONS } from 'enums/integrationEnums'; +import { Card, Switch } from 'components'; +import { + INTEGRATION_MODALS +} from 'enums/integrationEnums'; +import IntegrationModals from '../IntegrationModals/IntegrationModals'; type IntegrationCardProps = { logo?: ReactNode; @@ -21,11 +25,11 @@ const IntegrationCard: FC> = ({ channelDescription, isActive, }) => { - const { t } = useTranslation(); const [isModalOpen, setIsModalOpen] = useState(false); - const [modalType, setModalType] = useState(''); - const queryClient = useQueryClient(); + const [modalType, setModalType] = useState( + INTEGRATION_MODALS.NULL + ); const renderStatusIndicators = () => { //kept this, in case the logic is changed for the connected status @@ -54,36 +58,11 @@ const IntegrationCard: FC> = ({ ); }; - const platformEnableMutation = useMutation({ - mutationFn: (data: OperationConfig) => togglePlatform(data), - onSuccess: async () => { - setModalType(INTEGRATION_MODALS.INTEGRATION_SUCCESS); - await queryClient.invalidateQueries([ - 'classifier/integration/platform-status' - ]); - }, - onError: (error: AxiosError) => { - setModalType(INTEGRATION_MODALS.INTEGRATION_ERROR); - }, - }); - - const platformDisableMutation = useMutation({ - mutationFn: (data: OperationConfig) => togglePlatform(data), - onSuccess: async () => { - await queryClient.invalidateQueries([ - 'classifier/integration/platform-status' - ]); - setIsModalOpen(false) }, - onError: (error: AxiosError) => { - setModalType(INTEGRATION_MODALS.DISCONNECT_ERROR); - }, - }); - const onSelect = () => { if (isActive) { - setModalType(INTEGRATION_MODALS.DISCONNECT_CONFIRMATION) + setModalType(INTEGRATION_MODALS.DISCONNECT_CONFIRMATION); } else { - setModalType(INTEGRATION_MODALS.CONNECT_CONFIRMATION) + setModalType(INTEGRATION_MODALS.CONNECT_CONFIRMATION); } setIsModalOpen(true); }; @@ -98,10 +77,10 @@ const IntegrationCard: FC> = ({

    {channelDescription}

    -
    +
    -
    +
    {renderStatusIndicators()} @@ -112,103 +91,13 @@ const IntegrationCard: FC> = ({
    - {modalType === INTEGRATION_MODALS.INTEGRATION_SUCCESS && ( - setIsModalOpen(false)} - isOpen={isModalOpen} - title={t('integration.integrationSuccessTitle')} - > -
    - {t('integration.integrationSuccessDesc', { channel })} -
    -
    - )} - {modalType ===INTEGRATION_MODALS.INTEGRATION_ERROR && ( - setIsModalOpen(false)} - isOpen={isModalOpen} - title={t('integration.integrationErrorTitle')} - > -
    - {t('integration.integrationErrorDesc', { channel })} -
    -
    - )} - {modalType === INTEGRATION_MODALS.DISCONNECT_CONFIRMATION && ( - setIsModalOpen(false)} - isOpen={isModalOpen} - title={t('integration.confirmationModalTitle')} - footer={ -
    - - -
    - } - > -
    - {t('integration.disconnectConfirmationModalDesc', { channel })} -
    -
    - )} - {modalType === INTEGRATION_MODALS.CONNECT_CONFIRMATION && ( - setIsModalOpen(false)} - isOpen={isModalOpen} - title={t('integration.confirmationModalTitle')} - footer={ -
    - - -
    - } - > -
    - {t('integration.connectConfirmationModalDesc', { channel })} -
    -
    - )} - {modalType === INTEGRATION_MODALS.DISCONNECT_ERROR && ( - setIsModalOpen(false)} - isOpen={isModalOpen} - title={t('integration.disconnectErrorTitle')} - > -
    - {t('integration.disconnectErrorDesc', { channel })} -
    -
    - )} +
    ); }; diff --git a/GUI/src/components/molecules/IntegrationModals/IntegrationModals.tsx b/GUI/src/components/molecules/IntegrationModals/IntegrationModals.tsx new file mode 100644 index 00000000..e4e540c1 --- /dev/null +++ b/GUI/src/components/molecules/IntegrationModals/IntegrationModals.tsx @@ -0,0 +1,157 @@ +import { + INTEGRATION_MODALS, + INTEGRATION_OPERATIONS, +} from 'enums/integrationEnums'; +import React from 'react'; +import { Button, Dialog } from 'components'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { togglePlatform } from 'services/integration'; +import { useTranslation } from 'react-i18next'; +import { OperationConfig } from 'types/integration'; +import { integrationQueryKeys } from 'utils/queryKeys'; + +const IntegrationModals = ({ + modalType, + isModalOpen, + setIsModalOpen, + channel, + setModalType, +}: { + modalType: INTEGRATION_MODALS; + isModalOpen: boolean; + setIsModalOpen: React.Dispatch>; + channel?: string; + setModalType: React.Dispatch>; +}) => { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + + const platformEnableMutation = useMutation({ + mutationFn: (data: OperationConfig) => togglePlatform(data), + onSuccess: async () => { + setModalType(INTEGRATION_MODALS.INTEGRATION_SUCCESS); + await queryClient.invalidateQueries( + integrationQueryKeys.INTEGRATION_STATUS() + ); + }, + onError: () => { + setModalType(INTEGRATION_MODALS.INTEGRATION_ERROR); + }, + }); + + const platformDisableMutation = useMutation({ + mutationFn: (data: OperationConfig) => togglePlatform(data), + onSuccess: async () => { + await queryClient.invalidateQueries( + integrationQueryKeys.INTEGRATION_STATUS() + ); + setIsModalOpen(false); + }, + onError: () => { + setModalType(INTEGRATION_MODALS.DISCONNECT_ERROR); + }, + }); + return ( + <> + {modalType === INTEGRATION_MODALS.INTEGRATION_SUCCESS && ( + setIsModalOpen(false)} + isOpen={isModalOpen} + title={t('integration.integrationSuccessTitle')} + > +
    + {t('integration.integrationSuccessDesc', { channel })} +
    +
    + )} + {modalType === INTEGRATION_MODALS.INTEGRATION_ERROR && ( + setIsModalOpen(false)} + isOpen={isModalOpen} + title={t('integration.integrationErrorTitle')} + > +
    + {t('integration.integrationErrorDesc', { channel })} +
    +
    + )} + {modalType === INTEGRATION_MODALS.DISCONNECT_CONFIRMATION && ( + setIsModalOpen(false)} + isOpen={isModalOpen} + title={t('integration.confirmationModalTitle')} + footer={ +
    + + +
    + } + > +
    + {t('integration.disconnectConfirmationModalDesc', { channel })} +
    +
    + )} + {modalType === INTEGRATION_MODALS.CONNECT_CONFIRMATION && ( + setIsModalOpen(false)} + isOpen={isModalOpen} + title={t('integration.confirmationModalTitle')} + footer={ +
    + + +
    + } + > +
    + {t('integration.connectConfirmationModalDesc', { channel })} +
    +
    + )} + {modalType === INTEGRATION_MODALS.DISCONNECT_ERROR && ( + setIsModalOpen(false)} + isOpen={isModalOpen} + title={t('integration.disconnectErrorTitle')} + > +
    + {t('integration.disconnectErrorDesc', { channel })} +
    +
    + )} + + ); +}; + +export default IntegrationModals; diff --git a/GUI/src/components/molecules/ValidationAndHierarchyCards/ValidationAndHierarchyCards.tsx b/GUI/src/components/molecules/ValidationAndHierarchyCards/ValidationAndHierarchyCards.tsx new file mode 100644 index 00000000..e481e1f6 --- /dev/null +++ b/GUI/src/components/molecules/ValidationAndHierarchyCards/ValidationAndHierarchyCards.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import ValidationCriteriaRowsView from '../ValidationCriteria/RowsView'; +import { useTranslation } from 'react-i18next'; +import { Card } from 'components'; +import ClassHierarchy from '../ClassHeirarchy'; +import { TreeNode, ValidationRule } from 'types/datasetGroups'; + +const ValidationAndHierarchyCards = ({ + metadata, + isMetadataLoading, + validationRules, + setValidationRules, + validationRuleError, + setValidationRuleError, + nodes, + setNodes, + nodesError, + setNodesError, +}: { + metadata: any; + isMetadataLoading: boolean; + validationRules: ValidationRule[] | undefined; + setValidationRules: React.Dispatch< + React.SetStateAction + >; + validationRuleError: boolean; + setValidationRuleError: React.Dispatch>; + nodes: TreeNode[]; + setNodes: React.Dispatch; + nodesError: boolean; + setNodesError: React.Dispatch>; +}) => { + const { t } = useTranslation(); + return ( + <> + {metadata && ( +
    + + + + + + {!isMetadataLoading && ( + + )} + +
    + )} + + ); +}; + +export default ValidationAndHierarchyCards; diff --git a/GUI/src/components/molecules/ValidationCriteria/CardsView.tsx b/GUI/src/components/molecules/ValidationCriteria/CardsView.tsx index feeea79c..a781c8e1 100644 --- a/GUI/src/components/molecules/ValidationCriteria/CardsView.tsx +++ b/GUI/src/components/molecules/ValidationCriteria/CardsView.tsx @@ -1,19 +1,10 @@ -import React, { FC, PropsWithChildren, useCallback } from 'react'; -import { DndProvider, useDrag, useDrop } from 'react-dnd'; +import React, { FC, PropsWithChildren } from 'react'; +import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; -import dataTypes from '../../../config/dataTypesConfig.json'; -import { MdDehaze, MdDelete } from 'react-icons/md'; -import Card from 'components/Card'; -import { FormCheckbox, FormInput, FormSelect } from 'components/FormElements'; import Button from 'components/Button'; import { ValidationRule } from 'types/datasetGroups'; -import { Link } from 'react-router-dom'; -import { isFieldNameExisting } from 'utils/datasetGroupsUtils'; import { v4 as uuidv4 } from 'uuid'; - -const ItemTypes = { - ITEM: 'item', -}; +import DraggableItem from './DraggableItem/DraggableItem'; type ValidationRulesProps = { validationRules?: ValidationRule[]; @@ -29,114 +20,8 @@ const ValidationCriteriaCardsView: FC< setValidationRuleError, validationRuleError, }) => { - - - const setIsDataClass = (id, isDataClass) => { - const updatedItems = validationRules?.map((item) => - item.id === id ? { ...item, isDataClass: !isDataClass } : item - ); - setValidationRules(updatedItems); - }; - - const handleChange = useCallback((id, newValue) => { - setValidationRules((prevData) => - prevData.map((item) => - item.id === id ? { ...item, fieldName: newValue } : item - ) - ); - }, []); - - const changeDataType = (id, value) => { - const updatedItems = validationRules.map((item) => - item.id === id ? { ...item, dataType: value } : item - ); - setValidationRules(updatedItems); - }; - - const DraggableItem = ({ item, index, moveItem }) => { - const [, ref] = useDrag({ - type: ItemTypes.ITEM, - item: { index }, - }); - - const [, drop] = useDrop({ - accept: ItemTypes.ITEM, - hover: (draggedItem) => { - if (draggedItem.index !== index) { - moveItem(draggedItem.index, index); - draggedItem.index = index; - } - }, - }); - - return ( -
    ref(drop(node))}> - -
    - handleChange(item.id, e.target.value)} - error={ - validationRuleError && !item.fieldName - ? 'Enter a field name' - : validationRuleError && - item.fieldName && - item?.fieldName.toString().toLocaleLowerCase() === 'rowid' - ? `${item?.fieldName} cannot be used as a field name` - : item.fieldName && isFieldNameExisting(validationRules,item?.fieldName)?`${item?.fieldName} alreday exist as field name` - : '' - } - /> - - changeDataType(item.id, selection?.value) - } - error={ - validationRuleError && !item.dataType - ? 'Select a data type' - : '' - } - /> -
    - deleteItem(item.id)} - className="link" - > - - Delete - - setIsDataClass(item.id, item.isDataClass)} - style={{width:"150px"}} - /> - -
    -
    -
    -
    - ); - }; - - const moveItem = (fromIndex, toIndex) => { - const updatedItems = Array.from(validationRules); + const moveItem = (fromIndex: number, toIndex: number) => { + const updatedItems = Array.from(validationRules ?? []); const [movedItem] = updatedItems.splice(fromIndex, 1); updatedItems.splice(toIndex, 0, movedItem); setValidationRules(updatedItems); @@ -145,30 +30,27 @@ const ValidationCriteriaCardsView: FC< const addNewClass = () => { setValidationRuleError(false); const updatedItems = [ - ...validationRules, + ...(validationRules ?? []), { id: uuidv4(), fieldName: '', dataType: '', isDataClass: false }, ]; setValidationRules(updatedItems); }; - const deleteItem = (idToDelete) => { - const updatedItems = validationRules.filter( - (item) => item.id !== idToDelete - ); - setValidationRules(updatedItems); - }; - return (
    Create Validation Rule
    - {validationRules.map((item, index) => ( - - ))} + {validationRules && + validationRules?.map((item, index) => ( + + ))}
    diff --git a/GUI/src/components/molecules/ValidationCriteria/DraggableItem/DraggableItem.tsx b/GUI/src/components/molecules/ValidationCriteria/DraggableItem/DraggableItem.tsx new file mode 100644 index 00000000..5265333e --- /dev/null +++ b/GUI/src/components/molecules/ValidationCriteria/DraggableItem/DraggableItem.tsx @@ -0,0 +1,139 @@ +import React, { useCallback } from 'react'; +import { useDrag, useDrop } from 'react-dnd'; +import dataTypes from '../../../../config/dataTypesConfig.json'; +import { MdDehaze, MdDelete } from 'react-icons/md'; +import Card from 'components/Card'; +import { FormCheckbox, FormInput, FormSelect } from 'components/FormElements'; +import { ValidationRule } from 'types/datasetGroups'; +import { Link } from 'react-router-dom'; +import { isFieldNameExisting } from 'utils/datasetGroupsUtils'; + +const ItemTypes = { + ITEM: 'item', +}; + +const DraggableItem = ({ + item, + index, + moveItem, + setValidationRules, + validationRuleError, + validationRules, +}: { + item: ValidationRule; + index: number; + moveItem: (fromIndex: number, toIndex: number) => void; + validationRules?: ValidationRule[]; + setValidationRules: React.Dispatch>; + validationRuleError?: boolean; +}) => { + const [, ref] = useDrag({ + type: ItemTypes.ITEM, + item: { index }, + }); + + const [, drop] = useDrop({ + accept: ItemTypes.ITEM, + hover: (draggedItem: { + index: number + }) => { + if (draggedItem?.index !== index) { + moveItem(draggedItem.index, index); + draggedItem.index = index; + } + }, + }); + + const deleteItem = (idToDelete: string | number) => { + const updatedItems = validationRules?.filter( + (item) => item?.id !== idToDelete + ); + updatedItems && setValidationRules(updatedItems); + }; + + const setIsDataClass = (id: string | number, isDataClass: boolean) => { + const updatedItems = validationRules?.map((item) => + item.id === id ? { ...item, isDataClass: !isDataClass } : item + ); + updatedItems && setValidationRules(updatedItems); + }; + + const handleChange = useCallback((id: string | number, newValue: string) => { + setValidationRules((prevData) => + prevData.map((item) => + item.id === id ? { ...item, fieldName: newValue } : item + ) + ); + }, []); + + const changeDataType = (id: string | number, value: string) => { + const updatedItems = validationRules?.map((item) => + item.id === id ? { ...item, dataType: value } : item + ); + updatedItems && setValidationRules(updatedItems); + }; + return ( +
    ref(drop(node))}> + +
    + handleChange(item.id, e.target.value)} + error={ + validationRuleError && !item.fieldName + ? 'Enter a field name' + : validationRuleError && + item.fieldName && + item?.fieldName.toString().toLocaleLowerCase() === 'rowid' + ? `${item?.fieldName} cannot be used as a field name` + : item.fieldName && + isFieldNameExisting(validationRules, item?.fieldName) + ? `${item?.fieldName} alreday exist as field name` + : '' + } + /> + + changeDataType(item.id, selection?.value ?? "") + } + error={ + validationRuleError && !item.dataType ? 'Select a data type' : '' + } + /> +
    + deleteItem(item.id)} + className="link" + > + + Delete + + setIsDataClass(item.id, item.isDataClass)} + style={{ width: '150px' }} + /> + +
    +
    +
    +
    + ); +}; + +export default DraggableItem; diff --git a/GUI/src/components/molecules/ValidationCriteria/RowsView.tsx b/GUI/src/components/molecules/ValidationCriteria/RowsView.tsx index 5315bac0..fef02edd 100644 --- a/GUI/src/components/molecules/ValidationCriteria/RowsView.tsx +++ b/GUI/src/components/molecules/ValidationCriteria/RowsView.tsx @@ -20,17 +20,18 @@ const ValidationCriteriaRowsView: FC> = validationRuleError, }) => { - const setIsDataClass = (id, isDataClass) => { - const updatedItems = validationRules.map((item) => - item.id === id ? { ...item, isDataClass: !isDataClass } : item + const setIsDataClass = (id: string | number, isDataClass: boolean) => { + const updatedItems = validationRules?.map((item) => + item?.id === id ? { ...item, isDataClass: !isDataClass } : item ); - setValidationRules(updatedItems); + updatedItems && setValidationRules(updatedItems); }; - const changeName = (id, newValue) => { + const changeName = (id: number | string, newValue: string) => { + setValidationRules((prevData) => - prevData.map((item) => - item.id === id ? { ...item, fieldName: newValue } : item + prevData?.map((item) => + item?.id === id ? { ...item, fieldName: newValue } : item ) ); @@ -40,9 +41,9 @@ const ValidationCriteriaRowsView: FC> = setValidationRuleError(false) } - const changeDataType = (id, value) => { - const updatedItems = validationRules.map((item) => - item.id === id ? { ...item, dataType: value } : item + const changeDataType = (id: number | string, value: string) => { + const updatedItems = validationRules?.map((item) => + item?.id === id ? { ...item, dataType: value } : item ); setValidationRules(updatedItems); }; @@ -50,7 +51,7 @@ const ValidationCriteriaRowsView: FC> = const addNewClass = () => { setValidationRuleError(false) const updatedItems = [ - ...validationRules, + ...validationRules ?? [], { id: uuidv4(), fieldName: '', dataType: '', isDataClass: false }, ]; @@ -58,8 +59,8 @@ const ValidationCriteriaRowsView: FC> = setValidationRules(updatedItems); }; - const deleteItem = (idToDelete) => { - const updatedItems = validationRules.filter( + const deleteItem = (idToDelete: number | string) => { + const updatedItems = validationRules?.filter( (item) => item.id !== idToDelete ); setValidationRules(updatedItems); @@ -93,7 +94,7 @@ const ValidationCriteriaRowsView: FC> = options={dataTypes} defaultValue={item.dataType} onSelectionChange={(selection) => - changeDataType(item.id, selection?.value) + changeDataType(item.id, selection?.value ?? "") } error={ validationRuleError && !item.dataType diff --git a/GUI/src/components/molecules/ValidationStatus/ValidationStatus.tsx b/GUI/src/components/molecules/ValidationStatus/ValidationStatus.tsx new file mode 100644 index 00000000..06835f29 --- /dev/null +++ b/GUI/src/components/molecules/ValidationStatus/ValidationStatus.tsx @@ -0,0 +1,43 @@ +import { ValidationStatus } from 'enums/datasetEnums'; +import React from 'react'; +import Label from 'components/Label'; +import { LabelType } from 'enums/commonEnums'; +import { useTranslation } from 'react-i18next'; + +const DatasetValidationStatus = ({ + status, +}: { + status: string | undefined; +}) => { + const { t } = useTranslation(); + + if (status === ValidationStatus.SUCCESS) { + return ( + + ); + } else if (status === ValidationStatus.FAIL) { + return ( + + ); + } else if (status === ValidationStatus.UNVALIDATED) { + return ( + + ); + } else if (status === ValidationStatus.IN_PROGRESS) { + return ( + + ); + } else { + return null; + } +}; + +export default DatasetValidationStatus; diff --git a/GUI/src/components/molecules/ViewDatasetGroupModalController/ViewDatasetGroupModalController.tsx b/GUI/src/components/molecules/ViewDatasetGroupModalController/ViewDatasetGroupModalController.tsx new file mode 100644 index 00000000..a14a0743 --- /dev/null +++ b/GUI/src/components/molecules/ViewDatasetGroupModalController/ViewDatasetGroupModalController.tsx @@ -0,0 +1,227 @@ +import React, { RefObject } from 'react'; +import { Button, Dialog, FormRadios } from 'components'; +import DynamicForm from 'components/FormElements/DynamicForm'; +import FileUpload, { FileUploadHandle } from 'components/FileUpload'; +import formats from '../../../config/formatsConfig.json'; +import { ViewDatasetGroupModalContexts } from 'enums/datasetEnums'; +import { ButtonAppearanceTypes } from 'enums/commonEnums'; +import { useDialog } from 'hooks/useDialog'; +import { useTranslation } from 'react-i18next'; +import { SelectedRowPayload } from 'types/datasetGroups'; +import './styles.scss'; + +const ViewDatasetGroupModalController = ({ + setImportStatus, + fileUploadRef, + handleFileSelect, + handleImport, + importStatus, + setImportFormat, + importFormat, + handleExport, + setExportFormat, + selectedRow, + patchDataUpdate, + isModalOpen, + setIsModalOpen, + setOpenedModalContext, + openedModalContext, + closeModals, + deleteRow, + file, + exportFormat, +}: { + setImportStatus: React.Dispatch>; + handleFileSelect: (file: File | null) => void; + fileUploadRef: RefObject; + handleImport: () => void; + importStatus: string; + setImportFormat: React.Dispatch>; + importFormat: string; + handleExport: () => void; + setExportFormat: React.Dispatch>; + selectedRow: SelectedRowPayload | undefined; + patchDataUpdate: (dataset: any) => void; + isModalOpen: boolean; + setIsModalOpen: React.Dispatch>; + openedModalContext: ViewDatasetGroupModalContexts; + setOpenedModalContext: React.Dispatch< + React.SetStateAction + >; + closeModals: () => void; + deleteRow: (dataRow: any) => void; + file: File | undefined; + exportFormat: string; +}) => { + const { close } = useDialog(); + const { t } = useTranslation(); + + return ( + <> + {isModalOpen && + openedModalContext === ViewDatasetGroupModalContexts.IMPORT_MODAL && ( + + + +
    + } + onClose={() => { + closeModals(); + setImportStatus('ABORTED'); + setImportFormat(''); + }} + > +
    +

    + {t('datasetGroups.detailedView.modals.import.fileFormatlabel')} +

    +
    + +
    +

    {t('datasetGroups.detailedView.modals.import.attachments')}

    + + {importStatus === 'STARTED' && ( +
    +
    + {t( + 'datasetGroups.detailedView.modals.import.uploadInProgress' + )} +
    +

    + {t('datasetGroups.detailedView.modals.import.uploadDesc')} +

    +
    + )} +
    + + )} + {isModalOpen && + openedModalContext === ViewDatasetGroupModalContexts.EXPORT_MODAL && ( + + + +
    + } + onClose={() => { + closeModals(); + setImportStatus('ABORTED'); + setExportFormat(''); + }} + > +
    +

    + {t('datasetGroups.detailedView.modals.export.fileFormatlabel')} +

    +
    + +
    +
    + + )} + {isModalOpen && + openedModalContext === + ViewDatasetGroupModalContexts.PATCH_UPDATE_MODAL && ( + + + + )} + + {isModalOpen && + openedModalContext === + ViewDatasetGroupModalContexts.DELETE_ROW_MODAL && ( + + + +
    + } + > + {t('datasetGroups.detailedView.modals.delete.description')} + + )} + + ); +}; + +export default ViewDatasetGroupModalController; diff --git a/GUI/src/components/molecules/ViewDatasetGroupModalController/styles.scss b/GUI/src/components/molecules/ViewDatasetGroupModalController/styles.scss new file mode 100644 index 00000000..5affed21 --- /dev/null +++ b/GUI/src/components/molecules/ViewDatasetGroupModalController/styles.scss @@ -0,0 +1,13 @@ +.flex-grid { + margin-bottom: 20px; +} + +.upload-progress-wrapper { + text-align: center; + padding: 20px; +} + +.upload-progress-text-wrapper { + margin-bottom: 10px; + font-size: 18px; +} \ No newline at end of file diff --git a/GUI/src/enums/commonEnums.ts b/GUI/src/enums/commonEnums.ts index 25f7cd63..79f9444f 100644 --- a/GUI/src/enums/commonEnums.ts +++ b/GUI/src/enums/commonEnums.ts @@ -9,3 +9,10 @@ export enum ButtonAppearanceTypes { ERROR = 'error', TEXT = 'text', } + +export enum LabelType { + SUCCESS = 'success', + ERROR = 'error', + INFO = 'info', + WARNING = 'warning', +} diff --git a/GUI/src/enums/datasetEnums.ts b/GUI/src/enums/datasetEnums.ts new file mode 100644 index 00000000..8efea54e --- /dev/null +++ b/GUI/src/enums/datasetEnums.ts @@ -0,0 +1,38 @@ +export enum ValidationStatus { + SUCCESS = 'success', + FAIL = 'fail', + UNVALIDATED = 'unvalidated', + IN_PROGRESS = 'in-progress', +} + +export enum DatasetViewEnum { + LIST = 'list', + INDIVIDUAL = 'individual', +} + +export enum CreateDatasetGroupModals { + SUCCESS = 'SUCCESS', + VALIDATION_ERROR = 'VALIDATION_ERROR', + NULL = 'NULL', +} + +export enum ViewDatasetGroupModalContexts { + EXPORT_MODAL = 'EXPORT_MODAL', + IMPORT_MODAL = 'IMPORT_MODAL', + PATCH_UPDATE_MODAL = 'PATCH_UPDATE_MODAL', + DELETE_ROW_MODAL = 'DELETE_ROW_MODAL', + NULL = 'NULL', +} + +export enum UpdatePriority { + MAJOR = 'MAJOR', + MINOR = 'MINOR', + PATCH = 'PATCH', + NULL = 'NULL', +} + +export enum ImportExportDataTypes { + XLSX = 'xlsx', + JSON = 'json', + YAML = 'yaml', +} diff --git a/GUI/src/enums/roles.ts b/GUI/src/enums/roles.ts index 94b602f1..b5cfd8a6 100644 --- a/GUI/src/enums/roles.ts +++ b/GUI/src/enums/roles.ts @@ -1,4 +1,4 @@ export enum ROLES { - ROLE_ADMINISTRATOR = 'ROLE_ADMINISTRATOR', - ROLE_MODEL_TRAINER='ROLE_MODEL_TRAINER' - } \ No newline at end of file + ROLE_ADMINISTRATOR = 'ROLE_ADMINISTRATOR', + ROLE_MODEL_TRAINER = 'ROLE_MODEL_TRAINER', +} diff --git a/GUI/src/pages/DatasetGroups/CreateDatasetGroup.tsx b/GUI/src/pages/DatasetGroups/CreateDatasetGroup.tsx index 5ab886d5..4e873e00 100644 --- a/GUI/src/pages/DatasetGroups/CreateDatasetGroup.tsx +++ b/GUI/src/pages/DatasetGroups/CreateDatasetGroup.tsx @@ -1,7 +1,7 @@ import { FC, useCallback, useState } from 'react'; import './DatasetGroups.scss'; import { useTranslation } from 'react-i18next'; -import { Button, Card, Dialog, FormInput } from 'components'; +import { Button, Card, FormInput } from 'components'; import { v4 as uuidv4 } from 'uuid'; import ClassHierarchy from 'components/molecules/ClassHeirarchy'; import { @@ -11,18 +11,19 @@ import { validateClassHierarchy, validateValidationRules, } from 'utils/datasetGroupsUtils'; -import { DatasetGroup, ValidationRule } from 'types/datasetGroups'; +import { DatasetGroup, TreeNode, ValidationRule } from 'types/datasetGroups'; import { useNavigate } from 'react-router-dom'; import ValidationCriteriaCardsView from 'components/molecules/ValidationCriteria/CardsView'; import { useMutation } from '@tanstack/react-query'; import { createDatasetGroup } from 'services/datasets'; import { useDialog } from 'hooks/useDialog'; +import { CreateDatasetGroupModals } from 'enums/datasetEnums'; +import CreateDatasetGroupModalController from 'components/molecules/CreateDatasetGroupModals/CreateDatasetGroupModal'; +import { ButtonAppearanceTypes } from 'enums/commonEnums'; const CreateDatasetGroup: FC = () => { const { t } = useTranslation(); const { open } = useDialog(); - const [isModalOpen, setIsModalOpen] = useState(false); - const [modalType, setModalType] = useState(''); const navigate = useNavigate(); const initialValidationRules = [ @@ -34,15 +35,17 @@ const CreateDatasetGroup: FC = () => { { id: uuidv4(), fieldName: '', level: 0, children: [] }, ]; + const [isModalOpen, setIsModalOpen] = useState(false); + const [modalType, setModalType] = useState( + CreateDatasetGroupModals.NULL + ); const [datasetName, setDatasetName] = useState(''); const [datasetNameError, setDatasetNameError] = useState(false); - const [validationRules, setValidationRules] = useState( initialValidationRules ); const [validationRuleError, setValidationRuleError] = useState(false); - - const [nodes, setNodes] = useState(initialClass); + const [nodes, setNodes] = useState(initialClass); const [nodesError, setNodesError] = useState(false); const validateData = useCallback(() => { @@ -56,7 +59,7 @@ const CreateDatasetGroup: FC = () => { ) { if (!isValidationRulesSatisfied(validationRules)) { setIsModalOpen(true); - setModalType('VALIDATION_ERROR'); + setModalType(CreateDatasetGroupModals.VALIDATION_ERROR); } else { const payload: DatasetGroup = { groupName: datasetName, @@ -70,14 +73,14 @@ const CreateDatasetGroup: FC = () => { const createDatasetGroupMutation = useMutation({ mutationFn: (data: DatasetGroup) => createDatasetGroup(data), - onSuccess: async (response) => { + onSuccess: async () => { setIsModalOpen(true); - setModalType('SUCCESS'); + setModalType(CreateDatasetGroupModals.SUCCESS); }, onError: () => { open({ - title: 'Dataset Group Creation Unsuccessful', - content:

    Something went wrong. Please try again.

    , + title: t('datasetGroups.modals.createDatasetUnsuccessTitle'), + content:

    {t('datasetGroups.modals.errorDesc')}

    , }); }, }); @@ -86,18 +89,27 @@ const CreateDatasetGroup: FC = () => {
    -
    Create Dataset Group
    +
    {t('datasetGroups.createDataset.title')}
    - +
    setDatasetName(e.target.value)} error={ - !datasetName && datasetNameError ? 'Enter dataset name' : '' + !datasetName && datasetNameError + ? t( + 'datasetGroups.createDataset.datasetInputPlaceholder' + ) ?? '' + : '' } />
    @@ -121,51 +133,13 @@ const CreateDatasetGroup: FC = () => { />
    - {modalType === 'VALIDATION_ERROR' && ( - - - -
    - } - onClose={() => setIsModalOpen(false)} - > - The dataset must have at least 2 columns. Additionally, there needs - to be at least one column designated as a data class and one column - that is not a data class. Please adjust your dataset accordingly. - - )} - {modalType === 'SUCCESS' && ( - - - -
    - } - onClose={() => setIsModalOpen(false)} - > - You have successfully created the dataset group. In the detailed - view, you can now see and edit the dataset as needed. - - )} + + +
    { marginTop: '25px', }} > - +
    diff --git a/GUI/src/pages/DatasetGroups/DatasetGroups.scss b/GUI/src/pages/DatasetGroups/DatasetGroups.scss index 1e5a7af8..ffa9196b 100644 --- a/GUI/src/pages/DatasetGroups/DatasetGroups.scss +++ b/GUI/src/pages/DatasetGroups/DatasetGroups.scss @@ -38,5 +38,12 @@ padding: 20px 200px; text-align: center; color: #cf0000; - } + +.footer-button-group { + display: flex; + align-items: flex-end; + justify-content: flex-end; + gap: 10px; + margin-top: 25px; +} \ No newline at end of file diff --git a/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx b/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx index ab86b31f..5cb4f7dd 100644 --- a/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx +++ b/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx @@ -1,35 +1,18 @@ -import { - FC, - PropsWithChildren, - SetStateAction, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; +import { FC, PropsWithChildren, useEffect, useRef, useState } from 'react'; import './DatasetGroups.scss'; import { useTranslation } from 'react-i18next'; -import { - Button, - Card, - DataTable, - Dialog, - FormRadios, - Icon, - Label, - Switch, -} from 'components'; -import ClassHierarchy from 'components/molecules/ClassHeirarchy'; -import { createColumnHelper, PaginationState } from '@tanstack/react-table'; +import { Button } from 'components'; +import { PaginationState } from '@tanstack/react-table'; import { DatasetGroup, + SelectedRowPayload, ImportDataset, + MinorPayLoad, + PatchPayLoad, + TreeNode, ValidationRule, } from 'types/datasetGroups'; -import BackArrowButton from 'assets/BackArrowButton'; -import { Link, useNavigate } from 'react-router-dom'; -import ValidationCriteriaRowsView from 'components/molecules/ValidationCriteria/RowsView'; -import { MdOutlineDeleteOutline, MdOutlineEdit } from 'react-icons/md'; +import { useNavigate } from 'react-router-dom'; import { exportDataset, getDatasets, @@ -51,14 +34,23 @@ import { validateClassHierarchy, validateValidationRules, } from 'utils/datasetGroupsUtils'; -import formats from '../../config/formatsConfig.json'; -import FileUpload, { FileUploadHandle } from 'components/FileUpload'; -import DynamicForm from 'components/FormElements/DynamicForm'; +import { datasetQueryKeys } from 'utils/queryKeys'; +import { + DatasetViewEnum, + UpdatePriority, + ViewDatasetGroupModalContexts, +} from 'enums/datasetEnums'; +import { ButtonAppearanceTypes } from 'enums/commonEnums'; +import ViewDatasetGroupModalController from 'components/molecules/ViewDatasetGroupModalController/ViewDatasetGroupModalController'; +import { FileUploadHandle } from 'components/FileUpload'; +import DatasetDetailedViewTable from 'components/molecules/DatasetDetailedViewTable/DatasetDetailedViewTable'; +import ValidationAndHierarchyCards from 'components/molecules/ValidationAndHierarchyCards/ValidationAndHierarchyCards'; type Props = { dgId: number; - setView: React.Dispatch>; + setView: React.Dispatch>; }; + const ViewDatasetGroup: FC> = ({ dgId, setView }) => { const { t } = useTranslation(); const { open, close } = useDialog(); @@ -73,19 +65,20 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { pageIndex: 0, pageSize: 10, }); - const [isImportModalOpen, setIsImportModalOpen] = useState(false); - const [isExportModalOpen, setIsExportModalOpen] = useState(false); - const [patchUpdateModalOpen, setPatchUpdateModalOpen] = useState(false); - const [deleteRowModalOpen, setDeleteRowModalOpen] = useState(false); const fileUploadRef = useRef(null); const [fetchEnabled, setFetchEnabled] = useState(true); - const [file, setFile] = useState(''); - const [selectedRow, setSelectedRow] = useState({}); + const [file, setFile] = useState(); + const [selectedRow, setSelectedRow] = useState(); const [bannerMessage, setBannerMessage] = useState(''); - const [minorPayload, setMinorPayload] = useState(''); - const [patchPayload, setPatchPayload] = useState(''); - const [deletedDataRows, setDeletedDataRows] = useState([]); - const [updatePriority, setUpdatePriority] = useState(''); + const [minorPayload, setMinorPayload] = useState(); + const [patchPayload, setPatchPayload] = useState(); + const [deletedDataRows, setDeletedDataRows] = useState([]); + const [updatePriority, setUpdatePriority] = useState( + UpdatePriority.NULL + ); + const [openedModalContext, setOpenedModalContext] = + useState(ViewDatasetGroupModalContexts.NULL); + const [isModalOpen, setIsModalOpen] = useState(false); const navigate = useNavigate(); @@ -94,23 +87,17 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { }, []); useEffect(() => { - if (updatePriority === 'MAJOR') - setBannerMessage( - 'You have updated key configurations of the dataset schema which are not saved, please save to apply changes. Any files imported or edits made to the existing data will be discarded after changes are applied' - ); - else if (updatePriority === 'MINOR') - setBannerMessage( - 'You have imported new data into the dataset, please save the changes to apply. Any changes you made to the individual data items will be discarded after changes are applied' - ); - else if (updatePriority === 'PATCH') - setBannerMessage( - 'You have edited individual items in the dataset which are not saved. Please save the changes to apply' - ); + if (updatePriority === UpdatePriority.MAJOR) + setBannerMessage(t('datasetGroups.detailedView.majorUpdateBanner') ?? ''); + else if (updatePriority === UpdatePriority.MINOR) + setBannerMessage(t('datasetGroups.detailedView.minorUpdateBanner') ?? ''); + else if (updatePriority === UpdatePriority.PATCH) + setBannerMessage(t('datasetGroups.detailedView.patchUpdateBanner') ?? ''); else setBannerMessage(''); }, [updatePriority]); const { data: datasets, isLoading } = useQuery( - ['datasets/groups/data', pagination, dgId], + datasetQueryKeys.GET_DATA_SETS(dgId, pagination), () => getDatasets(pagination, dgId), { keepPreviousData: true, @@ -118,42 +105,31 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { ); const { data: metadata, isLoading: isMetadataLoading } = useQuery( - ['datasets/groups/metadata', dgId], + datasetQueryKeys.GET_MATA_DATA(dgId), () => getMetadata(dgId), { enabled: fetchEnabled } ); const [updatedDataset, setUpdatedDataset] = useState(datasets?.dataPayload); + useEffect(() => { setUpdatedDataset(datasets?.dataPayload); }, [datasets]); - const [nodes, setNodes] = useState( - reverseTransformClassHierarchy( - metadata?.response?.data?.[0]?.classHierarchy - ) + const [nodes, setNodes] = useState( + reverseTransformClassHierarchy(metadata?.[0]?.classHierarchy) ); const [validationRules, setValidationRules] = useState< ValidationRule[] | undefined - >( - transformObjectToArray( - metadata?.response?.data?.[0]?.validationCriteria?.validationRules - ) - ); + >(transformObjectToArray(metadata?.[0]?.validationCriteria?.validationRules)); useEffect(() => { - setNodes( - reverseTransformClassHierarchy( - metadata?.response?.data?.[0]?.classHierarchy - ) - ); + setNodes(reverseTransformClassHierarchy(metadata?.[0]?.classHierarchy)); }, [metadata]); useEffect(() => { setValidationRules( - transformObjectToArray( - metadata?.response?.data?.[0]?.validationCriteria?.validationRules - ) + transformObjectToArray(metadata?.[0]?.validationCriteria?.validationRules) ); }, [metadata]); @@ -162,9 +138,8 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { metadata && isMajorUpdate( { - validationRules: - metadata?.response?.data?.[0]?.validationCriteria?.validationRules, - classHierarchy: metadata?.response?.data?.[0]?.classHierarchy, + validationRules: metadata?.[0]?.validationCriteria?.validationRules, + classHierarchy: metadata?.[0]?.classHierarchy, }, { validationRules: @@ -173,20 +148,20 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { } ) ) { - setUpdatePriority('MAJOR'); + setUpdatePriority(UpdatePriority.MAJOR); } else { - setUpdatePriority(''); + setUpdatePriority(UpdatePriority.NULL); } }, [validationRules, nodes]); - const deleteRow = (dataRow) => { + const deleteRow = (dataRow: SelectedRowPayload) => { setDeletedDataRows((prevDeletedDataRows) => [ ...prevDeletedDataRows, dataRow?.rowId, ]); - const payload = updatedDataset?.filter((row) => { - if (row.rowId !== selectedRow?.rowId) return row; - }); + const payload = updatedDataset?.filter( + (row) => row.rowId !== selectedRow?.rowId + ); setUpdatedDataset(payload); const updatedPayload = { @@ -197,12 +172,15 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { }, }; setPatchPayload(updatedPayload); - setDeleteRowModalOpen(false); - if (updatePriority !== 'MAJOR' && updatePriority !== 'MINOR') - setUpdatePriority('PATCH'); + handleCloseModals(); + if ( + updatePriority !== UpdatePriority.MAJOR && + updatePriority !== UpdatePriority.MINOR + ) + setUpdatePriority(UpdatePriority.PATCH); }; - const patchDataUpdate = (dataRow) => { + const patchDataUpdate = (dataRow: SelectedRowPayload) => { const payload = updatedDataset?.map((row) => row.rowId === selectedRow?.rowId ? dataRow : row ); @@ -216,120 +194,71 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { }, }; setPatchPayload(updatedPayload); - setPatchUpdateModalOpen(false); - if (updatePriority !== 'MAJOR' && updatePriority !== 'MINOR') - setUpdatePriority('PATCH'); + handleCloseModals(); + if ( + updatePriority !== UpdatePriority.MAJOR && + updatePriority !== UpdatePriority.MINOR + ) + setUpdatePriority(UpdatePriority.PATCH); }; const patchUpdateMutation = useMutation({ - mutationFn: (data) => patchUpdate(data), + mutationFn: (data: PatchPayLoad) => patchUpdate(data), onSuccess: async () => { - await queryClient.invalidateQueries(['datasets/groups/data']); + await queryClient.invalidateQueries(datasetQueryKeys.GET_DATA_SETS()); close(); - setView('list'); + setView(DatasetViewEnum.LIST); }, onError: () => { - setPatchUpdateModalOpen(false); + handleCloseModals(); open({ - title: 'Patch Data Update Unsuccessful', - content:

    Something went wrong. Please try again.

    , + title: t('datasetGroups.detailedView.patchDataUnsuccessfulTitle') ?? '', + content: ( +

    + {t('datasetGroups.detailedView.patchDataUnsuccessfulDesc') ?? ''} +

    + ), }); }, }); - const generateDynamicColumns = (columnsData, editView, deleteView) => { - const columnHelper = createColumnHelper(); - const dynamicColumns = columnsData?.map((col) => { - return columnHelper.accessor(col, { - header: col ?? '', - id: col, - }); - }); - - const staticColumns = [ - columnHelper.display({ - id: 'edit', - cell: editView, - meta: { - size: '1%', - }, - }), - columnHelper.display({ - id: 'delete', - cell: deleteView, - meta: { - size: '1%', - }, - }), - ]; - if (dynamicColumns) return [...dynamicColumns, ...staticColumns]; - else return []; - }; - - const editView = (props) => { - return ( - - ); - }; - - const deleteView = (props: any) => ( - - ); - - const dataColumns = useMemo( - () => generateDynamicColumns(datasets?.fields, editView, deleteView), - [datasets?.fields] - ); - const handleExport = () => { exportDataMutation.mutate({ dgId, exportType: exportFormat }); }; const exportDataMutation = useMutation({ - mutationFn: (data) => exportDataset(data?.dgId, data?.exportType), + mutationFn: (data: { dgId: number; exportType: string }) => + exportDataset(data?.dgId, data?.exportType), onSuccess: async (response) => { + console.log("response export ", response, exportFormat) handleDownload(response, exportFormat); open({ - title: 'Data export was successful', - content:

    Your data has been successfully exported.

    , + title: t('datasetGroups.detailedView.exportDataSuccessTitle') ?? '', + content: ( +

    {t('datasetGroups.detailedView.exportDataSuccessDesc') ?? ''}

    + ), }); - setIsExportModalOpen(false); + handleCloseModals(); }, onError: () => { open({ - title: 'Dataset Export Unsuccessful', - content:

    Something went wrong. Please try again.

    , + title: t('datasetGroups.detailedView.exportDataUnsucessTitle') ?? '', + content: ( +

    {t('datasetGroups.detailedView.exportDataUnsucessDesc') ?? ''}

    + ), }); }, }); - const handleFileSelect = (file: File) => { - setFile(file); + const handleFileSelect = (file: File | null) => { + if (file) setFile(file); }; const handleImport = () => { setImportStatus('STARTED'); const payload = { dgId, - dataFile: file, + dataFile: file as File, }; importDataMutation.mutate(payload); @@ -343,66 +272,59 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { dgId, s3FilePath: response?.saved_file_path, }); - if (updatePriority !== 'MAJOR') setUpdatePriority('MINOR'); + if (updatePriority !== UpdatePriority.MAJOR) + setUpdatePriority(UpdatePriority.MINOR); - setIsImportModalOpen(false); + handleCloseModals(); }, onError: () => { open({ - title: 'Dataset Import Unsuccessful', - content:

    Something went wrong. Please try again.

    , + title: t('datasetGroups.detailedView.ImportDataUnsucessTitle') ?? '', + content: ( +

    {t('datasetGroups.detailedView.importDataUnsucessDesc') ?? ''}

    + ), }); }, }); const minorUpdateMutation = useMutation({ - mutationFn: (data) => minorUpdate(data), - onSuccess: async (response) => { + mutationFn: (data: MinorPayLoad) => minorUpdate(data), + onSuccess: async () => { open({ - title: 'Dataset uploaded and validation initiated', + title: t('datasetGroups.detailedView.validationInitiatedTitle') ?? '', content: ( -

    - The dataset file was successfully uploaded. The validation and - preprocessing is now initiated -

    +

    {t('datasetGroups.detailedView.validationInitiatedDesc') ?? ''}

    ), footer: (
    + -
    ), }); - setIsImportModalOpen(false); + setIsModalOpen(false); + setOpenedModalContext(ViewDatasetGroupModalContexts.NULL); }, onError: () => { open({ - title: 'Dataset Import Unsuccessful', - content:

    Something went wrong. Please try again.

    , + title: t('datasetGroups.detailedView.ImportDataUnsucessTitle') ?? '', + content: ( +

    {t('datasetGroups.detailedView.importDataUnsucessDesc') ?? ''}

    + ), }); }, }); - const renderValidationStatus = (status: string | undefined) => { - if (status === 'success') { - return ; - } else if (status === 'fail') { - return ; - } else if (status === 'unvalidated') { - return ; - } else if (status === 'in-progress') { - return ; - } - }; - const handleMajorUpdate = () => { const payload: DatasetGroup = { dgId, @@ -413,80 +335,81 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { }; const datasetGroupUpdate = () => { - setNodesError(validateClassHierarchy(nodes)); - setValidationRuleError(validateValidationRules(validationRules)); + const classHierarchyError = validateClassHierarchy(nodes); + const validationRulesError = validateValidationRules(validationRules); + + setNodesError(classHierarchyError); + setValidationRuleError(validationRulesError); + if ( - !validateClassHierarchy(nodes) && - !validateValidationRules(validationRules) && - !nodesError && - !validationRuleError + classHierarchyError || + validationRulesError || + nodesError || + validationRuleError ) { - if ( - isMajorUpdate( - { - validationRules: - metadata?.response?.data?.[0]?.validationCriteria - ?.validationRules, - classHierarchy: metadata?.response?.data?.[0]?.classHierarchy, - }, - { - validationRules: - transformValidationRules(validationRules)?.validationRules, - ...transformClassHierarchy(nodes), - } - ) - ) { - open({ - content: - 'Any files imported or edits made to the existing data will be discarded after changes are applied', - title: 'Confirm major update', - footer: ( -
    - - -
    - ), - }); - } else if (minorPayload) { - open({ - content: - 'Any changes you made to the individual data items (patch update) will be discarded after changes are applied', - title: 'Confirm minor update', - footer: ( -
    - - -
    - ), - }); - } else if (patchPayload) { - open({ - content: 'Changed data rows will be updated in the dataset', - title: 'Confirm patch update', - footer: ( -
    - - -
    - ), - }); + return; + } + + const isMajorUpdateDetected = isMajorUpdate( + { + validationRules: metadata?.[0]?.validationCriteria?.validationRules, + classHierarchy: metadata?.[0]?.classHierarchy, + }, + { + validationRules: + transformValidationRules(validationRules)?.validationRules, + ...transformClassHierarchy(nodes), } + ); + + const openConfirmationModal = ( + content: string, + title: string, + onConfirm: () => void + ) => { + open({ + content, + title, + footer: ( +
    + + +
    + ), + }); + }; + + if (isMajorUpdateDetected) { + openConfirmationModal( + t('datasetGroups.detailedView.confirmMajorUpdatesDesc'), + t('datasetGroups.detailedView.confirmMajorUpdatesTitle'), + handleMajorUpdate + ); + } else if (minorPayload) { + openConfirmationModal( + t('datasetGroups.detailedView.confirmMinorUpdatesDesc'), + t('datasetGroups.detailedView.confirmMinorUpdatesTitle'), + () => minorUpdateMutation.mutate(minorPayload) + ); + } else if (patchPayload) { + openConfirmationModal( + t('datasetGroups.detailedView.confirmPatchUpdatesDesc'), + t('datasetGroups.detailedView.confirmPatchUpdatesTitle'), + () => patchUpdateMutation.mutate(patchPayload) + ); } }; const majorUpdateDatasetGroupMutation = useMutation({ mutationFn: (data: DatasetGroup) => majorUpdate(data), - onSuccess: async (response) => { - await queryClient.invalidateQueries(['datasetgroup/overview']); - setView('list'); + onSuccess: async () => { + await queryClient.invalidateQueries(datasetQueryKeys.DATASET_OVERVIEW()); + setView(DatasetViewEnum.LIST); close(); }, onError: () => { @@ -497,320 +420,108 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { }, }); + const handleOpenModals = (context: ViewDatasetGroupModalContexts) => { + setIsModalOpen(true); + setOpenedModalContext(context); + }; + + const handleCloseModals = () => { + setIsModalOpen(false); + setOpenedModalContext(ViewDatasetGroupModalContexts.NULL); + }; + return (
    - {metadata && ( -
    - {' '} - -
    - navigate(0)}> - - -
    - {metadata?.response?.data?.[0]?.name} -
    - {metadata && ( - - )} - {metadata?.response?.data?.[0]?.latest ? ( - - ) : null} - {renderValidationStatus( - metadata?.response?.data?.[0]?.validationStatus - )} -
    - -
    - } - > -
    -
    -

    - Connected Models : - {metadata?.response?.data?.[0]?.linkedModels?.map( - (model, index) => { - return index === metadata?.linkedModels?.length - 1 - ? ` ${model?.modelName}` - : ` ${model?.modelName}, `; - } - )} -

    -

    - Number of items : - {` ${metadata?.response?.data?.[0]?.numSamples}`} -

    -
    -
    - - -
    -
    - - {bannerMessage &&
    {bannerMessage}
    } - {(!datasets || (datasets && datasets?.length < 10)) && ( - -
    - {!datasets && ( -
    -
    - No Data Available -
    -

    - You have created the dataset group, but there are no - datasets available to show here. You can upload a - dataset to view it in this space. Once added, you can - edit or delete the data as needed. -

    - -
    - )} - {datasets && datasets?.length < 10 && ( -
    -

    - Insufficient examples - at least 10 examples are - needed to activate the dataset group. -

    - -
    - )} -
    -
    - )} -
    - )} -
    - {!isLoading && updatedDataset && ( - { - if ( - state.pageIndex === pagination.pageIndex && - state.pageSize === pagination.pageSize - ) - return; - setPagination(state); - getDatasets(state, dgId); - }} - pagesCount={datasets?.numPages} - isClientSide={false} - /> - )} -
    - {metadata && ( -
    - - - - - - {!isMetadataLoading && ( - - )} - -
    - )} -
    + + + + +
    -
    ), }) } > - Delete Dataset + {t('datasetGroups.detailedView.delete') ?? ''} + + -
    - {isImportModalOpen && ( - - - - - } - onClose={() => { - setIsImportModalOpen(false); - setImportStatus('ABORTED'); - }} - > -
    -

    Select the file format

    -
    - -
    -

    Attachments

    - - {importStatus === 'STARTED' && ( -
    -
    - Upload in Progress... -
    -

    - Uploading dataset. Please wait until the upload finishes. If - you cancel midway, the data and progress will be lost. -

    -
    - )} -
    -
    - )} - {isExportModalOpen && ( - - - - - } - onClose={() => { - setIsExportModalOpen(false); - setImportStatus('ABORTED'); - }} - > -
    -

    Select the file format

    -
    - -
    -
    -
    - )} - {patchUpdateModalOpen && ( - setPatchUpdateModalOpen(false)} - isOpen={patchUpdateModalOpen} - > - - - )} - {deleteRowModalOpen && ( - setDeleteRowModalOpen(false)} - title="Are you sure?" - footer={ -
    - - -
    - } - > - Confirm that you are wish to delete the following record -
    - )} + ); }; diff --git a/GUI/src/pages/DatasetGroups/index.tsx b/GUI/src/pages/DatasetGroups/index.tsx index 43e55e49..7d50d793 100644 --- a/GUI/src/pages/DatasetGroups/index.tsx +++ b/GUI/src/pages/DatasetGroups/index.tsx @@ -1,19 +1,17 @@ import { FC, useEffect, useState } from 'react'; import './DatasetGroups.scss'; import { useTranslation } from 'react-i18next'; -import { Button, FormInput, FormSelect } from 'components'; +import { Button, FormSelect } from 'components'; import DatasetGroupCard from 'components/molecules/DatasetGroupCard'; import Pagination from 'components/molecules/Pagination'; import { getDatasetsOverview, getFilterData } from 'services/datasets'; import { useQuery } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; -import { - convertTimestampToDateTime, - formattedArray, - parseVersionString, -} from 'utils/commonUtilts'; -import { DatasetGroup } from 'types/datasetGroups'; +import { formattedArray, parseVersionString } from 'utils/commonUtilts'; +import { SingleDatasetType } from 'types/datasetGroups'; import ViewDatasetGroup from './ViewDatasetGroup'; +import { datasetQueryKeys } from 'utils/queryKeys'; +import { DatasetViewEnum } from 'enums/datasetEnums'; const DatasetGroups: FC = () => { const { t } = useTranslation(); @@ -22,11 +20,11 @@ const DatasetGroups: FC = () => { const [pageIndex, setPageIndex] = useState(1); const [id, setId] = useState(0); const [enableFetch, setEnableFetch] = useState(true); - const [view, setView] = useState("list"); + const [view, setView] = useState(DatasetViewEnum.LIST); -useEffect(()=>{ - setEnableFetch(true) -},[view]); + useEffect(() => { + setEnableFetch(true); + }, [view]); const [filters, setFilters] = useState({ datasetGroupName: 'all', @@ -35,20 +33,16 @@ useEffect(()=>{ sort: 'asc', }); - const { - data: datasetGroupsData, - isLoading, - } = useQuery( - [ - 'datasetgroup/overview', + const { data: datasetGroupsData, isLoading } = useQuery( + datasetQueryKeys.DATASET_OVERVIEW( pageIndex, filters.datasetGroupName, parseVersionString(filters?.version)?.major, parseVersionString(filters?.version)?.minor, parseVersionString(filters?.version)?.patch, filters.validationStatus, - filters.sort, - ], + filters.sort + ), () => getDatasetsOverview( pageIndex, @@ -64,8 +58,9 @@ useEffect(()=>{ enabled: enableFetch, } ); - const { data: filterData } = useQuery(['datasets/filters'], () => - getFilterData() + const { data: filterData } = useQuery( + datasetQueryKeys.DATASET_FILTERS(), + () => getFilterData() ); const pageCount = datasetGroupsData?.response?.data?.[0]?.totalPages || 1; @@ -77,108 +72,117 @@ useEffect(()=>{ })); }; - return (
    - {view==="list" &&(
    -
    -
    Dataset Groups
    - -
    -
    -
    - - handleFilterChange('datasetGroupName', selection?.value ?? '') - } - /> - - handleFilterChange('version', selection?.value ?? '') - } - /> - - handleFilterChange('validationStatus', selection?.value ?? '') - } - /> - - handleFilterChange('sort', selection?.value ?? '') - } - /> - + {view === DatasetViewEnum.LIST && ( +
    +
    +
    {t('datasetGroups.title')}
    -
    - {isLoading &&
    Loading...
    } - {datasetGroupsData?.response?.data?.map( - (dataset, index: number) => { - return ( - - ); - } - )} +
    +
    + + handleFilterChange('datasetGroupName', selection?.value ?? '') + } + /> + + handleFilterChange('version', selection?.value ?? '') + } + /> + + handleFilterChange('validationStatus', selection?.value ?? '') + } + /> + + handleFilterChange('sort', selection?.value ?? '') + } + /> + + +
    +
    + {isLoading &&
    Loading...
    } + {datasetGroupsData?.response?.data?.map( + (dataset: SingleDatasetType, index: number) => { + return ( + + ); + } + )} +
    + 1} + canNextPage={pageIndex < pageCount} + onPageChange={setPageIndex} + />
    - 1} - canNextPage={pageIndex < pageCount} - onPageChange={setPageIndex} - />
    -
    )} - {view==="individual" && ( - + )} + {view === DatasetViewEnum.INDIVIDUAL && ( + )}
    ); diff --git a/GUI/src/pages/Integrations/index.tsx b/GUI/src/pages/Integrations/index.tsx index a74a5bfc..dac7c6eb 100644 --- a/GUI/src/pages/Integrations/index.tsx +++ b/GUI/src/pages/Integrations/index.tsx @@ -7,41 +7,43 @@ import Pinal from 'assets/Pinal'; import Jira from 'assets/Jira'; import { useQuery } from '@tanstack/react-query'; import { getIntegrationStatus } from 'services/integration'; +import { integrationQueryKeys } from 'utils/queryKeys'; const Integrations: FC = () => { const { t } = useTranslation(); - + const { data: integrationStatus } = useQuery( - ['classifier/integration/platform-status'], + integrationQueryKeys.INTEGRATION_STATUS(), () => getIntegrationStatus() ); - return ( -
    -
    -
    {t('integration.title')}
    +
    +
    +
    +
    {t('integration.title')}
    +
    +
    + } + channel={t('integration.jira') ?? ''} + channelDescription={t('integration.jiraDesc') ?? ''} + isActive={integrationStatus?.jira_connection_status} + /> + } + channel={t('integration.outlook') ?? ''} + channelDescription={t('integration.outlookDesc') ?? ''} + isActive={integrationStatus?.outlook_connection_status} + /> + } + channel={t('integration.outlookAndPinal') ?? ''} + channelDescription={t('integration.pinalDesc') ?? ''} + isActive={integrationStatus?.pinal_connection_status} + /> +
    -
    - } - channel={t('integration.jira')??""} - channelDescription={t('integration.jiraDesc')??""} - isActive={integrationStatus?.jira_connection_status} - /> - } - channel={t('integration.outlook')??""} - channelDescription={t('integration.outlookDesc')??""} - isActive={integrationStatus?.outlook_connection_status} - /> - } - channel={"Outlook+Pinal"} - channelDescription={t('integration.pinalDesc')??""} - isActive={integrationStatus?.pinal_connection_status} - /> -
    ); }; diff --git a/GUI/src/pages/UserManagement/UserModal.tsx b/GUI/src/pages/UserManagement/UserModal.tsx index 254d439c..7fa76eb7 100644 --- a/GUI/src/pages/UserManagement/UserModal.tsx +++ b/GUI/src/pages/UserManagement/UserModal.tsx @@ -11,6 +11,8 @@ import Select from 'react-select'; import './SettingsUsers.scss'; import { FC, useMemo } from 'react'; import { ROLES } from 'enums/roles'; +import { userManagementQueryKeys } from 'utils/queryKeys'; +import { ButtonAppearanceTypes, ToastTypes } from 'enums/commonEnums'; type UserModalProps = { onClose: () => void; @@ -53,9 +55,11 @@ const UserModal: FC = ({ onClose, user, isModalOpen }) => { const userCreateMutation = useMutation({ mutationFn: (data: UserDTO) => createUser(data), onSuccess: async () => { - await queryClient.invalidateQueries(['accounts/users']); + await queryClient.invalidateQueries( + userManagementQueryKeys.getAllEmployees() + ); toast.open({ - type: 'success', + type: ToastTypes.SUCCESS, title: t('global.notification'), message: t('toast.success.newUserAdded'), }); @@ -63,9 +67,9 @@ const UserModal: FC = ({ onClose, user, isModalOpen }) => { }, onError: (error: AxiosError) => { toast.open({ - type: 'error', + type: ToastTypes.ERROR, title: t('global.notificationError'), - message: error.message, + message: error?.message ?? '', }); }, }); @@ -78,10 +82,12 @@ const UserModal: FC = ({ onClose, user, isModalOpen }) => { id: string | number; userData: UserDTO; }) => editUser(id, userData), - onSuccess: async () => { - await queryClient.invalidateQueries(['accounts/users']); + onSuccess: async () => { + await queryClient.invalidateQueries( + userManagementQueryKeys.getAllEmployees() + ); toast.open({ - type: 'success', + type: ToastTypes.SUCCESS, title: t('global.notification'), message: t('toast.success.userUpdated'), }); @@ -89,9 +95,9 @@ const UserModal: FC = ({ onClose, user, isModalOpen }) => { }, onError: (error: AxiosError) => { toast.open({ - type: 'error', + type: ToastTypes.ERROR, title: t('global.notificationError'), - message: error.message, + message: error?.message ?? '', }); }, }); @@ -102,7 +108,7 @@ const UserModal: FC = ({ onClose, user, isModalOpen }) => { onSuccess: async (data) => { if (data.response === 'true') { toast.open({ - type: 'error', + type: ToastTypes.SUCCESS, title: t('global.notificationError'), message: t('settings.users.userExists'), }); @@ -112,23 +118,20 @@ const UserModal: FC = ({ onClose, user, isModalOpen }) => { }, onError: (error: AxiosError) => { toast.open({ - type: 'error', + type: ToastTypes.ERROR, title: t('global.notificationError'), - message: error.message, + message: error?.message, }); }, }); - const createNewUser = handleSubmit((userData) => { - userCreateMutation.mutate(userData); - }); + const createNewUser = handleSubmit((userData) => + userCreateMutation.mutate(userData) + ); const handleUserSubmit = handleSubmit((data) => { - if (user) { - userEditMutation.mutate({ id: user.useridcode, userData: data }); - } else { - checkIfUserExistsMutation.mutate({ userData: data }); - } + if (user) userEditMutation.mutate({ id: user.useridcode, userData: data }); + else checkIfUserExistsMutation.mutate({ userData: data }); }); const requiredText = t('settings.users.required') ?? '*'; @@ -136,11 +139,14 @@ const UserModal: FC = ({ onClose, user, isModalOpen }) => { return ( - @@ -154,12 +160,10 @@ const UserModal: FC = ({ onClose, user, isModalOpen }) => { }`.trim()} {...register('fullName', { required: requiredText })} label={t('userManagement.addUser.name')} - placeholder={t('userManagement.addUser.namePlaceholder')??""} + placeholder={t('userManagement.addUser.namePlaceholder') ?? ''} /> - {errors.fullName && ( - - {errors.fullName.message} - + {errors?.fullName && ( + {errors?.fullName?.message} )} = ({ onClose, user, isModalOpen }) => {
    )} /> - {errors.authorities && ( - - {errors.authorities.message} - + {errors?.authorities && ( + {errors?.authorities?.message} )} {!user && ( = ({ onClose, user, isModalOpen }) => { }, })} label={t('userManagement.addUser.personalId')} - placeholder={t('userManagement.addUser.personalIdPlaceholder')??""} - > + placeholder={ + t('userManagement.addUser.personalIdPlaceholder') ?? '' + } + /> )} - {!user && errors.useridcode && ( - - {errors.useridcode.message} - + {!user && errors?.useridcode && ( + {errors?.useridcode?.message} )} = ({ onClose, user, isModalOpen }) => { })} label={t('userManagement.addUser.email')} type="email" - placeholder={t('userManagement.addUser.emailPlaceholder')??""} + placeholder={t('userManagement.addUser.emailPlaceholder') ?? ''} /> - {errors.csaEmail && ( - - {errors.csaEmail.message} - + {errors?.csaEmail && ( + {errors?.csaEmail?.message} )} diff --git a/GUI/src/pages/UserManagement/index.tsx b/GUI/src/pages/UserManagement/index.tsx index 19d09091..ffff66ab 100644 --- a/GUI/src/pages/UserManagement/index.tsx +++ b/GUI/src/pages/UserManagement/index.tsx @@ -1,11 +1,5 @@ -import { FC, useEffect, useMemo, useState } from 'react'; -import { - Button, - DataTable, - Dialog, - Icon, -} from '../../components'; -import users from '../../config/users.json'; +import { FC, useMemo, useState } from 'react'; +import { Button, DataTable, Icon } from '../../components'; import { PaginationState, Row, @@ -23,16 +17,15 @@ import { useToast } from 'hooks/useToast'; import { AxiosError } from 'axios'; import apiDev from 'services/api-dev'; import UserModal from './UserModal'; -import { ROLES } from 'enums/roles'; +import { userManagementQueryKeys } from 'utils/queryKeys'; +import { userManagementEndpoints } from 'utils/endpoints'; +import { ButtonAppearanceTypes, ToastTypes } from 'enums/commonEnums'; +import { useDialog } from 'hooks/useDialog'; const UserManagement: FC = () => { const columnHelper = createColumnHelper(); const [newUserModal, setNewUserModal] = useState(false); const [editableRow, setEditableRow] = useState(null); - const [deletableRow, setDeletableRow] = useState( - null - ); - const [usersList, setUsersList] = useState(null); const [totalPages, setTotalPages] = useState(1); const [pagination, setPagination] = useState({ pageIndex: 0, @@ -42,132 +35,143 @@ const UserManagement: FC = () => { const { t } = useTranslation(); const toast = useToast(); const queryClient = useQueryClient(); + const { open, close } = useDialog(); - const fetchUsers = async (pagination: PaginationState, sorting: SortingState) => { + const fetchUsers = async ( + pagination: PaginationState, + sorting: SortingState + ) => { const sort = - sorting.length === 0 + sorting?.length === 0 ? 'name asc' - : sorting[0].id + ' ' + (sorting[0].desc ? 'desc' : 'asc'); - const { data } = await apiDev.post('accounts/users', { - page: pagination.pageIndex + 1, - page_size: pagination.pageSize, + : sorting[0]?.id + + ' ' + + (sorting[0]?.desc ? t('global.desc') : t('global.asc')); + const { data } = await apiDev.post(userManagementEndpoints.FETCH_USERS(), { + page: pagination?.pageIndex + 1, + page_size: pagination?.pageSize, sorting: sort, }); return data?.response ?? []; }; const { data: users, isLoading } = useQuery( - ['accounts/users', pagination, sorting], + userManagementQueryKeys.getAllEmployees(), () => fetchUsers(pagination, sorting) ); - const editView = (props: any) => ( - - ); - - const deleteView = (props: any) => ( - + const ActionButtons: FC<{ row: User }> = ({ row }) => ( +
    + + + +
    + ), + }); + }} + > + } /> + {t('global.delete')} + +
    ); - const usersColumns = useMemo( () => [ columnHelper.accessor( - (row) => `${row.firstName ?? ''} ${row.lastName ?? ''}`, + (row: User) => `${row?.firstName ?? ''} ${row?.lastName ?? ''}`, { - id: `name`, - header: t('settings.users.name') ?? '', + id: 'name', + header: t('userManagement.table.fullName') ?? '', } ), columnHelper.accessor('useridcode', { - header: t('global.idCode') ?? '', + header: t('userManagement.table.personalId') ?? '', }), columnHelper.accessor( - (data: { authorities: ROLES[] }) => { + (data: User) => { const output: string[] = []; - data.authorities?.map?.((role) => { - return output.push(t(`roles.${role}`)); - }); + data.authorities?.forEach((role) => output.push(t(`roles.${role}`))); return output; }, { - header: t('settings.users.role') ?? '', - cell: (props) => props.getValue().join(', '), - filterFn: (row: Row, _, filterValue) => { - const rowAuthorities: string[] = []; - row.original.authorities.map((role) => { - return rowAuthorities.push(t(`roles.${role}`)); - }); - const filteredArray = rowAuthorities.filter((word) => + header: t('userManagement.table.role') ?? '', + cell: (props) => props.getValue().join(', '), + filterFn: (row: Row, _, filterValue: string) => { + const rowAuthorities = row.original.authorities.map((role) => + t(`roles.${role}`) + ); + return rowAuthorities.some((word) => word.toLowerCase().includes(filterValue.toLowerCase()) ); - return filteredArray.length > 0; }, } ), - columnHelper.accessor('displayName', { - header: t('settings.users.displayName') ?? '', - }), - columnHelper.accessor('csaTitle', { - header: t('settings.users.userTitle') ?? '', - }), columnHelper.accessor('csaEmail', { - header: t('settings.users.email') ?? '', + header: t('userManagement.table.email') ?? '', }), columnHelper.display({ - id: 'edit', - cell: editView, - meta: { - size: '1%', - }, - }), - columnHelper.display({ - id: 'delete', - cell: deleteView, + id: 'actions', + header: t('userManagement.table.actions') ?? '', + cell: (props) => , meta: { size: '1%', }, }), ], - [] + [t] ); - const deleteUserMutation = useMutation({ mutationFn: ({ id }: { id: string | number }) => deleteUser(id), onSuccess: async () => { - await queryClient.invalidateQueries(['accounts/users']); + await queryClient.invalidateQueries( + userManagementQueryKeys.getAllEmployees() + ); toast.open({ - type: 'success', + type: ToastTypes.SUCCESS, title: t('global.notification'), message: t('toast.success.userDeleted'), }); - setDeletableRow(null); }, onError: (error: AxiosError) => { toast.open({ - type: 'error', + type: ToastTypes.ERROR, title: t('global.notificationError'), - message: error.message, + message: error?.message ?? '', }); }, }); - if (isLoading) return
    Loading...
    ; + if (isLoading) return
    {t('global.loading')}...
    ; return (
    @@ -175,13 +179,13 @@ const UserManagement: FC = () => {
    {t('userManagement.title')}
    @@ -193,8 +197,8 @@ const UserManagement: FC = () => { pagination={pagination} setPagination={(state: PaginationState) => { if ( - state.pageIndex === pagination.pageIndex && - state.pageSize === pagination.pageSize + state?.pageIndex === pagination?.pageIndex && + state?.pageSize === pagination?.pageSize ) return; setPagination(state); @@ -208,38 +212,19 @@ const UserManagement: FC = () => { pagesCount={totalPages} isClientSide={false} /> - {deletableRow !== null && ( - setDeletableRow(null)} - isOpen={true} - footer={ -
    - - -
    - } - > -

    {t('global.removeValidation')}

    -
    + + {newUserModal && ( + setNewUserModal(false)} + /> )} - {newUserModal && setNewUserModal(false)} />} {editableRow && ( setEditableRow(null)} - isModalOpen={editableRow !==null} + isModalOpen={editableRow !== null} /> )}
    diff --git a/GUI/src/services/datasets.ts b/GUI/src/services/datasets.ts index 218e4e15..f1248064 100644 --- a/GUI/src/services/datasets.ts +++ b/GUI/src/services/datasets.ts @@ -1,9 +1,9 @@ +import { datasetsEndpoints } from 'utils/endpoints'; import apiDev from './api-dev'; import apiExternal from './api-external'; - - import { PaginationState } from '@tanstack/react-table'; -import { DatasetGroup, Operation } from 'types/datasetGroups'; +import { DatasetDetails, DatasetGroup, MetaData, MinorPayLoad, Operation, PatchPayLoad } from 'types/datasetGroups'; + export async function getDatasetsOverview( pageNum: number, name: string, @@ -13,23 +13,23 @@ export async function getDatasetsOverview( validationStatus: string, sort: string ) { - const { data } = await apiDev.get('classifier/datasetgroup/overview', { + const { data } = await apiDev.get(datasetsEndpoints.GET_OVERVIEW(), { params: { page: pageNum, - groupName:name, + groupName: name, majorVersion, minorVersion, patchVersion, validationStatus, - sortType:sort, - pageSize:12 + sortType: sort, + pageSize: 12, }, }); return data; } export async function enableDataset(enableData: Operation) { - const { data } = await apiDev.post('classifier/datasetgroup/update/status', { + const { data } = await apiDev.post(datasetsEndpoints.ENABLE_DATASET(), { dgId: enableData.dgId, operationType: enableData.operationType, }); @@ -37,7 +37,9 @@ export async function enableDataset(enableData: Operation) { } export async function getFilterData() { - const { data } = await apiDev.get('classifier/datasetgroup/overview/filters'); + const { data } = await apiDev.get( + datasetsEndpoints.GET_DATASET_OVERVIEW_BY_FILTERS() + ); return data; } @@ -45,91 +47,100 @@ export async function getDatasets( pagination: PaginationState, groupId: string | number | null ) { - const { data } = await apiDev.get('classifier/datasetgroup/group/data', { + const { data } = await apiDev.get(datasetsEndpoints.GET_DATASETS(), { params: { - pageNum: pagination.pageIndex+1, + pageNum: pagination.pageIndex + 1, groupId, }, }); - - - return data?.response?.data[0]; + + return data?.response?.data[0] as DatasetDetails; } export async function getMetadata(groupId: string | number | null) { - const { data } = await apiDev.get('classifier/datasetgroup/group/metadata', { + const { data } = await apiDev.get(datasetsEndpoints.GET_METADATA(), { params: { - groupId + groupId, }, }); - return data; + return data?.response?.data as MetaData[]; } export async function createDatasetGroup(datasetGroup: DatasetGroup) { - - const { data } = await apiDev.post('classifier/datasetgroup/create', { + const { data } = await apiDev.post(datasetsEndpoints.CREATE_DATASET_GROUP(), { ...datasetGroup, }); return data; } -export async function importDataset(file: File, id: string|number) { - - - const { data } = await apiExternal.post('datasetgroup/data/import', { - dataFile:file, - dgId:id +export async function importDataset(file: File, id: string | number) { + const { data } = await apiExternal.post(datasetsEndpoints.IMPORT_DATASETS(), { + dataFile: file, + dgId: id, }); return data; } -export async function exportDataset(id: string, type: string) { +export async function exportDataset(id: number, type: string) { const headers = { 'Content-Type': 'application/json', - } - const { data } = await apiExternal.post('datasetgroup/data/download', { - dgId: id, - exportType: type, - },{headers,responseType: 'blob'}); + }; + const { data } = await apiExternal.post( + datasetsEndpoints.EXPORT_DATASETS(), + { + dgId: id, + exportType: type, + }, + { headers, responseType: 'blob' } + ); return data; } -export async function patchUpdate(updatedData: DatasetGroup) { - const { data } = await apiDev.post('classifier/datasetgroup/update/patch', { - ...updatedData, - }); +export async function patchUpdate(updatedData: PatchPayLoad) { + const { data } = await apiDev.post( + datasetsEndpoints.DATASET_GROUP_PATCH_UPDATE(), + { + ...updatedData, + } + ); return data; } -export async function minorUpdate(updatedData) { - const { data } = await apiDev.post('classifier/datasetgroup/update/minor', { - ...updatedData, - }); +export async function minorUpdate(updatedData: MinorPayLoad) { + const { data } = await apiDev.post( + datasetsEndpoints.DATASET_GROUP_MINOR_UPDATE(), + { + ...updatedData, + } + ); return data; } export async function majorUpdate(updatedData: DatasetGroup) { - const { data } = await apiDev.post('classifier/datasetgroup/update/major', { - ...updatedData, - }); + const { data } = await apiDev.post( + datasetsEndpoints.DATASET_GROUP_MAJOR_UPDATE(), + { + ...updatedData, + } + ); return data; } export async function getStopWords() { - const { data } = await apiDev.get('classifier/datasetgroup/stop-words'); + const { data } = await apiDev.get(datasetsEndpoints.GET_STOP_WORDS()); return data?.response?.stopWords; } export async function addStopWord(stopWordData) { - const { data } = await apiDev.post('classifier/datasetgroup/update/stop-words',{ -...stopWordData + const { data } = await apiDev.post(datasetsEndpoints.POST_STOP_WORDS(), { + ...stopWordData, }); return data; } export async function deleteStopWord(stopWordData) { - const { data } = await apiDev.post('classifier/datasetgroup/delete/stop-words',{ -...stopWordData + const { data } = await apiDev.post(datasetsEndpoints.DELETE_STOP_WORD(), { + ...stopWordData, }); return data; } @@ -137,4 +148,4 @@ export async function deleteStopWord(stopWordData) { export async function getDatasetGroupsProgress() { const { data } = await apiDev.get('classifier/datasetgroup/progress'); return data?.response?.data; -} \ No newline at end of file +} diff --git a/GUI/src/types/datasetGroups.ts b/GUI/src/types/datasetGroups.ts index 1c7a8ea6..9fa2bce2 100644 --- a/GUI/src/types/datasetGroups.ts +++ b/GUI/src/types/datasetGroups.ts @@ -1,5 +1,7 @@ +import { ValidationStatus } from 'enums/datasetEnums'; + export interface ValidationRule { - id: number; + id: number | string; fieldName: string; dataType: string; isDataClass: boolean; @@ -30,33 +32,102 @@ export interface Dataset { } export interface Operation { - dgId: number|undefined; + dgId: number | undefined; operationType: 'enable' | 'disable'; } export interface ValidationRuleResponse { type: string; isDataClass: boolean; -}; +} export interface ValidationCriteria { fields: string[]; validationRules: Record; -}; +} export interface ClassNode { class: string; subclasses: ClassNode[]; -}; +} export interface DatasetGroup { dgId?: number; - groupName?:string; + groupName?: string; validationCriteria: ValidationCriteria; classHierarchy: ClassNode[]; -}; +} export interface ImportDataset { - dgId: number|string; + dgId: number | string; dataFile: File; -} \ No newline at end of file +} + +export type SingleDatasetType = { + createdTimestamp: Date; + enableAllowed: boolean; + groupName: string; + id: number; + isEnabled: boolean; + lastModelTrained: Date | null; + lastTrainedTimestamp: Date | null; + lastUpdatedTimestamp: Date | null; + latest: boolean; + majorVersion: number; + minorVersion: number; + patchVersion: number; + totalPages: number; + validationStatus: ValidationStatus; +}; + +export type TreeNode = { + id: string; + fieldName: string; + level: number; + children: TreeNode[] | []; +}; + +export type MinorPayLoad = { + dgId: number; + s3FilePath: any; +}; + +export type PatchPayLoad = { + dgId: number; + updateDataPayload: { + deletedDataRows: any[]; + editedData: any; + }; +}; + +export type MetaData = { + dgId: number; + name: string; + majorVersion: number; + minorVersion: number; + patchVersion: number; + latest: boolean; + operationSuccessful: boolean; + isEnabled: boolean; + numSamples: number; + enableAllowed: boolean; + validationStatus: ValidationStatus; + validationErrors: string[]; + isValidated: boolean; + linkedModels: LinkedModel[]; + validationCriteria: ValidationCriteria; + classHierarchy: ClassNode[]; +}; + +type DataPayload = Record; + +export type DatasetDetails = { + dgId: number; + numPages: number; + dataPayload: DataPayload[]; + fields: string[]; +}; + +export type SelectedRowPayload = { + rowId: number; +} & Record; \ No newline at end of file diff --git a/GUI/src/types/integration.ts b/GUI/src/types/integration.ts index 9e58f749..b223e15b 100644 --- a/GUI/src/types/integration.ts +++ b/GUI/src/types/integration.ts @@ -1,4 +1,4 @@ - export interface OperationConfig { - operation: 'enable'|'disable'; - platform: string | undefined; - } \ No newline at end of file +export interface OperationConfig { + operation: 'enable' | 'disable'; + platform: string | undefined; +} diff --git a/GUI/src/types/mainNavigation.ts b/GUI/src/types/mainNavigation.ts index 2a2077b9..d53f6993 100644 --- a/GUI/src/types/mainNavigation.ts +++ b/GUI/src/types/mainNavigation.ts @@ -1,4 +1,4 @@ -import { ReactNode } from "react"; +import { ReactNode } from 'react'; export interface MenuItem { id?: string; @@ -6,7 +6,7 @@ export interface MenuItem { path: string | null; target?: '_blank' | '_self'; children?: MenuItem[]; - icon?:ReactNode + icon?: ReactNode; } export interface MainNavigation { diff --git a/GUI/src/utils/commonUtilts.ts b/GUI/src/utils/commonUtilts.ts index a72a6976..7ee6dcaf 100644 --- a/GUI/src/utils/commonUtilts.ts +++ b/GUI/src/utils/commonUtilts.ts @@ -30,3 +30,7 @@ export const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { }); return itemRank.passed; }; + +export const formatDate = (date: Date, format: string) => { + return moment(date).format(format); +}; diff --git a/GUI/src/utils/dataTableUtils.ts b/GUI/src/utils/dataTableUtils.ts new file mode 100644 index 00000000..5b757aa6 --- /dev/null +++ b/GUI/src/utils/dataTableUtils.ts @@ -0,0 +1,34 @@ +import { CellContext, createColumnHelper } from '@tanstack/react-table'; + +export const generateDynamicColumns = ( + columnsData: string[], + editView: (props: CellContext) => JSX.Element, + deleteView: (props: CellContext) => JSX.Element +) => { + const columnHelper = createColumnHelper(); + const dynamicColumns = columnsData?.map((col) => { + return columnHelper.accessor(col, { + header: col ?? '', + id: col, + }); + }); + + const staticColumns = [ + columnHelper.display({ + id: 'edit', + cell: editView, + meta: { + size: '1%', + }, + }), + columnHelper.display({ + id: 'delete', + cell: deleteView, + meta: { + size: '1%', + }, + }), + ]; + if (dynamicColumns) return [...dynamicColumns, ...staticColumns]; + else return []; +}; diff --git a/GUI/src/utils/datasetGroupsUtils.ts b/GUI/src/utils/datasetGroupsUtils.ts index c99ce576..2ecd392a 100644 --- a/GUI/src/utils/datasetGroupsUtils.ts +++ b/GUI/src/utils/datasetGroupsUtils.ts @@ -1,4 +1,4 @@ -import { Class, ValidationRule } from 'types/datasetGroups'; +import { Class, TreeNode, ValidationRule, ValidationRuleResponse } from 'types/datasetGroups'; import { v4 as uuidv4 } from 'uuid'; import isEqual from 'lodash/isEqual'; @@ -11,9 +11,9 @@ export const transformValidationRules = ( }; data?.forEach((item) => { - validationCriteria.fields.push(item.fieldName); + validationCriteria?.fields.push(item?.fieldName); - validationCriteria.validationRules[item.fieldName] = { + validationCriteria.validationRules[item?.fieldName] = { type: item.dataType.toLowerCase(), isDataClass: item.isDataClass, }; @@ -35,13 +35,13 @@ export const transformClassHierarchy = (data: Class[]) => { }; }; -export const reverseTransformClassHierarchy = (data) => { - const traverse = (node, level: number) => { +export const reverseTransformClassHierarchy = (data: any) => { + const traverse = (node: any, level: number) => { const flatNode = { id: uuidv4(), - fieldName: node.class, + fieldName: node?.class, level, - children: node?.subclasses.map((subclass) => + children: node?.subclasses?.map((subclass: any) => traverse(subclass, level + 1) ), }; @@ -49,10 +49,10 @@ export const reverseTransformClassHierarchy = (data) => { return flatNode; }; - return data?.map((item) => traverse(item, 0)); + return data?.map((item: any) => traverse(item, 0)); }; -export const transformObjectToArray = (data) => { +export const transformObjectToArray = (data: Record | undefined) => { if (data) { const output = Object.entries(data).map(([fieldName, details], index) => ({ id: index + 1, @@ -116,7 +116,7 @@ export const isValidationRulesSatisfied = (data: ValidationRule[]) => { return false; }; -export const isFieldNameExisting = (dataArray, fieldNameToCheck) => { +export const isFieldNameExisting = (dataArray: ValidationRule[] | undefined, fieldNameToCheck: string) => { const count = dataArray?.reduce((acc, item) => { return item?.fieldName?.toLowerCase() === fieldNameToCheck?.toLowerCase() ? acc + 1 @@ -126,10 +126,10 @@ export const isFieldNameExisting = (dataArray, fieldNameToCheck) => { return count === 2; }; -export const countFieldNameOccurrences = (dataArray, fieldNameToCheck) => { +export const countFieldNameOccurrences = (dataArray:TreeNode[] | undefined, fieldNameToCheck: string) => { let count = 0; - function countOccurrences(node) { + function countOccurrences(node: TreeNode) { if (node?.fieldName?.toLowerCase() === fieldNameToCheck?.toLowerCase()) { count += 1; } @@ -139,20 +139,20 @@ export const countFieldNameOccurrences = (dataArray, fieldNameToCheck) => { } } - dataArray.forEach((node) => countOccurrences(node)); + dataArray && dataArray.forEach((node) => countOccurrences(node)); return count; }; -export const isClassHierarchyDuplicated = (dataArray, fieldNameToCheck) => { +export const isClassHierarchyDuplicated = (dataArray: TreeNode[] | undefined, fieldNameToCheck: string) => { const count = countFieldNameOccurrences(dataArray, fieldNameToCheck); return count === 2; }; -export const handleDownload = (response, format) => { +export const handleDownload = (response: any, format: string) => { try { // Create a URL for the Blob - const url = window.URL.createObjectURL(new Blob([response.data])); + const url = window.URL.createObjectURL(new Blob([response])); const link = document.createElement('a'); link.href = url; link.setAttribute('download', `export.${format}`); // Specify the file name and extension diff --git a/GUI/src/utils/endpoints.ts b/GUI/src/utils/endpoints.ts index 8d0afdfe..a5cf49cc 100644 --- a/GUI/src/utils/endpoints.ts +++ b/GUI/src/utils/endpoints.ts @@ -4,7 +4,7 @@ export const userManagementEndpoints = { CHECK_ACCOUNT_AVAILABILITY: (): string => `/accounts/exists`, EDIT_USER: (): string => `/accounts/edit`, DELETE_USER: (): string => `/accounts/delete`, - FETCH_USER_ROLES: (): string => `/accounts/user-role` + FETCH_USER_ROLES: (): string => `/accounts/user-role`, }; export const integrationsEndPoints = { @@ -12,3 +12,21 @@ export const integrationsEndPoints = { `/classifier/integration/platform-status`, TOGGLE_PLATFORM: (): string => `/classifier/integration/toggle-platform`, }; + +export const datasetsEndpoints = { + GET_OVERVIEW: (): string => '/classifier/datasetgroup/overview', + GET_DATASET_OVERVIEW_BY_FILTERS: (): string => + '/classifier/datasetgroup/overview/filters', + ENABLE_DATASET: (): string => `/classifier/datasetgroup/update/status`, + GET_DATASETS: (): string => `/classifier/datasetgroup/group/data`, + GET_METADATA: (): string => `/classifier/datasetgroup/group/metadata`, + CREATE_DATASET_GROUP: (): string => `/classifier/datasetgroup/create`, + IMPORT_DATASETS: (): string => `/datasetgroup/data/import`, + EXPORT_DATASETS: (): string => `/datasetgroup/data/download`, + DATASET_GROUP_PATCH_UPDATE: (): string => `/classifier/datasetgroup/update/patch`, + DATASET_GROUP_MINOR_UPDATE: (): string => `/classifier/datasetgroup/update/minor`, + DATASET_GROUP_MAJOR_UPDATE: (): string => `/classifier/datasetgroup/update/major`, + GET_STOP_WORDS: (): string => `/classifier/datasetgroup/stop-words`, + POST_STOP_WORDS: (): string => `/classifier/datasetgroup/update/stop-words`, + DELETE_STOP_WORD: (): string => `/classifier/datasetgroup/delete/stop-words` +}; diff --git a/GUI/src/utils/queryKeys.ts b/GUI/src/utils/queryKeys.ts index 7f54ad3a..7f025f93 100644 --- a/GUI/src/utils/queryKeys.ts +++ b/GUI/src/utils/queryKeys.ts @@ -15,3 +15,37 @@ export const integrationQueryKeys = { ], USER_ROLES: (): string[] => ['/accounts/user-role', 'prod'], }; + +export const datasetQueryKeys = { + DATASET_FILTERS: (): string[] => ['datasets/filters'], + DATASET_OVERVIEW: function ( + pageIndex?: number, + datasetGroupName?: string, + versionMajor?: number, + versionMinor?: number, + versionPatch?: number, + validationStatus?: string, + sort?: string + ) { + return [ + 'datasetgroup/overview', + pageIndex, + datasetGroupName, + versionMajor, + versionMinor, + versionPatch, + validationStatus, + sort, + ].filter((val) => val !== undefined); + }, + GET_MATA_DATA: function (dgId?: number) { + return ['datasets/groups/metadata', `${dgId}`].filter( + (val) => val !== undefined + ); + }, + GET_DATA_SETS: function (dgId?: number, pagination?: PaginationState) { + return ['datasets/groups/data', `${dgId}`, pagination].filter( + (val) => val !== undefined + ); + }, +}; diff --git a/GUI/translations/en/common.json b/GUI/translations/en/common.json index 31e0257c..d7ecfbb2 100644 --- a/GUI/translations/en/common.json +++ b/GUI/translations/en/common.json @@ -35,10 +35,13 @@ "endDate": "End date", "preview": "Preview", "logout": "Logout", + "change": "Change", + "loading": "Loading", + "asc": "asc", + "desc": "desc", + "reset": "Reset", "choose": "Choose" - }, - "menu": { "userManagement": "User Management", "integration": "Integration", @@ -54,6 +57,10 @@ "title": "User Management", "addUserButton": " Add a user", "addUser": { + "addUserModalTitle": "Add a new user", + "editUserModalTitle": "Edit User", + "deleteUserModalTitle": "Are you sure?", + "deleteUserModalDesc": "Confirm that you are wish to delete the following record", "name": "First and last name", "namePlaceholder": "Enter name", "role": "Role", @@ -64,6 +71,13 @@ "titlePlaceholder": "Enter title", "email": "Email", "emailPlaceholder": "Enter email" + }, + "table": { + "fullName": "Full Name", + "personalId": "Personal ID", + "role": "Role", + "email": "Email", + "actions": "Actions" } }, "integration": { @@ -71,6 +85,7 @@ "jira": "Jira", "outlook": "Outlook", "pinal": "Pinal", + "outlookAndPinal": "Outlook+Pinal", "jiraDesc": "Atlassian issue tracking and project management software", "outlookDesc": "Personal information manager and email application developed by Microsoft", "pinalDesc": "Atlassian issue tracking and project management software", @@ -83,7 +98,7 @@ "confirmationModalTitle": "Are you sure?", "disconnectConfirmationModalDesc": "Are you sure you want to disconnect the {{channel}} integration? This action cannot be undone and may affect your workflow and linked issues.", "connectConfirmationModalDesc": "Are you sure you want to connect the {{channel}} integration?", - "disconnectErrorTitle": "Disconnection Unsuccessful", + "disconnectErrorTi/tle": "Disconnection Unsuccessful", "disconnectErrorDesc": "Failed to disconnect {{channel}}. Please check your settings and try again. If the problem persists, contact support for assistance.", "addUserButton": " Add a user", "addUser": { @@ -103,7 +118,6 @@ "ROLE_ADMINISTRATOR": "Administrator", "ROLE_MODEL_TRAINER": "Model Trainer" }, - "toast": { "success": { "updateSuccess": "Updated Successfully", @@ -112,5 +126,170 @@ "newUserAdded": "New user added", "userUpdated": "User updated" } + }, + "datasetGroups": { + "title": "Dataset Groups", + "createDatasetGroupButton": "Create Dataset Group", + "table": { + "group": "Dataset Group", + "version": "Version", + "validationStatus": "Validation Status", + "sortBy": "Sort by name ({{sortOrder}})", + "email": "Email", + "actions": "Actions" + }, + "datasetCard": { + "validationFail": "Validation Failed", + "validationSuccess": "Validation Successful", + "validationInprogress": "Validation in Progress", + "notValidated": "Not Validated", + "settings": "Settings", + "lastModelTrained": "Last Model Trained", + "lastUsedForTraining": "Last Used For Training", + "lastUpdate": "Last Updated", + "latest": "Latest" + }, + "createDataset": { + "title": "Create Dataset Group", + "datasetDetails": "Dataset Details", + "datasetName": "Dataset Name", + "datasetInputPlaceholder": "Enter dataset name", + "validationCriteria": "Create Validation Criteria", + "fieldName": "Field name", + "datasetType": "Dataset types", + "dataClass": "Data class", + "typeText": "Text", + "typeNumbers": "Numbers", + "typeDateTime": "DateTime", + "addClassButton": "Add class", + "addNowButton": "Add now" + }, + "classHierarchy": { + "title": "Class Hierarchy", + "addClassButton":"Add main class", + "addSubClass": "Add Subclass", + "fieldHint": "Enter a field name", + "filedHintIfExists": "Class name already exists" + }, + "modals": { + "deleteClassTitle": "Are you sure?", + "deleteClaassDesc": "Confirm that you are wish to delete the following record", + "columnInsufficientHeader": "Insufficient Columns in Dataset", + "columnInsufficientDescription": "The dataset must have at least 2 columns. Additionally, there needs to be at least one column designated as a data class and one column that is not a data class. Please adjust your dataset accordingly.", + "createDatasetSuccessTitle": "Dataset Group Created Successfully", + "createDatasetUnsuccessTitle": "Dataset Group Creation Unsuccessful", + "createDatasetSucceessDesc": "You have successfully created the dataset group. In the detailed view, you can now see and edit the dataset as needed.", + "navigateDetailedViewButton": "Go to Detailed View", + "enableDatasetTitle": "Cannot Enable Dataset Group", + "enableDatasetDesc": "The dataset group cannot be enabled until data is added. Please add datasets to this group and try again.", + "errorTitle": "Operation Unsuccessful", + "errorDesc": "Something went wrong. Please try again." + }, + "detailedView": { + "connectedModels": "Connected Models", + "noOfItems": "Number of items", + "export": "Export Dataset", + "import": "Import Dataset", + "unsavedChangesWarning": "You have made changes to the ataset which are not saved. Please save the changes to apply", + "insufficientExamplesDesc": "Insufficient examples - at least 10 examples are needed to activate the dataset group", + "noData": "No Data Available", + "noDataDesc": "You have created the dataset group, but there are no datasets available to show here. You can upload a dataset to view it in this space. Once added, you can edit or delete the data as needed.", + "importExamples": "Import Examples", + "importNewData": "Import New Data", + "majorUpdateBanner": "You have updated key configurations of the dataset schema which are not saved, please save to apply changes. Any files imported or edits made to the existing data will be discarded after changes are applied", + "minorUpdateBanner": "You have imported new data into the dataset, please save the changes to apply. Any changes you made to the individual data items will be discarded after changes are applied", + "patchUpdateBanner": "You have edited individual items in the dataset which are not saved. Please save the changes to apply", + "confirmMajorUpdatesTitle": "Confirm major update", + "confirmMajorUpdatesDesc": "Any files imported or edits made to the existing data will be discarded after changes are applied", + "confirmMinorUpdatesTitle": "Confirm minor update", + "confirmMinorUpdatesDesc": "Any changes you made to the individual data items (patch update) will be discarded after changes are applied", + "confirmPatchUpdatesTitle": "Confirm patch update", + "confirmPatchUpdatesDesc": "Changed data rows will be updated in the dataset", + "patchDataUnsuccessfulTitle": "Patch Data Update Unsuccessful", + "patchDataUnsuccessfulDesc": "Something went wrong. Please try again.", + "exportDataSuccessTitle": "Data export was successful", + "exportDataSuccessDesc": "Your data has been successfully exported.", + "exportDataUnsucessTitle": "Dataset Export Unsuccessful", + "exportDataUnsucessDesc": "Something went wrong. Please try again.", + "ImportDataUnsucessTitle": "Dataset Import Unsuccessful", + "importDataUnsucessDesc": "Something went wrong. Please try again.", + "validationInitiatedTitle": "Dataset uploaded and validation initiated", + "validationInitiatedDesc": "The dataset file was successfully uploaded. The validation and preprocessing is now initiated", + "viewValidations": "View Validation Sessions", + "table": { + "id": "rowId", + "data": "Data", + "label": "Label", + "actions": "Actions" + }, + "validationsTitle": "Dataset Group Validations", + "classHierarchy": "Class Hierarchies", + "delete": "Delete Dataset", + "modals": { + "import": { + "title": "Import New Data", + "fileFormatlabel": "Select the file format", + "attachments": "Attachments", + "maxSize": "Maximum file size - 10mb", + "browse": "Browse file", + "import": "Import", + "cancel": "Cancel", + "uploadInProgress": "Upload in Progress...", + "uploadDesc": "Uploading dataset. Please wait until the upload finishes. If you cancel midwway, the data and progress will be lost.", + "invalidFile": "Invalid File Format", + "invalidFileDesc": "The uploaded file is not in the correct {{format}} format. Please upload a valid {{format}} file and try again." + }, + "export": { + "export": "Export Data", + "exportButton": "Export", + "fileFormatlabel": "Select the file format", + "title": "Data export was successful", + "description": "Your data has been successfully exported." + }, + "delete": { + "title": "Are you sure?", + "description": "Once you delete the dataset all models connected to this model will become untrainable. Are you sure you want to proceed?" + }, + "edit": { + "title": "Edit", + "data": "Data", + "label": "Label", + "update": "Update" + }, + "upload": { + "title": "Data upload successful", + "desc": "The dataset file was successfully uploaded. Please save the changes to initiate data validaiton and preprocessing" + }, + "datasetDelete": { + "confirmationTitle": "Are you sure?", + "confirmationDesc": "Confirm that you are wish to delete the following dataset", + "successTitle": "Success: Dataset Deleted", + "successDesc": "You have successfully deleted the dataset. The dataset is no longer available and all related data has been removed." + } + } + } + }, + "stopWords": { + "title": "Stop Words", + "import": "Import stop words", + "stopWordInputHint": "Enter stop word", + "add": "Add", + "importModal": { + "title": "Import stop words", + "selectionLabel": "Select the option below", + "addOption": "Import to add", + "updateOption": "Import to update", + "deleteOption": "Import to delete", + "attachements": "Attachments (TXT, XLSX, YAML, JSON)", + "inprogressTitle": "Import in Progress", + "inprogressDesc": "The import of stop words is currently in progress. Please wait until the process is complete.", + "successTitle": "Data import waas successful", + "successDesc": "Your data has been successfully imported." + } + }, + "validationSessions": { + "title": "Validation Sessions", + "inprogress": "Validation in-Progress", + "fail": "Validation failed because {{class}} class found in the {{column}} column does not exist in hierarchy" } } diff --git a/GUI/translations/et/common.json b/GUI/translations/et/common.json index 7d70424b..e8d1858a 100644 --- a/GUI/translations/et/common.json +++ b/GUI/translations/et/common.json @@ -5,355 +5,290 @@ "edit": "Muuda", "delete": "Kustuta", "cancel": "Tühista", + "confirm": "Kinnita", "modifiedAt": "Viimati muudetud", "addNew": "Lisa uus", - "dependencies": "Sõltuvused", - "language": "Keel", - "choose": "Vali", "search": "Otsi", - "notification": "Teade", - "notificationError": "Veateade", + "notification": "Teavitus", + "notificationError": "Viga", "active": "Aktiivne", "activate": "Aktiveeri", "deactivate": "Deaktiveeri", + "disconnect":"Ühenda lahti", + "connect":"Ühenda", "on": "Sees", "off": "Väljas", "back": "Tagasi", "from": "Alates", "to": "Kuni", - "view": "Vaata", - "resultCount": "Kuvan korraga", - "paginationNavigation": "Lehtedel navigeerimine", + "view": "Vaade", + "resultCount": "Tulemuste arv", + "paginationNavigation": "Lehitsemine", "gotoPage": "Mine lehele", "name": "Nimi", - "idCode": "Isikukood", + "idCode": "ID kood", "status": "Staatus", - "statusChangeQuestion": "Kas soovid muuta staatuseks \"kohal\"?", "yes": "Jah", "no": "Ei", - "removeValidation": "Oled sa kindel?", - "startDate": "Algusaeg", - "endDate": "Lõppaeg", + "removeValidation": "Oled kindel?", + "startDate": "Alguskuupäev", + "endDate": "Lõppkuupäev", "preview": "Eelvaade", "logout": "Logi välja", - "anonymous": "Anonüümne", - "csaStatus": "Nõustaja", - "present": "Kohal", - "away": "Eemal", - "today": "Täna", - "forward": "Suuna", - "chosen": "Valitud", - "read": "Loetud" - }, - "mainMenu": { - "menuLabel": "Põhinavigatsioon", - "closeMenu": "Kitsendan menüü", - "openMenuIcon": "Ava menüü ikoon", - "closeMenuIcon": "Sulge menüü ikoon" + "change": "Muuda", + "loading": "Laadimine", + "asc": "kasvav", + "desc": "kahanev", + "reset": "Lähtesta", + "choose": "Vali" }, "menu": { - "conversations": "Vestlused", - "unanswered": "Vastamata", - "active": "Aktiivsed", - "history": "Ajalugu", - "training": "Treening", - "themes": "Teemad", - "answers": "Vastused", - "userStories": "Vestlusvood", - "configuration": "Seaded", - "forms": "Vormid", - "slots": "Pilud", - "historicalConversations": "Ajaloolised vestlused", - "modelBankAndAnalytics": "Mudelipank ja analüütika", - "overviewOfTopics": "Teemade ülevaade", - "comparisonOfModels": "Mudelite võrdlus", - "appeals": "Pöördumised", - "testTracks": "Testlood", - "trainNewModel": "Treeni uus mudel", - "analytics": "Analüütika", - "settings": "Seaded", - "overview": "Ülevaade", - "chats": "Vestlused", - "burokratt": "Bürokratt", - "feedback": "Tagasiside", - "advisors": "Nõustajad", - "reports": "Avaandmed", - "users": "Kasutajad", - "administration": "Haldus", - "chatbot": "Vestlusbot", - "welcomeMessage": "Tervitussõnum", - "appearanceAndBehavior": "Välimus ja käitumine", - "emergencyNotices": "Erakorralised teated", - "officeOpeningHours": "Asutuse tööaeg", - "sessionLength": "Sessiooni pikkus", - "monitoring": "Seire", - "workingHours": "Tööaeg" + "userManagement": "Kasutajahaldus", + "integration": "Integratsioon", + "datasets": "Andmekogumid", + "datasetGroups": "Andmekogumite grupid", + "validationSessions": "Valideerimise seansid", + "dataModels": "Andmemudelid", + "testModel": "Testimismudel", + "stopWords": "Peatussõnad", + "incomingTexts": "Saabuvad tekstid" }, - "chat": { - "reply": "Vasta", - "unansweredChats": "Vastamata vestlused", - "unanswered": "Vastamata", - "forwarded": "Suunatud", - "endUser": "Vestleja nimi", - "endUserId": "Vestleja isikukood", - "csaName": "Nõustaja nimi", - "endUserEmail": "Vestleja e-post", - "endUserPhoneNumber": "Vestleja telefoninumber", - "startedAt": "Vestlus alustatud", - "device": "Seade", - "location": "Lähtekoht", - "redirectedMessageByOwner": "{{from}} suunas vestluse kasutajale {{to}} {{date}}", - "redirectedMessageClaimed": "{{to}} võttis vestluse üle kasutajalt {{from}} {{date}}", - "redirectedMessage": "{{user}} suunas vestluse kasutajalt {{from}} kasutajale {{to}} {{date}}", - "new": "uued", - "inProcess": "töös", - "status": { - "active": "Aktiivne", - "ended": "Määramata" - }, - "chatStatus": "Vestluse staatus", - "changeStatus": "Muuda staatust", - "active": { - "list": "Aktiivsed vestlused", - "myChats": "Minu vestlused", - "newChats": "Uued vestlused", - "chooseChat": "Alustamiseks vali vestlus", - "endChat": "Lõpeta vestlus", - "takeOver": "Võta üle", - "askAuthentication": "Küsi autentimist", - "askForContact": "Küsi kontaktandmeid", - "askPermission": "Küsi nõusolekut", - "forwardToColleague": "Suuna kolleegile", - "forwardToOrganization": "Suuna asutusele", - "startedAt": "Vestlus alustatud {{date}}", - "forwardChat": "Kellele vestlus suunata?", - "searchByName": "Otsi nime või tiitli järgi", - "onlyActiveAgents": "Näita ainult kohal olevaid nõustajaid", - "establishment": "Asutus", - "searchByEstablishmentName": "Otsi asutuse nime järgi", - "sendToEmail": "Saada e-posti", - "chooseChatStatus": "Vali vestluse staatus", - "statusChange": "Vestluse staatus", - "startService": "Alusta teenust", - "selectService": "Vali teenus", - "start": "Alusta", - "service": "Teenus", - "ContactedUser": "Kasutajaga ühendust võetud", - "couldNotReachUser": "Kasutajaga ei õnnestunud ühendust saada" - }, - "history": { - "title": "Ajalugu", - "searchChats": "Otsi üle vestluste", - "startTime": "Algusaeg", - "endTime": "Lõppaeg", - "csaName": "Nõustaja nimi", - "contact": "Kontaktandmed", - "comment": "Kommentaar", - "label": "Märksõna", - "nps": "Soovitusindeks", - "forwarded": "Suunatud", - "addACommentToTheConversation": "Lisa vestlusele kommentaar", - "rating": "Hinnang", - "feedback": "Tagasiside" - }, - "plainEvents": { - "answered": "Vastatud", - "terminated": "Määramata", - "sent_to_csa_email": "Vestlus saadetud nõustaja e-posti", - "client-left": "Klient lahkus", - "client_left_with_accepted": "Klient lahkus aktsepteeritud vastusega", - "client_left_with_no_resolution": "Klient lahkus vastuseta", - "client_left_for_unknown_reasons": "Klient lahkus määramata põhjustel", - "accepted": "Aktsepteeritud", - "hate_speech": "Vihakõne", - "other": "Muud põhjused", - "response_sent_to_client_email": "Kliendile vastati tema jäetud kontaktile", - "greeting": "Tervitus", - "requested-authentication": "Autentimine algatatud", - "authentication_successful": "Autentimine õnnestus", - "authentication_failed": "Autentimine ebaõnnestus", - "ask-permission": "Küsiti nõusolekut", - "ask-permission-accepted": "Nõusolek antud", - "ask-permission-rejected": "Nõusolekust keelduti", - "ask-permission-ignored": "Nõusolek ignoreeritud", - "rating": "Hinnang", - "contact-information": "Küsiti kontaktandmeid", - "contact-information-rejected": "Kontaktandmetest keeldutud", - "contact-information-fulfilled": "Kontaktandmed saadetud", - "requested-chat-forward": "Küsiti vestluse suunamist", - "requested-chat-forward-accepted": "Vestluse suunamine aktsepteeritud", - "requested-chat-forward-rejected": "Vestluse suunamine tagasi lükatud", - "inactive-chat-ended": "Lõpetatud tegevusetuse tõttu", - "message-read": "Loetud", - "contact-information-skipped": "Kontaktandmeid ei saadetud", - "unavailable-contact-information-fulfilled": "Kontaktandmed on antud", - "unavailable_organization": "Organisatsioon pole saadaval", - "unavailable_csas": "Nõustajad pole saadaval", - "unavailable_holiday": "Puhkus", - "user-reached": "Kasutajaga võeti ühendust", - "user-not-reached": "Kasutajaga ei õnnestunud ühendust saada" + "userManagement": { + "title": "Kasutajahaldus", + "addUserButton": " Lisa kasutaja", + "addUser": { + "addUserModalTitle": "Lisa uus kasutaja", + "editUserModalTitle": "Muuda kasutajat", + "deleteUserModalTitle": "Oled kindel", + "name": "Ees- ja perekonnanimi", + "namePlaceholder": "Sisesta nimi", + "role": "Roll", + "rolePlaceholder": "-Vali-", + "personalId": "Isikukood", + "personalIdPlaceholder": "Sisesta isikukood", + "title": "Ametinimetus", + "titlePlaceholder": "Sisesta ametinimetus", + "email": "E-post", + "emailPlaceholder": "Sisesta e-post" }, - "events": { - "answered": "Vastatud {{date}}", - "terminated": "Määramata {{date}}", - "sent_to_csa_email": "Vestlus saadetud nõustaja e-posti {{date}}", - "client-left": "Klient lahkus {{date}}", - "client_left_with_accepted": "Klient lahkus aktsepteeritud vastusega {{date}}", - "client_left_with_no_resolution": "Klient lahkus vastuseta {{date}}", - "client_left_for_unknown_reasons": "Klient lahkus määramata põhjustel {{date}}", - "accepted": "Aktsepteeritud {{date}}", - "hate_speech": "Vihakõne {{date}}", - "other": "Muud põhjused {{date}}", - "response_sent_to_client_email": "Kliendile vastati tema jäetud kontaktile {{date}}", - "greeting": "Tervitus", - "requested-authentication": "Küsiti autentimist", - "authentication_successful": "Autentimine õnnestus {{date}}", - "authentication_failed": "Autentimine ebaõnnestus {{date}}", - "ask-permission": "Küsiti nõusolekut", - "ask-permission-accepted": "Nõusolek antud {{date}}", - "ask-permission-rejected": "Nõusolekust keeldutud {{date}}", - "ask-permission-ignored": "Nõusolek ignoreeritud {{date}}", - "rating": "Hinnang {{date}}", - "contact-information": "Küsiti kontaktandmeid {{date}}", - "contact-information-rejected": "Kontaktandmetest keeldutud {{date}}", - "contact-information-fulfilled": "Kontaktandmed saadetud {{date}}", - "requested-chat-forward": "Küsiti vestluse suunamist", - "requested-chat-forward-accepted": "Vestluse suunamine aktsepteeritud {{date}}", - "requested-chat-forward-rejected": "Vestluse suunamine tagasi lükatud {{date}}", - "message-read": "Loetud", - "contact-information-skipped": "Kontaktandmeid pole esitatud", - "unavailable-contact-information-fulfilled": "Kontaktandmed on antud", - "unavailable_organization": "Organisatsioon pole saadaval", - "unavailable_csas": "Nõustajad pole saadaval", - "unavailable_holiday": "Puhkus", - "pending-assigned": "{{name}} määratud kontaktkasutajale", - "user-reached": "{{name}} võttis kasutajaga ühendust", - "user-not-reached": "{{name}} ei saanud kasutajaga ühendust", - "user-authenticated": "{{name}} on autenditud {{date}}" + "table": { + "fullName": "Täisnimi", + "personalId": "Isikukood", + "role": "Roll", + "email": "E-post", + "actions": "Toimingud" + } + }, + "integration": { + "title": "Integratsioon", + "jira": "Jira", + "outlook": "Outlook", + "pinal": "Pinal", + "outlookAndPinal": "Outlook+Pinal", + "jiraDesc": "Atlassiani probleemide jälgimise ja projektijuhtimise tarkvara", + "outlookDesc": "Microsofti poolt arendatud isikliku teabe halduri ja e-posti rakendus", + "pinalDesc": "Atlassiani probleemide jälgimise ja projektijuhtimise tarkvara", + "connected": "Ühendatud", + "disconnected": "Lahutatud", + "integrationErrorTitle": "Integratsioon ebaõnnestus", + "integrationErrorDesc": "Ühendus {{channel}}-iga ebaõnnestus. Palun kontrolli oma seadeid ja proovi uuesti. Kui probleem püsib, võta ühendust toega.", + "integrationSuccessTitle": "Integratsioon õnnestus", + "integrationSuccessDesc": "Oled edukalt ühendatud {{channel}}-iga! Sinu integratsioon on nüüd täielik ja saad alustada {{channel}}-iga töötamist.", + "confirmationModalTitle": "Oled kindel?", + "disconnectConfirmationModalDesc": "Kas oled kindel, et soovid lahutada {{channel}} integratsiooni? See toimingut ei saa tagasi võtta ja see võib mõjutada sinu töövoogu ja seotud probleeme.", + "connectConfirmationModalDesc": "Kas oled kindel, et soovid ühendada {{channel}} integratsiooni?", + "disconnectErrorTi/tle": "Lahutamine ebaõnnestus", + "disconnectErrorDesc": "Ühenduse katkestamine {{channel}}-iga ebaõnnestus. Palun kontrolli oma seadeid ja proovi uuesti. Kui probleem püsib, võta ühendust toega.", + "addUserButton": " Lisa kasutaja", + "addUser": { + "name": "Ees- ja perekonnanimi", + "namePlaceholder": "Sisesta nimi", + "role": "Roll", + "rolePlaceholder": "-Vali-", + "personalId": "Isikukood", + "personalIdPlaceholder": "Sisesta isikukood", + "title": "Ametinimetus", + "titlePlaceholder": "Sisesta ametinimetus", + "email": "E-post", + "emailPlaceholder": "Sisesta e-post" } }, "roles": { "ROLE_ADMINISTRATOR": "Administraator", - "ROLE_SERVICE_MANAGER": "Teenuse haldur", - "ROLE_CUSTOMER_SUPPORT_AGENT": "Nõustaja", - "ROLE_CHATBOT_TRAINER": "Vestlusroboti treener", - "ROLE_ANALYST": "Analüütik", - "ROLE_UNAUTHENTICATED": "Autentimata" + "ROLE_MODEL_TRAINER": "Mudelitreener" }, - "settings": { - "title": "Seaded", - "users": { - "title": "Kasutajad", - "name": "Nimi", - "idCode": "Isikukood", - "role": "Roll", - "displayName": "Kuvatav nimi", - "userTitle": "Tiitel", + "toast": { + "success": { + "updateSuccess": "Edukas uuendus", + "copied": "Kopeeritud", + "userDeleted": "Kasutaja kustutatud", + "newUserAdded": "Uus kasutaja lisatud", + "userUpdated": "Kasutaja uuendatud" + } + }, + "datasetGroups": { + "title": "Andmekogumite grupid", + "createDatasetGroupButton": "Loo andmekogumi grupp", + "table": { + "group": "Andmekogumi grupp", + "version": "Versioon", + "validationStatus": "Valideerimise staatus", + "sortBy": "Sorteeri nime järgi ({{sortOrder}})", "email": "E-post", - "addUser": "Lisa kasutaja", - "editUser": "Muuda kasutajat", - "deleteUser": "Kustuta kasutaja", - "fullName": "Ees- ja perekonnanimi", - "userRoles": "Kasutaja roll(id)", - "autoCorrector": "Autokorrektor", - "emailNotifications": "Märguanded e-postile", - "soundNotifications": "Häälmärguanded", - "popupNotifications": "Pop-up märguanded", - "newUnansweredChat": "Uus vastamata vestlus", - "newForwardedChat": "Uus suunatud vestlus", - "useAutocorrect": "Teksti autokorrektor", - "required": "Nõutud", - "invalidemail": "Kehtetu e-posti", - "invalidIdCode": "Kehtetu isikukood", - "idCodePlaceholder": "Isikukood peab algama riigi eesliitega, (eg.EE12345678910)", - "choose": "Vali", - "userExists": "Kasutaja on olemas" - }, - "chat": { - "chatActive": "Vestlusrobot aktiivne", - "showSupportName": "Kuva nõustaja nimi", - "showSupportTitle": "Kuva nõustaja tiitel" - }, - "emergencyNotices": { - "title": "Erakorralised teated", - "noticeActive": "Teade aktiivne", - "notice": "Teade", - "displayPeriod": "Kuvamisperiood", - "noticeChanged": "Teate seadeid muudeti edukalt" - }, - "appearance": { - "title": "Välimus ja käitumine", - "widgetBubbleMessageText": "Märguandesõnum", - "widgetProactiveSeconds": "Animatsiooni algus sekundites", - "widgetDisplayBubbleMessageSeconds": "Märguandesõnumi aeg sekundites", - "widgetColor": "Põhivärv", - "widgetAnimation": "Animatsioon" + "actions": "Toimingud" }, - "workingTime": { - "title": "Asutuse tööaeg", - "description": "Peale tööaja lõppemist, juhul kui juturobot ise kliendi küsimustele vastata ei oska, küsib ta kliendi kontaktandmeid.", - "openFrom": "Avatud alates", - "openUntil": "Avatud kuni", - "publicHolidays": "Arvesta riigipühadega", - "consider": "Arvesta", - "dontConsider": "Ära arvesta", - "closedOnWeekends": "Nädalavahetustel suletud", - "theSameOnAllWorkingDays": "Kõigil tööpäevadel sama", - "open": "Avatud", - "closed": "Suletud", - "until": "kuni", - "allWeekdaysExceptWeekend": "E-R", - "allWeekdays": "E-P" + "datasetCard": { + "validationFail": "Valideerimine ebaõnnestus", + "validationSuccess": "Valideerimine edukas", + "validationInprogress": "Valideerimine pooleli", + "notValidated": "Pole valideeritud", + "settings": "Seaded", + "lastModelTrained": "Viimati treenitud mudel", + "lastUsedForTraining": "Viimati kasutatud treeninguks", + "lastUpdate": "Viimati uuendatud", + "latest": "Viimane" }, - "userSession": { - "title": "Kasutaja sessioon", - "sessionLength": "Sessiooni pikkus", - "description": "Kasutaja sessiooni pikkus, mille möödumisel logitakse mitteaktiivsed kasutajad välja", - "rule": "Sessiooni pikkus on lubatud vahemikus 30 min - 480 min (8h)", - "minutes": "minutit", - "sessionChanged": "Seansi pikkuse muutmine õnnestus", - "emptySession": "Seansi pikkuse väli ei tohi olla tühi", - "invalidSession": "Seansi pikkus peab olema vahemikus 30 - 480 minutit" + "createDataset": { + "title": "Loo andmekogumi grupp", + "datasetDetails": "Andmekogumi andmed", + "datasetName": "Andmekogumi nimi", + "datasetInputPlaceholder": "Sisesta andmekogumi nimi", + "validationCriteria": "Loo valideerimiskriteeriumid", + "fieldName": "Välja nimi", + "datasetType": "Andmekogumi tüübid", + "dataClass": "Andmeklass", + "typeText": "Tekst", + "typeNumbers": "Numbrid", + "typeDateTime": "Kuupäev ja kellaaeg", + "addClassButton": "Lisa klass", + "addNowButton": "Lisa nüüd" }, - "greeting": { - "title": "Tervitussõnum" + "classHierarchy": { + "title": "Klassihierarhia", + "addClassButton":"Lisa põhiklass", + "addSubClass": "Lisa alamhierarhia", + "fieldHint": "Sisesta välja nimi", + "filedHintIfExists": "Klassi nimi on juba olemas" }, - "weekdays": { - "label": "Nädalapäevad", - "monday": "Esmaspäev", - "tuesday": "Teisipäev", - "wednesday": "Kolmapäev", - "thursday": "Neljapäev", - "friday": "Reede", - "saturday": "Laupäev", - "sunday": "Pühapäev" + "modals": { + "deleteClassTitle": "Oled kindel?", + "deleteClaassDesc": "Kinnita, et soovid kustutada järgmise kirje", + "columnInsufficientHeader": "Andmekogumis pole piisavalt veerge", + "columnInsufficientDescription": "Andmekogumil peab olema vähemalt 2 veergu. Lisaks peab olema vähemalt üks veerg määratud andmeklassiks ja üks veerg, mis ei ole andmeklass. Palun kohanda oma andmekogumit vastavalt.", + "createDatasetSuccessTitle": "Andmekogumi grupp edukalt loodud", + "createDatasetUnsuccessTitle": "Andmekogumi grupi loomine ebaõnnestus", + "createDatasetSucceessDesc": "Oled edukalt loonud andmekogumi grupi. Üksikasjalikus vaates saad nüüd andmekogumit vaadata ja redigeerida.", + "navigateDetailedViewButton": "Mine üksikasjalikku vaatesse", + "enableDatasetTitle": "Andmekogumi gruppi ei saa lubada", + "enableDatasetDesc": "Andmekogumi gruppi ei saa lubada enne andmete lisamist. Palun lisa andmekogumid sellesse gruppi ja proovi uuesti.", + "errorTitle": "Toiming ebaõnnestus", + "errorDesc": "Midagi läks valesti. Palun proovi uuesti." }, - "nationalHolidays": "Riiklikud pühad", - "welcomeMessage": { - "welcomeMessage": "Tervitussõnum", - "description": "Bürokrati automaatne tervitussõnum, mida kuvatakse esimese sõnumina vestlusakna avamisel", - "greetingActive": "Tervitus aktiivne", - "messageChanged": "Tervitust muudeti edukalt", - "emptyMessage": "Tervitussõnumi väli ei tohi olla tühi" + "detailedView": { + "connectedModels": "Ühendatud mudelid", + "noOfItems": "Üksuste arv", + "export": "Ekspordi andmed", + "import": "Impordi andmed", + "unsavedChangesWarning": "Oled teinud andmekogumisse muudatusi, mida pole salvestatud. Palun salvesta muudatused, et neid rakendada", + "insufficientExamplesDesc": "Ebapiisavad näited - andmekogumi grupi aktiveerimiseks on vaja vähemalt 10 näidet", + "noData": "Andmed puuduvad", + "noDataDesc": "Oled loonud andmekogumi grupi, kuid siin pole andmeid näha. Saad andmekogumi üles laadida, et seda siin kuvada. Kui andmed on lisatud, saad neid vajadusel redigeerida või kustutada.", + "importExamples": "Impordi näited", + "importNewData": "Impordi uusi andmeid", + "majorUpdateBanner": "Oled muutnud andmekogumi skeemi olulisi konfiguratsioone, mis pole salvestatud, palun salvesta muudatused. Kõik imporditud failid või olemasolevate andmete redigeerimised tühistatakse pärast muudatuste rakendamist", + "minorUpdateBanner": "Oled importinud andmekogumisse uusi andmeid, palun salvesta muudatused. Kõik muudatused, mida tegid üksikutele andmeüksustele, tühistatakse pärast muudatuste rakendamist", + "patchUpdateBanner": "Oled redigeerinud andmekogumi üksikuid üksusi, mida pole salvestatud. Palun salvesta muudatused, et neid rakendada", + "confirmMajorUpdatesTitle": "Kinnita suur uuendus", + "confirmMajorUpdatesDesc": "Kõik imporditud failid või olemasolevate andmete redigeerimised tühistatakse pärast muudatuste rakendamist", + "confirmMinorUpdatesTitle": "Kinnita väike uuendus", + "confirmMinorUpdatesDesc": "Kõik muudatused, mida tegid üksikutele andmeüksustele (osauuendus), tühistatakse pärast muudatuste rakendamist", + "confirmPatchUpdatesTitle": "Kinnita osauuendus", + "confirmPatchUpdatesDesc": "Muudetud andmeread uuendatakse andmekogumis", + "patchDataUnsuccessfulTitle": "Osauuenduse andmete uuendamine ebaõnnestus", + "patchDataUnsuccessfulDesc": "Midagi läks valesti. Palun proovi uuesti.", + "exportDataSuccessTitle": "Andmete eksport oli edukas", + "exportDataSuccessDesc": "Sinu andmed on edukalt eksporditud.", + "exportDataUnsucessTitle": "Andmekogumi eksport ebaõnnestus", + "exportDataUnsucessDesc": "Midagi läks valesti. Palun proovi uuesti.", + "ImportDataUnsucessTitle": "Andmekogumi import ebaõnnestus", + "importDataUnsucessDesc": "Midagi läks valesti. Palun proovi uuesti.", + "validationInitiatedTitle": "Andmekogum üles laaditud ja valideerimine alustatud", + "validationInitiatedDesc": "Andmekogumifail laaditi edukalt üles. Valideerimine ja eeltöötlus on nüüd alustatud", + "viewValidations": "Vaata valideerimisseansse", + "table": { + "id": "Rea ID", + "data": "Andmed", + "label": "Märgis", + "actions": "Toimingud" + }, + "validationsTitle": "Andmekogumi grupi valideerimised", + "classHierarchy": "Klassihierarhiad", + "delete": "Kustuta andmekogum", + "modals": { + "import": { + "title": "Impordi uusi andmeid", + "fileFormatlabel": "Vali failivorming", + "attachments": "Manused", + "maxSize": "Maksimaalne faili suurus - 10 MB", + "browse": "Sirvi faili", + "import": "Impordi", + "cancel": "Tühista", + "uploadInProgress": "Üleslaadimine pooleli...", + "uploadDesc": "Andmekogumi üleslaadimine. Palun oota, kuni üleslaadimine lõpeb. Kui tühistad poole pealt, andmed ja edenemine kaovad.", + "invalidFile": "Vigane failivorming", + "invalidFileDesc": "Üleslaaditud fail ei ole õigel {{format}} vormingus. Palun laadi üles kehtiv {{format}} fail ja proovi uuesti." + }, + "export": { + "export": "Ekspordi andmed", + "exportButton": "Ekspordi", + "fileFormatlabel": "Vali failivorming", + "title": "Andmete eksport oli edukas", + "description": "Sinu andmed on edukalt eksporditud." + }, + "delete": { + "title": "Oled kindel?", + "description": "Kui kustutad andmekogumi, muutuvad kõik sellega seotud mudelid treenimatuks. Oled kindel, et soovid jätkata?" + }, + "edit": { + "title": "Muuda", + "data": "Andmed", + "label": "Märgis", + "update": "Uuenda" + }, + "upload": { + "title": "Andmete üleslaadimine oli edukas", + "desc": "Andmekogumifail laaditi edukalt üles. Palun salvesta muudatused, et alustada andmete valideerimist ja eeltöötlust" + }, + "datasetDelete": { + "confirmationTitle": "Oled kindel?", + "confirmationDesc": "Kinnita, et soovid kustutada järgmise andmekogumi", + "successTitle": "Edu: Andmekogum kustutatud", + "successDesc": "Oled edukalt kustutanud andmekogumi. Andmekogum pole enam saadaval ja kõik seotud andmed on eemaldatud." + } + } } }, - "monitoring": { - "uptime": { - "title": "Tööaeg", - "daysAgo": "{{days}} päeva tagasi", - "uptimePercent": "{{percent}}% tööaeg" + "stopWords": { + "title": "Peatussõnad", + "import": "Impordi peatussõnad", + "stopWordInputHint": "Sisesta peatussõna", + "add": "Lisa", + "importModal": { + "title": "Impordi peatussõnad", + "selectionLabel": "Vali allpool olev valik", + "addOption": "Impordi lisamiseks", + "updateOption": "Impordi uuendamiseks", + "deleteOption": "Impordi kustutamiseks", + "attachements": "Manused (TXT, XLSX, YAML, JSON)", + "inprogressTitle": "Importimine pooleli", + "inprogressDesc": "Peatussõnade importimine on hetkel pooleli. Palun oota, kuni protsess lõpeb.", + "successTitle": "Andmete import oli edukas", + "successDesc": "Sinu andmed on edukalt imporditud." } }, - "toast": { - "success": { - "updateSuccess": "Värskendamine õnnestus", - "messageToUserEmail": "Sõnum saadeti kasutaja e-posti", - "chatStatusChanged": "Vestluse staatus muudetud", - "chatCommentChanged": "Vestluse kommentaar muudetud", - "copied": "Kopeeritud", - "userDeleted": "Kasutaja kustutatud", - "newUserAdded": "Uus kasutaja lisatud", - "userUpdated": "Kasutaja uuendatud" - } + "validationSessions": { + "title": "Valideerimise seansid", + "inprogress": "Valideerimine pooleli", + "fail": "Valideerimine ebaõnnestus, sest {{class}} klassi leiti {{column}} veerust, mis ei eksisteeri hierarhias" } } From a38b9e270b953da47e6d256c71e0a5f5aa34ab74 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 5 Aug 2024 17:47:51 +0530 Subject: [PATCH 364/582] stop words fix --- file-handler/file_handler_api.py | 84 +++++++++++++++++++------------- 1 file changed, 50 insertions(+), 34 deletions(-) diff --git a/file-handler/file_handler_api.py b/file-handler/file_handler_api.py index fc6d2cdf..2217f195 100644 --- a/file-handler/file_handler_api.py +++ b/file-handler/file_handler_api.py @@ -16,7 +16,7 @@ import pandas as pd from typing import List from io import BytesIO, TextIOWrapper - +from dataset_deleter import DatasetDeleter app = FastAPI() @@ -34,6 +34,7 @@ S3_FERRY_URL = os.getenv("S3_FERRY_URL") IMPORT_STOPWORDS_URL = os.getenv("IMPORT_STOPWORDS_URL") DELETE_STOPWORDS_URL = os.getenv("DELETE_STOPWORDS_URL") +DELETE_CONFIRMATION_URL = os.getenv("DELETE_CONFIRMATION_URL") s3_ferry = S3Ferry(S3_FERRY_URL) class ExportFile(BaseModel): @@ -191,7 +192,10 @@ async def download_and_convert(request: Request, dgId: int, background_tasks: Ba @app.get("/datasetgroup/data/download/json/location") async def download_and_convert(request: Request, saveLocation:str, background_tasks: BackgroundTasks): + print("$$$") + print(request.cookies.get("customJwtCookie")) cookie = request.cookies.get("customJwtCookie") + await authenticate_user(f'customJwtCookie={cookie}') print(saveLocation) @@ -213,6 +217,9 @@ async def download_and_convert(request: Request, saveLocation:str, background_ta @app.post("/datasetgroup/data/import/chunk") async def upload_and_copy(request: Request, import_chunks: ImportChunks): + print("%") + print(request.cookies.get("customJwtCookie")) + print("$") cookie = request.cookies.get("customJwtCookie") await authenticate_user(f'customJwtCookie={cookie}') @@ -356,20 +363,8 @@ async def import_stop_words(request: Request, stopWordsFile: UploadFile = File(. response = requests.post(url, headers=headers, json={"stopWords": words_list}) - if response.status_code == 200: - response_data = response.json() - if response_data['response']['operationSuccessful']: - return response_data - elif response_data['response']['duplicate']: - duplicate_items = response_data['response']['duplicateItems'] - new_words_list = [word for word in words_list if word not in duplicate_items] - if new_words_list: - response = requests.post(url, headers=headers, json={"stopWords": new_words_list}) - return response.json() - else: - return response_data - else: - raise HTTPException(status_code=response.status_code, detail="Failed to update stop words") + response_data = response.json() + return response_data except Exception as e: print(f"Error in import/stop-words: {e}") raise HTTPException(status_code=500, detail=str(e)) @@ -390,25 +385,46 @@ async def delete_stop_words(request: Request, stopWordsFile: UploadFile = File(. response = requests.post(url, headers=headers, json={"stopWords": words_list}) - if response.status_code == 200: - response_data = response.json() - if response_data['response']['operationSuccessful']: - return response_data - elif response_data['response']['nonexistent']: - nonexistent_items = response_data['response']['nonexistentItems'] - new_words_list = [word for word in words_list if word not in nonexistent_items] - if new_words_list: - response = requests.post(url, headers=headers, json={"stopWords": new_words_list}) - return response.json() - else: - return JSONResponse( - status_code=400, - content={ - "message": f"The following words are not in the list and cannot be deleted: {', '.join(nonexistent_items)}" - } - ) - else: - raise HTTPException(status_code=response.status_code, detail="Failed to delete stop words") + response_data = response.json() + return response_data except Exception as e: print(f"Error in delete/stop-words: {e}") raise HTTPException(status_code=500, detail=str(e)) + +@app.post("/datasetgroup/data/delete") +async def delete_dataset_files(request: Request): + try: + print("Reach : 1") + cookie = request.cookies.get("customJwtCookie") + await authenticate_user(f'customJwtCookie={cookie}') + + payload = await request.json() + dgId = int(payload["dgId"]) + + deleter = DatasetDeleter(S3_FERRY_URL) + print("Reach : 2") + success, files_deleted = deleter.delete_dataset_files(dgId, f'customJwtCookie={cookie}') + + print("Reach : 6") + print(success) + print(files_deleted) + + if success: + headers = { + 'cookie': f'customJwtCookie={cookie}', + 'Content-Type': 'application/json' + } + payload = {"dgId": dgId} + + response = requests.post(DELETE_CONFIRMATION_URL, headers=headers, json=payload) + print(f"Reach : 7 {response}") + if response.status_code != 200: + print(f"Failed to notify deletion endpoint. Status code: {response.status_code}") + + return JSONResponse(status_code=200, content={"message": "Dataset deletion completed successfully.", "files_deleted": files_deleted}) + else: + return JSONResponse(status_code=500, content={"message": "Dataset deletion failed.", "files_deleted": files_deleted}) + except Exception as e: + print(f"Error in delete_dataset_files: {e}") + raise HTTPException(status_code=500, detail=str(e)) + From feacd91d3b7f1ae5e98d2616883e3238ee7e982e Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 5 Aug 2024 17:48:55 +0530 Subject: [PATCH 365/582] dataset_deletor --- file-handler/dataset_deleter.py | 68 +++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 file-handler/dataset_deleter.py diff --git a/file-handler/dataset_deleter.py b/file-handler/dataset_deleter.py new file mode 100644 index 00000000..cd1347dd --- /dev/null +++ b/file-handler/dataset_deleter.py @@ -0,0 +1,68 @@ +import os +import json +import requests +from s3_ferry import S3Ferry + +GET_PAGE_COUNT_URL = os.getenv("GET_PAGE_COUNT_URL") +UPLOAD_DIRECTORY = os.getenv("UPLOAD_DIRECTORY", "/shared") + +class DatasetDeleter: + def __init__(self, s3_ferry_url): + self.s3_ferry = S3Ferry(s3_ferry_url) + + def get_page_count(self, dg_id, custom_jwt_cookie): + headers = { + 'cookie': custom_jwt_cookie + } + + try: + page_count_url = GET_PAGE_COUNT_URL.replace("dgId", str(dg_id)) + response = requests.get(page_count_url, headers=headers) + response.raise_for_status() + data = response.json() + print(page_count_url) + print(f"data : {data}") + + page_count = data["response"]["data"][0]["numPages"] + return page_count + except requests.exceptions.RequestException as e: + print(f"An error occurred: {e}") + return None + + def delete_dataset_files(self, dg_id, cookie): + print("Reach : 3") + page_count = self.get_page_count(dg_id, cookie) + print(f"Page count : {page_count}") + + if page_count is None: + print(f"Failed to get page count for dg_id: {dg_id}") + return False, 0 + + file_locations = [ + f"/dataset/{dg_id}/primary_dataset/dataset_{dg_id}_aggregated.json", + f"/dataset/{dg_id}/temp/temp_dataset.json" + ] + + if page_count>0: + print("Reach : 4") + for page_id in range(1, page_count + 1): + file_locations.append(f"/dataset/{dg_id}/chunks/{page_id}.json") + + print(f"Reach : 5 > {file_locations}") + empty_json_path = os.path.join(UPLOAD_DIRECTORY, "empty.json") + with open(empty_json_path, 'w') as empty_file: + json.dump({}, empty_file) + + success_count = 0 + for file_location in file_locations: + response = self.s3_ferry.transfer_file(file_location, "S3", empty_json_path, "FS") + if response.status_code == 201: + success_count += 1 + print("SUCESS : FILE DELETED") + else: + print(f"Failed to transfer file to {file_location}") + + all_files_deleted = success_count >= len(file_locations)-2 + os.remove(empty_json_path) + print(f"Dataset Deletion Final : {all_files_deleted} / {success_count}") + return all_files_deleted, success_count \ No newline at end of file From 514ee344370f435e5ad7d1bb093df4f09eed359c Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Mon, 5 Aug 2024 20:10:57 +0530 Subject: [PATCH 366/582] ESCLASS-162: Add Model progress sessions API's with openserch and notification-service endpoints --- .../DSL/data_model_progress_session.yml | 4 + ..._completed_data_model_progress_sessions.sh | 47 +++++ ...cript-v10-data-model-progress-sessions.sql | 19 ++ DSL/OpenSearch/deploy-opensearch.sh | 8 +- .../data_model_progress_sessions.json | 24 +++ .../mock/data_model_progress_sessions.json | 8 + ...completed-data-model-progress-sessions.sql | 3 + .../get-data-model-progress-sessions.sql | 12 ++ .../insert-data-model-progress-session.sql | 17 ++ .../update-data-model-progress-session.sql | 6 + .../DSL/GET/classifier/datamodel/progress.yml | 48 +++++ .../classifier/datamodel/progress/create.yml | 188 ++++++++++++++++++ .../classifier/datamodel/progress/delete.yml | 46 +++++ .../classifier/datamodel/progress/update.yml | 146 ++++++++++++++ notification-server/src/addOns.js | 27 ++- notification-server/src/config.js | 1 + notification-server/src/openSearch.js | 67 ++++++- notification-server/src/server.js | 48 ++++- 18 files changed, 703 insertions(+), 16 deletions(-) create mode 100644 DSL/CronManager/DSL/data_model_progress_session.yml create mode 100644 DSL/CronManager/script/delete_completed_data_model_progress_sessions.sh create mode 100644 DSL/Liquibase/changelog/classifier-script-v10-data-model-progress-sessions.sql create mode 100644 DSL/OpenSearch/fieldMappings/data_model_progress_sessions.json create mode 100644 DSL/OpenSearch/mock/data_model_progress_sessions.json create mode 100644 DSL/Resql/delete-completed-data-model-progress-sessions.sql create mode 100644 DSL/Resql/get-data-model-progress-sessions.sql create mode 100644 DSL/Resql/insert-data-model-progress-session.sql create mode 100644 DSL/Resql/update-data-model-progress-session.sql create mode 100644 DSL/Ruuter.private/DSL/GET/classifier/datamodel/progress.yml create mode 100644 DSL/Ruuter.private/DSL/POST/classifier/datamodel/progress/create.yml create mode 100644 DSL/Ruuter.private/DSL/POST/classifier/datamodel/progress/delete.yml create mode 100644 DSL/Ruuter.private/DSL/POST/classifier/datamodel/progress/update.yml diff --git a/DSL/CronManager/DSL/data_model_progress_session.yml b/DSL/CronManager/DSL/data_model_progress_session.yml new file mode 100644 index 00000000..4ad1adbf --- /dev/null +++ b/DSL/CronManager/DSL/data_model_progress_session.yml @@ -0,0 +1,4 @@ +delete_completed_sessions: + trigger: "0 0 0 * * ?" + type: exec + command: "../app/scripts/delete_completed_data_model_progress_sessions.sh" \ No newline at end of file diff --git a/DSL/CronManager/script/delete_completed_data_model_progress_sessions.sh b/DSL/CronManager/script/delete_completed_data_model_progress_sessions.sh new file mode 100644 index 00000000..09da9c58 --- /dev/null +++ b/DSL/CronManager/script/delete_completed_data_model_progress_sessions.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +cd "$(dirname "$0")" + +source ../config/config.ini + +script_name=$(basename $0) +pwd + +echo $(date -u +"%Y-%m-%d %H:%M:%S.%3NZ") - $script_name started + +delete_dataset_progress_sessions() { + delete_response=$(curl -s -X POST -H "Content-Type: application/json" "http://resql:8082/delete-completed-data-model-progress-sessions") + + echo "Response from delete request: $delete_response" + + session_ids=$(echo "$delete_response" | grep -oP '"id":\K\d+' | tr '\n' ' ' | sed 's/ $//') # Remove trailing space + + echo "Session IDs: $session_ids" + + if [ -n "$session_ids" ]; then + delete_from_opensearch "$session_ids" + else + echo "No session IDs were returned in the response." + fi +} + +delete_from_opensearch() { + local session_ids="$1" + + delete_query="{\"query\": {\"terms\": {\"sessionId\": [" + for id in $session_ids; do + delete_query+="\"$id\"," + done + delete_query=$(echo "$delete_query" | sed 's/,$//') # Remove trailing comma + delete_query+="]}}}" + + echo "delete query: $delete_query" + + opensearch_response=$(curl -s -X POST -H "Content-Type: application/json" -d "$delete_query" "http://opensearch-node:9200/data_model_progress_sessions/_delete_by_query") + + echo "Response from OpenSearch delete request: $opensearch_response" +} + +delete_dataset_progress_sessions + +echo $(date -u +"%Y-%m-%d %H:%M:%S.%3NZ") - $script_name finished diff --git a/DSL/Liquibase/changelog/classifier-script-v10-data-model-progress-sessions.sql b/DSL/Liquibase/changelog/classifier-script-v10-data-model-progress-sessions.sql new file mode 100644 index 00000000..35d663be --- /dev/null +++ b/DSL/Liquibase/changelog/classifier-script-v10-data-model-progress-sessions.sql @@ -0,0 +1,19 @@ +-- liquibase formatted sql + +-- changeset kalsara Magamage:classifier-script-v10-changeset1 +CREATE TYPE Training_Progress_Status AS ENUM ('Initiating Training', 'Training In-Progress', 'Deploying Model', 'Model Trained And Deployed'); + +-- changeset kalsara Magamage:classifier-script-v10-changeset2 +CREATE TABLE model_progress_sessions ( + id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY, + model_id BIGINT NOT NULL, + model_name TEXT NOT NULL, + major_version INT NOT NULL DEFAULT 0, + minor_version INT NOT NULL DEFAULT 0, + latest BOOLEAN DEFAULT false, + process_complete BOOLEAN DEFAULT false, + progress_percentage INT, + training_progress_status Training_Progress_Status, + training_message TEXT , + CONSTRAINT model_progress_sessions_pkey PRIMARY KEY (id) +); \ No newline at end of file diff --git a/DSL/OpenSearch/deploy-opensearch.sh b/DSL/OpenSearch/deploy-opensearch.sh index aebf6959..06b3bb0b 100644 --- a/DSL/OpenSearch/deploy-opensearch.sh +++ b/DSL/OpenSearch/deploy-opensearch.sh @@ -12,4 +12,10 @@ fi # ddataset_progress_sessions curl -XDELETE "$URL/dataset_progress_sessions?ignore_unavailable=true" -u "$AUTH" --insecure curl -H "Content-Type: application/x-ndjson" -X PUT "$URL/dataset_progress_sessions" -ku "$AUTH" --data-binary "@fieldMappings/dataset_progress_sessions.json" -if $MOCK_ALLOWED; then curl -H "Content-Type: application/x-ndjson" -X PUT "$URL/dataset_progress_sessions/_bulk" -ku "$AUTH" --data-binary "@mock/dataset_progress_sessions.json"; fi \ No newline at end of file +if $MOCK_ALLOWED; then curl -H "Content-Type: application/x-ndjson" -X PUT "$URL/dataset_progress_sessions/_bulk" -ku "$AUTH" --data-binary "@mock/dataset_progress_sessions.json"; fi + + +# data_model_progress_sessions +curl -XDELETE "$URL/data_model_progress_sessions?ignore_unavailable=true" -u "$AUTH" --insecure +curl -H "Content-Type: application/x-ndjson" -X PUT "$URL/data_model_progress_sessions" -ku "$AUTH" --data-binary "@fieldMappings/data_model_progress_sessions.json" +if $MOCK_ALLOWED; then curl -H "Content-Type: application/x-ndjson" -X PUT "$URL/data_model_progress_sessions/_bulk" -ku "$AUTH" --data-binary "@mock/data_model_progress_sessions.json"; fi \ No newline at end of file diff --git a/DSL/OpenSearch/fieldMappings/data_model_progress_sessions.json b/DSL/OpenSearch/fieldMappings/data_model_progress_sessions.json new file mode 100644 index 00000000..b21f7890 --- /dev/null +++ b/DSL/OpenSearch/fieldMappings/data_model_progress_sessions.json @@ -0,0 +1,24 @@ +{ + "mappings": { + "properties": { + "sessionId": { + "type": "keyword" + }, + "trainingStatus": { + "type": "keyword" + }, + "trainingMessage": { + "type": "keyword" + }, + "progressPercentage": { + "type": "integer" + }, + "timestamp": { + "type": "keyword" + }, + "sentTo": { + "type": "keyword" + } + } + } +} diff --git a/DSL/OpenSearch/mock/data_model_progress_sessions.json b/DSL/OpenSearch/mock/data_model_progress_sessions.json new file mode 100644 index 00000000..b60ca8e1 --- /dev/null +++ b/DSL/OpenSearch/mock/data_model_progress_sessions.json @@ -0,0 +1,8 @@ +{"index":{"_id":"1"}} +{"sessionId": "101","trainingStatus": "Initiating Training","trainingMessage": "","progressPercentage": 1,"timestamp": "1801371325497", "sentTo": []} +{"index":{"_id":"2"}} +{"sessionId": "102","trainingStatus": "Training In-Progress","trainingMessage": "","progressPercentage": 26,"timestamp": "1801371325597", "sentTo": []} +{"index":{"_id":"3"}} +{"sessionId": "103","trainingStatus": "Deploying Model","trainingMessage": "","progressPercentage": 52,"timestamp": "1801371325697", "sentTo": []} +{"index":{"_id":"4"}} +{"sessionId": "104","trainingStatus": "Model Trained And Deployed","trainingMessage": "","progressPercentage": 97,"timestamp": "1801371325797", "sentTo": []} diff --git a/DSL/Resql/delete-completed-data-model-progress-sessions.sql b/DSL/Resql/delete-completed-data-model-progress-sessions.sql new file mode 100644 index 00000000..f675ab0e --- /dev/null +++ b/DSL/Resql/delete-completed-data-model-progress-sessions.sql @@ -0,0 +1,3 @@ +DELETE FROM model_progress_sessions +WHERE process_complete = true +RETURNING id; diff --git a/DSL/Resql/get-data-model-progress-sessions.sql b/DSL/Resql/get-data-model-progress-sessions.sql new file mode 100644 index 00000000..76e8ff9b --- /dev/null +++ b/DSL/Resql/get-data-model-progress-sessions.sql @@ -0,0 +1,12 @@ +SELECT + id, + model_id, + model_name, + major_version, + minor_version, + latest, + process_complete, + progress_percentage, + training_progress_status as training_status, + training_message +FROM model_progress_sessions; diff --git a/DSL/Resql/insert-data-model-progress-session.sql b/DSL/Resql/insert-data-model-progress-session.sql new file mode 100644 index 00000000..d038409f --- /dev/null +++ b/DSL/Resql/insert-data-model-progress-session.sql @@ -0,0 +1,17 @@ +INSERT INTO "model_progress_sessions" ( + model_id, + model_name, + major_version, + minor_version, + latest, + progress_percentage, + training_progress_status +) VALUES ( + :model_id, + :model_name, + :major_version, + :minor_version, + :latest, + :progress_percentage, + :training_progress_status::Training_Progress_Status +)RETURNING id; \ No newline at end of file diff --git a/DSL/Resql/update-data-model-progress-session.sql b/DSL/Resql/update-data-model-progress-session.sql new file mode 100644 index 00000000..c2c213b3 --- /dev/null +++ b/DSL/Resql/update-data-model-progress-session.sql @@ -0,0 +1,6 @@ +UPDATE model_progress_sessions +SET + training_progress_status = :training_progress_status::Training_Progress_Status, + training_message = :training_message, + progress_percentage = :progress_percentage +WHERE id = :id; \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/GET/classifier/datamodel/progress.yml b/DSL/Ruuter.private/DSL/GET/classifier/datamodel/progress.yml new file mode 100644 index 00000000..728ed06b --- /dev/null +++ b/DSL/Ruuter.private/DSL/GET/classifier/datamodel/progress.yml @@ -0,0 +1,48 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'PROGRESS'" + method: get + accepts: json + returns: json + namespace: classifier + +get_data_mdoel_progress_sessions: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-data-model-progress-sessions" + result: res_sessions + next: check_status + +check_status: + switch: + - condition: ${200 <= res_sessions.response.statusCodeValue && res_sessions.response.statusCodeValue < 300} + next: assign_success_response + next: assign_fail_response + +assign_success_response: + assign: + format_res: { + operationSuccessful: true, + data: '${res_sessions.response.body}' + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + operationSuccessful: false, + data: '${[]}' + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end + diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/progress/create.yml b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/progress/create.yml new file mode 100644 index 00000000..175f30ec --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/progress/create.yml @@ -0,0 +1,188 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'CREATE'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: modelId + type: number + description: "Body field 'modelId'" + - field: modelName + type: string + description: "Body field 'modelName'" + - field: majorVersion + type: number + description: "Body field 'majorVersion'" + - field: minorVersion + type: number + description: "Body field 'minorVersion'" + - field: latest + type: boolean + description: "Body field 'latest'" + headers: + - field: cookie + type: string + description: "Cookie field" + +extract_request_data: + assign: + model_id: ${incoming.body.modelId} + model_name: ${incoming.body.modelName} + major_version: ${incoming.body.majorVersion} + minor_version: ${incoming.body.minorVersion} + latest: ${incoming.body.latest} + next: check_for_request_data + +check_for_request_data: + switch: + - condition: ${model_id !== null || major_version !=null || minor_version !==null} + next: get_data_model_by_id + next: return_incorrect_request + +get_data_model_by_id: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-data-model-by-id" + body: + id: ${model_id} + result: res_model + next: check_data_model_status + +check_data_model_status: + switch: + - condition: ${200 <= res_model.response.statusCodeValue && res_model.response.statusCodeValue < 300} + next: check_data_model_exist + next: assign_fail_response + +check_data_model_exist: + switch: + - condition: ${res_model.response.body.length>0} + next: get_random_string_cookie + next: return_model_not_found + +get_random_string_cookie: + call: http.post + args: + url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/return_random_string" + headers: + type: json + result: res_cookie + next: check_random_string_status + +check_random_string_status: + switch: + - condition: ${200 <= res_cookie.response.statusCodeValue && res_cookie.response.statusCodeValue < 300} + next: assign_csrf_cookie + next: assign_fail_response + +assign_csrf_cookie: + assign: + csrf_cookie: ${'_csrf=' + res_cookie.response.body.randomHexString+';'} + next: create_data_model_progress_session + +create_data_model_progress_session: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/insert-data-model-progress-session" + body: + model_id: ${model_id} + model_name: ${model_name} + major_version: ${major_version} + minor_version: ${minor_version} + latest: ${latest} + progress_percentage: 0 + training_progress_status: 'Initiating Training' + result: res + next: check_status + +check_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: assign_session_id + next: assign_fail_response + +assign_session_id: + assign: + session_id: ${res.response.body[0].id} + next: get_csrf_token + +get_csrf_token: + call: http.get + args: + url: "[#CLASSIFIER_NOTIFICATIONS]/csrf-token" + headers: + cookie: ${csrf_cookie} + result: res_token + next: check_token_status + +check_token_status: + switch: + - condition: ${200 <= res_token.response.statusCodeValue && res_token.response.statusCodeValue < 300} + next: assign_csrf_token + next: assign_fail_response + +assign_csrf_token: + assign: + token: ${res_token.response.body.csrfToken} + next: update_progress + +update_progress: + call: http.post + args: + url: "[#CLASSIFIER_NOTIFICATIONS]/model/progress" + headers: + X-CSRF-Token: ${token} + cookie: ${csrf_cookie} + body: + sessionId: ${session_id} + progressPercentage: 0 + trainingStatus: 'Initiating Training' + trainingMessage: '' + result: res_node + next: check_node_server_status + +check_node_server_status: + switch: + - condition: ${200 <= res_node.response.statusCodeValue && res_node.response.statusCodeValue < 300} + next: assign_success_response + next: assign_fail_response + +assign_success_response: + assign: + format_res: { + sessionId: '${session_id}', + operationSuccessful: true, + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + sessionId: '', + operationSuccessful: false, + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end + +return_incorrect_request: + status: 400 + return: 'Missing Required Fields' + next: end + +return_model_not_found: + status: 404 + return: "Model Not Found" + next: end diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/progress/delete.yml b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/progress/delete.yml new file mode 100644 index 00000000..36876174 --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/progress/delete.yml @@ -0,0 +1,46 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'DELETE'" + method: post + accepts: json + returns: json + namespace: classifier + +delete_dataset_progress_sessions: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/delete-completed-data-model-progress-sessions" + result: res_sessions + next: check_status + +check_status: + switch: + - condition: ${200 <= res_sessions.response.statusCodeValue && res_sessions.response.statusCodeValue < 300} + next: assign_success_response + next: assign_fail_response + +assign_success_response: + assign: + format_res: { + operationSuccessful: true, + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + operationSuccessful: false, + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end + diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/progress/update.yml b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/progress/update.yml new file mode 100644 index 00000000..f3d5b47d --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/progress/update.yml @@ -0,0 +1,146 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'UPDATE'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: sessionId + type: number + description: "Body field 'sessionId'" + - field: trainingStatus + type: string + description: "Body field 'trainingStatus'" + - field: trainingMessage + type: string + description: "Body field 'trainingMessage'" + - field: progressPercentage + type: number + description: "Body field 'progressPercentage'" + +extract_request_data: + assign: + session_id: ${incoming.body.sessionId} + training_status: ${incoming.body.trainingStatus} + training_message: ${incoming.body.trainingMessage} + progress_percentage: ${incoming.body.progressPercentage} + next: check_for_request_data + +check_for_request_data: + switch: + - condition: ${session_id !== null || !training_status || progress_percentage !==null} + next: get_random_string_cookie + next: return_incorrect_request + +get_random_string_cookie: + call: http.post + args: + url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/return_random_string" + headers: + type: json + result: res_cookie + next: check_random_string_status + +check_random_string_status: + switch: + - condition: ${200 <= res_cookie.response.statusCodeValue && res_cookie.response.statusCodeValue < 300} + next: assign_csrf_cookie + next: assign_fail_response + +assign_csrf_cookie: + assign: + csrf_cookie: ${'_csrf=' + res_cookie.response.body.randomHexString+';'} + next: update_data_model_progress_session + +update_data_model_progress_session: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/update-data-model-progress-session" + body: + id: ${session_id} + training_progress_status: ${training_status} + progress_percentage: ${progress_percentage} + training_message: ${training_message} + result: res + next: check_status + +check_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: get_csrf_token + next: assign_fail_response + +get_csrf_token: + call: http.get + args: + url: "[#CLASSIFIER_NOTIFICATIONS]/csrf-token" + headers: + cookie: ${csrf_cookie} + result: res_token + next: check_token_status + +check_token_status: + switch: + - condition: ${200 <= res_token.response.statusCodeValue && res_token.response.statusCodeValue < 300} + next: assign_csrf_token + next: assign_fail_response + +assign_csrf_token: + assign: + token: ${res_token.response.body.csrfToken} + next: update_progress + +update_progress: + call: http.post + args: + url: "[#CLASSIFIER_NOTIFICATIONS]/model/progress" + headers: + X-CSRF-Token: ${token} + cookie: ${csrf_cookie} + body: + sessionId: ${session_id} + progressPercentage: ${progress_percentage} + trainingStatus: ${training_status} + trainingMessage: ${training_message} + result: res_node + next: check_node_server_status + +check_node_server_status: + switch: + - condition: ${200 <= res_node.response.statusCodeValue && res_node.response.statusCodeValue < 300} + next: assign_success_response + next: assign_fail_response + +assign_success_response: + assign: + format_res: { + sessionId: '${session_id}', + operationSuccessful: true, + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + sessionId: '', + operationSuccessful: false, + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end + +return_incorrect_request: + status: 400 + return: 'Missing Required Fields' + next: end diff --git a/notification-server/src/addOns.js b/notification-server/src/addOns.js index aa3a531c..42c212b2 100644 --- a/notification-server/src/addOns.js +++ b/notification-server/src/addOns.js @@ -1,14 +1,32 @@ -const { searchNotification } = require("./openSearch"); +const { searchDatasetGroupNotification, searchModelNotification } = require("./openSearch"); const { serverConfig } = require("./config"); -function buildNotificationSearchInterval({ +function buildDatasetGroupNotificationSearchInterval({ sessionId, interval = serverConfig.refreshInterval, }) { return ({ connectionId, sender }) => { const intervalHandle = setInterval( () => - searchNotification({ + searchDatasetGroupNotification({ + connectionId, + sessionId, + sender, + }), + interval + ); + return () => clearInterval(intervalHandle); + }; +} + +function buildModelNotificationSearchInterval({ + sessionId, + interval = serverConfig.refreshInterval, +}) { + return ({ connectionId, sender }) => { + const intervalHandle = setInterval( + () => + searchModelNotification({ connectionId, sessionId, sender, @@ -20,5 +38,6 @@ function buildNotificationSearchInterval({ } module.exports = { - buildNotificationSearchInterval, + buildDatasetGroupNotificationSearchInterval, + buildModelNotificationSearchInterval, }; diff --git a/notification-server/src/config.js b/notification-server/src/config.js index 04ce118f..340a14c0 100644 --- a/notification-server/src/config.js +++ b/notification-server/src/config.js @@ -3,6 +3,7 @@ require("dotenv").config(); module.exports = { openSearchConfig: { datasetGroupProgress: "dataset_progress_sessions", + dataModelProgress: "data_model_progress_sessions", ssl: { rejectUnauthorized: false, }, diff --git a/notification-server/src/openSearch.js b/notification-server/src/openSearch.js index 9c7bf4d6..6e649b80 100644 --- a/notification-server/src/openSearch.js +++ b/notification-server/src/openSearch.js @@ -6,7 +6,7 @@ const client = new Client({ ssl: openSearchConfig.ssl, }); -async function searchNotification({ sessionId, connectionId, sender }) { +async function searchDatasetGroupNotification({ sessionId, connectionId, sender }) { try { const response = await client.search({ index: openSearchConfig.datasetGroupProgress, @@ -27,7 +27,39 @@ async function searchNotification({ sessionId, connectionId, sender }) { sessionId: hit._source.sessionId, progressPercentage: hit._source.progressPercentage, validationStatus: hit._source.validationStatus, - validationMessage: hit._source.validationMessage + validationMessage: hit._source.validationMessage, + }; + await sender(sessionJson); + await markAsSent(hit, connectionId); + } + } catch (e) { + console.error(e); + await sender({}); + } +} + +async function searchModelNotification({ sessionId, connectionId, sender }) { + try { + const response = await client.search({ + index: openSearchConfig.dataModelProgress, + body: { + query: { + bool: { + must: { match: { sessionId } }, + must_not: { match: { sentTo: connectionId } }, + }, + }, + sort: { timestamp: { order: "asc" } }, + }, + }); + + for (const hit of response.body.hits.hits) { + console.log(`hit: ${JSON.stringify(hit)}`); + const sessionJson = { + sessionId: hit._source.sessionId, + progressPercentage: hit._source.progressPercentage, + trainingStatus: hit._source.trainingStatus, + trainingMessage: hit._source.trainingMessage, }; await sender(sessionJson); await markAsSent(hit, connectionId); @@ -57,7 +89,12 @@ async function markAsSent({ _index, _id }, connectionId) { }); } -async function updateProgress(sessionId, progressPercentage, validationStatus, validationMessage) { +async function updateDatasetGroupProgress( + sessionId, + progressPercentage, + validationStatus, + validationMessage +) { await client.index({ index: "dataset_progress_sessions", body: { @@ -70,7 +107,27 @@ async function updateProgress(sessionId, progressPercentage, validationStatus, v }); } +async function updateModelProgress( + sessionId, + progressPercentage, + trainingStatus, + trainingMessage +) { + await client.index({ + index: "data_model_progress_sessions", + body: { + sessionId, + trainingStatus, + progressPercentage, + trainingMessage, + timestamp: new Date(), + }, + }); +} + module.exports = { - searchNotification, - updateProgress, + searchDatasetGroupNotification, + searchModelNotification, + updateDatasetGroupProgress, + updateModelProgress, }; diff --git a/notification-server/src/server.js b/notification-server/src/server.js index 196c6e9d..953b0d3a 100644 --- a/notification-server/src/server.js +++ b/notification-server/src/server.js @@ -2,8 +2,11 @@ const express = require("express"); const cors = require("cors"); const { buildSSEResponse } = require("./sseUtil"); const { serverConfig } = require("./config"); -const { buildNotificationSearchInterval } = require("./addOns"); -const { updateProgress } = require("./openSearch"); +const { + buildDatasetGroupNotificationSearchInterval, + buildModelNotificationSearchInterval, +} = require("./addOns"); +const { updateDatasetGroupProgress, updateModelProgress } = require("./openSearch"); const helmet = require("helmet"); const cookieParser = require("cookie-parser"); const csurf = require("csurf"); @@ -16,19 +19,29 @@ app.use(express.json({ extended: false })); app.use(cookieParser()); app.use(csurf({ cookie: true })); -app.get("/sse/notifications/:sessionId", (req, res) => { +app.get("/sse/dataset/notifications/:sessionId", (req, res) => { const { sessionId } = req.params; console.log(`session id: ${sessionId}`); buildSSEResponse({ req, res, - buildCallbackFunction: buildNotificationSearchInterval({ sessionId }), + buildCallbackFunction: buildDatasetGroupNotificationSearchInterval({ sessionId }), + }); +}); + +app.get("/sse/model/notifications/:sessionId", (req, res) => { + const { sessionId } = req.params; + console.log(`session id: ${sessionId}`); + buildSSEResponse({ + req, + res, + buildCallbackFunction: buildModelNotificationSearchInterval({ sessionId }), }); }); app.get("/csrf-token", (req, res) => { console.log(`Cookies: ${JSON.stringify(req.cookies)}`); - res.json({ csrfToken: req.csrfToken(), }); + res.json({ csrfToken: req.csrfToken() }); }); // Endpoint to update the dataset_progress_sessions index @@ -41,7 +54,7 @@ app.post("/dataset/progress", async (req, res) => { } try { - await updateProgress( + await updateDatasetGroupProgress( sessionId, progressPercentage, validationStatus, @@ -54,6 +67,29 @@ app.post("/dataset/progress", async (req, res) => { } }); +// Endpoint to update the dataset_progress_sessions index +app.post("/model/progress", async (req, res) => { + const { sessionId, progressPercentage, trainingStatus, trainingMessage } = + req.body; + + if (!sessionId || progressPercentage === undefined || !trainingStatus) { + return res.status(400).json({ error: "Missing required fields" }); + } + + try { + await updateModelProgress( + sessionId, + progressPercentage, + trainingStatus, + trainingMessage + ); + res.status(201).json({ message: "Document created successfully" }); + } catch (error) { + console.error("Error creating document:", error); + res.status(500).json({ error: "Failed to create document" }); + } +}); + const server = app.listen(serverConfig.port, () => { console.log(`Server running on port ${serverConfig.port}`); }); From 7c4e0823cd0d18556bc0a9af699542647a9cd740 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 5 Aug 2024 20:31:24 +0530 Subject: [PATCH 367/582] XLSX files getting corrrupted bug fix --- file-handler/file_converter.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/file-handler/file_converter.py b/file-handler/file_converter.py index d231fa71..8be8ea6b 100644 --- a/file-handler/file_converter.py +++ b/file-handler/file_converter.py @@ -74,10 +74,22 @@ def _convert_xlsx_to_json(self, filePath): def convert_json_to_xlsx(self, jsonData, outputPath): try: - with pd.ExcelWriter(outputPath) as writer: - for sheetName, data in jsonData.items(): - df = pd.DataFrame(data) - df.to_excel(writer, sheet_name=sheetName, index=False) + if isinstance(jsonData, list): + df = pd.DataFrame(jsonData) + with pd.ExcelWriter(outputPath) as writer: + df.to_excel(writer, sheet_name='Sheet1', index=False) + elif isinstance(jsonData, dict): + with pd.ExcelWriter(outputPath) as writer: + for sheetName, data in jsonData.items(): + if isinstance(data, list): + df = pd.DataFrame(data) + df.to_excel(writer, sheet_name=sheetName, index=False) + else: + print(f"Error: Expected list of dictionaries for sheet '{sheetName}', but got {type(data)}") + return False + else: + print(f"Error: Unsupported JSON data format '{type(jsonData)}'") + return False print(f"JSON data successfully converted to XLSX and saved at '{outputPath}'") return True except Exception as e: From 9f853fc08c6e1d6c38d74a7b79e1ab00929c5970 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 5 Aug 2024 20:57:49 +0530 Subject: [PATCH 368/582] docker file bug fix --- file-handler/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/file-handler/Dockerfile b/file-handler/Dockerfile index 11a36499..1a4d3d2a 100644 --- a/file-handler/Dockerfile +++ b/file-handler/Dockerfile @@ -8,6 +8,7 @@ COPY file_handler_api.py . COPY file_converter.py . COPY constants.py . COPY s3_ferry.py . +COPY dataset_deleter.py . RUN mkdir -p /shared && chown appuser:appuser /shared && chmod 770 /shared RUN chown -R appuser:appuser /app From 91bc2ea3e993702dad69897c6eb2d44e6261d121 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Mon, 5 Aug 2024 21:56:45 +0530 Subject: [PATCH 369/582] ESCLASS-162: Data Model delete API's implemented --- DSL/Resql/delete-data-model-by-id.sql | 2 + .../delete-dataset-group-connected-models.sql | 7 ++ .../get-data-model-dataset-group-by-id.sql | 2 + .../DSL/POST/classifier/datamodel/delete.yml | 86 ++++++++++++++ .../classifier/datamodel/metadata/delete.yml | 112 ++++++++++++++++++ .../datasetgroup/metadata/delete.yml | 2 +- 6 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 DSL/Resql/delete-data-model-by-id.sql create mode 100644 DSL/Resql/delete-dataset-group-connected-models.sql create mode 100644 DSL/Resql/get-data-model-dataset-group-by-id.sql create mode 100644 DSL/Ruuter.private/DSL/POST/classifier/datamodel/delete.yml create mode 100644 DSL/Ruuter.private/DSL/POST/classifier/datamodel/metadata/delete.yml diff --git a/DSL/Resql/delete-data-model-by-id.sql b/DSL/Resql/delete-data-model-by-id.sql new file mode 100644 index 00000000..3ce4e24e --- /dev/null +++ b/DSL/Resql/delete-data-model-by-id.sql @@ -0,0 +1,2 @@ +DELETE FROM models_metadata +WHERE id =:id; \ No newline at end of file diff --git a/DSL/Resql/delete-dataset-group-connected-models.sql b/DSL/Resql/delete-dataset-group-connected-models.sql new file mode 100644 index 00000000..e7d5f1d2 --- /dev/null +++ b/DSL/Resql/delete-dataset-group-connected-models.sql @@ -0,0 +1,7 @@ +UPDATE dataset_group_metadata +SET connected_models = ( + SELECT jsonb_agg(elem) + FROM jsonb_array_elements(connected_models) elem + WHERE (elem->>'modelId')::int <> :model_id +) +WHERE id = :id; \ No newline at end of file diff --git a/DSL/Resql/get-data-model-dataset-group-by-id.sql b/DSL/Resql/get-data-model-dataset-group-by-id.sql new file mode 100644 index 00000000..4a77b6a3 --- /dev/null +++ b/DSL/Resql/get-data-model-dataset-group-by-id.sql @@ -0,0 +1,2 @@ +SELECT id, connected_dg_id +FROM models_metadata WHERE id =:id; \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/delete.yml b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/delete.yml new file mode 100644 index 00000000..26f40005 --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/delete.yml @@ -0,0 +1,86 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'DELETE'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: modelId + type: number + description: "Body field 'modelId'" + +extract_request_data: + assign: + model_id: ${incoming.body.modelId} + next: check_for_request_data + +check_for_request_data: + switch: + - condition: ${model_id !== null} + next: get_data_model_by_id + next: return_incorrect_request + +get_data_model_by_id: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-data-model-by-id" + body: + id: ${model_id} + result: res_model + next: check_data_model_status + +check_data_model_status: + switch: + - condition: ${200 <= res_model.response.statusCodeValue && res_model.response.statusCodeValue < 300} + next: check_data_model_exist + next: assign_fail_response + +check_data_model_exist: + switch: + - condition: ${res_model.response.body.length>0} + next: execute_cron_manager + next: assign_fail_response + +execute_cron_manager: + call: reflect.mock + args: + url: "[#CLASSIFIER_CRON_MANAGER]/execute/dataset_processing/dataset_deletion" + query: + cookie: ${incoming.headers.cookie} + modelId: ${model_id} + result: res + next: assign_success_response + +assign_success_response: + assign: + format_res: { + modelId: '${model_id}', + operationSuccessful: true, + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + modelId: '', + operationSuccessful: false, + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_incorrect_request: + status: 400 + return: 'Missing Required Fields' + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/metadata/delete.yml b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/metadata/delete.yml new file mode 100644 index 00000000..b19d3775 --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/metadata/delete.yml @@ -0,0 +1,112 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'DELETE'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: modelId + type: number + description: "Body field 'modelId'" + +extract_request_data: + assign: + model_id: ${incoming.body.modelId} + next: check_for_request_data + +check_for_request_data: + switch: + - condition: ${model_id !== null} + next: get_data_model_by_id + next: return_incorrect_request + +get_data_model_by_id: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-data-model-dataset-group-by-id" + body: + id: ${model_id} + result: res_model + next: check_data_model_status + +check_data_model_status: + switch: + - condition: ${200 <= res_model.response.statusCodeValue && res_model.response.statusCodeValue < 300} + next: check_data_model_exist + next: assign_fail_response + +check_data_model_exist: + switch: + - condition: ${res_model.response.body.length>0} + next: assign_dataset_group_id + next: assign_fail_response + +assign_dataset_group_id: + assign: + dg_id: ${res_model.response.body[0].connectedDgId} + next: delete_data_model_by_id + +delete_data_model_by_id: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/delete-data-model-by-id" + body: + id: ${model_id} + result: res_delete + next: check_dataset_delete_status + +check_dataset_delete_status: + switch: + - condition: ${200 <= res_delete.response.statusCodeValue && res_delete.response.statusCodeValue < 300} + next: delete_dataset_group_connected_models + next: assign_fail_response + +delete_dataset_group_connected_models: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/delete-dataset-group-connected-models" + body: + id: ${dg_id} + model_id: ${model_id} + result: res_dataset_update + next: check_dataset_group_connected_models_status + +check_dataset_group_connected_models_status: + switch: + - condition: ${200 <= res_dataset_update.response.statusCodeValue && res_dataset_update.response.statusCodeValue < 300} + next: assign_success_response + next: assign_fail_response + +assign_success_response: + assign: + format_res: { + modelId: '${model_id}', + operationSuccessful: true, + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + modelId: '', + operationSuccessful: false, + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_incorrect_request: + status: 400 + return: 'Missing Required Fields' + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/metadata/delete.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/metadata/delete.yml index 05714123..8122dd3a 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/metadata/delete.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/metadata/delete.yml @@ -15,7 +15,7 @@ declaration: extract_request_data: assign: dg_id: ${incoming.body.dgId} - next: check_for_request_data + next: check_for_request_data check_for_request_data: switch: From 382ace89fb1e8ed3c66c956236945273685ec0a3 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Tue, 6 Aug 2024 10:15:31 +0530 Subject: [PATCH 370/582] data models integration --- .../FormElements/FormCheckboxes/index.tsx | 2 +- .../FormElements/FormRadios/index.tsx | 2 +- .../FormSelect/FormMultiselect.tsx | 2 +- .../FormElements/FormSelect/index.tsx | 197 +++++++++++------- .../molecules/DataModelCard/index.tsx | 7 +- .../molecules/DataModelForm/index.tsx | 53 +++-- .../ViewDatasetGroupModalController.tsx | 2 + GUI/src/enums/dataModelsEnums.ts | 30 ++- .../pages/DataModels/ConfigureDataModel.tsx | 128 ++++++++++-- GUI/src/pages/DataModels/CreateDataModel.tsx | 64 +++--- GUI/src/pages/DataModels/index.tsx | 8 +- GUI/src/pages/StopWords/index.tsx | 1 + GUI/src/services/data-models.ts | 26 +-- GUI/src/types/dataModels.ts | 10 + GUI/src/utils/commonUtilts.ts | 8 - GUI/src/utils/dataModelsUtils.ts | 25 ++- 16 files changed, 377 insertions(+), 188 deletions(-) create mode 100644 GUI/src/types/dataModels.ts diff --git a/GUI/src/components/FormElements/FormCheckboxes/index.tsx b/GUI/src/components/FormElements/FormCheckboxes/index.tsx index ab3df8ef..50829eaf 100644 --- a/GUI/src/components/FormElements/FormCheckboxes/index.tsx +++ b/GUI/src/components/FormElements/FormCheckboxes/index.tsx @@ -53,7 +53,7 @@ const FormCheckboxes: FC = ({ )}
    - {items.map((item, index) => ( + {items?.map((item, index) => (
    = ({ )}
    - {items.map((item, index) => ( + {items?.map((item, index) => (
    = ( {label && !hideLabel && }
    - {selectedItems.length > 0 ? `${t('global.chosen')} (${selectedItems.length})` : placeholderValue} + {selectedItems?.length > 0 ? `${t('global.chosen')} (${selectedItems?.length})` : placeholderValue} } />
    diff --git a/GUI/src/components/FormElements/FormSelect/index.tsx b/GUI/src/components/FormElements/FormSelect/index.tsx index 09fbd4f8..027393bc 100644 --- a/GUI/src/components/FormElements/FormSelect/index.tsx +++ b/GUI/src/components/FormElements/FormSelect/index.tsx @@ -1,4 +1,10 @@ -import { forwardRef, ReactNode, SelectHTMLAttributes, useId, useState } from 'react'; +import { + forwardRef, + ReactNode, + SelectHTMLAttributes, + useId, + useState, +} from 'react'; import { useSelect } from 'downshift'; import clsx from 'clsx'; import { useTranslation } from 'react-i18next'; @@ -8,92 +14,127 @@ import { Icon } from 'components'; import './FormSelect.scss'; import { ControllerRenderProps } from 'react-hook-form'; -type FormSelectProps = Partial & SelectHTMLAttributes & { - label: ReactNode; - name: string; - placeholder?:string; - hideLabel?: boolean; - direction?: 'down' | 'up'; - options: { - label: string; - value: string; - }[]; - onSelectionChange?: (selection: { label: string, value: string } | null) => void; - error?:string -} +type FormSelectProps = Partial & + SelectHTMLAttributes & { + label: ReactNode; + name: string; + placeholder?: string; + hideLabel?: boolean; + direction?: 'down' | 'up'; + options: + | { + label: string; + value: string; + }[] + | { + label: string; + value: { name: string; id: string }; + }[]; + onSelectionChange?: ( + selection: { label: string; value: string } |{ + label: string; + value: { name: string; id: string }; + }| null + ) => void; + error?: string; + }; -const itemToString = (item: ({ label: string, value: string } | null)) => { +const itemToString = (item: { label: string; value: string } | null) => { return item ? item.value : ''; }; -const FormSelect= forwardRef(( - { - label, - hideLabel, - direction = 'down', - options, - disabled, - placeholder, - defaultValue, - onSelectionChange, - error, - ...rest - }, - ref -) => { - const id = useId(); - const { t } = useTranslation(); - const defaultSelected = options.find((o) => o.value === defaultValue) || null; - const [selectedItem, setSelectedItem] = useState<{ label: string, value: string } | null>(defaultSelected); - const { - isOpen, - getToggleButtonProps, - getLabelProps, - getMenuProps, - highlightedIndex, - getItemProps, - } = useSelect({ - id, - items: options, - itemToString, - selectedItem, - onSelectedItemChange: ({ selectedItem: newSelectedItem }) => { - setSelectedItem(newSelectedItem ?? null); - if (onSelectionChange) onSelectionChange(newSelectedItem ?? null); +const FormSelect = forwardRef( + ( + { + label, + hideLabel, + direction = 'down', + options, + disabled, + placeholder, + defaultValue, + onSelectionChange, + error, + ...rest }, - }); + ref + ) => { + const id = useId(); + const { t } = useTranslation(); + const defaultSelected = + options?.find((o) => o.value === defaultValue) || options?.find((o) => o.value?.name === defaultValue) ||null; + const [selectedItem, setSelectedItem] = useState<{ + label: string; + value: string; + } | null>(defaultSelected); + const { + isOpen, + getToggleButtonProps, + getLabelProps, + getMenuProps, + highlightedIndex, + getItemProps, + } = useSelect({ + id, + items: options, + itemToString, + selectedItem, + onSelectedItemChange: ({ selectedItem: newSelectedItem }) => { + setSelectedItem(newSelectedItem ?? null); + if (onSelectionChange) onSelectionChange(newSelectedItem ?? null); + }, + }); - const selectClasses = clsx( - 'select', - disabled && 'select--disabled' ); + const selectClasses = clsx('select', disabled && 'select--disabled'); + const placeholderValue = placeholder || t('global.choose'); - const placeholderValue = placeholder || t('global.choose'); - - return ( -
    - {label && !hideLabel && } -
    -
    - {selectedItem?.label ?? placeholderValue} - } /> + return ( +
    + {label && !hideLabel && ( + + )} +
    +
    + {selectedItem?.label ?? placeholderValue} + } + /> +
    +
      + {isOpen && + options.map((item, index) => ( +
    • + {item.label} +
    • + ))} +
    + {error &&

    {error}

    }
    -
      - {isOpen && ( - options.map((item, index) => ( -
    • - {item.label} -
    • - )) - )} -
    - {error &&

    {error}

    } -
    -
    - ); -}); - + ); + } +); export default FormSelect; diff --git a/GUI/src/components/molecules/DataModelCard/index.tsx b/GUI/src/components/molecules/DataModelCard/index.tsx index fc5d0fa9..b82fa2f0 100644 --- a/GUI/src/components/molecules/DataModelCard/index.tsx +++ b/GUI/src/components/molecules/DataModelCard/index.tsx @@ -50,6 +50,8 @@ const DataModelCard: FC> = ({ return ; } else if (status === TrainingStatus.UNTRAINABLE) { return ; + }else if (status === TrainingStatus.NOT_TRAINED) { + return ; } }; @@ -116,7 +118,7 @@ const DataModelCard: FC> = ({
    } > -
    + {results ?(
    {results?.classes?.map((c) => { return
    {c}
    ; @@ -132,7 +134,8 @@ const DataModelCard: FC> = ({ return
    {c}
    ; })}
    -
    +
    ):(
    No training results available
    )} +
    ), diff --git a/GUI/src/components/molecules/DataModelForm/index.tsx b/GUI/src/components/molecules/DataModelForm/index.tsx index 65f6e5fe..8826e4d5 100644 --- a/GUI/src/components/molecules/DataModelForm/index.tsx +++ b/GUI/src/components/molecules/DataModelForm/index.tsx @@ -1,9 +1,16 @@ import { FC } from 'react'; import { useTranslation } from 'react-i18next'; -import { FormCheckboxes, FormInput, FormRadios, FormSelect, Label } from 'components'; -import { customFormattedArray, formattedArray } from 'utils/commonUtilts'; +import { + FormCheckboxes, + FormInput, + FormRadios, + FormSelect, + Label, +} from 'components'; +import { formattedArray } from 'utils/commonUtilts'; import { useQuery } from '@tanstack/react-query'; import { getCreateOptions } from 'services/data-models'; +import { customFormattedArray } from 'utils/dataModelsUtils'; type DataModelFormType = { dataModel: any; @@ -11,13 +18,19 @@ type DataModelFormType = { errors: Record; }; -const DataModelForm: FC = ({ dataModel, handleChange, errors }) => { +const DataModelForm: FC = ({ + dataModel, + handleChange, + errors, +}) => { const { t } = useTranslation(); - + const { data: createOptions } = useQuery(['datamodels/create-options'], () => getCreateOptions() ); + console.log(createOptions); + return (
    @@ -31,41 +44,49 @@ const DataModelForm: FC = ({ dataModel, handleChange, errors />
    - Model Version + Model Version
    - + {createOptions && (
    Select Dataset Group
    handleChange('dgName', selection.value)} + onSelectionChange={(selection) => { + handleChange('dgName', selection?.value?.name); + handleChange('dgId', selection?.value?.id); + }} error={errors?.dgName} - value={dataModel?.dgName} + defaultValue={ dataModel?.dgName} />
    - +
    Select Base Models
    handleChange('baseModels', values.baseModels)} + onValuesChange={(values) => + handleChange('baseModels', values.baseModels) + } error={errors?.baseModels} selectedValues={dataModel?.baseModels} />
    - +
    Select Deployment Platform
    handleChange('platform', value)} @@ -73,11 +94,11 @@ const DataModelForm: FC = ({ dataModel, handleChange, errors selectedValue={dataModel?.platform} />
    - +
    Select Maturity Label
    handleChange('maturity', value)} diff --git a/GUI/src/components/molecules/ViewDatasetGroupModalController/ViewDatasetGroupModalController.tsx b/GUI/src/components/molecules/ViewDatasetGroupModalController/ViewDatasetGroupModalController.tsx index a14a0743..a6ecd179 100644 --- a/GUI/src/components/molecules/ViewDatasetGroupModalController/ViewDatasetGroupModalController.tsx +++ b/GUI/src/components/molecules/ViewDatasetGroupModalController/ViewDatasetGroupModalController.tsx @@ -101,6 +101,7 @@ const ViewDatasetGroupModalController = ({ name="format" items={formats} onChange={setImportFormat} + selectedValue={importFormat} >

    {t('datasetGroups.detailedView.modals.import.attachments')}

    @@ -164,6 +165,7 @@ const ViewDatasetGroupModalController = ({ name="format" items={formats} onChange={setExportFormat} + selectedValue={exportFormat} >
    diff --git a/GUI/src/enums/dataModelsEnums.ts b/GUI/src/enums/dataModelsEnums.ts index c805ea02..a463983e 100644 --- a/GUI/src/enums/dataModelsEnums.ts +++ b/GUI/src/enums/dataModelsEnums.ts @@ -1,13 +1,21 @@ export enum TrainingStatus { - TRAINING_INPROGRESS = 'training in-progress', - TRAINED = 'trained', - RETRAINING_NEEDED = 'retraining needed', - UNTRAINABLE = 'untrainable', - } + NOT_TRAINED = 'not trained', + TRAINING_INPROGRESS = 'training in-progress', + TRAINED = 'trained', + RETRAINING_NEEDED = 'retraining needed', + UNTRAINABLE = 'untrainable', +} - export enum Maturity { - PRODUCTION = 'production', - STAGING = 'staging', - DEVELOPMENT = 'development', - TESTING = 'testing', - } \ No newline at end of file +export enum Maturity { + PRODUCTION = 'production ready', + STAGING = 'staging', + DEVELOPMENT = 'development', +} + +export enum Platform { + JIRA = 'jira', + OUTLOOK = 'outlook', + PINAL = 'pinal', + UNDEPLOYED = 'undeployed', + +} \ No newline at end of file diff --git a/GUI/src/pages/DataModels/ConfigureDataModel.tsx b/GUI/src/pages/DataModels/ConfigureDataModel.tsx index b75dd97b..46731da6 100644 --- a/GUI/src/pages/DataModels/ConfigureDataModel.tsx +++ b/GUI/src/pages/DataModels/ConfigureDataModel.tsx @@ -6,34 +6,57 @@ import { useDialog } from 'hooks/useDialog'; import BackArrowButton from 'assets/BackArrowButton'; import { getMetadata } from 'services/data-models'; import DataModelForm from 'components/molecules/DataModelForm'; -import { validateDataModel } from 'utils/dataModelsUtils'; +import { getChangedAttributes, validateDataModel } from 'utils/dataModelsUtils'; +import { Platform } from 'enums/dataModelsEnums'; +import { ButtonAppearanceTypes } from 'enums/commonEnums'; type ConfigureDataModelType = { id: number; }; const ConfigureDataModel: FC = ({ id }) => { - const { open } = useDialog(); + const { open, close } = useDialog(); const navigate = useNavigate(); + const [enabled, setEnabled] = useState(true); + const [initialData, setInitialData] = useState({}); const [dataModel, setDataModel] = useState({ + modelId: '', modelName: '', dgName: '', + dgId: '', platform: '', baseModels: [], maturity: '', + version: '', }); - const { data: dataModelData } = useQuery(['datamodels/metadata', id], () => - getMetadata(id), + const { data: dataModelData } = useQuery( + ['datamodels/metadata', id], + () => getMetadata(id), + { + enabled, onSuccess: (data) => { setDataModel({ + modelId: data?.modelId || '', modelName: data?.modelName || '', dgName: data?.connectedDgName || '', + dgId: data?.modelId || '', platform: data?.deploymentEnv || '', baseModels: data?.baseModels || [], maturity: data?.maturityLabel || '', + version: `V${data?.majorVersion}.${data?.minorVersion}`, }); + setInitialData({ + modelName: data?.modelName || '', + dgName: data?.connectedDgName || '', + dgId: data?.modelId || '', + platform: data?.deploymentEnv || '', + baseModels: data?.baseModels || [], + maturity: data?.maturityLabel || '', + version: `V${data?.majorVersion}.${data?.minorVersion}`, + }); + setEnabled(false); }, } ); @@ -56,18 +79,66 @@ const ConfigureDataModel: FC = ({ id }) => { const validateData = () => { const validationErrors = validateDataModel(dataModel); setErrors(validationErrors); - return Object.keys(validationErrors).length === 0; + return Object.keys(validationErrors)?.length === 0; }; const handleSave = () => { - console.log(dataModel); + const payload = getChangedAttributes(initialData, dataModel); + if (validateData()) { - // Trigger the mutation or save action + if ( + dataModel.dgId !== initialData.dgId || + dataModel.dgName !== initialData.dgName + ) { + } + } + }; + + const handleDelete = () => { + if ( + dataModel.platform === Platform.JIRA || + dataModel.platform === Platform.OUTLOOK || + dataModel.platform === Platform.PINAL + ) { + open({ + title: 'Cannot Delete Model', + content: ( +

    + The model cannot be deleted because it is currently in production. + Please escalate another model to production before proceeding to + delete this model. +

    + ), + footer: ( +
    + + +
    + ), + }); } else { open({ - title: 'Validation Error', - content:

    Please correct the errors and try again.

    , + title: 'Are you sure?', + content: ( +

    Confirm that you are wish to delete the following data model

    + ), + footer: ( +
    + + +
    + ), }); } }; @@ -91,20 +162,15 @@ const ConfigureDataModel: FC = ({ id }) => { }} >
    -
    - No Data Available -

    - You have created the dataset group, but there are no datasets - available to show here. You can upload a dataset to view it in - this space. Once added, you can edit or delete the data as - needed. + Model updated. Please initiate retraining to continue benefiting + from the latest improvements.

    - +
    - + = ({ id }) => { background: 'white', }} > - - + + +
    + ), + }) + } + > + Retrain +
    ); diff --git a/GUI/src/pages/DataModels/CreateDataModel.tsx b/GUI/src/pages/DataModels/CreateDataModel.tsx index 022253e6..761ded6b 100644 --- a/GUI/src/pages/DataModels/CreateDataModel.tsx +++ b/GUI/src/pages/DataModels/CreateDataModel.tsx @@ -1,47 +1,34 @@ -import { FC, useCallback, useState } from 'react'; +import { FC, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { - Button, - FormCheckbox, - FormCheckboxes, - FormInput, - FormRadios, - FormSelect, - Label, -} from 'components'; -import { DatasetGroup } from 'types/datasetGroups'; +import { Button } from 'components'; import { Link, useNavigate } from 'react-router-dom'; import './DataModels.scss'; -import { useMutation, useQuery } from '@tanstack/react-query'; -import { createDatasetGroup } from 'services/datasets'; +import { useMutation } from '@tanstack/react-query'; import { useDialog } from 'hooks/useDialog'; import BackArrowButton from 'assets/BackArrowButton'; -import { getCreateOptions } from 'services/data-models'; -import { customFormattedArray, formattedArray } from 'utils/commonUtilts'; import { validateDataModel } from 'utils/dataModelsUtils'; import DataModelForm from 'components/molecules/DataModelForm'; +import { ButtonAppearanceTypes } from 'enums/commonEnums'; +import { createDataModel } from 'services/data-models'; const CreateDataModel: FC = () => { const { t } = useTranslation(); - const { open } = useDialog(); + const { open,close } = useDialog(); const [isModalOpen, setIsModalOpen] = useState(false); const [modalType, setModalType] = useState(''); const navigate = useNavigate(); - const { data: createOptions } = useQuery(['datamodels/create-options'], () => - getCreateOptions() - ); const [dataModel, setDataModel] = useState({ modelName: '', dgName: '', + dgId: '', platform: '', baseModels: [], maturity: '', + version: 'V1.0', }); const handleDataModelAttributesChange = (name: string, value: string) => { - - setDataModel((prevFilters) => ({ ...prevFilters, [name]: value, @@ -57,21 +44,32 @@ const CreateDataModel: FC = () => { }); const validateData = () => { - console.log(dataModel); - setErrors(validateDataModel(dataModel)); + const payload={ + modelName: dataModel.modelName, + datasetGroupName: dataModel.dgName, + dgId: dataModel.dgId, + baseModels: dataModel.baseModels, + deploymentPlatform: dataModel.platform, + maturityLabel:dataModel.maturity + } + + createDataModelMutation.mutate(payload); }; - const createDatasetGroupMutation = useMutation({ - mutationFn: (data: DatasetGroup) => createDatasetGroup(data), + const createDataModelMutation = useMutation({ + mutationFn: (data) => createDataModel(data), onSuccess: async (response) => { - setIsModalOpen(true); - setModalType('SUCCESS'); + open({ + title: 'Data Model Created and Trained', + content:

    You have successfully created and trained the data model. You can view it on the data model dashboard.

    , + footer:
    + }); }, onError: () => { open({ - title: 'Dataset Group Creation Unsuccessful', - content:

    Something went wrong. Please try again.

    , + title: 'Error Creating Data Model', + content:

    There was an issue creating or training the data model. Please try again. If the problem persists, contact support for assistance.

    , }); }, }); @@ -81,13 +79,17 @@ const CreateDataModel: FC = () => {
    - navigate(0)}> +
    Create Data Model
    - +
    { label="" name="maturity" placeholder="Maturity" - options={formattedArray(filterData?.maturity) ?? []} + options={formattedArray(filterData?.maturityLabels) ?? []} onSelectionChange={(selection) => handleFilterChange('maturity', selection?.value ?? '') } @@ -177,7 +177,7 @@ const DataModels: FC = () => { return ( { dgVersion={dataset?.dgVersion} lastTrained={dataset?.lastTrained} trainingStatus={dataset.trainingStatus} - platform={dataset?.deploymentPlatform} - maturity={dataset?.deploymentMaturity} + platform={dataset?.deploymentEnv} + maturity={dataset?.maturityLabel} results={dataset?.trainingResults} setId={setId} setView={setView} diff --git a/GUI/src/pages/StopWords/index.tsx b/GUI/src/pages/StopWords/index.tsx index 7477cdbb..dc2b3d50 100644 --- a/GUI/src/pages/StopWords/index.tsx +++ b/GUI/src/pages/StopWords/index.tsx @@ -120,6 +120,7 @@ const StopWords: FC = () => { label="" onChange={setImportOption} items={importOptions} + selectedValue={importOption} />

    Attachments (TXT, XLSX, YAML, JSON)

    diff --git a/GUI/src/services/data-models.ts b/GUI/src/services/data-models.ts index d4e1aa09..3e8ff795 100644 --- a/GUI/src/services/data-models.ts +++ b/GUI/src/services/data-models.ts @@ -6,7 +6,7 @@ import { DatasetGroup, Operation } from 'types/datasetGroups'; export async function getDataModelsOverview( pageNum: number, - modelGroup: string, + modelName: string, majorVersion: number, minorVersion: number, patchVersion: number, @@ -17,10 +17,10 @@ export async function getDataModelsOverview( sort: string, ) { - const { data } = await apiMock.get('classifier/datamodel/overview', { + const { data } = await apiDev.get('classifier/datamodel/overview', { params: { page: pageNum, - modelGroup, + modelName, majorVersion, minorVersion, patchVersion, @@ -32,32 +32,32 @@ export async function getDataModelsOverview( pageSize:5 }, }); - return data; + return data?.response; } export async function getFilterData() { - const { data } = await apiMock.get('classifier/datamodel/overview/filters'); - return data; + const { data } = await apiDev.get('classifier/datamodel/overview/filters'); + return data?.response; } export async function getCreateOptions() { - const { data } = await apiMock.get('classifier/datamodel/create/options'); - return data; + const { data } = await apiDev.get('classifier/datamodel/create/options'); + return data?.response; } export async function getMetadata(modelId: string | number | null) { - const { data } = await apiMock.get('classifier/datamodel/metadata', { + const { data } = await apiDev.get('classifier/datamodel/metadata', { params: { modelId }, }); - return data; + return data?.response?.data[0]; } -export async function createDatasetGroup(datasetGroup: DatasetGroup) { +export async function createDataModel(dataModel) { - const { data } = await apiDev.post('classifier/datasetgroup/create', { - ...datasetGroup, + const { data } = await apiDev.post('classifier/datamodel/create', { + ...dataModel, }); return data; } diff --git a/GUI/src/types/dataModels.ts b/GUI/src/types/dataModels.ts new file mode 100644 index 00000000..54500f01 --- /dev/null +++ b/GUI/src/types/dataModels.ts @@ -0,0 +1,10 @@ +export type DataModel = { + modelId: string; + modelName: string; + dgName: string; + dgId: string; + platform: string; + baseModels: string[]; + maturity: string; + version: string; + }; \ No newline at end of file diff --git a/GUI/src/utils/commonUtilts.ts b/GUI/src/utils/commonUtilts.ts index fc710a33..7ee6dcaf 100644 --- a/GUI/src/utils/commonUtilts.ts +++ b/GUI/src/utils/commonUtilts.ts @@ -9,14 +9,6 @@ export const formattedArray = (data: string[]) => { })); }; -export const customFormattedArray = >(data: T[], attributeName: keyof T) => { - return data?.map((item) => ({ - label: item[attributeName], - value: item[attributeName], - })); -}; - - export const convertTimestampToDateTime = (timestamp: number) => { return moment.unix(timestamp).format('YYYY-MM-DD HH:mm:ss'); }; diff --git a/GUI/src/utils/dataModelsUtils.ts b/GUI/src/utils/dataModelsUtils.ts index 6419bfe9..fb76b340 100644 --- a/GUI/src/utils/dataModelsUtils.ts +++ b/GUI/src/utils/dataModelsUtils.ts @@ -1,3 +1,5 @@ +import { DataModel } from "types/dataModels"; + export const validateDataModel = (dataModel) => { const { modelName, dgName, platform, baseModels, maturity } = dataModel; const newErrors: any = {}; @@ -5,9 +7,30 @@ export const validateDataModel = (dataModel) => { if (!modelName.trim()) newErrors.modelName = 'Model Name is required'; if (!dgName.trim()) newErrors.dgName = 'Dataset Group Name is required'; if (!platform.trim()) newErrors.platform = 'Platform is required'; - if (baseModels.length === 0) + if (baseModels?.length === 0) newErrors.baseModels = 'At least one Base Model is required'; if (!maturity.trim()) newErrors.maturity = 'Maturity is required'; return newErrors; }; + +export const customFormattedArray = >(data: T[], attributeName: keyof T) => { + return data?.map((item) => ({ + label: item[attributeName], + value: {name:item[attributeName],id:item.dgId}, + })); +}; + +export const getChangedAttributes = (original: DataModel, updated: DataModel): Partial> => { + const changes: Partial> = {}; + + (Object.keys(original) as (keyof DataModel)[]).forEach((key) => { + if (original[key] !== updated[key]) { + changes[key] = updated[key]; + } else { + changes[key] = null; + } + }); + + return changes; +}; \ No newline at end of file From 5cd0b037e41bc99ca3e72c6cf57525cd38944a32 Mon Sep 17 00:00:00 2001 From: Pamoda Dilranga <51948729+pamodaDilranga@users.noreply.github.com> Date: Tue, 6 Aug 2024 10:45:52 +0530 Subject: [PATCH 371/582] Disabling the CICD pipeline since it is interfering with QA work --- .github/workflows/est-workflow-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/est-workflow-dev.yml b/.github/workflows/est-workflow-dev.yml index 958f4fea..4a069613 100644 --- a/.github/workflows/est-workflow-dev.yml +++ b/.github/workflows/est-workflow-dev.yml @@ -3,7 +3,7 @@ name: Deploy EST Frontend and Backend to development on: push: branches: - - dev + - main jobs: deploy: From 15590e6eedb9e5a18e15eff0afc84ca8ab48c014 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Tue, 6 Aug 2024 10:59:16 +0530 Subject: [PATCH 372/582] stop words bulk import and delete --- .../CreateDatasetGroupModal.tsx | 2 +- .../DatasetDetailedViewTable.tsx | 14 +- .../TableSkeleton/SkeletonTable.scss | 31 ++++ .../molecules/TableSkeleton/TableSkeleton.tsx | 24 +++ GUI/src/config/importOptionsConfig.json | 1 - GUI/src/enums/datasetEnums.ts | 5 + .../pages/DatasetGroups/DatasetGroups.scss | 6 + GUI/src/pages/StopWords/index.tsx | 158 +++++++++++++---- GUI/src/pages/UserManagement/index.tsx | 53 +++--- GUI/src/services/datasets.ts | 34 +++- GUI/src/utils/endpoints.ts | 15 +- GUI/src/utils/queryKeys.ts | 4 + GUI/translations/en/common.json | 9 +- GUI/translations/et/common.json | 160 +++++++++--------- 14 files changed, 368 insertions(+), 148 deletions(-) create mode 100644 GUI/src/components/molecules/TableSkeleton/SkeletonTable.scss create mode 100644 GUI/src/components/molecules/TableSkeleton/TableSkeleton.tsx diff --git a/GUI/src/components/molecules/CreateDatasetGroupModals/CreateDatasetGroupModal.tsx b/GUI/src/components/molecules/CreateDatasetGroupModals/CreateDatasetGroupModal.tsx index bb63eaa7..01915ffe 100644 --- a/GUI/src/components/molecules/CreateDatasetGroupModals/CreateDatasetGroupModal.tsx +++ b/GUI/src/components/molecules/CreateDatasetGroupModals/CreateDatasetGroupModal.tsx @@ -68,7 +68,7 @@ const CreateDatasetGroupModalController = ({ {modalType === CreateDatasetGroupModals.SUCCESS && (
    )}
    + {isLoading && } {!isLoading && updatedDataset && ( = ({ rowCount }) => { + const skeletonRows = Array.from({ length: rowCount }, (_, index) => ( + + +
    + + + )); + + return ( + + {skeletonRows} +
    + ); +}; + +export default SkeletonTable; \ No newline at end of file diff --git a/GUI/src/config/importOptionsConfig.json b/GUI/src/config/importOptionsConfig.json index 280cd15b..d7111cb0 100644 --- a/GUI/src/config/importOptionsConfig.json +++ b/GUI/src/config/importOptionsConfig.json @@ -1,6 +1,5 @@ [ { "label": "Import to add", "value": "add" }, - { "label": "Import to update", "value": "update" }, { "label": "Import to delete", "value": "delete" } ] \ No newline at end of file diff --git a/GUI/src/enums/datasetEnums.ts b/GUI/src/enums/datasetEnums.ts index 8efea54e..9b5cf334 100644 --- a/GUI/src/enums/datasetEnums.ts +++ b/GUI/src/enums/datasetEnums.ts @@ -36,3 +36,8 @@ export enum ImportExportDataTypes { JSON = 'json', YAML = 'yaml', } + +export enum StopWordImportOptions { + ADD = 'add', + DELETE = 'delete', +} \ No newline at end of file diff --git a/GUI/src/pages/DatasetGroups/DatasetGroups.scss b/GUI/src/pages/DatasetGroups/DatasetGroups.scss index ffa9196b..5b32f1df 100644 --- a/GUI/src/pages/DatasetGroups/DatasetGroups.scss +++ b/GUI/src/pages/DatasetGroups/DatasetGroups.scss @@ -46,4 +46,10 @@ justify-content: flex-end; gap: 10px; margin-top: 25px; +} + +.skeleton-container { + background-color: #fff; + padding: 20px; + margin-top: 20px; } \ No newline at end of file diff --git a/GUI/src/pages/StopWords/index.tsx b/GUI/src/pages/StopWords/index.tsx index 7477cdbb..ea931268 100644 --- a/GUI/src/pages/StopWords/index.tsx +++ b/GUI/src/pages/StopWords/index.tsx @@ -1,21 +1,33 @@ -import { FC, useState } from 'react'; +import { FC, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Button, Card, Dialog, FormInput, FormRadios } from 'components'; import LabelChip from 'components/LabelChip'; -import FileUpload from 'components/FileUpload'; +import FileUpload, { FileUploadHandle } from 'components/FileUpload'; import { useForm } from 'react-hook-form'; import importOptions from '../../config/importOptionsConfig.json'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { addStopWord, deleteStopWord, getStopWords } from 'services/datasets'; +import { + addStopWord, + deleteStopWord, + deleteStopWords, + getStopWords, + importStopWords, +} from 'services/datasets'; +import { stopWordsQueryKeys } from 'utils/queryKeys'; +import { ButtonAppearanceTypes } from 'enums/commonEnums'; +import { StopWordImportOptions } from 'enums/datasetEnums'; +import { useDialog } from 'hooks/useDialog'; +import { AxiosError } from 'axios'; const StopWords: FC = () => { const { t } = useTranslation(); const queryClient = useQueryClient(); + const { open } = useDialog(); const [isModalOpen, setIsModalOpen] = useState(false); const [importOption, setImportOption] = useState(''); - const [file, setFile] = useState(''); - + const [file, setFile] = useState(); + const fileUploadRef = useRef(null); const [addedStopWord, setAddedStopWord] = useState(''); const { register, setValue, watch } = useForm({ @@ -24,8 +36,9 @@ const StopWords: FC = () => { }, }); - const { data: stopWordsData } = useQuery(['datasetgroups/stopwords'], () => - getStopWords() + const { data: stopWordsData, refetch: stopWordRefetch } = useQuery( + stopWordsQueryKeys.GET_ALL_STOP_WORDS(), + () => getStopWords() ); const watchedStopWord = watch('stopWord'); @@ -34,36 +47,115 @@ const StopWords: FC = () => { deleteStopWordMutation.mutate({ stopWords: [wordToRemove] }); }; - const addStopWordMutation = useMutation({ - mutationFn: (data) => addStopWord(data), + mutationFn: (data: { stopWords: string[] }) => addStopWord(data), onSuccess: async (res) => { - await queryClient.invalidateQueries(['datasetgroups/stopwords']); - + await queryClient.invalidateQueries( + stopWordsQueryKeys.GET_ALL_STOP_WORDS() + ); }, onError: () => {}, }); const deleteStopWordMutation = useMutation({ - mutationFn: (data) => deleteStopWord(data), + mutationFn: (data: { stopWords: string[] }) => deleteStopWord(data), onSuccess: async (res) => { - await queryClient.invalidateQueries(['datasetgroups/stopwords']); - + await queryClient.invalidateQueries( + stopWordsQueryKeys.GET_ALL_STOP_WORDS() + ); }, onError: () => {}, }); + const importMutationSuccessFunc = async () => { + setIsModalOpen(false); + open({ + title: t('stopWords.importModal.successTitle') ?? '', + content:

    {t('stopWords.importModal.successDesc') ?? ''}

    , + }); + setFile(null); + await queryClient.invalidateQueries( + stopWordsQueryKeys.GET_ALL_STOP_WORDS() + ); + + if (importOption === StopWordImportOptions.DELETE) { + stopWordRefetch(); + } + }; + + const importStopWordsMutation = useMutation({ + mutationFn: (file: File) => importStopWords(file), + onSuccess: async () => { + setIsModalOpen(false); + importMutationSuccessFunc(); + }, + onError: async (error: AxiosError) => { + setIsModalOpen(true); + open({ + title: t('stopWords.importModal.unsuccessTitle') ?? '', + content:

    {t('stopWords.importModal.unsuccessDesc') ?? ''}

    , + }); + }, + }); + + const deleteStopWordsMutation = useMutation({ + mutationFn: (file: File) => deleteStopWords(file), + onSuccess: async () => { + setIsModalOpen(false); + importMutationSuccessFunc(); + }, + onError: async (error: AxiosError) => { + setIsModalOpen(true); + open({ + title: t('stopWords.importModal.unsuccessTitle') ?? '', + content:

    {t('stopWords.importModal.unsuccessDesc') ?? ''}

    , + }); + }, + }); + + const handleFileSelect = (file: File | null) => { + if (file) setFile(file); + }; + + const handleStopWordFileOperations = () => { + if ( + importOption === StopWordImportOptions.ADD && + file && + file !== undefined + ) + importStopWordsMutation.mutate(file); + else if ( + importOption === StopWordImportOptions.DELETE && + file && + file !== undefined + ) + deleteStopWordsMutation.mutate(file); + }; + + useEffect(() => { + if ( + importStopWordsMutation.isLoading && + !importStopWordsMutation.isSuccess + ) { + setIsModalOpen(false); + open({ + title: t('stopWords.importModal.inprogressTitle') ?? '', + content:

    {t('stopWords.importModal.inprogressDesc') ?? ''}

    , + }); + } + }, [importStopWordsMutation]); + return (
    -
    Stop Words
    +
    {t('stopWords.title') ?? ''}
    - {stopWordsData?.map((word) => ( + {stopWordsData?.map((word: string) => ( { value={watchedStopWord} name="stopWord" label="" - placeholder="Enter stop word" - + placeholder={t('stopWords.stopWordInputHint') ?? ''} />
    @@ -96,24 +188,31 @@ const StopWords: FC = () => { setIsModalOpen(false); setImportOption(''); }} - title={'Import stop words'} + title={t('stopWords.importModal.title') ?? ''} footer={
    + -
    } >
    -

    Select the option below

    +

    {t('stopWords.importModal.selectionLabel') ?? ''}

    { items={importOptions} />
    -

    Attachments (TXT, XLSX, YAML, JSON)

    +

    {t('stopWords.importModal.attachements') ?? ''}

    - setFile(selectedFile?.name ?? '') - } + onFileSelect={handleFileSelect} />
    @@ -137,4 +235,4 @@ const StopWords: FC = () => { ); }; -export default StopWords; +export default StopWords; \ No newline at end of file diff --git a/GUI/src/pages/UserManagement/index.tsx b/GUI/src/pages/UserManagement/index.tsx index ffff66ab..3bd4f3fc 100644 --- a/GUI/src/pages/UserManagement/index.tsx +++ b/GUI/src/pages/UserManagement/index.tsx @@ -21,6 +21,7 @@ import { userManagementQueryKeys } from 'utils/queryKeys'; import { userManagementEndpoints } from 'utils/endpoints'; import { ButtonAppearanceTypes, ToastTypes } from 'enums/commonEnums'; import { useDialog } from 'hooks/useDialog'; +import SkeletonTable from 'components/molecules/TableSkeleton/TableSkeleton'; const UserManagement: FC = () => { const columnHelper = createColumnHelper(); @@ -189,29 +190,33 @@ const UserManagement: FC = () => {
    - { - if ( - state?.pageIndex === pagination?.pageIndex && - state?.pageSize === pagination?.pageSize - ) - return; - setPagination(state); - fetchUsers(state, sorting); - }} - sorting={sorting} - setSorting={(state: SortingState) => { - setSorting(state); - fetchUsers(pagination, state); - }} - pagesCount={totalPages} - isClientSide={false} - /> + {!isLoading && ( + { + if ( + state?.pageIndex === pagination?.pageIndex && + state?.pageSize === pagination?.pageSize + ) + return; + setPagination(state); + fetchUsers(state, sorting); + }} + sorting={sorting} + setSorting={(state: SortingState) => { + setSorting(state); + fetchUsers(pagination, state); + }} + pagesCount={totalPages} + isClientSide={false} + /> + )} + + {isLoading && } {newUserModal && ( { ); }; -export default UserManagement; +export default UserManagement; \ No newline at end of file diff --git a/GUI/src/services/datasets.ts b/GUI/src/services/datasets.ts index f1248064..8f68efde 100644 --- a/GUI/src/services/datasets.ts +++ b/GUI/src/services/datasets.ts @@ -2,7 +2,14 @@ import { datasetsEndpoints } from 'utils/endpoints'; import apiDev from './api-dev'; import apiExternal from './api-external'; import { PaginationState } from '@tanstack/react-table'; -import { DatasetDetails, DatasetGroup, MetaData, MinorPayLoad, Operation, PatchPayLoad } from 'types/datasetGroups'; +import { + DatasetDetails, + DatasetGroup, + MetaData, + MinorPayLoad, + Operation, + PatchPayLoad, +} from 'types/datasetGroups'; export async function getDatasetsOverview( pageNum: number, @@ -74,6 +81,7 @@ export async function createDatasetGroup(datasetGroup: DatasetGroup) { } export async function importDataset(file: File, id: string | number) { + console.log('fileeee ', file); const { data } = await apiExternal.post(datasetsEndpoints.IMPORT_DATASETS(), { dataFile: file, dgId: id, @@ -131,14 +139,14 @@ export async function getStopWords() { return data?.response?.stopWords; } -export async function addStopWord(stopWordData) { +export async function addStopWord(stopWordData: { stopWords: string[] }) { const { data } = await apiDev.post(datasetsEndpoints.POST_STOP_WORDS(), { ...stopWordData, }); return data; } -export async function deleteStopWord(stopWordData) { +export async function deleteStopWord(stopWordData: { stopWords: string[] }) { const { data } = await apiDev.post(datasetsEndpoints.DELETE_STOP_WORD(), { ...stopWordData, }); @@ -149,3 +157,23 @@ export async function getDatasetGroupsProgress() { const { data } = await apiDev.get('classifier/datasetgroup/progress'); return data?.response?.data; } + +export async function importStopWords(file: File) { + const { data } = await apiExternal.post( + datasetsEndpoints.IMPORT_STOP_WORDS(), + { + stopWordsFile: file, + } + ); + return data; +} + +export async function deleteStopWords(file: File) { + const { data } = await apiExternal.post( + datasetsEndpoints.DELETE_STOP_WORDS(), + { + stopWordsFile: file, + } + ); + return data; +} \ No newline at end of file diff --git a/GUI/src/utils/endpoints.ts b/GUI/src/utils/endpoints.ts index a5cf49cc..85cfa2f4 100644 --- a/GUI/src/utils/endpoints.ts +++ b/GUI/src/utils/endpoints.ts @@ -23,10 +23,15 @@ export const datasetsEndpoints = { CREATE_DATASET_GROUP: (): string => `/classifier/datasetgroup/create`, IMPORT_DATASETS: (): string => `/datasetgroup/data/import`, EXPORT_DATASETS: (): string => `/datasetgroup/data/download`, - DATASET_GROUP_PATCH_UPDATE: (): string => `/classifier/datasetgroup/update/patch`, - DATASET_GROUP_MINOR_UPDATE: (): string => `/classifier/datasetgroup/update/minor`, - DATASET_GROUP_MAJOR_UPDATE: (): string => `/classifier/datasetgroup/update/major`, + DATASET_GROUP_PATCH_UPDATE: (): string => + `/classifier/datasetgroup/update/patch`, + DATASET_GROUP_MINOR_UPDATE: (): string => + `/classifier/datasetgroup/update/minor`, + DATASET_GROUP_MAJOR_UPDATE: (): string => + `/classifier/datasetgroup/update/major`, GET_STOP_WORDS: (): string => `/classifier/datasetgroup/stop-words`, POST_STOP_WORDS: (): string => `/classifier/datasetgroup/update/stop-words`, - DELETE_STOP_WORD: (): string => `/classifier/datasetgroup/delete/stop-words` -}; + DELETE_STOP_WORD: (): string => `/classifier/datasetgroup/delete/stop-words`, + IMPORT_STOP_WORDS: (): string => `/datasetgroup/data/import/stop-words`, + DELETE_STOP_WORDS: (): string => `/datasetgroup/data/delete/stop-words`, +}; \ No newline at end of file diff --git a/GUI/src/utils/queryKeys.ts b/GUI/src/utils/queryKeys.ts index 7f025f93..ffb200af 100644 --- a/GUI/src/utils/queryKeys.ts +++ b/GUI/src/utils/queryKeys.ts @@ -49,3 +49,7 @@ export const datasetQueryKeys = { ); }, }; + +export const stopWordsQueryKeys = { + GET_ALL_STOP_WORDS: () => [`datasetgroups/stopwords`], +}; diff --git a/GUI/translations/en/common.json b/GUI/translations/en/common.json index d7ecfbb2..f5065881 100644 --- a/GUI/translations/en/common.json +++ b/GUI/translations/en/common.json @@ -276,6 +276,7 @@ "add": "Add", "importModal": { "title": "Import stop words", + "importButton": "Import", "selectionLabel": "Select the option below", "addOption": "Import to add", "updateOption": "Import to update", @@ -283,8 +284,10 @@ "attachements": "Attachments (TXT, XLSX, YAML, JSON)", "inprogressTitle": "Import in Progress", "inprogressDesc": "The import of stop words is currently in progress. Please wait until the process is complete.", - "successTitle": "Data import waas successful", - "successDesc": "Your data has been successfully imported." + "successTitle": "Data import was successful", + "successDesc": "Your data has been successfully imported.", + "unsuccessTitle": "Data import was unsuccessful", + "unsuccessDesc": "Stopword Import Unsuccessful" } }, "validationSessions": { @@ -292,4 +295,4 @@ "inprogress": "Validation in-Progress", "fail": "Validation failed because {{class}} class found in the {{column}} column does not exist in hierarchy" } -} +} \ No newline at end of file diff --git a/GUI/translations/et/common.json b/GUI/translations/et/common.json index e8d1858a..053cfe4c 100644 --- a/GUI/translations/et/common.json +++ b/GUI/translations/et/common.json @@ -9,25 +9,25 @@ "modifiedAt": "Viimati muudetud", "addNew": "Lisa uus", "search": "Otsi", - "notification": "Teavitus", + "notification": "Teade", "notificationError": "Viga", "active": "Aktiivne", "activate": "Aktiveeri", "deactivate": "Deaktiveeri", - "disconnect":"Ühenda lahti", - "connect":"Ühenda", + "disconnect": "Lahuta", + "connect": "Ühenda", "on": "Sees", "off": "Väljas", "back": "Tagasi", "from": "Alates", "to": "Kuni", - "view": "Vaade", + "view": "Vaata", "resultCount": "Tulemuste arv", - "paginationNavigation": "Lehitsemine", + "paginationNavigation": "Lehekülgede navigeerimine", "gotoPage": "Mine lehele", "name": "Nimi", "idCode": "ID kood", - "status": "Staatus", + "status": "Olek", "yes": "Jah", "no": "Ei", "removeValidation": "Oled kindel?", @@ -37,37 +37,38 @@ "logout": "Logi välja", "change": "Muuda", "loading": "Laadimine", - "asc": "kasvav", - "desc": "kahanev", + "asc": "Kasvav", + "desc": "Kahanev", "reset": "Lähtesta", "choose": "Vali" }, "menu": { - "userManagement": "Kasutajahaldus", + "userManagement": "Kasutajate haldus", "integration": "Integratsioon", "datasets": "Andmekogumid", "datasetGroups": "Andmekogumite grupid", - "validationSessions": "Valideerimise seansid", + "validationSessions": "Valideerimisseansid", "dataModels": "Andmemudelid", - "testModel": "Testimismudel", + "testModel": "Testmudel", "stopWords": "Peatussõnad", "incomingTexts": "Saabuvad tekstid" }, "userManagement": { - "title": "Kasutajahaldus", - "addUserButton": " Lisa kasutaja", + "title": "Kasutajate haldus", + "addUserButton": "Lisa kasutaja", "addUser": { "addUserModalTitle": "Lisa uus kasutaja", "editUserModalTitle": "Muuda kasutajat", - "deleteUserModalTitle": "Oled kindel", + "deleteUserModalTitle": "Oled kindel?", + "deleteUserModalDesc": "Kinnita, et soovid järgmise kirje kustutada", "name": "Ees- ja perekonnanimi", "namePlaceholder": "Sisesta nimi", "role": "Roll", "rolePlaceholder": "-Vali-", "personalId": "Isikukood", "personalIdPlaceholder": "Sisesta isikukood", - "title": "Ametinimetus", - "titlePlaceholder": "Sisesta ametinimetus", + "title": "Tiitel", + "titlePlaceholder": "Sisesta tiitel", "email": "E-post", "emailPlaceholder": "Sisesta e-post" }, @@ -86,20 +87,20 @@ "pinal": "Pinal", "outlookAndPinal": "Outlook+Pinal", "jiraDesc": "Atlassiani probleemide jälgimise ja projektijuhtimise tarkvara", - "outlookDesc": "Microsofti poolt arendatud isikliku teabe halduri ja e-posti rakendus", + "outlookDesc": "Microsofti välja töötatud isikliku teabe halduri ja e-posti rakendus", "pinalDesc": "Atlassiani probleemide jälgimise ja projektijuhtimise tarkvara", "connected": "Ühendatud", "disconnected": "Lahutatud", "integrationErrorTitle": "Integratsioon ebaõnnestus", - "integrationErrorDesc": "Ühendus {{channel}}-iga ebaõnnestus. Palun kontrolli oma seadeid ja proovi uuesti. Kui probleem püsib, võta ühendust toega.", + "integrationErrorDesc": "Ühendamine kanaliga {{channel}} ebaõnnestus. Palun kontrolli oma seadeid ja proovi uuesti. Kui probleem püsib, võta ühendust tugiteenusega.", "integrationSuccessTitle": "Integratsioon õnnestus", - "integrationSuccessDesc": "Oled edukalt ühendatud {{channel}}-iga! Sinu integratsioon on nüüd täielik ja saad alustada {{channel}}-iga töötamist.", + "integrationSuccessDesc": "Oled edukalt ühendanud kanaliga {{channel}}! Sinu integratsioon on nüüd täielik ja saad hakata kanaliga {{channel}} sujuvalt töötama.", "confirmationModalTitle": "Oled kindel?", - "disconnectConfirmationModalDesc": "Kas oled kindel, et soovid lahutada {{channel}} integratsiooni? See toimingut ei saa tagasi võtta ja see võib mõjutada sinu töövoogu ja seotud probleeme.", - "connectConfirmationModalDesc": "Kas oled kindel, et soovid ühendada {{channel}} integratsiooni?", - "disconnectErrorTi/tle": "Lahutamine ebaõnnestus", - "disconnectErrorDesc": "Ühenduse katkestamine {{channel}}-iga ebaõnnestus. Palun kontrolli oma seadeid ja proovi uuesti. Kui probleem püsib, võta ühendust toega.", - "addUserButton": " Lisa kasutaja", + "disconnectConfirmationModalDesc": "Oled kindel, et soovid lahutada kanali {{channel}} integratsiooni? See toiming ei saa tagasi võtta ja võib mõjutada sinu töövoogu ja seotud probleeme.", + "connectConfirmationModalDesc": "Oled kindel, et soovid ühendada kanali {{channel}} integratsiooni?", + "disconnectErrorTitle": "Lahutamine ebaõnnestus", + "disconnectErrorDesc": "Kanali {{channel}} lahutamine ebaõnnestus. Palun kontrolli oma seadeid ja proovi uuesti. Kui probleem püsib, võta ühendust tugiteenusega.", + "addUserButton": "Lisa kasutaja", "addUser": { "name": "Ees- ja perekonnanimi", "namePlaceholder": "Sisesta nimi", @@ -107,19 +108,19 @@ "rolePlaceholder": "-Vali-", "personalId": "Isikukood", "personalIdPlaceholder": "Sisesta isikukood", - "title": "Ametinimetus", - "titlePlaceholder": "Sisesta ametinimetus", + "title": "Tiitel", + "titlePlaceholder": "Sisesta tiitel", "email": "E-post", "emailPlaceholder": "Sisesta e-post" } }, "roles": { "ROLE_ADMINISTRATOR": "Administraator", - "ROLE_MODEL_TRAINER": "Mudelitreener" + "ROLE_MODEL_TRAINER": "Mudeli treener" }, "toast": { "success": { - "updateSuccess": "Edukas uuendus", + "updateSuccess": "Uuendamine õnnestus", "copied": "Kopeeritud", "userDeleted": "Kasutaja kustutatud", "newUserAdded": "Uus kasutaja lisatud", @@ -132,28 +133,28 @@ "table": { "group": "Andmekogumi grupp", "version": "Versioon", - "validationStatus": "Valideerimise staatus", + "validationStatus": "Valideerimise olek", "sortBy": "Sorteeri nime järgi ({{sortOrder}})", "email": "E-post", "actions": "Toimingud" }, "datasetCard": { "validationFail": "Valideerimine ebaõnnestus", - "validationSuccess": "Valideerimine edukas", + "validationSuccess": "Valideerimine õnnestus", "validationInprogress": "Valideerimine pooleli", "notValidated": "Pole valideeritud", "settings": "Seaded", "lastModelTrained": "Viimati treenitud mudel", - "lastUsedForTraining": "Viimati kasutatud treeninguks", + "lastUsedForTraining": "Viimati kasutatud treenimiseks", "lastUpdate": "Viimati uuendatud", "latest": "Viimane" }, "createDataset": { "title": "Loo andmekogumi grupp", - "datasetDetails": "Andmekogumi andmed", + "datasetDetails": "Andmekogumi üksikasjad", "datasetName": "Andmekogumi nimi", "datasetInputPlaceholder": "Sisesta andmekogumi nimi", - "validationCriteria": "Loo valideerimiskriteeriumid", + "validationCriteria": "Loo valideerimise kriteeriumid", "fieldName": "Välja nimi", "datasetType": "Andmekogumi tüübid", "dataClass": "Andmeklass", @@ -161,50 +162,50 @@ "typeNumbers": "Numbrid", "typeDateTime": "Kuupäev ja kellaaeg", "addClassButton": "Lisa klass", - "addNowButton": "Lisa nüüd" + "addNowButton": "Lisa kohe" }, "classHierarchy": { "title": "Klassihierarhia", "addClassButton":"Lisa põhiklass", - "addSubClass": "Lisa alamhierarhia", + "addSubClass": "Lisa alklass", "fieldHint": "Sisesta välja nimi", "filedHintIfExists": "Klassi nimi on juba olemas" }, "modals": { "deleteClassTitle": "Oled kindel?", - "deleteClaassDesc": "Kinnita, et soovid kustutada järgmise kirje", - "columnInsufficientHeader": "Andmekogumis pole piisavalt veerge", - "columnInsufficientDescription": "Andmekogumil peab olema vähemalt 2 veergu. Lisaks peab olema vähemalt üks veerg määratud andmeklassiks ja üks veerg, mis ei ole andmeklass. Palun kohanda oma andmekogumit vastavalt.", + "deleteClaassDesc": "Kinnita, et soovid järgmise kirje kustutada", + "columnInsufficientHeader": "Ebapiisavad veerud andmekogumis", + "columnInsufficientDescription": "Andmekogumis peab olema vähemalt 2 veergu. Lisaks peab olema vähemalt üks veerg määratud andmeklassiks ja üks veerg, mis ei ole andmeklass. Palun kohanda oma andmekogumit vastavalt.", "createDatasetSuccessTitle": "Andmekogumi grupp edukalt loodud", "createDatasetUnsuccessTitle": "Andmekogumi grupi loomine ebaõnnestus", - "createDatasetSucceessDesc": "Oled edukalt loonud andmekogumi grupi. Üksikasjalikus vaates saad nüüd andmekogumit vaadata ja redigeerida.", + "createDatasetSucceessDesc": "Oled edukalt loonud andmekogumi grupi. Üksikasjalikus vaates näed ja saad andmekogumit vajadusel redigeerida.", "navigateDetailedViewButton": "Mine üksikasjalikku vaatesse", - "enableDatasetTitle": "Andmekogumi gruppi ei saa lubada", - "enableDatasetDesc": "Andmekogumi gruppi ei saa lubada enne andmete lisamist. Palun lisa andmekogumid sellesse gruppi ja proovi uuesti.", + "enableDatasetTitle": "Ei saa andmekogumi gruppi lubada", + "enableDatasetDesc": "Andmekogumi gruppi ei saa lubada, kuni andmed on lisatud. Palun lisa andmekogumid sellesse gruppi ja proovi uuesti.", "errorTitle": "Toiming ebaõnnestus", "errorDesc": "Midagi läks valesti. Palun proovi uuesti." }, "detailedView": { "connectedModels": "Ühendatud mudelid", "noOfItems": "Üksuste arv", - "export": "Ekspordi andmed", - "import": "Impordi andmed", - "unsavedChangesWarning": "Oled teinud andmekogumisse muudatusi, mida pole salvestatud. Palun salvesta muudatused, et neid rakendada", + "export": "Ekspordi andmekogum", + "import": "Impordi andmekogum", + "unsavedChangesWarning": "Oled teinud andmekogumisse muudatusi, mis pole salvestatud. Palun salvesta muudatused, et neid rakendada", "insufficientExamplesDesc": "Ebapiisavad näited - andmekogumi grupi aktiveerimiseks on vaja vähemalt 10 näidet", - "noData": "Andmed puuduvad", - "noDataDesc": "Oled loonud andmekogumi grupi, kuid siin pole andmeid näha. Saad andmekogumi üles laadida, et seda siin kuvada. Kui andmed on lisatud, saad neid vajadusel redigeerida või kustutada.", + "noData": "Andmeid pole saadaval", + "noDataDesc": "Oled loonud andmekogumi grupi, kuid andmekogumeid pole siin näidata. Sa saad andmekogumi üles laadida, et seda selles ruumis vaadata. Pärast lisamist saad andmeid vajadusel redigeerida või kustutada.", "importExamples": "Impordi näited", - "importNewData": "Impordi uusi andmeid", - "majorUpdateBanner": "Oled muutnud andmekogumi skeemi olulisi konfiguratsioone, mis pole salvestatud, palun salvesta muudatused. Kõik imporditud failid või olemasolevate andmete redigeerimised tühistatakse pärast muudatuste rakendamist", - "minorUpdateBanner": "Oled importinud andmekogumisse uusi andmeid, palun salvesta muudatused. Kõik muudatused, mida tegid üksikutele andmeüksustele, tühistatakse pärast muudatuste rakendamist", - "patchUpdateBanner": "Oled redigeerinud andmekogumi üksikuid üksusi, mida pole salvestatud. Palun salvesta muudatused, et neid rakendada", - "confirmMajorUpdatesTitle": "Kinnita suur uuendus", - "confirmMajorUpdatesDesc": "Kõik imporditud failid või olemasolevate andmete redigeerimised tühistatakse pärast muudatuste rakendamist", - "confirmMinorUpdatesTitle": "Kinnita väike uuendus", - "confirmMinorUpdatesDesc": "Kõik muudatused, mida tegid üksikutele andmeüksustele (osauuendus), tühistatakse pärast muudatuste rakendamist", - "confirmPatchUpdatesTitle": "Kinnita osauuendus", + "importNewData": "Impordi uued andmed", + "majorUpdateBanner": "Oled uuendanud andmekogumi skeemi võtmekonfiguratsioone, mida pole salvestatud, palun salvesta muudatuste rakendamiseks. Kõik imporditud failid või olemasolevate andmete muudatused tühistatakse pärast muudatuste rakendamist", + "minorUpdateBanner": "Oled importinud uusi andmeid andmekogumisse, palun salvesta muudatuste rakendamiseks. Kõik individuaalsete andmekirjete muudatused tühistatakse pärast muudatuste rakendamist", + "patchUpdateBanner": "Oled muutnud andmekogumi üksikuid kirjeid, mis pole salvestatud. Palun salvesta muudatuste rakendamiseks", + "confirmMajorUpdatesTitle": "Kinnita suurem uuendus", + "confirmMajorUpdatesDesc": "Kõik imporditud failid või olemasolevate andmete muudatused tühistatakse pärast muudatuste rakendamist", + "confirmMinorUpdatesTitle": "Kinnita väiksem uuendus", + "confirmMinorUpdatesDesc": "Kõik individuaalsete andmekirjete muudatused (parandusuuendus) tühistatakse pärast muudatuste rakendamist", + "confirmPatchUpdatesTitle": "Kinnita parandusuuendus", "confirmPatchUpdatesDesc": "Muudetud andmeread uuendatakse andmekogumis", - "patchDataUnsuccessfulTitle": "Osauuenduse andmete uuendamine ebaõnnestus", + "patchDataUnsuccessfulTitle": "Parandusandmete uuendamine ebaõnnestus", "patchDataUnsuccessfulDesc": "Midagi läks valesti. Palun proovi uuesti.", "exportDataSuccessTitle": "Andmete eksport oli edukas", "exportDataSuccessDesc": "Sinu andmed on edukalt eksporditud.", @@ -212,13 +213,13 @@ "exportDataUnsucessDesc": "Midagi läks valesti. Palun proovi uuesti.", "ImportDataUnsucessTitle": "Andmekogumi import ebaõnnestus", "importDataUnsucessDesc": "Midagi läks valesti. Palun proovi uuesti.", - "validationInitiatedTitle": "Andmekogum üles laaditud ja valideerimine alustatud", - "validationInitiatedDesc": "Andmekogumifail laaditi edukalt üles. Valideerimine ja eeltöötlus on nüüd alustatud", + "validationInitiatedTitle": "Andmekogum üles laaditud ja valideerimine algatatud", + "validationInitiatedDesc": "Andmekogumi fail laaditi edukalt üles. Valideerimine ja eeltöötlus on nüüd algatatud", "viewValidations": "Vaata valideerimisseansse", "table": { - "id": "Rea ID", + "id": "rea ID", "data": "Andmed", - "label": "Märgis", + "label": "Silt", "actions": "Toimingud" }, "validationsTitle": "Andmekogumi grupi valideerimised", @@ -226,42 +227,42 @@ "delete": "Kustuta andmekogum", "modals": { "import": { - "title": "Impordi uusi andmeid", - "fileFormatlabel": "Vali failivorming", + "title": "Impordi uued andmed", + "fileFormatlabel": "Vali failiformaat", "attachments": "Manused", - "maxSize": "Maksimaalne faili suurus - 10 MB", + "maxSize": "Maksimaalne failisuurus - 10 MB", "browse": "Sirvi faili", "import": "Impordi", "cancel": "Tühista", "uploadInProgress": "Üleslaadimine pooleli...", - "uploadDesc": "Andmekogumi üleslaadimine. Palun oota, kuni üleslaadimine lõpeb. Kui tühistad poole pealt, andmed ja edenemine kaovad.", - "invalidFile": "Vigane failivorming", - "invalidFileDesc": "Üleslaaditud fail ei ole õigel {{format}} vormingus. Palun laadi üles kehtiv {{format}} fail ja proovi uuesti." + "uploadDesc": "Andmekogumi üleslaadimine. Palun oota, kuni üleslaadimine lõpeb. Kui tühistad protsessi, lähevad andmed ja edusammud kaduma.", + "invalidFile": "Vale failivorming", + "invalidFileDesc": "Üleslaaditud fail pole õiges {{format}} vormingus. Palun laadi üles kehtiv {{format}} fail ja proovi uuesti." }, "export": { "export": "Ekspordi andmed", "exportButton": "Ekspordi", - "fileFormatlabel": "Vali failivorming", + "fileFormatlabel": "Vali failiformaat", "title": "Andmete eksport oli edukas", "description": "Sinu andmed on edukalt eksporditud." }, "delete": { "title": "Oled kindel?", - "description": "Kui kustutad andmekogumi, muutuvad kõik sellega seotud mudelid treenimatuks. Oled kindel, et soovid jätkata?" + "description": "Kui kustutad andmekogumi, muutuvad kõik selle mudeliga seotud mudelid treenimatuks. Oled kindel, et soovid jätkata?" }, "edit": { "title": "Muuda", "data": "Andmed", - "label": "Märgis", + "label": "Silt", "update": "Uuenda" }, "upload": { - "title": "Andmete üleslaadimine oli edukas", - "desc": "Andmekogumifail laaditi edukalt üles. Palun salvesta muudatused, et alustada andmete valideerimist ja eeltöötlust" + "title": "Andmete üleslaadimine õnnestus", + "desc": "Andmekogumi fail laaditi edukalt üles. Palun salvesta muudatuste rakendamiseks, et algatada andmete valideerimine ja eeltöötlus." }, "datasetDelete": { "confirmationTitle": "Oled kindel?", - "confirmationDesc": "Kinnita, et soovid kustutada järgmise andmekogumi", + "confirmationDesc": "Kinnita, et soovid järgmise andmekogumi kustutada", "successTitle": "Edu: Andmekogum kustutatud", "successDesc": "Oled edukalt kustutanud andmekogumi. Andmekogum pole enam saadaval ja kõik seotud andmed on eemaldatud." } @@ -275,20 +276,23 @@ "add": "Lisa", "importModal": { "title": "Impordi peatussõnad", - "selectionLabel": "Vali allpool olev valik", + "importButton": "Impordi", + "selectionLabel": "Vali allolev valik", "addOption": "Impordi lisamiseks", "updateOption": "Impordi uuendamiseks", "deleteOption": "Impordi kustutamiseks", "attachements": "Manused (TXT, XLSX, YAML, JSON)", "inprogressTitle": "Importimine pooleli", - "inprogressDesc": "Peatussõnade importimine on hetkel pooleli. Palun oota, kuni protsess lõpeb.", - "successTitle": "Andmete import oli edukas", - "successDesc": "Sinu andmed on edukalt imporditud." + "inprogressDesc": "Peatussõnade importimine on pooleli. Palun oota, kuni protsess lõpeb.", + "successTitle": "Andmete import õnnestus", + "successDesc": "Sinu andmed on edukalt imporditud.", + "unsuccessTitle": "Andmete import ebaõnnestus", + "unsuccessDesc": "Peatussõnade import ebaõnnestus" } }, "validationSessions": { - "title": "Valideerimise seansid", + "title": "Valideerimisseansid", "inprogress": "Valideerimine pooleli", - "fail": "Valideerimine ebaõnnestus, sest {{class}} klassi leiti {{column}} veerust, mis ei eksisteeri hierarhias" + "fail": "Valideerimine ebaõnnestus, kuna klass {{class}} leiti veerust {{column}}, kuid seda pole hierarhias olemas" } } From 44ed944fb1f3c701845e181c1abddc2716f9d1e1 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Tue, 6 Aug 2024 15:31:40 +0530 Subject: [PATCH 373/582] docker compose update for the file handiler --- docker-compose.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index bfe7bcd6..97e42e15 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -191,6 +191,9 @@ services: - S3_FERRY_URL=http://s3-ferry:3000/v1/files/copy - IMPORT_STOPWORDS_URL=http://ruuter-private:8088/classifier/datasetgroup/update/stop-words - DELETE_STOPWORDS_URL=http://ruuter-private:8088/classifier/datasetgroup/delete/stop-words + - GET_PAGE_COUNT_URL=http://ruuter-private:8088/classifier/datasetgroup/group/page-count?groupId=dgId + - DATAGROUP_DELETE_CONFIRMATION_URL= http://ruuter-private:8088/classifier/datasetgroup/metadata/delete + - DATAMODEL_DELETE_CONFIRMATION_URL= http://ruuter-private:8088/classifier/datamodel/update/datasetgroup ports: - "8000:8000" networks: From 9fccdef48252a81d847377b2392421e801789847 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Tue, 6 Aug 2024 16:22:36 +0530 Subject: [PATCH 374/582] docker compose datamodel delete bug fix --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 97e42e15..207e11ad 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -193,7 +193,7 @@ services: - DELETE_STOPWORDS_URL=http://ruuter-private:8088/classifier/datasetgroup/delete/stop-words - GET_PAGE_COUNT_URL=http://ruuter-private:8088/classifier/datasetgroup/group/page-count?groupId=dgId - DATAGROUP_DELETE_CONFIRMATION_URL= http://ruuter-private:8088/classifier/datasetgroup/metadata/delete - - DATAMODEL_DELETE_CONFIRMATION_URL= http://ruuter-private:8088/classifier/datamodel/update/datasetgroup + - DATAMODEL_DELETE_CONFIRMATION_URL= http://ruuter-private:8088/classifier/datamodel/update/dataset-group ports: - "8000:8000" networks: From abce9b66ff7ebb8f6cd4311ee327995a1be87649 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Tue, 6 Aug 2024 16:26:03 +0530 Subject: [PATCH 375/582] validation confirmation URL --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index 207e11ad..99d872f6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -241,6 +241,7 @@ services: - S3_BUCKET_NAME=esclassifier-test - S3_REGION_NAME=eu-west-1 - PARAPHRASE_API_URL=http://data-enrichment-api:8005/paraphrase + - VALIDATION_CONFIRMATION_URL=http://ruuter-private:8088/classifier/datasetgroup/update/validation/status ports: - "8001:8001" networks: From 009ee468dd61712a834cea3e22304dff86f4bb37 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Tue, 6 Aug 2024 16:26:18 +0530 Subject: [PATCH 376/582] validation API endpoint --- dataset-processor/dataset_processor_api.py | 67 ++++++++++------------ 1 file changed, 30 insertions(+), 37 deletions(-) diff --git a/dataset-processor/dataset_processor_api.py b/dataset-processor/dataset_processor_api.py index d2d831f4..2440c60d 100644 --- a/dataset-processor/dataset_processor_api.py +++ b/dataset-processor/dataset_processor_api.py @@ -5,10 +5,13 @@ from dataset_processor import DatasetProcessor import requests import os +from dataset_validator import DatasetValidator app = FastAPI() processor = DatasetProcessor() +validator = DatasetValidator() RUUTER_PRIVATE_URL = os.getenv("RUUTER_PRIVATE_URL") +VALIDATION_CONFIRMATION_URL = os.getenv("VALIDATION_CONFIRMATION_URL") app.add_middleware( CORSMiddleware, @@ -42,9 +45,7 @@ async def authenticate_user(request: Request): @app.post("/init-dataset-process") async def process_handler_endpoint(request: Request): - print("in init dataset") payload = await request.json() - print(payload) await authenticate_user(request) authCookie = payload["cookie"] @@ -61,39 +62,31 @@ async def forward_request(request: Request, response: Response): except Exception as e: raise HTTPException(status_code=400, detail=f"Invalid JSON payload: {str(e)}") + validator_response = validator.process_request(int(payload["dgId"]), payload["cookie"], payload["updateType"], payload["savedFilePath"]) + if validator_response["response"]["operationSuccessful"] != True: + return False + else: + headers = { + 'cookie': payload["cookie"], + 'Content-Type': 'application/json' + } - headers = { - 'cookie': payload["cookie"], - 'Content-Type': 'application/json' - } - - print(payload) - payload2 = {} - payload2["dgId"] = int(payload["dgId"]) - payload2["newDgId"] = int(payload["newDgId"]) - payload2["updateType"] = payload["updateType"] - payload2["patchPayload"] = payload["patchPayload"] - payload2["savedFilePath"] = payload["savedFilePath"] - payload2["validationStatus"] = "success" - payload2["validationErrors"] = [] - - cookie = payload["cookie"] - - forward_url = "http://ruuter-private:8088/classifier/datasetgroup/update/validation/status" - - try: - print("8") - forward_response = requests.post(forward_url, json=payload2, headers=headers) - print(headers) - print("9") - forward_response.raise_for_status() - print("10") - return JSONResponse(content=forward_response.json(), status_code=forward_response.status_code) - except requests.HTTPError as e: - print("11") - print(e) - raise HTTPException(status_code=e.response.status_code, detail=e.response.text) - except Exception as e: - print("12") - print(e) - raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file + payload2 = {} + payload2["dgId"] = int(payload["dgId"]) + payload2["newDgId"] = int(payload["newDgId"]) + payload2["updateType"] = payload["updateType"] + payload2["patchPayload"] = payload["patchPayload"] + payload2["savedFilePath"] = payload["savedFilePath"] + payload2["validationStatus"] = "success" + payload2["validationErrors"] = [] + try: + forward_response = requests.post(VALIDATION_CONFIRMATION_URL, json=payload2, headers=headers) + forward_response.raise_for_status() + + return JSONResponse(content=forward_response.json(), status_code=forward_response.status_code) + except requests.HTTPError as e: + print(e) + raise HTTPException(status_code=e.response.status_code, detail=e.response.text) + except Exception as e: + print(e) + raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file From 5e35eb35e30f445537c73300ad37f6da588e473e Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Tue, 6 Aug 2024 16:26:47 +0530 Subject: [PATCH 377/582] dataset validator class --- dataset-processor/dataset_validator.py | 190 +++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 dataset-processor/dataset_validator.py diff --git a/dataset-processor/dataset_validator.py b/dataset-processor/dataset_validator.py new file mode 100644 index 00000000..70a07352 --- /dev/null +++ b/dataset-processor/dataset_validator.py @@ -0,0 +1,190 @@ +import os +import re +import json +import requests +import urllib.parse +import datetime + +class DatasetValidator: + def __init__(self): + pass + + def process_request(self, dgId, cookie, updateType, savedFilePath): + print("Process request started") + print(f"dgId: {dgId}, updateType: {updateType}, savedFilePath: {savedFilePath}") + if updateType == "minor": + return self.handle_minor_update(dgId, cookie, savedFilePath) + elif updateType == "patch": + return self.handle_patch_update() + else: + return self.generate_response(False, "Unknown update type") + + def handle_minor_update(self, dgId, cookie, savedFilePath): + try: + print("Handling minor update") + data = self.get_dataset_by_location(savedFilePath, cookie) + if data is None: + print("Failed to download and load data") + return self.generate_response(False, "Failed to download and load data") + print("Data downloaded and loaded successfully") + + validation_criteria, class_hierarchy = self.get_validation_criteria(dgId, cookie) + if validation_criteria is None: + print("Failed to get validation criteria") + return self.generate_response(False, "Failed to get validation criteria") + print("Validation criteria retrieved successfully") + + field_validation_result = self.validate_fields(data, validation_criteria) + if not field_validation_result['success']: + print("Field validation failed") + return self.generate_response(False, field_validation_result['message']) + print("Field validation successful") + + hierarchy_validation_result = self.validate_class_hierarchy(data, validation_criteria, class_hierarchy) + if not hierarchy_validation_result['success']: + print("Class hierarchy validation failed") + return self.generate_response(False, hierarchy_validation_result['message']) + print("Class hierarchy validation successful") + + print("Minor update processed successfully") + return self.generate_response(True, "Minor update processed successfully") + + except Exception as e: + print(f"Internal error: {e}") + return self.generate_response(False, f"Internal error: {e}") + + def handle_patch_update(self): + print("Handling patch update") + return self.generate_response(True, "Patch update processed successfully") + + def get_dataset_by_location(self, fileLocation, custom_jwt_cookie): + print("Downloading dataset by location") + params = {'saveLocation': fileLocation} + headers = {'cookie': custom_jwt_cookie} + try: + response = requests.get(os.getenv("FILE_HANDLER_DOWNLOAD_LOCATION_JSON_URL"), params=params, headers=headers) + response.raise_for_status() + print("Dataset downloaded successfully") + return response.json() + except requests.exceptions.RequestException as e: + print(f"Error downloading dataset: {e}") + return None + + def get_validation_criteria(self, dgId, cookie): + print("Fetching validation criteria") + params = {'dgId': dgId} + headers = {'cookie': cookie} + try: + response = requests.get(os.getenv("GET_VALIDATION_SCHEMA"), params=params, headers=headers) + response.raise_for_status() + print("Validation criteria fetched successfully") + validation_criteria = response.json().get('response', {}).get('validationCriteria', {}) + class_hierarchy = response.json().get('response', {}).get('classHierarchy', {}) + return validation_criteria, class_hierarchy + except requests.exceptions.RequestException as e: + print(f"Error fetching validation criteria: {e}") + return None + + def validate_fields(self, data, validation_criteria): + print("Validating fields") + try: + fields = validation_criteria.get('fields', []) + validation_rules = validation_criteria.get('validationRules', {}) + + for field in fields: + if field not in data[0]: + print(f"Missing field: {field}") + return {'success': False, 'message': f"Missing field: {field}"} + + for idx, row in enumerate(data): + for field, rules in validation_rules.items(): + if field in row: + value = row[field] + if not self.validate_value(value, rules['type']): + print(f"Validation failed for field '{field}' in row {idx + 1}") + return {'success': False, 'message': f"Validation failed for field '{field}' in row {idx + 1}"} + print("Fields validation successful") + return {'success': True, 'message': "Fields validation successful"} + except Exception as e: + print(f"Error validating fields: {e}") + return {'success': False, 'message': f"Error validating fields: {e}"} + + def validate_value(self, value, value_type): + if value_type == 'email': + return re.match(r"[^@]+@[^@]+\.[^@]+", value) is not None + elif value_type == 'text': + return isinstance(value, str) + elif value_type == 'int': + return isinstance(value, int) or value.isdigit() + elif value_type == 'float': + try: + float(value) + return True + except ValueError: + return False + elif value_type == 'datetime': + try: + datetime.datetime.fromisoformat(value) + return True + except ValueError: + return False + return False + + def validate_class_hierarchy(self, data, validation_criteria, class_hierarchy): + print("Validating class hierarchy") + try: + data_class_columns = [field for field, rules in validation_criteria.get('validationRules', {}).items() if rules.get('isDataClass', False)] + + hierarchy_values = self.extract_hierarchy_values(class_hierarchy) + data_values = self.extract_data_class_values(data, data_class_columns) + + missing_in_hierarchy = data_values - hierarchy_values + missing_in_data = hierarchy_values - data_values + + if missing_in_hierarchy: + print(f"Values missing in class hierarchy: {missing_in_hierarchy}") + return {'success': False, 'message': f"Values missing in class hierarchy: {missing_in_hierarchy}"} + if missing_in_data: + print(f"Values missing in data class columns: {missing_in_data}") + return {'success': False, 'message': f"Values missing in data class columns: {missing_in_data}"} + + print("Class hierarchy validation successful") + return {'success': True, 'message': "Class hierarchy validation successful"} + except Exception as e: + print(f"Error validating class hierarchy: {e}") + return {'success': False, 'message': f"Error validating class hierarchy: {e}"} + + def extract_hierarchy_values(self, hierarchy): + print("Extracting hierarchy values") + print(hierarchy) + values = set() + + def traverse(node): + if 'class' in node: + values.add(node['class']) + if 'subclasses' in node: + for subclass in node['subclasses']: + traverse(subclass) + + for item in hierarchy: + traverse(item) + print(f"Hierarchy values extracted: {values}") + return values + + def extract_data_class_values(self, data, columns): + print("Extracting data class values") + values = set() + for row in data: + for col in columns: + values.add(row.get(col)) + print(f"Data class values extracted: {values}") + return values + + def generate_response(self, success, message): + print(f"Generating response: success={success}, message={message}") + return { + 'response': { + 'operationSuccessful': success, + 'message': message + } + } From 987ffce32232a79f44a9bd5f597133e512a4ed4a Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Tue, 6 Aug 2024 16:27:12 +0530 Subject: [PATCH 378/582] creating seperate cron manger files --- DSL/CronManager/DSL/data_validation.yml | 5 +++++ DSL/CronManager/DSL/dataset_deletion.yml | 5 +++++ .../DSL/{dataset_processing.yml => dataset_processor.yml} | 6 ------ 3 files changed, 10 insertions(+), 6 deletions(-) create mode 100644 DSL/CronManager/DSL/data_validation.yml create mode 100644 DSL/CronManager/DSL/dataset_deletion.yml rename DSL/CronManager/DSL/{dataset_processing.yml => dataset_processor.yml} (50%) diff --git a/DSL/CronManager/DSL/data_validation.yml b/DSL/CronManager/DSL/data_validation.yml new file mode 100644 index 00000000..037f45ba --- /dev/null +++ b/DSL/CronManager/DSL/data_validation.yml @@ -0,0 +1,5 @@ +data_validation: + trigger: off + type: exec + command: "../app/scripts/data_validator_exec.sh" + allowedEnvs: ["cookie","dgId", "newDgId","updateType","savedFilePath","patchPayload"] \ No newline at end of file diff --git a/DSL/CronManager/DSL/dataset_deletion.yml b/DSL/CronManager/DSL/dataset_deletion.yml new file mode 100644 index 00000000..fdb7ac3e --- /dev/null +++ b/DSL/CronManager/DSL/dataset_deletion.yml @@ -0,0 +1,5 @@ +dataset_deletion: + trigger: off + type: exec + command: "../app/scripts/dataset_deletion_exec.sh" + allowedEnvs: ["cookie","dgId"] \ No newline at end of file diff --git a/DSL/CronManager/DSL/dataset_processing.yml b/DSL/CronManager/DSL/dataset_processor.yml similarity index 50% rename from DSL/CronManager/DSL/dataset_processing.yml rename to DSL/CronManager/DSL/dataset_processor.yml index c8a3b03b..b57c7886 100644 --- a/DSL/CronManager/DSL/dataset_processing.yml +++ b/DSL/CronManager/DSL/dataset_processor.yml @@ -2,10 +2,4 @@ dataset_processor: trigger: off type: exec command: "../app/scripts/data_processor_exec.sh" - allowedEnvs: ["cookie","dgId", "newDgId","updateType","savedFilePath","patchPayload"] - -data_validation: - trigger: off - type: exec - command: "../app/scripts/data_validator_exec.sh" allowedEnvs: ["cookie","dgId", "newDgId","updateType","savedFilePath","patchPayload"] \ No newline at end of file From a61982e5fa2452669fe5e88fba9407792767b615 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Tue, 6 Aug 2024 16:27:27 +0530 Subject: [PATCH 379/582] dataset deletion script for cron manager --- .../script/dataset_deletion_exec.sh | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 DSL/CronManager/script/dataset_deletion_exec.sh diff --git a/DSL/CronManager/script/dataset_deletion_exec.sh b/DSL/CronManager/script/dataset_deletion_exec.sh new file mode 100644 index 00000000..d4814415 --- /dev/null +++ b/DSL/CronManager/script/dataset_deletion_exec.sh @@ -0,0 +1,42 @@ +#!/bin/bash +echo "Started Shell Script to delete" +# Ensure required environment variables are set +if [ -z "$dgId" ] || [ -z "$cookie" ]; then + echo "One or more environment variables are missing." + echo "Please set dgId, newDgId, cookie, updateType, savedFilePath, and patchPayload." + exit 1 +fi + +# Construct the payload using here document +payload=$(cat < Date: Tue, 6 Aug 2024 16:27:39 +0530 Subject: [PATCH 380/582] new page count ruuter endpoint --- .../datasetgroup/group/page-count.yml | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/group/page-count.yml diff --git a/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/group/page-count.yml b/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/group/page-count.yml new file mode 100644 index 00000000..d42266bd --- /dev/null +++ b/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/group/page-count.yml @@ -0,0 +1,83 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'PAGE-COUNT'" + method: get + accepts: json + returns: json + namespace: classifier + allowlist: + params: + - field: groupId + type: number + description: "Parameter 'groupId'" + headers: + - field: cookie + type: string + description: "Cookie field" + +extract_data: + assign: + group_id: ${Number(incoming.params.groupId)} + cookie: ${incoming.headers.cookie} + next: get_dataset_group_fields_by_id + +get_dataset_group_fields_by_id: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-dataset-group-fields-by-id" + body: + id: ${group_id} + result: res_dataset + next: check_fields_status + +check_fields_status: + switch: + - condition: ${200 <= res_dataset.response.statusCodeValue && res_dataset.response.statusCodeValue < 300} + next: check_fields_data_exist + next: assign_fail_response + +check_fields_data_exist: + switch: + - condition: ${res_dataset.response.body.length>0} + next: assign_fields_response + next: assign_fail_response + +assign_fields_response: + assign: + num_pages: ${res_dataset.response.body[0].numPages} + next: assign_formated_response + +assign_formated_response: + assign: + val: { + dgId: '${group_id}', + numPages: '${num_pages}' + } + next: assign_success_response + +assign_success_response: + assign: + format_res: { + operationSuccessful: true, + data: '${val}' + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + operationSuccessful: false, + data: '${[]}' + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end \ No newline at end of file From 516cbbde281711b293daf45dd737c9914779466b Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Tue, 6 Aug 2024 16:28:21 +0530 Subject: [PATCH 381/582] mock removal and cron manager location update --- .../DSL/POST/classifier/datasetgroup/delete.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/delete.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/delete.yml index 92b1ab32..0e3d4333 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/delete.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/delete.yml @@ -45,9 +45,9 @@ check_dataset_data_exist: next: assign_fail_response execute_cron_manager: - call: reflect.mock + call: http.post args: - url: "[#CLASSIFIER_CRON_MANAGER]/execute/dataset_processing/dataset_deletion" + url: "[#CLASSIFIER_CRON_MANAGER]/execute/dataset_deletion/dataset_deletion" query: cookie: ${incoming.headers.cookie} dgId: ${dg_id} From 7c20537f9bb0f6446dc2f1d20f691c148eaf8d75 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Tue, 6 Aug 2024 16:28:39 +0530 Subject: [PATCH 382/582] cron manager DSL location update --- .../DSL/POST/classifier/datasetgroup/update/minor.yml | 2 +- .../DSL/POST/classifier/datasetgroup/update/patch.yml | 2 +- .../POST/classifier/datasetgroup/update/validation/status.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/minor.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/minor.yml index d06bc3e7..879251e3 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/minor.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/minor.yml @@ -98,7 +98,7 @@ assign_new_dg_id: execute_cron_manager: call: http.post args: - url: "[#CLASSIFIER_CRON_MANAGER]/execute/dataset_processing/data_validation" + url: "[#CLASSIFIER_CRON_MANAGER]/execute/data_validation/data_validation" query: cookie: ${incoming.headers.cookie} dgId: ${dg_id} diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/patch.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/patch.yml index c7ac0b5d..12bc7a04 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/patch.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/patch.yml @@ -74,7 +74,7 @@ check_old_dataset_status: execute_cron_manager: call: http.post args: - url: "[#CLASSIFIER_CRON_MANAGER]/execute/dataset_processing/data_validation" + url: "[#CLASSIFIER_CRON_MANAGER]/execute/data_validation/data_validation" query: cookie: ${incoming.headers.cookie} dgId: ${dg_id} diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/validation/status.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/validation/status.yml index e594350d..045f8bc8 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/validation/status.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/validation/status.yml @@ -85,7 +85,7 @@ check_validation_status_type: execute_cron_manager: call: http.post args: - url: "[#CLASSIFIER_CRON_MANAGER]/execute/dataset_processing/dataset_processor" + url: "[#CLASSIFIER_CRON_MANAGER]/execute/dataset_processor/dataset_processor" query: cookie: ${incoming.headers.cookie} dgId: ${dg_id} From f54bdcb5f8afd0d85aa69996c4b109e539b66aaa Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Tue, 6 Aug 2024 16:29:02 +0530 Subject: [PATCH 383/582] dataset deletor updates --- file-handler/dataset_deleter.py | 16 ++++++---------- file-handler/file_handler_api.py | 16 +++++++--------- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/file-handler/dataset_deleter.py b/file-handler/dataset_deleter.py index cd1347dd..68a3034f 100644 --- a/file-handler/dataset_deleter.py +++ b/file-handler/dataset_deleter.py @@ -20,19 +20,14 @@ def get_page_count(self, dg_id, custom_jwt_cookie): response = requests.get(page_count_url, headers=headers) response.raise_for_status() data = response.json() - print(page_count_url) - print(f"data : {data}") - - page_count = data["response"]["data"][0]["numPages"] + page_count = data["response"]["data"]["numPages"] return page_count except requests.exceptions.RequestException as e: print(f"An error occurred: {e}") return None def delete_dataset_files(self, dg_id, cookie): - print("Reach : 3") page_count = self.get_page_count(dg_id, cookie) - print(f"Page count : {page_count}") if page_count is None: print(f"Failed to get page count for dg_id: {dg_id}") @@ -44,22 +39,23 @@ def delete_dataset_files(self, dg_id, cookie): ] if page_count>0: - print("Reach : 4") for page_id in range(1, page_count + 1): file_locations.append(f"/dataset/{dg_id}/chunks/{page_id}.json") - print(f"Reach : 5 > {file_locations}") - empty_json_path = os.path.join(UPLOAD_DIRECTORY, "empty.json") + empty_json_path = os.path.join('..', 'shared', "empty.json") with open(empty_json_path, 'w') as empty_file: json.dump({}, empty_file) + empty_json_path_s3 = "empty.json" + success_count = 0 for file_location in file_locations: - response = self.s3_ferry.transfer_file(file_location, "S3", empty_json_path, "FS") + response = self.s3_ferry.transfer_file(file_location, "S3", empty_json_path_s3, "FS") if response.status_code == 201: success_count += 1 print("SUCESS : FILE DELETED") else: + print(response.status_code) print(f"Failed to transfer file to {file_location}") all_files_deleted = success_count >= len(file_locations)-2 diff --git a/file-handler/file_handler_api.py b/file-handler/file_handler_api.py index 2217f195..a9f1e97f 100644 --- a/file-handler/file_handler_api.py +++ b/file-handler/file_handler_api.py @@ -34,7 +34,8 @@ S3_FERRY_URL = os.getenv("S3_FERRY_URL") IMPORT_STOPWORDS_URL = os.getenv("IMPORT_STOPWORDS_URL") DELETE_STOPWORDS_URL = os.getenv("DELETE_STOPWORDS_URL") -DELETE_CONFIRMATION_URL = os.getenv("DELETE_CONFIRMATION_URL") +DATAGROUP_DELETE_CONFIRMATION_URL = os.getenv("DATAGROUP_DELETE_CONFIRMATION_URL") +DATAMODEL_DELETE_CONFIRMATION_URL = os.getenv("DATAMODEL_DELETE_CONFIRMATION_URL") s3_ferry = S3Ferry(S3_FERRY_URL) class ExportFile(BaseModel): @@ -394,7 +395,6 @@ async def delete_stop_words(request: Request, stopWordsFile: UploadFile = File(. @app.post("/datasetgroup/data/delete") async def delete_dataset_files(request: Request): try: - print("Reach : 1") cookie = request.cookies.get("customJwtCookie") await authenticate_user(f'customJwtCookie={cookie}') @@ -402,12 +402,7 @@ async def delete_dataset_files(request: Request): dgId = int(payload["dgId"]) deleter = DatasetDeleter(S3_FERRY_URL) - print("Reach : 2") success, files_deleted = deleter.delete_dataset_files(dgId, f'customJwtCookie={cookie}') - - print("Reach : 6") - print(success) - print(files_deleted) if success: headers = { @@ -416,10 +411,13 @@ async def delete_dataset_files(request: Request): } payload = {"dgId": dgId} - response = requests.post(DELETE_CONFIRMATION_URL, headers=headers, json=payload) - print(f"Reach : 7 {response}") + response = requests.post(DATAGROUP_DELETE_CONFIRMATION_URL, headers=headers, json=payload) if response.status_code != 200: print(f"Failed to notify deletion endpoint. Status code: {response.status_code}") + else: + response = requests.post(DATAMODEL_DELETE_CONFIRMATION_URL, headers=headers, json=payload) + if response.status_code != 200: + print(f"Failed to notify model dataset deletion endpoint. Status code: {response.status_code}") return JSONResponse(status_code=200, content={"message": "Dataset deletion completed successfully.", "files_deleted": files_deleted}) else: From 9ffb51578ccdc44b5691bdd7cfb10567e9f22a5b Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Tue, 6 Aug 2024 19:51:11 +0530 Subject: [PATCH 384/582] ESCLASS-195: change model data filters implementation and dataset group validation status and get connected group logic revamp --- .../classifier-script-v9-models-metadata.sql | 3 + DSL/Resql/get-data-model-filters.sql | 12 +++- DSL/Resql/get-data-model-metadata-by-id.sql | 5 +- ...get-dataset-group-basic-metadata-by-id.sql | 8 +++ .../get-paginated-data-model-metadata.sql | 5 +- DSL/Resql/insert-model-metadata.sql | 10 ++- DSL/Resql/snapshot-major-data-model.sql | 6 ++ ...data-model-dataset-group-patch-version.sql | 4 ++ .../update-data-model-progress-session.sql | 3 +- DSL/Resql/update-dataset-progress-session.sql | 3 +- ...e-minor-dataset-group-validation-data.sql} | 3 +- ...te-patch-dataset-group-validation-data.sql | 5 ++ .../update-patch-version-dataset-group.sql | 4 -- ...pdate-patch-version-only-dataset-group.sql | 5 ++ .../DSL/GET/classifier/datamodel/overview.yml | 4 +- .../classifier/datasetgroup/stop-words.yml | 10 ++- .../DSL/POST/classifier/datamodel/create.yml | 47 ++++++++++-- .../classifier/datamodel/progress/update.yml | 5 ++ .../DSL/POST/classifier/datamodel/update.yml | 51 +++++++++++-- .../datasetgroup/progress/update.yml | 5 ++ .../datasetgroup/update/validation/status.yml | 72 ++++++++++++++++--- 21 files changed, 233 insertions(+), 37 deletions(-) create mode 100644 DSL/Resql/get-dataset-group-basic-metadata-by-id.sql create mode 100644 DSL/Resql/update-data-model-dataset-group-patch-version.sql rename DSL/Resql/{update-dataset-group-validation-data.sql => update-minor-dataset-group-validation-data.sql} (53%) create mode 100644 DSL/Resql/update-patch-dataset-group-validation-data.sql create mode 100644 DSL/Resql/update-patch-version-only-dataset-group.sql diff --git a/DSL/Liquibase/changelog/classifier-script-v9-models-metadata.sql b/DSL/Liquibase/changelog/classifier-script-v9-models-metadata.sql index d01ec384..a43f65ac 100644 --- a/DSL/Liquibase/changelog/classifier-script-v9-models-metadata.sql +++ b/DSL/Liquibase/changelog/classifier-script-v9-models-metadata.sql @@ -28,6 +28,9 @@ CREATE TABLE models_metadata ( created_timestamp TIMESTAMP WITH TIME ZONE, connected_dg_id INT, connected_dg_name TEXT, + connected_dg_major_version INT NOT NULL DEFAULT 0, + connected_dg_minor_version INT NOT NULL DEFAULT 0, + connected_dg_patch_version INT NOT NULL DEFAULT 0, model_s3_location TEXT, inference_routes JSONB, training_results JSONB, diff --git a/DSL/Resql/get-data-model-filters.sql b/DSL/Resql/get-data-model-filters.sql index 2d663837..ea341747 100644 --- a/DSL/Resql/get-data-model-filters.sql +++ b/DSL/Resql/get-data-model-filters.sql @@ -15,9 +15,17 @@ FROM ( array_agg(DISTINCT 'x.' || minor_version::TEXT ) FILTER (WHERE minor_version > 0) AS modelVersions, - array_agg(DISTINCT connected_dg_name) AS datasetGroups, + array_agg(DISTINCT + jsonb_build_object( + 'id', connected_dg_id, + 'name', connected_dg_name || ' ' || + connected_dg_major_version::TEXT || '.' || + connected_dg_minor_version::TEXT || '.' || + connected_dg_patch_version::TEXT + ) + ) AS datasetGroups, array_agg(DISTINCT deployment_env) AS deploymentsEnvs, array_agg(DISTINCT training_status) AS trainingStatuses, array_agg(DISTINCT maturity_label) AS maturityLabels FROM models_metadata -) AS subquery; \ No newline at end of file +) AS subquery; diff --git a/DSL/Resql/get-data-model-metadata-by-id.sql b/DSL/Resql/get-data-model-metadata-by-id.sql index 2f88d651..ece03d66 100644 --- a/DSL/Resql/get-data-model-metadata-by-id.sql +++ b/DSL/Resql/get-data-model-metadata-by-id.sql @@ -9,6 +9,9 @@ SELECT training_status, base_models, connected_dg_id, - connected_dg_name + connected_dg_name, + connected_dg_major_version, + connected_dg_minor_version, + connected_dg_patch_version FROM models_metadata WHERE id = :id; diff --git a/DSL/Resql/get-dataset-group-basic-metadata-by-id.sql b/DSL/Resql/get-dataset-group-basic-metadata-by-id.sql new file mode 100644 index 00000000..66f9b4a2 --- /dev/null +++ b/DSL/Resql/get-dataset-group-basic-metadata-by-id.sql @@ -0,0 +1,8 @@ +SELECT + id AS dg_id, + group_name, + major_version, + minor_version, + patch_version +FROM dataset_group_metadata +WHERE id = :id; diff --git a/DSL/Resql/get-paginated-data-model-metadata.sql b/DSL/Resql/get-paginated-data-model-metadata.sql index 13d30b12..c780be20 100644 --- a/DSL/Resql/get-paginated-data-model-metadata.sql +++ b/DSL/Resql/get-paginated-data-model-metadata.sql @@ -13,6 +13,9 @@ SELECT dt.created_timestamp, dt.connected_dg_id, dt.connected_dg_name, + dt.connected_dg_major_version, + dt.connected_dg_minor_version, + dt.connected_dg_patch_version, jsonb_pretty(dt.training_results) AS training_results, CEIL(COUNT(*) OVER() / :page_size::DECIMAL) AS total_pages FROM @@ -24,7 +27,7 @@ WHERE AND (:deployment_maturity = 'all' OR dt.maturity_label = :deployment_maturity::Maturity_Label) AND (:training_status = 'all' OR dt.training_status = :training_status::Training_Status) AND (:platform = 'all' OR dt.deployment_env = :platform::Deployment_Env) - AND (:dataset_group = 'all' OR dt.connected_dg_name = :dataset_group) + AND (:dataset_group = -1 OR dt.connected_dg_id = :dataset_group) ORDER BY CASE WHEN :sort_type = 'asc' THEN dt.model_name END ASC, CASE WHEN :sort_type = 'desc' THEN dt.model_name END DESC diff --git a/DSL/Resql/insert-model-metadata.sql b/DSL/Resql/insert-model-metadata.sql index 93e66bb6..15f41110 100644 --- a/DSL/Resql/insert-model-metadata.sql +++ b/DSL/Resql/insert-model-metadata.sql @@ -10,7 +10,10 @@ INSERT INTO models_metadata ( base_models, created_timestamp, connected_dg_id, - connected_dg_name + connected_dg_name, + connected_dg_major_version, + connected_dg_minor_version, + connected_dg_patch_version ) VALUES ( :model_group_key, :model_name, @@ -23,5 +26,8 @@ INSERT INTO models_metadata ( ARRAY [:base_models]::Base_Models[], :created_timestamp::timestamp with time zone, :connected_dg_id, - :connected_dg_name + :connected_dg_name, + :connected_dg_major_version, + :connected_dg_minor_version, + :connected_dg_patch_version ) RETURNING id; diff --git a/DSL/Resql/snapshot-major-data-model.sql b/DSL/Resql/snapshot-major-data-model.sql index 058a6413..41c4292a 100644 --- a/DSL/Resql/snapshot-major-data-model.sql +++ b/DSL/Resql/snapshot-major-data-model.sql @@ -12,6 +12,9 @@ INSERT INTO models_metadata ( created_timestamp, connected_dg_id, connected_dg_name, + connected_dg_major_version, + connected_dg_minor_version, + connected_dg_patch_version, model_s3_location, inference_routes, training_results @@ -34,6 +37,9 @@ SELECT created_timestamp, :connected_dg_id, :connected_dg_name, + :connected_dg_major_version, + :connected_dg_minor_version, + :connected_dg_patch_version, NULL AS model_s3_location, NULL AS inference_routes, NULL AS training_results diff --git a/DSL/Resql/update-data-model-dataset-group-patch-version.sql b/DSL/Resql/update-data-model-dataset-group-patch-version.sql new file mode 100644 index 00000000..d6d381b0 --- /dev/null +++ b/DSL/Resql/update-data-model-dataset-group-patch-version.sql @@ -0,0 +1,4 @@ +UPDATE models_metadata +SET + connected_dg_patch_version = connected_dg_patch_version + 1 +WHERE connected_dg_id = :dg_id; \ No newline at end of file diff --git a/DSL/Resql/update-data-model-progress-session.sql b/DSL/Resql/update-data-model-progress-session.sql index c2c213b3..13b296f3 100644 --- a/DSL/Resql/update-data-model-progress-session.sql +++ b/DSL/Resql/update-data-model-progress-session.sql @@ -2,5 +2,6 @@ UPDATE model_progress_sessions SET training_progress_status = :training_progress_status::Training_Progress_Status, training_message = :training_message, - progress_percentage = :progress_percentage + progress_percentage = :progress_percentage, + process_complete = :process_complete WHERE id = :id; \ No newline at end of file diff --git a/DSL/Resql/update-dataset-progress-session.sql b/DSL/Resql/update-dataset-progress-session.sql index 72d501b5..1514321b 100644 --- a/DSL/Resql/update-dataset-progress-session.sql +++ b/DSL/Resql/update-dataset-progress-session.sql @@ -2,5 +2,6 @@ UPDATE dataset_progress_sessions SET validation_status = :validation_status::Validation_Progress_Status, validation_message = :validation_message, - progress_percentage = :progress_percentage + progress_percentage = :progress_percentage, + process_complete = :process_complete WHERE id = :id; \ No newline at end of file diff --git a/DSL/Resql/update-dataset-group-validation-data.sql b/DSL/Resql/update-minor-dataset-group-validation-data.sql similarity index 53% rename from DSL/Resql/update-dataset-group-validation-data.sql rename to DSL/Resql/update-minor-dataset-group-validation-data.sql index 3f99f601..8c2f6a60 100644 --- a/DSL/Resql/update-dataset-group-validation-data.sql +++ b/DSL/Resql/update-minor-dataset-group-validation-data.sql @@ -1,5 +1,6 @@ UPDATE dataset_group_metadata SET validation_status = :validation_status::Validation_Status, - validation_errors = :validation_errors::jsonb + validation_errors = :validation_errors::jsonb, + last_updated_timestamp = CURRENT_TIMESTAMP WHERE id = :id; diff --git a/DSL/Resql/update-patch-dataset-group-validation-data.sql b/DSL/Resql/update-patch-dataset-group-validation-data.sql new file mode 100644 index 00000000..192db523 --- /dev/null +++ b/DSL/Resql/update-patch-dataset-group-validation-data.sql @@ -0,0 +1,5 @@ +UPDATE dataset_group_metadata +SET + validation_errors = :validation_errors::jsonb, + last_updated_timestamp = CURRENT_TIMESTAMP +WHERE id = :id; diff --git a/DSL/Resql/update-patch-version-dataset-group.sql b/DSL/Resql/update-patch-version-dataset-group.sql index 9d5f4784..ca39976b 100644 --- a/DSL/Resql/update-patch-version-dataset-group.sql +++ b/DSL/Resql/update-patch-version-dataset-group.sql @@ -7,10 +7,6 @@ WITH update_latest AS ( update_specific AS ( UPDATE dataset_group_metadata SET - patch_version = patch_version + 1, - enable_allowed = false, - validation_status = 'in-progress'::Validation_Status, - is_enabled = false, latest = true, last_updated_timestamp = :last_updated_timestamp::timestamp with time zone WHERE id = :id diff --git a/DSL/Resql/update-patch-version-only-dataset-group.sql b/DSL/Resql/update-patch-version-only-dataset-group.sql new file mode 100644 index 00000000..06871cfa --- /dev/null +++ b/DSL/Resql/update-patch-version-only-dataset-group.sql @@ -0,0 +1,5 @@ +UPDATE dataset_group_metadata + SET + patch_version = patch_version + 1, + last_updated_timestamp = CURRENT_TIMESTAMP +WHERE id = :id \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/GET/classifier/datamodel/overview.yml b/DSL/Ruuter.private/DSL/GET/classifier/datamodel/overview.yml index e6acc0a0..fe5b13b2 100644 --- a/DSL/Ruuter.private/DSL/GET/classifier/datamodel/overview.yml +++ b/DSL/Ruuter.private/DSL/GET/classifier/datamodel/overview.yml @@ -30,7 +30,7 @@ declaration: type: string description: "Parameter 'platform'" - field: datasetGroup - type: string + type: number description: "Parameter 'datasetGroup'" - field: trainingStatus type: string @@ -48,7 +48,7 @@ extract_data: major_version: ${Number(incoming.params.majorVersion)} minor_version: ${Number(incoming.params.minorVersion)} platform: ${incoming.params.platform} - dataset_group: ${incoming.params.datasetGroup} + dataset_group: ${Number(incoming.params.datasetGroup)} training_status: ${incoming.params.trainingStatus} deployment_maturity: ${incoming.params.deploymentMaturity} next: get_data_model_meta_data_overview diff --git a/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/stop-words.yml b/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/stop-words.yml index 41538a7d..ffa5fa45 100644 --- a/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/stop-words.yml +++ b/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/stop-words.yml @@ -24,7 +24,7 @@ check_data_exist: switch: - condition: ${res.response.body.length>0 && res.response.body[0].stopWordsArray !== null} next: assign_success_response - next: assign_fail_response + next: assign_empty_response assign_success_response: assign: @@ -34,6 +34,14 @@ assign_success_response: } next: return_ok +assign_empty_response: + assign: + format_res: { + operationSuccessful: true, + stopWords: '${[]}' + } + next: return_ok + assign_fail_response: assign: format_res: { diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/create.yml b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/create.yml index 79f2c517..3f16502a 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/create.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/create.yml @@ -11,9 +11,6 @@ declaration: - field: modelName type: string description: "Body field 'modelName'" - - field: datasetGroupName - type: string - description: "Body field 'datasetGroupName'" - field: dgId type: number description: "Body field 'dgId'" @@ -34,7 +31,6 @@ declaration: extract_request_data: assign: model_name: ${incoming.body.modelName} - dataset_group_name: ${incoming.body.datasetGroupName} dg_id: ${incoming.body.dgId} base_models: ${incoming.body.baseModels} deployment_platform: ${incoming.body.deploymentPlatform} @@ -43,10 +39,39 @@ extract_request_data: check_for_request_data: switch: - - condition: ${model_name !== null && dataset_group_name !== null && dg_id !== null && base_models !== null && deployment_platform !== null && maturity_label !== null} - next: get_epoch_date + - condition: ${model_name !== null && dg_id !== null && base_models !== null && deployment_platform !== null && maturity_label !== null} + next: get_dataset_group_data next: return_incorrect_request +get_dataset_group_data: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-dataset-group-basic-metadata-by-id" + body: + id: ${dg_id} + result: res_dataset_group + next: check_dataset_group_status + +check_dataset_group_status: + switch: + - condition: ${200 <= res_dataset_group.response.statusCodeValue && res_dataset_group.response.statusCodeValue < 300} + next: check_dataset_group_exist + next: assign_fail_response + +check_dataset_group_exist: + switch: + - condition: ${res_dataset_group.response.body.length>0} + next: assign_dataset_group_data + next: return_dataset_group_not_found + +assign_dataset_group_data: + assign: + dataset_group_name: ${res_dataset_group.response.body[0].groupName} + dg_major_version: ${res_dataset_group.response.body[0].majorVersion} + dg_minor_version: ${res_dataset_group.response.body[0].minorVersion} + dg_patch_version: ${res_dataset_group.response.body[0].patchVersion} + next: get_epoch_date + get_epoch_date: assign: current_epoch: ${Date.now()} @@ -60,8 +85,11 @@ create_model_metadata: body: model_name: ${model_name} model_group_key: "${random_num+ '_'+current_epoch}" - connected_dg_name: ${dataset_group_name} connected_dg_id: ${dg_id} + connected_dg_name: ${dataset_group_name} + connected_dg_major_version: ${dg_major_version} + connected_dg_minor_version: ${dg_minor_version} + connected_dg_patch_version: ${dg_patch_version} base_models: ${base_models} deployment_env: ${deployment_platform} maturity_label: ${maturity_label} @@ -164,6 +192,11 @@ return_incorrect_request: return: 'Missing Required Fields' next: end +return_dataset_group_not_found: + status: 404 + return: "Dataset Group Not Found" + next: end + return_not_found: status: 404 return: "Model Not Found" diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/progress/update.yml b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/progress/update.yml index f3d5b47d..64a9d4c3 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/progress/update.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/progress/update.yml @@ -20,6 +20,9 @@ declaration: - field: progressPercentage type: number description: "Body field 'progressPercentage'" + - field: processComplete + type: boolean + description: "Body field 'processComplete'" extract_request_data: assign: @@ -27,6 +30,7 @@ extract_request_data: training_status: ${incoming.body.trainingStatus} training_message: ${incoming.body.trainingMessage} progress_percentage: ${incoming.body.progressPercentage} + process_complete: ${incoming.body.processComplete} next: check_for_request_data check_for_request_data: @@ -64,6 +68,7 @@ update_data_model_progress_session: training_progress_status: ${training_status} progress_percentage: ${progress_percentage} training_message: ${training_message} + process_complete: ${process_complete} result: res next: check_status diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update.yml b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update.yml index 57feb53e..08d1de3f 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update.yml @@ -14,9 +14,6 @@ declaration: - field: connectedDgId type: number description: "Body field 'connectedDgId'" - - field: connectedDgName - type: string - description: "Body field 'connectedDgName'" - field: deploymentEnv type: string description: "Body field 'deploymentEnv'" @@ -38,23 +35,57 @@ extract_request_data: assign: model_id: ${incoming.body.modelId} connected_dg_id: ${incoming.body.connectedDgId} - connected_dg_name: ${incoming.body.connectedDgName} deployment_env: ${incoming.body.deploymentEnv} base_models: ${incoming.body.baseModels} maturity_label: ${incoming.body.maturityLabel} update_type: ${incoming.body.updateType} next: check_event_type +check_for_request_data: + switch: + - condition: ${model_id !== null && update_type !== null} + next: check_event_type + next: return_incorrect_request + check_event_type: switch: - condition: ${update_type == 'major'} - next: get_data_model_major_data + next: get_dataset_group_data - condition: ${update_type == 'minor'} next: get_data_model_minor_data - condition: ${update_type == 'maturityLabel'} next: check_for_maturity_request_data next: return_type_found +get_dataset_group_data: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-dataset-group-basic-metadata-by-id" + body: + id: ${connected_dg_id} + result: res_dataset_group + next: check_dataset_group_status + +check_dataset_group_status: + switch: + - condition: ${200 <= res_dataset_group.response.statusCodeValue && res_dataset_group.response.statusCodeValue < 300} + next: check_dataset_group_exist + next: assign_fail_response + +check_dataset_group_exist: + switch: + - condition: ${res_dataset_group.response.body.length>0} + next: assign_dataset_group_data + next: return_dataset_group_not_found + +assign_dataset_group_data: + assign: + dataset_group_name: ${res_dataset_group.response.body[0].groupName} + dg_major_version: ${res_dataset_group.response.body[0].majorVersion} + dg_minor_version: ${res_dataset_group.response.body[0].minorVersion} + dg_patch_version: ${res_dataset_group.response.body[0].patchVersion} + next: get_data_model_major_data + get_data_model_major_data: call: http.post args: @@ -115,7 +146,10 @@ snapshot_major_data_model: id: ${model_id} group_key: ${group_key} connected_dg_id: ${connected_dg_id} - connected_dg_name: ${connected_dg_name} + connected_dg_name: ${dataset_group_name} + connected_dg_major_version: ${dg_major_version} + connected_dg_minor_version: ${dg_minor_version} + connected_dg_patch_version: ${dg_patch_version} deployment_env: ${deployment_env == null ? deployment_env_prev :deployment_env } base_models: ${base_models == null ? base_models_prev :base_models} maturity_label: ${maturity_label == null ? 'development' :maturity_label} @@ -373,6 +407,11 @@ return_type_found: return: "Update Type Not Found" next: end +return_dataset_group_not_found: + status: 404 + return: "Dataset Group Not Found" + next: end + return_bad_request: status: 400 return: ${format_res} diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/progress/update.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/progress/update.yml index 56798d97..45fbc13a 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/progress/update.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/progress/update.yml @@ -20,6 +20,9 @@ declaration: - field: progressPercentage type: number description: "Body field 'progressPercentage'" + - field: processComplete + type: boolean + description: "Body field 'processComplete'" extract_request_data: assign: @@ -27,6 +30,7 @@ extract_request_data: validation_status: ${incoming.body.validationStatus} validation_message: ${incoming.body.validationMessage} progress_percentage: ${incoming.body.progressPercentage} + process_complete: ${incoming.body.processComplete} next: check_for_request_data check_for_request_data: @@ -64,6 +68,7 @@ update_dataset_progress_session: validation_status: ${validation_status} progress_percentage: ${progress_percentage} validation_message: ${validation_message} + process_complete: ${process_complete} result: res next: check_status diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/validation/status.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/validation/status.yml index 0dd27f2e..ff9d3203 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/validation/status.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/validation/status.yml @@ -29,6 +29,10 @@ declaration: - field: validationErrors type: array description: "Body field 'validationErrors'" + headers: + - field: cookie + type: string + description: "Cookie field" extract_request_data: assign: @@ -52,36 +56,88 @@ check_request_type: assign_patch_type: assign: active_dg_id: ${incoming.body.dgId} - next: update_dataset_group_validation + next: update_patch_dataset_group_validation assign_minor_type: assign: active_dg_id: ${incoming.body.newDgId} - next: update_dataset_group_validation + next: update_minor_dataset_group_validation -update_dataset_group_validation: +update_minor_dataset_group_validation: call: http.post args: - url: "[#CLASSIFIER_RESQL]/update-dataset-group-validation-data" + url: "[#CLASSIFIER_RESQL]/update-minor-dataset-group-validation-data" body: id: ${active_dg_id} validation_status: ${validation_status} validation_errors: ${JSON.stringify(validation_errors)} result: res - next: check_status + next: check_minor_status -check_status: +check_minor_status: switch: - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} - next: check_validation_status_type + next: check_minor_validation_status_type next: assign_fail_response -check_validation_status_type: +check_minor_validation_status_type: switch: - condition: ${validation_status === 'success'} next: execute_cron_manager next: assign_success_response +update_patch_dataset_group_validation: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/update-patch-dataset-group-validation-data" + body: + id: ${active_dg_id} + validation_errors: ${JSON.stringify(validation_errors)} + result: res + next: check_patch_status + +check_patch_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: check_patch_validation_status_type + next: assign_fail_response + +check_patch_validation_status_type: + switch: + - condition: ${validation_status === 'success'} + next: update_patch_version_only + next: assign_success_response + +update_patch_version_only: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/update-patch-version-only-dataset-group" + body: + id: ${dg_id} + result: res + next: check_patch_version_only_status + +check_patch_version_only_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: update_patch_version_in_model_metadata + next: assign_fail_response + +update_patch_version_in_model_metadata: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/update-data-model-dataset-group-patch-version" + body: + dg_id: ${dg_id} + result: res + next: check_patch_version_in_model_metadata_status + +check_patch_version_in_model_metadata_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: execute_cron_manager + next: assign_fail_response + execute_cron_manager: call: http.post args: From bb4ce23b13babea9fd063d7bbc94f09195371013 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Tue, 6 Aug 2024 23:12:30 +0530 Subject: [PATCH 385/582] Deployment-platform issue: deployment platform create and update not update all other existing platform issue fixed --- DSL/Resql/snapshot-major-data-model.sql | 2 +- DSL/Resql/snapshot-minor-data-model.sql | 6 + .../update-models-deployment-platform.sql | 4 + .../DSL/POST/classifier/datamodel/create.yml | 24 +++- .../DSL/POST/classifier/datamodel/update.yml | 88 +++++------- .../datamodel/update/maturity-label.yml | 75 ---------- .../classifier/datamodel/update/minor.yml | 133 ------------------ 7 files changed, 73 insertions(+), 259 deletions(-) create mode 100644 DSL/Resql/update-models-deployment-platform.sql delete mode 100644 DSL/Ruuter.private/DSL/POST/classifier/datamodel/update/maturity-label.yml delete mode 100644 DSL/Ruuter.private/DSL/POST/classifier/datamodel/update/minor.yml diff --git a/DSL/Resql/snapshot-major-data-model.sql b/DSL/Resql/snapshot-major-data-model.sql index 41c4292a..2c3818d2 100644 --- a/DSL/Resql/snapshot-major-data-model.sql +++ b/DSL/Resql/snapshot-major-data-model.sql @@ -14,7 +14,7 @@ INSERT INTO models_metadata ( connected_dg_name, connected_dg_major_version, connected_dg_minor_version, - connected_dg_patch_version, + connected_dg_patch_version, model_s3_location, inference_routes, training_results diff --git a/DSL/Resql/snapshot-minor-data-model.sql b/DSL/Resql/snapshot-minor-data-model.sql index 90568453..dc2d3b33 100644 --- a/DSL/Resql/snapshot-minor-data-model.sql +++ b/DSL/Resql/snapshot-minor-data-model.sql @@ -12,6 +12,9 @@ INSERT INTO models_metadata ( created_timestamp, connected_dg_id, connected_dg_name, + connected_dg_major_version, + connected_dg_minor_version, + connected_dg_patch_version, model_s3_location, inference_routes, training_results @@ -34,6 +37,9 @@ SELECT created_timestamp, connected_dg_id, connected_dg_name, + connected_dg_major_version, + connected_dg_minor_version, + connected_dg_patch_version, NULL AS model_s3_location, NULL AS inference_routes, NULL AS training_results diff --git a/DSL/Resql/update-models-deployment-platform.sql b/DSL/Resql/update-models-deployment-platform.sql new file mode 100644 index 00000000..149100fb --- /dev/null +++ b/DSL/Resql/update-models-deployment-platform.sql @@ -0,0 +1,4 @@ +UPDATE models_metadata +SET + deployment_env = :updating_platform::Deployment_Env +WHERE deployment_env = :existing_platform::Deployment_Env \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/create.yml b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/create.yml index 3f16502a..1d4cdfb4 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/create.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/create.yml @@ -76,7 +76,29 @@ get_epoch_date: assign: current_epoch: ${Date.now()} random_num: ${Math.floor(Math.random() * 100000)} - next: create_model_metadata + next: check_deployment_platform + +check_deployment_platform: + switch: + - condition: ${deployment_platform == 'jira' || deployment_platform == 'outlook' || deployment_platform == 'pinal'} + next: update_existing_models_deployment_platform + next: assign_fail_response + +update_existing_models_deployment_platform: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/update-models-deployment-platform" + body: + updating_platform: 'undeployed' + existing_platform: ${deployment_platform} + result: res_update + next: check_deployment_platform__status + +check_deployment_platform__status: + switch: + - condition: ${200 <= res_update.response.statusCodeValue && res_update.response.statusCodeValue < 300} + next: create_model_metadata + next: assign_fail_response create_model_metadata: call: http.post diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update.yml b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update.yml index 08d1de3f..54581eb5 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update.yml @@ -127,6 +127,35 @@ update_latest_in_old_versions: check_latest_status: switch: - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: set_deployment_env + next: assign_fail_response + +set_deployment_env: + assign: + deployment_env_data_set: ${deployment_env == null ? deployment_env_prev :deployment_env } + next: check_deployment_platform + +check_deployment_platform: + switch: + - condition: ${deployment_env_data_set == 'jira' || deployment_env_data_set == 'outlook'|| deployment_env_data_set == 'pinal' } + next: update_existing_models_deployment_platform + - condition: ${deployment_env_data_set == 'testing' || deployment_env_data_set == 'undeployed' } + next: check_event_type_for_latest_version_again + next: return_deployment_type_found + +update_existing_models_deployment_platform: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/update-models-deployment-platform" + body: + updating_platform: 'undeployed' + existing_platform: ${deployment_env_data_set} + result: res_update + next: check_deployment_platform__status + +check_deployment_platform__status: + switch: + - condition: ${200 <= res_update.response.statusCodeValue && res_update.response.statusCodeValue < 300} next: check_event_type_for_latest_version_again next: assign_fail_response @@ -156,12 +185,6 @@ snapshot_major_data_model: result: res next: check_snapshot_status -check_major_previous_deployment_env: - switch: - - condition: ${deployment_env == deployment_env_prev && (deployment_env_prev == 'jira' || deployment_env_prev == 'outlook' || deployment_env_prev == 'pinal')} - next: update_data_model_deployment_env - next: get_major_model_data_by_id - get_data_model_minor_data: call: http.post args: @@ -194,13 +217,9 @@ assign_minor_day: check_minor_data_request_status: switch: - - condition: ${deployment_env == null && (deployment_env_prev == 'jira' || deployment_env_prev == 'outlook' || deployment_env_prev == 'pinal')} - next: assign_previous_deployment_env_and_update_old - - condition: ${deployment_env == null && (deployment_env_prev == 'testing' || deployment_env_prev == 'undeployed')} + - condition: ${deployment_env == null } next: assign_previous_deployment_env - - condition: ${deployment_env !== null && deployment_env == deployment_env_prev && (deployment_env_prev == 'jira' || deployment_env_prev == 'outlook' || deployment_env_prev == 'pinal')} - next: assign_new_deployment_env_and_update_old - - condition: ${deployment_env !== null && deployment_env !== deployment_env_prev} + - condition: ${deployment_env !== null} next: assign_new_deployment_env next: assign_fail_response @@ -209,45 +228,11 @@ assign_new_deployment_env: deployment_env_data: ${deployment_env} next: update_latest_in_old_versions -assign_previous_deployment_env_and_update_old: - assign: - deployment_env_data: ${deployment_env_prev} - next: update_data_model_deployment_env - -assign_new_deployment_env_and_update_old: - assign: - deployment_env_data: ${deployment_env} - next: update_data_model_deployment_env - assign_previous_deployment_env: assign: deployment_env_data: ${deployment_env_prev} next: update_latest_in_old_versions -update_data_model_deployment_env: - call: http.post - args: - url: "[#CLASSIFIER_RESQL]/update-data-model-deployment-env" - body: - id: ${model_id} - deployment_env: testing - result: res - next: check_data_model_deployment_env_status - -check_data_model_deployment_env_status: - switch: - - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} - next: check_event_type_again - next: assign_fail_response - -check_event_type_again: - switch: - - condition: ${update_type == 'major'} - next: get_major_model_data_by_id - - condition: ${update_type == 'minor'} - next: update_latest_in_old_versions - next: return_type_found - get_major_model_data_by_id: call: http.post args: @@ -314,12 +299,12 @@ check_updated_data_exist: assign_new_model_id: assign: new_model_id: ${res.response.body[0].id} - next: check_event_type_again_again + next: check_event_type_again -check_event_type_again_again: +check_event_type_again: switch: - condition: ${update_type == 'major'} - next: check_major_previous_deployment_env + next: get_major_model_data_by_id - condition: ${update_type == 'minor'} next: execute_cron_manager next: return_type_found @@ -407,6 +392,11 @@ return_type_found: return: "Update Type Not Found" next: end +return_deployment_type_found: + status: 400 + return: "Deployment Platform Type Not Found" + next: end + return_dataset_group_not_found: status: 404 return: "Dataset Group Not Found" diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update/maturity-label.yml b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update/maturity-label.yml deleted file mode 100644 index cc280981..00000000 --- a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update/maturity-label.yml +++ /dev/null @@ -1,75 +0,0 @@ -declaration: - call: declare - version: 0.1 - description: "Description placeholder for 'MATURITY-LABEL'" - method: post - accepts: json - returns: json - namespace: classifier - allowlist: - body: - - field: modelId - type: number - description: "Body field 'modelId'" - - field: maturityLabel - type: string - description: "Body field 'maturityLabel'" - -extract_request_data: - assign: - model_id: ${incoming.body.modelId} - maturity_label: ${incoming.body.maturityLabel} - next: check_for_request_data - -check_for_request_data: - switch: - - condition: ${model_id !== null && maturity_label !== null} - next: update_data_model - next: return_incorrect_request - -update_data_model: - call: http.post - args: - url: "[#CLASSIFIER_RESQL]/update-data-model-maturity-label" - body: - id: ${model_id} - maturity_label: ${maturity_label} - result: res_update - next: check_model_update_status - -check_model_update_status: - switch: - - condition: ${200 <= res_update.response.statusCodeValue && res_update.response.statusCodeValue < 300} - next: assign_success_response - next: assign_fail_response - -assign_success_response: - assign: - format_res: { - modelId: '${model_id}', - operationSuccessful: true, - } - next: return_ok - -assign_fail_response: - assign: - format_res: { - modelId: '', - operationSuccessful: false, - } - next: return_bad_request - -return_ok: - status: 200 - return: ${format_res} - next: end - -return_incorrect_request: - status: 400 - return: 'Missing Required Fields' - next: end - -return_bad_request: - status: 400 - return: ${format_res} - next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update/minor.yml b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update/minor.yml deleted file mode 100644 index 7fdb2e55..00000000 --- a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update/minor.yml +++ /dev/null @@ -1,133 +0,0 @@ -declaration: - call: declare - version: 0.1 - description: "Description placeholder for 'MINOR'" - method: post - accepts: json - returns: json - namespace: classifier - allowlist: - body: - - field: modelId - type: number - description: "Body field 'modelId'" - - field: deploymentEnv - type: string - description: "Body field 'deploymentEnv'" - - field: baseModels - type: array - description: "Body field 'baseModels'" - headers: - - field: cookie - type: string - description: "Cookie field" - -extract_request_data: - assign: - model_id: ${incoming.body.modelId} - deployment_env: ${incoming.body.deploymentEnv} - base_models: ${incoming.body.baseModels} - next: get_data_model - -get_data_model: - call: http.post - args: - url: "[#CLASSIFIER_RESQL]/get-data-model-group-key-by-id" - body: - id: ${model_id} - result: res - next: check_data_model_status - -check_data_model_status: - switch: - - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} - next: check_data_exist - next: return_not_found - -check_data_exist: - switch: - - condition: ${res.response.body.length>0} - next: assign_group_key - next: return_not_found - -assign_group_key: - assign: - group_key: ${res.response.body[0].modelGroupKey} - major_version: ${res.response.body[0].majorVersion} - next: snapshot_dataset_group - -snapshot_dataset_group: - call: http.post - args: - url: "[#CLASSIFIER_RESQL]/snapshot-minor-data-model" - body: - id: ${model_id} - group_key: ${group_key} - major_version: ${major_version} - deployment_env: ${deployment_env} - base_models: ${base_models} - result: res - next: check_snapshot_status - -check_snapshot_status: - switch: - - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} - next: check_updated_data_exist - next: assign_fail_response - -check_updated_data_exist: - switch: - - condition: ${res.response.body.length>0} - next: assign_new_model_id - next: return_not_found - -assign_new_model_id: - assign: - new_model_id: ${res.response.body[0].id} - next: execute_cron_manager - -execute_cron_manager: - call: reflect.mock - args: - url: "[#CLASSIFIER_CRON_MANAGER]/execute/data_model/model_trainer" - query: - cookie: ${incoming.headers.cookie} - model_id: ${model_id} - new_model_id: ${new_model_id} - updateType: 'minor' - result: res - next: assign_success_response - -assign_success_response: - assign: - format_res: { - model_id: '${model_id}', - new_model_id: '${new_model_id}', - operationSuccessful: true, - } - next: return_ok - -assign_fail_response: - assign: - format_res: { - model_id: '${model_id}', - new_model_id: '', - operationSuccessful: false, - } - next: return_bad_request - -return_ok: - status: 200 - return: ${format_res} - next: end - -return_not_found: - status: 400 - return: "Data Group Not Found" - next: end - -return_bad_request: - status: 400 - return: ${format_res} - next: end - From 3e6b0b30e796828c8b60693db37bc7d03c0f2e7d Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 7 Aug 2024 00:40:39 +0530 Subject: [PATCH 386/582] dataset validation patch update --- dataset-processor/dataset_processor_api.py | 70 +++++++----- dataset-processor/dataset_validator.py | 121 ++++++++++++++++++++- 2 files changed, 160 insertions(+), 31 deletions(-) diff --git a/dataset-processor/dataset_processor_api.py b/dataset-processor/dataset_processor_api.py index 2440c60d..0bed4a5a 100644 --- a/dataset-processor/dataset_processor_api.py +++ b/dataset-processor/dataset_processor_api.py @@ -62,31 +62,51 @@ async def forward_request(request: Request, response: Response): except Exception as e: raise HTTPException(status_code=400, detail=f"Invalid JSON payload: {str(e)}") - validator_response = validator.process_request(int(payload["dgId"]), payload["cookie"], payload["updateType"], payload["savedFilePath"]) + validator_response = validator.process_request(int(payload["dgId"]), payload["cookie"], payload["updateType"], payload["savedFilePath"], payload["patchPayload"]) + forward_payload = {} + forward_payload["dgId"] = int(payload["dgId"]) + forward_payload["newDgId"] = int(payload["newDgId"]) + forward_payload["updateType"] = payload["updateType"] + forward_payload["patchPayload"] = payload["patchPayload"] + forward_payload["savedFilePath"] = payload["savedFilePath"] + + headers = { + 'cookie': payload["cookie"], + 'Content-Type': 'application/json' + } if validator_response["response"]["operationSuccessful"] != True: - return False + forward_payload["validationStatus"] = "failed" + forward_payload["validationErrors"] = [validator_response["response"]["message"]] else: - headers = { - 'cookie': payload["cookie"], - 'Content-Type': 'application/json' - } + forward_payload["validationStatus"] = "success" + forward_payload["validationErrors"] = [] - payload2 = {} - payload2["dgId"] = int(payload["dgId"]) - payload2["newDgId"] = int(payload["newDgId"]) - payload2["updateType"] = payload["updateType"] - payload2["patchPayload"] = payload["patchPayload"] - payload2["savedFilePath"] = payload["savedFilePath"] - payload2["validationStatus"] = "success" - payload2["validationErrors"] = [] - try: - forward_response = requests.post(VALIDATION_CONFIRMATION_URL, json=payload2, headers=headers) - forward_response.raise_for_status() - - return JSONResponse(content=forward_response.json(), status_code=forward_response.status_code) - except requests.HTTPError as e: - print(e) - raise HTTPException(status_code=e.response.status_code, detail=e.response.text) - except Exception as e: - print(e) - raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file + try: + forward_response = requests.post(VALIDATION_CONFIRMATION_URL, json=forward_payload, headers=headers) + forward_response.raise_for_status() + + return JSONResponse(content=forward_response.json(), status_code=forward_response.status_code) + except requests.HTTPError as e: + forward_payload = {} + forward_payload["dgId"] = int(payload["dgId"]) + forward_payload["newDgId"] = int(payload["newDgId"]) + forward_payload["updateType"] = payload["updateType"] + forward_payload["patchPayload"] = payload["patchPayload"] + forward_payload["savedFilePath"] = payload["savedFilePath"] + forward_payload["validationStatus"] = "failed" + forward_payload["validationErrors"] = [e] + forward_response = requests.post(VALIDATION_CONFIRMATION_URL, json=forward_payload, headers=headers) + print(e) + raise HTTPException(status_code=e.response.status_code, detail=e.response.text) + except Exception as e: + forward_payload = {} + forward_payload["dgId"] = int(payload["dgId"]) + forward_payload["newDgId"] = int(payload["newDgId"]) + forward_payload["updateType"] = payload["updateType"] + forward_payload["patchPayload"] = payload["patchPayload"] + forward_payload["savedFilePath"] = payload["savedFilePath"] + forward_payload["validationStatus"] = "failed" + forward_payload["validationErrors"] = [e] + forward_response = requests.post(VALIDATION_CONFIRMATION_URL, json=forward_payload, headers=headers) + print(e) + raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file diff --git a/dataset-processor/dataset_validator.py b/dataset-processor/dataset_validator.py index 70a07352..0797e537 100644 --- a/dataset-processor/dataset_validator.py +++ b/dataset-processor/dataset_validator.py @@ -9,13 +9,13 @@ class DatasetValidator: def __init__(self): pass - def process_request(self, dgId, cookie, updateType, savedFilePath): + def process_request(self, dgId, cookie, updateType, savedFilePath, patchPayload=None): print("Process request started") print(f"dgId: {dgId}, updateType: {updateType}, savedFilePath: {savedFilePath}") if updateType == "minor": return self.handle_minor_update(dgId, cookie, savedFilePath) elif updateType == "patch": - return self.handle_patch_update() + return self.handle_patch_update(dgId, cookie, patchPayload) else: return self.generate_response(False, "Unknown update type") @@ -53,10 +53,6 @@ def handle_minor_update(self, dgId, cookie, savedFilePath): print(f"Internal error: {e}") return self.generate_response(False, f"Internal error: {e}") - def handle_patch_update(self): - print("Handling patch update") - return self.generate_response(True, "Patch update processed successfully") - def get_dataset_by_location(self, fileLocation, custom_jwt_cookie): print("Downloading dataset by location") params = {'saveLocation': fileLocation} @@ -180,6 +176,119 @@ def extract_data_class_values(self, data, columns): print(f"Data class values extracted: {values}") return values + def handle_patch_update(self, dgId, cookie, patchPayload): + print("Handling patch update") + min_label_value = 1 + + try: + validation_criteria, class_hierarchy = self.get_validation_criteria(dgId, cookie) + if validation_criteria is None: + return self.generate_response(False, "Failed to get validation criteria") + + if patchPayload is None: + return self.generate_response(False, "No patch payload provided") + + decoded_patch_payload = urllib.parse.unquote(patchPayload) + patch_payload_dict = json.loads(decoded_patch_payload) + + edited_data = patch_payload_dict.get("editedData", []) + + if edited_data: + for row in edited_data: + row_id = row.pop("rowId", None) + if row_id is None: + return self.generate_response(False, "Missing rowId in edited data") + + for key, value in row.items(): + if key not in validation_criteria['validationRules']: + return self.generate_response(False, f"Invalid field: {key}") + + if not self.validate_value(value, validation_criteria['validationRules'][key]['type']): + return self.generate_response(False, f"Validation failed for field type '{key}' in row {row_id}") + + data_class_columns = [field for field, rules in validation_criteria['validationRules'].items() if rules.get('isDataClass', False)] + hierarchy_values = self.extract_hierarchy_values(class_hierarchy) + for row in edited_data: + for col in data_class_columns: + if row.get(col) and row[col] not in hierarchy_values: + return self.generate_response(False, f"New class '{row[col]}' does not exist in the schema hierarchy") + + aggregated_data = self.get_dataset_by_location(f"/dataset/{dgId}/primary_dataset/dataset_{dgId}_aggregated.json", cookie) + if aggregated_data is None: + return self.generate_response(False, "Failed to download aggregated dataset") + + if not self.check_label_counts(aggregated_data, edited_data, data_class_columns, min_label_value): + return self.generate_response(False, "Editing this data will cause the dataset to have insufficient data examples for one or more labels.") + + deleted_data_rows = patch_payload_dict.get("deletedDataRows", []) + if deleted_data_rows: + if 'aggregated_data' not in locals(): + aggregated_data = self.get_dataset_by_location(f"/dataset/{dgId}/primary_dataset/dataset_{dgId}_aggregated.json", cookie) + if aggregated_data is None: + return self.generate_response(False, "Failed to download aggregated dataset") + + if not self.check_label_counts_after_deletion(aggregated_data, deleted_data_rows, data_class_columns, min_label_value): + return self.generate_response(False, "Deleting this data will cause the dataset to have insufficient data examples for one or more labels.") + + return self.generate_response(True, "Patch update processed successfully") + + except Exception as e: + print(f"Internal error: {e}") + return self.generate_response(False, f"Internal error: {e}") + + def check_label_counts(self, aggregated_data, edited_data, data_class_columns, min_label_value): + # Aggregate data class values from edited data + edited_values = {col: set() for col in data_class_columns} + for row in edited_data: + for col in data_class_columns: + if col in row: + edited_values[col].add(row[col]) + + # Aggregate counts from the existing dataset + class_counts = {col: {} for col in data_class_columns} + for row in aggregated_data: + for col in data_class_columns: + value = row.get(col) + if value: + class_counts[col][value] = class_counts[col].get(value, 0) + 1 + + # Add counts from the edited data + for col, values in edited_values.items(): + for value in values: + class_counts[col][value] = class_counts[col].get(value, 0) + 1 + + # Check the counts against min_label_value + for col, counts in class_counts.items(): + for value, count in counts.items(): + if count < min_label_value: + return False + return True + + def check_label_counts_after_deletion(self, aggregated_data, deleted_data_rows, data_class_columns, min_label_value): + # Aggregate counts from the existing dataset + class_counts = {col: {} for col in data_class_columns} + for row in aggregated_data: + for col in data_class_columns: + value = row.get(col) + if value: + class_counts[col][value] = class_counts[col].get(value, 0) + 1 + + # Subtract counts from the deleted data + for row_id in deleted_data_rows: + for row in aggregated_data: + if row.get('rowId') == row_id: + for col in data_class_columns: + value = row.get(col) + if value: + class_counts[col][value] = class_counts[col].get(value, 0) - 1 + + # Check the counts against min_label_value + for col, counts in class_counts.items(): + for value, count in counts.items(): + if count < min_label_value: + return False + return True + def generate_response(self, success, message): print(f"Generating response: success={success}, message={message}") return { From 9b4dcf6406bf55629b35a3e88e28e0e4d0da9d74 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Wed, 7 Aug 2024 10:33:59 +0530 Subject: [PATCH 387/582] data models update int --- GUI/src/components/MainNavigation/index.tsx | 4 +- .../molecules/DataModelForm/index.tsx | 11 +- .../pages/DataModels/ConfigureDataModel.tsx | 113 +++++++-- GUI/src/pages/DataModels/CreateDataModel.tsx | 33 ++- GUI/src/pages/DataModels/index.tsx | 238 ++++++++++-------- .../pages/DatasetGroups/DatasetGroups.scss | 1 - GUI/src/pages/DatasetGroups/index.tsx | 4 +- GUI/src/services/data-models.ts | 10 +- GUI/src/services/sse-service.ts | 2 +- GUI/src/utils/dataModelsUtils.ts | 21 +- 10 files changed, 282 insertions(+), 155 deletions(-) diff --git a/GUI/src/components/MainNavigation/index.tsx b/GUI/src/components/MainNavigation/index.tsx index c6906827..c6bceb8b 100644 --- a/GUI/src/components/MainNavigation/index.tsx +++ b/GUI/src/components/MainNavigation/index.tsx @@ -98,7 +98,9 @@ const MainNavigation: FC = () => { return res?.data?.response; }, onSuccess: (res) => { - const role = res[0]; + console.log(res); + + const role = res?.includes(ROLES.ROLE_ADMINISTRATOR) ? ROLES.ROLE_ADMINISTRATOR :ROLES.ROLE_MODEL_TRAINER const filteredItems = filterItemsByRole(role, items); setMenuItems(filteredItems); }, diff --git a/GUI/src/components/molecules/DataModelForm/index.tsx b/GUI/src/components/molecules/DataModelForm/index.tsx index 8826e4d5..aad4d865 100644 --- a/GUI/src/components/molecules/DataModelForm/index.tsx +++ b/GUI/src/components/molecules/DataModelForm/index.tsx @@ -15,13 +15,15 @@ import { customFormattedArray } from 'utils/dataModelsUtils'; type DataModelFormType = { dataModel: any; handleChange: (name: string, value: any) => void; - errors: Record; + errors?: Record; + type: string; }; const DataModelForm: FC = ({ dataModel, handleChange, errors, + type }) => { const { t } = useTranslation(); @@ -33,7 +35,7 @@ const DataModelForm: FC = ({ return (
    -
    + {type==="create" ? (
    = ({
    Model Version
    +
    ):( +
    +
    {dataModel.modelName}
    +
    + )} {createOptions && (
    diff --git a/GUI/src/pages/DataModels/ConfigureDataModel.tsx b/GUI/src/pages/DataModels/ConfigureDataModel.tsx index 46731da6..f68f6ef3 100644 --- a/GUI/src/pages/DataModels/ConfigureDataModel.tsx +++ b/GUI/src/pages/DataModels/ConfigureDataModel.tsx @@ -1,10 +1,10 @@ import { FC, useState } from 'react'; -import { useQuery } from '@tanstack/react-query'; +import { useMutation, useQuery } from '@tanstack/react-query'; import { Link, useNavigate } from 'react-router-dom'; import { Button, Card } from 'components'; import { useDialog } from 'hooks/useDialog'; import BackArrowButton from 'assets/BackArrowButton'; -import { getMetadata } from 'services/data-models'; +import { getMetadata, updateDataModel } from 'services/data-models'; import DataModelForm from 'components/molecules/DataModelForm'; import { getChangedAttributes, validateDataModel } from 'utils/dataModelsUtils'; import { Platform } from 'enums/dataModelsEnums'; @@ -68,33 +68,95 @@ const ConfigureDataModel: FC = ({ id }) => { })); }; - const [errors, setErrors] = useState({ - modelName: '', - dgName: '', - platform: '', - baseModels: '', - maturity: '', - }); - - const validateData = () => { - const validationErrors = validateDataModel(dataModel); - setErrors(validationErrors); - return Object.keys(validationErrors)?.length === 0; - }; - const handleSave = () => { const payload = getChangedAttributes(initialData, dataModel); - + let updateType; + if (payload.dgId || payload.dgName) { + updateType = 'major'; + } else if (payload.baseModels || payload.platform) { + updateType = 'minor'; + } else if (payload.maturity) { + updateType = 'maturityLabel'; + } + + const updatedPayload = { + modelId: dataModel.modelId, + connectedDgId: payload.dgId, + connectedDgName: 'Alpha Dataset 24', + deploymentEnv: payload.platform, + baseModels: payload.baseModels, + maturityLabel: payload.maturity, + updateType, + }; - if (validateData()) { + + if (updateType) { if ( - dataModel.dgId !== initialData.dgId || - dataModel.dgName !== initialData.dgName + initialData.platform === Platform.UNDEPLOYED && + (dataModel.platform === Platform.JIRA || + dataModel.platform === Platform.OUTLOOK || + dataModel.platform === Platform.PINAL) ) { + open({ + title: 'Warning: Replace Production Model', + content: + 'Adding this model to production will replace the current production model. Are you sure you want to proceed?', + footer:
    , + }); + } else { + + updateDataModelMutation.mutate(updatedPayload); } } }; + const updateDataModelMutation = useMutation({ + mutationFn: (data) => updateDataModel(data), + onSuccess: async (response) => { + open({ + title: 'Changes Saved Successfully', + content: ( +

    + You have successfully saved the changes. You can view the data model + in the "All Data Models" view. +

    + ), + footer: ( +
    + {' '} + +
    + ), + }); + }, + onError: () => { + open({ + title: 'Error Updating Data Model', + content: ( +

    + There was an issue updating the data model. Please try again. If the + problem persists, contact support for assistance. +

    + ), + }); + }, + }); + const handleDelete = () => { if ( dataModel.platform === Platform.JIRA || @@ -174,7 +236,7 @@ const ConfigureDataModel: FC = ({ id }) => {
    = ({ id }) => { content: 'Are you sure you want to retrain this model?', footer: (
    - - +
    ), }) diff --git a/GUI/src/pages/DataModels/CreateDataModel.tsx b/GUI/src/pages/DataModels/CreateDataModel.tsx index 761ded6b..1ecd4097 100644 --- a/GUI/src/pages/DataModels/CreateDataModel.tsx +++ b/GUI/src/pages/DataModels/CreateDataModel.tsx @@ -44,19 +44,27 @@ const CreateDataModel: FC = () => { }); const validateData = () => { - setErrors(validateDataModel(dataModel)); - const payload={ - modelName: dataModel.modelName, - datasetGroupName: dataModel.dgName, - dgId: dataModel.dgId, - baseModels: dataModel.baseModels, - deploymentPlatform: dataModel.platform, - maturityLabel:dataModel.maturity - } - - createDataModelMutation.mutate(payload); + const validationErrors = validateDataModel(dataModel); + setErrors(validationErrors); + return Object.keys(validationErrors)?.length === 0; + + }; + const handleCreate = ()=>{ +if(validateData()){ + const payload={ + modelName: dataModel.modelName, + datasetGroupName: dataModel.dgName, + dgId: dataModel.dgId, + baseModels: dataModel.baseModels, + deploymentPlatform: dataModel.platform, + maturityLabel:dataModel.maturity +} + +createDataModelMutation.mutate(payload); +} + } const createDataModelMutation = useMutation({ mutationFn: (data) => createDataModel(data), onSuccess: async (response) => { @@ -89,6 +97,7 @@ const CreateDataModel: FC = () => { errors={errors} dataModel={dataModel} handleChange={handleDataModelAttributesChange} + type='create' />
    { background: 'white', }} > - + diff --git a/GUI/src/pages/DataModels/index.tsx b/GUI/src/pages/DataModels/index.tsx index 7d76bd0c..757bd7b6 100644 --- a/GUI/src/pages/DataModels/index.tsx +++ b/GUI/src/pages/DataModels/index.tsx @@ -8,6 +8,8 @@ import { formattedArray, parseVersionString } from 'utils/commonUtilts'; import { getDataModelsOverview, getFilterData } from 'services/data-models'; import DataModelCard from 'components/molecules/DataModelCard'; import ConfigureDataModel from './ConfigureDataModel'; +import { INTEGRATION_OPERATIONS } from 'enums/integrationEnums'; +import { Platform } from 'enums/dataModelsEnums'; const DataModels: FC = () => { const { t } = useTranslation(); @@ -67,7 +69,7 @@ const DataModels: FC = () => { const { data: filterData } = useQuery(['datamodels/filters'], () => getFilterData() ); - // const pageCount = datasetGroupsData?.response?.data?.[0]?.totalPages || 1; + const pageCount = dataModelsData?.data[0]?.totalPages || 1; const handleFilterChange = (name: string, value: string) => { setEnableFetch(false); @@ -79,101 +81,131 @@ const DataModels: FC = () => { return (
    - {view==="list"&&(
    -
    -
    Data Models
    - -
    -
    -
    - - handleFilterChange('modelName', selection?.value ?? '') - } - /> - - handleFilterChange('version', selection?.value ?? '') - } - /> - - handleFilterChange('platform', selection?.value ?? '') - } - /> - - handleFilterChange('datasetGroup', selection?.value ?? '') - } - /> - - handleFilterChange('trainingStatus', selection?.value ?? '') - } - /> - - handleFilterChange('maturity', selection?.value ?? '') - } - /> - - handleFilterChange('sort', selection?.value ?? '') - } - /> - + {view === 'list' && ( +
    +
    +
    Data Models
    -
    +
    +
    + + handleFilterChange('modelName', selection?.value ?? '') + } + /> + + handleFilterChange('version', selection?.value ?? '') + } + /> + + handleFilterChange('platform', selection?.value ?? '') + } + /> + + handleFilterChange('datasetGroup', selection?.value ?? '') + } + /> + + handleFilterChange('trainingStatus', selection?.value ?? '') + } + /> + + handleFilterChange('maturity', selection?.value ?? '') + } + /> + + handleFilterChange('sort', selection?.value ?? '') + } + /> + + +
    {isLoading &&
    Loading...
    } - {dataModelsData?.data?.map( - (dataset, index: number) => { + +
    + Production Models +
    + +
    + {dataModelsData?.data?.map((dataset, index: number) => { + if ( + dataset.deploymentEnv === Platform.JIRA || + dataset.deploymentEnv === Platform.OUTLOOK || + dataset.deploymentEnv === Platform.PINAL + ) + return ( + + ); + })} +
    +
    Data Models
    +
    + {dataModelsData?.data?.map((dataset, index: number) => { return ( { setView={setView} /> ); - } - )} + })} +
    + + 1} + canNextPage={pageIndex < 10} + onPageChange={setPageIndex} + />
    - 1} - canNextPage={pageIndex < 10} - onPageChange={setPageIndex} - />
    -
    )} - {view==="individual" &&( - )} - + {view === 'individual' && }
    ); }; diff --git a/GUI/src/pages/DatasetGroups/DatasetGroups.scss b/GUI/src/pages/DatasetGroups/DatasetGroups.scss index 5b32f1df..7422bbbc 100644 --- a/GUI/src/pages/DatasetGroups/DatasetGroups.scss +++ b/GUI/src/pages/DatasetGroups/DatasetGroups.scss @@ -26,7 +26,6 @@ display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; - padding: 16px; width: 100%; } diff --git a/GUI/src/pages/DatasetGroups/index.tsx b/GUI/src/pages/DatasetGroups/index.tsx index 7d50d793..a99794ce 100644 --- a/GUI/src/pages/DatasetGroups/index.tsx +++ b/GUI/src/pages/DatasetGroups/index.tsx @@ -146,8 +146,8 @@ const DatasetGroups: FC = () => {
    {isLoading &&
    Loading...
    } {datasetGroupsData?.response?.data?.map( diff --git a/GUI/src/services/data-models.ts b/GUI/src/services/data-models.ts index 3e8ff795..c54ceeab 100644 --- a/GUI/src/services/data-models.ts +++ b/GUI/src/services/data-models.ts @@ -29,7 +29,7 @@ export async function getDataModelsOverview( trainingStatus, deploymentMaturity, sortType:sort, - pageSize:5 + pageSize:12 }, }); return data?.response; @@ -62,3 +62,11 @@ export async function createDataModel(dataModel) { return data; } +export async function updateDataModel(dataModel) { + + const { data } = await apiDev.post('classifier/datamodel/update', { + ...dataModel, + }); + return data; +} + diff --git a/GUI/src/services/sse-service.ts b/GUI/src/services/sse-service.ts index ac913baf..99a688d0 100644 --- a/GUI/src/services/sse-service.ts +++ b/GUI/src/services/sse-service.ts @@ -6,7 +6,7 @@ const sse = (url: string, onMessage: (data: T) => void): EventSource => { throw new Error('Notification node url is not defined'); } const eventSource = new EventSource( - `${notificationNodeUrl}/sse/notifications${url}` + `${notificationNodeUrl}/sse/dataset/notifications${url}` ); eventSource.onmessage = (event: MessageEvent) => { diff --git a/GUI/src/utils/dataModelsUtils.ts b/GUI/src/utils/dataModelsUtils.ts index fb76b340..0df45fdb 100644 --- a/GUI/src/utils/dataModelsUtils.ts +++ b/GUI/src/utils/dataModelsUtils.ts @@ -1,4 +1,4 @@ -import { DataModel } from "types/dataModels"; +import { DataModel } from 'types/dataModels'; export const validateDataModel = (dataModel) => { const { modelName, dgName, platform, baseModels, maturity } = dataModel; @@ -14,14 +14,23 @@ export const validateDataModel = (dataModel) => { return newErrors; }; -export const customFormattedArray = >(data: T[], attributeName: keyof T) => { +export const customFormattedArray = >( + data: T[], + attributeName: keyof T +) => { return data?.map((item) => ({ - label: item[attributeName], - value: {name:item[attributeName],id:item.dgId}, + label: `${item[attributeName]} (${item?.majorVersion}.${item?.minorVersion}.${item?.patchVersion})`, + value: { + name: `${item[attributeName]} (${item?.majorVersion}.${item?.minorVersion}.${item?.patchVersion})`, + id: item.dgId, + }, })); }; -export const getChangedAttributes = (original: DataModel, updated: DataModel): Partial> => { +export const getChangedAttributes = ( + original: DataModel, + updated: DataModel +): Partial> => { const changes: Partial> = {}; (Object.keys(original) as (keyof DataModel)[]).forEach((key) => { @@ -33,4 +42,4 @@ export const getChangedAttributes = (original: DataModel, updated: DataModel): P }); return changes; -}; \ No newline at end of file +}; From 6e74438d9c46d5d39d2f979de0d1dca126875ded Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Wed, 7 Aug 2024 11:57:37 +0530 Subject: [PATCH 388/582] adding loaders and other refctorings --- .../FormElements/FormRadios/index.tsx | 56 ++++++++--- GUI/src/components/LabelChip/index.scss | 41 ++++---- .../CircularSpinner/CircularSpinner.tsx | 19 ++++ .../molecules/CircularSpinner/Spinner.scss | 23 +++++ .../DatasetDetailedViewTable.tsx | 95 ++++++++++--------- .../DatasetGroups/CreateDatasetGroup.tsx | 15 +-- .../pages/DatasetGroups/DatasetGroups.scss | 27 +++++- .../pages/DatasetGroups/ViewDatasetGroup.tsx | 16 +++- GUI/src/pages/DatasetGroups/index.tsx | 7 +- GUI/src/pages/StopWords/index.tsx | 7 +- .../pages/UserManagement/SettingsUsers.scss | 5 + GUI/src/pages/UserManagement/UserModal.tsx | 24 ++++- GUI/src/pages/UserManagement/index.tsx | 3 +- 13 files changed, 234 insertions(+), 104 deletions(-) create mode 100644 GUI/src/components/molecules/CircularSpinner/CircularSpinner.tsx create mode 100644 GUI/src/components/molecules/CircularSpinner/Spinner.scss diff --git a/GUI/src/components/FormElements/FormRadios/index.tsx b/GUI/src/components/FormElements/FormRadios/index.tsx index 619a961a..fffba69a 100644 --- a/GUI/src/components/FormElements/FormRadios/index.tsx +++ b/GUI/src/components/FormElements/FormRadios/index.tsx @@ -1,5 +1,4 @@ import { FC, useId } from 'react'; - import './FormRadios.scss'; type FormRadiosType = { @@ -11,27 +10,56 @@ type FormRadiosType = { value: string; }[]; onChange: (selectedValue: string) => void; + selectedValue?: string; // New prop for the selected value isStack?: boolean; -} + error?: string; +}; -const FormRadios: FC = ({ label, name, hideLabel, items, onChange,isStack=false }) => { +const FormRadios: FC = ({ + label, + name, + hideLabel, + items, + onChange, + selectedValue, // Use selectedValue to control the selected radio button + isStack = false, + error, +}) => { const id = useId(); return ( -
    - {label && !hideLabel && } -
    - {items.map((item, index) => ( -
    - { - onChange(event.target.value); - }} /> - +
    +
    +
    + {label && !hideLabel && ( + + )} +
    + {items?.map((item, index) => ( +
    + { + onChange(event.target.value); + }} + /> + +
    + ))}
    - ))} +
    -
    +
    {error &&

    {error}

    }
    +
    ); }; export default FormRadios; + + + + diff --git a/GUI/src/components/LabelChip/index.scss b/GUI/src/components/LabelChip/index.scss index 9e49c645..ed40b04a 100644 --- a/GUI/src/components/LabelChip/index.scss +++ b/GUI/src/components/LabelChip/index.scss @@ -1,20 +1,23 @@ .label-chip { - display: inline-flex; - align-items: center; - padding: 4px 15px; - border-radius: 16px; - background-color: #e0e0e0; - margin: 4px; - } - - .label-chip .label { - margin-right: 8px; - } - - .label-chip .button { - background: none; - border: none; - cursor: pointer; - display: flex; - align-items: center; - } \ No newline at end of file + display: inline-flex; + align-items: center; + justify-content: center; + padding: 6px 20px; + border-radius: 16px; + background-color: #e0e0e0; + margin: 4px; + gap: 7px; +} + +.label-chip .label { + margin-right: 8px; +} + +.label-chip .button { + background: none; + border: none; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; +} \ No newline at end of file diff --git a/GUI/src/components/molecules/CircularSpinner/CircularSpinner.tsx b/GUI/src/components/molecules/CircularSpinner/CircularSpinner.tsx new file mode 100644 index 00000000..60eaa8ad --- /dev/null +++ b/GUI/src/components/molecules/CircularSpinner/CircularSpinner.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import './Spinner.scss'; + +interface SpinnerProps { + size?: number; +} + +const CircularSpinner: React.FC = ({ size = 80 }) => { + return ( +
    +
    +
    + ); +}; + +export default CircularSpinner; \ No newline at end of file diff --git a/GUI/src/components/molecules/CircularSpinner/Spinner.scss b/GUI/src/components/molecules/CircularSpinner/Spinner.scss new file mode 100644 index 00000000..d6e2abcc --- /dev/null +++ b/GUI/src/components/molecules/CircularSpinner/Spinner.scss @@ -0,0 +1,23 @@ +.spinner-container { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + } + + .spinner { + border: 4px solid rgba(0, 0, 0, 0.1); + border-top: 4px solid #3498db; + border-radius: 50%; + animation: spin 1s linear infinite; + } + + @keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } + \ No newline at end of file diff --git a/GUI/src/components/molecules/DatasetDetailedViewTable/DatasetDetailedViewTable.tsx b/GUI/src/components/molecules/DatasetDetailedViewTable/DatasetDetailedViewTable.tsx index 4262dccf..787af49a 100644 --- a/GUI/src/components/molecules/DatasetDetailedViewTable/DatasetDetailedViewTable.tsx +++ b/GUI/src/components/molecules/DatasetDetailedViewTable/DatasetDetailedViewTable.tsx @@ -145,58 +145,59 @@ const DatasetDetailedViewTable = ({
    {bannerMessage &&
    {bannerMessage}
    } - {(!datasets || (datasets && datasets?.numPages < 2)) && ( - -
    - {(!datasets || datasets?.dataPayload?.length === 0) && ( -
    -
    - {t('datasetGroups.detailedView.noData') ?? ''} + {(!datasets || (datasets && datasets?.numPages < 2)) && + !isLoading && ( + +
    + {(!datasets || datasets?.numPages === 0) && ( +
    +
    + {t('datasetGroups.detailedView.noData') ?? ''} +
    +

    {t('datasetGroups.detailedView.noDataDesc') ?? ''}

    +
    -

    {t('datasetGroups.detailedView.noDataDesc') ?? ''}

    - -
    - )} - {datasets && datasets?.numPages <= 2 && ( -
    -

    - {t( - 'datasetGroups.detailedView.insufficientExamplesDesc' - ) ?? ''} -

    - -
    - )} -
    - - )} + )} + {datasets && datasets?.numPages !== 0 && datasets?.numPages <= 2 && ( +
    +

    + {t( + 'datasetGroups.detailedView.insufficientExamplesDesc' + ) ?? ''} +

    + +
    + )} +
    + + )}
    )}
    {isLoading && } - {!isLoading && updatedDataset && ( + {!isLoading && updatedDataset && updatedDataset.length > 0 && ( []} diff --git a/GUI/src/pages/DatasetGroups/CreateDatasetGroup.tsx b/GUI/src/pages/DatasetGroups/CreateDatasetGroup.tsx index 4e873e00..c709cf11 100644 --- a/GUI/src/pages/DatasetGroups/CreateDatasetGroup.tsx +++ b/GUI/src/pages/DatasetGroups/CreateDatasetGroup.tsx @@ -140,16 +140,11 @@ const CreateDatasetGroup: FC = () => { setIsModalOpen={setIsModalOpen} /> -
    - -
    @@ -526,4 +532,4 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { ); }; -export default ViewDatasetGroup; +export default ViewDatasetGroup; \ No newline at end of file diff --git a/GUI/src/pages/DatasetGroups/index.tsx b/GUI/src/pages/DatasetGroups/index.tsx index 7d50d793..600e46e8 100644 --- a/GUI/src/pages/DatasetGroups/index.tsx +++ b/GUI/src/pages/DatasetGroups/index.tsx @@ -12,6 +12,7 @@ import { SingleDatasetType } from 'types/datasetGroups'; import ViewDatasetGroup from './ViewDatasetGroup'; import { datasetQueryKeys } from 'utils/queryKeys'; import { DatasetViewEnum } from 'enums/datasetEnums'; +import CircularSpinner from 'components/molecules/CircularSpinner/CircularSpinner'; const DatasetGroups: FC = () => { const { t } = useTranslation(); @@ -145,11 +146,15 @@ const DatasetGroups: FC = () => { {t('global.reset')}
    + {isLoading && ( +
    + +
    + )}
    - {isLoading &&
    Loading...
    } {datasetGroupsData?.response?.data?.map( (dataset: SingleDatasetType, index: number) => { return ( diff --git a/GUI/src/pages/StopWords/index.tsx b/GUI/src/pages/StopWords/index.tsx index ea931268..bf0fcee3 100644 --- a/GUI/src/pages/StopWords/index.tsx +++ b/GUI/src/pages/StopWords/index.tsx @@ -203,7 +203,11 @@ const StopWords: FC = () => { - +
    } > @@ -243,4 +257,4 @@ const UserModal: FC = ({ onClose, user, isModalOpen }) => { ); }; -export default UserModal; +export default UserModal; \ No newline at end of file diff --git a/GUI/src/pages/UserManagement/index.tsx b/GUI/src/pages/UserManagement/index.tsx index 3bd4f3fc..d3fd8423 100644 --- a/GUI/src/pages/UserManagement/index.tsx +++ b/GUI/src/pages/UserManagement/index.tsx @@ -22,6 +22,7 @@ import { userManagementEndpoints } from 'utils/endpoints'; import { ButtonAppearanceTypes, ToastTypes } from 'enums/commonEnums'; import { useDialog } from 'hooks/useDialog'; import SkeletonTable from 'components/molecules/TableSkeleton/TableSkeleton'; +import CircularSpinner from 'components/molecules/CircularSpinner/CircularSpinner'; const UserManagement: FC = () => { const columnHelper = createColumnHelper(); @@ -172,7 +173,7 @@ const UserManagement: FC = () => { }, }); - if (isLoading) return
    {t('global.loading')}...
    ; + if (isLoading) return ; return (
    From a160382014ba3257574e52f31d3ff5095ab42bce Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Wed, 7 Aug 2024 12:42:17 +0530 Subject: [PATCH 389/582] ESCLASS-192: add cookie extend API --- .../DSL/GET/auth/jwt/extend.yml | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 DSL/Ruuter.private/DSL/GET/auth/jwt/extend.yml diff --git a/DSL/Ruuter.private/DSL/GET/auth/jwt/extend.yml b/DSL/Ruuter.private/DSL/GET/auth/jwt/extend.yml new file mode 100644 index 00000000..124e0983 --- /dev/null +++ b/DSL/Ruuter.private/DSL/GET/auth/jwt/extend.yml @@ -0,0 +1,41 @@ +declaration: + call: declare + version: 0.1 + description: "Decription placeholder for 'EXTEND'" + method: get + accepts: json + returns: json + namespace: classifier + allowlist: + headers: + - field: cookie + type: string + description: "Cookie field" + +extend_cookie: + call: http.post + args: + url: "[#CHATBOT_TIM]/jwt/custom-jwt-extend" + contentType: plaintext + headers: + cookie: ${incoming.headers.cookie} + plaintext: + "customJwtCookie" + result: cookie_result + next: assign_cookie + +assign_cookie: + assign: + setCookie: + customJwtCookie: ${cookie_result.response.body.token} + Domain: "[#DOMAIN]" + Secure: true + HttpOnly: true + SameSite: "Lax" + next: return_value + +return_value: + headers: + Set-Cookie: ${setCookie} + return: ${cookie_result.response.body.token} + next: end From 81802b10f6445972a7f76272d74c8309c1034a21 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Wed, 7 Aug 2024 17:40:41 +0530 Subject: [PATCH 390/582] ESCLASS-192: add cookie extend API --- DSL/Ruuter.private/DSL/GET/auth/jwt/extend.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DSL/Ruuter.private/DSL/GET/auth/jwt/extend.yml b/DSL/Ruuter.private/DSL/GET/auth/jwt/extend.yml index 124e0983..5b3de56f 100644 --- a/DSL/Ruuter.private/DSL/GET/auth/jwt/extend.yml +++ b/DSL/Ruuter.private/DSL/GET/auth/jwt/extend.yml @@ -15,7 +15,7 @@ declaration: extend_cookie: call: http.post args: - url: "[#CHATBOT_TIM]/jwt/custom-jwt-extend" + url: "[#CLASSIFIER_TIM]/jwt/custom-jwt-extend" contentType: plaintext headers: cookie: ${incoming.headers.cookie} From b15a70aed0f64a1a5bb077eb2696703fc481bdcf Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Wed, 7 Aug 2024 19:21:03 +0530 Subject: [PATCH 391/582] ESCLASS-192: create model bug fixed --- DSL/Ruuter.private/DSL/POST/classifier/datamodel/create.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/create.yml b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/create.yml index 1d4cdfb4..bc08d3b2 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/create.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/create.yml @@ -82,6 +82,8 @@ check_deployment_platform: switch: - condition: ${deployment_platform == 'jira' || deployment_platform == 'outlook' || deployment_platform == 'pinal'} next: update_existing_models_deployment_platform + - condition: ${deployment_platform == 'testing' || deployment_platform == 'undeployed'} + next: create_model_metadata next: assign_fail_response update_existing_models_deployment_platform: From 4eef54b1eef87a5f903b230efb8629f7cc0010df Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Wed, 7 Aug 2024 20:27:58 +0530 Subject: [PATCH 392/582] ESCLASS-192: model overview new filter logic added --- .../get-production-data-model-metadata.sql | 23 +++++++++++++++++++ .../DSL/GET/classifier/datamodel/overview.yml | 17 ++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 DSL/Resql/get-production-data-model-metadata.sql diff --git a/DSL/Resql/get-production-data-model-metadata.sql b/DSL/Resql/get-production-data-model-metadata.sql new file mode 100644 index 00000000..193cc2fe --- /dev/null +++ b/DSL/Resql/get-production-data-model-metadata.sql @@ -0,0 +1,23 @@ +SELECT + dt.id, + dt.model_group_key, + dt.model_name, + dt.major_version, + dt.minor_version, + dt.latest, + dt.maturity_label, + dt.deployment_env, + dt.training_status, + dt.base_models, + dt.last_trained_timestamp, + dt.created_timestamp, + dt.connected_dg_id, + dt.connected_dg_name, + dt.connected_dg_major_version, + dt.connected_dg_minor_version, + dt.connected_dg_patch_version, + jsonb_pretty(dt.training_results) AS training_results +FROM + models_metadata dt +WHERE + dt.deployment_env IN ('jira', 'pinal', 'outlook'); \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/GET/classifier/datamodel/overview.yml b/DSL/Ruuter.private/DSL/GET/classifier/datamodel/overview.yml index fe5b13b2..36ffa833 100644 --- a/DSL/Ruuter.private/DSL/GET/classifier/datamodel/overview.yml +++ b/DSL/Ruuter.private/DSL/GET/classifier/datamodel/overview.yml @@ -38,6 +38,9 @@ declaration: - field: deploymentMaturity type: string description: "Parameter 'deploymentMaturity'" + - field: isProductionModel + type: boolean + description: "Parameter 'isProductionModel'" extract_data: assign: @@ -51,8 +54,22 @@ extract_data: dataset_group: ${Number(incoming.params.datasetGroup)} training_status: ${incoming.params.trainingStatus} deployment_maturity: ${incoming.params.deploymentMaturity} + is_production_model: ${incoming.params.isProductionModel} + next: check_production_model_status + +check_production_model_status: + switch: + - condition: ${is_production_model == 'true'} + next: get_production_data_model_meta_data_overview next: get_data_model_meta_data_overview +get_production_data_model_meta_data_overview: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-production-data-model-metadata" + result: res_model + next: check_status + get_data_model_meta_data_overview: call: http.post args: From caf308e3d0462a689dbe72ce6389bb39181b0714 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Wed, 7 Aug 2024 21:59:47 +0530 Subject: [PATCH 393/582] data model retrain,delete int --- GUI/src/components/MainNavigation/index.tsx | 16 +- .../molecules/DataModelForm/index.tsx | 51 ++-- .../pages/DataModels/ConfigureDataModel.tsx | 85 ++++-- GUI/src/pages/DataModels/CreateDataModel.tsx | 59 ++-- GUI/src/pages/DataModels/DataModels.scss | 30 +++ GUI/src/pages/DataModels/index.tsx | 254 +++++++++--------- .../pages/DatasetGroups/DatasetGroups.scss | 2 +- GUI/src/services/data-models.ts | 32 ++- GUI/src/services/datasets.ts | 3 +- GUI/src/styles/generic/_base.scss | 4 +- GUI/src/utils/dataModelsUtils.ts | 17 +- GUI/translations/en/common.json | 2 + 12 files changed, 346 insertions(+), 209 deletions(-) diff --git a/GUI/src/components/MainNavigation/index.tsx b/GUI/src/components/MainNavigation/index.tsx index c6bceb8b..3136bb95 100644 --- a/GUI/src/components/MainNavigation/index.tsx +++ b/GUI/src/components/MainNavigation/index.tsx @@ -60,8 +60,18 @@ const MainNavigation: FC = () => { { id: 'dataModels', label: t('menu.dataModels'), - path: '/data-models', + path: '#', icon: , + children: [ + { + label: t('menu.models'), + path: '/data-models', + }, + { + label: t('menu.trainingSessions'), + path: 'training-sessions', + } + ], }, { id: 'incomingTexts', @@ -97,9 +107,7 @@ const MainNavigation: FC = () => { const res = await apiDev.get(userManagementEndpoints.FETCH_USER_ROLES()); return res?.data?.response; }, - onSuccess: (res) => { - console.log(res); - + onSuccess: (res) => { const role = res?.includes(ROLES.ROLE_ADMINISTRATOR) ? ROLES.ROLE_ADMINISTRATOR :ROLES.ROLE_MODEL_TRAINER const filteredItems = filterItemsByRole(role, items); setMenuItems(filteredItems); diff --git a/GUI/src/components/molecules/DataModelForm/index.tsx b/GUI/src/components/molecules/DataModelForm/index.tsx index aad4d865..dbe84d18 100644 --- a/GUI/src/components/molecules/DataModelForm/index.tsx +++ b/GUI/src/components/molecules/DataModelForm/index.tsx @@ -10,7 +10,10 @@ import { import { formattedArray } from 'utils/commonUtilts'; import { useQuery } from '@tanstack/react-query'; import { getCreateOptions } from 'services/data-models'; -import { customFormattedArray } from 'utils/dataModelsUtils'; +import { + customFormattedArray, + dgArrayWithVersions, +} from 'utils/dataModelsUtils'; type DataModelFormType = { dataModel: any; @@ -23,7 +26,7 @@ const DataModelForm: FC = ({ dataModel, handleChange, errors, - type + type, }) => { const { t } = useTranslation(); @@ -31,28 +34,28 @@ const DataModelForm: FC = ({ getCreateOptions() ); - console.log(createOptions); - return (
    - {type==="create" ? (
    -
    - handleChange('modelName', e.target.value)} - error={errors?.modelName} - /> -
    -
    - Model Version + {type === 'create' ? ( +
    +
    + handleChange('modelName', e.target.value)} + error={errors?.modelName} + /> +
    +
    + Model Version +
    -
    ):( + ) : (
    -
    {dataModel.modelName}
    - -
    +
    {dataModel.modelName}
    + +
    )} {createOptions && ( @@ -61,17 +64,15 @@ const DataModelForm: FC = ({
    { - handleChange('dgName', selection?.value?.name); - handleChange('dgId', selection?.value?.id); + handleChange('dgId', selection?.value); }} - error={errors?.dgName} - defaultValue={ dataModel?.dgName} + defaultValue={dataModel?.dgId} />
    diff --git a/GUI/src/pages/DataModels/ConfigureDataModel.tsx b/GUI/src/pages/DataModels/ConfigureDataModel.tsx index f68f6ef3..fb7f80f5 100644 --- a/GUI/src/pages/DataModels/ConfigureDataModel.tsx +++ b/GUI/src/pages/DataModels/ConfigureDataModel.tsx @@ -4,7 +4,12 @@ import { Link, useNavigate } from 'react-router-dom'; import { Button, Card } from 'components'; import { useDialog } from 'hooks/useDialog'; import BackArrowButton from 'assets/BackArrowButton'; -import { getMetadata, updateDataModel } from 'services/data-models'; +import { + deleteDataModel, + getMetadata, + retrainDataModel, + updateDataModel, +} from 'services/data-models'; import DataModelForm from 'components/molecules/DataModelForm'; import { getChangedAttributes, validateDataModel } from 'utils/dataModelsUtils'; import { Platform } from 'enums/dataModelsEnums'; @@ -20,9 +25,8 @@ const ConfigureDataModel: FC = ({ id }) => { const [enabled, setEnabled] = useState(true); const [initialData, setInitialData] = useState({}); const [dataModel, setDataModel] = useState({ - modelId: '', + modelId: 0, modelName: '', - dgName: '', dgId: '', platform: '', baseModels: [], @@ -40,8 +44,7 @@ const ConfigureDataModel: FC = ({ id }) => { setDataModel({ modelId: data?.modelId || '', modelName: data?.modelName || '', - dgName: data?.connectedDgName || '', - dgId: data?.modelId || '', + dgId: data?.connectedDgId || '', platform: data?.deploymentEnv || '', baseModels: data?.baseModels || [], maturity: data?.maturityLabel || '', @@ -49,7 +52,6 @@ const ConfigureDataModel: FC = ({ id }) => { }); setInitialData({ modelName: data?.modelName || '', - dgName: data?.connectedDgName || '', dgId: data?.modelId || '', platform: data?.deploymentEnv || '', baseModels: data?.baseModels || [], @@ -71,7 +73,7 @@ const ConfigureDataModel: FC = ({ id }) => { const handleSave = () => { const payload = getChangedAttributes(initialData, dataModel); let updateType; - if (payload.dgId || payload.dgName) { + if (payload.dgId) { updateType = 'major'; } else if (payload.baseModels || payload.platform) { updateType = 'minor'; @@ -82,14 +84,12 @@ const ConfigureDataModel: FC = ({ id }) => { const updatedPayload = { modelId: dataModel.modelId, connectedDgId: payload.dgId, - connectedDgName: 'Alpha Dataset 24', deploymentEnv: payload.platform, baseModels: payload.baseModels, maturityLabel: payload.maturity, updateType, }; - if (updateType) { if ( initialData.platform === Platform.UNDEPLOYED && @@ -101,10 +101,23 @@ const ConfigureDataModel: FC = ({ id }) => { title: 'Warning: Replace Production Model', content: 'Adding this model to production will replace the current production model. Are you sure you want to proceed?', - footer:
    , + footer: ( +
    + + +
    + ), }); } else { - updateDataModelMutation.mutate(updatedPayload); } } @@ -176,7 +189,7 @@ const ConfigureDataModel: FC = ({ id }) => {
    @@ -198,13 +211,55 @@ const ConfigureDataModel: FC = ({ id }) => { > Cancel - +
    ), }); } }; + const deleteDataModelMutation = useMutation({ + mutationFn: (modelId: number) => deleteDataModel(modelId), + onSuccess: async (response) => { + close(); + navigate(0); + }, + onError: () => { + open({ + title: 'Error Deleting Data Model', + content: ( +

    + There was an issue deleting the data model. Please try again. If the + problem persists, contact support for assistance. +

    + ), + }); + }, + }); + + const retrainDataModelMutation = useMutation({ + mutationFn: (modelId: number) => retrainDataModel(modelId), + onSuccess: async () => { + close(); + navigate(0); + }, + onError: () => { + open({ + title: 'Error Deleting Data Model', + content: ( +

    + There was an issue retraining the data model. Please try again. If the + problem persists, contact support for assistance. +

    + ), + }); + }, + }); return (
    @@ -228,7 +283,7 @@ const ConfigureDataModel: FC = ({ id }) => { Model updated. Please initiate retraining to continue benefiting from the latest improvements.

    - +
    @@ -266,7 +321,7 @@ const ConfigureDataModel: FC = ({ id }) => { > Cancel - +
    ), }) diff --git a/GUI/src/pages/DataModels/CreateDataModel.tsx b/GUI/src/pages/DataModels/CreateDataModel.tsx index 1ecd4097..fb0b74f2 100644 --- a/GUI/src/pages/DataModels/CreateDataModel.tsx +++ b/GUI/src/pages/DataModels/CreateDataModel.tsx @@ -13,7 +13,7 @@ import { createDataModel } from 'services/data-models'; const CreateDataModel: FC = () => { const { t } = useTranslation(); - const { open,close } = useDialog(); + const { open, close } = useDialog(); const [isModalOpen, setIsModalOpen] = useState(false); const [modalType, setModalType] = useState(''); const navigate = useNavigate(); @@ -47,37 +47,56 @@ const CreateDataModel: FC = () => { const validationErrors = validateDataModel(dataModel); setErrors(validationErrors); return Object.keys(validationErrors)?.length === 0; - - }; - const handleCreate = ()=>{ -if(validateData()){ - const payload={ - modelName: dataModel.modelName, - datasetGroupName: dataModel.dgName, - dgId: dataModel.dgId, - baseModels: dataModel.baseModels, - deploymentPlatform: dataModel.platform, - maturityLabel:dataModel.maturity -} + const handleCreate = () => { + if (validateData()) { + const payload = { + modelName: dataModel.modelName, + dgId: dataModel.dgId, + baseModels: dataModel.baseModels, + deploymentPlatform: dataModel.platform, + maturityLabel: dataModel.maturity, + }; -createDataModelMutation.mutate(payload); -} - } + createDataModelMutation.mutate(payload); + } + }; const createDataModelMutation = useMutation({ mutationFn: (data) => createDataModel(data), onSuccess: async (response) => { open({ title: 'Data Model Created and Trained', - content:

    You have successfully created and trained the data model. You can view it on the data model dashboard.

    , - footer:
    + content: ( +

    + You have successfully created and trained the data model. You can + view it on the data model dashboard. +

    + ), + footer: ( +
    + {' '} + +
    + ), }); }, onError: () => { open({ title: 'Error Creating Data Model', - content:

    There was an issue creating or training the data model. Please try again. If the problem persists, contact support for assistance.

    , + content: ( +

    + There was an issue creating or training the data model. Please try + again. If the problem persists, contact support for assistance. +

    + ), }); }, }); @@ -97,7 +116,7 @@ createDataModelMutation.mutate(payload); errors={errors} dataModel={dataModel} handleChange={handleDataModelAttributesChange} - type='create' + type="create" />
    { const { t } = useTranslation(); @@ -28,7 +30,7 @@ const DataModels: FC = () => { modelName: 'all', version: 'x.x.x', platform: 'all', - datasetGroup: 'all', + datasetGroup: -1, trainingStatus: 'all', maturity: 'all', sort: 'asc', @@ -54,7 +56,6 @@ const DataModels: FC = () => { filters.modelName, parseVersionString(filters?.version)?.major, parseVersionString(filters?.version)?.minor, - parseVersionString(filters?.version)?.patch, filters.platform, filters.datasetGroup, filters.trainingStatus, @@ -69,7 +70,7 @@ const DataModels: FC = () => { const { data: filterData } = useQuery(['datamodels/filters'], () => getFilterData() ); - const pageCount = dataModelsData?.data[0]?.totalPages || 1; + const pageCount = dataModelsData?.data[0]?.totalPages || 1; const handleFilterChange = (name: string, value: string) => { setEnableFetch(false); @@ -83,106 +84,134 @@ const DataModels: FC = () => {
    {view === 'list' && (
    -
    -
    Data Models
    - -
    -
    - - handleFilterChange('modelName', selection?.value ?? '') - } - /> - - handleFilterChange('version', selection?.value ?? '') - } - /> - - handleFilterChange('platform', selection?.value ?? '') - } - /> - - handleFilterChange('datasetGroup', selection?.value ?? '') - } - /> - - handleFilterChange('trainingStatus', selection?.value ?? '') - } - /> - - handleFilterChange('maturity', selection?.value ?? '') - } - /> - - handleFilterChange('sort', selection?.value ?? '') - } - /> - - -
    - {isLoading &&
    Loading...
    } +
    +
    +
    Production Models
    {' '} + +
    -
    - Production Models +
    + {dataModelsData?.data?.map((dataset, index: number) => { + if ( + dataset.deploymentEnv === Platform.JIRA || + dataset.deploymentEnv === Platform.OUTLOOK || + dataset.deploymentEnv === Platform.PINAL + ) + return ( + + ); + })} +
    +
    +
    +
    Data Models
    + +
    +
    + + handleFilterChange('modelName', selection?.value ?? '') + } + /> + + handleFilterChange('version', selection?.value ?? '') + } + /> + + handleFilterChange('platform', selection?.value ?? '') + } + /> + + handleFilterChange('datasetGroup', selection?.value?.id) + } + /> + + handleFilterChange('trainingStatus', selection?.value ?? '') + } + /> + + handleFilterChange('maturity', selection?.value ?? '') + } + /> + + handleFilterChange('sort', selection?.value ?? '') + } + /> + + +
    -
    - {dataModelsData?.data?.map((dataset, index: number) => { - if ( - dataset.deploymentEnv === Platform.JIRA || - dataset.deploymentEnv === Platform.OUTLOOK || - dataset.deploymentEnv === Platform.PINAL - ) +
    + {dataModelsData?.data?.map((dataset, index: number) => { return ( { setView={setView} /> ); - })} + })} +
    -
    Data Models
    -
    - {dataModelsData?.data?.map((dataset, index: number) => { - return ( - - ); - })} -
    - { const newErrors: any = {}; if (!modelName.trim()) newErrors.modelName = 'Model Name is required'; - if (!dgName.trim()) newErrors.dgName = 'Dataset Group Name is required'; if (!platform.trim()) newErrors.platform = 'Platform is required'; if (baseModels?.length === 0) newErrors.baseModels = 'At least one Base Model is required'; @@ -19,14 +18,24 @@ export const customFormattedArray = >( attributeName: keyof T ) => { return data?.map((item) => ({ - label: `${item[attributeName]} (${item?.majorVersion}.${item?.minorVersion}.${item?.patchVersion})`, + label: `${item[attributeName]}`, value: { - name: `${item[attributeName]} (${item?.majorVersion}.${item?.minorVersion}.${item?.patchVersion})`, - id: item.dgId, + name: `${item[attributeName]}`, + id: item.id, }, })); }; +export const dgArrayWithVersions = >( + data: T[], + attributeName: keyof T +) => { + return data?.map((item) => ({ + label: `${item[attributeName]} (${item.majorVersion}.${item.minorVersion}.${item.patchVersion})`, + value: item.dgId + })); +}; + export const getChangedAttributes = ( original: DataModel, updated: DataModel diff --git a/GUI/translations/en/common.json b/GUI/translations/en/common.json index f5065881..4f38ba5c 100644 --- a/GUI/translations/en/common.json +++ b/GUI/translations/en/common.json @@ -49,6 +49,8 @@ "datasetGroups": "Dataset Groups", "validationSessions": "Validation Sessions", "dataModels": "Data Models", + "models": "Models", + "trainingSessions": "Training Sessions", "testModel": "Test Model", "stopWords": "Stop Words", "incomingTexts": "Incoming Texts" From 45dc72ab8715b9121ae4e5be992c9363c00daa66 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Thu, 8 Aug 2024 10:28:56 +0530 Subject: [PATCH 394/582] prod data models ui change --- GUI/src/pages/DataModels/index.tsx | 92 ++++++++++++++++++++---------- GUI/src/services/data-models.ts | 6 +- 2 files changed, 67 insertions(+), 31 deletions(-) diff --git a/GUI/src/pages/DataModels/index.tsx b/GUI/src/pages/DataModels/index.tsx index 4e46fcfb..030f6665 100644 --- a/GUI/src/pages/DataModels/index.tsx +++ b/GUI/src/pages/DataModels/index.tsx @@ -12,6 +12,8 @@ import { INTEGRATION_OPERATIONS } from 'enums/integrationEnums'; import { Platform } from 'enums/dataModelsEnums'; import { customFormattedArray } from 'utils/dataModelsUtils'; import { MdPin, MdPinEnd, MdStar } from 'react-icons/md'; +import CircularSpinner from 'components/molecules/CircularSpinner/CircularSpinner'; +import { ButtonAppearanceTypes } from 'enums/commonEnums'; const DataModels: FC = () => { const { t } = useTranslation(); @@ -36,7 +38,7 @@ const DataModels: FC = () => { sort: 'asc', }); - const { data: dataModelsData, isLoading } = useQuery( + const { data: dataModelsData, isLoading: isModelDataLoading } = useQuery( [ 'datamodels/overview', pageIndex, @@ -49,6 +51,7 @@ const DataModels: FC = () => { filters.trainingStatus, filters.maturity, filters.sort, + false, ], () => getDataModelsOverview( @@ -60,13 +63,47 @@ const DataModels: FC = () => { filters.datasetGroup, filters.trainingStatus, filters.maturity, - filters.sort + filters.sort, + false ), { keepPreviousData: true, enabled: enableFetch, } ); + const { data: prodDataModelsData, isLoading: isProdModelDataLoading } = + useQuery( + [ + 'datamodels/overview', + 0, + 'all', + -1, + -1, + 'all', + -1, + 'all', + 'all', + 'asc', + true, + ], + () => + getDataModelsOverview( + 1, + 'all', + -1, + -1, + 'all', + -1, + 'all', + 'all', + 'asc', + true + ), + { + keepPreviousData: true, + enabled: enableFetch, + } + ); const { data: filterData } = useQuery(['datamodels/filters'], () => getFilterData() ); @@ -84,7 +121,7 @@ const DataModels: FC = () => {
    {view === 'list' && (
    -
    + {!isModelDataLoading && !isProdModelDataLoading ?(
    Production Models
    {' '} @@ -92,30 +129,25 @@ const DataModels: FC = () => {
    - {dataModelsData?.data?.map((dataset, index: number) => { - if ( - dataset.deploymentEnv === Platform.JIRA || - dataset.deploymentEnv === Platform.OUTLOOK || - dataset.deploymentEnv === Platform.PINAL - ) - return ( - - ); + {prodDataModelsData?.data?.map((dataset, index: number) => { + return ( + + ); })}
    @@ -205,7 +237,7 @@ const DataModels: FC = () => { onClick={() => { navigate(0); }} - > + appearance={ButtonAppearanceTypes.SECONDARY} > Reset
    @@ -240,7 +272,9 @@ const DataModels: FC = () => { canNextPage={pageIndex < 10} onPageChange={setPageIndex} /> -
    +
    ):( + + )}
    )} {view === 'individual' && } diff --git a/GUI/src/services/data-models.ts b/GUI/src/services/data-models.ts index b97a6393..74b8757f 100644 --- a/GUI/src/services/data-models.ts +++ b/GUI/src/services/data-models.ts @@ -10,10 +10,11 @@ export async function getDataModelsOverview( majorVersion: number, minorVersion: number, platform: string, - datasetGroup: string, + datasetGroup: number, trainingStatus: string, deploymentMaturity: string, - sort: string + sort: string, + isProductionModel: boolean ) { const { data } = await apiDev.get('classifier/datamodel/overview', { params: { @@ -27,6 +28,7 @@ export async function getDataModelsOverview( deploymentMaturity, sortType: sort, pageSize: 12, + isProductionModel }, }); return data?.response; From bfe34eded5c55de69c990b9661c1ad5c38534868 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Thu, 8 Aug 2024 12:55:17 +0530 Subject: [PATCH 395/582] ESCLASS-169: outlook validation payload --- .../datamodel/deployment/outlook/validate.yml | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 DSL/Ruuter.private/DSL/POST/classifier/datamodel/deployment/outlook/validate.yml diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/deployment/outlook/validate.yml b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/deployment/outlook/validate.yml new file mode 100644 index 00000000..ebd3bc89 --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/deployment/outlook/validate.yml @@ -0,0 +1,97 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'VALIDATE'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: modelId + type: string + description: "Body field 'modelId'" + +extract_request: + assign: + model_id: ${incoming.body.modelId} + next: get_dataset_group_id_by_model_id + +get_token_info: + call: http.get + args: + url: "[#CLASSIFIER_RUUTER_PRIVATE]/classifier/integration/outlook/token" + headers: + cookie: ${incoming.headers.cookie} + result: res + next: assign_access_token + +assign_access_token: + assign: + access_token: ${res.response.body.response.access_token} + next: get_dataset_group_id_by_model_id + +get_dataset_group_id_by_model_id: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-data-model-dataset-group-by-id" + body: + id: ${model_id} + result: res_model + next: check_data_model_status + +check_data_model_status: + switch: + - condition: ${200 <= res_model.response.statusCodeValue && res_model.response.statusCodeValue < 300} + next: check_data_model_exist + next: assign_fail_response + +check_data_model_exist: + switch: + - condition: ${res_model.response.body.length>0} + next: assign_dataset_group_id + next: assign_fail_response + +assign_dataset_group_id: + assign: + dg_id: ${res_model.response.body[0].connectedDgId} + next: get_dataset_group_class_hierarchy + +get_dataset_group_class_hierarchy: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-dataset-group-class-hierarchy" + body: + id: ${dg_id} + result: res_dataset + next: check_dataset_group_status + +check_dataset_group_status: + switch: + - condition: ${200 <= res_dataset.response.statusCodeValue && res_dataset.response.statusCodeValue < 300} + next: check_dataset_group_exist + next: assign_fail_response + +check_dataset_group_exist: + switch: + - condition: ${res_dataset.response.body.length>0} + next: assign_dataset_class_hierarchy + next: assign_fail_response + +assign_dataset_class_hierarchy: + assign: + class_hierarchy: ${JSON.parse(res_dataset.response.body[0].classHierarchy.value)} + next: get_outlook_folder_hierarchy + +get_outlook_folder_hierarchy: + call: http.post + args: + url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/validate_outlook_class_hierarchy" + headers: + type: json + result: result + next: output_val + +output_val: + return: ${result} + end: next From f2e679b661a25f052357fd4b03a2edcd5d94fa82 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Thu, 8 Aug 2024 14:17:10 +0530 Subject: [PATCH 396/582] Read Me: Add Setup documentation flows --- README.md | 95 +++++++++++++++++++++++++++++++++++++++++++++++++-- constants.ini | 2 +- migrate.sh | 14 -------- token.sh | 15 ++++++++ 4 files changed, 109 insertions(+), 17 deletions(-) create mode 100644 token.sh diff --git a/README.md b/README.md index 96061858..4134e03f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,94 @@ -# Introduction +# Classifier -A template repository.. \ No newline at end of file +# Scope + +This repo will primarily contain: + +1. Architectural and other documentation; +2. Docker Compose file to set up and run Classifier as a fully functional service; + +## Dev setup + +- Clone [Ruuter](https://github.com/buerokratt/Ruuter) +- Navigate to Ruuter and build the image `docker build -t ruuter .` +- Clone [Resql](https://github.com/buerokratt/Resql) +- Navigate to Resql and build the image `docker build -t resql .` +- Clone [Data Mapper](https://github.com/buerokratt/DataMapper) +- Navigate to Data Mapper and build the image `docker build -t data-mapper .` +- Clone [TIM](https://github.com/buerokratt/TIM) +- Navigate to TIM and build the image `docker build -t tim .` +- Clone [Authentication Layer](https://github.com/buerokratt/Authentication-layer) +- Navigate to Authentication Layer and build the image `docker build -f Dockerfile.dev -t authentication-layer .` +- Clone [Cron Manager](https://github.com/buerokratt/CronManager.git) +- Navigate to Cron Manager dev branch and build the image `docker build -t cron-manager .` + + +### Refresh Token setup + +- Navigate to outlook-consent-app +- setup environment variables in .env file + - NEXT_PUBLIC_CLIENT_ID + - CLIENT_SECRET + - REDIRECT_URI = http://localhost:3003/callback +- build and run image using `docker compose up -d` +- copy the token from app and set it in constant.ini file under the key `OUTLOOK_REFRESH_KEY` + +### Database setup + +- For setting up the database initially, run helper script `./token.sh` +- Then setup database password in constant.ini under the key DB_PASSWORD +- Run migrations added in this repository by running the helper script `./migrate.sh`(consider db properties before run the script) + +### Open Search + +- To Initialize Open Search run `./deploy-opensearch.sh ` +- To Use Opensearch locally run `./deploy-opensearch.sh http://localhost:9200 admin:admin true` + +### Use external components. + +Currently, Header and Main Navigation used as external components, they are defined as dependency in package.json + +``` + "@buerokrat-ria/header": "^0.0.1" + "@buerokrat-ria/menu": "^0.0.1" + "@buerokrat-ria/styles": "^0.0.1" +``` + +### Outlook Setup +- Register Application in Azure portal + - Supported account types - Supported account types + - Redirect URI platform - Web +- Client ID, Client Secret should be set in constant.ini under OUTLOOK_CLIENT_ID and OUTLOOK_SECRET_KEY +- Navigate CronManger/config folder and add Client ID, Client Secret values in config.ini file also + +### Jira Setup + +- Navigate to https://id.atlassian.com/manage-profile +- Log in with your JIRA credentials and click on security tab +- Scroll down to API token section and then click `Create and manage API tokens---> Create API Token` +- Navigate to Jira Account(This is the Account where jira issue create and prediction happen) +- Then go to settings--> system--> webHooks +- create webhook + - url - `Base_URL/classifier/integration/jira/cloud/accept` + - click issue created,updated check boxes +- Set Values in Constant.ini + - JIRA_API_TOKEN + - JIRA_USERNAME + - JIRA_CLOUD_DOMAIN + - JIRA_WEBHOOK_ID + +### Notes + +-To get Jira webhook id ,can use below CURL request with valid credentials + +`curl -X GET \ +-u your-email@example.com:your-api-token \ +-H "Content-Type: application/json" \ +JIRA_CLOUD_DOMAIN/rest/webhooks/1.0/webhook` +- self attribute has the id + - example: "self": "https://example.net/rest/webhooks/1.0/webhook/1 + - webhook id: 1 + +##### Ruuter Internal Requests + +- When running ruuter either on local or in an environment make sure to adjust `- application.internalRequests.allowedIPs=127.0.0.1,{YOUR_IPS}` under ruuter environments diff --git a/constants.ini b/constants.ini index 23b5dca2..ad94c4d5 100644 --- a/constants.ini +++ b/constants.ini @@ -19,4 +19,4 @@ OUTLOOK_CLIENT_ID=value OUTLOOK_SECRET_KEY=value OUTLOOK_REFRESH_KEY=value OUTLOOK_SCOPE=User.Read Mail.ReadWrite MailboxSettings.ReadWrite offline_access -DB_PASSWORD=value \ No newline at end of file +DB_PASSWORD=ad@1fgbR!sd \ No newline at end of file diff --git a/migrate.sh b/migrate.sh index 9a1b580e..779d9bab 100644 --- a/migrate.sh +++ b/migrate.sh @@ -1,19 +1,5 @@ #!/bin/bash -# Define the path where the SQL file will be generated -SQL_FILE="DSL/Liquibase/data/update_refresh_token.sql" - -# Read the OUTLOOK_REFRESH_KEY value from the INI file -OUTLOOK_REFRESH_KEY=$(awk -F '=' '/OUTLOOK_REFRESH_KEY/ {print $2}' constants.ini | xargs) - -# Generate a SQL script with the extracted value -cat << EOF > "$SQL_FILE" --- Update the refresh token in the database -UPDATE integration_status -SET token = '$OUTLOOK_REFRESH_KEY' -WHERE platform='OUTLOOK'; -EOF - # Function to parse ini file and extract the value for a given key under a given section get_ini_value() { local file=$1 diff --git a/token.sh b/token.sh new file mode 100644 index 00000000..b84f3afe --- /dev/null +++ b/token.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +# Define the path where the SQL file will be generated +SQL_FILE="DSL/Liquibase/data/update_refresh_token.sql" + +# Read the OUTLOOK_REFRESH_KEY value from the INI file +OUTLOOK_REFRESH_KEY=$(awk -F '=' '/OUTLOOK_REFRESH_KEY/ {print $2}' constants.ini | xargs) + +# Generate a SQL script with the extracted value +cat << EOF > "$SQL_FILE" +-- Update the refresh token in the database +UPDATE integration_status +SET token = '$OUTLOOK_REFRESH_KEY' +WHERE platform='OUTLOOK'; +EOF From 9e2ef3cfc41d7c0b8862822de230a090904f28f2 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Thu, 8 Aug 2024 14:18:41 +0530 Subject: [PATCH 397/582] ESCLASS-169: add sql query --- DSL/Resql/get-dataset-group-class-hierarchy.sql | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 DSL/Resql/get-dataset-group-class-hierarchy.sql diff --git a/DSL/Resql/get-dataset-group-class-hierarchy.sql b/DSL/Resql/get-dataset-group-class-hierarchy.sql new file mode 100644 index 00000000..ac395f8e --- /dev/null +++ b/DSL/Resql/get-dataset-group-class-hierarchy.sql @@ -0,0 +1,2 @@ +SELECT class_hierarchy +FROM dataset_group_metadata WHERE id =:id; \ No newline at end of file From 480feff433a9941e5e1be121070d87e85137c320 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Thu, 8 Aug 2024 14:21:32 +0530 Subject: [PATCH 398/582] Read Me: Remove opensearch dashboard service --- docker-compose.yml | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index bfe7bcd6..1b3d1805 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -272,17 +272,6 @@ services: networks: - bykstack - opensearch-dashboards: - image: opensearchproject/opensearch-dashboards:2.11.1 - container_name: opensearch-dashboards - environment: - - OPENSEARCH_HOSTS=http://opensearch-node:9200 - - DISABLE_SECURITY_DASHBOARDS_PLUGIN=true - ports: - - 5601:5601 - networks: - - bykstack - notifications-node: container_name: notifications-node build: From d0d2a4ce95b4deed7411da5e4d1d80c55a08f501 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Thu, 8 Aug 2024 19:34:46 +0530 Subject: [PATCH 399/582] Read Me: outlook refresh token flow encrypted --- .../script/outlook_refresh_token.sh | 44 +++++++++++++------ .../return_decrypted_outlook_token.handlebars | 3 ++ DSL/DMapper/lib/helpers.js | 24 ++++++++++ .../classifier/integration/outlook/token.yml | 17 +++++-- token.sh | 14 +++++- 5 files changed, 83 insertions(+), 19 deletions(-) create mode 100644 DSL/DMapper/hbs/return_decrypted_outlook_token.handlebars diff --git a/DSL/CronManager/script/outlook_refresh_token.sh b/DSL/CronManager/script/outlook_refresh_token.sh index ec5281c1..25740016 100644 --- a/DSL/CronManager/script/outlook_refresh_token.sh +++ b/DSL/CronManager/script/outlook_refresh_token.sh @@ -11,19 +11,31 @@ pwd echo $(date -u +"%Y-%m-%d %H:%M:%S.%3NZ") - $script_name started -# Fetch the refresh token -response=$(curl -X POST -H "Content-Type: application/json" -d '{"platform":"OUTLOOK"}' "$CLASSIFIER_RESQL/get-token") -refresh_token=$(echo $response | grep -oP '"token":"\K[^"]+') +# Function to Base64 encode +base64_encode() { + echo -n "$1" | base64 +} + +# Function to Base64 decode +base64_decode() { + echo -n "$1" | base64 --decode +} -if [ -z "$refresh_token" ]; then - echo "No refresh token found" +# Fetch the encrypted refresh token +encrypted_refresh_token=$(curl -s -X GET "$CLASSIFIER_RESQL/get-outlook-token" | grep -oP '"token":"\K[^"]+') + +if [ -z "$encrypted_refresh_token" ]; then + echo "No encrypted refresh token found" exit 1 fi -# Request a new access token using the refresh token +# Decrypt the previous refresh token +decrypted_refresh_token=$(base64_decode "$encrypted_refresh_token") + +# Request a new access token using the decrypted refresh token access_token_response=$(curl -X POST \ -H "Content-Type: application/x-www-form-urlencoded" \ - -d "client_id=$OUTLOOK_CLIENT_ID&scope=$OUTLOOK_SCOPE&refresh_token=$refresh_token&grant_type=refresh_token&client_secret=$OUTLOOK_SECRET_KEY" \ + -d "client_id=$OUTLOOK_CLIENT_ID&scope=$OUTLOOK_SCOPE&refresh_token=$decrypted_refresh_token&grant_type=refresh_token&client_secret=$OUTLOOK_SECRET_KEY" \ https://login.microsoftonline.com/common/oauth2/v2.0/token) new_refresh_token=$(echo $access_token_response | grep -oP '"refresh_token":"\K[^"]+') @@ -33,16 +45,20 @@ if [ -z "$new_refresh_token" ]; then exit 1 fi -# Function to save the new refresh token +# Encrypt the new refresh token +encrypted_new_refresh_token=$(base64_encode "$new_refresh_token") + +# Function to save the new encrypted refresh token save_refresh_token() { - new_refresh_token="$1" - curl -s -X POST -H "Content-Type: application/json" -d '{"platform":"OUTLOOK", "token":"'"$new_refresh_token"'"}' "$CLASSIFIER_RESQL/save-outlook-token" + encrypted_new_refresh_token="$1" + curl -s -X POST -H "Content-Type: application/json" -d '{"platform":"OUTLOOK", "token":"'"$encrypted_new_refresh_token"'"}' "$CLASSIFIER_RESQL/save-outlook-token" } -# Call the function to save the new refresh token -save_refresh_token "$new_refresh_token" +# Call the function to save the encrypted new refresh token +save_refresh_token "$encrypted_new_refresh_token" -# Print the new refresh token -echo "New refresh token: $new_refresh_token" +# Print the new refresh token (decrypted for readability) +decrypted_new_refresh_token=$(base64_decode "$encrypted_new_refresh_token") +echo "New refresh token: $decrypted_new_refresh_token" echo $(date -u +"%Y-%m-%d %H:%M:%S.%3NZ") - $script_name finished diff --git a/DSL/DMapper/hbs/return_decrypted_outlook_token.handlebars b/DSL/DMapper/hbs/return_decrypted_outlook_token.handlebars new file mode 100644 index 00000000..b345957f --- /dev/null +++ b/DSL/DMapper/hbs/return_decrypted_outlook_token.handlebars @@ -0,0 +1,3 @@ +{ + "token": {{{base64Decrypt token false}}} +} diff --git a/DSL/DMapper/lib/helpers.js b/DSL/DMapper/lib/helpers.js index fc73bb69..9387cbbc 100644 --- a/DSL/DMapper/lib/helpers.js +++ b/DSL/DMapper/lib/helpers.js @@ -74,3 +74,27 @@ export function getRandomString() { const randomHexString = randomBytes(32).toString("hex"); return randomHexString; } + +export function base64Decrypt(cipher, isObject) { + if (!cipher) { + return JSON.stringify({ + error: true, + message: 'Cipher is missing', + }); + } + + try { + const decodedContent = !isObject ? atob(cipher) : JSON.parse(atob(cipher)); + const cleanedContent = decodedContent.replace(/\r/g, ''); + return JSON.stringify({ + error: false, + content: cleanedContent + }); + } catch (err) { + return JSON.stringify({ + error: true, + message: 'Base64 Decryption Failed', + }); + } +} + diff --git a/DSL/Ruuter.private/DSL/GET/classifier/integration/outlook/token.yml b/DSL/Ruuter.private/DSL/GET/classifier/integration/outlook/token.yml index 2dfc3cd2..7fc61b63 100644 --- a/DSL/Ruuter.private/DSL/GET/classifier/integration/outlook/token.yml +++ b/DSL/Ruuter.private/DSL/GET/classifier/integration/outlook/token.yml @@ -24,9 +24,20 @@ set_refresh_token: check_refresh_token: switch: - condition: ${refresh_token !== null} - next: get_access_token + next: decrypt_token next: return_not_found +decrypt_token: + call: http.post + args: + url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/return_decrypted_outlook_token" + headers: + type: json + body: + token: ${refresh_token} + result: token_data + next: get_access_token + get_access_token: call: http.post args: @@ -37,7 +48,7 @@ get_access_token: body: client_id: "[#OUTLOOK_CLIENT_ID]" scope: "User.Read Mail.ReadWrite MailboxSettings.ReadWrite offline_access" - refresh_token: ${refresh_token} + refresh_token: ${token_data.response.body.token.content} grant_type: "refresh_token" client_secret: "[#OUTLOOK_SECRET_KEY]" result: res @@ -50,4 +61,4 @@ return_result: return_not_found: status: 404 return: "refresh token not found" - next: end + next: end \ No newline at end of file diff --git a/token.sh b/token.sh index b84f3afe..21e9d865 100644 --- a/token.sh +++ b/token.sh @@ -6,10 +6,20 @@ SQL_FILE="DSL/Liquibase/data/update_refresh_token.sql" # Read the OUTLOOK_REFRESH_KEY value from the INI file OUTLOOK_REFRESH_KEY=$(awk -F '=' '/OUTLOOK_REFRESH_KEY/ {print $2}' constants.ini | xargs) -# Generate a SQL script with the extracted value +# Function to Base64 encode +base64_encode() { + echo -n "$1" | base64 +} + +# Encrypt the refresh token +encrypted_refresh_token=$(base64_encode "$OUTLOOK_REFRESH_KEY") + +# Generate a SQL script with the encrypted value cat << EOF > "$SQL_FILE" -- Update the refresh token in the database UPDATE integration_status -SET token = '$OUTLOOK_REFRESH_KEY' +SET token = '$encrypted_refresh_token' WHERE platform='OUTLOOK'; EOF + +echo "SQL file created at $SQL_FILE with encrypted token." From 9cf261b968ac39a6000b6cf4f9d7d87fc105ea61 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Fri, 9 Aug 2024 08:11:28 +0530 Subject: [PATCH 400/582] Read Me: change base models --- .../changelog/classifier-script-v9-models-metadata.sql | 4 ++-- constants.ini | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DSL/Liquibase/changelog/classifier-script-v9-models-metadata.sql b/DSL/Liquibase/changelog/classifier-script-v9-models-metadata.sql index a43f65ac..01cb5580 100644 --- a/DSL/Liquibase/changelog/classifier-script-v9-models-metadata.sql +++ b/DSL/Liquibase/changelog/classifier-script-v9-models-metadata.sql @@ -10,7 +10,7 @@ CREATE TYPE Deployment_Env AS ENUM ('jira', 'outlook', 'pinal', 'testing', 'unde CREATE TYPE Training_Status AS ENUM ('not trained', 'training in progress', 'trained', 'retraining needed', 'untrainable'); -- changeset kalsara Magamage:classifier-script-v9-changeset4 -CREATE TYPE Base_Models AS ENUM ('xlnet', 'roberta', 'albert'); +CREATE TYPE Base_Models AS ENUM ('distil-bert', 'roberta', 'bert'); -- changeset kalsara Magamage:classifier-script-v9-changeset5 CREATE TABLE models_metadata ( @@ -49,7 +49,7 @@ CREATE TABLE model_configurations ( -- changeset kalsara Magamage:classifier-script-v9-changeset7 INSERT INTO model_configurations (base_models, deployment_platforms, maturity_labels) VALUES ( - ARRAY['xlnet', 'roberta', 'albert']::Base_Models[], + ARRAY['distil-bert', 'roberta', 'bert']::Base_Models[], ARRAY['jira', 'outlook', 'pinal', 'testing', 'undeployed']::Deployment_Env[], ARRAY['development', 'staging', 'production ready']::Maturity_Label[] ); \ No newline at end of file diff --git a/constants.ini b/constants.ini index ad94c4d5..23b5dca2 100644 --- a/constants.ini +++ b/constants.ini @@ -19,4 +19,4 @@ OUTLOOK_CLIENT_ID=value OUTLOOK_SECRET_KEY=value OUTLOOK_REFRESH_KEY=value OUTLOOK_SCOPE=User.Read Mail.ReadWrite MailboxSettings.ReadWrite offline_access -DB_PASSWORD=ad@1fgbR!sd \ No newline at end of file +DB_PASSWORD=value \ No newline at end of file From d489085f2227a16cac148500819b90cd7ba78b48 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Fri, 9 Aug 2024 10:00:59 +0530 Subject: [PATCH 401/582] data models int and training sessions --- GUI/src/App.tsx | 3 +- .../molecules/DataModelForm/index.tsx | 10 ++- .../molecules/TrainingSessionCard/index.tsx | 68 +++++++++++++++ .../pages/DataModels/ConfigureDataModel.tsx | 47 +++++++---- GUI/src/pages/DataModels/CreateDataModel.tsx | 83 +++++++++++++++++-- GUI/src/pages/DataModels/index.tsx | 42 +++++++--- GUI/src/pages/DatasetGroups/index.tsx | 2 +- GUI/src/pages/TrainingSessions/index.tsx | 73 ++++++++++++++++ GUI/src/pages/ValidationSessions/index.tsx | 2 +- GUI/src/services/data-models.ts | 5 ++ GUI/src/services/sse-service.ts | 4 +- GUI/src/styles/generic/_base.scss | 16 ++++ GUI/src/utils/dataModelsUtils.ts | 13 ++- 13 files changed, 325 insertions(+), 43 deletions(-) create mode 100644 GUI/src/components/molecules/TrainingSessionCard/index.tsx create mode 100644 GUI/src/pages/TrainingSessions/index.tsx diff --git a/GUI/src/App.tsx b/GUI/src/App.tsx index f5486026..675a8254 100644 --- a/GUI/src/App.tsx +++ b/GUI/src/App.tsx @@ -14,6 +14,7 @@ import StopWords from 'pages/StopWords'; import ValidationSessions from 'pages/ValidationSessions'; import DataModels from 'pages/DataModels'; import CreateDataModel from 'pages/DataModels/CreateDataModel'; +import TrainingSessions from 'pages/TrainingSessions'; const App: FC = () => { @@ -39,7 +40,7 @@ const App: FC = () => { } /> } /> } /> - + } /> diff --git a/GUI/src/components/molecules/DataModelForm/index.tsx b/GUI/src/components/molecules/DataModelForm/index.tsx index dbe84d18..ed076c9e 100644 --- a/GUI/src/components/molecules/DataModelForm/index.tsx +++ b/GUI/src/components/molecules/DataModelForm/index.tsx @@ -14,6 +14,7 @@ import { customFormattedArray, dgArrayWithVersions, } from 'utils/dataModelsUtils'; +import CircularSpinner from '../CircularSpinner/CircularSpinner'; type DataModelFormType = { dataModel: any; @@ -30,7 +31,7 @@ const DataModelForm: FC = ({ }) => { const { t } = useTranslation(); - const { data: createOptions } = useQuery(['datamodels/create-options'], () => + const { data: createOptions, isLoading } = useQuery(['datamodels/create-options'], () => getCreateOptions() ); @@ -58,12 +59,12 @@ const DataModelForm: FC = ({
    )} - {createOptions && ( + {createOptions && !isLoading? (
    Select Dataset Group
    = ({ handleChange('dgId', selection?.value); }} defaultValue={dataModel?.dgId} + error={errors?.dgId} />
    @@ -115,7 +117,7 @@ const DataModelForm: FC = ({ />
    - )} + ):()}
    ); }; diff --git a/GUI/src/components/molecules/TrainingSessionCard/index.tsx b/GUI/src/components/molecules/TrainingSessionCard/index.tsx new file mode 100644 index 00000000..3186c4ee --- /dev/null +++ b/GUI/src/components/molecules/TrainingSessionCard/index.tsx @@ -0,0 +1,68 @@ +import { FC, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import ProgressBar from 'components/ProgressBar'; +import { Card, Label } from 'components'; + +type TrainingSessionCardProps = { + modelName: string; + dgName: string; + deployedModel: string; + lastTrained: string; + isLatest: boolean; + version: string; + status?: string; + errorMessage?: string; + progress: number; + platform?: string; + maturity?: string; +}; + +const TrainingSessionCard: React.FC = ({ + dgName, + modelName, + deployedModel, + lastTrained, + version, + isLatest, + status, + errorMessage, + progress, + maturity, + platform, +}) => { + const { t } = useTranslation(); + + return ( + +
    {modelName}
    +

    Dataset Group : {dgName}

    +

    Deployed Model : {deployedModel}

    +

    Last Trained : {lastTrained}

    + +
    + {isLatest && } + + {platform && }{' '} + {maturity && } + {status === 'Fail' && } +
    +
    + } + > +
    + {errorMessage ? ( +
    {errorMessage}
    + ) : ( +
    +
    {status}
    + +
    + )} +
    + + ); +}; + +export default TrainingSessionCard; diff --git a/GUI/src/pages/DataModels/ConfigureDataModel.tsx b/GUI/src/pages/DataModels/ConfigureDataModel.tsx index fb7f80f5..3a7b373b 100644 --- a/GUI/src/pages/DataModels/ConfigureDataModel.tsx +++ b/GUI/src/pages/DataModels/ConfigureDataModel.tsx @@ -14,12 +14,14 @@ import DataModelForm from 'components/molecules/DataModelForm'; import { getChangedAttributes, validateDataModel } from 'utils/dataModelsUtils'; import { Platform } from 'enums/dataModelsEnums'; import { ButtonAppearanceTypes } from 'enums/commonEnums'; +import CircularSpinner from 'components/molecules/CircularSpinner/CircularSpinner'; type ConfigureDataModelType = { id: number; + availableProdModels?: string[] }; -const ConfigureDataModel: FC = ({ id }) => { +const ConfigureDataModel: FC = ({ id,availableProdModels }) => { const { open, close } = useDialog(); const navigate = useNavigate(); const [enabled, setEnabled] = useState(true); @@ -34,7 +36,7 @@ const ConfigureDataModel: FC = ({ id }) => { version: '', }); - const { data: dataModelData } = useQuery( + const { data: dataModelData, isLoading } = useQuery( ['datamodels/metadata', id], () => getMetadata(id), @@ -52,7 +54,7 @@ const ConfigureDataModel: FC = ({ id }) => { }); setInitialData({ modelName: data?.modelName || '', - dgId: data?.modelId || '', + dgId: data?.connectedDgId || '', platform: data?.deploymentEnv || '', baseModels: data?.baseModels || [], maturity: data?.maturityLabel || '', @@ -92,10 +94,7 @@ const ConfigureDataModel: FC = ({ id }) => { if (updateType) { if ( - initialData.platform === Platform.UNDEPLOYED && - (dataModel.platform === Platform.JIRA || - dataModel.platform === Platform.OUTLOOK || - dataModel.platform === Platform.PINAL) + availableProdModels?.includes(dataModel.platform) ) { open({ title: 'Warning: Replace Production Model', @@ -253,8 +252,8 @@ const ConfigureDataModel: FC = ({ id }) => { title: 'Error Deleting Data Model', content: (

    - There was an issue retraining the data model. Please try again. If the - problem persists, contact support for assistance. + There was an issue retraining the data model. Please try again. If + the problem persists, contact support for assistance.

    ), }); @@ -283,16 +282,26 @@ const ConfigureDataModel: FC = ({ id }) => { Model updated. Please initiate retraining to continue benefiting from the latest improvements.

    - +
    - + {isLoading ? ( + + ) : ( + + )}
    = ({ id }) => { > Cancel - +
    ), }) diff --git a/GUI/src/pages/DataModels/CreateDataModel.tsx b/GUI/src/pages/DataModels/CreateDataModel.tsx index fb0b74f2..2d047a24 100644 --- a/GUI/src/pages/DataModels/CreateDataModel.tsx +++ b/GUI/src/pages/DataModels/CreateDataModel.tsx @@ -3,13 +3,15 @@ import { useTranslation } from 'react-i18next'; import { Button } from 'components'; import { Link, useNavigate } from 'react-router-dom'; import './DataModels.scss'; -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQuery } from '@tanstack/react-query'; import { useDialog } from 'hooks/useDialog'; import BackArrowButton from 'assets/BackArrowButton'; -import { validateDataModel } from 'utils/dataModelsUtils'; +import { extractedArray, validateDataModel } from 'utils/dataModelsUtils'; import DataModelForm from 'components/molecules/DataModelForm'; import { ButtonAppearanceTypes } from 'enums/commonEnums'; -import { createDataModel } from 'services/data-models'; +import { createDataModel, getDataModelsOverview } from 'services/data-models'; +import { integrationQueryKeys } from 'utils/queryKeys'; +import { getIntegrationStatus } from 'services/integration'; const CreateDataModel: FC = () => { const { t } = useTranslation(); @@ -17,17 +19,57 @@ const CreateDataModel: FC = () => { const [isModalOpen, setIsModalOpen] = useState(false); const [modalType, setModalType] = useState(''); const navigate = useNavigate(); + const [availableProdModels, setAvailableProdModels] = useState([]); const [dataModel, setDataModel] = useState({ modelName: '', dgName: '', - dgId: '', + dgId: 0, platform: '', baseModels: [], maturity: '', version: 'V1.0', }); + useQuery( + [ + 'datamodels/overview', + 0, + 'all', + -1, + -1, + 'all', + -1, + 'all', + 'all', + 'asc', + true, + ], + () => + getDataModelsOverview( + 1, + 'all', + -1, + -1, + 'all', + -1, + 'all', + 'all', + 'asc', + true + ), + { + onSuccess:(data)=>{ + setAvailableProdModels(extractedArray(data?.data,"deploymentEnv")) + }, + } + ); + + const { data: integrationStatus } = useQuery( + integrationQueryKeys.INTEGRATION_STATUS(), + () => getIntegrationStatus() + ); + const handleDataModelAttributesChange = (name: string, value: string) => { setDataModel((prevFilters) => ({ ...prevFilters, @@ -59,7 +101,38 @@ const CreateDataModel: FC = () => { maturityLabel: dataModel.maturity, }; - createDataModelMutation.mutate(payload); + if ( + availableProdModels?.includes(dataModel.platform) + ) { + open({ + title: 'Warning: Replace Production Model', + content: +
    + Adding this model to production will replace the current production model. Are you sure you want to proceed? + {!integrationStatus[`${dataModel.platform}_connection_status`] &&
    {`${dataModel.platform} integration is currently disabled, therefore the model wouldn't recieve any inputs or make any predictions`}
    } + +
    , + footer: ( +
    + + +
    + ), + }); + }else{ + createDataModelMutation.mutate(payload); + + } + } }; const createDataModelMutation = useMutation({ diff --git a/GUI/src/pages/DataModels/index.tsx b/GUI/src/pages/DataModels/index.tsx index 030f6665..f80cb946 100644 --- a/GUI/src/pages/DataModels/index.tsx +++ b/GUI/src/pages/DataModels/index.tsx @@ -10,7 +10,7 @@ import DataModelCard from 'components/molecules/DataModelCard'; import ConfigureDataModel from './ConfigureDataModel'; import { INTEGRATION_OPERATIONS } from 'enums/integrationEnums'; import { Platform } from 'enums/dataModelsEnums'; -import { customFormattedArray } from 'utils/dataModelsUtils'; +import { customFormattedArray, extractedArray } from 'utils/dataModelsUtils'; import { MdPin, MdPinEnd, MdStar } from 'react-icons/md'; import CircularSpinner from 'components/molecules/CircularSpinner/CircularSpinner'; import { ButtonAppearanceTypes } from 'enums/commonEnums'; @@ -22,7 +22,10 @@ const DataModels: FC = () => { const [pageIndex, setPageIndex] = useState(1); const [id, setId] = useState(0); const [enableFetch, setEnableFetch] = useState(true); + const [enableProdModelsFetch, setEnableProdModelsFetch] = useState(true); + const [view, setView] = useState('list'); + const [availableProdModels, setAvailableProdModels] = useState([]); useEffect(() => { setEnableFetch(true); @@ -100,8 +103,12 @@ const DataModels: FC = () => { true ), { + onSuccess:(data)=>{ + setAvailableProdModels(extractedArray(data?.data,"deploymentEnv")) + setEnableProdModelsFetch(false) + }, keepPreviousData: true, - enabled: enableFetch, + enabled: enableProdModelsFetch, } ); const { data: filterData } = useQuery(['datamodels/filters'], () => @@ -165,34 +172,39 @@ const DataModels: FC = () => {
    handleFilterChange('modelName', selection?.value ?? '') } + defaultValue={filters?.modelName} /> handleFilterChange('version', selection?.value ?? '') } + defaultValue={filters?.version} + /> handleFilterChange('platform', selection?.value ?? '') } + defaultValue={filters?.platform} + /> { onSelectionChange={(selection) => handleFilterChange('datasetGroup', selection?.value?.id) } + defaultValue={filters?.datasetGroup} + /> - handleFilterChange('trainingStatus', selection?.value ?? '') + handleFilterChange('trainingStatus', selection?.value ) } + defaultValue={filters?.trainingStatus} + /> handleFilterChange('maturity', selection?.value ?? '') } + defaultValue={filters?.maturity} + /> { onSelectionChange={(selection) => handleFilterChange('sort', selection?.value ?? '') } + defaultValue={filters?.sort} + />
    )} - {view === 'individual' && } + {view === 'individual' && }
    ); }; diff --git a/GUI/src/pages/DatasetGroups/index.tsx b/GUI/src/pages/DatasetGroups/index.tsx index 782d48d8..0c4d3699 100644 --- a/GUI/src/pages/DatasetGroups/index.tsx +++ b/GUI/src/pages/DatasetGroups/index.tsx @@ -163,7 +163,7 @@ const DatasetGroups: FC = () => { datasetGroupId={dataset?.id} isEnabled={dataset?.isEnabled} datasetName={dataset?.groupName} - version={`${dataset?.majorVersion}.${dataset?.minorVersion}.${dataset?.patchVersion}`} + version={`V${dataset?.majorVersion}.${dataset?.minorVersion}.${dataset?.patchVersion}`} isLatest={dataset.latest} lastUpdated={dataset?.lastUpdatedTimestamp} lastUsed={dataset?.lastTrainedTimestamp} diff --git a/GUI/src/pages/TrainingSessions/index.tsx b/GUI/src/pages/TrainingSessions/index.tsx new file mode 100644 index 00000000..15424bcf --- /dev/null +++ b/GUI/src/pages/TrainingSessions/index.tsx @@ -0,0 +1,73 @@ +import { FC, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import ValidationSessionCard from 'components/molecules/ValidationSessionCard'; +import sse from 'services/sse-service'; +import { useQuery } from '@tanstack/react-query'; +import { getDatasetGroupsProgress } from 'services/datasets'; +import { getDataModelsProgress } from 'services/data-models'; +import TrainingSessionCard from 'components/molecules/TrainingSessionCard'; + +const TrainingSessions: FC = () => { + const { t } = useTranslation(); + const [progresses, setProgresses] = useState([]); + + const { data: progressData } = useQuery( + ['datamodels/progress'], + () => getDataModelsProgress(), + { + onSuccess: (data) => { + setProgresses(data); + }, + } + ); + + useEffect(() => { + if (!progressData) return; + + const handleUpdate = (sessionId, newData) => { + setProgresses((prevProgresses) => + prevProgresses.map((progress) => + progress.id === sessionId ? { ...progress, ...newData } : progress + ) + ); + }; + + const eventSources = progressData.map((progress) => { + return sse(`/${progress.id}`, 'model', (data) => { + console.log(`New data for notification ${progress.id}:`, data); + handleUpdate(data.sessionId, data); + }); + }); + + return () => { + eventSources.forEach((eventSource) => eventSource.close()); + console.log('SSE connections closed'); + }; + }, [progressData]); + + return ( +
    +
    +
    +
    Validation Sessions
    +
    + {progresses?.map((session) => { + return ( + + ); + })} +
    +
    + ); +}; + +export default TrainingSessions; diff --git a/GUI/src/pages/ValidationSessions/index.tsx b/GUI/src/pages/ValidationSessions/index.tsx index 67062487..483e25a0 100644 --- a/GUI/src/pages/ValidationSessions/index.tsx +++ b/GUI/src/pages/ValidationSessions/index.tsx @@ -31,7 +31,7 @@ const ValidationSessions: FC = () => { }; const eventSources = progressData.map((progress) => { - return sse(`/${progress.id}`, (data) => { + return sse(`/${progress.id}`, 'dataset',(data) => { console.log(`New data for notification ${progress.id}:`, data); handleUpdate(data.sessionId, data); }); diff --git a/GUI/src/services/data-models.ts b/GUI/src/services/data-models.ts index 74b8757f..46464152 100644 --- a/GUI/src/services/data-models.ts +++ b/GUI/src/services/data-models.ts @@ -79,4 +79,9 @@ export async function retrainDataModel(modelId : number) { modelId }); return data; +} + +export async function getDataModelsProgress() { + const { data } = await apiDev.get('classifier/datamodel/progress'); + return data?.response?.data; } \ No newline at end of file diff --git a/GUI/src/services/sse-service.ts b/GUI/src/services/sse-service.ts index 99a688d0..8ff7b811 100644 --- a/GUI/src/services/sse-service.ts +++ b/GUI/src/services/sse-service.ts @@ -1,12 +1,12 @@ const notificationNodeUrl = import.meta.env.REACT_APP_NOTIFICATION_NODE_URL; -const sse = (url: string, onMessage: (data: T) => void): EventSource => { +const sse = (url: string,module:string, onMessage: (data: T) => void): EventSource => { if (!notificationNodeUrl) { console.error('Notification node url is not defined'); throw new Error('Notification node url is not defined'); } const eventSource = new EventSource( - `${notificationNodeUrl}/sse/dataset/notifications${url}` + `${notificationNodeUrl}/sse/${module}/notifications${url}` ); eventSource.onmessage = (event: MessageEvent) => { diff --git a/GUI/src/styles/generic/_base.scss b/GUI/src/styles/generic/_base.scss index a6a4185c..93bdd0c6 100644 --- a/GUI/src/styles/generic/_base.scss +++ b/GUI/src/styles/generic/_base.scss @@ -57,6 +57,22 @@ body { margin: 20px 0px; } +.title-m { + font-size: 1.3rem; + color: #000; + font-weight: 300; + display: flex; + justify-content: space-between; + align-items: center; + margin: 15px 0px; +} + +.warning{ + font-size: 13px; + color: rgb(223, 116, 2); + margin-top: 20px; +} + .flex-between { display: flex; justify-content: space-between; diff --git a/GUI/src/utils/dataModelsUtils.ts b/GUI/src/utils/dataModelsUtils.ts index 792fcbb3..6028cd8f 100644 --- a/GUI/src/utils/dataModelsUtils.ts +++ b/GUI/src/utils/dataModelsUtils.ts @@ -1,11 +1,13 @@ import { DataModel } from 'types/dataModels'; export const validateDataModel = (dataModel) => { - const { modelName, dgName, platform, baseModels, maturity } = dataModel; + const { modelName, dgId, platform, baseModels, maturity } = dataModel; const newErrors: any = {}; if (!modelName.trim()) newErrors.modelName = 'Model Name is required'; if (!platform.trim()) newErrors.platform = 'Platform is required'; + if (dgId === 0) newErrors.dgId = 'Dataset group is required'; + if (baseModels?.length === 0) newErrors.baseModels = 'At least one Base Model is required'; if (!maturity.trim()) newErrors.maturity = 'Maturity is required'; @@ -26,13 +28,20 @@ export const customFormattedArray = >( })); }; +export const extractedArray = >( + data: T[], + attributeName: keyof T +): string[] => { + return data?.map((item) => item[attributeName]); +}; + export const dgArrayWithVersions = >( data: T[], attributeName: keyof T ) => { return data?.map((item) => ({ label: `${item[attributeName]} (${item.majorVersion}.${item.minorVersion}.${item.patchVersion})`, - value: item.dgId + value: item.dgId, })); }; From 458d844b8f3433627e4c7961e3e187eabf292a12 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Fri, 9 Aug 2024 10:19:48 +0530 Subject: [PATCH 402/582] remove postman refs --- GUI/src/services/api-mock.ts | 35 ----------------------------------- 1 file changed, 35 deletions(-) delete mode 100644 GUI/src/services/api-mock.ts diff --git a/GUI/src/services/api-mock.ts b/GUI/src/services/api-mock.ts deleted file mode 100644 index 4932793d..00000000 --- a/GUI/src/services/api-mock.ts +++ /dev/null @@ -1,35 +0,0 @@ -import axios, { AxiosError } from 'axios'; - -const instance = axios.create({ - baseURL: 'https://d5e7cde0-f9b1-4425-8a16-c5f93f503e2e.mock.pstmn.io', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/x-www-form-urlencoded', - }, -}); - -instance.interceptors.response.use( - (axiosResponse) => { - return axiosResponse; - }, - (error: AxiosError) => { - return Promise.reject(new Error(error.message)); - } -); - -instance.interceptors.request.use( - (axiosRequest) => { - return axiosRequest; - }, - (error: AxiosError) => { - if (error.response?.status === 401) { - // To be added: handle unauthorized requests - } - if (error.response?.status === 403) { - // To be added: handle unauthorized requests - } - return Promise.reject(new Error(error.message)); - } -); - -export default instance; From 9540848605352cf235c48f67bcb5a364d1fc1ab8 Mon Sep 17 00:00:00 2001 From: Thirunayan22 Date: Fri, 9 Aug 2024 15:49:21 +0530 Subject: [PATCH 403/582] updated main readme file and added architecture documents folder --- README.md | 5 +- architecture-docs/README.md | 8 + .../classifier-architecture.drawio | 2434 +++++++++++++++++ ...Classifier-Dataset-Groups-Architecture.jpg | Bin 0 -> 589697 bytes .../Classifier-Integrations-Architecture.jpg | Bin 0 -> 35754 bytes .../images/Classifier-Models-Architecture.jpg | Bin 0 -> 739346 bytes 6 files changed, 2445 insertions(+), 2 deletions(-) create mode 100644 architecture-docs/README.md create mode 100644 architecture-docs/classifier-architecture.drawio create mode 100644 architecture-docs/images/Classifier-Dataset-Groups-Architecture.jpg create mode 100644 architecture-docs/images/Classifier-Integrations-Architecture.jpg create mode 100644 architecture-docs/images/Classifier-Models-Architecture.jpg diff --git a/README.md b/README.md index 4134e03f..144ef445 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ # Classifier +The classifier is an open-source model training platform in which can integrated with JIRA and Outlook to deploy custom classification models to label emails or JIRA tickets. # Scope This repo will primarily contain: -1. Architectural and other documentation; +1. Architectural and other documentation (under the documentation folder); 2. Docker Compose file to set up and run Classifier as a fully functional service; ## Dev setup @@ -25,7 +26,7 @@ This repo will primarily contain: ### Refresh Token setup -- Navigate to outlook-consent-app +- Navigate to outlook-consent-app folder - setup environment variables in .env file - NEXT_PUBLIC_CLIENT_ID - CLIENT_SECRET diff --git a/architecture-docs/README.md b/architecture-docs/README.md new file mode 100644 index 00000000..2961f593 --- /dev/null +++ b/architecture-docs/README.md @@ -0,0 +1,8 @@ +# Classifier Architecture +
      + +The classifier-architecture.drawio in this folder is an XML structured document which can be uploaded to the [draw.io](https://app.diagrams.net) platform to view and edit on your own cloud or local storage. + +The **images** folder contains the image snapshots of the architecture diagrams from the draw.io file. + + diff --git a/architecture-docs/classifier-architecture.drawio b/architecture-docs/classifier-architecture.drawio new file mode 100644 index 00000000..ff58bb90 --- /dev/null +++ b/architecture-docs/classifier-architecture.drawio @@ -0,0 +1,2434 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/architecture-docs/images/Classifier-Dataset-Groups-Architecture.jpg b/architecture-docs/images/Classifier-Dataset-Groups-Architecture.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0504f737f989d9d71617c8af283ab4aaea60e7de GIT binary patch literal 589697 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

      R~CX3Bp@Z1n!WwMza*)UTp%4m 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�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 Y5p-X6y-Y~Zs9E&}O%E@0>`UMO0mH-}L;wH) literal 0 HcmV?d00001 diff --git a/architecture-docs/images/Classifier-Models-Architecture.jpg b/architecture-docs/images/Classifier-Models-Architecture.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b5c6580f049342b9936a571842d4b3de7e13f965 GIT binary patch literal 739346 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ŀ<3|FY1~S*#({&?z%^VrP|Ki4 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 z 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�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

      |an3XMstYylLZ(vdLl8jJM$G??Tkon#+r-*c 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 z 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

      4LHM6XZefK9+bAh|+y-wBrhhz{BGt(e_(s^l%G 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%0D}>?f0_u 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 >Wp#ZjFvI-IK3!GE|qASJ~fsY<_E-aV@2 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

      v0D(M5iCQ0z5- zH(&H>X0BlGt8r#ys{YVtMEV@HF1?Bt26JFX9>?8YxPvu+)+8-UAr-JJo7V5xXn+!2 zFa>JZwDB3P7YpMb8IeBID@r*#H=TJVOZ!RomlU=Kh8ApWTB$|=XFg3}a~n>~XGf>8 zJm)>d;&;HM^E>*=XX7O!ID#b4LW?pFx0V4{S6KKV+?RNQN_25MXF=OWro#_&tQ*LaJg4Q|QBOTAg*pxCk&mYLI8e!?M}nDQxsw%J4BGMUpr|Kiyf$)nMo9 zYQPRz^4+Pv_Ukt}t4<+3e~Dl`%6cN+LiKA=p8QRSp4f zp>pDa?Ox@k0>1=+^Ktr_zUI6lEGkwMH^kjbFj&?HN+0ZRZU!7nifWycck28tV-U;_ zN$YMOP~Wq{g0(ON#lq2>M{|r-jdQ>EHaM?8@sXgf+o*Rt?Wj1TMJK9d zN{8)-su5Wdc^dYuqi0Bc0S_FhtV-ZI4erkQT!wShThmxpGC}T5YHN*jpd9c-8z3Nn zf0`zo#9>sFagcJrdx1UN^-2jH;X6lA zsT;9#TO`T4h2i=C!QOj^HI?ms-#U&m7IY9%IwJv*CIZsiD7}ji0)(PeNhm@nLMV1= zq4z2vEg=a3Ll20w&_xI#Kw#*-6FT_BbIv^Doa;VwpZk8_>%E@$dhY!Pglp}!_RePS zz1I4z@9*<>gVtym_{Vp=GuiM?uUm-71 zn~dBtPgFT(8RHM>m7NTElKv=};+cG5)VfJQ0oP}i=do7DDq@n$!k5bL;_5H21tn|d zrq9qqSrAUJnu?NdBcuOR4D(G6^Brw~E{AC}Y#6YaJq16eIh^>19Hzm3TS1hl!hp4l z1H>Rg_DCCh-V12pHbu<&a|(uTuk> ztYJs1SastZsp9*R@tsY2ovoKHdN8%cKY21UK)Bnc5|~QvUF0c`y6J9+hosj#tPDZr|LuAQ#FBtd?2qE7@)LE?m?;SQ@B{GCTS}`E|5tb~!8Me|Gyn z_JUYzy`=`G4bb80qk-X>bc`ZlAynnS26lLih-X}ob1FByP^EaMZ>euBVlYDaW6F66 z+tMr4sP=nJDKtJhn4jM|6u_PNwgRgLG#tfd`+FNy!7Ql3qc)E=*ACLIl;VW}amOa6 z1rC0!^`F9Lq^K&d5p>ti@X{>jly2uN$*z4tQvNbiya8P9hOFmIyKOLM`E>FLCDgYN zsmxk<_n0hQyir|Ar@B|Vk6+8Edm>2pmUzN%a`q4V^iT98xJ%>Ykgq4UzMj~YX1RZ+ zH0`Rp;%t7_JrrHH&{OJbT}LO)zTU8Ns!k{*7*_!i)|u0mnQ=Qr2_p4Dm)UzSH=-ek zZIm>J)|Cc4rw<*BdxiFmzhrqB`LiwHvkNSQyi+I_F1UD9AhC1`B(JxPVeKne1`yKH zT;rjR68A6cc60qR%?urU>|p$mzQ zPW5xf?NHEx+_(eh(&q%@o?#Wq##^4oOGaCG>>m$W?tiqK+yCtL=j-cNEaz(-buF9|U# zcI!-bkFYijN-1j#IIJyx1B3u_9H3&3p!y?Qi@j-h*RLm%B=?pyQrb%G(ao<%x6)q= znFylG8X#Y()*3A09PCyfT7HP|E;5d+_17i=o_x&jSm(cfri;9qQ)ePo@>g zxe;;v=b`g`Hi$21{y?)?moj;o;&UOz>UMEowp2I_LsXb7%~5v6)4n0)Bay3x^eF|_ z?J~lo!IBV%((!)IxtkM{QNd)E=%DdF!aX3QRqkhn5>D{T>8Q)YSA9=48;nd3Y+UpE zp@ne6Nl^7HOeA?5x-n(LmD(?m`Cd9r1QKCln$xmtIY*`fD!QGfthUKQJ;=FQYDC{{ zucs?B;|uwd^P^yC;o*&aRKBJ_Q7`HsM<%x(J~ShQeE#L*B=u5LlE?~2Vo{5XOKa5N z4m5Rp+9)#bvT|mw6U#ykP7X;@H?>4LW~Iz~`scF%HNA{Wo}GY#>)@~kAo^BH_WoR| zIZK)9_5&enT_dH7D={owq(H8p8CAZIM;SbvMy#gHj>+1B(-{|>p{rNlqpo4XqG%>4 z^h_#_rm^LuA+Nb3mb;@o7D#+Rh~6VlEgmNsAN?%!E&+ah6Wc~Ux>c|bpj^rbGN_;T zR_XmF6#PThr1XDH1u~asWOKP&+SQj89!u5bV4LQN&2yx-M(;%7(ZnSqr2HKN5)`*V zQ$L7wR{u5({-;f+mM;B@s|oHt)3-O^NjGNs?$0OxkU8^P7OfFJjeWU*ozZPJN$@QA zv~xaKOQJqS?C;S@2JWXXcFntj*YUw+Ek10mnZMLsh1nds+AOW7x~(X$GTUR7hrQSxpX|oVrGS93_pmin~KMxtb&A-)sUS1NQs3r&YE; zOzxBAA~kXCWGVlgoGlaM-E&Y^4EDOq)zo%5WaaX6gYf%OnH;Q|b_|#ozK~VF zlXUzNI;}tOIv~+kAN35H!tv-`sjizjAoMeUo!rb(|8z>j$Cvlu{Q`=N%+B{1;ME&l zf0Z-Toyh~b%O9`d3wQ|awTBVTh4i~yBkQ8Z#m$$(rZ-bK%aV}P3aQsy$m64h)tdQ( zd~5N_u-h=SsefN0EWX=_Io(O=v8@w>ZLK||FR|FEOg3W1POx3RbjzTAZGFu+oQ?pX zd_7^g8GZ53e@j&c*FwH2zLl4iR}1JDnVxx$s-hFIbZUWt7{%}-9{L!^gw9Z7v4KYM zem$YN98CHF`P6k7b+4F=H4no>daIrDQ|4MUS_(jN-8T1ld4-TzI)y}BXkF=ecf4a) zz~7c4$;#!}2-?BE~?Yz&0>S^0k5C0BchG3XIT@^Js5bFJ!kOu8}Ow3Ot)jhlhLwmg9(XI(^;>l z3qNgX>TMb7Qg-+)dg1rD?+TA)Ul^WNvIg zNjNP60-rY$i0aYc3%};+o67=ZqADF{7#U;tqONbUq&E=7JujqZa5qRwmT6P@5L+6% zM?94%rRnfoBL$_EkSuTcELB-lH104v*Uo2ObYsl%jzeXQWhi4}4qvfB-GLF+UyN$- zrms4%d16P(k-EPSHixqB)LbDYF=+BF_1yaD;hn4VYx|zlK9BRAkr_DV*@C2kM8%QP z;b&)(DbN1)eSbb0z}s9LX0+BJd90g!k=)a%T&`z670cdV4E_+)oQypW?&UL|g^J{O zZL)49j4rUOR$N(ciNz$t z=e0C|fBJ1o09qq-seHKrn$h7mN${}wv_pSBwZv^w zZYKO=w|}#xzqfftMFsoYcDmj!R~)mk00KWe+h1{6_X3d&h~aMa=ghFke66lNKflFx z;>WCJOL5RzLFete%z!Xur9svf@j?XDn1hb<5vUD${qUa>)BiZ<|L~Je)y^?lMV2-A zsGpva5%~24zNh;P4}E4srx1SIPMJ(2u2EFOWo}6x2yRP~-Rff+{40o?1lKhO@$hG8o7YZ=`6~f@w&v6#;r7%@itBtCaAulE{U?5=-6zUZ3fuWZRuu#EMNfZ zHrAkU25+2`BWQQx-pbmn)D-?Ou^?u+^47p5NyP!WQfoJCc)=>wT#;g*%F?y1wL?Vi z$kAz8@Vte(xa%rBPs8wuxmjgx0W#9YY!^uLR&img^8Pq|%B+OoHMxw z3KA|ogG6v<9-q~RVuq^t>?LSl^3(qa@@u~qmUbOEjP8W z-uud|D4A{)bW63h_HO9M*+)e+w|b2^h{k!W){$sKDPzXKB96i0x1_6>Mm|Wt`o=** zSmwTW){V!qK_-)3aR_M+8R1Fk2&;7cHfN>cOj##{*^b66zDtu=&R+=MMjlfaFu!y* zz$SXAf~#R*^YbXR)c@FY(+?OPwv=*{#kwvFhe7BllmzD(9XyEsBAIzdCXmB1 zsJT7B`$itNEzh)LQe1MTh}bc(GPP)moJUth=tSD;)Ojj(aJm44L0dwDJ!kiUdW~5L zL)&zYt3?1I-4qo_195~W-Dt}#FG2m{vZ5Z@l_*}Sv)w|;L>TILeJpFZkqu$9D)X%8 zUAmUj)YoY1K3bz)Ha=e9$q}DrZi{Rw{@inlYepwl_()K|-%E{AJitZ&gEn^BJH8q4 zB<&28r_AV+qtMKp{RFz$s{Pjl_og17=71+TC@{y+?t8nm-fK0m&$cs6 z3v?RFP!#tNYj?Bxm8-(-z3_{2YoCVo6Q$q}WV99QoZK}9stu@(XU97jOlO$I%U9u! zlZlNp7$+WgDJ3WAk#h0U$*l9ggrz1CqYrx9G)b{H20?f4+=5QT;IJzpr8&F`u!7%Ob=)dohTWdr z%V~`jW)=Crp{O`ROnpe{oT(XvkdQiClLuMS7BIJrc#T**SXmx(qBElMm*M%Ue#EIV z@v%1Z0a0B}NwsMFu5j5%AwnHt8L}s$#J@1T0+HevBmnP$HJeR)Y)(e&J4JcI2Q(QL zbH!6#_8{^31rgb41f8ob5wFoHt29n@Xa*W+e||=1Ua4=}QYEZfE>K0dLQ*&LhMlyl zF?i$Zh7fUS#4LqF#=7U8`nFXedX6xvvk#`Oc-^vE!FS83k(E5FXo@)B)g44M~3h}OP|7f7E z8gZzE5NLjCa0R2OF%1H6hBakl-(>jtQlxuqI1C51r7u~97wz;PTuI4nHU#qSaTgu! z@7394Oz+jLRut_yy{6u%!{#sSE*H4IqcR&*<&CeJ)N@KxOg}wPFPZ1K99+j*xywnG zV(&&C&&aVbof7|8+jI;eVV1(fKAA?Mr%{aQ+^uKac|0<~+_CD8H*iMA1hcs*L=-f+ z?zKUrg}KUv8&|8xgW1*nnGP2}LjIEP#-3$7@Ook=?i11cAVKTV>%s_xk{}N(0cbfZ zM)-S&07=KKnO6mCZm3Y+OP85UN4@Ck*jI@;?)v4Tz^+JNaPIEvQg50$(nF=eG3|(_ zHP|s?=;QBQ$zKx^{wrPAA1%@YV;K8g_2076&u39B*Ma<&ZEN8TDzev9s;hqcJujE1 zyRdGJ=kvE(mu5|No*h5?zJU|}k1^EWT03PiDfZIUa4+)hwhxCO+9RoEqE*ikSW!MPQy*(d;IVaI5*Tz#hQw)?(8@)~mRlSj}|TF0{Ag~(f+ry*X8 z_Rd2&Wchv{4!3@B864|M7=@cGpbn{GVF>`&ht_6s&6WFIa$Os1k6$<2O4OZ;{8(q< zAzAi53hD_RA)`p_5g>&Tg({8}_>0lA(?Paj=hzPq+v2n- z_|Ys+dR*8yTj_>7OPeJu!_8_}>FWtFvBCy)!wTZ?FhzVJ1X*N<@0j*d16m(vZagnN zWxNfZNazRG7u;9~!czreN5&|~(Rb$ZTT8lJ$2Pt;ng%NbL!FDR6CDavjO<&VrWzT; z(^or0wB3}OT8UVVE-JOxI9GwZ%Q_<$cv1cS=-q}*_&vo=-Eo!-Fd7cz2OVqZ_Z2s;zNTzoCW?aH9kQXK^xOB)&txa!#Q!_i`u ztZ)P4puMTQRLrWzK^_Ek4O{K5Zctc&YYUidJ~YlBruv}kuMG7*TArB)hF`Z5b%2te z_Ynkf8&U$ExVl-qzZlFP$xAyGc;-X5_Gjg8W0Di~ws+YMGH>wq=F~%n*-z+|RV^0k zLcgI7lD8mtOsdPh$@k1Mdtl&pk|0#7#An0w$r_jt|Dueq>PHM>UCNqnw1+b4QroJE zvWxpX1w8jFe|o|M2XOL2UaGm3yIq@6Q!hHcdwQKT9w;KJa9Nwv*eoCx*RwE}i6n9e z5B0CWjF4e9VH+ykHyV|5YG$fahPpL+A`S!@kP|?=_iA2B)McO@-Iz%yh=#1&xXJPJ zosNLM#&~g$S+R}oG%%M``RB+f%0lt;?L=vrJZ~H2a2PlWHC&J9-v6b|fO54gjdByvb5Tj=SpU0@GBP~fakSwxvGfr$U z*|S7>RVy8xwDY5Ha3u_g*@7p~r{tdpKeCg)o_LjhZIc^(pnl3h3ib8GSy{0e#ft4P z`kub)k-OjqKkWtbQG)dSlf3Wfuf-pLn7|K*Z#RSUk39z4|3v0<_!}zWPbn9kNZmtY zVCK#nW$Qds<-Ov^e!IEuCI+(#Jl~f8?)igOp5JG9lUFDPe6wt>Fpi_U7E@UiQl}Qe zdjkdMY^wd9adUVJ*$e64J2ERCDC5XZWlN5t+q-AbyV<~VVPS=Zb+6SGKQac9c^-ox z=Gic&=06Zih? zC`nT+7ps0iFh+`{$i{^YqilMITdHfkN8UeChhG^{_bPIZZsxrG9tTk1pX3?p7!uk>c&Bb!iTjoGFK1B29w63vvqNiv z`;!Fs^M(wL;AG;>iQk^b7wdB5CO$So8{X1v1q{wsq4r*!$J#OSB8DhOKH}DXQJkpc;-ZJ9{;P*vJ zZ}zTD+qKiJVr+`S=;53Ho67WMc|vzO2D$QRwfY;TN;r28zFQ6u_TgAi8NY&+WNoJ| znXnKx3jnf)p@s$uGz#wj$#0&aWQ_N)*blVshN9ix=9scch!Q#$B?aYwgQ$L2CjPdi zZ%Xuh`kf=NKv$aE%2Mg@4*E#YDPl>2v7Pg5 z`N~0^szZzY`p*O9(Yb7jrSC&2%&Y_>91^~4Q?trmxA+9KKFIDmKA(yl2yK^>C@OO^ zX0tnuK+fu2Z5htmo} zxVm6!EyMAUJLwXnWsi>VSmQZxt~Y^o_%rAZp)9D#9KZ4aFs(~2hmPX28gi8TdgAag zf{KGxR0y-<0IK$TNB|uqk^|U-Kh{T zG^A=&2aVQm<)qEx?E=1@sCjzn=))M2n}parUj9vGyDz8^w{4obIe6&p+*2X)Ij|`E zkX(-}u90k0Ag=l?_FIL&?0-5WA=wdIM_2l$3Hi(KpqMz13FyTU)GGVB!*;QI){gs0 zdNeDIruyAC``PNVh3(d?C&{>vj+BA=yQ`T;psD#Pv-j?J(@`r?FVby}rF-^gV{N{_ z1XfS6M#Q-xppv_=l3(#aKsRi$s=Zm0I4hv;$$QXoc&4pNlo|1H5?K{eaisDhRD`~Mb9s!xEGnIm z`G6GukLdG>lh{>o3!&A&i-2;dZl?)VjNn1kE}@+5rluWIx5<{kkr082<$8DhWeNB(FsEVo%4%%{M*rdz8_*`pC zz-Cumm|L2c_dKeGgidtMi0M!)TZ_8wWAp65bgbCbD|2=~QJ6N3U;*Lz0^BsW4l?}3 zGyMGnSL&Zkn&Nvr#StL6j$S5aBr>CByxwj6qVS~uU}uHYoPNjDl;xYdp=HLNs(_)8 z!TL#gZ>UATb!OzsN?1{^R&W!%YR;mhzfTHeWirjgELjRmq4XwA#Ce#`F3S=mne@fw zI=d~!<275?yq?{;Gdp_L2A=vs>g$Qk$gIT+n3{G@r;5=7wxW$2xYVj%tAzj`YeI@Y zeHCrHg=U?>6igG`Td2J#M3n^kAVZ=Q`&0z%3RrBbNcoj1^KZIO2o-$m6V8HOt>W$8 z9J|w2yFRl#0FEmxn~m3}9lw#>$q{R z+Vk5o#t+U}GaPZtY@(OK?@)XdkD1%`YK|=1%{swT?(j!AS0Zhbl)*_K1w=uVWwc`O z%bu@AE@Lf&1dng*qBfFZ_Cx$9*A3TYnr-51qxyfPClJwEC$c8o!|Db7cnaTQRIbqF zBmuxZUE-12w|t?0SyL7}4o?k!&x-Ag*D31R#{im@%PLlCK)7HQbJ}=x5uCWKMl9>E z)4wUS?Hl;g`nXOr*cvfU?aSF(sImF?kMrL@L&fhQ%IRNEbg)KUIm~WOOJX~?wr=|S z`VWN}AX5|ww9pC|6I zmpMzsPrcA6jfLjpKfb5tA8MI@h5-E`u=(X%O5p##G=EX$lliQ&C92x)1Dm3vPL)gMtrgp= z6_EU5SN|^qVe0Ujz;+b6c_{o~$|XbP={w7l|6Z&A)`=v*g6WZthZAVe`kD&(Q>-iN zm~P5x9iQS3p@I=xKNnlNVUoP5KwdW;HoQ0ixPI_P@`d+dK=_<_nL>`ljo>j}(;~@? zzP`D+QPfa|5sT%R@4X3^Hyp6~_L}4Vqc|ARNYB!<@jP;J{SL4bzVM|O>yN|6%8v0x z!KY$6oq!s+h{q4U-P-?^?JsQZ{?*LkKWbj5_WRdTEcQ>7tKTbJd@{)m{d~u=hwnFO zZJ(c3WCGedj%foDzPvE2X)n``)4naM)#1Gzy+%gZdhrx6hq`~6P&%M^$|Wj6m&<3IiM!@q`7pFwwXV1E%1Fqu5Ei=X$Xlg>!&?z3fAG4=M}}52--!-nA;W#cfu+w zO@&Er^6{R-=2Py-G~pCCT_Z0Tz(rh&yA@s_py!tE)XEtd&q1`fSW|XG$e~PKl9lMj zZw@K|wa(zFky`?J^6<7phBrY16N?I+mKk=D_#&TO)|lqZAfeNBExB-}%iiw(UbZOX zzBvdIKKe{{N2Z6H7tehwDnZN>;Syf1@8qAEFaAUd@b;P$i$(vIi^i6FK-9eXWep<6 zu7<$qZfIZ=vsd3FaLKY~ZhObjn$0rLN@zfa|LWD(<{o05_HKQ6PVqKA;FKDTBOuD8 zJtQij2rwEtzo-!4+6r2~|C{?``RNFUr|7$kf*$vkf0`&IOLTCsSKv8>?cw%n22q+- zTqG95QCQRmrBsm!4(^2K?V8EiSH*rAPM&kr^_qZ>vG~DL3G3n;#2Fmbf~+;IrtbN? z-9PL0=Js%UqX)OUSC)QeV3N!|{TyllS8>_9+^7OaA)jARqG3M|!nVG; zV3R&c#g0ykNSNyg{@Bo}dbBxgvRGf+!(<*KEi+X-{6_EfO|<;Y-X1M$9A6)w&Vk4# z=eBr;0=wI}ab0-_Wx28@f0rSQvKJn7{?^5^xkeeFjd(Olf$7L{1*5h==pY3O!K>W( zCps6pm&Q0CI|Bp!m23muytUys#Ic&?X*J1&hnjedR?wq}p0X*SN1{_%rjg2diNB#<3P`;CkU!g7RSg0mvo>pkGuuyRnl4W ztw(7{Z1Ny5BZRi%3*z;*p7q}8-%tr#C$X`J8ryd+Jgr_@ddc$K)#5|;!Za))#_#+P z29`cOSC;^^sb&z}RGud)L=Fs_U)0Ku{P@ss*nlKFztVblri;`3g7+JW*Wy7%24JL2 ziYcJ}Iza*0j?s?_p(k;c6Lvyja@NMJ_P1+`xEp+0(Az>EByBk!$jGw-5;7cY1mSim z_8_75v3q#bVAAw&EBttz3AHTU597F99Z@{6WG zfbw1jsb}!xV%8r$)$8+?x5R17^}#Q>z?NRw{3;3Q&0;P>XDfJ4NDi8&Xk&IV{gQ&|Msv< z@WsI(wU!4BZg5OPR?|TH=e8b1r!*Rw7>j?SKdsFZ+Yhs$3=;ecpc!+l2?2gz9A!ju7uw>E<5pzp(+T z%zWQ>--0w2{-(34a;_r=&M|M=6f2qpuwR~M$6v$wa~$_XO&yZO*N@*CHtD-yz)V@4 z4U3U=SJO&mcX?igIhm$jZArKqgw>Q9wNVRm^p&h)>YmrCw0rwot|ughl!Zj;xojnkC{>pv7(M{%m8@ zz9{3*Vbf|`Ow_2%J;RgjlseWo!(vW2wneb9QG%_uz?a+>bRrJDe6)%6`MfFb3tw3Q z`RfuQZx^tab3q?>E$r_02GqJS_v4WovvQU@(o(OfEZy`wd2t>HJjjP~6h8Ez+>~aR zTpa0XjC2&F)JRQOz9Ly1(;0i4_`+!?f7AhUE<6ELa9`Hu^aQ+VW4)P*q|&V<2ZEyl zeJ6_@-Ze;^^UZ*y0caPlxOO>0F1i-6&sx}OktLKdno4Fv)_rY?h zZ9UnKh4~X&$30*JiPQs8`{-$5spTr|FMMmI9}*67-A(0fB&l2{U;TE>I*^hYZarb8 z!rq0LJ8Tph7uFbgky{hjqfz!ggNsCrq<;yUKtq^t)1g(y$h@btdn6upU@8^iph!wL zxlF}G0AUI@V8G>Npl3x6U6@*6fMnoV&b%#T}V_LO3)24djDbBiSteg#>jpsA^V$l0rsjJk1ngtV2?6I%M1vBNLT z{I6o$3({lNZ)vwTLt|7tO?>_36#R7+8frz$%9yxYn^JxBQx9YH!yIb4oWbg=!SI1A zFc!c{aEv0=%gB z*-tF3#PYhP3d&Paw)PU+`Yez2+jEtWzI;QH$#t-ip=7T;lm)RP%#J6{dasxm^i|J6 zf6S8-YmHZlZF@i;u3`QXKFhZW zSo0*dbN;c?d^!+zf3D`Z%>wReYl=qIF8LPom`k)gq=@ zYTm5Ljz@ARq*RVQq#q=Nvo3+d309jSbH0b zdiy8*hZII)SGu2WKQ|Zb^r$qI%oe}_3GVb#DJ!99E$zwsUw7we!O)Pa(KD-;5An70 z3VJ@3v>2;2%}=T+1O(2_OxUU-o~VjwZG2>BqZgQEKeeMZ1)X?(gc|Y+Eu59j{gtBJ zjd4G@kVkRMR@OJ|?=m0LUJA4hBj@0XVeC^-1q&nwsJ`NB&cZoX^7O-u5;VGWUw13a zuj&KbEWoYt%r%o9OTCOVkGo25`>QT^n&uN9l{Q0N<*We7tGne@7#nH#mK!_oUBDZw z{sSCby5Q^*aPz~Z&qQNFw8J%p{xULAiA!h8hZO-Ez6M>~Il!!ty~>xBId`LA)V^pR zM*k^_W`_aXLp`bLH0LWkXC#Wh+M|7FJL7UL{HobV(NO;8cY~j$-Y9}gJ-TExNtrqf~2b1F1W!X zJ+*_41FPF?>kJm_9r6UDh(OZy+g;!FHA?3`f&wIBFKa7 zeix2)sDq`lsDWiwI}*%b9?mgq3I%9x=iKcpP~Y11tN*k?Uo3<&WCQ8h{zt(loh)7m zd)lMgtkWpx0d|ggwgNp?=B};R)VC}MKap%zm zb&Vx1?dh=G8l*`L^(v#K8(0QVU%IifEk7M08kU zjRm$PZfF{SKTIYFQv!m(K$I|dQEw5+sOU>f(PQ>?1#0R7w`_o zwztPN2>GfDTSnQ!6Ol3_&y|{=7LT}WW+9$)A(b7fWUgb~jI6q{z=_J*J;$up7-@78+ z?Bi_ST(L`UoA>2^4Z)#pk%Hb)pOP3_ag87HE8@Wko{42r*Mpj4yyX+od_}I0kY#&J z(!t8^atK=q8*%E~>fOogaj};-p*yq!XbO}h*6;vjPjvAUPISKez#+BRYe`=ruW-=8 zdYkW}6Ex=9nA#6FGx_#%1xgDf4u{c>Y=>RTSr=^J;;g-L|z!RHDO zG<=kJzC2Eyf>BniW;=IL>kI`GJPDLL%V!o#Yu%;4o_LK#;G9`X^Ev}Waur5bBGnvL z^_DeqGkT2>8c6wB!$uI(WXa%&z9QFj5_3w@qZdaJ&BNCm+jLL8ON2}F2z9u3zV2Y2 zOzE+koNJY`2niLPz@9cMKI3z29Fy{<-oVwWI{oP(uoD}-Usv>|F})uTQ7^iz{6X2u zVqtLds4u6c{xi2Iwn-pFR6}%3)c17uU~ZE)Zo@U zD!VUZ?s2y0_^O_VA*!`x^Zt0b8i^WW;ZXO(cjdX>LpP@rEPLnRx&LRzz1{- zY;Sj+8)M_EVLRw{rB%C6Q~YhY5a~|uO&O?hU4p|V)E`s)p!~cwm|Rr{<>~9)Vfy*g zRDjUh(lF!EFb8J%rETVbrr-mo8Ejpi9%5+ou4AXkfV*+aJXkSYz{PnK8EK7y0#DtY zjA9OmxgehjPLb-=+w!F{`3C6gi?IY>4@$EOyxL)5W>#AF32C1Vw_#=voo){cLug_Y zVigTHtyQj<%~X>02lB{L@uZ~wG=6BmL=sO*N=dnZINTJK;}rp`C626QfCt0pg%nSZ z=id9R_y)^v`vArz-g_aLgH6~RoWxGdr9*a+5}jC`5@l1rOcSjZ+VVo?G(mc4mdRtL zRoSxzYE4s15`+Odv+G}OAd*j ze&eI&3~$IEMPP)%ozuAGA`qL-{-C3LQkrXzr~Sk53Icl=nPAn+gj!rU=9i1Ro<8V% zl#v_=WsID>S;k#a=7s5XVbhqx8f$0n841{XwwZGxytr+fb}bOA3z#}rYtG#y_e5G} z@Mcut(V8=Ni$3e0{B2kMLYlMb@_0JPr zd3Egf9|pSe`|s&Si*G5?^Q)w=8af#p(%xR^6sm(uBy4C4mS6&quMa!67?u#36wzR| z(I;QWH@s|}mKZt)F{<_(H0j31HYFdpxqg6yt+fW5;Q`$<#IS+f=z)^V8*(tcNooG{ zMCR)il);@EUr0w($I@lLr-%K$5_m1=oDLR^9Y1GBC+VAJ#5g>a*iTUSB*sw3L0mE* zE~apJZAea0D;zSS!K4)7>*d2x2S@vP&M#`EQggE!MeIl%8WNN?e(9NQv)z+;8Ye>D zti|Rzu79xfK00;BiKdR+w(j_8@xHc9jhC@7h1}bhgS@E)eHt^h3j=rWNy9Br7)t+^ zWnz6Ou@5iw1)m&%Hkr4w5-pPF%c^YV3FvmY;N(;dLvwS;`6_kF2SEX4lWfL+lge`nRg`ZP7 zu2iNQQ^ps$$sL2YULjadt*$zh`gbwh$vWG zF9uaD22m(0unzCT);{LEHc_q=6-p!I#+RgZrrq+4lSrIwSU2stpkQdL zGeNhA&CjOtn>CllQYo?>_K_gpeuzL4b0J`Q>);6C3-Pz=RpJ*O7HzeGr=#`+hUze! zZ`T%^g7Xr*-7@xO*A7$b$*f_}2%4IUF`VG2A)(ENoCUtzEB zNw(<*BH|+z-*0I)LuwDNe!H&XV7iZ>45-uIJ3@ZCjODRe8c&qA`Q_o#!4aJ57$CJ} zhrm9H@syIM0oAtB8yGHn%A9qa8~fx8zVyo@pNn$Y`Ag0iN8w2r%SM7@nZ#J6bFt~N z%WA!YkUO2`TI(k@`%2oO*oQ6!bA==DS zr)Z0NtNDveYP*V2CPEskLQ*C`Ls5n3lTGQs#&jxj`6>FXU<(^cqK_)g880mBiXqWt&ojLmFLs#3!>k;j8idGg+s`Z@0XlIw@e~`)aY686Dh{P``)*=C{&Hz{m97(f3bJmahn5=Fox<>>eukc>ck5| zqL;5-FAGCih`g@kcS%kW(d3hC92upUUc-;R1w^kq%ASHxW=av8qe&Ba5lKqr_QR}6 zbf@X?ITMqEXHVQ>-l=w{PsdZ{p0tSZ6x+!O*mO90lv=icBUOx{<~I-LV?TTW1r#YE zg{PbLI744Q(%bTwGV6-0|=Vr?lAX9_WO4{+7y z-u5#hEcC~!slkheoB$8E(ULYpn33%8G`Y}%cyGa5xNhu12%C^N5Dug=&1JxivGu+3 z&p)LjvNzQNge|xhUyIPGyr4^@UPoGx?W@lAX=cGtb$e%PXU?Gw2~mZ*yF$QZNIXimznYH};p7^zeDjB;N5VpUo*YtvX_haQAhB z$**kgCNz|ix%zk|OM|aIjAbh&1+0p5fxc`#7eL(+O#lMzZQ-vVxt)B< zch=)e?vx|2;hU8aj=h+kviS^Aa%KR{RM+C>gjuhVu%Xas;f(l-$#N1^4oQ|e^`JLE z$f9**;sAc>%6OvQMX{#`l6*xEG$mFv`VElSx#>+Sz98RfgDG2~+ysKrSw(B+JxXP* zPgxe(_q^EP2ZY#Jk;;I`%Avt<)*&NxM-Cs%U8Tx^qQL{EHcbNeEY=`$M^nFNBJFxq znY(Vsy8iwlg4>DhO%Yl>;e{DmnP$GMDq*RnA$_CKV1kGoMO*Y}ijjL>`6jLP9&O9y8zsYDv0xTr&c^$;KSom5t*x$R*Kl=#}T7S?6nufB28=1Ff_(HcSn{lvGcU~d%ON^|i@ zjt{laU%yX3i2|5KD0({i1pmC^*xW>I0b9Pyd8q@u0s1Sep=dZ@Zged7*NcD=*{ICb zjU$&$*-a^_3)XvBr{pEQWLEKk^@Mk*3vv1)m;->)K(3>0>lo)*W_cL`{DGb+^ko%Q zDj+CIHC!SAViL5fN=kNu_TUs#L<*2Xgg$GpP#Z2$-yEH}9GZ>SPoo1llJ&K}{w$)& z=28Pn<~y$-)=CY5O*6&#$+R9sgx$pRVp#9e{1$5~ znT+4}gR<=>D+aLXx@av~MV4_Wb{hI&W08L9F;xJSo@sj}}~-lLBO+W0GmkM{Kly$Zxz|E7xaoTcOCQ57fY9y>eRCaIpl4 zSfw~y6lgN#xzAdc|AzGdyegpKeavDkXOW?!4;Li8Inx1{@mFht$ZKL@nFOUBAtVx8 zhb%}w$u>I*`y8O+C`AvfxY!5kyS*{wxR)>gszP+Ma!-)hzShbv2C9nDB3Pm!K3LFo zM>)Gzo>Oj?J9dbMvg561-X@b$5e8v~^ev z^=Pa`Vch-nv;fLx+hqRGYa4C1Xi=lSRjhxWFSTrxT+M(ovSP17qCN5c1tviC`8lN* zz-8MBo{2P2pUc>?-B5FrO0zUJpHe^6#_~-mQ*5bvXbjnRO*<-vn^nHu%_nUQ%N&^f zMbV!Or1T~4O)xS{}dA{NUnwjt>td{JR}_`3pAzeO@Ko0!{hEKZJwust zYl?VL-WFw-K=(a19C5oB<=-frgg3Hc-j}kN+eV8ec_}R{4eD8&Z($tZ|D5ScB!dP-CCeWu{iSP5Eec$*%yG` zVSd6@+Jm!$_3?Exbu&6s;Z&)Vk$yF=C{NlJFTjX>AV&(VHuS$w_*8$}G|tQdLr7`s z&o(4b@vGbuxdNRizRQZVPhk@ym> zIg8C7JDEC`e`bKum|_>MEJ3qrm8#c+i+DJ-Sfh4lD(0W#Uz%*2P4q+%K$HxL)V8Sr9|1XNipCd47}C)Zyv~hk26w*DXJ{7!y2oDxM^eCm3FyY_FX6f zq^wD34;jE0(r1qJ5vmFKyT*GuM+%y@c>|a?r1SV5NSSS`h?!f=AiYdXK>J4b8709T zwn_hsz4s1lDqZ_VaU6AK6dgoBsUx8%MFgZI5U|jz2%!ZMrD{Tv(EF&PLLfm(=tb!S zLI@BbKtQE;DWMajhu(YfWcGgFy`6Kuz0dcZbDi%y=Q{8D19Dwi>se3kwVt)^=YD>r zzK}Ud&`VMOAjLOttcmP&joMKKkamCq17st=9QDO+zYmX9?z(Zk);F|XU&r+oS7Sn= z-)!w8SInD5I_nqYrz{+TKyk=ScYcPG?lTU)dD8}&1Laji+IK;_EnkF4{bvkpzY7{` z0e8-KG59WMr;%s#z6 z6p)8$(hD&%K6&+b8Iixp)u8a&$_m3T8_#*6uPLQLBjrP~n0{1+Rxw@kC&8#4V#eNG z!tYF3t=?tq$dpsjyzOaaqJTW8*#bAG&?W*f7RJ4+IT>GSqLXf0 z@l>sAEE~J-oJ;c^V2qDR_sK1ItrrGfmEl6PX-qqlm98&E{j&PTpiukjKzY8O_?K>% zJ=iR(Ek>r=+V-_Y&8$41w+ZpV6PE}4!v{z4j7jZz@4>AWmy6Ej zqmM0W2la|z_$U4dYPrIn7ky#$4{=|T$KtF6AKgYFJ$-wbxlkmfydHG;qF0tfJ~>Ei z_Ar!~`1y%UdH$#9)avd`%)GS_iqaf5zhovTCY5F2NK;lqm8qs$t9V~3w`ls*k^r8S zn7V~rx4E3zru2cyJvXkswci7TLTI$*Ffvz0&mE-R#n6e}#BEWxSkNu;mb#e6$rz8atr!VxsheBm}F3 z?@M?TpqKiY3$bO2;}Tj}<)njg$ z*eq$sIju+HF6q*S6t@x#B?~Q}qiC+>+LSxXW^cvZUvnpbBThwyama%t^xXWi7g6%R zy(GrQ3WbJ_E=r5-QmO1jxQ+){Up#GlEXlTgenf+q_C^$QV= zD06&qV%M!AgwP%z?^g1|vm4x>pc}OK@PAxCv&8Mp^F{`nfAIldIQfi}$wAn44XE@3 z@$n0KrgcAt40vnOEYT_W+&?;--md6cc!Nu@ds4Ew4~vft0gJA3Wv0#3?z`*2W+{pA zIGS;t#pTjR^?e?U0gwhh10Z`UNt8&K++xWb<;Kk9MIF!<&fpG6CQ9>Dd; zE)+n>o#{!oUTqO14`=x9VXy~KP+OUi>Gpi*FuJQ`qsiGwv z3;lyQD4mX8g;Wn#j7@uZ*qzt3Gb%~i5AsIPF!+i`GJ zCRIs5AW6A%Ys(-vBqVg6xezPI1{TAIr0#E)Cjm>I%!1$2;yAW%Z}Uz!*+z~**@Ne5 zS3v59fkAWCsjnEfm@Ut}$mQw^>v6_+^|AX>40g2guuEz>H! zq}=0rFq-8#m5U<-9;QpBu%vpQTfZRi)!D&bHiCdI+1Y(czpA_9@ioWb1~qlyOdb2s zPo2xfdNnS!Xg@NCl4K`ss|xRD|8N^p@w^9Q=_rGIQL+zoo<17euL&dc`<+dNVi z?LC@3hi@D(P-QP3lTDGH*YiCP^oOmnd@U^aGx@tr{COPF#CkNoIG*&LNpJYnlfT&75!%4MMvw z5G4XQUEtkE5j)S$u^(|htL;AW-g)|wDTHt;WVme1U#DKg+p#%I%%J?yE4z(x!ODhw zwt<)OG$E?^;k6DnEq}y`7R*R{ZiGaFAUY+Z zge%6x*e2CX7t{{fEJy8!qM!LLaU}CZbwKL-hGouM?%b2Uvx$0)Vw;d0Gt0mH6AJs{ zX7je6mqHmtQz^p|zj|+V?nrBGhg8~?=TN5m&LYoC!oH1A2aCR@GhJiZ1Z%waj$m$& z(iNy+7WG#QsGyvO*)Q+8Hh8o4jHG-RjP^cM?u(q)PWCUNc7;Y|8YbX}bp}31xI<^j zrm7y}p4|`Ar*r;49}F{W)dXNCvUdJ0wu95EOx0%Oiswtia!zopGHkWRcS zd$)c8YH;3{_I#v_;7_ZtekA}G$Ck{fsHzmKmuY1#`2j{oo5+4ZHe;f?8vGs(!bmY& zIdy!NM}K6Sez$M!urniaJBVFXhZhSPh!%nc`kF^zj-%v=uBLb?O*sygh3g5-o9u#1 z1Jjp|mt#4}q1CK$l2laLP{jm~@JcI;yT`2vUaf8dAQbtkxYDOxhYl6uLwlK=Q~`(2 zLRu^IeLJ**3}=32I*8evkr8O?q5NaNs~2Wf9X>xCbc5w>bLv!kry05GR5-)ZiU;pLW(u?5oL)%zo!6hDtG-!ae@15w?JzBV*w5t>Fr-N0eG3Do@epp^&yZ^I6K(C!3}X#_0@vl<1!Svf zWW*7BCcig3KJy>_z}eLd2l_LC=(77w+{_$acR6RE#&uIQ+3o#bWJP`=KZoGuQsZR z9jmmSRQOwy$HRjl2Z6X_w?A#8|Ld#&Rnhok^WYl`lOYlT%<*ipv)w9|INn{l%9JxG z^O30PFqNXfM@3)E52x^l%w_D{x=ly%U}XUh?3&CPm$}F0Y}`xpo9D0F$Jh$l6;Yb8 z9+w-09yJ!K!G@HqR}!c7+i_2V=`}ay_a?SvuYXwV)f=abvmTXLP8&{`wsp5sAx4oo z8Rb3S**N_hlY##cg#G2ea6K~fzq6BU&Hc}z`UB)6e5Y~$V|(z5x(V(%*D6ZK3>%30mM11{)A&7Vnhw~nyXgp- zk_?unbR%NO9cznT0cEB!KyFy#+75=xaxd+YKoS}^f+g{8^C?ZK`%grPHaBU^?CLN3_hv+oDUHK9b$|DfmCl>m(ks6> z?=9DEvgm*n3E*(-Bgky8L@DA@2&iq?pO|$sA`I_Oxd;l(*AXNAN!Yc@=i*tAxq5Vyv3sy=p}Rusv{LlyeBsteGttbzb`?E ziCi{QX|raJOc}FfKty}lVDKLkC=$?eKnOuA(`>%Ali$Ydgv)T;lb` zl1jRa?-QX014BFVSk>#=60i>tKC>I>o$);T4W^Tp(9?aNfI{7SPU4Kt?Z*w4vn zaJf>`p)?UGi7*7HyFnnPM0?2EyulP--=GCC>zTnon$h^zfI(yk)x)+t*yh+s{sPzs zZ@Urtmbid_S5F^IxRR36j^xJoXF!yUAnfU4A13HN(PpEHwYHN50c^16l^lu?9i?U>1Mp_=Iy<898n$rYJ4i{`P`^?nkn(YE6dAQ`UiE8yNyh|Ahco)v2u z|3T>1%3}W=4Jp$DmYIv1%y;>($zXTYjFB<0k39`=LCa7YX8Q~|I!(%4bZdFpjur%GTn!Ej_pI*ji*=AK?X4Ue@+C>+faFw)S zXsN_%)^?eMb*yj~TCzQaP$QG4mM=Mk0nRpMJw`>&-cS!~mY)S(iyBi*-EDxuW{ClM z0mZ-qZ&Pv)(1^_{Cj&Fdf3xoE+Hj{hM$Ruj1+^&?kA67$*y-wF0mO9ZMF>5z9X%%% zPsc9IY#o@e$`Dze?WGEgDDn&lk^0Agt4$88G z5iFN1jtvSmB~%;@Rwb=MR_Ihvx|G5D=iV}|qm*+z+RQP~gdYAnNKSF|j8KplBOnHO zYADj-q1^Ah_Hq1C%{!U+Ii{qP(4ye3hkgWVP9>yep^R4e9zQECV&!-+liSL7oD=&4r|9U@p+RSEZ*XJ#L1 zc3hd5fbw*MvU`hA$=Ir4ow( zN*u>VTva(HjMTLIr`itUZumsZASL=%Q9|4WSC66gRZ%4d*jqD;t2t4Dpw_~dP>%eL z8RE(^W>uq&Ju;VGGiTVj_iFZ*+*fCvoexyU&A2-!fKMyeULSIWd0dR@6*e~(MvO*M zyTL^TM#V{d_MNt=yFjayv4Iy~N}Vmmo+B+?F0u22A_gllSrO-jWS&jmlrtbn)y+vbVSfvNYA?bsO1^gtGo#t( zZYH*v7aX1Qjv9;~02~%RDy8}z(idLwd}Dbqu=ndw8NW*3j^C!ZkO5PP(|I#-;OqrX1@ zE3c4#7)#1cVf*!>eqVt8-s?^W9cXjlaUBCrvLGWjQh$?3MtloxU#V*(zA#> zZiANhOC|w@8h^fER*@jrn<(ZZsA;dpY_|H14!8*|>A0bIpjb(MS_YwpN0N%~IQnJq ziTi04uEM2GbYVME5R(bI@Q2cwaq75Oxwyd#FA;B^Y%Fv)=FRYHrEszDjUZGffvf`1L0j zOv7*!r%wW{Xz&+EzqebXuf(`cJGLS7P&y(qRD3uE7QP9?EaWp`P#z0e5wR-g6{Hv^ znumAZ79_ilw=K=&L-|_L;HOl6$d-H(T6B-M_tTwX?{sfU>q7V8VmCWc4V-GeCox)^ zu`jUH!!jBCvP0oO6?@WgjhvYC&jLor-%2!p&n18Uizz8nq$20*qSWRK>Ngg@;@AJp z$>D$Ip#S>+DY03@KVU!af217Kx|qTj?P)(%vv0m|Kg>`4v%YicZQssVu+OIl#S6Q~ zPz*_Etw|wf?=bBG@r~NDA0Rq2k_uimG{~cIT)FV^_VCF>p|gT)>206@#E@V&a4E(y zX2&me^4^6LxS1T=IK}++SjWP+cQ4VzM6a3UX*|q*()q17sY%hA{+jxR9Cl^QDCA^d zs1RND8y4ak$MhGMO5ouwbW~qaV`Mj#I}h)?Ewmo-s(zd6rZ_9NpSl@P5VOiiO~n`w z)!FCGt!=qitg7}UaCbvr(Ks@RQgHyIj#ad2nHE#+FP~XN$ft3z89lu8{4`IolaHh- zym=X_VOl237D5ni8<40izVRCdKSyobWn<&G+w#McSL@<+zWuYo2Q!qApdVh&5bM%D z)9YGWe`0cCNBEj;Jl=Vl=ET>#`%!Z7=v>$Rs>a>P5h|-BPf}H;CyX*LG3vYklkGVYFyXG0U|B=po@>|?z8cW|aAPTAx}_}SsKO}piS%fR7BM@ z%Vn|eI`)^3a)tN4u@uP#r`@hi&SxrgqAnf3$UoYDeKcb}q&3xQs1{(lI9ck6UwQwx zM*gjGy|C3k!HIcd)2eS_fNv~J^`OB)$lZR*xs#8ylYq}Br~+nJn^kwt5;teLHgUH0 zfQXx5wsj~G-QAyG`8$i&KPG0=z$4ks)O>eDGo!P+xJ)5&#FYC`_L}&^I&5BK2ff*( zgdJ*W2%P7elX(iciggF5MOxCQ0FIwThwv3i*OK4zfR3$1#Chxoe7d;ea6PmvR&TM9 z;=V*v{AWDPPcXwD+Rc{x7VhU}ub-y&OCtT-~0o{y6YJrZ#bn#%lo2Q#je-D7aOFB;a1y5d-3Kcw_!xT(YP zm@&={*6VY*bry7EOlCXLUULh{<}Y-r&G**X+2dDFy}@>fXLl%^Z<^38Q$>)CGP}kW zY?j8dT%O!^L6lIG!c8wVKQmJj9m=G*#SDsm<=d6~$YT&4+l=g9@h_}jP7*rU=-2`M zoEG_}DVjG-AbzLmM~P5=5*aqTZA#)w_xSC*D~&Wex%?d7?(4dIg*B$1cqH_-HhyD# z_y*fwfha?F|0=|K3hx?V+*qA<#_#A=`h@IBMr4Ue?6$nNoP{M7DkOvVuHS;du(pYL zv_O%57AB6TqEgjeW9~Own?u}sasi?7k=uZV5PR+nu8)@4iJEiLb;{swQrt-vsKgIQzpG^^KR3JwM&T#X8cVMX0B%OC) zN5y#7+Euo4gAneOS8V&#=K9W~p@fB}!k?y=wK`|VHB}4xg@euVaY~hf8AfcN81(bF zy5NxD`8NWGWb|pt|K79x4|P+2t@d+q*O_p76>f;LI#l*riBp2!G-69#%{_x^V+uCn zutZNo)R=%-zh6wn+0h@TkWY$yTH_b4Olcu$dFP+J;S{azXPPu1MArm8z(xPKroH=) zKIEoh;iD6gaI;NgZTPi6vSe|jkydB|ZoUB!-80zlYrDyxRvke-*)$pb*-Ygd%kbRX z=}Sx>x);l~$|`Qr%jHk#o&l!IWbN)9_n`weXc2J%Yx*%NBdXv=Zc0(ZaO0NxR#kr7 z;~mk-t}uPGbvf5B@qvU3pvtWgM2x&_loj1Yp? z{Dr`!`u}^=2BF1zzd8OgtZiJx8-Od?1MJ zlP+J84kyc+_Ze1%^r@-sMza0-x7W-mjeU>4_*W-uG zF4HI7Wt6e_DPaLtbOoJ?GsV5KnzjZAG#{uqYPCQ~1`=em2Yjt#dElG986Qf1 zp>SL_&qNB_;Kcxgw0VXcU257X);RlBi&GwYT0#%eM!R;H3^X5N=wW_DZAX>nbuaXp zfw+J+k^V`vPyY?;CZQA28K)ynM++MjT_sZ}JF)+DxLH9VIp`-ky3bJekuYv$v6~tm zj7s-3$%B@gzP2F3N;nX-43A!Y+3s)*#(*hBQ1geH8U(_0{$U#v+oY^|)o;vr*{vVA zBp|jd>*bM|!HxvobMKDIkt6ixYrL`73-bo&$}H$q+kn7NPlaK^H5W+&irk{R`J7Co zOsMOt0?p2|p~njif0HKr+jv&dMBN|kM5*+~mygc7IJk7KuIP>yn zgL3@+hTV8(*$c-s_-3wL%HR)vU3Y_XE-919_X-x*g_y#1;S{%}KZWZ&SLRoVqsTx$ zvvfbOSemAXB-g1X5Lwa?ijRck)~`kyDYsqTGT65>*`?1I;F$dLYMXy6rj{8mk z*Li70bcW-GW|s0%ghbs+qeKztlAd#WvXq-1q9@Lpmd^82mfi2%UZ^de(Er)!i@M z^MS^I%5czw{o3AO#U)4Z9^cnO#Fq*K6`A%85n59M27c3zI_l)3r;pz0*B%GB43H!p zReUnlO_1~%lMPzU8*;9z;zjux(Y+{oZb`jq8(*$Rpn-);_N9x&M1}FL3_()@tX@N0 z2qkXBn=f94GvW^6yZf^l2`&|oQ^o{x84jKM?la4ul3iwP<2RS{87J&J_m*3z z!g1}c9}1I<)93Jk4JfiD4L)Il5QY1)(@zf!Zx>oJ-ysTZ%D%4#yexeK9ogPcUQ?su z1l+9ecj|Zq`c~%&nt^4&xh+cEg*Kj1cOWR&O3qCp${7$o!&bs}uQVP0 zxEH_gwj6w4ZguD`gTeFmo)>{Efxo-FJHaf1osf z+Kb3b0Sk@E^8Pkg3FA-a0jE;ub|@fTvOhI3~#qVb;U>IOA0SHBY(VgmnI*e@E^ z+wZJsOi>*q?3*T}V^^FlbhS^%7PlB&L8myj^pg1(xvo7SEYbqiW_BaB9N!YpEAd0? zcCV>MQ7-ug>7FWRx&<(Aqvhxn)<#7ONZR?QqYJ#?LJ}l^Wpwj40*~MJRyX*};&vuE zmR7N6G*AtAGzqTbYL1*L8qU@LZdV!ENIU0ATu2j1;F9f&cuo2w)GdLct_H{Uxq8D= z`L*-QzLuqI9~r-xVRd~c!y~?YFE9I|6FsGj3}?6Sdp3h#=o`7CZ__?I7}^akT%_mH zyivachAzn4*GQ6}hJ#M8iq_o9bduwSOf}7%-`dSni+0n!_R)>t=1yuWhME2Fj+8OH zBpQgzyj_j{{iru{F9tVh>+#``{UuxZfY-TM`5f_!f|cjl9&I4%?&M-Zk!`dN$bxr5 zwySbJ@u{z+x!!B&fU8yGbjhukNmu9Y-4EB=Us9!XsJRBbQ=^B`;7VY=+_^g{ewqzO zT?>3l9ElB1<^_FL!5I&GGF@K9v|854KD|rmajEZV*2}>Vxg3uv-}4Ac~@r3v@K)`&?!{Q)vpf~k>*p9**V-( zw|QK7SD&?PJhKc`k!hAa9qHjWx^D6!mU}+FODsGR>a=ODox)us%ET?FPLJ;#i~F~_ zj^=%iUxMk-tD5$x89|@@a9y~57e~@YlSUE%k&o*hOac)c4K)wP$)LkA+eWMT#$*zt zhP;FLIYw(XH9%q)c^pN!;tt}7vRsPnwB_hnz{2lmBprb-osBMDJM+tk$j}Gp1LOBq zv6@TQK-c_w<%X`oB1vT;ns_1foE*j$2NKJ$3}xH}vLn`NIe#cyf7e&B_!K$_1Jh)N zrzUlK!CtN=quciJmH2|VNZfp8p+PagDuw>`Adq0JIiuC|ipdNgw36zl8)8QXMC+6J-F#QuzguVhDF^XyKb?X;f zs+OwZ6?Qw-Ob63=E6926cLWKPHZ}k#^hWVOtAt!QFQyTussZ2`p7TQ;-NdgbpoXx* zZ;jgr`s7&$P9<+HGWI21SvhZIsh5E2ET^VD^b_eSzpUC$YqrdZ(?W*WtOtiKT1-|O zlsxck-Ehd$g|$)(KE#p|2z=r$-mpHjH~5+$QJsN-EOr|O|jJMIE+38Bu8 zO4r{t_n}Kq0;Cdh8#eu;Gu^;#^nOnbd^v>;aykt8{4COkRvTTIqJFMVBrhdS zuRDJ%r>QuDiXYf)Nj6IuB;H6Hpb%HoHK<_&L$z#HM&Z;~b(3BNK@Bm(B#R4WRNYHn z)VDKJxqW8ERY&!<^lb=|ovCDm>?KlA*{gWxtCHj9^(&S&_m%bnW^SOaNNNt0AvQ?- zO*wEKPh_N@R2Iu*jl_g?hV5WQjs9EqqLI%}#8-jgg4Um`KX*5^my>QastaoubO}8K z4A~&oETSnq++rHzH`#(v57b+`y`=OZ;;bD3HcJI9 zP-KE~q<>LE|{ugCd)G!BCoiS+;V4H`$RRGh}Yg@9o;Gn;+ zTL`~SmhAVs)%!kB)GVsEQk@6NT`7lEgJ-f_Fvx;R8Jc|V9h7Wyy?Iup)Y}n8j0e?(*67@Rkl!K>7>A|fupl)qM;YXrj%E?ma15>%B>TdC6+1dr=h}7nUM>K z;w5j$SPAfvs-(q0jeG+)m|*rB-yk!^ngboB*LHIUXM!Vu12Y^aA-%`H{b{<(?@nwL zKW580crdt`5@ZBtU{rhlG~NAw-~f#Wzn2AOI89NcRxHemeGxn&Ew5syAxYK}vI2=! zIyaTem>9AK3v_NOmU=h+cVMrGeD^AOH4aSh&4! zqJ)%@zh}f+*-Sf4ms~wkN8~Bs=!@*+D6PU}Z^|11khh>5#NvD;_&1l|u$oLtLtXO0 z3~O-?pvjdhCebHP*X){?x49Mot36}q^cu#naq8ZW1_b2N#)$4#=+LxgpFbw59opXZ zEv<5-_$$Z6hJ7U}Z}XRPW!(wP9b^c4EcUiKMakJMq=c;Gp81ts?@}l~`%t;vR6GCn zndHs6D%cIM_LIoLKs5+?x|7j!GwK#ilM>->xr)MvF>Zt6!uW4=^z{P<-KD>7j5x`; zJj|FGEN)&HOX;41+tdka<*W26mrF-c(1m^^{7FhH=Q}mUjD|Tj6hu{U*01cG+!-fa zTMR*>1$OmD`>eUUi;BoSL8kfkhFqbU26k5~Bt?&|(7^ShJvqE_+&Cz-Vu>p+(0Tza z|GucN^rRKv+M_m<<+Q5qB+kPdg%^{hJpARo;QVBHm_RI1T$W>smR4w2QdSacYT1=1 ztVG7EWa5NEMV#7%Q&Z5PqV7f^P2ykZyDoQgav$G5Je~34aofzgPowZ$2c%_TMEWxw zqXqlMBBxxx9NOXPUMbfZfv22y{!z+mJ>ks5gy7-Y_5; zf*$aSi@xjIpzkvmt~S}2mja7$@Ji!U;oGW?c7uxEu*El%=SMv{-uUKg!bbe_^@T$Q z85vj;lO_LD3<@tOhTr<3j1`LASJIjZ;2pX6Hqq-o4vKnMGHa4pKwBbl7oyD_ZG|w+ z1EewDzJ&of<1e|{+22^M1YB&RwDQ+Z1SBqT70}&p3xVzeuJ+s0LYA;P<| zsa9cwEppajjS*)jwnOE^H5^5g(Y)C_d_o+z2cR-dQN`~D%6SBS0a-4D0!AdB&T7x> zNO$_j51bj~=A?9v8keo~E3U$aN03TTnpV*Pw*GnRL65JO1?$_!ckO#=D|U9S?(w;~ z(*|hQ_kSN9KbtBn=L`n9u8k+7n7}66py9x`%*he!ev{CDA_SQ`{+M59_^BU0ciR7C z+y0ll|J{nj-&^|c`TKvSqW}+`%HK z%GvDsH!ktZNZYo)G15EBFj3w;hFn;}Q-_zI>zl;+X6jcT*m?&T+H4z8>5J9yCCjaf zL5lcmCnPd12ipnRDCr*EDC(Et$TM^Z8cdpLv)Dz{m>W~6QDT|s0@SIVkF)Jeo&OX} z67TTJtGdfeZ3~zWIHxLAKE<3@KO8(CNr7}LrAvkx->o-TA->@7lUtaz;c9Z$Q~BWT zHm+FNNH(3VeKX{lZPD5PjfIZWOCcJY={^GT@ST%e6{x?k>DX%~GbG z9hUlxii9@Brz9b2OLg->Wq{`d*UM4moJV0!O;K1mr+2=3?p8IHS_7ozC5W0P)5N(x z=(yFYJZD0T(MJMxA|ajQZR#r8!sVJbF|8#6%Fd=uBjVR&fFZerRc)o?E=Oct#yyqU z)JM&32A6&gzLcY1GVOKYSkf=$rxh9E%8q|zga;NGS%4_dkIOM}WmdKg!bGC$J4aN; zLW%CsAd$|qtd(W7f`KYnkFGzTDJHQ4Jsplwf^(FC3*k3G2B|ipuJ1uldEcsHB#Ew{ zf>%rW<<9R{!&VEvqG0bwY+G%Gb7Dv{yeeECwQHKI9=E4sAiVnIQr!6^Z@>JiVK zH3biI`Ho(@k)3`Ul6cC?SXK7db#R?W`31!}!4zD#27O0o9}Y+xWQcIEy+41hejCpYUbQQ*`4&r4E|#M1@vW28opqD^n_WN*}tX?-8c)d zYnfmQHC^(O=8$XeKYuN)dMxogYaXYK&-*MP2`6huf4Gcq_clT!ltzrhpX&YX#d;H3 zsXD1@Inm|x-RymG|BNlV(uv$F68p^D+=64_vUF6B^J{}W`$cgMCJqdN7aS$U-WqP~ zBS(H%;Bmjl8%^7M1Q%3Ch-a+2ei+NADMf#wf?h2Db9{2={1au*T0jHZel0pw;SsGFSnH}(dQyZ_qW;Q#$c zZf__>pm`lsI(34sO%A9(;_BRT6t;u3_LCugo}c`We=UqpDLKM1N4&xyGfnvlhJlBV zgPwx=Hy18FJeaKOY4eNg*kpDdrMh67 zkoW(%Aph>e_pXkUcpXag1lP-;=eL4A-9PFp8$M@}fpdiAPzSM4xzHvzuP+NhIrb6P zDL9fkLe%^KP#`E&7@AKTW7>*(+K6cuA4wO@Lt@`3X^+|U%2bxUz>VXLiGB?Qqf5;& zz4BmJ3x`q?!Ep$aVjrNtY)V849-m@)4tnOZ(EEj2w4S|aTep=|2I~|xB^1usrDpJj z7nPm#)x?A%dh+?}5gFYDk$l^Fr!IfxPSDqtY+bAd>2S&>POZZNArq2knZ%slw0+0^4Q}Cmvr4sxZt&@m=g6M z#}*MKNewj@ktw$MIZ#^wMR3CiVmTn_Ig2!=d1*F?G!SfxBVKTjK+B4A=G}o?2r5?& zEOa&a)Y9bh!2s4fO<&hd)@hbAsC$pj)^P&^x0exuo)3Z739U{w*L^ktzkexbec}-q z2WnrTdY@f5ApcTa_fB&T33K+zNMx4k8eFx77GmvJZ286o2dnkPoU7T(7KhWb9dEi6 zXv47JKqDJ_?I~0Sz|l-8T@b%z=*yuqR*7?^C1Mu3%?VGi=%@BJ;3(;q?hbb|rgsFD z<4XNxsXpMGX9BNC(yOKs3YaV%bZim1?cokq5Pv6UruA6D@z5l*DwWf z$`Xn+$t@ieqKTa~Ushz|7Mzct{~WlY{Q=R@Z$4vU!6yJwhb%Fe@EJ*#OR0x+B4vX!qS?|biR%VR zB~pQZ8qT9Ib{{choUS700o8EhyieX>$q)D_|%| z`kVTC8@xI{Yr8UFc~GkTGS*P60E4KrF2us+{CT<`yW5#;;CM#vUOXM~M&VfC?m&5v zTwlpxje6VT-C>Fo4oA9@()DsQe#bHZj|s&j;ErAgBcUt)eu6~HbFbTjBEr`%k-Dd> z1)TcUNb4&c{iOX?`T30E^IpTcvVz4SbHL_$#+?19TEYJe{0kJ4PQV)RO8#OV;tV{mC8$NJZayG2! zU!ri5Rog?NxBguFVpB6yL-fWp9nD%7R$WDxw?#{vtj(}V%eEbM#|ES%M;5=b+)?@X zjYVK6D0;dU2979FZQ6ln?n@1%xl>yBYNI=|xq9=5A_)I_$M6-Drt37~Ng#+Ce=u@G zj%d4dY5h2EUPX6%YVh#%QLz1iVbrlt=bVt&iN=4m_KyJnPmJ59rFVMWrhLM#O?e8{ zMl?9VLZ}mwGiqY8m5%psmHqQVN9MiWTG+8&^l^p0a_KN`v`LNbsT;Geew{4_l9Imz z!B&~k-4AII-ZdDFEg06_&)M-MhFh^U@?m4|%Pe!Akt??~A{!gOF&&dr*w{~Abq1y< zU`pz4td3<@=cUVJD+2u-Q~isv&sqHM{#hS`f8M?GjYUfuo^S*A=zH=PHfD?c?fDjVpki#i~V74(PXnIr`qBlp->8Og)=b?>mbScJDYO0&hdtLd$6%zY}6d@l{XMI$_`Aa^+X4x1<51YF-%rzG)kY7#X_9D%!H zhs~+c5JySUH7bCt;yqPd^Ci$Y^ zG0)7ElZdnKF~(^kr8E3HC#>g(0B(CP#)DZu9@~MV6a+%}cGpHP^=po4&o2K_>p+X`Qv!diGD>MtJdxtv?Pz7$z;ba%D( z&DWq}?&yw@b3^4+G1Ud=K@#JRlpUsN@?7AN!s+dIJ6fYbzj=?>Jy*40WuErc(g+!H%IG~n+gao(CiJs^Q zIKVgc|Dz)=r(@+V%s%1^s*hVs$mcsTm6g-5lo!?@s+(8WYqW>0rZ>)sei-*GR2z+u zWjFkVa3r@RXIY+?=$NW#8w<2eW)b-Hca|TG|ES?Pdhm@!?>4g_RA7VzhH1^DKqK}K z)7B|VrZWhnk}1EJ($85A?$Urj=gE=op6-0vt1G!R6P=nK3@S)W?5zP)UMqHrNbQ6i zMK8n6E`6`+`KPJbzY-1ooe8Y?|IC3=g@NB#$X%Uh1(-A_5tvW_Of&2GtiQ(;w-QET z=gjqHjU)lCB{Hf13D#7CHk)cGVfV98;IcNyvIYf8Yfg=cS`Ur@n>AEERRj%dc>i2v zt6-&`)Tu*E;u*&JvuDK^4AQsFfFvNq-^dTV5cWlBjX8rocWDFGTj87>TIeVk7wuj- z+grV)*b3T7D(QGj0$(r%BQoN*D3FveFKX2jP&uV!J6w z(9Uw~usZ~=YUjUMYkTv7ejiC5Mb7d-Glc{Vb~R<1NuT>AA{g7S_YH0W{1ex5n!*Q; zGQXrJ@&%fXu6k;;{Zc1LvD&ssX3&hRrTlRa>j8>iTA}u23Rf&iPMW7t1S#1Bbv_Ej z$2n%9FtO?B`mhe~;-~QLPDDO`icxDAjpN}(i?QQKPrk08ulyp@M=_<>3As!^@*Tcn zFKfFU;;?hOuvNjLaQwJK)*TO>LjMdi<_dEdjY60CJmr1W`$Tqdb<-g&y8nGfl0$ji z*a{)pPn#oK#*g9`Cm`1vRjH7J6Y4|?oLgo8Vj;d~JMr{RdtsPJzQdTr3tXk4~>J2Oa5xa68p@aF|)&mD~9P1}}{5i&_UTw?Xu~J&|`Kf=jM_5eC zNy5~_e}FnhPDf~+%%uY&c~e$bdT#99UE9@I9tq3naw~r}-gpqTgE+=}=8bI)6Jez# zAntXont2O$?hq;m(~ReZ=!eOS6bbc3=R3)I32o4er`a{7BWWcS7wkyWv2FIQc^en7 zb<+h4%`@)21Y9wRz0KRiMfUmnJ?g@>w2~1g$co+GFzYbZo$qBT(-pNN(SuZm*B%0- z4W?Q*cl-An-t3Iah)dtjSq@^|8y9`q^!2tb?lpFZ5DZ(h_h zzM7~|U>zj~fZ^$AJk#z^S$?fX>eAVZUQIFq;dXlXZ-2LHoK?gDOM+or>h(%Q!HRa;^>&}+&OKFU1~w5r!=qCsR5cSFX1 z<)Mu^>h#O8db1VTqj*?(fZ*seaZQj!-hex^Z zY8AIz^wF2I+o9GfI@w||vG1FQU)Jk2w%m!azCdao_#`A%B9lqVhf$At=L7{sNjcBq zR~ha$w~!S-4|)HzxM5o52ymb!2v?E_6vtQ#-o?w>(+#wZ{N#{&4ZP_8M%{bIHI?rF zzBuYQqauR}0-=l)87U$lV5p-s0fj*bp`(N%2}N3fgkl3BKrkS^DV+oo0|pEbQ0ZMt z=tX)5S)NApO4G;0^2O8BrBa0_&oC!%hG&M z^VWX5-m$2`PsBy;!b7tEu1HW=wXdFPBekWVrV6TNF+f6*bRjGKpEs5fyL>6H$$CX* z#YTJ$GwuQRFtqnn8?l&nM!NmG3Y*g*VNR$F9zda~-tS4%}ffc-kfUoVf^l%=U>@o}z|( zN1C&#Wi{z%0`X^EWqmdsa~ZK(A+ByZql<>Qm4LbD&>uCGsw)n!8`oL;c|1cXm_(q zVzQjYy7kfpd}Z5Wp-Aff_@A68TQM+T=a_k?8WizG;_zjD>2xk7$t}N8%Dif`%I7*o zB%Vf)f(*>*N{EvmQ^g_e0ycDeXz}UiV(#b!?P6 zuh8z`St9jf`x;#yAI>c}ZvJ)7~sC5CIMeZh$gkvp8_K2nQp==10iQ{*uJUa}y3sajkl+@;P18+V5zH6 z4Q{eT2UCs3lU3=Mic-O1A6Xz|Cj5W}6Z{*bgGK-vt$ zy44;{;dh>{FV?aUx}}RN4jFyu`9ZN_Y28-9^bdvz78&`>X%t~Lsgs#gahRGb$wH*> zCYvSidW}YLuHbue8n!W38CQ?MHwkMlxV??wTvm0S} z8}T*r?Xw7tmy#tyRtZ<&I{CYMf4= zB#FW|b;Tse4b2r2!sT>HWr_&_a{38yj{czC2L@dxt-daCu0PERL6IN)ji+;L!O7d- z#!v8|>{DWHR5oZTdduD1K~`Z+jod&-(>b|1(U!eS3*NX3-316M@f(j>smA?VN9H|` zeUnkvR(Z|N_`c|CI^iWy=$#ECg@OX2^D>T0`3>mRb-tr-Y;Tz4%OfelF$?n~dAji-RIK zOfee5#l;T{>z=B3P<~I`a#{e@K&DJtctv9)yP(-qsNgLfu$r`&?*NS@`_7L4vzwv6huy^-Y!V!n}gC zNti?3T5Wx+k4=`_NYo_quF(mKy26w+6hrAlq)>DfFw1iF$q)gxWR1Bd;I>lZ$;rui zVNBDNZ8N;i1_-`8+{?o+rp-EsInK7H`lCJJy^eQX!=DId*vY^AXdG;!5}zNP`~83H z)-hm@$U&Po5Jn2ZiRhGG!|FsZz{h);z#Q^#F_Qy>9@7iW0M76FohX3xiy@-ZfD$ec ziU9o*{^F|bV}H0+;j2RSJ#PC_%Qm+ZUck!`t1}H~LYe`5J@D9&xcapwrFOFDufo@@ zCi&`SodpHD_DG$5q-uHVj270^LTIL5a^H%MhM_Bb;P_J&--~woRpLPeex>ygQn33I zeC&@1x1aF;AwTVJDyv`A+uuG2K-XOx&hxss8z1gRm&K?H(1?2fw6RPZ~*}XkR{remSqrW=_V|R z{L$k8)crG*2T^5=d5N2Ma&zp_+dr8Q{OP@Qi)JJSewAx}sjP18mIWrA2?^yJ>($a` zYKc^-G+L)tts>7PBiVxmYG9koB%Mfo`214J(XYfe!+%xR4m65(7Y{e3&trzw=PJ8K zN6qkkG)p)XQEyKRAH7J{uS1K%;<8g_NpFpfAzYWnHQwD~e*`PG2$1;pMFgIio}*= zJb(uE@W`%D{~9#>n$YPb!y!l}8Hz<9KbEq-NL$YJB&dLpy(m>t*i!LpX*KzYd`J^t0h(8gP;h5U#<1*_|Z7_1Hz zf@fhcn0Fp$NA$>^Ieb`o$Ca9<*QGoCdMDeT`c;@rX8T}q0vZLw@qXs}cKZ$e5ThUv z{Wr#!Q>PqMMb@NkL6gNE7E^(~QVka{%MTUtWb^z>1X6LESUJoIC)90PLXqPv3MWPr zL`H~)(~t)DJ@+2$v;=>|tCf015#G|ZFoFj!Ub#=4?l-D?Cf>Chj2rMZpM_ImV0Mz0 ztfoCG5sExKp{jVbw`|@HUL>#lKEp>+gtPTxIv0((wBPUJ9fwDuB)+lz!Ve>Q%80vE zy2~ecR|{PkBq!#NoUe|)-*>eK5%HRGAHL?3frLQy;~An${8)yS8XwUkU~%;u+lwRJ zPgmlejwt8{L?JIo<_uLWxIt4Yt)lD3u)z&?j<)5_T==uW*TXAIp$QX?Pp@K|X2LLK zhLY1!f`>_6Uvu#fkOD_DgzE!&17!1&7heX8J=-lftvK#nfgm^??3>cf!-=LvJNNt7 zEIVJ`JHwUmR_%;Igk$;rbXtk2E6b<-Y4bNWF_>$#vk`Up3*HIW?GXRczi6_u~9B%i>~K@)WDY=O~S5Uwtr znxoU8`JW(+lLr(pQn$tyY~ai;e8w&(gwX=D4*s`%?45Z zuR||c_ZC&3R%_MyjI>H`?n7?1ia??eXb3bXMtkE>9`Lm_TPv*xg>1Sh*!@<+W}SG* z0q8aCUk_0<)}R1l)9%pf&(^;ms|(dyQ&oMd74Ti{Qtb~m?isc{mvPJM>4{_@kQ&Fy zSG+c8i=LlZ8Xc8X7Tz<>V!OHhU?r~E=dr4hI{Au>SYr3itN@3!zCgcxG1S7crFHQ` zF)Ss{=)B(X{WrL1JwAY*LoGl*_BRJkOGMA`n{ta!T_3yq9u0I%dGG4Ss%L=h4Q?>( zbM(G8$ub1?YvmE23d=t^k zj><5|N*A)Na^b5mKPcqtG!1#neZ)51-uS6+g8F#2xW0tpZ|(D;0_3LB_tl34SCM2- z_&h&6K*w>elN}K8>Ba(G&@c9@f)}ZdRCOT!aO4_P&HpD(`a>}*!26=$` zN>{x~E?_Odxc+#8xC}a7n54ycd`i?y=*5xoO#5@rX0(o-=|`3_Q#HZhL{b1l`fjkV zM-Tay(_xDS^fnIS|A{k%a;vB4EvqI_cZx<1NUNj10X9sVhlWZO; zG7?O<7OMA)_8KBW5AoAJJ+~xwNxCv{(a7QMe_REBw^GM2$Lr5oUK~}7 z4x-%PH;8}i*GF9S^autU2 zBg3VVe7g|Xh6n^S(vKLfd7(Nof)>nwI@3O! z!PO1yNg6|-qau{U=bsPLl=xUYQNid3(~Mcf{opvvOL$W+LEx-Iy46e$ogj-+upShW zOcm`|v*@(K1QjyjMp-Bapxi7V{FX zB880nsj93IMuXn}=)$wG4z^Ny&(syeSG4GlB~~|86eSk7j`V|js^JmBDwX*b4lG?? z!wf9qvo62W%FQAbxxSxiVinCJTS< zMOsMd<1u>fPVm&Zd*nh`*%0;YCNp$rC_$?FuFY8pPvM`f?16uj$a4D;l8DUE`LT?| zD~VC7h~}Qhonr1(W+JDi^wX)fpS{nBNYphsQQ-|bHTX%`9%1U;B0a4EYz-j5@~df@ zS-vqGA4aZO9V=n*hkkIgV4U;n3!(uMsb+swi?%H z0Lf>-&rQ;)lXaf~R;%uyc37azqweKtMMppQvc_#;D;8bsrLZI-GEe9k5s4cGvyh)s z=LC^cjC-i+mK|FAzI$$1hmy)Q!6F4p#IlpiUlv16oYihx7{AKd}Z!VB`he17gm z2Lk!d>RKyhrr1%W{V<`$2wv&tP~-?B2}GXH{{2o@yW<@Yh^L{UkIXl#rglO+X;(AV zCpzv@_}hyIwV6{Hx19&T_i2`jUgrmUN_zAxksooN=Wfl45@sk^pZl@}m~U+QqyCjq zzO`7;)t=9;zs>ocR6m9*&5x~f*Ef1hvmO^Km;;Y?{I#}kyn3NM zso60gB&a5--TaenqL{=KuNXQhQc~^en~2xLCmxN_04oQXT1dAVBci6))Op_BY4fFN zzA9syfH7@z>s+Pg{r6*Al;l18mLcA*X0ez4T(GW(OTlM%Wo5hw5U9Z_dwyw@@0jN5 z!z|aX#?t$rY@4R06L@9|8k-HH*irk33>{-w^|I}D$WMIL=vUiSN@BO_Q?G1lzJfKl zR_Iust=mC)7VA3XRX0my+rwq?z4`oIcfw0V#OFy~S>W5HR-f&7jeE+1_^_G}o`cHZ z{-er^xdrKx68ATu4sGZxyV0?7C!d%QFEARymytm|%zyHpS<%sh5Q}YVIII+T(mZzG z7FSX^tQRN*q0R$D+wSK zS?LgfQG?&m+KvbjdhDKEe-M%|nUZ>Z`uU@mz4N78a!{oGlCd8wWJV?tLbCe`l^MBx zvW0IJ{dugP_)Ly9ajdc&Z!gT$cDwxlIq)4dSU@NI)-)L6MhM^Kd zd-XBA*AL$Qp>~shA>*Fh%=}t^ zoho^qU2YcK4!gPKS1%04GSPg~iy1E!xYYi5V);p8E8pLI_r@yp%ZT5iE=A>YV{<;H zI&7-u{z=pGB)DH_Oq>x{5PM^A4%(zjDlo;LFz>Fi`8z;D3Cnz5RMYYE_Tgd+$B*-+ z_y?{uPn0XXopE;xRQD|2=UfKm>KHhiypPsA+NbilHPt;?v43zW2b)D3oety19g9gmEn{x zYvuClLO1&k!?&Zmv&Bjazr-`2vB!y>l@&=pkC(fd4Zsw~5IlYPT3WXge_S_U3nL7r zPr#;o>g_Z=#|s62+^O$LY=vXo>i1M38%hFuxc?msYVOG2>E78-1S71!_@2E&iEu~) zr)qME6x|^PZaKLce;+7?beL)Z z2pnJt`l5QzT{_r=w{-{*{cL|GEJVJhBoA(jJ{RF9lMXrT2~ePrw14!Jv01CZYcBM8 z2tBqm9S=AWJdd(!oJeuswL<@xq?Yl)c}nv5j+M@~ooJ@R)S-58yWN%VqNAm-V7Yf1 zq3_XZk2@R8POX$hv#Ryfn;Z8qhbh(H)k~%QXAc}7gq(S_g__8WD&m-M1C0j82F1l} za_uVwewRTx6M1)9J|1y*{d03l6}zJQ;zEm(TkzfQda6!q`PKNR2h!R9+?U;8f1`p5q1{&RD{mx*Jfg%pqm3De`Z@+YW~5oP=@%mc;67PsZ(@rugEj&H24 zL2Sb#F8PH(7Sq)PGYSu`d<{r(%HXmZKo7tv9>zC1Rqy&eIX?LtV7Q~>&T?4W``GzIF--ae*Cn3m+ z92obS121BtV1hFnHwM{HXMSU|R&B?XD&|L`1 zVbJ!ix<3uqbrcY$9WL7`zE1}#GlS;!rba=^`J-w*8Z#L`>z)jc$UI5YAmD)-=}huB zwi{lKb2HI#v#pPQmNXfFyA73rT;DXx5fMUmBFC)@@VIu616(5iEi^lBY=hUNuE9!7 zXQrYUdIQRP*U+SgjZO9cp;i-46I&Ud6xw_E@`!2wFM-mFPM zihmKmT@ zQ7sN{wwp`4RyLyUWhargQK-nX)RjDDN_z$FHc~pUI#e8E(OL7Z?-fSBv3*VlrO|Nt z1@kFthosYOTj|9uOa4(hmseZ$5v~b$emBotj8u@)2MWL3R@^#Rq(1jjqm)7kJ?qYWAe0T?oPH6Kg3nm{h&9B_0i(s>=TUEF1^C7Vrq>fZ z2V5&t&pnKP8@*fIHX6Y`deX*jKAOV_q7;0HH@SpecLjkr|0Tu!KYG}-TJ zO4)U}C7}Omuv4~I1Q%_8hlHu3q-;x8(%VcS^Y~B?g=XvJqGiPo68+L8^=a7A7xncO z=AU{nfi?yoTFb^k4-jgG3jzu!!YqrIF4`cIL-YH|@ zGtrm=eDA_RdE*e*p+c1}`p!oSI>=Vp?bE&J(4FaL3ffrFqT;oEzG4pq8YK?*F}i)M zWtZK~;cK;8hijU5W7XwGD|Hk3q6Z7p@A1_HsJ5Td{h|dpSh9AMPcG!PNW~_^)C@12 zVR?|;;V}cU6>wV@aT&-hvn4DI1#(>ku>9}*8BwMQg1yCa;N4%H@kinP=%k|*!cC0a$^B*%Rwnok)L_`7sPl#ezUh`1~YmmJ9MH&69V zGp0aK)TS#lFn*u}D$+D5IpCJ+SvLU7&_Z9T)~N&J?Y8(RxDpBObNuCp#Mc4#>R=~% zySclHBu%R+RqWPob}gMOsK4ecR~(pGnVmL-y#v#wU}CmCw*Xj3MbudEY*3g~r$pD# z*H85REt-kd^BBhlD0+?4QKUgxw^Ie@s_1tZ8`r!xGB6b1Lla`rr4s`sR}!oOBYoOd z`$)KAT@I5A(Y1r0Ah8+XFP5dwolh|xxTd+`5I?xMZLeLB=?H06>5`)oJS{$MDjhyH zd@CAD^l~-js5xVaiPkYJUiQ%P#wp9Zd8)4OP&D9kiArrFMEhQ8G0Ycs<~}e zhl9;#5Wx=5P9luw2j7;f0Sr1_IjF>@ciIdCHG+QA^Q_(yrCxJ_d%lL({^sf{N~sBY z8ofm0qzPQeyg>vKMWA;!O&4dDNK93s#$ol_VYhq_km`42rliM%l&{xc)0gt@vs7@x zfkxidDr-tw^yi!7YG@Gc)Bm_G9%GxlCNYJzUKu1JR1{|Z!Fe*L$AOrS;n z12FN}_e9M9^6}rX0gtt2NC(6j)I_vBK3>!c^({^6wToypXSgQk86^@ogA}(VXI=G9 zRKv54d50H(I`#GELLVG<6WNWl+8=saB(7iEcsJB$pVLH#nfyjaDgQF}v;*Q{s;ze+ z3onzOug%)lzK}6!1A;J?hGvWmfS&v%me4!(ARFYeu24JwarwuCEW_z)z3kAcHbmlu zm`Y6QulGljMIv*E1@(=$>@U&G9M>~Ph$xcFqm@Rq62Zkf{S!Jbb4&RsGR$0uHYl`j?F)Q| z`grwIxr_TtcE>MrdvqMkj4XK_PTty_N#vn{+?yD*a9noLw3$m!p0m)gs>{MZ~@ z88yOWD3PfI-ju&~J1od6n$fr}0~bgOGa&t(9SPMO)K9~CP)Jxb~bOKAq!4|;0dBeAWIcbM{m||@&$rKF# zOf0!M1qwD^X^t3G;*V#~zZ^C^??jr;&1er)_{BQe68pY-;k?0(0&=@yx{0*7VR&Ts zsls(XrD-35#s}ps@8sbgd@QgV;gXeS3HLc#*UW z^JBr7{XbA566tX{e9L3 z-jO?@$xvYCE0BlXHIJ!9W!QIS!rQuMU7EJRC}9O!hbe+Dyib9%80ZFovr%)dqZvl+ z-7&B70P1intD*zS`Fg}gt>Dc|-Z*`nUYIL8DcXLZ_%syfXu(iu!ed#s<2m9}&5=}_ z8F@lHKhQ;jn2i0U?h4!0bnm-~-|XZ_n@AsZ`P8t306A=za=ah)7Q3P!bzWU;ZN}Pr zSbJAVox7p>HWX`g6+#op{KTrXwWw(CTT|no<=|+J4SL!PQ#@1o%sBqWNb4L-ru}{; zDcga86LC%{_8t}T^3lovlF$g^YUxwpS{h+R?9cq|&~sG1T;q920zG_oMHdvr4t&Lk zZ)~@J=gxs(4Z^ZUMERX9wfqX;SWHnq0S zL5R)iVtJJCCTytK%g5AoJ3eT`RIFOa&$75qN@REl#?9_tX$a+S z{r&$FCdsOp(TnGY#&kV~*CDbz*4lnUmB`-krZ*9Vbn6+r!#f#!_$+aV$)7(QtkTP* z9n{33x4emcaY4|l=Bb?Z0;_fhX~*2R;#DPPzRhBm%cdKW*?`P29A6Orvv}=h;4D(Q zJhAMoO_kN|sp}(QGD66#zH$@Jll}x#*HvnVnVzuLE}dpVkrI~V*+A?6J%Re~H^2W? zs)oO>?H|uZ2e%K7dD(4f$RA=%;6K#&HB=Hx4Saj$Q{1(e9q(=_cF6h3Z?|Lp(?RhX zon0;Pw96k1$S$uCgRGW)r<{S6N&zly*aY>0akaN)LFBYyU(T1Bd|h1;h?<)5PhtPq z+<*P5Pp)xOuU!ubJD7@5{izBJQ992ZUF<29U$QzSj#zOb_^hvbTDwKQ z+uX!}Z>$D3?_Z>PLSMBdke}1q;NWRu(M^W*xOT>eL zcrzO=vuyNGbYKULbSD|wr=SMCQ+tc(lF}i0ETXu-`E6vbfaR+1_C73{v-lfZ)00Pn zL+YpZcaMswdvBT9Ur9BYi>;Y>i8sGO%VM5YAB7OE1s9+n?MN=q?Ul>{5oiGG{hxFF zYtbw`z?A>zTrdB%==X~6|Go`mKknL9!Uo6{r&jq@Bld6HaJb`Ec_B7zyfD{v5!ZsF znkj93V~b@)9>E-M1nr0J%;6Zf;s<=Uw9|5C)f4=LL<*-D)v$=cnV|?Pak%=;5i+od zl-n{4r+lpHCyPc4!#kz*F%5OMK|;Zp zfs2M&7dhVj*J<(JM7aKa#$ys+W%gl#U!63zg#qf@nWKU6o4hfHO&tB~Uws+p64QEo z1g!{U@xilUw}yV~`h zW@B^O^6p0mxSMaDC8iX`s_|qjUY(9L-xC(~K&REgN%u{flhS2suRx=>qReU)2}KUu z;dnP_@%UO{t`V8n^l6zbjc*{K_Z7;PMmw^z#f0nM2zu!{7ezbW+NRQ{a?*pVIRL+0 zI}7-!#&QP-e+FA`Y(Hvxfa5IMK=Jj-nE5Ga5){X-eidsOnRc7QzSk&`uO_(7$$h|Q zc#k#1HZ_7GK>buv*FQRlOCtkaYJZHSD*D~~$Ovfnd)R8KM}(JOX@UtBjE^|SzNe(I zXv*K%>Ww5(!dYk{p+C!Kx0Srsesp^ZPql^2x{k?vQ=wQlL`4zrMGVnB^n)zY-P-a| zcmlepHAqNY+dSy*7$!VjfPx6DX#=aPJw?+4n2mH5rM<(~sB@*Vxr zx-$eh2N?#ZA_F%qMS8t^PzkDwm&(VmFx6d!^^i6E4U#DO1 z1-){IP#!%bd^&IAvFs{hUZ)jbElyB9zT`b^VotJV>jgGd*5oWuFScihEhY-Q@s|Z>$@iOvHB923+A1Kr<-jo zMt0r(a9p$gvCydpl##yd&G<7TgDNZ-i!k};xw+2Z|N zr6PEVx40bR%R#;gNgr603@v(RYB4ZBYd$zQRF8{6+ZJRj@E5RXrlV{;VqZ2Ki``=v zl4MNA%gqi8sv9*2)l!;Io2BS%ZTR5C;0vIBd}&i8#e_@~t0)hRM%c}2%^Fa{<-p2i z0JdTJ;}?RP5N5uJBsp6#`{ASQ2XgBRb`IiVfYVPkBad;Ye#ey25yQVRiqgZM>6Z5RSbx#9# z)c*Sq0`61`{0~%eAYA)@`r!GIRW&8`L#^@&Jy0$1m}mZm2KymK_=(ZMkMI7c>-%v+ zytnvgnG*+Jt&bMxFoi!RSRGScm1(sm)vLyxL)>qf7$-g4$@d*%JY@C89~D(b>l;Sc zh=_V<=^C9E*V<5U72brBVf0pI`X#v*i%o4YPbEX-*6PxdlctD@`A-Z6-M+C&zBH)hitLx{c}6@XRyb^|3zmFMNI9pT6V zb~M4h0IUbXmZT$lA@8xOctgdQJA2}#r|EK(<@PwY<~6e-{Uz`FVXsSQ4!BX4FX{^! zU9o31x<3bQ#lD?r43mx1w#TNNzw2V z`RtRYQoU18uZ-$j@&|^RJZhbkc5r+)Y1TwJev8kQo#$6O zXpPF{qV`K)J{i0DcofsoW)Mgq(Zfo>vL3f=MnMrQ3(|qw<*%g6{<$;hN|&4b5b%oW z9ci>@ zs5G`i=<>VauJy63O@5zAGFNXMm=}i?uXcFaX}eW7KQaTQv@iM9e6se-ps!c-wZCwi z9x8#XQ!9TPAY1CKb!pow+Jv?xRu@HYiupRnZf!|DDxg6ifJO_;@6H`2SgrDqAbmDE zir_^@rls+^6q`qr2Vz}cAkVjrIk-k+b(^Nm8u@$-8<1gP8CSRTZV2>yJ{KYkAj`w( zC8j{MU75>Z+*Oc(graScg@`VYaIZs#DEo=q-O5@D%jrmul)fu@tqou+~_rm(gDavpo^6 z?JBuz_Fk1aYGan!V*r?Pnbo=m<#=RWdQL%!-;F|jk-Q~qA?2>$p>jC}$7G`4P|WVk z#!#zYDhLb#$%NYTPrQ)LWnO)8EpaQ2T+0Dxivu}CmngDcj6o~2?n=3vQm?ljS&_%* zfX?tWkTh10Yq3P9V`HA~t}8pajP7xpD^(#D8jgL+eSnnmP1%X`i{r&LLEG%{W(gAW zB-_Tl3I#8MXo`9)8EZ?_wG&^VeCEEfSE0FSQ1sSFX(>fT)>)*U7}>lJ!q-bPHe;L#0+%G){#L$fe|Y{Q5Ew`Nt^r1~xZHxK!k; zyD}14VjC;u=>-kzX~~gG5*Ly!ca>6zlF6MGg=V z<}=Yus4Q1kB_Zy^@WcwG))=AW8*zQN^4C{QW-Xns$tBadt$V}@WPd#Ml3`gASpia{ z(-PiCZHnmXSjwR9C`I{H5jm6y55ya-`#rbFKI7c_-Pucnsh!DXd<$Gs)EZih26hep35lnF%kqjw4pgRJT)j*vnT7Vg@lx z=OjYr=GXYi)h2K4cvCvvUte{rdO-~`x=^k6tkb&i+{c|i28$nxMXy2e(LU-%=@(sQ z>oS(I0VEUI-?O)DvoEC8c}7q85r&9c2B37C-(yCoT&gia z#w^Wd#OMAqtt13$R^wzst!_bXPVHE&EBI`%PK9M$+xtl5ufz6QDMo(6dLotMtaN%4 z=ES8K#YAtziL8}sP28?nSg?egpGd-0W2Xhs z$lQh}P&ZA>ju}wQI6kHBFjS!Xd|E*N>^UcvAv%G)HlAzWmbAnz>ecFAYR45Iz~6x? zLaJ~y8C46Nd6Q^Q?(9f0b?FaW5?zT+TbQLqM4zh|&y`-B|EFrA~+%3^W zVC`32#`W)NJUPaBXO>|-!e5L?a^b&G<61crI6?mZ{l$SyPI8oY?f7{d5? ze3{*EsR_MDcgH;g?h;QaG>o`JbUIMpH{BW9VH~MQzP1j9HujU-Q=vL%h12tYZ;+fX_Gac!Yos13tz#P?MzO)tV01l8sNOA# zyC>98y~mr3xEVOw?x!FODShFtdwCIRl;oi9c*$_LL^i9)Qza)jX(Wo}*L4TE%&9Qx z@Y`d-+`swj|4Rv=J2VaO{GO8dk z{p5<^W&}8R10?E~G^=qYE~jbAIi4R2iQNDU>snd}AS?8Cf%W%Y|L=#({w*#NP>-`~ zoU#Z>V_)82I;VkphJO#RZiuX0}*&HEH|zR)Dc_;Ckc4ce{o|5@POP6xU;`V5eoM=Y7ODmGRkaux-H%AbToT8cT0B=A5b zjdc&cKiV?0k#X))y_I|ZQqPTr%;ZtgyUcNL3-M_zh5K}|HpEQs4>a!p)-1cKUfXsC zGTtnaZr7EYP;W1OT>i!94$Ddj{7rbK`j~*-4K3HCVU<6Rfqwh@5&!%1hCJ%%l;fKN zv&I*1<8#Gr_xcfX{^@i)eX%hsYma|=n>J&*pR;y^D_!-~Tj$r9b8yiiS2yGa_e?)s zA#?JN1jhL7_c>}okyM)tBh|0%D`^scZr<}Yy3GO!q1pLxd!|P0@@2^jF;R}Pvq=de zCPSVeVx@ueCj*g%F?)iQ6oQ|uMl{pA7-eT53LBbLi(EjYOxx-6dIjkf_v{9{-ZNJ% za$@SZW@QPBy0$>flV!AwjC{V_X_R?;q1agl0=;^ROUI=2=^U?9B&6P~#;c$&U!6bS z$)xkb1w*uat7_Sk<1JbL#(vZLH%r)0kfxAxRl48U^1rc>-vI(6%^lxKjYEyY=>Hr> zd+hC_j3r#aRn`WTdN=S9(2p|#*%;Ss_xuZEh-nuBi`59)^6k+(vBwd0!ud7<|9ED@Zz@B@l0lIL(GdI`dUSUX5G2BW=n5U2t!Ml>1Yk5EEX_I?41B#``H{?FFrX0XN^|em(teI%c~{ z$4k|WpudwFT`87FA;Ixr70aXX+gYh0LLn7uQx{@N=xeg;+$I{f;#sY_s?oHw%>81o zrVJPg80NL614@;`b=~6&{NBY^=ucOb-gt5CD-E7V8UZnsyG4e!mHh&uw3ym(;XTwQ ziu1Zgw_j-HZ#*#(j90zi$<$^IlQrP7!QoZ2}lsiCmcX%SfgzR7Jdqbm&65BE5N)mZgo; zOUTe2E|W34^*?yO{_RJ?cj+h8O*;eo;4M|@M+2Fj#RyKm-lG#hkq^iR4BLBTeP%@- zphVvQB4SO)jr=r%QP2&NP)$E!KZi~5;!jsU+p!;A{>GNa6T<5IgMu?WzvHpWad^G< z8yk}Rr)7uFoDAo%2WYw+F~n5WP7@L`?X!tPEZp-NC0N|ex+P_tbDzR94=<zojIUQ+pkWZ!|&>fybid)l%fsb2W(RYW54&T8|PaDwHqz2!!3ylt!~*6KF}2$>oc zScDksm(l8fd?c~J3areTw+zltjXYIq)qXpA ztfN@k?r5y)b(W>#M9vVkWy8Z|1kT8@>^RLH@ZjB0zi^-%wmr9GI{pi#9mYS9OfR;b zhWMF941&0WY}X)ho}m7WfEVxTYBf+>cjrC%fzI z5R6{@u8XEMlCKZ-V+MX5ZNyJ#-yFoW7v$O&Y}nKb)ELDhIg(71LEXXjNCsU})6 zz4BfUQXdIl+~Ed_;qYEd{CY73wDb@}3+c2J>0 zB;Qu2wEnK8B}>y0_`!YdZ5wh$u#2~ph^8+tOxRP&6^hkn3d@MZx4hj4!hjp%@s@`1 zjG@+ZUL7pxQSS44U6Q2ql(gd;>OJRj89byc^(^=7+Cal?e5|;4yb0B%dj~dAWr*AIf zu9r*B$?k0#S|{+`d{aMCGqd-!OmHxLo;;^JIFn7MG1t$l7d#q*h8~0sYbegFxwjxL zDvb~%5DF!}Fkm27t?c|}Gw6(9dZDw3TKN{WiPcv}==suH;5sHi^PELSm6JG3_NGTE zZjG(`LgR}e?>e%o^Ok1wL(&&Hb~+*kXOA`ytLJ5rfli)|5@73@Ho@VaPiJT-;2LIg z8f4#>w$*Ekmh)^btgqA_E9W9~Fpz zf|Mk5l&XXxgql!B9q9xEg0z5wv;+tN0|ZEr-a&*w=m>-oAV>|N`E1YJdyePz+AaMQ>Jy088e^C6r9ex`|VH#x%se4m|7rAp>qcDNUZK8)z=n#Nt%#4tulL{G zbQE_q!)46Nz2ySZ3%+q!h8l$p!GU~t6X=g&THD&oi5CHef+bHXAV7kIernB;@Pb1> zQ5i9-7xpvLJNBEwv@?)mJfqqfMW8F~YqHBA8!xePzP#7eZV1|ylNT?dt_n+Qo{|=r zZ&~{5T;~Vy=b?Sf=;0IPte$dU9rWF|Z#ujZ*7>8t&0jhC4ss4Fe#Isf_phJ;wWx)| zzONkC7ykXEn#7%)G$|oPL5FLMhkpsQ!Y4{y@! zVNV9vO6&Mul0K=p)AF(_)V=-e12i7>baEPDhqARlwp&>I{?`lS5Q>V?}(%5b&8U|XU}K7^ES$8iH#boQ~0 z)7jdNlW@yJ(6rm0sOaN0x>SGDAa*H0jjm~F;zTxu627P&s5KP%Tv~xwv-Ao<({_rk zf;+#{zU(qd2vkn=j4kL$MA?pAsrEp!PqBryLcV)~eEbVL47hzx3Ce08T>tY-fzJld zJtbQ?!(OIJUbGZV$hIis)7@EjcPB12kaQ9lKmp;XU=y{F0aka|Q@?a8`(tZ^7{pbF z@}~o97us`DfgAS4ELuez4dAqRa8w>H#6c4)P6d5!Xg==kzGNYNqRImF$$#3GQoDc%kdo=!s`y>PYJSwXElg$CYhE}Oi#|xSo{K$x-Bnc( zOaN>|v{wdo0pNMh6TB^Qs{Duvd6F49MO)`8(;; z_zpV|G#?e}KG@TI3P9TyNl}0zWsrb=M=YCoV2}^X%%KF{;s~F4OC=O@r&N zHP^&4>r0#$oxOv8wsycUApDw0F2L(_XVw8{M>RH(lFCkuw+X#^czFl6{u;d-nYAt>-&Uy1qdF!zvh@>m{O#Xjx z=HD`z{!^Fy-_-C}Zd3YLZB`4S_o8v6id!9ugPQ%9t>Pj!QsQh|Ta zt=1%09rrHAr&sQTjJ==O+T0DfWAS&`xRVe?S`p`OF~yA@7n?71V+pn5Y#Aqz zbJa}?1=09IyQa{jm4kk4PM=2Q;0dTgu-F&lo}<0YlE9N821$>=;c)_ekYX$4#?!wN zF0|1G76yrh3xIV%M+Z>op8J<09l@_!w~$*A-DI3|b;p)etHep*?!OuDe>xTr|6wExP{1v8t3Cjk-0no`)vh@M0Dx$cORZBd8~ zbf9qno+yk0`MKZwYtVkxB9G?QUgkQ2v(-m1kz8$KM6}OwNIj4u0qlk#mfrA&&Jr?b zoU{0ZtCasZj^#L`F@8X@4D4?ScbH&lhU(xU!boW~N(e98`u&s##DRrAOqN6ik=o(iCztVaUXfV9$UMn5ratqXcP2{n+ z2YPAI*90r`81U&@Y!kG*nv{Fj_M^g9Cd#SH+w|-kX{tf-)y{J^r8y(=`!k(SI zF`9TlQt0J;b4IyI+Srxo0?a2Ruvxx&@xQ|h`Np36uV6*Y$6CnPA)*kK)n|qk&B@fP z5s-}~24546VXMfxN(4y9H5SVV8O6(b2`Wz4a0XQSR~Cg@PvFrO&3ahzq=llpgT~F3 zLNi-q#}@66R5&TcPKXL;rmI2)nsQ@u{TOYc&Y$iK1q6t(K-^A9F7=E7x9kF|QEsK} zP~Gme!Nwey(&Pu@o^BVA3d($qe$=&4d~iIZt;Ch-f!JJOWt|;|_hptCHxI_n4>m1T zaprBCNyK-}{^D!#DpTNzqo<)!ce>_fH|5L5eEYLdrDeUneBa+n8CDhI`qVYG@MC_2Ei6-NmWOLxkjcUh&7BYl-}2t;fN_3;Mee{-{$NB##_8zv?xs~Hj7;RmbhsMP z_PB{^$Z_roBg|jo@Y=Hkb#1YSK2|PfTGfN&#?}ocLsu`omQ}V9)`*!@&r5nq5LnDV z=f|5Tp@CR8brzw!8vS zAu$dMdcJZ5o{8z6&gpTu_K;5U$+>oG!B$iH%(KA0_Wo*B!9~YJrWE4-=KDDHqpfM4 zr*2V~+K-9av_hNjN9*%~6l%FDZj6)!oi=uds~}Y)%g#> zJ5x&bXTQ^q`>#vMJ=)K0Nmk`oO1BvAlq_;q(&#S_+}o5M!YoP`jK14eO%@OKbZhpv zXKR|604laW1_BcZ{A-HYD3I|Wx%~XQ^6%er0Ho=wuePb1VJG%}`M&&ZE*Q;5(lv=8 zq79*Kn%|Ybp#vN!c5Gu0d(fAxva{cnzs;z>JHUVPVDJk24COAB*fG~XmPD@lAANlh zzgxK7Z`Y86a~|J&e|>@u= zdW(6RX;%DZoiee&TE=)Wkv4|u@{y9$)iGye?usR0lOE(i1i+5V;k6#o%3I(Sm>G~c zu!(=GwfNS1^8D=Pc-@1E3N5`ZkKwrIg0W2$*yiG(J5QmZk40L`V6-w8x7vd((Y2q* zAG`A4&td%E;mb_z@z1oX6>al>5awRWNPT>P8A%p;MK`xMW_O8-1govjAO3#WvLW9) zc|<7wfU7Ia@fFL9HIo~rt7D|qWSnozx*df72@Gq79P@8JB`7FJh2{XjR6qL-k>Adq zs4p-&0eoDyFp!*uN)qC_0$i>V(%sp6($5%hT`$n zej&N#s@e-6u&e#SgAo1WDuI@3OAR8@F6qY$y2DPCL!w+>pGbD(SD!HsSyM&9CEP+L zV#YFeiD%G%-r65;&1#QTqOO%rPU@G9VhboNSSg90Cv0(vJ8T_Dn4Sf^bqU&?BGQMi zE%qCeRQktlxcb(TZ}ZPSIRH%DrllN#j-UPkJ@CCMxIw|W@4bfqC@uBo&olV9KRLhi z5vDJ`eH8X{@az56+Z)bi;~Lz3YvH$#&w2~r0Zt%%mqSB-isQ(JN~RYr4Tqr7UdDB}$x$8@Pb-&YQ7#8-}8mED4k zzHWfTre8wyw39W=+_sdKL&~S}uYDORon@tqPa{R*4n<565JpOTd%uG~ zo$p>hrgy-rsU}l;NyaNBi$%7WUE06mP&dX{Q!RbMr}f}Tg+e*v<7i1^RHqqosKYVW zM?w1M%nh4X5*d2vR)yKYtSP3g@s~L|#n|TK+|;P{gxxukE}}lZD64_YO=2!!Z2P!( zxObc^!XO99x!a00)Xc*8fdk4{j?^&>S-W@WjVQyLhlk4qk-dBWFjF8T1av&0j$VmB z%<1~}6py|5Q1_#n*M^Sh*1gS-ReV#f_YNODKP;qVGDG(_@r!oaVR$I(4u{5- zE1!cSVzf+cZm;8q6lpb2J~<{)3eyJ%B?v7gIzB`!ONqw>3pjnb5WZ@!eqZY${64O# z*wm9lbogeyjZ5b<*iz?Q68fTFPvgMBP_jJ5$gEKv4`<@MXSbOJUNO zI&{_2`MrdEzZh;UDCnf-LR(U&K!Ez#mJfE1w0d@5`#sd4=Gg0_3+cqhTJg)}YS}K+ zD|%MO0SgOpIu>6!rm1S#L=*7&Uj|5xnQly<1&iRzMK#ID!2FU)o7~O~$47V4{A_Yl ze~Ib#X=!O2<|617!w$C0gX3SamIB|<+a7%mIxbz}mRZh1jviCAYyB{)?fj@czA&_l zdkj(6i7u-YmeNv|k2%-Yv>FETt*wmMp{K{=H-`|ely3EhTlUy_0Jd@}?yf`k@Z|7U zjt`H}{@dGMIXV_6nXzZI8@10kD$68yTH8_<*I{Tlqcfc>A<$bl&KQ<9_w79T_%bs9 zIqfJ!?pY`tBi|Byxn?*mX!F4=OC7tw3s*NfWzn;=#Za4l0@;&D$&XJft|aw9ai&Wm z!;X5{Qc}`FQOvTyuyaDJh!LTl+w}`RuquzWU0%pt9jKt^KuK$Hi)~55ow=r#oX>F| z>&QL!@@?DMO1$*MsnDxkA(49$SoY+Df|)fMfTP$-W-Bd>J)=VqmyDR5JkoWzYFv%F zO^F+cZ!|HYYR=kpJkbVpPphMw0+J=|s>{#>e{}ar1a^)LygJ$|o?tvbay67RotKxP@i=rap5ig*wIGLXi9YVAI>;wx_0(gKXmCtmw=Ix{sLHPn}Kvqa8P{ z84muso7`_Eh@bqly)=AsB0rIOt`|b~L+Kd|3)1Ffx*>ALx7ZlOmX|xFGP=iYT1%^e zIvaeMjOm!az7il&(vFI`=;A`F@geg#Z+fLGRTAAnQbnghZ3Wv((ep~3)O0-Wqy31N z&CV4?wlT`3gt2ECrS=4{si1cdL@X>}n~f$%P85YfwqQ0<)PlkYTCL{Vv7y+QvsnXz zZp7g8YGWrDOESLKFQ#36>+aA z8N0mher0Y}h$aobQ?p1uXAad@!HU71boH7r3v(R>(afTm5}8jscE9l}X{v!?nkGAUZ3Xs%rTAvHH_7uM}iEee_OQ4(7}pxH^1TvOFDv*Br)` zI|;^HNe`_ua>OBKm;-TwOlkF`bBAn__e?TL%}-upL2sSBb1tD#h#+@6vIW)nsOZUk!P|bG2O>FcHW^SK{*xbRT@z_9ELWSa zLv1-FyNEgZqYhs3#c=}k0H1sa@xE$D*!he`vYVfj%M+C^Sx(Hyq_p8+2pQ9$d#`+% zR=q8sU>tg7)Z3o3B-8z5Y*MC=%%h3uCF3aVzX;}^bbdSyC|vL-r90OT$6KLVQ&8oE zT!s?P3_)zmv?)Z^^!1JZf<|nzDmal1ua7N(UGfc~ZI)51tuoq9io>T$3VBX>kLBCd zEqTz*_(_(>~U$UHBctqz(O%Q# zbCHWyXuc?=Pl2CPf8Okpqs`InA%>3`sm>e1RvG8t+K&q{F+|3PUXT$P(@E`^zv4di zZ%?Ye`|D4*Mm$@+a(~K08{;bVE~e3$R{VffAP(yrU<*EarVALAd}f`)a`wkXve<~H zw*Pq-7soLYWkGHJ6717b`Xyzq=vB4i4u4TArdEeiw{u!K!Q=}-`Kxhq$j%1?u7~VJ z{k*`ZRA*nc2$`vhyM_XT^~O)?$wM=n__~+*GWU>H(UFn6C1{hvN$CtMy@*4?{!8AW zL0u7O|7u+Ve(4;L@)_2U&XzhPi2hmeO``wfSyd5-yjlsa zcv0X$*36_~plMpZj}3lIjpG-3ge?ItvcNVa@C-7z#|H~+sSp0;QKCGQ*@D4y8eciM zrve7uteAq}2Kvfy>x}hftFF{jmUrxE?j2jv)$&yfyXtMoI=^ocGSMTRINXEx7%$;M z*!d7pOf%Hkt+WWA)Yg(uVPAr3fv{UpO(qcT4OInqV5^Ujw)@vbyARXZt&V>Nh8~F+ zQ|`JPg>zOBPUxI!`OIkdY|M%Kbf?(OZv~~CU9B%f764cYJ@B-_Y-~1nusKDHEhuR1 zU`R8P&Go#Va9Xp=bWWitZsL_+s>Z_SXPy{sADFYA6dpdgH*gAI@wH|wtw(?5(0P3L za$95G{uOjr+<5bwzRq$`Pu&&#re4AY9uRI z^?mWrwP%0o)Vq2qfL%=j;#3!mD5yyAoltar2B~$#?#G)$8n-*TjEFI~h&RJTD=#E$ zD2kwm3J9pS8&ts_P!&VJJIFWlL%iw`o#!yJ|KQ@^&+D%Sh7C-piTy&H_9v0=TbR+A zhRqStBU(zLpATR2KMV-TGAhq#hE>RFSrwr&Zy6*Ev`wxj9a!7tsDrzg;nSt$$!cVN zfES=uzMoy!ClL1TaSHwvGFdQ5F@t(LbxF~O=1m36#+l`dL8!u$*%j9pR&Pn=sK1^J zczsR^tJn0%mq%~J*}@X>D6fn*lBj>$P*CJBbFtfj)G+{N6aYvp$sFw7d`c2h1di{J zKH$pVy-oYu$$txI`1_$X|8}*%0O1V(I~(Y6OST7BHViRF=4ix&7{GVWKw_gn!?Fx1DkQp#A)h$ipq?lZ8@W`oH+S zW`CIY%0agL?Qg-(|2|OfFMr$TU-~@vm4l0Sr90c~6T>nYkI$G>hm^bU;Vy+IkCn{d6ZQ({0Y(7#bb8_h|M~;q|2DiDm_&jjw`y$B>fjc=CmL za|lU$;xIsWaeO3ZV1G}_ht(e8wtMrBgQC0{0&QPUt3r}nk1Tcltuum(Yobj*yR0P8 zMr+9yw;Id_WLf~H%554Tw@}zQd07~@c#M0S@Ljp=TO#~-U4CEwmJ8pJL0*42LV5X6 z8z%By`P+N4EMG1-#UKCfg7HY}cjdooJN*CfVChrrve@Bj0SY^3ML@(}TBVS%+&vU- z@Y;0+8gzfWP=;CyxG~DEUUI_g*bPb%7e;PAn|NPzR&0?SBI;Vg_9ms_aoWkucE_c? zVTZ6fp&}opWwd6$NDE0#N-Ra>)aZu#{8@&zUB^yVT20Igh4^r-7PaNabQ;_$Gd#Pa z6)Pr{Ew-s^zFFY);O_(ce=7a-J=7JTrGD?6`;!29WRbqU({)Ut$5jy_;QP3xtXHFF z#mU_`1Z?c`_$~g_vPBYFmm9o_9E*clMNz@v5J1kxcPfip>lYXL>1xIWtcy^LPx*b#uV<%*nD~(35v+)`1oVBTNZllE-HEvGI zPzC8=B*>L6;xFB#t?8utTya<8(DdRq3f4o#Bq&uiwAeC0vN=!Pd>0bc;OLmHQlKNP72WPNc*@ zB6{wg{i`%3*6_TqMV}c{E*ZULTtzlxiYioKszaO|;?M02T_Iocf^#j}YHpLe&CS8~ zAq91F;7woVHve>bQx3ou=>5vE!oPc&P$PRhZu~NDtjM|Q$9YnNShrqJlUF*<+~w|F zhn&yh0wB(}cxqIWnW7@Uj-Fj8|ENZ9>F?|lqS12aPOYvpL&EkQ!*mw4e;UJ2tDseR z-S*D3!!-*A@M3i5T7VWcv92038|1YD!ckWiX)*moJ1oZJj7hF`V}^fp{aDL5*$Wcc zBaQkOX*#YmOfw zbY-dRot;|*c)fqy{;A!50RK%oK-WC!3Eu&M@NB4T-lb?u~80FBJVwVIIIX!pt{d1 z@LGFFFL(RM7x|XJ#J7Zx58X+N>`7Er4;kHif44O{)20d0K2L55=dXs?>y8B6)jM^< zzsXk zeiPnA3nK))Ns980kui2qzEfi>nBya*`YC(Mcv2+LwDR3U464n?Y(qJh3a=hxiALSA zl}VM!7{WZZO2CQ{Tp<3EZ^zZEEAeiRZ=B`TsIwAOt`ruiA*94qAH?Q8tj>dKwv5 zp5_hyB^<-*y|g3=P@gjt$ zT-|!F2=88|)WNE&2FV#k<4w7+fdJJp22O~fmM+H7NP==tBShmzAziPw!qqK24Q51b zOX8Ra8B--Ohvo?upg*bOonF5ay>^7+PQ9^uzkcDu%Ci^37&eZrQz?5w=JPuGqrI83 zs>LSC4=qe(f+NPoYp9r#)0B3Wo*yyn#nAPx;6Ddu27ha0oz~EA| zoAL-m*0)@+rmq-5y&-*LBB?~a@KE;-v&~d+UYoITB4A&Rodkzr#zZD z+g8u#g;3(bBWkk6%{4phnPChIPG*UtWtM?w?WhGT0hHXwa4PqEEo?{j2Qq*ZccIT9 z=214(7feTtbPk+np?JH}{wH=w4mdaeW*cIuP*)2P26oyyUykJ00!g}TcseXAborPO z&snb5P(R5ZntNIFXA}HJDYE-FXNhqUun*o7_!lvtnJQMs7H4#c-YGamon#?d{FB$6 ztW(bMyo(OOeJmxB0l4TSH8t&qQQ$mhd9pW{^kPTIvxZ2?>CIx78=uWMQ7j zxvgzw`*ZtMy93Gynyvl_?_}06G1eb5?cHR;^3VrqkFx3lSfO>I@IfZ!}T)|*e>!4^kd+?4NLaXW7G zBRXq(DM`^>F+KBbzS~mKZK&z<)1B)yk4}EsXenuMIWDB>{%?B)i^Bb+?uEe|Fi&T4 zfc8e0HRH(-GuQP=N8$2Acc-Bw5gzZ(IKQEGhL=*m#2=7%cF+v6$;xZ23husItWenJ zjV4jC@>^|#W8~XNanumYqpUlMc@I|BeJ}CikJ8Fe5)ai8rA)~D!`$&g6d*t`(9zE`iDdAtgJ@IAH9$29P1?$s%K^eu+oEy(g||8W zUqs~i+f?DN1vU%dpu+3o6%NI*c3nQ#rVjQLW7@q;J+_KKr>W&noC;;m-L$n@$a$ue z+D3ztObP}-`EF7nKS@~1Rb<(*>$(oTTb>#hjNZkalDl_)&|1*+@x>@d$I1S2)gq{> zRWupQWkc85^s^7%_{Ssv>$m^QaLNB@#diz)uibC}5cPO1;UM@|j^Dx@gHq;Ke@B51 zBj>aU9S?W@miC&83f=j6Hf#aqxF?if6ILfI6CO3Qtp+f=eE%U|v9@C6Zrj@zs04ix zvOvo>Y};OBA&iSf6Wbnh_Y!&Usy2GDf&cpCgR6TlRJEJ%@ z(_Yqjd)20tx#)g(i!o0MWnl5(AG)VN(l46=>!zc;2j4y2R<=sI1d%gZco*h4Qa1;| z0umXY!g7AxpWYGM+^LzCTlCaEaN`pJA`*SMn??T;m^>!l;$ypC!OdMd@=QZnzglV z3tcq+_D}(LcKmuPU^>bj)c%zYcp*vR`-JJbMZfqiRkILHYQ3;1zmt3>=5j#wg1pEJPdftE-R#@@kV@!Xb@3Sg+A8wM&QCE)R5VYYhH4UPlKLzbGL!Uh8AEboM*WL#X9#wup6Ejc zxVldlL-d8>KuRm-3k3xfAkwhvIC~fPUAp|7x8_}}1rblC$f>wUm>(>oXJp8LCpsrB zI%`u1NG!AIbdsSfa8r=b%OSw*XA3@yOzQE$f5Bo4591}iw0{1|p zwUWBbQ#C4;0P3H)v9VFSS!pSMS4D3F`|MvX7_(HWP%VjZmJry&(jA_+j$Qj_sOpgMLT4_6wAD!^GX@A|J?GJE-pPIlTfV9)Sn1+HWAvO!fQPs>lCI zfTd}bqv~fa9?;$HD_<4^jKJB*r9;0TZ+we!Gm6aoL;KCQr+MV<6S)sP>HB@h56nWY zUpuVry5e6STQbOi3hr>IU)we*8)@G()SkO%$mbJQ!{5ikG?n1#! zAZ&SVo>H;y3LI;9o*kARVB3%;Dd!po}%2-vTob?j^ksS!9@t=8dbGnJt+D*=`v+m@0vi2^Wso;8-b`^?Pmwf!*KnR_f@4|kxrqN zHFE^tJF!NJ8XuC%GRw_CcYQfimvhCcRTF%d6ndpv=f$Mhp?BjJ2JH3oXM(K)lf8Jx zW@}ytC%bFF6UQE_OP@KZMq4sbEBjEjgB?vC+Y(5ms6xi{#cj}1IG&z3_0V3pp-zTXOSrYHYFbWG$wdofL_RF+shZm!G+ax#n_*o+EO}JDT?h*; zl(^et#ub6*OuxbkQ2nG@Y^;c@CYg+z0IB1j$iMx*er$s>qY_v(`EdLIJ!Cz+8Iq8B zOF#*GXV|khMEIs@wK$LVgm?QnK>0~h2af3zqG%Y)1t!(xE~y@PC3^39!qI_4l9a)g z+Yz|)7KXlqv3&zittMFYa@eeUoe%A$w23Qlu+ZR^OJ$Rgao$XN#6VT6c|)JeO_Iyt z+X(zm_o}Ik&YuV6xg@;yUmvnHg?Y{zi#A$`@s@6h}&t5YpAAZN-YgRyiU zeKBzpi3LEcOPH#jfm_8s?F^da(I~F7Xq>x&nlQQtdbvIHV5`Q$EwIq%gaJyz-8#g% zt&<0OPqtTCZaj0=-FJ4`fqZ}K8r)UYN9#oZ=j&Mwtr%s#n>0lhvpP?0GiV;tW}Pc| z$yf2-=__qG`E6@EU5LJOF|1GUBSVHSf^xap`xy80vJp=?N9wOh)6r_F*k4DSJ`us4 z50@eSK_$q{Ex4NrSjp>dYd0%dBK$tylNPV0S0+y1eU9qR}d_G~jV!?Ms-O8=!l_>0r8 z)Op7eQo)dgIg*DLkXgrm<(QO`)CC*=b}pE8-N$stypP1={;)LN=(930w#^w=ElArp zo%MtkD=q9@3O3anNVONW6cvSn#^#myN~%{{Qxt+@;-*F@;@(@S5K8?^VimNZFJ#mx zdl_jNpL|t(OI#v`+M5udE8yoSuE&PWNh2JuVwd>IJ|5bNC601a?hpgG!}0*iDgmma za|d$@ZBeuki6N``-iEe6;X5{`WaAl^^m$xrq9#1arPaZu9votjAZItPd#;=i&BRen zxbfhnA?(xk&S{k-d4uIPq9o(e3R7{7Bi8zRPTYL;>v{QM7*wKMY?nM#Trc5v&;2f5 zfC{=3-AYH5>Gzv4x7bG<{e5-312NqnTl6@+}ZKzXsbclc6u zr1hdt#b;N~E{nM|wK(75jcQw2UD^mfl`xp2o}I>@gi0L}rsp;Rwve_>f63KO$rr88 zxnD+`x8dnWYsZ(YP*MX!#oIw;MMOt5SI2-(fSan|7-g|-01CKAaYRsykO%Lgws>pn z?u>uu{oPFWX@@Dpb=}cX+LAEEA>T`H6N+q$S{38x46~tLzZWWWruca?Y)1p`Pq zf-1J^hGWF3MGoQ@q5&{mANMEh8|SAo=}oQYWbiH`bA5z{R_PFE(PC;5T?L2%T`AXl z_mQNQoo|i+asxHDv6AznvaR;W3>;f>gi`X&qf}vyqOeq<>&CC11c8S01pHD{6tGaDB3cNQQBc!dti1tH4arzzjoys4%lri!BqMHYVS{{7oj zNQ}Z4wj6Ldc)@*gB*uj2OAVknKvJB)?Q%G96tE;Gd1ko>06~tR?f6|<1Jx&N()e{1`BaLrW9Z=x*t#-3?&J|pxku|g}i6;SV(Ew+5yq+Xs~aXz9w zGt3~M46&9Eq1b(7xx;H~WMeEQxIeBVXE#06a*JZ#*|_lX5rAkoIiqI7lbKnEN1Ya; zqfwB;y*;Jrgdt|%S}WfSt&SY-(Q)v4b1!;cnzaxp#P6E*d{~xF`zP2*0^7BBJ%7>9Tw_~JY2xGL`*kgr( z>AB-`ez6Hypqfw$?2X^rL^0zU^=3mHUy4)1{v`+Q?VFchCcPf}i_+TDKI^6p~mdeK~#kS=afw%Ijy~UVUYAWMM>u z6ro#J*UNm+Z1b+jgh@iOYn&i=D?LX(FF!#haJa^~7_ z$#3!0Qao9%fWWVj6k50{=UmO>WJFnSiN^47nPTB3lW#l_c-bfZx7EA%N@nctO)tyl zq!+%3{&}LB;%(|uEEE!i>=P)~f#TFO$E|Q9#S3pZKB|s-b}wewrqmSA+2AEc8(RVGICE!E?Jk_U=)v3&?6aWTeN>MXdK*n+S0>&_cU$B8V_sdYdS=-* zuO4RNK|Bl+33)rGzG>tuM?6lTBI=KO zd!iAEp6%L?yKD+GB?I6EachZMdck>( zCEnol-D5}m*y}Yy!zs9S9u1;Fg=3}~PM|2}lq}&B(aBXWAA%bi`^C3F6$)9?ZCB7Q zW$f4<9X+gn+UY@(iLR44CZ%@6AZTf`P*>ock$yfLh{#nkAx{`o>jMc~{u!W{v19di z*XtROBHIoZKZJzt`N=pYCj2KV%G}WRXZ$@ zDe;`Vm+mD3h?t#tb$7gg;cZcIevb{QTex%B?&Gg8!gE2z%=KzHVkGtr z^?bIzW_&e&fL2}=d>5^~oHX_d;7Ko*>04ypvMl%1PcqAY&44P|Y-h6~ES_=WcV2hu zlcuYB183|@)t!s8IsI3xhQr5kKf$MEXWvwwKQ!ZYQGNd|%Bm=?8=)Fy8$n&dD>trvuS4cJx$hb@H0k4<^g$i5X_3AsqFV6(! zPM7S?JuHWzB(}oGvBJC|#lw$A73PG+pil`>IecNlNUq=OZcu=nd7M@BjN$mc12gf< zHqxhMZ7c2;r8nDMT}60){N=Od3SonoqDBcUDyE`l@?3?#Jj4ZOH5=x(mvpB3xSC26 zhG{x;)!$Cs5eBAhyIFbni8>XNn>1nqjs|eNUE(I|$E{dPj4Q~m~L0}2wvpw-zW)I0b2dZ@dHRPj7SkYdLB zF0rmbn4u#c4H~b;kgnl^q7pSOE;YtKjK`sJ6oun#;&GxOzZ~>j9)j&9w!;&2^s?3Q zPlV5%F`dm4sj`zJiby{0d+eZSF?P0uGlDyPWYO^&G^Qu>5RTHgy-Djk%WQ1R~iT>0;(e=GkMP9(_~>4)&WVX(7JCL8FTAe0JSOVFCW~ZxjDG zZ#@Ck`pPjfKmX&|uN>6?2d1}fo5cPAAnTZK03Ek)+Y!d>y7?2@XizLB8Je$mLWY*Q z)JJG)!DRpgLqSX`F;2TlB)4LEEfsem14T9zHys~#?zV1iWc-YSJ_~|$S~@R@>RA{8 z`DoFtY)llbiyQW&9s$w4Hv+a~051PZNP?N{$0Kb0zrVCvD5NMOb013DeTcp+%~TxOGFyo1DAR25e=UT zlN&F4hI_V{76o?Rul9XWglf#y2}b#B>9|QriS34dCOQS3y4kp#vZ5ka3)_UV{ex2W z!5JCyHdT`b0k=K%DYxlcreTmI%rdn%vLUCB=khEd=i)|m1?xd%vTyC2<-$b#RA`g&iF*ra+*W9=| z(90+Pf}>Kjb%h~%3z5b&&}%l7({iZwEh2Xm47RFgqRC(Yap0#i9y;;%lLpIY zr;!Su+RxnLD^rtOT`r@8eDB!O+dt{Dq-o5Uh5L#RhL+Yvo_amR7f>hs*yRHw%j(B38)g~y z*fRZ-#t@fI0g4Kck3B{;X8>$oALPOuRxbDa_J_ZA^>2QhD{$RZeD-T}{+IaP zUkSn@Z+~Lo`YSXr=H_2Hw5!p7+2!SI{f#+!hl%8zO0l|<22VY>lChB)h*cGsNFJ*= zdw23ntS(rGp4=WwUX#O61`fma>JN#zyVLd^i0!_n*JQ8dodqdZ=cq)GHT758dQL!q zz9?O_TbHF3uAXmKfQ_EKbzoiNDHI~jw_NiMoE`j04sg{KV1e|xoQc~rhxCjtuTg(% zcu!py`Strf(}MvopS@(2!>!_l0plqhwY=#{vs@Ju|J6VGb(I$)dm&O$sTu2W#JkRA z@Zq+PEWb&5+ z{BmB!!_D62SQfdx5?BJylcZre1xrcjL64g8jyaMe{#@v;C4fFs(vH{xb7@LN+$8%? z2vDIVU1s;>GrHK>0!dXR2i~4zuI4=^LS>sf@ISWg97mTJ)tg%8p;6Z+(9sKp^(;GP z=I%jDB{o1k+fRp4xFr|6Gh;oCQ4UYSLDm_jUeJy?qC~mW;o)PR0!(j!ao7$Qm*&rr zh>Uy90(EB@;H?nP8t^z5%TpC2LDDvg5kEN0n)9-YGrkpek{L12In1QYEv&98`&(rD zgL1WA?TdE{v1^p)Seix=M2hXGe+mCA>dsDY28Y0H*Kfn+pTbxCle_)wSrb2BqT0;_ z|7!CPw|t>?q8qMau$&HGY$F)as26r1iF-Cw7#g8z5^|W^cI5cXPS6ZWDP`-f=+6%% z3xzv}(A%uTMU74fym~|RI!hODj3iKgBiFt)4^I!zp~YX?gU?jB`1$-geyte z(`{aKfEVq)a!gg1g41wsp7Ha5ZH3$chX;4I@6vOYC8(aT5{X{}em7^JF zs=sTpf~q>N{0i#MOhK{?HJH%Q(kUh%i!{M*`S|*5X*L=uaBv}clP#bR%~DrW8*v= znf2EsUtOD-&(#ztG%ZTXeU>9?>sXR$ddXk_B8_e%cMctS{biisA~R(#7ttmK9`fcZ zrMk_qpNkuJhtrcS-Y;=CnpC)JSqjk6PD$;ODO(8D4wO+NAd|T^x3sOy6wp4o>waE= zv$0k4kt%POiN8Ze>wTC&#~pNqL>xw-Cvw&cFr&WNR4%zZuFUqbY1@G{4752Z!U*~a zEk2Z6FBaBM8A*92OaBGz^fCu`^SqIx5U}{AT4AxcPfzC7xaW#tFK2OeSM`dK6^|=) zEFa?4=bRK&DZ5HcyqZ!d15iGjGMo4@_3D}(HGXJAfGBzr@y7-7a|;(t0$%je=>K5v zJ)oLe*L`o6x|Ykn5drB-s!|1{HUQ&imwxo!5o4-lF;wK zfh=bfBMt#ti7M1U4WQZhpS)O|N^6tvv0ojqUJAF&5aGz&knt+-h4O&=Vh7np2^>yw zrSBN!dHUT!42%3r6C*Ay%kM+fG}WhsiKbVQ+qK_o7P`E2%RKZ1Eelh(&ZV4#dy^R9 zr{fLCsx)S8er?UpPn}4=nT@mOB(RhTILJpsp_*bJGbB0Az$7`d_c9&w8NO-5pCyQv zj3uUhTiGJ+_mA*G?Hix`gEbl$3OR1avq|q7_%+`^`xI=eetuO1di$R1*V;qNS>oq% z71CPsEb)*(aq;>wl}i%Z&LRBD%PJ@C@xV`*EMEqE7Ye-Hd0g&6YOKfW8Mluj6Yhq?uR61p4Tj zlrF>;fgY-nx1W%lA7qJFK|gh6(|ktGSSb)D=(pLG^}*4qp1Tn*Q(CO1;zgaB2C-nJ zRz^w*uKw|MNl@it@=j1OQ$?2cjcq6-K-PBY6aD--i-JYumJYhME{O&UBat16^Sz&g z9v*#x-SN(usg3I+p)d08JXi8cy(ncl-BB2rR=9r_@AICKSZnXZBkoUfN_A*-ydf;{ zx)XkG+3@`&^OqgRfebjiptky&N~&C;nt)#Muz4p)a+gc|8T)xP7wFg+f@yCvhSF!7 zw%WO^BPb>DY`fGm*}UC?a0Bo;F@72+0Ov}kAsa?P;ZJY>nylEiY(+y^4?8Q#DvV#N zRi!0LG(>VW-{yXqrE>PRMBC6QrZY)KsbKeQP7|b6VJ~CMTr_@$P(0S>>meEV`N*Xr zoYH+OQ`?%{7;kW*4mI6h(6s!*7r-1!W+;ss!!*?EVp^C=?FG%DMtNiVbd3jopk#us zpT{}mh>(Sq9E-~K>YBVWqS#0DF&X_4PYE{TE^FJDcUfdRz!;|!(Xq0;btEK^0oWS_ z&9A)iV7vQXT5lAtKk$Hu3tzcbo%wQBUxQd8`hbu)S0ALFw`cq}hW}sN5dZvg+GL1j zG!vDz6ov6ilh<(DmC9iG;#t@a9%Ml>TXpZAP&BKF{kjC}$EANR1eDtGkOCT&Dz z984i33wk4`WCpbAC&SL&r1M7-Ur_EXPdFzse)c!cF7Z!kt#VwX5}ISt=u5 zgNyPcr&BV)hG!(PgO<-9cb>G+6d3es+GG|6iumo15C5Uk|1EfpA2d^Mh5ieT6})PH z?Muu3933bQz-dIV30_=fV~Y)?4X?~QaO`S8n~mm{K0Z$7`Yufphu;HqPPM{m>+X|~ z#M$YbV`J6p`5H?z4|R>FsmUNLcodAtwJ@+Qm#Fbn-^bn9+N|BV-l%xkj=NX} zwL290nIlylBlI3xo5gZD96XHLk`2EXG2{7-&B$-OOwGe<+rP0ar`dJ0TnnDf9E4|2 zvp7MHhCBQ6X+Xac_}gXxViJwsm;=>&2dAcOC#9OsPM3Ym9+*gufS5GVp+wlW0m1x= zVCh0XKG70rexfWesq9*sGL=ijGwtpGT*XDNHj|{ayC?TXNQ$};$8rYEMax>dpcbXS zkPT|b1AyVdZ)~ZiEkf4c*l5>B6u1cFKYFH9y9yD+#MVzujyIj(wL+sz4^oFCe%B6) zs;+Oc)^dyqCY!W@!cSB?=RHmjfQ1pzgq9@J4e4Z~+}(3|ufJbf_wA4@t2M{X8p;em zAb;W(jY`vm9LnN%VH%FQ}Q zd(J-_mYtf&Si8|;HJ!-AJ66JPY(i4sI|mtFE<1p(<9e5Hi}A%rU#XCff9Riyl%>2q zY&ACsSQ;j-`wU6k<}J?$aj9`R%mghI7boP<`u|t`FMS&UzN%{%cufU5-+9*Mw*BGY zV|(UKxdyD2bY4SEojSd#?qY)B$LTs&qZ62Cm_MB2n7HxBd_rH&_->2}qX7%$f1LcJ z?vo4DYokTUR|Ct5+0V^pN_4v0>9h4NZA;#uxeDW1Ei>)Opt-me&VU=AS%2A;+Z~IU zX)CG;m0*QUL>=cDY77!NG~%0MTm9=5yEgb(>JLMk2KF`MCd9I1Sf}M&$v3v*-Jd5h zhb=yLDBjjul!kN&*R4-NgA#5CNFyg-^j zBRqH=dFf(!@K!1F#mKeEjJ8Fk!DdXUgg>bb0IC))GP0;fQTzEh7k2sXt#Q0rsnf|O zJu`u{O>LEKp?hn@`&w@DO+BUo7FBKI=F-?@-};3h)7v?Hj`{o%fWsFw|7+;VAB78c zvc#k(d`4oH5K%LE-b7fQM8H5%0BVH@Fy2y+nypTJsG&r6i8T1>V_j_@Od-Dv6Q#^> zRW7UyF>uivK$pv|rNz#!jw8SS`u%1HrXN8sncPPXPS4kO{w0bEkb=6$b5akbC@WhF zta-NM&)DK0q&GyxM$JbH?Gxf7-A1h7i~;ClDh;`rSraaNtfv)THc zaeWoFVtduJ8%}E#o*8@~u{4bht#xSm`vUz*A9=x?fYPe=WH~WT5H{ovhf$B(y@O@f z?cmDbGi0p_XF?j$R=+jB?4FQ7e{Z~3U%L78O%%sMfzm|Tyhtjt$VpvI)baJ#v&XO> z4`P}QRGJ{Fxu!#}htx!jp6HEIp*gm%MusG*_S+7_Mx-?uqQ}OG8K^J%+zl}82^DI} zeiFMTB&#hslo_%%LgwplTBtEG+I-d8L}C9ybp3Nfe=d{#UY(?2(*QcS%pP8TDUC!bllj&+80CQJA-MayaiziWX&wau)-#Z6Bbjai(SjX+dglL>#{@49V-SBsE_DALeHdSG;sLwqsu z`uFehSV-g>xyu3Ca`ENhi-!gdpC}bxp#+3O$NP%GWd3jtv}E8TT!{w=VYZwB)hDrI zEhA!6J#)QvE**@|*JlS`mHeZDGc@BEy8_@vE&m*926#~i0_GlJ?B~84bN{sCjK9S! zSF@VcCke*VK^~inJyB0SN0}`_O7CxDhGWrVJ=UmfK0eQ6S+Z#;Q4KeU({yQ z{cmj8=STiaL9gL~lHP?qabKv_tF%gi5%(4=BrhTmYur>U#ms}TcVK?LKTuRZy#?|| z6FmXZIrqmiQS-mF?lR{7rg}I6>12sq6Ffr^spuC7k<;L$`hl_&uH}9xa|}ZOZDhxT z*dkJQcvHfJtY&SMou^w30I#W3M&k}5(uKDH`Se(kUO+m4Wdfbc9F!5t2siW^@=oGR z!sn1Hl$c9oFdiwjw_PwT@P1<4E6*e5sBfYGrl28X7ED*lBIA(WBGx z+yoW1o_fdkuuDd~$~nvD8`~>#$$Vho-NcHhIY!7Ubnfn7#`8@Wp1M7x!OURTwb_++ z-zizR`q98W2h#chsOk$wE*P%!tG`LfBuC~o;*j=nq~d8$Zd{7yqNWKT@Jb_ zCkCGlLs6~hCKyNbVUsS%cXzL7^d|AF$lJg>%cl}Br{KOh5^0*f8?zx=rwW(m>%>$s z8k)WQDYHrGSJ~u^zdsU>7&dX<9oUU|6_ojW{SEPD)AAVv;J`L;x%>n_3bx5e5fVvN zFDeAVEpP!9b%d@1(~pDI{^1$KFO4-L$d~tv0-3m5W%~J2RD^(Z7euXu!As(_z};jo zJjB!990~zmLyOK`x7JRvrl|+xS1e~4+;qFHn|I%&;WBd_hXV$5#!aKaEy(_M{S{EZ z12W$RT?UiPE9eQ%dmVA&86#Qz){Y}bcAtOqmR4Y|v#b zRBaajx*9gB8qlf)=|UtN;*0ct!g;IfOZfs`HqzLv`iZt{y}VWMA;pubn&k{r(vI-8p}Ut!F*VQ1U6DW3fTx$4x~}#lv@P*ucjdOiqZ4fJ7dk{o@!+uM%<009&sj9m0iaq?*!g45 z_N(ejVEK|20)N`|U!HNFoCnGH)m2y8QTiuaoUsp!j$zl$xy^z^6CJjR4E;|l0X>}# zbUl#eLc+tYod+MB@u54baB^?ite>s;?fWJ8Kaw#k<5xSWOq6_#l)XPi-Jmt)%)#RK@99r%~-ggTpv|{j=?~j4-xZ8H)Bk9Y{hGz+zm16a$e{59G$ z5~R#X3v_j>N!Gp5;3bBL`=G~kh=zP?$EyfQvV>LF?dMhE3TXXS%VKN^x~X zcs^-wIB6J-Nt%{B7c@R;;V^w+z+>_qKfI*M{-bCJ*kCM+V12sm8ApLP_89Y?6A{$D zK;+c0U=GV>5dw~%vbWFo{N!bosL<^_=p}k_bn4!|d`81Zk1L+!;SukwUkAq{jn3Sf z&NS(-0WEZ4J>B>pb?5xXJQF2fGqLe%N%puLrF^Mc+J$Qn>wYiNPngnGX>L-QY=b$P zQ{06Tg^5H=A1z*W;Ct^KnNy{o22tPH@-~$!vaoi02EaR8tv3uMZl#nK6GWCRmpw#s zsbveOs~5*l@HQWnF!6Rwt|}Fm>e~tR-^o<*G&t)%*dmOxu{~cP8156OG?8fe_*RyP zRcuKw&fbU_D;Uz?wH@jy7S|&&UbZyYAuDy|mJWtkL(bog6XO@h*AQGM-3(HG4Y-pOkn7qu+Yoq&0cpW{$RH zH9E=|pT;c`7V6&UnQ`ICnUrMIt6ZOH9pztTlse$*1xW}Y+W1Y9WKPP3 zgZfLWi3zCDcXp0-P{kDItph=AK>yteu9Q?BebEjnX7m@1oU-pQ+}n-qb2NhS$6}8> zQF-PC>hQNFwT;w`#CxS~zoeNBZqu`Zc}?>9Y|mS63ExO9NY;^Sv2>JYIFy0>l9E1# z2>p)L0LO%kniER7sAY8Vks5%5HHCgPqVSD^lc>!Zz;+TDoM_w>#o)$NpDZ#b#WR1^L zFFyLDmNK=D3mFp9VLON&sob45?lR%bztDjsiL5UKmxqLi-tS|c)6>20cxA__f8M8b zaB57s=x%;j#r->Xx{Isp%a@a?sP^GWmXZ5RP*T_W!Fb5Xz~{S|9r-U2zV!D8xZiv- zw1W1=Y%M-QrSBNxJ*$@zcw2|=bd(MdJE6QSqxX7)cv}G_nS)n|7Yh>h^iz1Shtx z+#R^~JbE$GKED43eVgm_+Z#gW`aq^r{V2r(;|xy~v78^AL5m#OeY!fE?@3FS6LVVU zd4&?!8+v>1{C#p$!^;S=DhZ8BnHROo+ioHom>f!Hn+awOY$vu(X$G zdORM>k5_w-?a!-RFeW=yx{?V)DS1^rI$ntR;pVFHhtFpHn8c&#%TBCj4&RA^i|J5gmdqO7rxm8o?dsPwc(hwWvA zJEwxZg3y}+H1^?D}0`R}@nM+&Q`?a~M1oh;IB%@K6?_bfQ3agbz9 z?ui}WDPGRIt&o9tq4o(LgY+$JmA?#c-n*R`FI&8L4hqe{G6KD=dHYN@LyD>o2@d>` z>Pn6c!{1va-jEofLDgbv+|1q7H;!~QMJeglw+b&)feY_kx71TbsaMdyn%_MLNNt-JYwc0r` zG^|GBYHy*bkUinDW%T>gAaK*^`^^%d1rocKU@FM*;Pr!VY;n0~X(btjsXaRh&&9D% za}zzkUd3Q3k1*UAXmUFp`yyv$fAX>o?%A5yHNEg87+HUl;cbys9hJ7B8J03y&2%4D zJ~LSc;d{Tx+aKAi%)66iW<(CUR&3rxh$irBXcT59(3^ zo>d;^{i?=W*7V|o+sdMuvn$5-sDG9Zkd%VEp?2OKlW#qvwX!Y}kQz5oRePP^`gN$y zP=9lj5@AhSAzO`P>f@?%=33v9!882J_I6i(xVo0rK$xr(sfR@$sL>~klGLY$K7?b> zN-4gc4vp@DT&XR~^3dw04l)((eYD@I^IdJ5?VinFY&U6K-DPhXAXCUW%M`zds`0Zw ztq1KP{rdRzv}|_-8oY;H$I_;cx%!iK6cAVrPVp_%T*-0l1#FsfbMy6;pdMl6vZJe` zZNtBivZm~s=LvT+QjATS1h*3K2`MsKxn_lMs|1HYMUjl7Q+m%j6Y!S;{Rne-T!{5U zRSL3B-j`d+zCq>@bxAgOMFuibs{RD;+d8wlH5rG@7x2TOanO-j1->)-Ar1Te7^JDe zA75F9J>@n`vGIw{lu(@VZTHMEOj^rVEl03%K_K6m0m>;)=p<t%qxl- zN)TD>`%KU($&@;`SbgffMR2Y>)^~F~!wz2B)4#4}SucuNQ2mOX$>;OA_P7FsuNP-g z;Z|i1i)?=4It?)r)Y)2J($f*~hSpZ(3TOG2^y24xQ~5$pO?>nqx9K!j{c&>`s?5tL z>2tDte9`Zr)|0bq+{YJT(KjJQk2@%*X4@O=g#!wzL=ywXMUj$~267=Qwdo8AM}r&X ze|s1Ri4mEIsL@|WszumUE-c-CHN~y4lJ&69YL6J0M70Phe6^ou8w)%c-;vD5gSj>r zBHYCsxV4WEau1mM&L2#|)h5)?-N)tk#`hy9FlBvuCUZpG3)Ng zz0L2qu_{fA1AjjVy;o#wZVNT3$6}ZS^$s&(c7uUDyp4`E2m|MAy6IOS?omX>tfT@4 zL;a4iD@hTMIS%qW!5@aczDMX?b`p1()W}B~i$DD^UdH*|mFVvzyB{!o$nX379cA|e zxbOeddx#i~Y&jTy1p1-2v@qKNEr}IwXkyWz|=8N9{V~v&|>yP;zpP97s&=6?vKJsFiAqEr9%Q!>ty)rLBgrww1(vV z^3SEgYrzE=Nl7>Z3d9{&0c2{ziln5Zkaf%P*zwa#( zOEpY(B42%Zwjxa;MmDrfAu(4YmLQ-gU*mOwfbe;;JWdie;N#7SE*gcWvu@j<{t75V zM{rIEUik2!tFn4ea>UxRcz}+>z@)oPNZ-NjDSR*-Kps#>?^!nPz`lk@ox2O3p1!Ri zYa+f!4s^gi1GHL5p~D3^@=8P-DuS+SG`|vJTYR(2x9vOu6IwgzhVoB{@bW?YAAjmtd=tKjqTpSs2X7DB^^mkbl$BW zj8ZZAn^g3#_v^p9-S>?J-`ZZec|4w&)n-aJrwk-hGvIcNmV6HY-rfgth>ZuFlA<1N ze-OGHnhskFdbl=_y(U!e$3?wqGAw15aJR{j&(ZTl>1bx-#deN9&oryLn5lwfNZ2jx ztx~m?Mil2bBo6!^XrF(3s{BiD|EEUPzgOme;$Q#wYx;*u|37UGI6M}8U6k8+MEb@C zD}QGBXT-=q3OWL!uegxA0}Fi){iHq-PB#%7l1|8d>?h9JOqkUz1@_wp9(vt!vKfz-55?W>UPeZan9e8 z$TjImSZSZmBiL;1leqKt57o1wtoh?7?N*l?a|A*?SdsjrhybB-JagjFJ?F8YIX9E% z5H*WDBLs6`Y$2jn`};WgPZQlpH8LIJq-n5*AidcTCOK99eV9 z_)bAS)wZ!J3NN(8vBQ~85tHmMyFN>@8yuN4Xi4B>FJYwP?TX{5rr57eN;0O7J%rNW zH|WU;ij$wx5V-KZZ4a`!Z!R|4?yu)tEPUF)wPiZA?d_X)mY-Z4tJ>b#6Oy*P%<}Fj z#M1+>h(?UsQaMJ8^Z?folZ1n<^hu4WanRnemD1Cce;)Q`g-u8C2$eK!NgNt|!Tq4- zfVBh0uq)`pBO(1iTMAw{|6i{ANutruQQV4tCLm;d*YsR=f{k}0);#gu!r6e*o6|&l z;gR&uv9TnL$XW8UyML|!fBquoAQAbiSJW+~`tHE^63I1<)tk`0-?1!s0q8pK53~}M z-hUcA&7V3QeXeCv9my&@vkCZq3}Vwgshb@Z8@17`2_KUidn;NQg8YmwdxLfVShi=GVty2@5F zl6jJwcYGVcMR*rQnqRF`^^DJTiU$uSxLlon-oopP=%(vqDL*ly$g`Da>w(JwBMet~ z%9lPhSk&2^-<&4-RRkfEx2>Y@Jf#&Yygkx<1oAmcER*1LGRb8eP}7Rv(j|C(_gDXe7yV zs*ccPMf*j=vt`}21ajgPQkddg)CqR6O#&_JH%Qi(>n+aBQ9%p-FKO0YoI#@*?{^Y} zM|k#!T^$;Wt8GIvIE{nu`zX`}7X`VEMK?LhlcNHqXi1(qbp6aI!hROU6IHQhvbxl# z7J>I;=C^pN6eN>DTW(%zCoM4Uw{6_y%|osu$ye?=%J5V2`!p)Ym<7;n8yow>_T#p9 zTXidX8#|HD(xsbGFtRFWbj9~g#AU#PPQK2qZJ7J&-Q}h5FnnJJbdJt&l1Qt=!W25) zQ5WJEpoo*$6_5Qc=-}qUfG|$LdUc>PI80&5mW%Mt>WR}J!EVp_D)&{+)#@4OVviw@ zXg{VsHI6z{B9|)QvQ7tlwxN)Y$+b^m#s(+8D$7VELow*llz<_HzVP|4R_oVfr@d^j zu5w}wOW$-2D7fW0+@2$)ioiR4?q+k;)%$ffF$-|nq4XympS%c)jHU)Bp>nPTYNA`) zyPF~-GjeY@#`T$M>>%~D*%cdOxJ4j@aZ!H>_sFa2H1SvSjW_cH9k(>9UJn)O|F+N#?O@$1ox#v5*%-x92Ro2V~rw3!K*OdA+Y8*Jyp zQ<#noI7tZ|M?z|&cRxOjsFX$HLHBR%_{qHOqUaBaMSO3W%aaSOcxRgD4(Bg6{hQ!0x$wJ!JE`dNHi6*Df> zi8U3|=&yuGQxEc*;v6!nx_Y(OrNvrvEpHIxbi%A7tDUI~TEh(Bt}j6Re|buooQzz4 z=(&N2+o>!wx?tOhgz3MN2w@aYZ>m(`9IHcT={Xvh@}|O}aY9Qa&y3BQlVj?mD$!IC zulVKSf!BBjLD@-YA_x8 z32h`2Ojr_E=aOi=I$l6aX%ycZTkb?J z-j-Lgk+XX>Zd^KUWrvt^qWushhZJZOw$3K({*XGcy@wqH6NXugZ*HDN9m5QrLq(bZ zoGP6j!ItpF^`LKTw@X;D-y!2Kb_eK-taOV*_NADvdI>;Sl{@!+gzW#uJ^ti}JekBR zD542jEmFJB!;%{CncRR!7|sv&2t?HW>@{AJO?B=pIuiDxr9`-SwsDS*6o%TMk@ZE! zq3*OW9r%nuH*}gMd*`Tg?)BbeFLmdsS_a1@d~heN(NwM;jeEHg+&xAF{c-js+fS`5 zb)CHxYA{KU%*e@ThC9qgu#b}nvdW#O;dZ$}{6poYCgcr_lEs(U66K+4c&ci9LNLqa z^UjLVq-CRDvk7`?=NsE4nJl%qx85OpmCjpY_lIOc-(>V)Y<4y1j=>0%1X`GYfn|G@ zbVnsVu7-9fKIPx)^wMAaM!vbO(FRi~d=M#~!Jf1*mR#S%NG~%{y%f*YAJ3&Au5@v5 zxKIDPHJC7bR6XK@+HvomPzl6(9S1%%;N#adxL|h`)T58|Xz8|f2mgy!Sv{txTP^WC z1X>uF%-n86YM|B3AiwusdGiF+BGnM<>L7B3Z z6CQ2v+PPn$o+BV_9`b)IVKtua1SBbj1lymACkO<{APtnMOJH7-^^9jKXJ^2albQ`W z7L9^Q?nSIUpOEGI#7J3J7KsnywFRY82Bme%DE@G@b%xEj1S;>3Z(viH!DWN#0{>^G?d@w}Cs? z&vARem2aTJBq0^A8X!c`2RgX$Tk}@a`f>p^Rb$NC2a?AXscWaYd9)wY3|p2pVg8^u z&m~g2Ua86_7Q_$IGhEZA{7q^|?f0HLOY!CF<1aEu^eN{V2k@Y#qT$OiHE_;|kd&X` zibub2r8qX&$)zX}*_osADNdN6b=4?Y3h4repTDN(PKAp%ePx=DORh&xyj$bq@UI+N z1a-T}PI=H>ZwW4JD5bQ}k)uh_i4Z*pss|nxXsMYkH&Oq%dLs#Qj}W0@-l>=Rv4TCN zvx>uP-RnXss_eFcJUUGTSx+{Q%A_;*9tk)5cPm=8{Z1Gd|N2BQJHs&%`X%Ezn)DJh zf3&QW;nBU2D~Fva3Q4C|5ugsFW-PR^O3Q8Y0DC6tem-JoXN24Ir>fx;_6zUYD~1N4 z6#XrPMV?~cmW*6y zI#|hEY{dXmQ?PM#k^&@CE48jij9Y$*(V^6WyN&ptT5*A!h6_QxAUpUag4hUqGkyK- z?FT`O08VqA+uXI&UpD98=5!3_@ET&B-*lLY6zi6y9Y`aPHvSk`jjO`t%pvYkB@rj# zJSBCL$~vK^Wdek>PG74ri7!RtP8FsSZdW^k)dKJud4utgJ0lCxwInWmoWGpE6cYBR z+b%a})+5VXR(_D!wiWZ%Em8TRg5}lC`PSn@(oHqN?)hdw+}VJds4udQS}8ZUMRlm< zj7WK*%<@2HChpbfY2Rvf8$bJ@Bf=9bkTjrmTmPiehzl;EpwhL427Cgrm_ru|)cQGe zkN;ffj#L=!&Yg3S4fMPi9-n1bi#BkDfVI6eXj}w7yxkz01h+yLQ?$Bt_54fr$3sV+ ztiy~|BaM6Bmu&HKWV!MVWd}LpH#xO_-L3@h`bF+qpaVD6EEtbEl)|n2J=Wwd`S5fm zaU`|gtF}oTiAJV6IV(;(SG+14!OI`E&CS@nw;QCDd4vI2_S*y2L)+d{W}U7o2^wcj zA5`Hsg26Kq9PUUZe2u`#!!$@jyQduj*3z;Mg2A8q!;7@KmPjejRDJz})%HU;+=mDa zy}>#{+XlzM-6fJlQrTlU`Gg_fs-8gOcN3V&E<>Z0U7rel9q z>lT8UnXLwIqZf8=R>Bi$F)Ek;r6|ehi>iTLd}0x0_-#g#>xt!V^EdH@Hace74dP|# zP-l0BfXMj{2aiCA3XsQZLO+^Or48zuevnxtvY4IpOGhV%O@mv}V7kz0r6pRo%Wvr9 zg3G8EyOtuY@>V2AJ{ft3Y5x$}knq{FhV21jISRdM+G+JS9PUsSga9Wgj0+WGh-rV3 zcCjCfu~!&^nw5owcv5+b>3U9^5hl`8yceY_aR!M&@q=FLEkwIO)%YcBg!AHz^qPo< z3xg5jKzATPaOC^@a?fPX4oZ1mydOFaZeHBmcG=BX4SE2wHW1&USm*Uy#a%@BE2Y&U zL7Zkzcljsa*=kQ-1Rd5Jn%rytI@z;@MN>(Mb~2Egl`_%@w6Vhg{8gTF&~QcRuhb23 z4}8m5Y^c-P@z$D1sar8doi5UM3GuY3hi$#Ca(sfaD)bCJvG~q}^MymD>Zk66yPIpb z^b(vIOndbWxsTyja+~k261y(-DqK8-;s$Nf9)O&m@C_7MmQmzw&r9aX>E}sU>M?uH z3A#NDm2;}j;Aftm+vU6SM(~O_0oRv0e+RMb5(tq80S-~@SSC{IL=PKXUGq&BEX_$! zr9XJNRztamyi(EqBH!YK_>Izw?MOnZ=UaWFEwiMJeh5=4r(eH>&x`~@`F-F^>u%9h zDq+}WUlZx1uPrApe9qy)vamw33e3fDeoY@2PL9@Lt@zh*nw)t@*>E&_w)q2U6$= z{~wKw`UF6q$&USQt`7K+9e>Am1T<;;b&-AJQC<`b?_!y>Q;LtwJ>OTAK#6!!`^#E^ zp-oZtAjwS9`(l%G1Y=W?XvP6(<_6H&@-Nq(OaU5WM1h~9%v!&}^9Sc$(mZ&vQcG`% zLz5mgb>^-@3>n=Dk&E;v1qyk)CQeMr*AWK)?;E5)N;IG2%7xSqP*GnNqVn$6iHiH! z-wtb8x)|)la8~1UDc?hskDNG(r6^g?D6#Vw&kzA)NOd6cjN-gO2LgdV(x&c##B7U3 z1e(6F^&IB2DzBdW!RO#l@sOV+SDRLEI_~{m9tp4c4m*&0%nQJ3&!lXOe*RnDv377& z%_&lM_Vh_!I}3F0z`W8zqoYBtTw(h+Hd7BJoprvI{Vbt_qd>%PsyK@JYPO!+G=n9q zBBy~P5_lDH2jgNWokNKmQA9j|=+zi!smB~zCw?sSvWTWVQ=a=u&5PneXXJ)g_s2Ct z;zm;;QWO1UI={XxP6|{{d7+j7fC{M4api^35}&+Mc0<~(V4nkO^tqm;uVYQg=fTym z4(;|h4C;gP-TI_j-rGON3ej45>w_-LdaInmc;`s&nrq*r{Lx}hnYwQfk9&bAmw%Z} z3G0SB&Y7|}sjc7Gw4$CF*FQOnyg%Z?52p^Q!=rET>lD!neFoF>bynciG3F=)V8L+b zm0Eqe(2bU)5}?mV}%ExXc(_R)38dG8zWveUtv#Gcm0 zKg{F}wVN<89ANm^Mq`vZ2uqB65*fr(>>|mUPuHqw__@BheuCBs<_VnoTKzL`N%C#u z&zs@VAwk~O*)O2}!LG*sgm{ef0xrPj`8?I81i+e~>D8TzgbzPpE_h^@aPb*;&ML6b zPypZ>*|SuyAvnBZ8YT+Hq{52yK=Ui9)uEm@Cs;xwewmSC_)g`s>mwwUT7vu7%|nG- z#<{qIUtm}XhvPho0i5-u{;VeE+us6_>|q#FB`uO}$6+OjPHj$AI)XaX%rT|>@= zpOBC`s+gCD={JCQf;hVM^taX8hp9}XgdX-NxLgcNe8ibjuTpwzX6ou#)z|n+eX3Qu zz&PKC{aS;Ft%%JIGyvWMLZfU-sIVS3j^*x0)%D@&a=v#zj%)>cz_>~s3RA00`hu^B zd@23hc^VOZ4k3!v$OEJEQw3-VpSP6+FHG5Ub)Da?!nOb02`e)xl(KM^9?Yt^iQ6pR}RzKP7q2MR()K_R;@q4Q>9K3_ckJJ9XFpZ6%J=SZsWR?2x|WHm#ty)3J!r8M z1=yMn#!LfADqElifQ#8OeFXV;QbI{lG8rG~cU$S@YqQeMei`k9`;nzCm*<$}9vkv& zO4NIa67uoivzE>5(325zk64%R)e0e#Sa7d9kS`CjlH{pF=&pa+ss)RndQ7Nz2hjUI zD;G!w4d`KB>UnqqwQ|-@@5R_XIuBvQkDiA&n!-TZ?Vbo|p}jpNt$Y&5!r+ z1B~R?Y@)W1m@_(ZE(s5vt21{s(@r)w@!D00$Wa^B$}QvY>UwcWkdI)M_K~qUAf}5W zT9!Th_owf1p;gDHvYex#@Q1sZ(d3${-@a}hx{3Wld1Q%k>Oj0XLQETsn;v$KV+hSC zIh2At9mb08p2wXS_n@3P7+)OEdi~v(>Khv;aBE0?V`Iev7?y8rO?LpB+HZjVBJVd~ zbnVxEMXh!_>d3n}TV)-?sS&e+SJ{~R3foX6p*Pn47gE6lk8+7Z)LN$~663g^tJO$zcdpY3}D(tob zMIiJEB`jxM-6JkbkoNH_%a}P9$M)|(%-{I#yv4cBY=e93={!Ar@#FmWq41_~(t6G? zIcY-2S)R9w&7Ujdc@{e^G-X7JibuA%I78osx@}bi(<+zhF=+ID$&wKxXFUYS!qB}j z_%D|%_`^}Ye|LCCx$vS&T>g&f(D5JUzq1T~sQUln$N29)m}q73MiJkK2oB3(!t$(DlaEb~xpW%0iiy;w zOcRw)^E`o{k|!w)DEBvk*Sg|_IR%QsT;1*`mO!X-*UgvA=U}3K^aSzq8*2um!c~1= zP@hYgcS$c_kDRGX%u>i2ow&iR7?HMwK67{f?)%p<$P! za0otr1~2BM49-&87~v@8C>Gy(V`yRc(P&3%0@GrLlN${{T7gN|(o*5>zu%Dm@%;qkS@%P% z8_+;t4arxogZGoBBDA-Ad@5F~5o8lZp3?~L=UC>J(dO&LIct{`0%AXI0KKUTGQIsQ zaB=TAheu|j{o$@=r0j)OoqRKapyicvx<0X`&pKB)Pb51wJ~qA>b$#^ue?0Kt|8wF; z$1-&C4~Ur6{}d|Oo@M8FH>t46hTcf0_V-e=0t)ZxkZIj@Rc{J@ob2w&Pp zuEX@LS6hqoP%dy))n_I%$g_VoJ}S{f@*vYWlJuS6i|xMfjg3DWw0o`P5T@l5TC_u- z#I?RPSOnV5sW94_=~`=(V~TOoK>q)cJ^!SOJpQN3dG6_OgeHH!TKT}r$7{+OGu+`) z;Tx2rLY^(yJ^d`9SBaC@Tm*Hg5V1n3=d%~1r}g}w^_|E4s8{;&^SlPwY(EN}4E}Vm z?{d%o1A7k2UD&D***2RjO>!&hBfl%Zb}+s_uCvrQ=6I+Lq`%Jrr-+=|x;w9Yac4hu zD}JxJ$EBh*A;L4KgyME7uFIem4Dz%aGD8^JThGtPC+X=Sd6~*D{(8y(S@R&~z#DlM z7^+g`8)srJ|7;0t0%W-TmqwbvPu<_A|4p_kKzT-L$3LaV2wD)tmRF*;vQflTu&v>p z-*WU=+=kqqwE9_)l6}+%p(j|^Dc{LZ4u_3bq}GKZQ+HI~oZ*0<`xIKI5##zhpJExL$ z+6ARp=~f2~sX#`n9edAO+D#t%gG0k_Z07HPG4Kz6|H%K@%y}F~{V`>o0^af28o)F! z6t!~+fB0KJ@Lc^O`TESMn>#n@^|LUcEXl0-H}|7{4NpH7~RdGFMw8z0vY!Is5&aZ*zLRTo&%IqcMhiT zk3j!cq!L4~KE26ryZhz9WH`|%)U)z3K9?L6eVwTv#|rYl^zNZ*y8b|wbo55U4vS}q z;AIi<*n{LfyQ=5{&8c0obeN$Cpcj(Wx(qP1S>de^OOQiRLP-Da@dj!2s$=*0(i;46 zSMF)upY;8dz;|_*+9uua^N|l$nFmX_1ar;TYPaCwu=s+|;4toeXLFVavFP_CS4t)n zZi$6x*$uCeA-9l(>3i4CUS-X%&%UvP}k} zmyC5-@>}YnqJ4fTGh1rZZ{H$pJHL)_HW}g(Dd*zOuh~{(?^yCD z+1Kunhul5FDGk=)F_Md#VW&L3ddfVV3I)*Kp!nQdsmZrF;3y_{Jwxt|35L!8g4zi+ zT=-iSuDu!SmgyG zGI|IOjg|n)^K!yUo*YZ}=+mqGCFxNTLkZv+`vfQY+jYjrM?Oex@0AiRa;vvW>C1?j zfw)p`M_*hikY*zwke<@%|O#gxfP6Ve>zjF>4+Rx=~7 zOT6x)oR(K(#EZ*+(I77L=SiiHE!2N{?N@ltp?rC>%2uhi#kb~iA!1r!Nqx+9Xq?fDa8h&XI zCbnU%WD2Bj4HTu%Xt8sk^R_3Rx}qMr(hs*h)pnmXzZF+=*aCmF?X-3n99dCvj&dSd zd|8o{t*^VTc=vC2wU~n#`7B;Yl~;m^;qu)VFDdo8CerK;W9i6&`Pglvr3fREH=R^M z3=}nxa8W3!*I_KtBTrINuHdug?Qj7n?FZLPSHRN$7HJT35Qu6Wt)D&Wez5pmANh|Z zRiKG?1XL4&r#T)94eKmI0n6r5?LmedD`{D13;*y(`Q$&IH^3Y~viV08?+XAcln?*Q z0PBTUZVuybkCC4?;FPgc?W3>qhB8hT-idzhPdL;%B^^02Kd=)8tKT(p{8L1aw39J$ zS*<=%`ku$OK{iE_*4VKE@6z3DoZd&k;Hj*immP_pcSk9jO>Ja>3LY;o89!CpsR(W9 ztpy2D*FuDa^Y$!?0!S+p>dlB+@O+3GII%*$-VDQIDr#(O>g@7`4l|-^yc(5JH`-FH z8^EeHhRukJYq)90d^u~jy3%Y{n}W?6UaQP+=}&689q3ha!a0EjrRs#|0G)?^A6WDo z-2KdD83>qSV8HV`>cyvSA{7R76c@hR#FO_*8fz%~`6F6BE!!+D%D4wiK#b3%Yztr_ z8i)Y4R_`HOrb0k=2UsuLc&t`(kALyy!5lKGV-B>~d$7=_WE?zOb(S!61@4Eg)aE%%U#Oz+T1>&V6`-3?;2zT zxasVs&mu|uC(_>gY-Eg1OwgCx8(?ZTOp>t_W?tIySix?!hZrXBr zW3{V;Xg4{t{2rAlamvTN@A*6;$LH6PT#-&AR5qeTe)DyBzx4lL@4dsC%J#i+XLNL| zj3UxODWX&X1JW(@DkX%PC{+`Rgq~1F9XbR7LoZ4vA&CJ(3jvkhMIh9GKZJ0Bq=o3%;3xR>V96CasBnW_>fqm7ZeKqE4klM;jVth}?{@$MV#0l1}7d<|_BPIpTI;G%? z1oL*_;D_^2kd_t9yWhBf*3%d$kjdVCGCdsAmtqq!+9%Bgi5L?b%(`~crQ57W{@!Bl z&_yFB5--y(%vS}!>=GGar%J4{zNu=Z#I`SUkh+ts6)YmlD9=ByQ2(1YXJ8zd<}<|YtJ#q zH*xzXRQ~(u|Gp>x!Z-`O%8XofU&4Re2%1iWdyA}v z{YcwtL_TFSP0K}_#iViKOwo}ER^GWGoiAqcc1U zYpvXIOGs(9o=&(FQ=7s0y|FdMy-2QWQ5ec7FYp5YfpjEYOkFESzgh z>}zDMKTN#Usp70hoDqcIzswoQ)yBFX2%IHe1{Jv2(#96E0azd29?gfU&v&Ri)D0*KU#EC;((O1u|Y@R*o3TY{(8w61FMGSVI9u8T~(N2qJLP)$n zE?Dv71{zaxN8p`F-pToi3tB?ZZknS0tP)14TElQGGXGPhht~D^8Z2s>ak_pXF%A&E z#e*ljB}?ASng8^3qc64W%L5q=A;VN3v1`Jx6sh-CIK%h&=|elCkUWTO`<%t-&Hd%7 zVc@6_%-+=oz+GogJv3DldZS=_s{fD_VR9;N1tCksL^{Y5eyQorT|4cGUV3qjy~6m_ zQSq`KGHt@)11M6#QdRERoNkf;Iu;?A>vC>D+SoWyG}-8hA*^{yX2dc78F$d9>ZRUU zc>^s;ls@0X)UT*KtH;CfWhEq-OwA=1)XOUxmdNA=^PFG~_w&{$wR-0}Yr;R1TzzJR zBn|Z?PjbT>Dh<5Fzeb)OK!F|(T>~V_cWgOafGRrsG-asV&?R+nqjFvjEU$+q;3s58 z_&#>nLMxXu^+-9{!`&pirE=kdABlDZ@Msi&(r~RQYX?*N#PjvX-Ij@r*eBhOP=nYC zs{z39(7{l5blb@Mf^4qI_wUkR?|%)2*9HY>{3CLz0EbQ7um2a>9Hf z(x^q^Mu^U9T0%_^4|K=+4C5YMAyIhy1C3?Gj1@(jDuT}+U-u6jvVHAqzyfKje>EUo znd&D#^FWZODkxfkX1-1QL~uN`F8gbIv!58?vDxeFX9GYlc zDP3QDquJ_+FNsj>-GEYD1KAVawG#A$UYLJQ^q#5ui1(48RYlp1tmP;=m6Nw!9vy;C ziy)ZG4R+8_{uyeTQm}Y3=Wt8}_uR&X%^;SlhbV!VT^|Xj?fOV;bo>3*=?`t5PS_=? zEc_!QPN6fuw$OrOQv-IqkJgmBmH`hj~n{Szds;1=YB~F{Cu84jOJrBf;x`ur%f1?HNhM0DW*Mc03 zTZ(#O5hOg5p@S*>O*Z}B>$k<`UXfv+a66aNM&Ey-1X$!WVjJUyitTr^un%^SK=KSwXjcI9W4MJ3 zhxCLAt+-O2mv8CI2-^8=kat~1L1dU)+ae(|q~@mF>jT0U=Zp(WX?%E10abaXG>jQ= zF2z;vb7C(JO1^YXiHmOHwA(h5OV#k%T#6EbG9LDIS-Sd{)?Pa3TofFet+JY4f_gpU z#)BGF3t0UX6HlIb)9UCq1HEfcxC`$mB$0jY1?pUo{A){u!Y5`N7Bxb9c4h>V8qM=@{Z+ z7}-#s?*E)G&c_q2-E}7OjT4FpXb+gS>7NOl|W97>E^7d2$ET#?* z8!qE$+qs$Y#wfminqoWMIdTOHYiY3PnL{x-V~lQiMqsDdOMfmy$D%HBhgFx_ZC^EC zvAD)5LZ_Re_rS%|a>X9{y8;_|M~y3S42QrghSVLw%VxLwYBXNpwZLtzW;^>bb}xpP zxE)8pHid(0vi@VXZ#A4j0=VbHIH*`$4;EO+2(P@56s>m2BE69kv?B;e;R4-k*0W5; zepTxyiV7|OxXjI9f)F z;d0DkP!*0D&ml?^o@fZt%hkpA!$_VrnAQj)rfGU5-hc_uxQTA zbnGsJ_e*coZYgl8&-Etxhe(2^dTuGsD=wOOak1`r9i&qA_70_Mz86ze`KxNstlH?z z1cXfyTwl#xfA;%URp2J2O8da2%`#f0^dW8+U>q$TfHOM2MpOyp1s7(pywf@}T}S(I zg8y9VB~PotYsbN!w&VW9ODFlcNG$`)oWPh3!_D90asHctDvi0eHJ$dtd0Y-xGKwfU+UmJ3S}%wF2y?#aMqyL zs?mqd*KGnT?aI&fi6i3s-inu-y>e@+BOa+Cn1*H5QloHgCRKp7`1cb(E%>{1QlieL z40-D(YHB3iP^Ba#rrP?hwzKDph)Mw&nu66;ivu(=UTyIF9&{R#8@$x^N6Gc4LlT_u zRc+h;zd!z`_vGZmzFkwDe$N-nrfSk3W)}fV*u=q-1bT5FY#0|OR@`Mipp{+4I}OM% zrm>G^3%I}gOLFKRH@8Z<20rudbMaa+3}NXUC@)bv(+bhB;H z6O5`S);Wr&FZw%UmE=t zmbOHbmtyj}MfC0fz3?x2`F)c{8#B$ca*$}=9-#5%U+yj5ANIX?(&1~xCF^$gqoKpx z@Xp>dO zIo3;icZ@04s;Vbe#OZb}ddQH*6~!)@Wn$yPnSx~C0RU6TvO8pqz0cE2*1{HfGj38s zbz6kpStY1Tg#GEy3 z%iC6V6SoR7nHhAah%%gv&wzOqX{_IVtSP&79`3CQiP{w3xehxlNTMzd4U?ed^OG`$ z$eyjIB{43`nmKVOhUTUQT$IE{!(?y{*sa!KE*+yn5#jXZ98)*eQY26+g*hM18dv}#+z2hbrU1FW9YW+rV z=;2iSzd=U-yx4!*`hS)yuj7E@>zPFNjCtW>V>hc)&Pnfe!>%;h(FXZNU_h=dZKCct z?J})Mq;eUEz&-n`?(4h%<(C%*-%{I2`K%;&1R8bNJx(Vl$~YWt`be);d#fK`jhE9q zKhm$1ld`KzFR7&!{QU;~SJIlF;RCJRZh5hUUaj%X84R;|-#XkC zmoQA-^4-x>eVA$+LgH&yYPFd4c1+=kb-{zF1?~v1wabHtw-=T8R*#5>Ayr=*+e!BF z&%H6`qVn95PH*3+B(m3%3zmb6ayA5Yj8Q<(xXVX8FBQfDTDQ1I?a({7Qk`E1ZgS=gD~)r< z53h}SRXAzeB<~x>c2*A}(}YlF5rqR@&?;kN8KCmjdEkgOzm(95+?_Hpr?pKFmekiU zP#!u`)8;i+G`B=7Q5~))I4&ygVt5?68N8_StsBz)DX8YKu7xcvxoL84no1_d1UsIMjkj5^=sdu#M2>y1<|G1+Ip#IH@LIfACNZ7*STvG;wa z!5TKDagMlRopsi>;Ze%h0YB>VPid(BXq=pmal*sxT!VFS`Tni+=!H#B$h5USNvK*@4|(&}-eV4jtX(K7aFn6g09? z1+BJKF_i0e%Wi&M9A#zAHec*LGKNYm)|oMwsVliDPtKNlyj^-M?#ug>eDSdRjLs#g zSga|rcF?W)JiT5bwA@{id_^*epUqMr6C4{;7`tqrYqoiST62VEvfI&X2v;4lCl5uP zxlgx4uD2E&9$M!+bj4@cn{(vFmk~d2q1$$ReC1TP)8uw02#~Su!^BqAwK6**nakK9 z3G!|~;*r9Fyg>bI@ds5WZgOH=m*_rtoy*O!xr%SS`=GHp6y`sh^` z{MuL@0~#sfd}b0^*bL<^sE*hOcOHVs-LH7+fPIQ-4^PG}b6S!*g8~w(F7b_|g^XgR z*F2~#!`@Ch?l-LD{`cj5gOdO*F zyvAnLTfRk>LREOjHqnkDj1=CU1qyDt-gec!c(8B4FsDtaTot8JrRgI6dI^@@mH!YT z_qh|Siz1vqKqL9Vx6&Kn*-{0CeAZ*PWdJ_N5{<_4toL?47)7v_V}cmBNZ4(t_6@@} z+sKj!yit$qg=%3y9=thm8Co>`X*adQ!A}f1+d*KnYd>dMU;U?3Drl^26u1L6)8G*8cwt?tYymw4E3ca`k-tkpw-?5o~EcfID*Izst@|8h+0S<4Wy}t|gT8EUI_8Ov_Q?UUPBz~-=}H^lP5ME0A6uwV=uooElyv1v zOcFMBn9E=#yy;@5rwu8EY#($f-vP*azL$;qgXH`@fQ;{Z*{DC5(f{4?Ph8&r&do`r z(mHwUPnOzbnOco_7b+?qTbnmqzxU+jhv;`T>q+EgUdzU2&MgVWz}T#>W&pWOnjJj$ z0s>viZ*C#W+&;In6%U|GG?a~?3h~m*KrKKgv%&(7jEwwMzsB(Jm%-yVu;9#jHWT2a z@3+wA&DYk4JKC6E?@BQBE&UHmYbU2SRS)l__XP$|YwkjT2&ugc9YjHGMhm8P{+DqB z?l@^qQ3ZV|Vc6y}tq#A}&oIVhbW@@;EEhrnS?hGWW}Xi9O-#d#eD4iLtTLSN?WQP( z%&Or2cA;TomQBm0_v-8?&ROM)bUqY2AYyP0;8`iQN_+2fH}UMGuQ`QTZ5@VG7S_Hs zGlS^ztxdhu#Xz%*@(bIZoH+A$6x_c}ewtlU+ETm7Z-=C+aM*}EmHGZ|O#v-dHpasA zrhpf8AM_Y5IxcZZv*$n@uP$T!Z=?9eex6PV-zb+9{xm|ZBpZKpu%cHzI;+bRfi)NH zdQ;H}gzj5mj;H5$R8}68p9o0l@_ZCdF&9_{VJ0w6&THu(e^~oj z;H=AEt9Zi2DA1!kX`9ev5;eC$4sW)5A;{qG#*L3Ox#| zA2`;dT{8FzP*Le5h#q7~ATjxMHFHec>>G>gi)lPpiuDA=%}c?+&`vRFf|oh0ZQT26 z4>>1wd*3)Xgw6QTlT-2eFDLoI*h^B@vz@`hhBdAs39hN8>+Etv$f3Oy(6+}v~+M$Dg+Ob`#FW%lFjdOmuLK~Wa<`p)>jFeo$oeZFjozyuwDoDm38}SWx>=YW>onO7xceULH%Q;cmZ+> z@cW5eZjo7X9us??f&2` z4DXrq$AYx{Xri1z(R4&ROo{yedm=3)NHdW@)1q0T|W=EP^BOJ zRZ6p*0wNQR!5Nnb-?Dz-MEblN+{;@oFo=xT3VEgORT?~%IsS;7DSg%tgpj{55M|H4 zI45bQvkm=zLhe>i1al_9B2QUKJ`y1>xyeM3Lh>aV_BOf*pM4yMvXpKxzj^)ArAu_% zMxeZt@{ps^6u0Mcd&;8Sn%~k*#afZex?Elu&A11r#5GIgL~6n#R`|Z0j*tZydVU_y z7D#TpcjAY&P3CC$*NJTZ5Yqm`%~O9_;WMSK?6-?OMLLh`_p0Il239^JvpyT)S_jN2 zb^5?*paibBlONR#K&Yh#m93bE9`k%Mz*DH{}gJ0gKMc66YF{2kig7(D~743f>V+fM@Sv`(_bxJ`~*ZCxJ%;&l6r7-q|XN1uX z>l~%r7_1oe=Lsj(dzM_qD03_j(vPL>k|vracP&2ex;MKUgDPG(K3!XNo^B{E_Ii8s zm3iAjkt`^OS}jo8x#=6Uhd?|s>8~*X)x(gdH>x&H$F}$d+)ZwCFL(z{x=y(mHDHhZ zv8oh8oFR3`u@Ymn5NxJZ2!K!cq4oUNekwt`e0a{S;-I<2R&4ZZTAtw=S zypP=35doD9yt*B!wZDyP20ayZCazbyDw^@S!m%Xrg?V{<$G{6pk8!=Mpc~d1U8)}o z8y==~M=0e2flKz)V%D_|HRfxL7xQg+IB*F!Li64GbSz53d@Y*no!%_C0{)bq{)t;MIPVN>ycNumg zYbnqXjep7b{X~o=HlQ2M-?ZD^|04Nk@fm<^g#-E8C2HrJB%6-{4@}uFF0$nk1szH# znN|bvs=^(Wc}8;oj)C)r#AK#a=V`DJ)VRH;8rV(^H0|vIGsZzadx53| z^gxwTwLD9upU=qcFP+Slc!xp&M8IK7ckh}K4^fHxlAPjF)zkI8HWbRL?~0seG(#uf z9fSpqhUx!3_VM%=uF7BCeKm2R>3!RNjDG*Ey_0Wv&6)NFo6|wYKzl6i{QQ zItOTsxbgKbyqOb&_nf1MHDXPbK~kS*WH`m0PWJyYHC-nX&|~HuA@r)6d~EWFd+0of zDf=*1o8Zla+Oi!=c^YlLX4{L8V7hjLRwcbOF_2j!PCc*tumi4S}4#@WacV3Vo3q3l^Q-w%)Oi z9dB${@+R9}Sf#L83(0D(c=He`1pC3-L<$PJ|G4`KE`e!|3UZ{h@Cc)usae88NuJgD z2AEMd#6V(`oYNpwQTH0s9%gPXqdmUTdOqW;CM63q5j=MIy0In-XBj)%Hs_7C#1ztZ zMn^cB2Az|7X@d0ieE>p*W7CuCkI7qU&7(ti=ss&2lkDm27q&arzbPVraQ*-Py8<4s zctQqnMeJjd_}5vPpx`xPvw;oM{d?2sp#Y&KjPQn@X)#u0fSOd5YW+5@gcKa|g)Zmm z){T0mJP_LQ`YPYs6R&m3ZRab{D1fK_O>$Uxm)%bYdM_>l;3N!!Y zjWuqxEb8YW;;J|b=M?k2`tSXI;yyOVge0_Q?O(GE*0k(&!f$S@U*l@9QzF$)-WrbR zdD9f$Bm$1;x0!W)^53F3-It87{uuoI);~Sr_d8Ghg(Abm4&wvM z4#PHr^N&3SKmHwF4-Zj=P8TUe#Pq8++E6PlWVX9@x3@aZ$9!Hau`ldcctLC#Xtiso z7-gykk2$aa&6|f;7gjw8e7o&4fdeO1TVp5BdP>i1jvQ$Im}lTA8|$hkT`to99xy_b zA#G(Oh~|=P!toJS)GEGayszmRpe+sabki2v6zlBOv6SdhTT~U=Hf7grOo-L|S5>fZnQpy&PQh&ENO73_oJye`8fG_pc`dXr3!1NGAUR#nx^bJ;*FlFME z<9+l?hF@pbkIX2_i`UvGjlS%CGR2b>h4*hZ=_i`sUG^qRB8}X3lsRbbD(SBj6EJf5 zYDW`1u_$MSQvXDUc~(Vfu2z)|>A1DnN?CvAJcU}=7}`oF*?{$aIg~RAKq=a^L%p|b zXNYBmo#z?SDtc00C4z_p)XY0xmrJ}K{c>k{eNbvTts{+2+b&JbvH8mTddW#}1JhTLEEF750_B1NcXg6&Kw4s?#k8>{h#`82fBtS;3zAD4r^fSdOilJOB72ah%ws%II64&x{HCZ>B26cH`&@j@x zPa3Qe%@Oza$&Bwk>T*&LMDq|a9Vo`a*q5d2@B&u9PlK|Y`kmm10}_t62R=R?ylv4} zRQ@k1sK0qt(WzFv6kIQ;J?q8Ius>%?{oXYh|#S18MemeHS~WgpT# z^Z+TDjf>)~jh;77bwP-|pK-7&r#0hc95psI@;IN8Gmm znL8hG26Yhvka)X{e3~S*ly<|$%R@_vaz}wSW(r)`CTOxj&T$quHBTf&ZX8^)nlTlL z>ytKjvn4APWOj5HHwMIOZ-G?0n59cm42F<7sPTKo8#&m(Pe8^k|-#z4YJbocxw_^VIZz4ATtaKLg-8=Uzh%Z2KefId* zsk}2c;c1nNp+g(_1C`Ds#BHs=vB99GgeUQ@dwF4O79=i3cxM^-I7D`&y z639vTLZ7P*F$&l@JIktblfYIh(58BAq(6?GbCAMqe^HROpe2o+SC7D1bs6F}Kcav; zuD6_rbIjq$rom{yKCZVslOxm0*;RJ8$#lHz1m8bUeYmbM1t0dtE8C*ff=|P@b;frq z*W{lvSJO*4*~8;liHkwQ`60^7m5SL zpbRs{@;xcNHeGM+X-FBArk1wm7Uk)}(ZcOE|9mK>xbC7$LA>1)rX@8U%g+3ds9imf z#|C?CRW$Fa5h=mDT0`O})BX&#*Bu9JS_H$4fW)oE9z~|D*A&?r~5{$6&@>nZJoSe9yW)@fQ|pF1BIi z)5X~N(-nIRrnSL|lbUTF3{&3pEa((2Y6&o+vQiOMdZjahBAAW}AcSBBw3Mp7Hge#| z1%gBE+C;3!v_4btLZyDgVa85CM~<9YByJ*gI3qW;s8Gms2O>}y3D{}@a_|{SkVR+E z-QW84O@ks~?XR4b51yww1#_m?beKd((-J}+JBL~x^PxtvtRHS`j|N@$9j||(X363Z(>!@5 z@KFBbR$T2%98cWJUM8pWE<(vI(DgsLuAgJp#N2Y#R+-eF=Xmqosk!I4dRqB@Kaui( z?fa=rLOdZaD>!H?nBkVE7Gqg67Aq=<-%rrzSN4O$0#8~g2##+k%XF`9`i$=PPUp3c z1bYWiqMguD$YH#`Et-;$D5YgV1vL%AFq?3(}zv z5QTGWo^OZJHrpx`3k--?Xv2GsVP=^D2176vWl1pWY5=XF$8r$)0;%`BpMUF4^z3r- zgQw=hyWs72PWUt9!_Nh01LSwZj+37<_2DhDsej2C{{Z{z`25$lzL)TdGG<#PGpPnu zva^qzj{AOM>TaRkvRr(bPmk7i?Ec_q%wKf6p8SV04u4#hDa0T3Ewpa)wZq|#7ZXG9 zT_W$@AJQ&oqOHBta2^bH2a-gfm|ETBn$qhLnMT&N_(uB@F%}KO;dN`*dvqoRVgL)m zDCq6YF`Y?m(=NOJK%AI&^zfbZx;Af%Z~aE(rflnQ_gHNcXc`*#?-5dp7i+oqtVRy+ z+P|ody8cCSdV48|tN6~;)A_jO##oYslSq6k8eiLoAo0xgjG;9Va;gye#b%G`R<9$^ zgr!N3>I%WM2??nT))|mtvi2HNGr7;C z!qO6&jq5?7B&MR-fY2H#e8|`eGZw3{iD2gX^WCg|KM``wK^223xQS60>^*(5yRYeV#eqlt@p5Rzbsf?AgX81I_5CfNA?5ze5lfuEX8ud$nIgEi0SouLXL|8Y-Z&J|$?Ou02t35`D{dDW) zR;_5SuGIQC7ddCVu8~N)r#%#8xEV|~tN2z%xTri`{LLrbnQz`&PK3p&%?<*Cgy9uG z8?Km16AiAY)Yy|Cr;j7v0tGQ6}R~sJFbap+r=BipOrD^lr!qy#0rLL z3f(@5w8^xG2w-%$tzQrC;xFA_Q|j<_nv}dca-`?{(-osy%DmI1%0l)MzTw`WncDq* z)83s+^#wW4?;Ti>%kD1m!Z_|NJ&I1{2@ntDm^HH;IT=T>(_zDbau9-ORsuMbT<#bA zEpSI(uCrz}Glb|QPKr*ukw~2%VG2g{!HmDQbEg-^sH-p3{-%Eax}}>NUB$G4=FU>d z8+)m`Ud+7HJW%(77S&=-cYC2nL0~;#=ysZ5Tc;1gvGc_)65{EjS#gnmL#ai;xra;$0NU4ifgoo zd)tUx;$o(Y&q)-VQ+r|1%*%{HdDfeFZ)P6Y8M9Ehsf`d(hjM48HSyeBQcpS3i}bE> zb*BIx8$IKUiBu`G z^L3soq~965@JOcWg4L5EI=ElYEsFmPTDVWEV{@~q0cJt)^?2FrNeD;je6bt3!;__D znRK~+o;>2}BCHqF;wVt+qZ&E819+o7T)RvpK=jPzE3$XIYrE@5^~gf=m206z>07|L zbsyE138rrkuH@S1*731MM%;~WI6Rbs4*S&)FIJgW?Z)zx`6in*^=oYj z9cL$=sR3)WT#00hnU`ocWJCZF;$3lb)GF}%9|x5mtk;^mgzmhik2?K`;tuTdn(lLK z9EjVw-jXk|eRIQMQAT&OPNsR75^>=ih3&{@TqG>`mzuSf^soYYc`C?`x`X9xna=UQ z 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%0AJ;R@rwNiR83AfIF82rI|_o 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 z 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 H9E|+~mp@FJ literal 0 HcmV?d00001 From d406a18f58e11abe2970afd3f988fd1db51d3f3c Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Fri, 9 Aug 2024 17:15:07 +0530 Subject: [PATCH 404/582] adding translations --- .../molecules/DataModelCard/index.tsx | 30 ++++++++-------- GUI/src/enums/dataModelsEnums.ts | 2 ++ GUI/src/pages/DataModels/index.tsx | 28 +++++++-------- GUI/translations/en/common.json | 35 ++++++++++++++++--- 4 files changed, 60 insertions(+), 35 deletions(-) diff --git a/GUI/src/components/molecules/DataModelCard/index.tsx b/GUI/src/components/molecules/DataModelCard/index.tsx index b82fa2f0..2fb5ea97 100644 --- a/GUI/src/components/molecules/DataModelCard/index.tsx +++ b/GUI/src/components/molecules/DataModelCard/index.tsx @@ -1,14 +1,14 @@ import { FC, PropsWithChildren } from 'react'; import Button from 'components/Button'; import Label from 'components/Label'; -import { useQueryClient } from '@tanstack/react-query'; import { useDialog } from 'hooks/useDialog'; import './DataModel.scss'; import { Maturity, TrainingStatus } from 'enums/dataModelsEnums'; import Card from 'components/Card'; +import { useTranslation } from 'react-i18next'; type DataModelCardProps = { - modelId?:number; + modelId:number; dataModelName?: string | undefined; datasetGroupName?: string; version?: string; @@ -18,12 +18,12 @@ type DataModelCardProps = { trainingStatus?: string; platform?: string; maturity?: string; - setId?: React.Dispatch>; - setView?: React.Dispatch>; + setId: React.Dispatch>; + setView: React.Dispatch>; results?: any; }; -const DataModelCard: FC> = ({ +const DataModelCard: FC> = ({ modelId, dataModelName, datasetGroupName, @@ -38,32 +38,32 @@ const DataModelCard: FC> = ({ setId, setView, }) => { - const queryClient = useQueryClient(); const { open,close } = useDialog(); + const { t } = useTranslation(); const renderTrainingStatus = (status: string | undefined) => { if (status === TrainingStatus.RETRAINING_NEEDED) { - return ; + return ; } else if (status === TrainingStatus.TRAINED) { - return ; + return ; } else if (status === TrainingStatus.TRAINING_INPROGRESS) { - return ; + return ; } else if (status === TrainingStatus.UNTRAINABLE) { - return ; + return ; }else if (status === TrainingStatus.NOT_TRAINED) { - return ; + return ; } }; const renderMaturityLabel = (status: string | undefined) => { if (status === Maturity.DEVELOPMENT) { - return ; + return ; } else if (status === Maturity.PRODUCTION) { - return ; + return ; } else if (status === Maturity.STAGING) { - return ; + return ; } else if (status === Maturity.TESTING) { - return ; + return ; } }; diff --git a/GUI/src/enums/dataModelsEnums.ts b/GUI/src/enums/dataModelsEnums.ts index a463983e..f776ee3d 100644 --- a/GUI/src/enums/dataModelsEnums.ts +++ b/GUI/src/enums/dataModelsEnums.ts @@ -10,6 +10,8 @@ export enum Maturity { PRODUCTION = 'production ready', STAGING = 'staging', DEVELOPMENT = 'development', + TESTING = 'testing', + } export enum Platform { diff --git a/GUI/src/pages/DataModels/index.tsx b/GUI/src/pages/DataModels/index.tsx index f80cb946..e182d97b 100644 --- a/GUI/src/pages/DataModels/index.tsx +++ b/GUI/src/pages/DataModels/index.tsx @@ -8,10 +8,7 @@ import { formattedArray, parseVersionString } from 'utils/commonUtilts'; import { getDataModelsOverview, getFilterData } from 'services/data-models'; import DataModelCard from 'components/molecules/DataModelCard'; import ConfigureDataModel from './ConfigureDataModel'; -import { INTEGRATION_OPERATIONS } from 'enums/integrationEnums'; -import { Platform } from 'enums/dataModelsEnums'; import { customFormattedArray, extractedArray } from 'utils/dataModelsUtils'; -import { MdPin, MdPinEnd, MdStar } from 'react-icons/md'; import CircularSpinner from 'components/molecules/CircularSpinner/CircularSpinner'; import { ButtonAppearanceTypes } from 'enums/commonEnums'; @@ -131,8 +128,7 @@ const DataModels: FC = () => { {!isModelDataLoading && !isProdModelDataLoading ?(

      -
      Production Models
      {' '} - +
      {t("dataModels.productionModels")}
      {' '}
      @@ -160,20 +156,20 @@ const DataModels: FC = () => {
      -
      Data Models
      +
      {t("dataModels.dataModels")}
      handleFilterChange('modelName', selection?.value ?? '') @@ -183,7 +179,7 @@ const DataModels: FC = () => { handleFilterChange('version', selection?.value ?? '') @@ -194,7 +190,7 @@ const DataModels: FC = () => { handleFilterChange('platform', selection?.value ?? '') @@ -205,7 +201,7 @@ const DataModels: FC = () => { { handleFilterChange('trainingStatus', selection?.value ) @@ -230,7 +226,7 @@ const DataModels: FC = () => { handleFilterChange('maturity', selection?.value ?? '') @@ -241,7 +237,7 @@ const DataModels: FC = () => { { defaultValue={filters?.sort} /> - +
      diff --git a/GUI/translations/en/common.json b/GUI/translations/en/common.json index 4f38ba5c..5924dcea 100644 --- a/GUI/translations/en/common.json +++ b/GUI/translations/en/common.json @@ -14,8 +14,8 @@ "active": "Active", "activate": "Activate", "deactivate": "Deactivate", - "disconnect":"Disconnect", - "connect":"Connect", + "disconnect": "Disconnect", + "connect": "Connect", "on": "On", "off": "Off", "back": "Back", @@ -168,7 +168,7 @@ }, "classHierarchy": { "title": "Class Hierarchy", - "addClassButton":"Add main class", + "addClassButton": "Add main class", "addSubClass": "Add Subclass", "fieldHint": "Enter a field name", "filedHintIfExists": "Class name already exists" @@ -296,5 +296,32 @@ "title": "Validation Sessions", "inprogress": "Validation in-Progress", "fail": "Validation failed because {{class}} class found in the {{column}} column does not exist in hierarchy" + }, + "dataModels": { + "productionModels": "Production Models", + "dataModels": "Data Models", + "createModel": "Create Model", + "filters": { + "modelName": "Model Name", + "version": "Version", + "platform": "Platform", + "datasetGroup": "Dataset Group", + "trainingStatus": "Training Status", + "maturity": "Maturity", + "sort": "Sort by name (A - Z)" + }, + "trainingStatus": { + "retrainingNeeded": "Retraining Needed", + "trained": "Trained", + "trainingInProgress": "Training In Progress", + "untrainable": "Untrainable", + "notTrained": "Not Trained" + }, + "maturity": { + "development": "Development", + "production": "Production", + "staging": "Staging", + "testing": "Testing" + } } -} \ No newline at end of file +} From 6b8af21aec7bd79acc0e5b15d259aa822c3d9111 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Fri, 9 Aug 2024 17:37:51 +0530 Subject: [PATCH 405/582] bug fixes --- GUI/src/components/DataTable/DataTable.scss | 4 ++ .../FormElements/FormInput/index.tsx | 2 +- GUI/src/components/MainNavigation/index.tsx | 24 +++++------- .../pages/UserManagement/SettingsUsers.scss | 5 +++ .../pages/UserManagement/UserManagement.scss | 11 ++++++ GUI/src/pages/UserManagement/UserModal.tsx | 38 ++++++++++++------- GUI/src/pages/UserManagement/index.tsx | 30 ++++++++++----- GUI/translations/en/common.json | 15 ++++++-- 8 files changed, 88 insertions(+), 41 deletions(-) diff --git a/GUI/src/components/DataTable/DataTable.scss b/GUI/src/components/DataTable/DataTable.scss index 7e4d25c0..933abdf1 100644 --- a/GUI/src/components/DataTable/DataTable.scss +++ b/GUI/src/components/DataTable/DataTable.scss @@ -194,3 +194,7 @@ } } } + +.data-table__scrollWrapper { + padding: 10px 20px; +} \ No newline at end of file diff --git a/GUI/src/components/FormElements/FormInput/index.tsx b/GUI/src/components/FormElements/FormInput/index.tsx index b5ce3ea9..dad9bda3 100644 --- a/GUI/src/components/FormElements/FormInput/index.tsx +++ b/GUI/src/components/FormElements/FormInput/index.tsx @@ -33,7 +33,7 @@ const FormInput = forwardRef( { }, ]; - const filterItemsByRole = (role: string, items: MenuItem[]) => { + const filterItemsByRole = (role: string[], items: MenuItem[]) => { return items?.filter((item) => { - switch (role) { - case ROLES.ROLE_ADMINISTRATOR: - return item?.id; - case ROLES.ROLE_MODEL_TRAINER: - return item?.id !== 'userManagement' && item?.id !== 'integration'; - case 'ROLE_UNAUTHENTICATED': - return false; - default: - return false; - } + if (role.includes(ROLES.ROLE_ADMINISTRATOR)) return item?.id; + else if (role.includes(ROLES.ROLE_MODEL_TRAINER)) + return item?.id !== 'userManagement' && item?.id !== 'integration'; + else return false; }); }; @@ -107,9 +101,9 @@ const MainNavigation: FC = () => { const res = await apiDev.get(userManagementEndpoints.FETCH_USER_ROLES()); return res?.data?.response; }, - onSuccess: (res) => { - const role = res?.includes(ROLES.ROLE_ADMINISTRATOR) ? ROLES.ROLE_ADMINISTRATOR :ROLES.ROLE_MODEL_TRAINER - const filteredItems = filterItemsByRole(role, items); + onSuccess: (res) => { + const roles = res; + const filteredItems = filterItemsByRole(roles, items); setMenuItems(filteredItems); }, onError: (error) => { @@ -172,4 +166,4 @@ const MainNavigation: FC = () => { ); }; -export default MainNavigation; +export default MainNavigation; \ No newline at end of file diff --git a/GUI/src/pages/UserManagement/SettingsUsers.scss b/GUI/src/pages/UserManagement/SettingsUsers.scss index 88fcd30f..51096e43 100644 --- a/GUI/src/pages/UserManagement/SettingsUsers.scss +++ b/GUI/src/pages/UserManagement/SettingsUsers.scss @@ -36,4 +36,9 @@ .footer-button-wrapper { display: flex; gap: 10px; +} + +.button-wrapper { + display: flex; + gap: 10px; } \ No newline at end of file diff --git a/GUI/src/pages/UserManagement/UserManagement.scss b/GUI/src/pages/UserManagement/UserManagement.scss index b1a4aa65..969be2a6 100644 --- a/GUI/src/pages/UserManagement/UserManagement.scss +++ b/GUI/src/pages/UserManagement/UserManagement.scss @@ -15,3 +15,14 @@ .form-group { margin-bottom: 20px; } + +.table-header { + display: flex; + width: 100%; + justify-content: end; +} + +.action-button-container { + display: flex; + gap: 10px; +} diff --git a/GUI/src/pages/UserManagement/UserModal.tsx b/GUI/src/pages/UserManagement/UserModal.tsx index 3884f411..49f6cedf 100644 --- a/GUI/src/pages/UserManagement/UserModal.tsx +++ b/GUI/src/pages/UserManagement/UserModal.tsx @@ -7,12 +7,13 @@ import { Button, Dialog, FormInput, Track } from 'components'; import { User, UserDTO } from 'types/user'; import { checkIfUserExists, createUser, editUser } from 'services/users'; import { useToast } from 'hooks/useToast'; -import Select from 'react-select'; +import Select, { components } from 'react-select'; import './SettingsUsers.scss'; import { FC, useMemo } from 'react'; import { ROLES } from 'enums/roles'; import { userManagementQueryKeys } from 'utils/queryKeys'; import { ButtonAppearanceTypes, ToastTypes } from 'enums/commonEnums'; +import { FaChevronDown, FaChevronUp } from 'react-icons/fa'; type UserModalProps = { onClose: () => void; @@ -20,6 +21,14 @@ type UserModalProps = { isModalOpen?: boolean; }; +const DropdownIndicator = (props: any) => { + return ( + + {props.selectProps.menuIsOpen ? : } + + ); +}; + const UserModal: FC = ({ onClose, user, isModalOpen }) => { const { t } = useTranslation(); const toast = useToast(); @@ -108,9 +117,9 @@ const UserModal: FC = ({ onClose, user, isModalOpen }) => { onSuccess: async (data) => { if (data.response === 'true') { toast.open({ - type: ToastTypes.SUCCESS, + type: ToastTypes.ERROR, title: t('global.notificationError'), - message: t('settings.users.userExists'), + message: t('userManagement.addUser.userExists'), }); } else { createNewUser(); @@ -134,8 +143,6 @@ const UserModal: FC = ({ onClose, user, isModalOpen }) => { else checkIfUserExistsMutation.mutate({ userData: data }); }); - const requiredText = t('settings.users.required') ?? '*'; - return ( = ({ onClose, user, isModalOpen }) => { } onClose={onClose} footer={ -
      +
      @@ -214,10 +226,10 @@ const UserModal: FC = ({ onClose, user, isModalOpen }) => { {!user && ( = ({ onClose, user, isModalOpen }) => { { return data?.response ?? []; }; - const { data: users, isLoading } = useQuery( - userManagementQueryKeys.getAllEmployees(), - () => fetchUsers(pagination, sorting) - ); + const { data: users, isLoading } = useQuery({ + queryKey: userManagementQueryKeys.getAllEmployees(), + queryFn: () => fetchUsers(pagination, sorting), + onSuccess: (data) => { + setTotalPages( data[0]?.totalPages) + } + }); const ActionButtons: FC<{ row: User }> = ({ row }) => (
      @@ -78,14 +81,14 @@ const UserManagement: FC = () => { title: t('userManagement.addUser.deleteUserModalTitle') ?? '', content:

      {t('userManagement.addUser.deleteUserModalDesc')}

      , footer: ( -
      +
      ), @@ -118,6 +121,9 @@ const UserManagement: FC = () => { columnHelper.accessor('useridcode', { header: t('userManagement.table.personalId') ?? '', }), + columnHelper.accessor('csaTitle', { + header: t('userManagement.table.title') ?? '', + }), columnHelper.accessor( (data: User) => { const output: string[] = []; @@ -142,7 +148,11 @@ const UserManagement: FC = () => { }), columnHelper.display({ id: 'actions', - header: t('userManagement.table.actions') ?? '', + header: () => ( +
      + {t('userManagement.table.actions') ?? ''} +
      + ), cell: (props) => , meta: { size: '1%', @@ -155,6 +165,7 @@ const UserManagement: FC = () => { const deleteUserMutation = useMutation({ mutationFn: ({ id }: { id: string | number }) => deleteUser(id), onSuccess: async () => { + close(); await queryClient.invalidateQueries( userManagementQueryKeys.getAllEmployees() ); @@ -173,8 +184,9 @@ const UserManagement: FC = () => { }, }); - if (isLoading) return ; + if (isLoading) return ; + console.log('users ', users); return (
      diff --git a/GUI/translations/en/common.json b/GUI/translations/en/common.json index 4f38ba5c..de1d970b 100644 --- a/GUI/translations/en/common.json +++ b/GUI/translations/en/common.json @@ -59,7 +59,7 @@ "title": "User Management", "addUserButton": " Add a user", "addUser": { - "addUserModalTitle": "Add a new user", + "addUserModalTitle": "Add a New User", "editUserModalTitle": "Edit User", "deleteUserModalTitle": "Are you sure?", "deleteUserModalDesc": "Confirm that you are wish to delete the following record", @@ -72,14 +72,23 @@ "title": "Title", "titlePlaceholder": "Enter title", "email": "Email", - "emailPlaceholder": "Enter email" + "emailPlaceholder": "Enter email", + "nameRequired": "Name is required", + "roleRequired": "Role is required", + "idCodeRequired": "ID code is required", + "titleRequired": "Title is required", + "emailRequired": "Email is required", + "invalidIdCode": "Invalid ID code", + "invalidemail": "Invalid Email", + "userExists": "User already exists" }, "table": { "fullName": "Full Name", "personalId": "Personal ID", "role": "Role", "email": "Email", - "actions": "Actions" + "actions": "Actions", + "title": "Title" } }, "integration": { From 1b8d1684b474710e79347c5517109d275f22c209 Mon Sep 17 00:00:00 2001 From: Thirunayan22 Date: Fri, 9 Aug 2024 18:29:09 +0530 Subject: [PATCH 406/582] removed sonarcloud local reference --- .github/workflows/est-workflow-dev.yml | 103 ------------------ .github/workflows/est-workflow-staging.yml | 69 ------------ .github/workflows/sonarcloud.yml | 5 +- ...ifier-dev-ruuter-private.rootcode.software | 37 ------- .../esclassifier-dev-ruuter.rootcode.software | 36 ------ .../esclassifier-dev-tim.rootcode.software | 37 ------- rtc_nginx/esclassifier-dev.rootcode.software | 33 ------ .../login.esclassifier-dev.rootcode.software | 33 ------ sonar-project.properties | 13 --- 9 files changed, 4 insertions(+), 362 deletions(-) delete mode 100644 .github/workflows/est-workflow-dev.yml delete mode 100644 .github/workflows/est-workflow-staging.yml delete mode 100644 rtc_nginx/esclassifier-dev-ruuter-private.rootcode.software delete mode 100644 rtc_nginx/esclassifier-dev-ruuter.rootcode.software delete mode 100644 rtc_nginx/esclassifier-dev-tim.rootcode.software delete mode 100644 rtc_nginx/esclassifier-dev.rootcode.software delete mode 100644 rtc_nginx/login.esclassifier-dev.rootcode.software delete mode 100644 sonar-project.properties diff --git a/.github/workflows/est-workflow-dev.yml b/.github/workflows/est-workflow-dev.yml deleted file mode 100644 index 4a069613..00000000 --- a/.github/workflows/est-workflow-dev.yml +++ /dev/null @@ -1,103 +0,0 @@ -name: Deploy EST Frontend and Backend to development - -on: - push: - branches: - - main - -jobs: - deploy: - runs-on: [self-hosted, dev-dell] - - steps: - - name: Clean up workspace - run: | - Remove-Item -Path 'C:\Users\pamod\Desktop\CICD\actions-runner\_work\classifier\classifier\*' -Recurse -Force - - - name: Checkout code - uses: actions/checkout@v3 - with: - clean: true - - - name: Stop all containers except the specified ones - shell: powershell - run: | - $containers_to_keep = @( - $(docker ps -q --filter "name=users_db"), - $(docker ps -q --filter "name=tim"), - $(docker ps -q --filter "name=authentication-layer"), - $(docker ps -q --filter "name=tim-postgresql") - ) -join "|" - - $containers_to_stop = docker ps -a -q | Where-Object {$_ -notmatch $containers_to_keep} - if ($containers_to_stop) { - docker stop $containers_to_stop - } - - - name: Remove all containers except the specified ones - shell: powershell - run: | - $containers_to_keep = @( - $(docker ps -q --filter "name=users_db"), - $(docker ps -q --filter "name=tim"), - $(docker ps -q --filter "name=authentication-layer"), - $(docker ps -q --filter "name=tim-postgresql") - ) -join "|" - - $containers_to_remove = docker ps -a -q | Where-Object {$_ -notmatch $containers_to_keep} - if ($containers_to_remove) { - docker rm $containers_to_remove - } - - - name: Remove all images except the specified ones - shell: powershell - run: | - $images_to_keep = "authentication-layer|tim|data-mapper|resql|ruuter|cron-manager" - $images_to_remove = docker images --format "{{.Repository}}:{{.Tag}}" | Where-Object {$_ -notmatch $images_to_keep} - if ($images_to_remove) { - $images_to_remove | ForEach-Object { docker rmi $_ --force} - } - - - name: Prune unused volumes - shell: powershell - run: docker volume prune -f - - - name: Prune unused networks - shell: powershell - run: docker network prune -f - - - name: Update constants.ini with GitHub secret - run: | - powershell -Command "(Get-Content constants.ini) -replace 'DB_PASSWORD=value', 'DB_PASSWORD=${{ secrets.DB_PASSWORD }}' | Set-Content constants.ini" - - - name: Build and run Docker Compose - run: | - docker compose -f docker-compose.development.yml up --build -d - - - name: Run migration script - run: | - wsl sh migrate_win.sh - - - name: Send failure Slack notification - if: failure() - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} - shell: powershell - run: | - $payload = @{ - text = "The Development environment deployment failed during one of the steps. Please check the output for details." - } - $payloadJson = $payload | ConvertTo-Json - Invoke-RestMethod -Uri $env:SLACK_WEBHOOK_URL -Method Post -ContentType 'application/json' -Body $payloadJson - - - name: Send success Slack notification - if: success() - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} - shell: powershell - run: | - $payload = @{ - text = "The build is complete and the development environment is now available. Please click the following link to access it: https://esclassifier-dev.rootcode.software/classifier" - } - $payloadJson = $payload | ConvertTo-Json - Invoke-RestMethod -Uri $env:SLACK_WEBHOOK_URL -Method Post -ContentType 'application/json' -Body $payloadJson diff --git a/.github/workflows/est-workflow-staging.yml b/.github/workflows/est-workflow-staging.yml deleted file mode 100644 index bbfc9dbc..00000000 --- a/.github/workflows/est-workflow-staging.yml +++ /dev/null @@ -1,69 +0,0 @@ -name: Deploy EST Frontend and Backend to Staging - -on: - push: - branches: - - stage - -jobs: - deploy: - runs-on: [self-hosted, stage] - - steps: - - name: Set permissions for workspace directory - run: | - sudo chown -R ubuntu:ubuntu /home/ubuntu/actions-runner/_work/classifier/classifier - sudo chmod -R u+rwx /home/ubuntu/actions-runner/_work/classifier/classifier - - - name: Clean up workspace - run: | - sudo rm -rf /home/ubuntu/actions-runner/_work/classifier/classifier/* - - - name: Checkout code - uses: actions/checkout@v3 - with: - clean: true - - - name: Give execute permissions to testScript.sh - run: | - sudo chmod +x src/unitTesting.sh - - - name: Remove all running containers, images, and prune Docker system - run: | - docker stop $(docker ps -a -q) || true - docker rm $(docker ps -a -q) || true - images_to_keep="authentication-layer|tim|data-mapper|resql|ruuter|cron-manager" - docker images --format "{{.Repository}}:{{.Tag}}" | grep -Ev "$images_to_keep" | xargs -r docker rmi || true - docker volume prune -f - docker network prune -f - - - name: Build and run Docker Compose - run: | - docker compose up --build -d - - - name: Run unitTesting.sh - id: unittesting - run: | - output=$(bash src/unitTesting.sh) - if [ "$output" != "True" ]; then - echo "unitTesting.sh failed with output: $output" - exit 1 - fi - - - name: Send failure Slack notification - if: failure() - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} - run: | - curl -X POST -H 'Content-type: application/json' --data "{ - \"text\": \"The Staging environment deployment failed during one of the steps. Please check the output for details.\" - }" $SLACK_WEBHOOK_URL - - - name: Send success Slack notification - if: success() - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} - run: | - curl -X POST -H 'Content-type: application/json' --data "{ - \"text\": \"The build is complete and the staging environment is now available. Please click the following link to access it: \" - }" $SLACK_WEBHOOK_URL diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index dd1bf9db..97fb2ec5 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -24,4 +24,7 @@ jobs: uses: SonarSource/sonarcloud-github-action@master env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} # Needed to authenticate with SonarCloud \ No newline at end of file + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} # Needed to authenticate with SonarCloud + with: + -DSonar.projectKey=${{ secrets.SONAR_PROJECT_KEY}} + -DSonar.organization=${{ secrets.SONAR_ORGANIZATION}} \ No newline at end of file diff --git a/rtc_nginx/esclassifier-dev-ruuter-private.rootcode.software b/rtc_nginx/esclassifier-dev-ruuter-private.rootcode.software deleted file mode 100644 index 96cba1cd..00000000 --- a/rtc_nginx/esclassifier-dev-ruuter-private.rootcode.software +++ /dev/null @@ -1,37 +0,0 @@ -server { - - root /var/www/esclassifier-dev-ruuter-private.rootcode.software/html; - index index.html index.htm index.nginx-debian.html; - - server_name esclassifier-dev-ruuter-private.rootcode.software; - - - location / { - proxy_pass http://localhost:8088; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - - listen [::]:443 ssl ipv6only=on; # managed by Certbot - listen 443 ssl; # managed by Certbot - ssl_certificate /etc/letsencrypt/live/esclassifier-dev-ruuter-private.rootcode.software/fullchain.pem; # managed by Certbot - ssl_certificate_key /etc/letsencrypt/live/esclassifier-dev-ruuter-private.rootcode.software/privkey.pem; # managed by Certbot - include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot - ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot - -}server { - if ($host = esclassifier-dev-ruuter-private.rootcode.software) { - return 301 https://$host$request_uri; - } # managed by Certbot - - - listen 80; - listen [::]:80; - - server_name esclassifier-dev-ruuter-private.rootcode.software; - return 404; # managed by Certbot - - -} \ No newline at end of file diff --git a/rtc_nginx/esclassifier-dev-ruuter.rootcode.software b/rtc_nginx/esclassifier-dev-ruuter.rootcode.software deleted file mode 100644 index 79da1f5f..00000000 --- a/rtc_nginx/esclassifier-dev-ruuter.rootcode.software +++ /dev/null @@ -1,36 +0,0 @@ -server { - - root /var/www/esclassifier-dev-ruuter.rootcode.software/html; - index index.html index.htm index.nginx-debian.html; - - server_name esclassifier-dev-ruuter.rootcode.software; - - location / { - proxy_pass http://localhost:8086; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - - listen [::]:443 ssl; # managed by Certbot - listen 443 ssl; # managed by Certbot - ssl_certificate /etc/letsencrypt/live/esclassifier-dev-ruuter.rootcode.software/fullchain.pem; # managed by Certbot - ssl_certificate_key /etc/letsencrypt/live/esclassifier-dev-ruuter.rootcode.software/privkey.pem; # managed by Certbot - include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot - ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot - -} - -server { - if ($host = esclassifier-dev-ruuter.rootcode.software) { - return 301 https://$host$request_uri; - } # managed by Certbot - - listen 80; - listen [::]:80; - - server_name esclassifier-dev-ruuter.rootcode.software; - return 404; # managed by Certbot - -} diff --git a/rtc_nginx/esclassifier-dev-tim.rootcode.software b/rtc_nginx/esclassifier-dev-tim.rootcode.software deleted file mode 100644 index 00681a84..00000000 --- a/rtc_nginx/esclassifier-dev-tim.rootcode.software +++ /dev/null @@ -1,37 +0,0 @@ -server { - - root /var/www/esclassifier-dev-tim.rootcode.software/html; - index index.html index.htm index.nginx-debian.html; - - server_name esclassifier-dev-tim.rootcode.software; - - location / { - proxy_pass http://localhost:8085; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - - listen [::]:443 ssl; # managed by Certbot - listen 443 ssl; # managed by Certbot - ssl_certificate /etc/letsencrypt/live/esclassifier-dev-tim.rootcode.software/fullchain.pem; # managed by Certbot - ssl_certificate_key /etc/letsencrypt/live/esclassifier-dev-tim.rootcode.software/privkey.pem; # managed by Certbot - include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot - ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot - -} -server { - if ($host = esclassifier-dev-tim.rootcode.software) { - return 301 https://$host$request_uri; - } # managed by Certbot - - - listen 80; - listen [::]:80; - - server_name esclassifier-dev-tim.rootcode.software; - return 404; # managed by Certbot - - -} \ No newline at end of file diff --git a/rtc_nginx/esclassifier-dev.rootcode.software b/rtc_nginx/esclassifier-dev.rootcode.software deleted file mode 100644 index f8973d31..00000000 --- a/rtc_nginx/esclassifier-dev.rootcode.software +++ /dev/null @@ -1,33 +0,0 @@ -server { - listen 443 ssl; # managed by Certbot - - server_name esclassifier-dev.rootcode.software; - - root /var/www/esclassifier-dev.rootcode.software/html; - index index.html index.htm index.nginx-debian.html; - - location / { - proxy_pass http://esclassifier-dev.rootcode.software:3001; - proxy_redirect http://esclassifier-dev.rootcode.software:3001 /; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } - - ssl_certificate /etc/letsencrypt/live/esclassifier-dev.rootcode.software/fullchain.pem; # managed by Certbot - ssl_certificate_key /etc/letsencrypt/live/esclassifier-dev.rootcode.software/privkey.pem; # managed by Certbot - include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot - ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot -} - -server { - if ($host = esclassifier-dev.rootcode.software) { - return 301 https://$host$request_uri; - } # managed by Certbot - - listen 80; - listen [::]:80; - - server_name esclassifier-dev.rootcode.software; - return 404; # managed by Certbot -} diff --git a/rtc_nginx/login.esclassifier-dev.rootcode.software b/rtc_nginx/login.esclassifier-dev.rootcode.software deleted file mode 100644 index 003d0cac..00000000 --- a/rtc_nginx/login.esclassifier-dev.rootcode.software +++ /dev/null @@ -1,33 +0,0 @@ -server { - listen 443 ssl; # managed by Certbot - - server_name login.esclassifier-dev.rootcode.software; - - root /var/www/login.esclassifier-dev.rootcode.software/html; - index index.html index.htm index.nginx-debian.html; - - location / { - proxy_pass http://login.esclassifier-dev.rootcode.software:3004; - proxy_redirect http://login.esclassifier-dev.rootcode.software:3004 /; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } - - ssl_certificate /etc/letsencrypt/live/login.esclassifier-dev.rootcode.software/fullchain.pem; # managed by Certbot - ssl_certificate_key /etc/letsencrypt/live/login.esclassifier-dev.rootcode.software/privkey.pem; # managed by Certbot - include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot - ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot -} - -server { - if ($host = login.esclassifier-dev.rootcode.software) { - return 301 https://$host$request_uri; - } # managed by Certbot - - listen 80; - listen [::]:80; - - server_name login.esclassifier-dev.rootcode.software; - return 404; # managed by Certbot -} diff --git a/sonar-project.properties b/sonar-project.properties deleted file mode 100644 index 0e993790..00000000 --- a/sonar-project.properties +++ /dev/null @@ -1,13 +0,0 @@ -sonar.projectKey=rootcodelabs_classifier -sonar.organization=rootcode - -# This is the name and version displayed in the SonarCloud UI. -#sonar.projectName=classifier -#sonar.projectVersion=1.0 - - -# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. -#sonar.sources=. - -# Encoding of the source code. Default is default system encoding -#sonar.sourceEncoding=UTF-8 \ No newline at end of file From 11ae6fd420e956047b107f9b773420a59a4285bf Mon Sep 17 00:00:00 2001 From: Thirunayan22 Date: Fri, 9 Aug 2024 18:31:04 +0530 Subject: [PATCH 407/582] made changes to sonar cloud --- .github/workflows/sonarcloud.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 97fb2ec5..72e14f8b 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -26,5 +26,5 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} # Needed to authenticate with SonarCloud with: - -DSonar.projectKey=${{ secrets.SONAR_PROJECT_KEY}} - -DSonar.organization=${{ secrets.SONAR_ORGANIZATION}} \ No newline at end of file + -Dsonar.projectKey=${{ secrets.SONAR_PROJECT_KEY }} + -Dsonar.organization=${{ secrets.SONAR_ORGANIZATION }} \ No newline at end of file From d8c28cc66b013539085c966c6f0dd7ac31001767 Mon Sep 17 00:00:00 2001 From: Thirunayan22 Date: Fri, 9 Aug 2024 18:32:34 +0530 Subject: [PATCH 408/582] fixed error in sonarcloud configuration --- .github/workflows/sonarcloud.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 72e14f8b..49d71ddb 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -26,5 +26,8 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} # Needed to authenticate with SonarCloud with: - -Dsonar.projectKey=${{ secrets.SONAR_PROJECT_KEY }} - -Dsonar.organization=${{ secrets.SONAR_ORGANIZATION }} \ No newline at end of file + + args: + + -Dsonar.projectKey=${{ secrets.SONAR_PROJECT_KEY }} + -Dsonar.organization=${{ secrets.SONAR_ORGANIZATION }} \ No newline at end of file From fc872d159b8b6df9318c55f787c355ab034d3515 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Fri, 9 Aug 2024 18:41:17 +0530 Subject: [PATCH 409/582] remove docker-compose-dev --- docker-compose.development.yml | 180 --------------------------------- 1 file changed, 180 deletions(-) delete mode 100644 docker-compose.development.yml diff --git a/docker-compose.development.yml b/docker-compose.development.yml deleted file mode 100644 index 0e8d2699..00000000 --- a/docker-compose.development.yml +++ /dev/null @@ -1,180 +0,0 @@ -services: - ruuter-public: - container_name: ruuter-public - image: ruuter - environment: - - application.cors.allowedOrigins=https://login.esclassifier-dev.rootcode.software, https://esclassifier-dev-ruuter.rootcode.software, https://esclassifier-dev.rootcode.software - - application.httpCodesAllowList=200,201,202,400,401,403,500 - - application.internalRequests.allowedIPs=127.0.0.1 - - application.logging.displayRequestContent=true - - application.logging.displayResponseContent=true - - server.port=8086 - volumes: - - ./DSL/Ruuter.public/DSL:/DSL - - ./constants.ini:/app/constants.ini - ports: - - 8086:8086 - networks: - - bykstack - cpus: "0.5" - mem_limit: "512M" - - ruuter-private: - container_name: ruuter-private - image: ruuter - environment: - - application.cors.allowedOrigins=https://esclassifier-dev.rootcode.software,https://esclassifier-dev-ruuter-private.rootcode.software,https://login.esclassifier-dev.rootcode.software - - application.httpCodesAllowList=200,201,202,204,400,401,403,404,500 - - application.internalRequests.allowedIPs=127.0.0.1 - - application.logging.displayRequestContent=true - - application.incomingRequests.allowedMethodTypes=POST,GET,PUT - - application.logging.displayResponseContent=true - - application.logging.printStackTrace=true - - server.port=8088 - volumes: - - ./DSL/Ruuter.private/DSL:/DSL - - ./constants.ini:/app/constants.ini - ports: - - 8088:8088 - networks: - - bykstack - cpus: "0.5" - mem_limit: "512M" - - data-mapper: - container_name: data-mapper - image: data-mapper - environment: - - PORT=3000 - - CONTENT_FOLDER=/data - volumes: - - ./DSL:/data - - ./DSL/DMapper/hbs:/workspace/app/views/classifier - - ./DSL/DMapper/js:/workspace/app/js/classifier - - ./DSL/DMapper/lib:/workspace/app/lib - ports: - - 3000:3000 - networks: - - bykstack - - tim: - container_name: tim - image: tim - depends_on: - - tim-postgresql - environment: - - SECURITY_ALLOWLIST_JWT=ruuter-private,ruuter-public,data-mapper,resql,tim,tim-postgresql,chat-widget,authentication-layer,127.0.0.1,::1 - ports: - - 8085:8085 - networks: - - bykstack - extra_hosts: - - "host.docker.internal:host-gateway" - cpus: "0.5" - mem_limit: "512M" - - tim-postgresql: - container_name: tim-postgresql - image: postgres:14.1 - environment: - - POSTGRES_USER=tim - - POSTGRES_PASSWORD=123 - - POSTGRES_DB=tim - - POSTGRES_HOST_AUTH_METHOD=trust - volumes: - - ./tim-db:/var/lib/postgresql/data - ports: - - 9876:5432 - networks: - - bykstack - - gui: - container_name: gui - environment: - - NODE_ENV=development - - REACT_APP_RUUTER_API_URL=https://esclassifier-dev-ruuter.rootcode.software - - REACT_APP_RUUTER_PRIVATE_API_URL=https://esclassifier-dev-ruuter-private.rootcode.software - - REACT_APP_CUSTOMER_SERVICE_LOGIN=https://login.esclassifier-dev.rootcode.software - # - REACT_APP_NOTIFICATION_NODE_URL=http://localhost:4040 - - REACT_APP_CSP=upgrade-insecure-requests; default-src 'self'; font-src 'self' data:; img-src 'self' data:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self' https://esclassifier-dev-ruuter.rootcode.software https://esclassifier-dev-ruuter-private.rootcode.software https://esclassifier-dev-tim.rootcode.software; - - DEBUG_ENABLED=true - - CHOKIDAR_USEPOLLING=true - - PORT=3001 - - REACT_APP_SERVICE_ID=conversations,settings,monitoring - - REACT_APP_ENABLE_HIDDEN_FEATURES=TRUE - - build: - context: ./GUI - dockerfile: Dockerfile.dev - ports: - - 3001:3001 - volumes: - - /app/node_modules - - ./GUI:/app - networks: - - bykstack - cpus: "0.5" - mem_limit: "1G" - - authentication-layer: - container_name: authentication-layer - image: authentication-layer - ports: - - 3004:3004 - networks: - - bykstack - - resql: - container_name: resql - image: resql - depends_on: - - users_db - environment: - - sqlms.datasources.[0].name=classifier - - sqlms.datasources.[0].jdbcUrl=jdbc:postgresql://users_db:5432/classifier #For LocalDb Use - # sqlms.datasources.[0].jdbcUrl=jdbc:postgresql://171.22.247.13:5433/byk?sslmode=require - - sqlms.datasources.[0].username=postgres - - sqlms.datasources.[0].password=rootcode - - logging.level.org.springframework.boot=INFO - ports: - - 8082:8082 - volumes: - - ./DSL/Resql:/workspace/app/templates/classifier - networks: - - bykstack - - users_db: - container_name: users_db - image: postgres:14.1 - environment: - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=rootcode - - POSTGRES_DB=classifier - ports: - - 5433:5432 - volumes: - - /home/ubuntu/user_db_files:/var/lib/postgresql/data - networks: - - bykstack - restart: always - - cron-manager: - container_name: cron-manager - image: cron-manager - volumes: - - ./DSL/CronManager/DSL:/DSL - - ./DSL/CronManager/script:/app/scripts - - ./DSL/CronManager/config:/app/config - environment: - - server.port=9010 - ports: - - 9010:8080 - networks: - - bykstack - -networks: - bykstack: - name: bykstack - driver: bridge - driver_opts: - com.docker.network.driver.mtu: 1400 From f8a3148bc13cb2d2057de2091295ff03e46d6b57 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Fri, 9 Aug 2024 18:43:30 +0530 Subject: [PATCH 410/582] code cleanups --- GUI/.env.development | 2 +- docker-compose.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/GUI/.env.development b/GUI/.env.development index efa8ea86..7ff4d8bb 100644 --- a/GUI/.env.development +++ b/GUI/.env.development @@ -4,5 +4,5 @@ REACT_APP_EXTERNAL_API_URL=http://localhost:8000 REACT_APP_CUSTOMER_SERVICE_LOGIN=http://localhost:3004/et/dev-auth REACT_APP_SERVICE_ID=conversations,settings,monitoring REACT_APP_NOTIFICATION_NODE_URL=http://localhost:4040 -REACT_APP_CSP=upgrade-insecure-requests; default-src 'self'; font-src 'self' data:; img-src 'self' data:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self' http://localhost:8086 http://localhost:8088 http://localhost:8085 http://localhost:4040 http://localhost:3002/menu.json; +REACT_APP_CSP=upgrade-insecure-requests; default-src 'self'; font-src 'self' data:; img-src 'self' data:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self' http://localhost:8086 http://localhost:8088 http://localhost:8085 http://localhost:4040; REACT_APP_ENABLE_HIDDEN_FEATURES=TRUE \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index bfe7bcd6..4e8937ec 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -95,7 +95,7 @@ services: - REACT_APP_RUUTER_API_URL=http://localhost:8086 - REACT_APP_RUUTER_PRIVATE_API_URL=http://localhost:8088 - REACT_APP_CUSTOMER_SERVICE_LOGIN=http://localhost:3004/et/dev-auth - # - REACT_APP_NOTIFICATION_NODE_URL=http://localhost:4040 + - REACT_APP_NOTIFICATION_NODE_URL=http://localhost:4040 - REACT_APP_CSP=upgrade-insecure-requests; default-src 'self'; font-src 'self' data:; img-src 'self' data:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self' http://localhost:8086 http://localhost:8088 http://localhost:8085 http://localhost:4040 http://localhost:3001 http://localhost:8000; - DEBUG_ENABLED=true - CHOKIDAR_USEPOLLING=true From 856357b457cf9c24f6ef52ef82131cc7981011db Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Fri, 9 Aug 2024 18:45:39 +0530 Subject: [PATCH 411/582] update readme --- README.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/README.md b/README.md index 144ef445..35683061 100644 --- a/README.md +++ b/README.md @@ -45,16 +45,6 @@ This repo will primarily contain: - To Initialize Open Search run `./deploy-opensearch.sh ` - To Use Opensearch locally run `./deploy-opensearch.sh http://localhost:9200 admin:admin true` -### Use external components. - -Currently, Header and Main Navigation used as external components, they are defined as dependency in package.json - -``` - "@buerokrat-ria/header": "^0.0.1" - "@buerokrat-ria/menu": "^0.0.1" - "@buerokrat-ria/styles": "^0.0.1" -``` - ### Outlook Setup - Register Application in Azure portal - Supported account types - Supported account types From 96d1bbdc0b842025242874deb184ed4aa453efa7 Mon Sep 17 00:00:00 2001 From: Thirunayan22 Date: Fri, 9 Aug 2024 18:49:13 +0530 Subject: [PATCH 412/582] deleted docker development file --- docker-compose.development.yml | 180 --------------------------------- 1 file changed, 180 deletions(-) delete mode 100644 docker-compose.development.yml diff --git a/docker-compose.development.yml b/docker-compose.development.yml deleted file mode 100644 index 0e8d2699..00000000 --- a/docker-compose.development.yml +++ /dev/null @@ -1,180 +0,0 @@ -services: - ruuter-public: - container_name: ruuter-public - image: ruuter - environment: - - application.cors.allowedOrigins=https://login.esclassifier-dev.rootcode.software, https://esclassifier-dev-ruuter.rootcode.software, https://esclassifier-dev.rootcode.software - - application.httpCodesAllowList=200,201,202,400,401,403,500 - - application.internalRequests.allowedIPs=127.0.0.1 - - application.logging.displayRequestContent=true - - application.logging.displayResponseContent=true - - server.port=8086 - volumes: - - ./DSL/Ruuter.public/DSL:/DSL - - ./constants.ini:/app/constants.ini - ports: - - 8086:8086 - networks: - - bykstack - cpus: "0.5" - mem_limit: "512M" - - ruuter-private: - container_name: ruuter-private - image: ruuter - environment: - - application.cors.allowedOrigins=https://esclassifier-dev.rootcode.software,https://esclassifier-dev-ruuter-private.rootcode.software,https://login.esclassifier-dev.rootcode.software - - application.httpCodesAllowList=200,201,202,204,400,401,403,404,500 - - application.internalRequests.allowedIPs=127.0.0.1 - - application.logging.displayRequestContent=true - - application.incomingRequests.allowedMethodTypes=POST,GET,PUT - - application.logging.displayResponseContent=true - - application.logging.printStackTrace=true - - server.port=8088 - volumes: - - ./DSL/Ruuter.private/DSL:/DSL - - ./constants.ini:/app/constants.ini - ports: - - 8088:8088 - networks: - - bykstack - cpus: "0.5" - mem_limit: "512M" - - data-mapper: - container_name: data-mapper - image: data-mapper - environment: - - PORT=3000 - - CONTENT_FOLDER=/data - volumes: - - ./DSL:/data - - ./DSL/DMapper/hbs:/workspace/app/views/classifier - - ./DSL/DMapper/js:/workspace/app/js/classifier - - ./DSL/DMapper/lib:/workspace/app/lib - ports: - - 3000:3000 - networks: - - bykstack - - tim: - container_name: tim - image: tim - depends_on: - - tim-postgresql - environment: - - SECURITY_ALLOWLIST_JWT=ruuter-private,ruuter-public,data-mapper,resql,tim,tim-postgresql,chat-widget,authentication-layer,127.0.0.1,::1 - ports: - - 8085:8085 - networks: - - bykstack - extra_hosts: - - "host.docker.internal:host-gateway" - cpus: "0.5" - mem_limit: "512M" - - tim-postgresql: - container_name: tim-postgresql - image: postgres:14.1 - environment: - - POSTGRES_USER=tim - - POSTGRES_PASSWORD=123 - - POSTGRES_DB=tim - - POSTGRES_HOST_AUTH_METHOD=trust - volumes: - - ./tim-db:/var/lib/postgresql/data - ports: - - 9876:5432 - networks: - - bykstack - - gui: - container_name: gui - environment: - - NODE_ENV=development - - REACT_APP_RUUTER_API_URL=https://esclassifier-dev-ruuter.rootcode.software - - REACT_APP_RUUTER_PRIVATE_API_URL=https://esclassifier-dev-ruuter-private.rootcode.software - - REACT_APP_CUSTOMER_SERVICE_LOGIN=https://login.esclassifier-dev.rootcode.software - # - REACT_APP_NOTIFICATION_NODE_URL=http://localhost:4040 - - REACT_APP_CSP=upgrade-insecure-requests; default-src 'self'; font-src 'self' data:; img-src 'self' data:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self' https://esclassifier-dev-ruuter.rootcode.software https://esclassifier-dev-ruuter-private.rootcode.software https://esclassifier-dev-tim.rootcode.software; - - DEBUG_ENABLED=true - - CHOKIDAR_USEPOLLING=true - - PORT=3001 - - REACT_APP_SERVICE_ID=conversations,settings,monitoring - - REACT_APP_ENABLE_HIDDEN_FEATURES=TRUE - - build: - context: ./GUI - dockerfile: Dockerfile.dev - ports: - - 3001:3001 - volumes: - - /app/node_modules - - ./GUI:/app - networks: - - bykstack - cpus: "0.5" - mem_limit: "1G" - - authentication-layer: - container_name: authentication-layer - image: authentication-layer - ports: - - 3004:3004 - networks: - - bykstack - - resql: - container_name: resql - image: resql - depends_on: - - users_db - environment: - - sqlms.datasources.[0].name=classifier - - sqlms.datasources.[0].jdbcUrl=jdbc:postgresql://users_db:5432/classifier #For LocalDb Use - # sqlms.datasources.[0].jdbcUrl=jdbc:postgresql://171.22.247.13:5433/byk?sslmode=require - - sqlms.datasources.[0].username=postgres - - sqlms.datasources.[0].password=rootcode - - logging.level.org.springframework.boot=INFO - ports: - - 8082:8082 - volumes: - - ./DSL/Resql:/workspace/app/templates/classifier - networks: - - bykstack - - users_db: - container_name: users_db - image: postgres:14.1 - environment: - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=rootcode - - POSTGRES_DB=classifier - ports: - - 5433:5432 - volumes: - - /home/ubuntu/user_db_files:/var/lib/postgresql/data - networks: - - bykstack - restart: always - - cron-manager: - container_name: cron-manager - image: cron-manager - volumes: - - ./DSL/CronManager/DSL:/DSL - - ./DSL/CronManager/script:/app/scripts - - ./DSL/CronManager/config:/app/config - environment: - - server.port=9010 - ports: - - 9010:8080 - networks: - - bykstack - -networks: - bykstack: - name: bykstack - driver: bridge - driver_opts: - com.docker.network.driver.mtu: 1400 From e1a25dbd945db76f61a0c22430900f3276f5befc Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Fri, 9 Aug 2024 19:47:28 +0530 Subject: [PATCH 413/582] Read Me: encryption token issue fixed --- .../script/outlook_refresh_token.sh | 27 +++++++++---------- .../return_encrypted_outlook_token.handlebars | 3 +++ DSL/DMapper/lib/helpers.js | 21 +++++++++++++++ README.md | 1 + 4 files changed, 38 insertions(+), 14 deletions(-) create mode 100644 DSL/DMapper/hbs/return_encrypted_outlook_token.handlebars diff --git a/DSL/CronManager/script/outlook_refresh_token.sh b/DSL/CronManager/script/outlook_refresh_token.sh index 25740016..6d110b90 100644 --- a/DSL/CronManager/script/outlook_refresh_token.sh +++ b/DSL/CronManager/script/outlook_refresh_token.sh @@ -16,21 +16,22 @@ base64_encode() { echo -n "$1" | base64 } -# Function to Base64 decode -base64_decode() { - echo -n "$1" | base64 --decode -} - # Fetch the encrypted refresh token -encrypted_refresh_token=$(curl -s -X GET "$CLASSIFIER_RESQL/get-outlook-token" | grep -oP '"token":"\K[^"]+') +response=$(curl -X POST -H "Content-Type: application/json" -d '{"platform":"OUTLOOK"}' "$CLASSIFIER_RESQL/get-token") +encrypted_refresh_token=$(echo $response | grep -oP '"token":"\K[^"]+') + +echo "encrypted_refresh_token: $encrypted_refresh_token" if [ -z "$encrypted_refresh_token" ]; then echo "No encrypted refresh token found" exit 1 fi -# Decrypt the previous refresh token -decrypted_refresh_token=$(base64_decode "$encrypted_refresh_token") +responseVal=$(curl -X POST -H "Content-Type: application/json" -d '{"token":"'"$encrypted_refresh_token"'"}' "http://data-mapper:3000/hbs/classifier/return_decrypted_outlook_token") +decrypted_refresh_token=$(echo "$responseVal" | grep -oP '"content":"\K[^"]+' | sed 's/\\/\\\\/g') + + +echo "decrypted refresh token: $decrypted_refresh_token" # Request a new access token using the decrypted refresh token access_token_response=$(curl -X POST \ @@ -39,14 +40,15 @@ access_token_response=$(curl -X POST \ https://login.microsoftonline.com/common/oauth2/v2.0/token) new_refresh_token=$(echo $access_token_response | grep -oP '"refresh_token":"\K[^"]+') +echo "new_refresh_token: $new_refresh_token" if [ -z "$new_refresh_token" ]; then echo "Failed to get a new refresh token" exit 1 fi -# Encrypt the new refresh token -encrypted_new_refresh_token=$(base64_encode "$new_refresh_token") +responseEncrypt=$(curl -X POST -H "Content-Type: application/json" -d '{"token":"'"$new_refresh_token"'"}' "http://data-mapper:3000/hbs/classifier/return_encrypted_outlook_token") +encrypted_new_refresh_token=$(echo "$responseEncrypt" | grep -oP '"cipher":"\K[^"]+') # Function to save the new encrypted refresh token save_refresh_token() { @@ -56,9 +58,6 @@ save_refresh_token() { # Call the function to save the encrypted new refresh token save_refresh_token "$encrypted_new_refresh_token" - -# Print the new refresh token (decrypted for readability) -decrypted_new_refresh_token=$(base64_decode "$encrypted_new_refresh_token") -echo "New refresh token: $decrypted_new_refresh_token" +echo "Encrypted New refresh token: $encrypted_new_refresh_token" echo $(date -u +"%Y-%m-%d %H:%M:%S.%3NZ") - $script_name finished diff --git a/DSL/DMapper/hbs/return_encrypted_outlook_token.handlebars b/DSL/DMapper/hbs/return_encrypted_outlook_token.handlebars new file mode 100644 index 00000000..20249c22 --- /dev/null +++ b/DSL/DMapper/hbs/return_encrypted_outlook_token.handlebars @@ -0,0 +1,3 @@ +{ + "token": {{{base64Encrypt token}}} +} diff --git a/DSL/DMapper/lib/helpers.js b/DSL/DMapper/lib/helpers.js index 9387cbbc..f2e14ea4 100644 --- a/DSL/DMapper/lib/helpers.js +++ b/DSL/DMapper/lib/helpers.js @@ -98,3 +98,24 @@ export function base64Decrypt(cipher, isObject) { } } +export function base64Encrypt(content) { + if (!content) { + return { + error: true, + message: 'Content is missing', + } + } + + try { + return JSON.stringify({ + error: false, + cipher: btoa(typeof content === 'string' ? content : JSON.stringify(content)) + }); + } catch (err) { + return JSON.stringify({ + error: true, + message: 'Base64 Encryption Failed', + }); + } +} + diff --git a/README.md b/README.md index 35683061..3e8f877f 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ This repo will primarily contain: - Redirect URI platform - Web - Client ID, Client Secret should be set in constant.ini under OUTLOOK_CLIENT_ID and OUTLOOK_SECRET_KEY - Navigate CronManger/config folder and add Client ID, Client Secret values in config.ini file also +- Set the value of `CLASSIFIER_RUUTER_PUBLIC_FRONTEND_URL` in constant.ini - Allowing it to be accessed from the internet for validating Outlook subscription. ### Jira Setup From 262cb89aef1b837c1e6c1612c6dd6af97226e925 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Fri, 9 Aug 2024 20:12:12 +0530 Subject: [PATCH 414/582] cleanups --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 4e8937ec..6d9c5074 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -134,7 +134,7 @@ services: - sqlms.datasources.[0].jdbcUrl=jdbc:postgresql://users_db:5432/classifier #For LocalDb Use # sqlms.datasources.[0].jdbcUrl=jdbc:postgresql://171.22.247.13:5433/byk?sslmode=require - sqlms.datasources.[0].username=postgres - - sqlms.datasources.[0].password=rootcode + - sqlms.datasources.[0].password=password - logging.level.org.springframework.boot=INFO ports: - 8082:8082 @@ -148,7 +148,7 @@ services: image: postgres:14.1 environment: - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=rootcode + - POSTGRES_PASSWORD=password - POSTGRES_DB=classifier ports: - 5433:5432 From d6f43ce386f77b4bb20bd8782968b6a2162c98c8 Mon Sep 17 00:00:00 2001 From: Thirunayan22 Date: Fri, 9 Aug 2024 20:23:46 +0530 Subject: [PATCH 415/582] updated key values in docker compose --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 1b3d1805..97e214fc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -134,7 +134,7 @@ services: - sqlms.datasources.[0].jdbcUrl=jdbc:postgresql://users_db:5432/classifier #For LocalDb Use # sqlms.datasources.[0].jdbcUrl=jdbc:postgresql://171.22.247.13:5433/byk?sslmode=require - sqlms.datasources.[0].username=postgres - - sqlms.datasources.[0].password=rootcode + - sqlms.datasources.[0].password=admin - logging.level.org.springframework.boot=INFO ports: - 8082:8082 @@ -148,7 +148,7 @@ services: image: postgres:14.1 environment: - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=rootcode + - POSTGRES_PASSWORD=admin - POSTGRES_DB=classifier ports: - 5433:5432 From 8488c4a41dd2b8a508aec67797982a498294ba87 Mon Sep 17 00:00:00 2001 From: Thirunayan22 Date: Fri, 9 Aug 2024 20:28:03 +0530 Subject: [PATCH 416/582] moved architecture files to the right directory --- architecture-docs/README.md | 8 -------- docs/README.md | 7 ++++++- .../classifier-architecture.drawio | 0 .../Classifier-Dataset-Groups-Architecture.jpg | Bin .../images/Classifier-Integrations-Architecture.jpg | Bin .../images/Classifier-Models-Architecture.jpg | Bin 6 files changed, 6 insertions(+), 9 deletions(-) delete mode 100644 architecture-docs/README.md rename {architecture-docs => docs}/classifier-architecture.drawio (100%) rename {architecture-docs => docs}/images/Classifier-Dataset-Groups-Architecture.jpg (100%) rename {architecture-docs => docs}/images/Classifier-Integrations-Architecture.jpg (100%) rename {architecture-docs => docs}/images/Classifier-Models-Architecture.jpg (100%) diff --git a/architecture-docs/README.md b/architecture-docs/README.md deleted file mode 100644 index 2961f593..00000000 --- a/architecture-docs/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Classifier Architecture -
        - -The classifier-architecture.drawio in this folder is an XML structured document which can be uploaded to the [draw.io](https://app.diagrams.net) platform to view and edit on your own cloud or local storage. - -The **images** folder contains the image snapshots of the architecture diagrams from the draw.io file. - - diff --git a/docs/README.md b/docs/README.md index 02b120db..3465c7b5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1 +1,6 @@ -All docs live here... \ No newline at end of file +# Classifier Architecture +
          + +The classifier-architecture.drawio in this folder is an XML structured document which can be uploaded to the [draw.io](https://app.diagrams.net) platform to view and edit on your own cloud or local storage. + +The **images** folder contains the image snapshots of the architecture diagrams from the draw.io file. diff --git a/architecture-docs/classifier-architecture.drawio b/docs/classifier-architecture.drawio similarity index 100% rename from architecture-docs/classifier-architecture.drawio rename to docs/classifier-architecture.drawio diff --git a/architecture-docs/images/Classifier-Dataset-Groups-Architecture.jpg b/docs/images/Classifier-Dataset-Groups-Architecture.jpg similarity index 100% rename from architecture-docs/images/Classifier-Dataset-Groups-Architecture.jpg rename to docs/images/Classifier-Dataset-Groups-Architecture.jpg diff --git a/architecture-docs/images/Classifier-Integrations-Architecture.jpg b/docs/images/Classifier-Integrations-Architecture.jpg similarity index 100% rename from architecture-docs/images/Classifier-Integrations-Architecture.jpg rename to docs/images/Classifier-Integrations-Architecture.jpg diff --git a/architecture-docs/images/Classifier-Models-Architecture.jpg b/docs/images/Classifier-Models-Architecture.jpg similarity index 100% rename from architecture-docs/images/Classifier-Models-Architecture.jpg rename to docs/images/Classifier-Models-Architecture.jpg From 92e51258878b4f8169b8983d5036cbc0510e6bfd Mon Sep 17 00:00:00 2001 From: Thirunayan22 Date: Fri, 9 Aug 2024 20:38:36 +0530 Subject: [PATCH 417/582] minor fix to resolve merge conflict --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 6d9c5074..dedf5556 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -134,7 +134,7 @@ services: - sqlms.datasources.[0].jdbcUrl=jdbc:postgresql://users_db:5432/classifier #For LocalDb Use # sqlms.datasources.[0].jdbcUrl=jdbc:postgresql://171.22.247.13:5433/byk?sslmode=require - sqlms.datasources.[0].username=postgres - - sqlms.datasources.[0].password=password + - sqlms.datasources.[0].password=admin - logging.level.org.springframework.boot=INFO ports: - 8082:8082 @@ -148,7 +148,7 @@ services: image: postgres:14.1 environment: - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=password + - POSTGRES_PASSWORD=admin - POSTGRES_DB=classifier ports: - 5433:5432 From 062f9124728e6346530e9d6bfe8277e72ef58cb2 Mon Sep 17 00:00:00 2001 From: Thirunayan22 Date: Fri, 9 Aug 2024 20:56:46 +0530 Subject: [PATCH 418/582] added url to figma designs --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 3e8f877f..3d44b64c 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ This repo will primarily contain: 1. Architectural and other documentation (under the documentation folder); 2. Docker Compose file to set up and run Classifier as a fully functional service; +3. You can view the designs for this project in this [Figma file](https://www.figma.com/design/VWoZu2s7auo7YTw49RqNtV/Estonian-Classifier-English-Version?node-id=712-1695&t=cx6ZZVuEkfWqlbZB-1) ## Dev setup From edbcdae5a7583051c183fc82d5b2690968ad4be9 Mon Sep 17 00:00:00 2001 From: Thiru Dinesh <56014038+Thirunayan22@users.noreply.github.com> Date: Fri, 9 Aug 2024 21:05:57 +0530 Subject: [PATCH 419/582] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3d44b64c..a5d2fac6 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ This repo will primarily contain: 1. Architectural and other documentation (under the documentation folder); 2. Docker Compose file to set up and run Classifier as a fully functional service; -3. You can view the designs for this project in this [Figma file](https://www.figma.com/design/VWoZu2s7auo7YTw49RqNtV/Estonian-Classifier-English-Version?node-id=712-1695&t=cx6ZZVuEkfWqlbZB-1) +3. You can view the UI designs for this project in this [Figma file](https://www.figma.com/design/VWoZu2s7auo7YTw49RqNtV/Estonian-Classifier-English-Version?node-id=712-1695&t=cx6ZZVuEkfWqlbZB-1) ## Dev setup From 2939053ce32c486cb2dc78414716a6e75c3f81fe Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Fri, 9 Aug 2024 21:40:27 +0530 Subject: [PATCH 420/582] coorected texts uis --- GUI/src/App.tsx | 13 +- GUI/src/components/MainNavigation/index.tsx | 9 +- .../CorrectedTextsTables.tsx | 333 ++++++++++++++++++ GUI/src/data/mockData.ts | 242 +++++++++++++ GUI/src/pages/CorrectedTexts/index.scss | 0 GUI/src/pages/CorrectedTexts/index.tsx | 100 ++++++ GUI/src/types/correctedTextsTypes.ts | 11 + GUI/src/utils/commonUtilts.ts | 18 + GUI/src/utils/dataTableUtils.ts | 50 +-- GUI/src/utils/endpoints.ts | 12 +- GUI/translations/en/common.json | 19 +- 11 files changed, 776 insertions(+), 31 deletions(-) create mode 100644 GUI/src/components/molecules/CorrectedTextTables/CorrectedTextsTables.tsx create mode 100644 GUI/src/data/mockData.ts create mode 100644 GUI/src/pages/CorrectedTexts/index.scss create mode 100644 GUI/src/pages/CorrectedTexts/index.tsx create mode 100644 GUI/src/types/correctedTextsTypes.ts diff --git a/GUI/src/App.tsx b/GUI/src/App.tsx index 675a8254..40bfff4f 100644 --- a/GUI/src/App.tsx +++ b/GUI/src/App.tsx @@ -12,12 +12,12 @@ import CreateDatasetGroup from 'pages/DatasetGroups/CreateDatasetGroup'; import ViewDatasetGroup from 'pages/DatasetGroups/ViewDatasetGroup'; import StopWords from 'pages/StopWords'; import ValidationSessions from 'pages/ValidationSessions'; -import DataModels from 'pages/DataModels'; +import CorrectedTexts from 'pages/CorrectedTexts'; import CreateDataModel from 'pages/DataModels/CreateDataModel'; import TrainingSessions from 'pages/TrainingSessions'; +import DataModels from 'pages/DataModels'; const App: FC = () => { - useQuery<{ data: { custom_jwt_userinfo: UserInfo }; }>({ @@ -27,7 +27,7 @@ const App: FC = () => { return useStore.getState().setUserInfo(res.response); }, }); - + return ( }> @@ -37,11 +37,14 @@ const App: FC = () => { } /> } /> } /> - } /> + } + />{' '} } /> } /> } /> - + } /> ); diff --git a/GUI/src/components/MainNavigation/index.tsx b/GUI/src/components/MainNavigation/index.tsx index 3136bb95..7528c7fa 100644 --- a/GUI/src/components/MainNavigation/index.tsx +++ b/GUI/src/components/MainNavigation/index.tsx @@ -19,6 +19,7 @@ import apiDev from 'services/api-dev'; import { userManagementEndpoints } from 'utils/endpoints'; import { integrationQueryKeys } from 'utils/queryKeys'; import { ROLES } from 'enums/roles'; +import { TbLetterT } from "react-icons/tb"; const MainNavigation: FC = () => { const { t } = useTranslation(); @@ -74,10 +75,10 @@ const MainNavigation: FC = () => { ], }, { - id: 'incomingTexts', - label: t('menu.incomingTexts'), - path: '/incoming-texts', - icon: , + id: 'correctedTexts', + label: t('menu.correctedTexts'), + path: '/corrected-texts', + icon: , }, { id: 'testModel', diff --git a/GUI/src/components/molecules/CorrectedTextTables/CorrectedTextsTables.tsx b/GUI/src/components/molecules/CorrectedTextTables/CorrectedTextsTables.tsx new file mode 100644 index 00000000..70aad829 --- /dev/null +++ b/GUI/src/components/molecules/CorrectedTextTables/CorrectedTextsTables.tsx @@ -0,0 +1,333 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { generateDynamicColumns } from 'utils/dataTableUtils'; +import SkeletonTable from '../TableSkeleton/TableSkeleton'; +import DataTable from 'components/DataTable'; +import { + ColumnDef, + createColumnHelper, + PaginationState, +} from '@tanstack/react-table'; +import { CorrectedTextResponseType } from 'types/correctedTextsTypes'; +import { useTranslation } from 'react-i18next'; +import { useQuery } from '@tanstack/react-query'; +import mockDev from '../../../services/api-mock'; +import { correctedTextEndpoints } from 'utils/endpoints'; +import { correctedData } from 'data/mockData'; +import { formatDateTime } from 'utils/commonUtilts'; + +const CorrectedTextsTables = ({ + filters, + enableFetch, +}: { + filters: { + platform: string; + sort: string; + }; + enableFetch: boolean; +}) => { + console.log(filters); + const columnHelper = createColumnHelper(); + const [filteredData, setFilteredData] = useState( + [] + ); + const [loading, setLoading] = useState(false); + const { t } = useTranslation(); + const [totalPages, setTotalPages] = useState(1); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 5, + }); + const [platform, setPlatform] = useState('all'); + const [sortType, setSortType] = useState('all'); + + // const { data, isLoading } = useQuery({ + // queryKey: ['correctedText', platform, sortType, pagination.pageIndex], + // queryFn: async () => { + // return await mockDev.get( + // correctedTextEndpoints.GET_CORRECTED_WORDS( + // pagination.pageIndex, + // pagination.pageSize, + // platform, + // sortType + // ) + // ); + // }, + // onSuccess: () => {}, + // }); + const dataColumns = useMemo( + () => [ + columnHelper.accessor('inferenceTime', { + header: () => ( +
          + {t('correctedTexts.inferenceTime') ?? ''} +
          + ), + cell: (props) => ( +
          + + { + formatDateTime(props?.row?.original?.inferenceTime) + .formattedDate + } + + + { + formatDateTime(props?.row?.original?.inferenceTime) + .formattedTime + } + +
          + ), + }), + columnHelper.accessor('platform', { + header: t('correctedTexts.platform') ?? '', + }), + columnHelper.accessor('inferencedText', { + header: t('correctedTexts.text') ?? '', + }), + columnHelper.accessor('predictedLabels', { + header: () => ( +
          + + {t('correctedTexts.predictedHierarchy') ?? ''} + +
          + ), + cell: (props) => ( +
          + {formatArray(props?.row?.original?.predictedLabels)} +
          + ), + }), + columnHelper.accessor('averagePredictedClassesProbability', { + header: () => ( +
          + {t('correctedTexts.predictedConfidenceProbability') ?? ''} +
          + ), + cell: (props) => ( +
          + {props.row.original.averagePredictedClassesProbability}% +
          + ), + meta: { + size: '90%', + }, + }), + columnHelper.accessor('correctedLabels', { + header: () => ( +
          + + {t('correctedTexts.correctedHierarchy') ?? ''} + +
          + ), + cell: (props) => ( +
          + {formatArray(props?.row?.original?.correctedLabels)} +
          + ), + }), + columnHelper.accessor('averageCorrectedClassesProbability', { + header: () => ( +
          + {t('correctedTexts.correctedConfidenceProbability') ?? ''} +
          + ), + cell: (props) => ( +
          + {props.row.original.averageCorrectedClassesProbability}% +
          + ), + }), + ], + [t] + ); + + function paginateDataset(data: any[], pageIndex: number, pageSize: number) { + const startIndex = pageIndex * pageSize; + const endIndex = startIndex + pageSize; + + const pageData = data.slice(startIndex, endIndex); + + setFilteredData(pageData); + } + + const calculateNumberOfPages = (data: any[], pageSize: number) => { + return Math.ceil(data.length / pageSize); + }; + + const formatArray = (array: string[]) => { + return array + .map((item, index) => (index === array.length - 1 ? item : item + ' ->')) + .join(' '); + }; + + const filterItems = useCallback(() => { + if (!enableFetch) { + return; + } + + let newData = correctedData; + + if (filters.platform && filters.platform !== 'all') { + newData = newData.filter((item) => item.platform === filters.platform); + } + + if (filters.sort && filters.sort !== 'all') { + newData = newData.sort((a, b) => { + if (filters.sort === 'asc') { + return a.inferenceTime.localeCompare(b.inferenceTime); + } else if (filters.sort === 'desc') { + return b.inferenceTime.localeCompare(a.inferenceTime); + } + return 0; + }); + } + + setFilteredData(newData); + }, [filters, enableFetch]); + + useEffect(() => { + filterItems(); + }, [filterItems]); + + useEffect(() => { + paginateDataset(correctedData, pagination.pageIndex, pagination.pageSize); + }, []); + return ( +
          +
          + {/* {isLoading && } */} + {/* {!isLoading && ( + []} + pagination={pagination} + setPagination={(state: PaginationState) => { + if ( + state?.pageIndex === pagination?.pageIndex && + state?.pageSize === pagination?.pageSize + ) + return; + setPagination(state); + }} + pagesCount={pagination.pageIndex} + isClientSide={false} + /> + )} */} + + []} + pagination={pagination} + setPagination={(state: PaginationState) => { + if ( + state?.pageIndex === pagination?.pageIndex && + state?.pageSize === pagination?.pageSize + ) + return; + setPagination(state); + }} + pagesCount={calculateNumberOfPages( + correctedData, + pagination.pageSize + )} + isClientSide={false} + /> +
          +
          + ); +}; + +export default CorrectedTextsTables; diff --git a/GUI/src/data/mockData.ts b/GUI/src/data/mockData.ts new file mode 100644 index 00000000..b9cde6d8 --- /dev/null +++ b/GUI/src/data/mockData.ts @@ -0,0 +1,242 @@ +export const correctedData = [ + { + inferenceId: 1234, + itemId: 'alpha1232', + inferenceTime: '23-03-2024-00:23:33', + inferencedText: 'Hello, this is a test email', + predictedLabels: [ + 'Police', + 'Communications', + 'Internal', + 'Recruitment', + 'June Intake', + 'Research & Development', + ], + averagePredictedClassesProbability: 92, + platform: 'jira', + correctedLabels: [ + 'Police', + 'Special Agency', + 'External', + 'Reports', + 'Annual Report', + ], + averageCorrectedClassesProbability: 85, + }, + { + inferenceId: 1234, + itemId: 'alpha1232', + inferenceTime: '23-03-2024-00:23:33', + inferencedText: 'Hello, this is a test email', + predictedLabels: [ + 'Police', + 'Communications', + 'Internal', + 'Recruitment', + 'June Intake', + 'Research & Development', + ], + averagePredictedClassesProbability: 92, + platform: 'outlook', + correctedLabels: [ + 'Police', + 'Special Agency', + 'External', + 'Reports', + 'Annual Report', + ], + averageCorrectedClassesProbability: 85, + }, + { + inferenceId: 1234, + itemId: 'alpha1232', + inferenceTime: '23-03-2024-00:23:33', + inferencedText: 'Hello, this is a test email', + predictedLabels: [ + 'Police', + 'Communications', + 'Internal', + 'Recruitment', + 'June Intake', + 'Research & Development', + ], + averagePredictedClassesProbability: 92, + platform: 'outlook', + correctedLabels: [ + 'Police', + 'Special Agency', + 'External', + 'Reports', + 'Annual Report', + ], + averageCorrectedClassesProbability: 85, + }, + { + inferenceId: 1234, + itemId: 'alpha1232', + inferenceTime: '23-03-2024-00:23:33', + inferencedText: 'Hello, this is a test email', + predictedLabels: [ + 'Police', + 'Communications', + 'Internal', + 'Recruitment', + 'June Intake', + 'Research & Development', + ], + averagePredictedClassesProbability: 92, + platform: 'outlook', + correctedLabels: [ + 'Police', + 'Special Agency', + 'External', + 'Reports', + 'Annual Report', + ], + averageCorrectedClassesProbability: 85, + }, + { + inferenceId: 1234, + itemId: 'alpha1232', + inferenceTime: '23-03-2024-00:23:33', + inferencedText: 'Hello, this is a test email', + predictedLabels: [ + 'Police', + 'Communications', + 'Internal', + 'Recruitment', + 'June Intake', + 'Research & Development', + ], + averagePredictedClassesProbability: 92, + platform: 'outlook', + correctedLabels: [ + 'Police', + 'Special Agency', + 'External', + 'Reports', + 'Annual Report', + ], + averageCorrectedClassesProbability: 85, + }, + { + inferenceId: 1234, + itemId: 'alpha1232', + inferenceTime: '23-03-2024-00:23:33', + inferencedText: 'Hello, this is a test email', + predictedLabels: [ + 'Police', + 'Communications', + 'Internal', + 'Recruitment', + 'June Intake', + 'Research & Development', + ], + averagePredictedClassesProbability: 92, + platform: 'outlook', + correctedLabels: [ + 'Police', + 'Special Agency', + 'External', + 'Reports', + 'Annual Report', + ], + averageCorrectedClassesProbability: 85, + }, + { + inferenceId: 1234, + itemId: 'alpha1232', + inferenceTime: '23-03-2024-00:23:33', + inferencedText: 'Hello, this is a test email', + predictedLabels: [ + 'Police', + 'Communications', + 'Internal', + 'Recruitment', + 'June Intake', + 'Research & Development', + ], + averagePredictedClassesProbability: 92, + platform: 'outlook', + correctedLabels: [ + 'Police', + 'Special Agency', + 'External', + 'Reports', + 'Annual Report', + ], + averageCorrectedClassesProbability: 85, + }, + { + inferenceId: 1234, + itemId: 'alpha1232', + inferenceTime: '23-03-2024-00:23:33', + inferencedText: 'Hello, this is a test email', + predictedLabels: [ + 'Police', + 'Communications', + 'Internal', + 'Recruitment', + 'June Intake', + 'Research & Development', + ], + averagePredictedClassesProbability: 92, + platform: 'outlook', + correctedLabels: [ + 'Police', + 'Special Agency', + 'External', + 'Reports', + 'Annual Report', + ], + averageCorrectedClassesProbability: 85, + }, + { + inferenceId: 1234, + itemId: 'alpha1232', + inferenceTime: '23-03-2024-00:23:33', + inferencedText: 'Hello, this is a test email', + predictedLabels: [ + 'Police', + 'Communications', + 'Internal', + 'Recruitment', + 'June Intake', + 'Research & Development', + ], + averagePredictedClassesProbability: 92, + platform: 'outlook', + correctedLabels: [ + 'Police', + 'Special Agency', + 'External', + 'Reports', + 'Annual Report', + ], + averageCorrectedClassesProbability: 85, + }, + { + inferenceId: 1234, + itemId: 'alpha1232', + inferenceTime: '23-03-2024-00:23:33', + inferencedText: 'Hello, this is a test email', + predictedLabels: [ + 'Police', + 'Communications', + 'Internal', + 'Recruitment', + 'June Intake', + 'Research & Development', + ], + averagePredictedClassesProbability: 92, + platform: 'outlook', + correctedLabels: [ + 'Police', + 'Special Agency', + 'External', + 'Reports', + 'Annual Report', + ], + averageCorrectedClassesProbability: 85, + }, +]; diff --git a/GUI/src/pages/CorrectedTexts/index.scss b/GUI/src/pages/CorrectedTexts/index.scss new file mode 100644 index 00000000..e69de29b diff --git a/GUI/src/pages/CorrectedTexts/index.tsx b/GUI/src/pages/CorrectedTexts/index.tsx new file mode 100644 index 00000000..e07944da --- /dev/null +++ b/GUI/src/pages/CorrectedTexts/index.tsx @@ -0,0 +1,100 @@ +import React, { FC, useState } from 'react'; +import './index.scss'; +import { useTranslation } from 'react-i18next'; +import { ButtonAppearanceTypes } from 'enums/commonEnums'; +import { Button, FormInput, FormSelect } from 'components'; +import { useNavigate } from 'react-router-dom'; +import { formattedArray } from 'utils/commonUtilts'; +import CorrectedTextsTables from 'components/molecules/CorrectedTextTables/CorrectedTextsTables'; + +const CorrectedTexts: FC = () => { + const { t } = useTranslation(); + const navigate = useNavigate(); + + const [enableFetch, setEnableFetch] = useState(true); + + const [filters, setFilters] = useState({ + platform: 'all', + sort: 'asc', + }); + + const handleFilterChange = (name: string, value: string) => { + setEnableFetch(false); + setFilters((prevFilters) => ({ + ...prevFilters, + [name]: value, + })); + }; + return ( +
          +
          +
          {t('correctedTexts.title')}
          + +
          + +
          +
          +
          + + handleFilterChange('platform', selection?.value ?? '') + } + /> + + handleFilterChange('sort', selection?.value ?? '') + } + /> +
          + + +
          + + +
          +
          + ); +}; + +export default CorrectedTexts; diff --git a/GUI/src/types/correctedTextsTypes.ts b/GUI/src/types/correctedTextsTypes.ts new file mode 100644 index 00000000..ddf43d59 --- /dev/null +++ b/GUI/src/types/correctedTextsTypes.ts @@ -0,0 +1,11 @@ +export type CorrectedTextResponseType = { + inferenceId: number; + itemId: string; + inferenceTime: string; + inferencedText: string; + predictedLabels: string[]; + averagePredictedClassesProbability: number; + platform: string; + correctedLabels: string[]; + averageCorrectedClassesProbability: number; +}; diff --git a/GUI/src/utils/commonUtilts.ts b/GUI/src/utils/commonUtilts.ts index 7ee6dcaf..4e4ecfd1 100644 --- a/GUI/src/utils/commonUtilts.ts +++ b/GUI/src/utils/commonUtilts.ts @@ -34,3 +34,21 @@ export const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { export const formatDate = (date: Date, format: string) => { return moment(date).format(format); }; + +export const formatDateTime = (date: string) => { + const format = 'DD-MM-YYYY-HH:mm:ss'; + + // Parse the date string using moment + const momentDate = moment(date, format); + + // Format the date as MM/DD/YYYY + const formattedDate = momentDate.format('M/D/YYYY'); + + // Format the time as h.mm A (AM/PM) + const formattedTime = momentDate.format('h.mm A'); + + return { + formattedDate, + formattedTime, + }; +}; diff --git a/GUI/src/utils/dataTableUtils.ts b/GUI/src/utils/dataTableUtils.ts index 5b757aa6..447ff22e 100644 --- a/GUI/src/utils/dataTableUtils.ts +++ b/GUI/src/utils/dataTableUtils.ts @@ -2,10 +2,10 @@ import { CellContext, createColumnHelper } from '@tanstack/react-table'; export const generateDynamicColumns = ( columnsData: string[], - editView: (props: CellContext) => JSX.Element, - deleteView: (props: CellContext) => JSX.Element + editView?: (props: CellContext) => JSX.Element, + deleteView?: (props: CellContext) => JSX.Element ) => { - const columnHelper = createColumnHelper(); + const columnHelper = createColumnHelper(); // Specify the type for better type checking const dynamicColumns = columnsData?.map((col) => { return columnHelper.accessor(col, { header: col ?? '', @@ -13,22 +13,32 @@ export const generateDynamicColumns = ( }); }); - const staticColumns = [ - columnHelper.display({ - id: 'edit', - cell: editView, - meta: { - size: '1%', - }, - }), - columnHelper.display({ - id: 'delete', - cell: deleteView, - meta: { - size: '1%', - }, - }), - ]; + const staticColumns = []; + + if (editView) { + staticColumns.push( + columnHelper.display({ + id: 'edit', + cell: editView, + meta: { + size: '1%', + }, + }) + ); + } + + if (deleteView) { + staticColumns.push( + columnHelper.display({ + id: 'delete', + cell: deleteView, + meta: { + size: '1%', + }, + }) + ); + } + if (dynamicColumns) return [...dynamicColumns, ...staticColumns]; - else return []; + else return staticColumns; }; diff --git a/GUI/src/utils/endpoints.ts b/GUI/src/utils/endpoints.ts index 85cfa2f4..61b8a886 100644 --- a/GUI/src/utils/endpoints.ts +++ b/GUI/src/utils/endpoints.ts @@ -34,4 +34,14 @@ export const datasetsEndpoints = { DELETE_STOP_WORD: (): string => `/classifier/datasetgroup/delete/stop-words`, IMPORT_STOP_WORDS: (): string => `/datasetgroup/data/import/stop-words`, DELETE_STOP_WORDS: (): string => `/datasetgroup/data/delete/stop-words`, -}; \ No newline at end of file +}; + +export const correctedTextEndpoints = { + GET_CORRECTED_WORDS: ( + pageNumber: number, + pageSize: number, + platform: string, + sortType: string + ) => + `/classifier/correctedtext?pageNum=${pageNumber}&pageSize=${pageSize}&platform=${platform}&sortType=${sortType}`, +}; diff --git a/GUI/translations/en/common.json b/GUI/translations/en/common.json index 5924dcea..2820a0aa 100644 --- a/GUI/translations/en/common.json +++ b/GUI/translations/en/common.json @@ -53,7 +53,7 @@ "trainingSessions": "Training Sessions", "testModel": "Test Model", "stopWords": "Stop Words", - "incomingTexts": "Incoming Texts" + "correctedTexts": "Corrected texts" }, "userManagement": { "title": "User Management", @@ -297,6 +297,23 @@ "inprogress": "Validation in-Progress", "fail": "Validation failed because {{class}} class found in the {{column}} column does not exist in hierarchy" }, + + "correctedTexts": { + "title": "Corrected Texts", + "export": "Export Data", + "searchIncomingText": "Search incoming texts", + "filterAsc": "Filter by data created - Ascending", + "filterDesc": "Filter by data created - Descending", + "platform": "Platform", + "dateAndTime": "Date & Time", + "inferenceTime": "Inference Time", + "text": "Text", + "predictedHierarchy": "Predicted Class Hierarchy", + "predictedConfidenceProbability": "Predicted Classes Average Confidence Probability", + "correctedHierarchy": "Corrected Class Hierarchy", + "correctedConfidenceProbability": "Corrected Classes Average Confidence Probability" + }, + "dataModels": { "productionModels": "Production Models", "dataModels": "Data Models", From 730094221742cf6b12cd5c557129da8a9aea4d1b Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Fri, 9 Aug 2024 22:33:35 +0530 Subject: [PATCH 421/582] Read Me: add success response body --- .../datamodel/deployment/outlook/validate.yml | 51 ++++++++++++++----- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/deployment/outlook/validate.yml b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/deployment/outlook/validate.yml index ebd3bc89..01260256 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/deployment/outlook/validate.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/deployment/outlook/validate.yml @@ -15,7 +15,7 @@ declaration: extract_request: assign: model_id: ${incoming.body.modelId} - next: get_dataset_group_id_by_model_id + next: get_token_info get_token_info: call: http.get @@ -24,7 +24,13 @@ get_token_info: headers: cookie: ${incoming.headers.cookie} result: res - next: assign_access_token + next: check_token_status + +check_token_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: assign_access_token + next: assign_fail_response assign_access_token: assign: @@ -81,17 +87,34 @@ check_dataset_group_exist: assign_dataset_class_hierarchy: assign: class_hierarchy: ${JSON.parse(res_dataset.response.body[0].classHierarchy.value)} - next: get_outlook_folder_hierarchy + next: assign_success_response -get_outlook_folder_hierarchy: - call: http.post - args: - url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/validate_outlook_class_hierarchy" - headers: - type: json - result: result - next: output_val +assign_success_response: + assign: + format_res: { + modelId: '${model_id}', + outlook_access_token: '${access_token}', + class_hierarchy: '${class_hierarchy}', + operationSuccessful: true, + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + modelId: '${model_id}', + outlook_access_token: '', + class_hierarchy: '', + operationSuccessful: false, + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end -output_val: - return: ${result} - end: next +return_bad_request: + status: 400 + return: ${format_res} + next: end \ No newline at end of file From 9f6b91a874aa4791f332d0e1023224aaf5927a82 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Sat, 10 Aug 2024 14:46:40 +0530 Subject: [PATCH 422/582] ESCLASS-169: implement create ,update,get inference API's --- .../classifier-script-v3-integrations.sql | 29 ++-- DSL/Resql/get-basic-input-metadata-by-id.sql | 2 + DSL/Resql/get-input-metadata-exits-by-id.sql | 2 + DSL/Resql/insert-input-metadata.sql | 21 +++ DSL/Resql/update-input-metadata.sql | 7 + .../DSL/GET/classifier/inference/exist.yml | 79 ++++++++++ .../DSL/POST/classifier/inference/create.yml | 127 ++++++++++++++++ .../classifier/inference/update/corrected.yml | 141 ++++++++++++++++++ 8 files changed, 392 insertions(+), 16 deletions(-) create mode 100644 DSL/Resql/get-basic-input-metadata-by-id.sql create mode 100644 DSL/Resql/get-input-metadata-exits-by-id.sql create mode 100644 DSL/Resql/insert-input-metadata.sql create mode 100644 DSL/Resql/update-input-metadata.sql create mode 100644 DSL/Ruuter.private/DSL/GET/classifier/inference/exist.yml create mode 100644 DSL/Ruuter.private/DSL/POST/classifier/inference/create.yml create mode 100644 DSL/Ruuter.private/DSL/POST/classifier/inference/update/corrected.yml diff --git a/DSL/Liquibase/changelog/classifier-script-v3-integrations.sql b/DSL/Liquibase/changelog/classifier-script-v3-integrations.sql index fb92db69..236722af 100644 --- a/DSL/Liquibase/changelog/classifier-script-v3-integrations.sql +++ b/DSL/Liquibase/changelog/classifier-script-v3-integrations.sql @@ -21,22 +21,19 @@ VALUES ('PINAL', FALSE, NULL, NULL); -- changeset kalsara Magamage:classifier-script-v3-changeset4 -CREATE TABLE public."jira" ( +CREATE TABLE public."input" ( id int8 NOT NULL GENERATED BY DEFAULT AS IDENTITY, - input_id TEXT DEFAULT NULL, - anonym_text TEXT DEFAULT NULL, - corrected BOOLEAN NOT NULL DEFAULT FALSE, - predicted_labels TEXT[] DEFAULT NULL, - corrected_labels TEXT[] DEFAULT NULL, - CONSTRAINT jira_pkey PRIMARY KEY (id) -); - -CREATE TABLE public."outlook" ( - id int8 NOT NULL GENERATED BY DEFAULT AS IDENTITY, - input_id TEXT DEFAULT NULL, - anonym_text VARCHAR(50) DEFAULT NULL, - corrected BOOLEAN NOT NULL DEFAULT FALSE, + inference_time_stamp TIMESTAMP WITH TIME ZONE, + input_id TEXT NOT NULL, + inference_text TEXT DEFAULT NULL, + predicted_labels JSONB, + corrected_labels JSONB, + is_corrected BOOLEAN NOT NULL DEFAULT FALSE, + average_predicted_classes_probability INT, + average_corrected_classes_probability INT, primary_folder_id TEXT DEFAULT NULL, - parent_folder_ids TEXT[] DEFAULT NULL, - CONSTRAINT outlook_pkey PRIMARY KEY (id) + all_class_predicted_probabilities JSONB, + platform platform, + CONSTRAINT input_pkey PRIMARY KEY (id), + CONSTRAINT input_id_unique UNIQUE (input_id) ); diff --git a/DSL/Resql/get-basic-input-metadata-by-id.sql b/DSL/Resql/get-basic-input-metadata-by-id.sql new file mode 100644 index 00000000..1bea93f0 --- /dev/null +++ b/DSL/Resql/get-basic-input-metadata-by-id.sql @@ -0,0 +1,2 @@ +SELECT id, input_id, platform +FROM "input" WHERE id =:id; \ No newline at end of file diff --git a/DSL/Resql/get-input-metadata-exits-by-id.sql b/DSL/Resql/get-input-metadata-exits-by-id.sql new file mode 100644 index 00000000..8de7e2e9 --- /dev/null +++ b/DSL/Resql/get-input-metadata-exits-by-id.sql @@ -0,0 +1,2 @@ +SELECT id +FROM "input" WHERE id =:id; \ No newline at end of file diff --git a/DSL/Resql/insert-input-metadata.sql b/DSL/Resql/insert-input-metadata.sql new file mode 100644 index 00000000..be0edb8d --- /dev/null +++ b/DSL/Resql/insert-input-metadata.sql @@ -0,0 +1,21 @@ +INSERT INTO "input" ( + input_id, + inference_time_stamp, + inference_text, + predicted_labels, + average_predicted_classes_probability, + platform, + all_class_predicted_probabilities, + primary_folder_id +) +VALUES ( + :input_id, + :inference_time_stamp::timestamp with time zone, + :inference_text, + :predicted_labels::jsonb, + :average_predicted_classes_probability, + :platform::platform, + :all_class_predicted_probabilities::jsonb, + :primary_folder_id +) +RETURNING id; diff --git a/DSL/Resql/update-input-metadata.sql b/DSL/Resql/update-input-metadata.sql new file mode 100644 index 00000000..d1d1f695 --- /dev/null +++ b/DSL/Resql/update-input-metadata.sql @@ -0,0 +1,7 @@ +UPDATE input +SET + is_corrected = :is_corrected, + corrected_labels = :corrected_labels::JSONB, + average_corrected_classes_probability = :average_corrected_classes_probability, + primary_folder_id = :primary_folder_id +WHERE id = :id; diff --git a/DSL/Ruuter.private/DSL/GET/classifier/inference/exist.yml b/DSL/Ruuter.private/DSL/GET/classifier/inference/exist.yml new file mode 100644 index 00000000..39c3cf2e --- /dev/null +++ b/DSL/Ruuter.private/DSL/GET/classifier/inference/exist.yml @@ -0,0 +1,79 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'EXIST'" + method: get + accepts: json + returns: json + namespace: classifier + allowlist: + params: + - field: inferenceId + type: number + description: "Parameter 'inferenceId'" + +extract_data: + assign: + inference_id: ${Number(incoming.params.inferenceId)} + exist: false + next: get_input_metadata_by_id + +get_input_metadata_by_id: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-basic-input-metadata-by-id" + body: + id: ${inference_id} + result: res_input_id + next: check_input_metadata_status + +check_input_metadata_status: + switch: + - condition: ${200 <= res_input_id.response.statusCodeValue && res_input_id.response.statusCodeValue < 300} + next: check_input_metadata_exist + next: assign_fail_response + +check_input_metadata_exist: + switch: + - condition: ${res_input_id.response.body.length>0} + next: assign_exist_data + next: assign_fail_response + +assign_exist_data: + assign: + exist : true + inference_id: ${Number(incoming.params.inferenceId)} + value: [{ + inferenceId: '${res_input_id.response.body[0].id}', + inputId: '${res_input_id.response.body[0].inputId}', + platform: '${res_input_id.response.body[0].platform}' + }] + next: assign_success_response + +assign_success_response: + assign: + format_res: { + operationSuccessful: true, + exist: '${exist}', + data: '${value}' + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + operationSuccessful: false, + exist: '${exist}', + data: '${[]}' + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/POST/classifier/inference/create.yml b/DSL/Ruuter.private/DSL/POST/classifier/inference/create.yml new file mode 100644 index 00000000..e2f175ff --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/classifier/inference/create.yml @@ -0,0 +1,127 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'CREATE'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: inputId + type: string + description: "Body field 'inputId'" + - field: inferenceText + type: string + description: "Body field 'inferenceText'" + - field: predictedLabels + type: json + description: "Body field 'predictedLabels'" + - field: averagePredictedClassesProbability + type: int + description: "Body field 'averagePredictedClassesProbability'" + - field: platform + type: string + description: "Body field 'platform'" + - field: allClassPredictedProbabilities + type: json + description: "Body field 'allClassPredictedProbabilities'" + - field: primaryFolderId + type: string + description: "Body field 'primaryFolderId'" + headers: + - field: cookie + type: string + description: "Cookie field" + +extract_request_data: + assign: + input_id: ${incoming.body.inputId} + inference_text: ${incoming.body.inferenceText} + predicted_labels: ${incoming.body.predictedLabels} + average_predicted_classes_probability: ${incoming.body.averagePredictedClassesProbability} + platform: ${incoming.body.platform} + all_class_predicted_probabilities: ${incoming.body.allClassPredictedProbabilities} + primary_folder_id: ${incoming.body.primaryFolderId} + next: check_for_request_data + +check_for_request_data: + switch: + - condition: ${input_id !== null || inference_text !== null || predicted_labels !== null || average_predicted_classes_probability !== null || platform !== null || all_class_predicted_probabilities !== null} + next: check_platform_data + next: return_incorrect_request + +check_platform_data: + switch: + - condition: ${platform == 'OUTLOOK'} + next: check_primary_folder_exist + next: get_epoch_date + +check_primary_folder_exist: + switch: + - condition: ${primary_folder_id !== null} + next: get_epoch_date + next: return_primary_folder_not_found + +get_epoch_date: + assign: + current_epoch: ${Date.now()} + next: create_input_metadata + +create_input_metadata: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/insert-input-metadata" + body: + input_id: ${input_id} + inference_time_stamp: ${new Date(current_epoch).toISOString()} + inference_text: ${inference_text} + predicted_labels: ${JSON.stringify(predicted_labels)} + average_predicted_classes_probability: ${average_predicted_classes_probability} + platform: ${platform} + all_class_predicted_probabilities: ${JSON.stringify(all_class_predicted_probabilities)} + primary_folder_id: ${platform !== 'OUTLOOK' ? '' :primary_folder_id} + result: res_input + next: check_status + +check_status: + switch: + - condition: ${200 <= res_input.response.statusCodeValue && res_input.response.statusCodeValue < 300} + next: assign_success_response + next: assign_fail_response + +assign_success_response: + assign: + format_res: { + inferenceId: '${res_input.response.body[0].id}', + operationSuccessful: true, + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + inferenceId: '', + operationSuccessful: false, + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end + +return_incorrect_request: + status: 400 + return: 'Missing Required Fields' + next: end + +return_primary_folder_not_found: + status: 400 + return: 'Missing Primary Folder Id' + next: end diff --git a/DSL/Ruuter.private/DSL/POST/classifier/inference/update/corrected.yml b/DSL/Ruuter.private/DSL/POST/classifier/inference/update/corrected.yml new file mode 100644 index 00000000..45e7deb4 --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/classifier/inference/update/corrected.yml @@ -0,0 +1,141 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'CORRECTED'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: inferenceId + type: number + description: "Body field 'inferenceId'" + - field: isCorrected + type: boolean + description: "Body field 'isCorrected'" + - field: correctedLabels + type: json + description: "Body field 'correctedLabels'" + - field: averageCorrectedClassesProbability + type: int + description: "Body field 'averageCorrectedClassesProbability'" + - field: primaryFolderId + type: string + description: "Body field 'primaryFolderId'" + - field: platform + type: string + description: "Body field 'platform'" + headers: + - field: cookie + type: string + description: "Cookie field" + +extract_request_data: + assign: + inference_id: ${incoming.body.inferenceId} + is_corrected: ${incoming.body.isCorrected} + corrected_labels: ${incoming.body.correctedLabels} + average_corrected_classes_probability: ${incoming.body.averageCorrectedClassesProbability} + primary_folder_id: ${incoming.body.primaryFolderId} + platform: ${incoming.body.platform} + next: check_for_request_data + +check_for_request_data: + switch: + - condition: ${inference_id !== null || corrected_labels !== null || average_corrected_classes_probability !== null || platform !== null} + next: check_platform_data + next: return_incorrect_request + +check_platform_data: + switch: + - condition: ${platform == 'OUTLOOK'} + next: check_primary_folder_exist + next: get_input_metadata_by_id + +check_primary_folder_exist: + switch: + - condition: ${primary_folder_id !== null} + next: get_input_metadata_by_id + next: return_primary_folder_not_found + +get_input_metadata_by_id: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-input-metadata-exits-by-id" + body: + id: ${inference_id} + result: res_input_id + next: check_input_metadata_status + +check_input_metadata_status: + switch: + - condition: ${200 <= res_input_id.response.statusCodeValue && res_input_id.response.statusCodeValue < 300} + next: check_input_metadata_exist + next: assign_fail_response + +check_input_metadata_exist: + switch: + - condition: ${res_input_id.response.body.length>0} + next: update_input_metadata + next: return_inference_data_not_found + +update_input_metadata: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/update-input-metadata" + body: + id: ${inference_id} + is_corrected: ${is_corrected} + corrected_labels: ${JSON.stringify(corrected_labels)} + average_corrected_classes_probability: ${average_corrected_classes_probability} + primary_folder_id: ${platform !== 'OUTLOOK' ? '' :primary_folder_id} + result: res_input + next: check_status + +check_status: + switch: + - condition: ${200 <= res_input.response.statusCodeValue && res_input.response.statusCodeValue < 300} + next: assign_success_response + next: assign_fail_response + +assign_success_response: + assign: + format_res: { + inferenceId: '${inference_id}', + operationSuccessful: true, + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + inferenceId: '${inference_id}', + operationSuccessful: false, + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end + +return_incorrect_request: + status: 400 + return: 'Missing Required Fields' + next: end + +return_primary_folder_not_found: + status: 400 + return: 'Missing Primary Folder Id' + next: end + +return_inference_data_not_found: + status: 400 + return: 'Inference data not found' + next: end From f0410f9d68e8fee21821021bbf3f02c1ba9e0dc0 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Sat, 10 Aug 2024 20:35:10 +0530 Subject: [PATCH 423/582] ESCLASS-169: implement get corrected Text data with filters and get corrected text by input id API's --- .../get-corrected-input-metadata-by-id.sql | 11 +++ ...get-paginated-corrected-input-metadata.sql | 18 ++++ .../inference/corrected-metadata.yml | 88 +++++++++++++++++++ .../classifier/inference/corrected-text.yml | 73 +++++++++++++++ 4 files changed, 190 insertions(+) create mode 100644 DSL/Resql/get-corrected-input-metadata-by-id.sql create mode 100644 DSL/Resql/get-paginated-corrected-input-metadata.sql create mode 100644 DSL/Ruuter.private/DSL/GET/classifier/inference/corrected-metadata.yml create mode 100644 DSL/Ruuter.private/DSL/GET/classifier/inference/corrected-text.yml diff --git a/DSL/Resql/get-corrected-input-metadata-by-id.sql b/DSL/Resql/get-corrected-input-metadata-by-id.sql new file mode 100644 index 00000000..c9e460eb --- /dev/null +++ b/DSL/Resql/get-corrected-input-metadata-by-id.sql @@ -0,0 +1,11 @@ +SELECT id AS inference_id, + input_id, + inference_time_stamp, + inference_text, + jsonb_pretty(predicted_labels) AS predicted_labels, + jsonb_pretty(corrected_labels) AS corrected_labels, + average_predicted_classes_probability, + average_corrected_classes_probability, + platform +FROM "input" +WHERE is_corrected = true AND input_id=:input_id diff --git a/DSL/Resql/get-paginated-corrected-input-metadata.sql b/DSL/Resql/get-paginated-corrected-input-metadata.sql new file mode 100644 index 00000000..3b907541 --- /dev/null +++ b/DSL/Resql/get-paginated-corrected-input-metadata.sql @@ -0,0 +1,18 @@ +SELECT id AS inference_id, + input_id, + inference_time_stamp, + inference_text, + jsonb_pretty(predicted_labels) AS predicted_labels, + jsonb_pretty(corrected_labels) AS corrected_labels, + average_predicted_classes_probability, + average_corrected_classes_probability, + platform, + CEIL(COUNT(*) OVER() / :page_size::DECIMAL) AS total_pages +FROM "input" +WHERE + (is_corrected = true) + AND (:platform = 'all' OR platform = :platform::platform) +ORDER BY + CASE WHEN :sorting = 'asc' THEN inference_time_stamp END ASC, + CASE WHEN :sorting = 'desc' THEN inference_time_stamp END DESC +OFFSET ((GREATEST(:page, 1) - 1) * :page_size) LIMIT :page_size; diff --git a/DSL/Ruuter.private/DSL/GET/classifier/inference/corrected-metadata.yml b/DSL/Ruuter.private/DSL/GET/classifier/inference/corrected-metadata.yml new file mode 100644 index 00000000..5c929201 --- /dev/null +++ b/DSL/Ruuter.private/DSL/GET/classifier/inference/corrected-metadata.yml @@ -0,0 +1,88 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'CORRECTED-METADATA'" + method: get + accepts: json + returns: json + namespace: classifier + allowlist: + params: + - field: pageNum + type: number + description: "Parameter 'pageNum'" + - field: pageSize + type: number + description: "Parameter 'pageSize'" + - field: platform + type: string + description: "Parameter 'platform'" + - field: sortType + type: string + description: "Parameter 'sortType'" + +extract_data: + assign: + page_num: ${Number(incoming.params.pageNum)} + page_size: ${Number(incoming.params.pageSize)} + platform: ${incoming.params.platform} + sort_type: ${incoming.params.sortType} + next: get_corrected_input_metadata + +get_corrected_input_metadata: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-paginated-corrected-input-metadata" + body: + page: ${page_num} + page_size: ${page_size} + platform: ${platform} + sorting: ${sort_type} + result: res_corrected + next: check_input_metadata_status + +check_input_metadata_status: + switch: + - condition: ${200 <= res_corrected.response.statusCodeValue && res_corrected.response.statusCodeValue < 300} + next: check_input_metadata_exist + next: assign_fail_response + +check_input_metadata_exist: + switch: + - condition: ${res_corrected.response.body.length>0} + next: assign_success_response + next: assign_empty_response + +assign_success_response: + assign: + format_res: { + operationSuccessful: true, + data: '${res_corrected.response.body}' + } + next: return_ok + +assign_empty_response: + assign: + format_res: { + operationSuccessful: true, + data: '${[]}' + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + operationSuccessful: false, + data: '${[]}' + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/GET/classifier/inference/corrected-text.yml b/DSL/Ruuter.private/DSL/GET/classifier/inference/corrected-text.yml new file mode 100644 index 00000000..34afd10a --- /dev/null +++ b/DSL/Ruuter.private/DSL/GET/classifier/inference/corrected-text.yml @@ -0,0 +1,73 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'CORRECTED-TEXT'" + method: get + accepts: json + returns: json + namespace: classifier + allowlist: + params: + - field: inputId + type: string + description: "Parameter 'inputId'" + +extract_data: + assign: + input_id: ${incoming.params.inputId} + next: get_corrected_input_metadata_by_id + +get_corrected_input_metadata_by_id: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-corrected-input-metadata-by-id" + body: + input_id: ${input_id} + result: res_corrected + next: check_input_metadata_status + +check_input_metadata_status: + switch: + - condition: ${200 <= res_corrected.response.statusCodeValue && res_corrected.response.statusCodeValue < 300} + next: check_input_metadata_exist + next: assign_fail_response + +check_input_metadata_exist: + switch: + - condition: ${res_corrected.response.body.length>0} + next: assign_success_response + next: assign_empty_response + +assign_success_response: + assign: + format_res: { + operationSuccessful: true, + data: '${res_corrected.response.body}' + } + next: return_ok + +assign_empty_response: + assign: + format_res: { + operationSuccessful: true, + data: '${[]}' + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + operationSuccessful: false, + data: '${[]}' + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end \ No newline at end of file From a3d27b9f9167b979825bd0d6683d8e9771d326b4 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Sun, 11 Aug 2024 10:04:15 +0530 Subject: [PATCH 424/582] ESCLASS-169: implement create inference flow update jira and outlook labels and folder flows --- DSL/Resql/get-jira-input-row-data.sql | 6 +-- DSL/Resql/get-outlook-input-row-data.sql | 4 +- .../DSL/POST/classifier/inference/create.yml | 44 +++++++++++++++++-- .../integration/jira/cloud/label.yml | 30 ++----------- 4 files changed, 49 insertions(+), 35 deletions(-) diff --git a/DSL/Resql/get-jira-input-row-data.sql b/DSL/Resql/get-jira-input-row-data.sql index 6f5e4ea9..f4d3c2c0 100644 --- a/DSL/Resql/get-jira-input-row-data.sql +++ b/DSL/Resql/get-jira-input-row-data.sql @@ -1,3 +1,3 @@ -SELECT corrected_labels -FROM jira -WHERE input_id=:inputId; +SELECT predicted_labels,corrected_labels +FROM "input" +WHERE input_id=:inputId AND platform='JIRA'; diff --git a/DSL/Resql/get-outlook-input-row-data.sql b/DSL/Resql/get-outlook-input-row-data.sql index e90ea043..7393886b 100644 --- a/DSL/Resql/get-outlook-input-row-data.sql +++ b/DSL/Resql/get-outlook-input-row-data.sql @@ -1,3 +1,3 @@ SELECT primary_folder_id -FROM outlook -WHERE input_id=:inputId; +FROM "input" +WHERE input_id=:inputId AND platform='OUTLOOK'; diff --git a/DSL/Ruuter.private/DSL/POST/classifier/inference/create.yml b/DSL/Ruuter.private/DSL/POST/classifier/inference/create.yml index e2f175ff..4dadb5c6 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/inference/create.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/inference/create.yml @@ -38,7 +38,7 @@ extract_request_data: assign: input_id: ${incoming.body.inputId} inference_text: ${incoming.body.inferenceText} - predicted_labels: ${incoming.body.predictedLabels} + predicted_labels_org: ${incoming.body.predictedLabels} average_predicted_classes_probability: ${incoming.body.averagePredictedClassesProbability} platform: ${incoming.body.platform} all_class_predicted_probabilities: ${incoming.body.allClassPredictedProbabilities} @@ -47,7 +47,7 @@ extract_request_data: check_for_request_data: switch: - - condition: ${input_id !== null || inference_text !== null || predicted_labels !== null || average_predicted_classes_probability !== null || platform !== null || all_class_predicted_probabilities !== null} + - condition: ${input_id !== null || inference_text !== null || predicted_labels_org !== null || average_predicted_classes_probability !== null || platform !== null || all_class_predicted_probabilities !== null} next: check_platform_data next: return_incorrect_request @@ -76,7 +76,7 @@ create_input_metadata: input_id: ${input_id} inference_time_stamp: ${new Date(current_epoch).toISOString()} inference_text: ${inference_text} - predicted_labels: ${JSON.stringify(predicted_labels)} + predicted_labels: ${JSON.stringify(predicted_labels_org)} average_predicted_classes_probability: ${average_predicted_classes_probability} platform: ${platform} all_class_predicted_probabilities: ${JSON.stringify(all_class_predicted_probabilities)} @@ -87,6 +87,44 @@ create_input_metadata: check_status: switch: - condition: ${200 <= res_input.response.statusCodeValue && res_input.response.statusCodeValue < 300} + next: check_input_type + next: assign_fail_response + +check_input_type: + switch: + - condition: ${platform == 'OUTLOOK'} + next: outlook_flow + - condition: ${platform == 'JIRA'} + next: jira_flow + next: assign_success_response + +outlook_flow: + call: http.post + args: + url: "[#CLASSIFIER_RUUTER_PRIVATE]/classifier/integration/outlook/label" + headers: + cookie: ${incoming.headers.cookie} + body: + mailId: ${input_id} + folderId: ${primary_folder_id} + result: res_label + next: check_label_status + +jira_flow: + call: http.post + args: + url: "[#CLASSIFIER_RUUTER_PRIVATE]/classifier/integration/jira/cloud/label" + headers: + cookie: ${incoming.headers.cookie} + body: + issueKey: ${input_id} + labels: ${predicted_labels_org} + result: res_label + next: check_label_status + +check_label_status: + switch: + - condition: ${200 <= res_label.response.statusCodeValue && res_label.response.statusCodeValue < 300} next: assign_success_response next: assign_fail_response diff --git a/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/label.yml b/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/label.yml index 2487b060..fb91746b 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/label.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/label.yml @@ -37,37 +37,13 @@ get_auth_header: username: "[#JIRA_USERNAME]" token: "[#JIRA_API_TOKEN]" result: auth_header - next: get_jira_issue_info - -get_jira_issue_info: - call: http.get - args: - url: "[#JIRA_CLOUD_DOMAIN]/rest/api/3/issue/${issue_key}" - headers: - Authorization: ${auth_header.response.body.val} - result: jira_issue_info - next: assign_existing_labels - -assign_existing_labels: - assign: - existing_label_list: ${jira_issue_info.response.body.fields.labels} - next: merge_labels - -merge_labels: - call: http.post - args: - url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/return_label_field_array" - headers: - type: json - body: - labels: ${label_list} - existing_labels: ${existing_label_list} - result: res next: set_data set_data: assign: - all_labels: ${res.response.body} + all_labels: { + labels: '${label_list}' + } next: update_jira_issue update_jira_issue: From a1b01f71f51e90024edd2cfa1b8019bfff249cfa Mon Sep 17 00:00:00 2001 From: Thiru Dinesh <56014038+Thirunayan22@users.noreply.github.com> Date: Sun, 11 Aug 2024 14:21:57 +0530 Subject: [PATCH 425/582] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a5d2fac6..272e7786 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Classifier -The classifier is an open-source model training platform in which can integrated with JIRA and Outlook to deploy custom classification models to label emails or JIRA tickets. +The classifier is an open-source model training platform which can be integrated with JIRA and Outlook to deploy custom classification models to classify and label incoming emails or JIRA tickets. # Scope From 3e07ee0733ab3a3c97c9548e0f3a8fd9a8221673 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Sun, 11 Aug 2024 18:04:29 +0530 Subject: [PATCH 426/582] annonymizer fully completed --- anonymizer/Dockerfile | 15 ++++++++++ anonymizer/__init__.py | 0 anonymizer/anonymizer_api.py | 47 +++++++++++++++++++++++++++++++ anonymizer/fake_replacements.py | 48 ++++++++++++++++++++++++++++++++ anonymizer/language_detection.py | 9 ++++++ anonymizer/ner.py | 8 ++++++ anonymizer/requirements.txt | 37 ++++++++++++++++++++++++ anonymizer/text_processing.py | 15 ++++++++++ docker-compose.yml | 10 +++++++ 9 files changed, 189 insertions(+) create mode 100644 anonymizer/Dockerfile create mode 100644 anonymizer/__init__.py create mode 100644 anonymizer/anonymizer_api.py create mode 100644 anonymizer/fake_replacements.py create mode 100644 anonymizer/language_detection.py create mode 100644 anonymizer/ner.py create mode 100644 anonymizer/requirements.txt create mode 100644 anonymizer/text_processing.py diff --git a/anonymizer/Dockerfile b/anonymizer/Dockerfile new file mode 100644 index 00000000..18d46e77 --- /dev/null +++ b/anonymizer/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3.12.4-bookworm + +WORKDIR /app + +COPY requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +RUN python -c "from transformers import pipeline; pipeline('ner', model='xlm-roberta-large-finetuned-conll03-english')" + +COPY . . + +EXPOSE 8010 + +CMD ["uvicorn", "anonymizer_api:app", "--host", "0.0.0.0", "--port", "8010"] \ No newline at end of file diff --git a/anonymizer/__init__.py b/anonymizer/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/anonymizer/anonymizer_api.py b/anonymizer/anonymizer_api.py new file mode 100644 index 00000000..c68b9c1e --- /dev/null +++ b/anonymizer/anonymizer_api.py @@ -0,0 +1,47 @@ +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel +from language_detection import LanguageDetector +from ner import NERProcessor +from text_processing import TextProcessor +from fake_replacements import FakeReplacer + +app = FastAPI() + +class InputText(BaseModel): + text: str + +class OutputText(BaseModel): + original_text: str + processed_text: str + status: bool + +ner_processor = NERProcessor() + +@app.post("/process_text", response_model=OutputText) +async def process_text(input_text: InputText): + try: + text_chunks = TextProcessor.split_text(input_text.text, 2000) + processed_chunks = [] + + for chunk in text_chunks: + entities = ner_processor.identify_entities(chunk) + processed_chunk = FakeReplacer.replace_entities(chunk, entities) + processed_chunks.append(processed_chunk) + + processed_text = TextProcessor.combine_chunks(processed_chunks) + + return OutputText( + original_text=input_text.text, + processed_text=processed_text, + status=True + ) + except Exception as e: + return OutputText( + original_text=input_text.text, + processed_text=str(e), + status=False + ) + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8010) diff --git a/anonymizer/fake_replacements.py b/anonymizer/fake_replacements.py new file mode 100644 index 00000000..6c04768d --- /dev/null +++ b/anonymizer/fake_replacements.py @@ -0,0 +1,48 @@ +import faker + +fake = faker.Faker() + +class FakeReplacer: + @staticmethod + def replace_entities(text: str, entities: list): + print(f"Original text: {text}") + print(f"entities: {entities}") + + replacements = [] + + i = 0 + while i < len(entities): + entity_text = entities[i]['word'].replace('▁', '') + entity_type = entities[i]['entity'][2:] + start_pos = entities[i]['start'] + end_pos = entities[i]['end'] + j = i + 1 + + while j < len(entities) and entities[j]['entity'].startswith('I-') and entities[j]['entity'][2:] == entity_type: + if entities[j]['start'] == end_pos: + entity_text += entities[j]['word'].replace('▁', '') + end_pos = entities[j]['end'] + else: + break + j += 1 + + replacements.append((start_pos, end_pos, entity_text, entity_type)) + i = j + + print("Replacements to be made:") + for start_pos, end_pos, entity_text, entity_type in replacements: + print(f"Entity: {entity_text}, Type: {entity_type}, Start: {start_pos}, End: {end_pos}") + + for start_pos, end_pos, entity_text, entity_type in sorted(replacements, reverse=True): + if entity_type == 'PER': + fake_name = fake.name() + text = text[:start_pos] + fake_name + text[end_pos:] + elif entity_type == 'ORG': + fake_org = fake.company() + text = text[:start_pos] + fake_org + text[end_pos:] + elif entity_type == 'LOC': + fake_loc = fake.city() + text = text[:start_pos] + fake_loc + text[end_pos:] + + print(f"Processed text: {text}") + return text \ No newline at end of file diff --git a/anonymizer/language_detection.py b/anonymizer/language_detection.py new file mode 100644 index 00000000..bd3502c6 --- /dev/null +++ b/anonymizer/language_detection.py @@ -0,0 +1,9 @@ +from langdetect import detect, LangDetectException + +class LanguageDetector: + @staticmethod + def detect_language(text: str) -> str: + try: + return detect(text) + except LangDetectException: + return "unknown" diff --git a/anonymizer/ner.py b/anonymizer/ner.py new file mode 100644 index 00000000..b504213b --- /dev/null +++ b/anonymizer/ner.py @@ -0,0 +1,8 @@ +from transformers import pipeline + +class NERProcessor: + def __init__(self): + self.model = pipeline("ner", model="xlm-roberta-large-finetuned-conll03-english") + + def identify_entities(self, text: str): + return self.model(text) diff --git a/anonymizer/requirements.txt b/anonymizer/requirements.txt new file mode 100644 index 00000000..94cfdf4a --- /dev/null +++ b/anonymizer/requirements.txt @@ -0,0 +1,37 @@ +annotated-types==0.7.0 +anyio==4.4.0 +certifi==2024.7.4 +charset-normalizer==3.3.2 +click==8.1.7 +colorama==0.4.6 +Faker==26.2.0 +fastapi==0.112.0 +filelock==3.15.4 +fsspec==2024.6.1 +h11==0.14.0 +huggingface-hub==0.24.5 +idna==3.7 +langdetect==1.0.9 +numpy==2.0.1 +packaging==24.1 +pydantic==2.8.2 +pydantic_core==2.20.1 +python-dateutil==2.9.0.post0 +PyYAML==6.0.1 +regex==2024.7.24 +requests==2.32.3 +safetensors==0.4.4 +setuptools==72.1.0 +six==1.16.0 +sniffio==1.3.1 +starlette==0.37.2 +tokenizers==0.19.1 +torch==2.4.0 +torchaudio==2.4.0 +torchvision==0.19.0 +tqdm==4.66.5 +transformers==4.44.0 +typing_extensions==4.12.2 +urllib3==2.2.2 +uvicorn==0.30.5 +wheel==0.43.0 diff --git a/anonymizer/text_processing.py b/anonymizer/text_processing.py new file mode 100644 index 00000000..893a981d --- /dev/null +++ b/anonymizer/text_processing.py @@ -0,0 +1,15 @@ +class TextProcessor: + @staticmethod + def split_text(text: str, max_length: int): + chunks = [] + for i in range(0, len(text), max_length): + chunk = text[i:i + max_length] + if i != 0: + chunk = text[i-100:i + max_length] + chunks.append(chunk) + return chunks + + @staticmethod + def combine_chunks(chunks: list): + combined_text = "".join(chunks) + return combined_text diff --git a/docker-compose.yml b/docker-compose.yml index 99d872f6..80f755d7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -323,6 +323,16 @@ services: networks: - bykstack + anonymizer: + build: + context: ./anonymizer + dockerfile: Dockerfile + container_name: anonymizer + ports: + - "8010:8010" + networks: + - bykstack + volumes: shared-volume: opensearch-data: From 5a2dc4f7a0a5d0f75be29e61f327a6a99f41732d Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Sun, 11 Aug 2024 18:07:38 +0530 Subject: [PATCH 427/582] replacement debug statement deltetion --- anonymizer/fake_replacements.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/anonymizer/fake_replacements.py b/anonymizer/fake_replacements.py index 6c04768d..4a642485 100644 --- a/anonymizer/fake_replacements.py +++ b/anonymizer/fake_replacements.py @@ -5,8 +5,6 @@ class FakeReplacer: @staticmethod def replace_entities(text: str, entities: list): - print(f"Original text: {text}") - print(f"entities: {entities}") replacements = [] @@ -29,10 +27,6 @@ def replace_entities(text: str, entities: list): replacements.append((start_pos, end_pos, entity_text, entity_type)) i = j - print("Replacements to be made:") - for start_pos, end_pos, entity_text, entity_type in replacements: - print(f"Entity: {entity_text}, Type: {entity_type}, Start: {start_pos}, End: {end_pos}") - for start_pos, end_pos, entity_text, entity_type in sorted(replacements, reverse=True): if entity_type == 'PER': fake_name = fake.name() @@ -44,5 +38,4 @@ def replace_entities(text: str, entities: list): fake_loc = fake.city() text = text[:start_pos] + fake_loc + text[end_pos:] - print(f"Processed text: {text}") return text \ No newline at end of file From 11fd0a174dcf336f684ef0a5c9bd3d4614e15f39 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Sun, 11 Aug 2024 22:18:31 +0530 Subject: [PATCH 428/582] python script starter and data_model.yml completed --- DSL/CronManager/DSL/data_model.yml | 5 ++ .../script/python_script_starter.sh | 46 +++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 DSL/CronManager/DSL/data_model.yml create mode 100644 DSL/CronManager/script/python_script_starter.sh diff --git a/DSL/CronManager/DSL/data_model.yml b/DSL/CronManager/DSL/data_model.yml new file mode 100644 index 00000000..b9101422 --- /dev/null +++ b/DSL/CronManager/DSL/data_model.yml @@ -0,0 +1,5 @@ +model_trainer: + trigger: off + type: exec + command: "../app/scripts/python_script_starter.sh" + allowedEnvs: ['cookie', 'oldModelId', 'newModelId'] \ No newline at end of file diff --git a/DSL/CronManager/script/python_script_starter.sh b/DSL/CronManager/script/python_script_starter.sh new file mode 100644 index 00000000..046bc503 --- /dev/null +++ b/DSL/CronManager/script/python_script_starter.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +VENV_DIR="/home/cronmanager/clsenv" +REQUIREMENTS="model_trainer/requirements.txt" +PYTHON_SCRIPT="model_trainer/model_handler.py" + +is_package_installed() { + package=$1 + pip show "$package" > /dev/null 2>&1 +} + +if [ -d "$VENV_DIR" ]; then + echo "Virtual environment already exists. Activating..." +else + echo "Virtual environment does not exist. Creating..." + python3.12 -m venv $VENV_DIR +fi + +. $VENV_DIR/bin/activate + +echo "Python executable in use: $(which python3)" + +if [ -f "$REQUIREMENTS" ]; then + echo "Checking if required packages are installed..." + + while IFS= read -r requirement || [ -n "$requirement" ]; do + package_name=$(echo "$requirement" | cut -d '=' -f 1 | tr -d '[:space:]') + + if is_package_installed "$package_name"; then + echo "Package '$package_name' is already installed." + else + echo "Package '$package_name' is not installed. Installing..." + pip install "$requirement" + fi + done < "$REQUIREMENTS" +else + echo "Requirements file not found: $REQUIREMENTS" +fi + +# Check if the Python script exists +if [ -f "$PYTHON_SCRIPT" ]; then + echo "Running the Python script: $PYTHON_SCRIPT" + python "$PYTHON_SCRIPT" +else + echo "Python script not found: $PYTHON_SCRIPT" +fi From 6f86dc9117e3c03241c4a2d04d901ea9f8395416 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Sun, 11 Aug 2024 22:20:30 +0530 Subject: [PATCH 429/582] Docker compose file to support new cron manager with python --- docker-compose.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 80f755d7..9841d178 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -160,11 +160,12 @@ services: cron-manager: container_name: cron-manager - image: cron-manager + image: cron-manager-python volumes: - ./DSL/CronManager/DSL:/DSL - ./DSL/CronManager/script:/app/scripts - ./DSL/CronManager/config:/app/config + - ./model_trainer:/app/model_trainer environment: - server.port=9010 ports: From eaeb6d18809247e0bbed2ed71706752141902556 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Sun, 11 Aug 2024 22:21:20 +0530 Subject: [PATCH 430/582] example model trainer files --- model_trainer/model_handler.py | 28 ++++++++++++++++++++++++++++ model_trainer/requirements.txt | 2 ++ 2 files changed, 30 insertions(+) create mode 100644 model_trainer/model_handler.py create mode 100644 model_trainer/requirements.txt diff --git a/model_trainer/model_handler.py b/model_trainer/model_handler.py new file mode 100644 index 00000000..4ab42579 --- /dev/null +++ b/model_trainer/model_handler.py @@ -0,0 +1,28 @@ +import time +import random +import string +from datetime import datetime + +class EnvironmentPrinter: + def __init__(self): + self.generated_id = self.generate_random_id() + + def generate_random_id(self): + return ''.join(random.choices(string.ascii_letters + string.digits, k=8)) + + def print_id_with_timestamp(self): + current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + print(f'Generated ID: {self.generated_id}, Timestamp: {current_time}') + + def execute(self): + self.print_id_with_timestamp() + + for _ in range(2): + time.sleep(15) + self.print_id_with_timestamp() + + return True + +if __name__ == "__main__": + env_printer = EnvironmentPrinter() + env_printer.execute() diff --git a/model_trainer/requirements.txt b/model_trainer/requirements.txt new file mode 100644 index 00000000..25022cd1 --- /dev/null +++ b/model_trainer/requirements.txt @@ -0,0 +1,2 @@ +fastapi==0.111.1 +uvicorn==0.30.3 \ No newline at end of file From d80ef758591f5d0ca1d900227163bffb8e570a24 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 12 Aug 2024 01:09:33 +0530 Subject: [PATCH 431/582] dataset validator completed and tested with progress update and code refactoring --- dataset-processor/dataset_validator.py | 208 +++++++++++++++++++------ 1 file changed, 158 insertions(+), 50 deletions(-) diff --git a/dataset-processor/dataset_validator.py b/dataset-processor/dataset_validator.py index 0797e537..11a82a2a 100644 --- a/dataset-processor/dataset_validator.py +++ b/dataset-processor/dataset_validator.py @@ -4,57 +4,84 @@ import requests import urllib.parse import datetime +from constants import * class DatasetValidator: def __init__(self): pass def process_request(self, dgId, cookie, updateType, savedFilePath, patchPayload=None): - print("Process request started") + print(MSG_PROCESS_REQUEST_STARTED) print(f"dgId: {dgId}, updateType: {updateType}, savedFilePath: {savedFilePath}") - if updateType == "minor": - return self.handle_minor_update(dgId, cookie, savedFilePath) - elif updateType == "patch": - return self.handle_patch_update(dgId, cookie, patchPayload) - else: - return self.generate_response(False, "Unknown update type") + metadata = self.get_datagroup_metadata(dgId, cookie) + if not metadata: + return self.generate_response(False, MSG_REQUEST_FAILED.format("Metadata")) + + session_id = self.create_progress_session(metadata, cookie) + print(f"Progress Session ID : {session_id}") + if not session_id: + return self.generate_response(False, MSG_REQUEST_FAILED.format("Progress session creation")) + + try: + # Initializing dataset processing + self.update_progress(cookie, PROGRESS_INITIATING, MSG_INIT_VALIDATION, STATUS_MSG_VALIDATION_INIT, session_id) + + if updateType == "minor": + result = self.handle_minor_update(dgId, cookie, savedFilePath, session_id) + elif updateType == "patch": + result = self.handle_patch_update(dgId, cookie, patchPayload, session_id) + else: + result = self.generate_response(False, "Unknown update type") + + # Final progress update upon successful completion + self.update_progress(cookie, PROGRESS_VALIDATION_COMPLETE, MSG_VALIDATION_SUCCESS, STATUS_MSG_SUCCESS, session_id) + return result + except Exception as e: + self.update_progress(cookie, PROGRESS_FAIL, MSG_INTERNAL_ERROR.format(e), STATUS_MSG_FAIL, session_id) + return self.generate_response(False, MSG_INTERNAL_ERROR.format(e)) - def handle_minor_update(self, dgId, cookie, savedFilePath): + def handle_minor_update(self, dgId, cookie, savedFilePath, session_id): try: - print("Handling minor update") + print(MSG_HANDLING_MINOR_UPDATE) + self.update_progress(cookie, 10, MSG_DOWNLOADING_DATASET, STATUS_MSG_VALIDATION_INPROGRESS, session_id) data = self.get_dataset_by_location(savedFilePath, cookie) if data is None: - print("Failed to download and load data") + self.update_progress(cookie, PROGRESS_FAIL, "Failed to download and load data", STATUS_MSG_FAIL, session_id) return self.generate_response(False, "Failed to download and load data") print("Data downloaded and loaded successfully") + self.update_progress(cookie, 20, MSG_FETCHING_VALIDATION_CRITERIA, STATUS_MSG_VALIDATION_INPROGRESS, session_id) validation_criteria, class_hierarchy = self.get_validation_criteria(dgId, cookie) if validation_criteria is None: - print("Failed to get validation criteria") + self.update_progress(cookie, PROGRESS_FAIL, "Failed to get validation criteria", STATUS_MSG_FAIL, session_id) return self.generate_response(False, "Failed to get validation criteria") print("Validation criteria retrieved successfully") + self.update_progress(cookie, 30, MSG_VALIDATING_FIELDS, STATUS_MSG_VALIDATION_INPROGRESS, session_id) field_validation_result = self.validate_fields(data, validation_criteria) if not field_validation_result['success']: - print("Field validation failed") + self.update_progress(cookie, PROGRESS_FAIL, field_validation_result['message'], STATUS_MSG_FAIL, session_id) return self.generate_response(False, field_validation_result['message']) - print("Field validation successful") + print(MSG_VALIDATION_FIELDS_SUCCESS) + self.update_progress(cookie, 35, MSG_VALIDATING_CLASS_HIERARCHY, STATUS_MSG_VALIDATION_INPROGRESS, session_id) hierarchy_validation_result = self.validate_class_hierarchy(data, validation_criteria, class_hierarchy) if not hierarchy_validation_result['success']: - print("Class hierarchy validation failed") + self.update_progress(cookie, PROGRESS_FAIL, hierarchy_validation_result['message'], STATUS_MSG_FAIL, session_id) return self.generate_response(False, hierarchy_validation_result['message']) - print("Class hierarchy validation successful") + print(MSG_CLASS_HIERARCHY_SUCCESS) print("Minor update processed successfully") + self.update_progress(cookie, 40, "Minor update processed successfully", STATUS_MSG_SUCCESS, session_id) return self.generate_response(True, "Minor update processed successfully") except Exception as e: - print(f"Internal error: {e}") - return self.generate_response(False, f"Internal error: {e}") + print(MSG_INTERNAL_ERROR.format(e)) + self.update_progress(cookie, PROGRESS_FAIL, MSG_INTERNAL_ERROR.format(e), STATUS_MSG_FAIL, session_id) + return self.generate_response(False, MSG_INTERNAL_ERROR.format(e)) def get_dataset_by_location(self, fileLocation, custom_jwt_cookie): - print("Downloading dataset by location") + print(MSG_DOWNLOADING_DATASET) params = {'saveLocation': fileLocation} headers = {'cookie': custom_jwt_cookie} try: @@ -63,11 +90,11 @@ def get_dataset_by_location(self, fileLocation, custom_jwt_cookie): print("Dataset downloaded successfully") return response.json() except requests.exceptions.RequestException as e: - print(f"Error downloading dataset: {e}") + print(MSG_REQUEST_FAILED.format("Dataset download")) return None def get_validation_criteria(self, dgId, cookie): - print("Fetching validation criteria") + print(MSG_FETCHING_VALIDATION_CRITERIA) params = {'dgId': dgId} headers = {'cookie': cookie} try: @@ -78,32 +105,32 @@ def get_validation_criteria(self, dgId, cookie): class_hierarchy = response.json().get('response', {}).get('classHierarchy', {}) return validation_criteria, class_hierarchy except requests.exceptions.RequestException as e: - print(f"Error fetching validation criteria: {e}") + print(MSG_REQUEST_FAILED.format("Validation criteria fetch")) return None def validate_fields(self, data, validation_criteria): - print("Validating fields") + print(MSG_VALIDATING_FIELDS) try: fields = validation_criteria.get('fields', []) validation_rules = validation_criteria.get('validationRules', {}) for field in fields: if field not in data[0]: - print(f"Missing field: {field}") - return {'success': False, 'message': f"Missing field: {field}"} + print(MSG_MISSING_FIELD.format(field)) + return {'success': False, 'message': MSG_MISSING_FIELD.format(field)} for idx, row in enumerate(data): for field, rules in validation_rules.items(): if field in row: value = row[field] if not self.validate_value(value, rules['type']): - print(f"Validation failed for field '{field}' in row {idx + 1}") - return {'success': False, 'message': f"Validation failed for field '{field}' in row {idx + 1}"} - print("Fields validation successful") - return {'success': True, 'message': "Fields validation successful"} + print(MSG_VALIDATION_FIELD_FAIL.format(field, idx + 1)) + return {'success': False, 'message': MSG_VALIDATION_FIELD_FAIL.format(field, idx + 1)} + print(MSG_VALIDATION_FIELDS_SUCCESS) + return {'success': True, 'message': MSG_VALIDATION_FIELDS_SUCCESS} except Exception as e: - print(f"Error validating fields: {e}") - return {'success': False, 'message': f"Error validating fields: {e}"} + print(MSG_INTERNAL_ERROR.format(e)) + return {'success': False, 'message': MSG_INTERNAL_ERROR.format(e)} def validate_value(self, value, value_type): if value_type == 'email': @@ -127,7 +154,7 @@ def validate_value(self, value, value_type): return False def validate_class_hierarchy(self, data, validation_criteria, class_hierarchy): - print("Validating class hierarchy") + print(MSG_VALIDATING_CLASS_HIERARCHY) try: data_class_columns = [field for field, rules in validation_criteria.get('validationRules', {}).items() if rules.get('isDataClass', False)] @@ -138,21 +165,20 @@ def validate_class_hierarchy(self, data, validation_criteria, class_hierarchy): missing_in_data = hierarchy_values - data_values if missing_in_hierarchy: - print(f"Values missing in class hierarchy: {missing_in_hierarchy}") - return {'success': False, 'message': f"Values missing in class hierarchy: {missing_in_hierarchy}"} + print(MSG_CLASS_HIERARCHY_FAIL.format("class hierarchy", missing_in_hierarchy)) + return {'success': False, 'message': MSG_CLASS_HIERARCHY_FAIL.format("class hierarchy", missing_in_hierarchy)} if missing_in_data: - print(f"Values missing in data class columns: {missing_in_data}") - return {'success': False, 'message': f"Values missing in data class columns: {missing_in_data}"} + print(MSG_CLASS_HIERARCHY_FAIL.format("data class columns", missing_in_data)) + return {'success': False, 'message': MSG_CLASS_HIERARCHY_FAIL.format("data class columns", missing_in_data)} - print("Class hierarchy validation successful") - return {'success': True, 'message': "Class hierarchy validation successful"} + print(MSG_CLASS_HIERARCHY_SUCCESS) + return {'success': True, 'message': MSG_CLASS_HIERARCHY_SUCCESS} except Exception as e: - print(f"Error validating class hierarchy: {e}") - return {'success': False, 'message': f"Error validating class hierarchy: {e}"} + print(MSG_INTERNAL_ERROR.format(e)) + return {'success': False, 'message': MSG_INTERNAL_ERROR.format(e)} def extract_hierarchy_values(self, hierarchy): - print("Extracting hierarchy values") - print(hierarchy) + print(MSG_EXTRACTING_HIERARCHY_VALUES) values = set() def traverse(node): @@ -168,7 +194,7 @@ def traverse(node): return values def extract_data_class_values(self, data, columns): - print("Extracting data class values") + print(MSG_EXTRACTING_DATA_CLASS_VALUES) values = set() for row in data: for col in columns: @@ -176,65 +202,86 @@ def extract_data_class_values(self, data, columns): print(f"Data class values extracted: {values}") return values - def handle_patch_update(self, dgId, cookie, patchPayload): - print("Handling patch update") + def handle_patch_update(self, dgId, cookie, patchPayload, session_id): + print(MSG_HANDLING_PATCH_UPDATE) min_label_value = 1 try: + # Start with a small progress value + self.update_progress(cookie, 5, MSG_FETCHING_VALIDATION_CRITERIA, STATUS_MSG_VALIDATION_INPROGRESS, session_id) validation_criteria, class_hierarchy = self.get_validation_criteria(dgId, cookie) if validation_criteria is None: + self.update_progress(cookie, PROGRESS_FAIL, "Failed to get validation criteria", STATUS_MSG_FAIL, session_id) return self.generate_response(False, "Failed to get validation criteria") if patchPayload is None: + self.update_progress(cookie, PROGRESS_FAIL, "No patch payload provided", STATUS_MSG_FAIL, session_id) return self.generate_response(False, "No patch payload provided") decoded_patch_payload = urllib.parse.unquote(patchPayload) patch_payload_dict = json.loads(decoded_patch_payload) - + edited_data = patch_payload_dict.get("editedData", []) if edited_data: + self.update_progress(cookie, 10, "Processing edited data", STATUS_MSG_VALIDATION_INPROGRESS, session_id) for row in edited_data: row_id = row.pop("rowId", None) if row_id is None: + self.update_progress(cookie, PROGRESS_FAIL, "Missing rowId in edited data", STATUS_MSG_FAIL, session_id) return self.generate_response(False, "Missing rowId in edited data") - + for key, value in row.items(): if key not in validation_criteria['validationRules']: + self.update_progress(cookie, PROGRESS_FAIL, f"Invalid field: {key}", STATUS_MSG_FAIL, session_id) return self.generate_response(False, f"Invalid field: {key}") if not self.validate_value(value, validation_criteria['validationRules'][key]['type']): + self.update_progress(cookie, PROGRESS_FAIL, f"Validation failed for field type '{key}' in row {row_id}", STATUS_MSG_FAIL, session_id) return self.generate_response(False, f"Validation failed for field type '{key}' in row {row_id}") + self.update_progress(cookie, 20, "Validating data class hierarchy", STATUS_MSG_VALIDATION_INPROGRESS, session_id) data_class_columns = [field for field, rules in validation_criteria['validationRules'].items() if rules.get('isDataClass', False)] hierarchy_values = self.extract_hierarchy_values(class_hierarchy) for row in edited_data: for col in data_class_columns: if row.get(col) and row[col] not in hierarchy_values: + self.update_progress(cookie, PROGRESS_FAIL, f"New class '{row[col]}' does not exist in the schema hierarchy", STATUS_MSG_FAIL, session_id) return self.generate_response(False, f"New class '{row[col]}' does not exist in the schema hierarchy") + self.update_progress(cookie, 30, "Downloading aggregated dataset", STATUS_MSG_VALIDATION_INPROGRESS, session_id) aggregated_data = self.get_dataset_by_location(f"/dataset/{dgId}/primary_dataset/dataset_{dgId}_aggregated.json", cookie) if aggregated_data is None: + self.update_progress(cookie, PROGRESS_FAIL, "Failed to download aggregated dataset", STATUS_MSG_FAIL, session_id) return self.generate_response(False, "Failed to download aggregated dataset") + self.update_progress(cookie, 35, "Checking label counts for edited data", STATUS_MSG_VALIDATION_INPROGRESS, session_id) if not self.check_label_counts(aggregated_data, edited_data, data_class_columns, min_label_value): + self.update_progress(cookie, PROGRESS_FAIL, "Editing this data will cause the dataset to have insufficient data examples for one or more labels.", STATUS_MSG_FAIL, session_id) return self.generate_response(False, "Editing this data will cause the dataset to have insufficient data examples for one or more labels.") deleted_data_rows = patch_payload_dict.get("deletedDataRows", []) if deleted_data_rows: + self.update_progress(cookie, 40, "Processing deleted data rows", STATUS_MSG_VALIDATION_INPROGRESS, session_id) if 'aggregated_data' not in locals(): aggregated_data = self.get_dataset_by_location(f"/dataset/{dgId}/primary_dataset/dataset_{dgId}_aggregated.json", cookie) if aggregated_data is None: + self.update_progress(cookie, PROGRESS_FAIL, "Failed to download aggregated dataset", STATUS_MSG_FAIL, session_id) return self.generate_response(False, "Failed to download aggregated dataset") + self.update_progress(cookie, 45, "Checking label counts after deletion", STATUS_MSG_VALIDATION_INPROGRESS, session_id) if not self.check_label_counts_after_deletion(aggregated_data, deleted_data_rows, data_class_columns, min_label_value): + self.update_progress(cookie, PROGRESS_FAIL, "Deleting this data will cause the dataset to have insufficient data examples for one or more labels.", STATUS_MSG_FAIL, session_id) return self.generate_response(False, "Deleting this data will cause the dataset to have insufficient data examples for one or more labels.") - return self.generate_response(True, "Patch update processed successfully") + self.update_progress(cookie, PROGRESS_VALIDATION_COMPLETE, MSG_PATCH_UPDATE_SUCCESS, STATUS_MSG_SUCCESS, session_id) + return self.generate_response(True, MSG_PATCH_UPDATE_SUCCESS) except Exception as e: - print(f"Internal error: {e}") - return self.generate_response(False, f"Internal error: {e}") + print(MSG_INTERNAL_ERROR.format(e)) + self.update_progress(cookie, PROGRESS_FAIL, MSG_INTERNAL_ERROR.format(e), STATUS_MSG_FAIL, session_id) + return self.generate_response(False, MSG_INTERNAL_ERROR.format(e)) + def check_label_counts(self, aggregated_data, edited_data, data_class_columns, min_label_value): # Aggregate data class values from edited data @@ -290,10 +337,71 @@ def check_label_counts_after_deletion(self, aggregated_data, deleted_data_rows, return True def generate_response(self, success, message): - print(f"Generating response: success={success}, message={message}") + print(MSG_GENERATING_RESPONSE.format(success, message)) return { 'response': { 'operationSuccessful': success, 'message': message } } + + def get_datagroup_metadata(self, dgId, cookie): + url = GET_DATAGROUP_METADATA_URL.replace('dgId', str(dgId)) + headers = {'Cookie': cookie} + try: + response = requests.get(url, headers=headers) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + print(MSG_REQUEST_FAILED.format("Metadata fetch")) + return None + + def create_progress_session(self, metadata, cookie): + print(f"METADATA : >>>>>>>>>> {metadata}") + url = CREATE_PROGRESS_SESSION_URL + headers = {'Content-Type': 'application/json', 'Cookie': cookie} + payload = { + 'dgId': metadata['response']['data'][0]['dgId'], + 'groupName': metadata['response']['data'][0]['name'], + 'majorVersion': metadata['response']['data'][0]['majorVersion'], + 'minorVersion': metadata['response']['data'][0]['minorVersion'], + 'patchVersion': metadata['response']['data'][0]['patchVersion'], + 'latest': metadata['response']['data'][0]['latest'] + } + + try: + response = requests.post(url, headers=headers, json=payload) + response.raise_for_status() + response_data = response.json().get('response', {}) + session_id = response_data.get('sessionId') + return session_id + except requests.exceptions.RequestException as e: + print(MSG_REQUEST_FAILED.format("Progress session creation")) + return None + + def update_progress(self, cookie, progress, message, status, session_id=None): + + if progress == PROGRESS_FAIL: + process_complete = True + else: + process_complete = False + + url = UPDATE_PROGRESS_SESSION_URL + headers = {'Content-Type': 'application/json', 'Cookie': cookie} + payload = { + 'sessionId': session_id, + 'validationStatus': status, + 'validationMessage': message, + 'progressPercentage': progress, + 'processComplete': process_complete + } + try: + print(f"Update Payload : {payload}") + response = requests.post(url, headers=headers, json=payload) + response.raise_for_status() + print(f"Response : {response}") + return response.json() + except requests.exceptions.RequestException as e: + print(f"Progress update error : {e}") + print(MSG_REQUEST_FAILED.format("Progress update")) + return None From f48199f9ba5ce32686159383ec2a9bfa809aafa5 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 12 Aug 2024 01:36:45 +0530 Subject: [PATCH 432/582] Docker compose file with updated requirenments --- docker-compose.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 9841d178..51bb3998 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -243,6 +243,10 @@ services: - S3_REGION_NAME=eu-west-1 - PARAPHRASE_API_URL=http://data-enrichment-api:8005/paraphrase - VALIDATION_CONFIRMATION_URL=http://ruuter-private:8088/classifier/datasetgroup/update/validation/status + - GET_DATAGROUP_METADATA_URL=http://ruuter-private:8088/classifier/datasetgroup/group/metadata?groupId=dgId + - CREATE_PROGRESS_SESSION_URL=http://ruuter-private:8088/classifier/datasetgroup/progress/create + - UPDATE_PROGRESS_SESSION_URL=http://ruuter-private:8088/classifier/datasetgroup/progress/update + - GET_PROGRESS_SESSIONS_URL=http://ruuter-private:8088/classifier/datasetgroup/progress ports: - "8001:8001" networks: From 9fc1cd8370a62c10138c31cbd864b2b53ae20465 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 12 Aug 2024 01:37:01 +0530 Subject: [PATCH 433/582] dataset processor refactor and progress update --- dataset-processor/dataset_processor.py | 549 ++++++++++++------------- 1 file changed, 263 insertions(+), 286 deletions(-) diff --git a/dataset-processor/dataset_processor.py b/dataset-processor/dataset_processor.py index 04a5e3fe..3ef9f581 100644 --- a/dataset-processor/dataset_processor.py +++ b/dataset-processor/dataset_processor.py @@ -342,11 +342,15 @@ def update_preprocess_status(self,dg_id, cookie, processed_data_available, raw_d return None def process_handler(self, dgId, newDgId, cookie, updateType, savedFilePath, patchPayload): - print(f"Process handler started with updateType: {updateType}") + session_id = self.get_session_id(dgId, cookie) + if not session_id: + return self.generate_response(False, MSG_FAIL) + + print(MSG_PROCESS_HANDLER_STARTED.format(updateType)) page_count = self.get_page_count(dgId, cookie) - print(f"Page Count : {page_count}") + print(MSG_PAGE_COUNT.format(page_count)) - if updateType == "minor" and page_count>0: + if updateType == "minor" and page_count > 0: updateType = "minor_append_update" elif updateType == "patch": pass @@ -354,293 +358,266 @@ def process_handler(self, dgId, newDgId, cookie, updateType, savedFilePath, patc updateType = "minor_initial_update" if updateType == "minor_initial_update": - print("Handling Minor update") - # dataset = self.get_dataset(dgId, cookie) - dataset = self.get_dataset_by_location(savedFilePath, cookie) - if dataset is not None: - print("Dataset retrieved successfully") - structured_data = self.check_and_convert(dataset) - if structured_data is not None: - print("Dataset converted successfully") - selected_data_fields_to_enrich = self.get_selected_data_fields(newDgId, cookie) - if selected_data_fields_to_enrich is not None: - print("Selected data fields to enrich retrieved successfully") - max_row_id = max(item["rowId"] for item in structured_data) - enriched_data = self.enrich_data(structured_data, selected_data_fields_to_enrich, max_row_id) - - agregated_dataset = structured_data + enriched_data - - if enriched_data is not None: - print("Data enrichment successful") - stop_words = self.get_stopwords(cookie) - if stop_words is not None: - print("Stop words retrieved successfully") - print(agregated_dataset) - cleaned_data = self.remove_stop_words(agregated_dataset, stop_words) - if cleaned_data is not None: - print("Stop words removed successfully") - print(cleaned_data) - chunked_data = self.chunk_data(cleaned_data) - if chunked_data is not None: - print("Data chunking successful") - print(chunked_data) - operation_result = self.save_chunked_data(chunked_data, cookie, newDgId, 0) - if operation_result: - print("Chunked data saved successfully") - agregated_dataset_operation = self.save_aggregrated_data(newDgId, cookie, cleaned_data) - if agregated_dataset_operation != None: - return_data = self.update_preprocess_status(newDgId, cookie, True, False, f"/dataset/{newDgId}/chunks/", "", True, len(cleaned_data), len(chunked_data)) - print(return_data) - return SUCCESSFUL_OPERATION - else: - print("Failed to save aggregated dataset for minor update") - return FAILED_TO_SAVE_AGGREGATED_DATA - else: - print("Failed to save chunked data") - return FAILED_TO_SAVE_CHUNKED_DATA - else: - print("Failed to chunk cleaned data") - return FAILED_TO_CHUNK_CLEANED_DATA - else: - print("Failed to remove stop words") - return FAILED_TO_REMOVE_STOP_WORDS - else: - print("Failed to retrieve stop words") - return FAILED_TO_GET_STOP_WORDS - else: - print("Failed to enrich data") - return FAILED_TO_ENRICH_DATA - else: - print("Failed to get selected data fields to enrich") - return FAILED_TO_GET_SELECTED_FIELDS - else: - print("Failed to convert dataset") - return FAILED_TO_CHECK_AND_CONVERT - else: - print("Failed to retrieve dataset") - return FAILED_TO_GET_DATASET + result = self.handle_minor_initial_update(dgId, newDgId, cookie, savedFilePath, session_id) elif updateType == "minor_append_update": - print("Handling Minor update") - agregated_dataset = self.get_dataset(dgId, cookie) - max_row_id = max(item["rowId"] for item in agregated_dataset) - if agregated_dataset is not None: - print("Aggregated dataset retrieved successfully") - minor_update_dataset = self.get_dataset_by_location(savedFilePath, cookie) - if minor_update_dataset is not None: - print("Minor update dataset retrieved successfully") - structured_data = self.check_and_convert(minor_update_dataset) - structured_data = self.add_row_id(structured_data, max_row_id) - print(structured_data[-1]) - if structured_data is not None: - print("Minor update dataset converted successfully") - selected_data_fields_to_enrich = self.get_selected_data_fields(newDgId, cookie) - if selected_data_fields_to_enrich is not None: - print("Selected data fields to enrich for minor update retrieved successfully") - max_row_id = max(item["rowId"] for item in structured_data) - enriched_data = self.enrich_data(structured_data, selected_data_fields_to_enrich, max_row_id) - if enriched_data is not None: - print("Minor update data enrichment successful") - stop_words = self.get_stopwords(cookie) - if stop_words is not None: - combined_new_dataset = structured_data + enriched_data - print("Stop words for minor update retrieved successfully") - cleaned_data = self.remove_stop_words(combined_new_dataset, stop_words) - if cleaned_data is not None: - print("Stop words for minor update removed successfully") - chunked_data = self.chunk_data(cleaned_data) - if chunked_data is not None: - print("Minor update data chunking successful") - page_count = self.get_page_count(dgId, cookie) - if page_count is not None: - print(f"Page count retrieved successfully: {page_count}") - print(chunked_data) - copy_exsisting_files = self.copy_chunked_datafiles(dgId, newDgId, cookie, page_count) - if copy_exsisting_files is not None: - operation_result = self.save_chunked_data(chunked_data, cookie, newDgId, page_count) - if operation_result is not None: - print("Chunked data for minor update saved successfully") - agregated_dataset += cleaned_data - agregated_dataset_operation = self.save_aggregrated_data(newDgId, cookie, agregated_dataset) - if agregated_dataset_operation: - print("Aggregated dataset for minor update saved successfully") - return_data = self.update_preprocess_status(newDgId, cookie, True, False, f"/dataset/{newDgId}/chunks/", "", True, len(agregated_dataset), (len(chunked_data)+page_count)) - print(return_data) - return SUCCESSFUL_OPERATION - else: - print("Failed to save aggregated dataset for minor update") - return FAILED_TO_SAVE_AGGREGATED_DATA - else: - print("Failed to save chunked data for minor update") - return FAILED_TO_SAVE_CHUNKED_DATA - else: - print("Failed to copy existing chunked data for minor update") - return FAILED_TO_COPY_CHUNKED_DATA - else: - print("Failed to get page count") - return FAILED_TO_GET_PAGE_COUNT - else: - print("Failed to chunk cleaned data for minor update") - return FAILED_TO_CHUNK_CLEANED_DATA - else: - print("Failed to remove stop words for minor update") - return FAILED_TO_REMOVE_STOP_WORDS - else: - print("Failed to retrieve stop words for minor update") - return FAILED_TO_GET_STOP_WORDS - else: - print("Failed to enrich data for minor update") - return FAILED_TO_ENRICH_DATA - else: - print("Failed to get selected data fields to enrich for minor update") - return FAILED_TO_GET_SELECTED_FIELDS - else: - print("Failed to convert minor update dataset") - return FAILED_TO_CHECK_AND_CONVERT - else: - print("Failed to retrieve minor update dataset") - return FAILED_TO_GET_MINOR_UPDATE_DATASET - else: - print("Failed to retrieve aggregated dataset for minor update") - return FAILED_TO_GET_AGGREGATED_DATASET + result = self.handle_minor_append_update(dgId, newDgId, cookie, savedFilePath, session_id) elif updateType == "patch": - decoded_string = urllib.parse.unquote(patchPayload) - data_payload = json.loads(decoded_string) - if (data_payload["editedData"]!=[]): - print("Handling Patch update") - stop_words = self.get_stopwords(cookie) - if stop_words is not None: - print("Stop words for patch update retrieved successfully") - cleaned_patch_payload = self.remove_stop_words(data_payload["editedData"], stop_words) - if cleaned_patch_payload is not None: - print("Stop words for patch update removed successfully") - page_count = self.get_page_count(dgId, cookie) - if page_count is not None: - print(f"Page count for patch update retrieved successfully: {page_count}") - print(cleaned_patch_payload) - chunk_updates = {} - for entry in cleaned_patch_payload: - rowId = entry.get("rowId") - rowId = int(rowId) - chunkNum = (rowId - 1) // 5 + 1 - if chunkNum not in chunk_updates: - chunk_updates[chunkNum] = [] - chunk_updates[chunkNum].append(entry) - print(f"Chunk updates prepared: {chunk_updates}") - for chunkNum, entries in chunk_updates.items(): - chunk_data = self.download_chunk(dgId, cookie, chunkNum) - if chunk_data is not None: - print(f"Chunk {chunkNum} downloaded successfully") - for entry in entries: - rowId = entry.get("rowId") - rowId = int(rowId) - for idx, chunk_entry in enumerate(chunk_data): - if chunk_entry.get("rowId") == rowId: - chunk_data[idx] = entry - break - chunk_save_operation = self.save_chunked_data([chunk_data], cookie, dgId, chunkNum-1) - if chunk_save_operation == None: - print(f"Failed to save chunk {chunkNum}") - return FAILED_TO_SAVE_CHUNKED_DATA - else: - print(f"Failed to download chunk {chunkNum}") - return FAILED_TO_DOWNLOAD_CHUNK - agregated_dataset = self.get_dataset(dgId, cookie) - if agregated_dataset is not None: - print("Aggregated dataset for patch update retrieved successfully") - for entry in cleaned_patch_payload: - rowId = entry.get("rowId") - rowId = int(rowId) - for index, item in enumerate(agregated_dataset): - if item.get("rowId") == rowId: - entry["rowId"] = rowId - del entry["rowId"] - agregated_dataset[index] = entry - break - - save_result_update = self.save_aggregrated_data(dgId, cookie, agregated_dataset) - if save_result_update: - print("Aggregated dataset for patch update saved successfully") - else: - print("Failed to save aggregated dataset for patch update") - return FAILED_TO_SAVE_AGGREGATED_DATA - else: - print("Failed to retrieve aggregated dataset for patch update") - return FAILED_TO_GET_AGGREGATED_DATASET - else: - print("Failed to get page count for patch update") - return FAILED_TO_GET_PAGE_COUNT - else: - print("Failed to remove stop words for patch update") - return FAILED_TO_REMOVE_STOP_WORDS - else: - print("Failed to retrieve stop words for patch update") - return FAILED_TO_GET_STOP_WORDS - - print(data_payload["deletedDataRows"]) - if (data_payload["deletedDataRows"]!=[]): - try: - print("Handling deleted data rows") - deleted_rows = data_payload["deletedDataRows"] - aggregated_dataset = self.get_dataset(dgId, cookie) - if aggregated_dataset is not None: - print("Aggregated dataset for delete operation retrieved successfully") - updated_dataset = [row for row in aggregated_dataset if row.get('rowId') not in deleted_rows] - for idx, row in enumerate(updated_dataset, start=1): - row['rowId'] = idx - if updated_dataset is not None: - print("Deleted rows removed and dataset updated successfully") - chunked_data = self.chunk_data(updated_dataset) - if chunked_data is not None: - print("Data chunking after delete operation successful") - print(chunked_data) - operation_result = self.save_chunked_data(chunked_data, cookie, dgId, 0) - if operation_result: - print("Chunked data after delete operation saved successfully") - save_result_delete = self.save_aggregrated_data(dgId, cookie, updated_dataset) - if save_result_delete: - print("Aggregated dataset after delete operation saved successfully") - else: - print("Failed to save aggregated dataset after delete operation") - return FAILED_TO_SAVE_AGGREGATED_DATA - else: - print("Failed to save chunked data after delete operation") - return FAILED_TO_SAVE_CHUNKED_DATA - else: - print("Failed to chunk data after delete operation") - return FAILED_TO_CHUNK_CLEANED_DATA - else: - print("Failed to update dataset after deleting rows") - return FAILED_TO_UPDATE_DATASET - else: - print("Failed to retrieve aggregated dataset for delete operation") - return FAILED_TO_GET_AGGREGATED_DATASET - except Exception as e: - print(f"An error occurred while handling deleted data rows: {e}") - return FAILED_TO_HANDLE_DELETED_ROWS - - if data_payload["editedData"]==[] and data_payload["deletedDataRows"]==[]: - return SUCCESSFUL_OPERATION - elif data_payload["editedData"]!=[] and data_payload["deletedDataRows"]==[]: - if save_result_update: - return SUCCESSFUL_OPERATION - else: - return FAILED_TO_SAVE_AGGREGATED_DATA - elif data_payload["editedData"]==[] and data_payload["deletedDataRows"]!=[]: - if save_result_delete: - return_data = self.update_preprocess_status(dgId, cookie, True, False, f"/dataset/{dgId}/chunks/", "", True, len(updated_dataset), len(chunked_data)) - print(return_data) - return SUCCESSFUL_OPERATION - else: - return FAILED_TO_SAVE_AGGREGATED_DATA - elif data_payload["editedData"]!=[] and data_payload["deletedDataRows"]!=[]: - if save_result_update and save_result_delete: - return_data = self.update_preprocess_status(dgId, cookie, True, False, f"/dataset/{dgId}/chunks/", "", True, len(updated_dataset), len(chunked_data)) - print(return_data) - return SUCCESSFUL_OPERATION - else: - return FAILED_TO_SAVE_AGGREGATED_DATA + result = self.handle_patch_update(dgId, cookie, patchPayload, session_id) + + self.update_progress(cookie, PROGRESS_SUCCESS if result['response']['operationSuccessful'] else PROGRESS_FAIL, MSG_SUCCESS if result['response']['operationSuccessful'] else MSG_FAIL, STATUS_MSG_SUCCESS if result['response']['operationSuccessful'] else STATUS_MSG_FAIL, session_id) + return result + + def handle_minor_initial_update(self, dgId, newDgId, cookie, savedFilePath, session_id): + self.update_progress(cookie, PROGRESS_CLEANING_PROCESSING, MSG_CLEANING_PROCESSING, STATUS_MSG_CLEANING_DATASET, session_id) + + dataset = self.get_dataset_by_location(savedFilePath, cookie) + if dataset is None: + return self.generate_response(False, MSG_FAIL) + + structured_data = self.check_and_convert(dataset) + if structured_data is None: + return self.generate_response(False, MSG_FAIL) + + selected_data_fields_to_enrich = self.get_selected_data_fields(newDgId, cookie) + if selected_data_fields_to_enrich is None: + return self.generate_response(False, MSG_FAIL) + + max_row_id = max(item["rowId"] for item in structured_data) + enriched_data = self.enrich_data(structured_data, selected_data_fields_to_enrich, max_row_id) + if enriched_data is None: + return self.generate_response(False, MSG_FAIL) + + self.update_progress(cookie, PROGRESS_GENERATING_DATA, MSG_GENERATING_DATA, STATUS_MSG_GENERATING_DATA, session_id) + + aggregated_dataset = structured_data + enriched_data + stop_words = self.get_stopwords(cookie) + if stop_words is None: + return self.generate_response(False, MSG_FAIL) + + cleaned_data = self.remove_stop_words(aggregated_dataset, stop_words) + if cleaned_data is None: + return self.generate_response(False, MSG_FAIL) + + self.update_progress(cookie, PROGRESS_CHUNKING_UPLOADING, MSG_CHUNKING_UPLOADING, STATUS_MSG_GENERATING_DATA, session_id) + + chunked_data = self.chunk_data(cleaned_data) + if chunked_data is None: + return self.generate_response(False, MSG_FAIL) + + operation_result = self.save_chunked_data(chunked_data, cookie, newDgId, 0) + if not operation_result: + return self.generate_response(False, MSG_FAIL) + + aggregated_dataset_operation = self.save_aggregrated_data(newDgId, cookie, cleaned_data) + if not aggregated_dataset_operation: + return self.generate_response(False, MSG_FAIL) + + return_data = self.update_preprocess_status(newDgId, cookie, True, False, f"/dataset/{newDgId}/chunks/", "", True, len(cleaned_data), len(chunked_data)) + return self.generate_response(True, MSG_PROCESS_COMPLETE) + def handle_minor_append_update(self, dgId, newDgId, cookie, savedFilePath, session_id): + self.update_progress(cookie, PROGRESS_CLEANING_PROCESSING, MSG_CLEANING_PROCESSING, STATUS_MSG_CLEANING_DATASET, session_id) + + aggregated_dataset = self.get_dataset(dgId, cookie) + if aggregated_dataset is None: + return self.generate_response(False, MSG_FAIL) + + max_row_id = max(item["rowId"] for item in aggregated_dataset) + minor_update_dataset = self.get_dataset_by_location(savedFilePath, cookie) + if minor_update_dataset is None: + return self.generate_response(False, MSG_FAIL) + + structured_data = self.check_and_convert(minor_update_dataset) + if structured_data is None: + return self.generate_response(False, MSG_FAIL) + + structured_data = self.add_row_id(structured_data, max_row_id) + selected_data_fields_to_enrich = self.get_selected_data_fields(newDgId, cookie) + if selected_data_fields_to_enrich is None: + return self.generate_response(False, MSG_FAIL) + + enriched_data = self.enrich_data(structured_data, selected_data_fields_to_enrich, max_row_id) + if enriched_data is None: + return self.generate_response(False, MSG_FAIL) + + self.update_progress(cookie, PROGRESS_GENERATING_DATA, MSG_GENERATING_DATA, STATUS_MSG_GENERATING_DATA, session_id) + + stop_words = self.get_stopwords(cookie) + if stop_words is None: + return self.generate_response(False, MSG_FAIL) + + combined_new_dataset = structured_data + enriched_data + cleaned_data = self.remove_stop_words(combined_new_dataset, stop_words) + if cleaned_data is None: + return self.generate_response(False, MSG_FAIL) + + chunked_data = self.chunk_data(cleaned_data) + if chunked_data is None: + return self.generate_response(False, MSG_FAIL) + + page_count = self.get_page_count(dgId, cookie) + if page_count is None: + return self.generate_response(False, MSG_FAIL) + + copy_existing_files = self.copy_chunked_datafiles(dgId, newDgId, cookie, page_count) + if copy_existing_files is None: + return self.generate_response(False, MSG_FAIL) + + operation_result = self.save_chunked_data(chunked_data, cookie, newDgId, page_count) + if not operation_result: + return self.generate_response(False, MSG_FAIL) + + aggregated_dataset += cleaned_data + aggregated_dataset_operation = self.save_aggregrated_data(newDgId, cookie, aggregated_dataset) + if not aggregated_dataset_operation: + return self.generate_response(False, MSG_FAIL) + + return_data = self.update_preprocess_status(newDgId, cookie, True, False, f"/dataset/{newDgId}/chunks/", "", True, len(aggregated_dataset), (len(chunked_data) + page_count)) + return self.generate_response(True, MSG_PROCESS_COMPLETE) + + def handle_patch_update(self, dgId, cookie, patchPayload, session_id): + decoded_string = urllib.parse.unquote(patchPayload) + data_payload = json.loads(decoded_string) + + if data_payload["editedData"]: + self.update_progress(cookie, PROGRESS_CLEANING_PROCESSING, MSG_CLEANING_PROCESSING, STATUS_MSG_CLEANING_DATASET, session_id) + stop_words = self.get_stopwords(cookie) + if stop_words is None: + return self.generate_response(False, MSG_FAIL) + + cleaned_patch_payload = self.remove_stop_words(data_payload["editedData"], stop_words) + if cleaned_patch_payload is None: + return self.generate_response(False, MSG_FAIL) + + page_count = self.get_page_count(dgId, cookie) + if page_count is None: + return self.generate_response(False, MSG_FAIL) + + chunk_updates = self.prepare_chunk_updates(cleaned_patch_payload) + if chunk_updates is None: + return self.generate_response(False, MSG_FAIL) + + for chunk_num, entries in chunk_updates.items(): + chunk_data = self.download_chunk(dgId, cookie, chunk_num) + if chunk_data is None: + return self.generate_response(False, MSG_FAIL) + + for entry in entries: + row_id = int(entry.get("rowId")) + for idx, chunk_entry in enumerate(chunk_data): + if chunk_entry.get("rowId") == row_id: + chunk_data[idx] = entry + break + chunk_save_operation = self.save_chunked_data([chunk_data], cookie, dgId, chunk_num - 1) + if chunk_save_operation is None: + return self.generate_response(False, MSG_FAIL) + + aggregated_dataset = self.get_dataset(dgId, cookie) + if aggregated_dataset is None: + return self.generate_response(False, MSG_FAIL) + + for entry in cleaned_patch_payload: + row_id = int(entry.get("rowId")) + for index, item in enumerate(aggregated_dataset): + if item.get("rowId") == row_id: + aggregated_dataset[index] = entry + break + + save_result_update = self.save_aggregrated_data(dgId, cookie, aggregated_dataset) + if not save_result_update: + return self.generate_response(False, MSG_FAIL) + + if data_payload["deletedDataRows"]: + self.update_progress(cookie, PROGRESS_CLEANING_PROCESSING, MSG_CLEANING_PROCESSING, STATUS_MSG_CLEANING_DATASET, session_id) + deleted_rows = data_payload["deletedDataRows"] + aggregated_dataset = self.get_dataset(dgId, cookie) + if aggregated_dataset is None: + return self.generate_response(False, MSG_FAIL) + + updated_dataset = [row for row in aggregated_dataset if row.get('rowId') not in deleted_rows] + updated_dataset = self.reindex_dataset(updated_dataset) + if updated_dataset is None: + return self.generate_response(False, MSG_FAIL) + + chunked_data = self.chunk_data(updated_dataset) + if chunked_data is None: + return self.generate_response(False, MSG_FAIL) + + operation_result = self.save_chunked_data(chunked_data, cookie, dgId, 0) + if not operation_result: + return self.generate_response(False, MSG_FAIL) + + save_result_delete = self.save_aggregrated_data(dgId, cookie, updated_dataset) + if not save_result_delete: + return self.generate_response(False, MSG_FAIL) + + return self.generate_response(True, MSG_PROCESS_COMPLETE) + def get_session_id(self, dgId, cookie): + headers = {'Cookie': cookie} + try: + response = requests.get(GET_PROGRESS_SESSIONS_URL, headers=headers) + response.raise_for_status() + sessions = response.json().get("response", {}).get("data", []) + for session in sessions: + if session['dgId'] == dgId: + return session['id'] + return None + except requests.exceptions.RequestException as e: + print(MSG_FAIL) + return None + def update_progress(self, cookie, progress, message, status, session_id): + if progress == PROGRESS_SUCCESS or progress == PROGRESS_FAIL: + process_complete = True + else: + process_complete = False + url = UPDATE_PROGRESS_SESSION_URL + headers = {'Content-Type': 'application/json', 'Cookie': cookie} + payload = { + 'sessionId': session_id, + 'validationStatus': status, + 'validationMessage': message, + 'progressPercentage': progress, + 'processComplete': process_complete + } + try: + response = requests.post(url, json=payload, headers=headers) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + print(MSG_FAIL) + return None + + def generate_response(self, success, message): + return { + 'response': { + 'operationSuccessful': success, + 'message': message + } + } + + def prepare_chunk_updates(self, cleaned_patch_payload): + try: + chunk_updates = {} + for entry in cleaned_patch_payload: + row_id = int(entry.get("rowId")) + chunk_num = (row_id - 1) // 5 + 1 + if chunk_num not in chunk_updates: + chunk_updates[chunk_num] = [] + chunk_updates[chunk_num].append(entry) + return chunk_updates + except Exception as e: + print(MSG_FAIL) + return None + + def reindex_dataset(self, dataset): + try: + for idx, row in enumerate(dataset, start=1): + row['rowId'] = idx + return dataset + except Exception as e: + print(MSG_FAIL) + return None \ No newline at end of file From a14c918152763be0dd3441508ea29965f9cd9102 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 12 Aug 2024 01:37:11 +0530 Subject: [PATCH 434/582] dataset processor api updates --- dataset-processor/dataset_processor_api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dataset-processor/dataset_processor_api.py b/dataset-processor/dataset_processor_api.py index 0bed4a5a..e7841316 100644 --- a/dataset-processor/dataset_processor_api.py +++ b/dataset-processor/dataset_processor_api.py @@ -75,7 +75,7 @@ async def forward_request(request: Request, response: Response): 'Content-Type': 'application/json' } if validator_response["response"]["operationSuccessful"] != True: - forward_payload["validationStatus"] = "failed" + forward_payload["validationStatus"] = "fail" forward_payload["validationErrors"] = [validator_response["response"]["message"]] else: forward_payload["validationStatus"] = "success" @@ -93,7 +93,7 @@ async def forward_request(request: Request, response: Response): forward_payload["updateType"] = payload["updateType"] forward_payload["patchPayload"] = payload["patchPayload"] forward_payload["savedFilePath"] = payload["savedFilePath"] - forward_payload["validationStatus"] = "failed" + forward_payload["validationStatus"] = "fail" forward_payload["validationErrors"] = [e] forward_response = requests.post(VALIDATION_CONFIRMATION_URL, json=forward_payload, headers=headers) print(e) @@ -105,7 +105,7 @@ async def forward_request(request: Request, response: Response): forward_payload["updateType"] = payload["updateType"] forward_payload["patchPayload"] = payload["patchPayload"] forward_payload["savedFilePath"] = payload["savedFilePath"] - forward_payload["validationStatus"] = "failed" + forward_payload["validationStatus"] = "fail" forward_payload["validationErrors"] = [e] forward_response = requests.post(VALIDATION_CONFIRMATION_URL, json=forward_payload, headers=headers) print(e) From 2847b62e7917788640d6623e2076731956e81243 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 12 Aug 2024 01:37:27 +0530 Subject: [PATCH 435/582] constants file for dataset processor --- dataset-processor/constants.py | 83 ++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/dataset-processor/constants.py b/dataset-processor/constants.py index 1a26f8fe..6faed23e 100644 --- a/dataset-processor/constants.py +++ b/dataset-processor/constants.py @@ -1,3 +1,4 @@ +import os # Constants for return payloads SUCCESSFUL_OPERATION = { "operation_status": 200, @@ -99,3 +100,85 @@ "operation_successful": False, "reason": "Failed to update dataset after deleting rows" } + +# URLs +GET_DATAGROUP_METADATA_URL = os.getenv("GET_DATAGROUP_METADATA_URL") +CREATE_PROGRESS_SESSION_URL = os.getenv("CREATE_PROGRESS_SESSION_URL") +UPDATE_PROGRESS_SESSION_URL = os.getenv("UPDATE_PROGRESS_SESSION_URL") +GET_PROGRESS_SESSIONS_URL = os.getenv("GET_PROGRESS_SESSIONS_URL") +UPDATE_PROGRESS_SESSION_URL = os.getenv("UPDATE_PROGRESS_SESSION_URL") +GET_DATAGROUP_METADATA_URL = os.getenv("GET_DATAGROUP_METADATA_URL") +CREATE_PROGRESS_SESSION_URL = os.getenv("CREATE_PROGRESS_SESSION_URL") +PARAPHRASE_API_URL = os.getenv("PARAPHRASE_API_URL") +GET_VALIDATION_SCHEMA = os.getenv("GET_VALIDATION_SCHEMA") +FILE_HANDLER_DOWNLOAD_JSON_URL = os.getenv("FILE_HANDLER_DOWNLOAD_JSON_URL") +GET_STOPWORDS_URL = os.getenv("GET_STOPWORDS_URL") +FILE_HANDLER_IMPORT_CHUNKS_URL = os.getenv("FILE_HANDLER_IMPORT_CHUNKS_URL") +FILE_HANDLER_DOWNLOAD_LOCATION_JSON_URL = os.getenv("FILE_HANDLER_DOWNLOAD_LOCATION_JSON_URL") +GET_PAGE_COUNT_URL = os.getenv("GET_PAGE_COUNT_URL") +SAVE_JSON_AGGREGRATED_DATA_URL = os.getenv("SAVE_JSON_AGGREGRATED_DATA_URL") +DOWNLOAD_CHUNK_URL = os.getenv("DOWNLOAD_CHUNK_URL") +STATUS_UPDATE_URL = os.getenv("STATUS_UPDATE_URL") +FILE_HANDLER_COPY_CHUNKS_URL = os.getenv("FILE_HANDLER_COPY_CHUNKS_URL") + +# Messages +MSG_PROCESS_HANDLER_STARTED = "Process handler started with updateType: {}" +MSG_PAGE_COUNT = "Page Count : {}" +MSG_CLEANING_PROCESSING = "Preprocessing operations on dataset" +MSG_GENERATING_DATA = "Generating synthetic data to increase dataset volume" +MSG_CHUNKING_UPLOADING = "Chunking and uploading dataset" +MSG_SUCCESS = "Dataset processed and Uploaded Successfully" +MSG_FAIL = "Failed to process dataset" +MSG_PROCESS_COMPLETE = "Process complete" + +# Progress percentages +PROGRESS_CLEANING_PROCESSING = 50 +PROGRESS_GENERATING_DATA = 70 +PROGRESS_CHUNKING_UPLOADING = 80 +PROGRESS_SUCCESS = 100 +PROGRESS_FAIL = 100 + +# Messages +MSG_INIT_VALIDATION = "Validation Initiated" +MSG_VALIDATION_IN_PROGRESS = "Running validation criteria across dataset" +MSG_VALIDATION_SUCCESS = "Validation successful" +MSG_VALIDATION_FAIL = "Validation failed" +MSG_MISSING_FIELD = "Missing field: {}" +MSG_VALIDATION_FIELD_FAIL = "Validation failed for field '{}' in row {}" +MSG_VALIDATION_FIELDS_SUCCESS = "Fields validation successful" +MSG_CLASS_HIERARCHY_FAIL = "Values missing in {}: {}" +MSG_CLASS_HIERARCHY_SUCCESS = "Class hierarchy validation successful" +MSG_PATCH_UPDATE_SUCCESS = "Patch update processed successfully" +MSG_INTERNAL_ERROR = "Internal error: {}" +MSG_REQUEST_FAILED = "{} request failed" +MSG_GENERATING_RESPONSE = "Generating response: success={}, message={}" +MSG_PROCESS_REQUEST_STARTED = "Process request started" +MSG_HANDLING_MINOR_UPDATE = "Handling minor update" +MSG_HANDLING_PATCH_UPDATE = "Handling patch update" +MSG_DOWNLOADING_DATASET = "Downloading dataset by location" +MSG_FETCHING_VALIDATION_CRITERIA = "Fetching validation criteria" +MSG_VALIDATING_FIELDS = "Validating fields" +MSG_VALIDATING_CLASS_HIERARCHY = "Validating class hierarchy" +MSG_EXTRACTING_HIERARCHY_VALUES = "Extracting hierarchy values" +MSG_EXTRACTING_DATA_CLASS_VALUES = "Extracting data class values" + +# Progress percentages +PROGRESS_INITIATING = 0 +PROGRESS_VALIDATION_IN_PROGRESS = 34 +PROGRESS_VALIDATION_COMPLETE = 40 +PROGRESS_FAIL = 100 + +# Messages for validation progress updates +MSG_INIT_VALIDATION = "Initializing dataset processing" +MSG_VALIDATION_SUCCESS = "Validation successful" +MSG_VALIDATION_FAILED = "Validation failed" +MSG_PROCESSING_STARTED = "Processing the dataset" +MSG_PROCESSING_COMPLETED = "Dataset Processing Completed" + +#Status Messages for progress +STATUS_MSG_VALIDATION_INIT = 'Initiating Validation' +STATUS_MSG_VALIDATION_INPROGRESS = 'Validation In-Progress' +STATUS_MSG_CLEANING_DATASET = 'Cleaning Dataset' +STATUS_MSG_GENERATING_DATA = 'Generating Data' +STATUS_MSG_SUCCESS = 'Success' +STATUS_MSG_FAIL = 'Fail' From f77d5fe7f04e7b81b45374f21af07db8bc7a160b Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 12 Aug 2024 01:48:28 +0530 Subject: [PATCH 436/582] cleaning constants --- dataset-processor/constants.py | 111 +------------------------ dataset-processor/dataset_processor.py | 3 - 2 files changed, 3 insertions(+), 111 deletions(-) diff --git a/dataset-processor/constants.py b/dataset-processor/constants.py index 6faed23e..af471fb5 100644 --- a/dataset-processor/constants.py +++ b/dataset-processor/constants.py @@ -1,105 +1,4 @@ import os -# Constants for return payloads -SUCCESSFUL_OPERATION = { - "operation_status": 200, - "operation_successful": True -} - -FAILED_TO_SAVE_CHUNKED_DATA = { - "operation_status": 500, - "operation_successful": False, - "reason": "Failed to save chunked data into S3" -} - -FAILED_TO_COPY_CHUNKED_DATA = { - "operation_status": 500, - "operation_successful": False, - "reason": "Failed to copy existing chunked data in S3" -} - -FAILED_TO_CHUNK_CLEANED_DATA = { - "operation_status": 500, - "operation_successful": False, - "reason": "Failed to chunk the cleaned data" -} - -FAILED_TO_REMOVE_STOP_WORDS = { - "operation_status": 500, - "operation_successful": False, - "reason": "Failed to remove stop words from enriched data" -} - -FAILED_TO_GET_STOP_WORDS = { - "operation_status": 500, - "operation_successful": False, - "reason": "Failed to get stop words" -} - -FAILED_TO_ENRICH_DATA = { - "operation_status": 500, - "operation_successful": False, - "reason": "Failed to enrich data" -} - -FAILED_TO_GET_SELECTED_FIELDS = { - "operation_status": 500, - "operation_successful": False, - "reason": "Failed to get selected data fields to enrich" -} - -FAILED_TO_CHECK_AND_CONVERT = { - "operation_status": 500, - "operation_successful": False, - "reason": "Failed to check and convert dataset structure" -} - -FAILED_TO_GET_DATASET = { - "operation_status": 500, - "operation_successful": False, - "reason": "Failed to get dataset" -} - -FAILED_TO_GET_MINOR_UPDATE_DATASET = { - "operation_status": 500, - "operation_successful": False, - "reason": "Failed to get minor update dataset" -} - -FAILED_TO_GET_AGGREGATED_DATASET = { - "operation_status": 500, - "operation_successful": False, - "reason": "Failed to get aggregated dataset" -} - -FAILED_TO_GET_PAGE_COUNT = { - "operation_status": 500, - "operation_successful": False, - "reason": "Failed to get page count" -} - -FAILED_TO_SAVE_AGGREGATED_DATA = { - "operation_status": 500, - "operation_successful": False, - "reason": "Failed to save aggregated dataset" -} - -FAILED_TO_DOWNLOAD_CHUNK = { - "operation_status": 500, - "operation_successful": False, - "reason": "Failed to download chunk" -} - -FAILED_TO_HANDLE_DELETED_ROWS = { - "operation_status": 500, - "operation_successful": False, - "reason": "Failed to handle deleted rows" -} - -FAILED_TO_UPDATE_DATASET = { - "operation_status": 500, - "operation_successful": False, - "reason": "Failed to update dataset after deleting rows" -} # URLs GET_DATAGROUP_METADATA_URL = os.getenv("GET_DATAGROUP_METADATA_URL") @@ -161,6 +60,9 @@ MSG_VALIDATING_CLASS_HIERARCHY = "Validating class hierarchy" MSG_EXTRACTING_HIERARCHY_VALUES = "Extracting hierarchy values" MSG_EXTRACTING_DATA_CLASS_VALUES = "Extracting data class values" +MSG_VALIDATION_FAILED = "Validation failed" +MSG_PROCESSING_STARTED = "Processing the dataset" +MSG_PROCESSING_COMPLETED = "Dataset Processing Completed" # Progress percentages PROGRESS_INITIATING = 0 @@ -168,13 +70,6 @@ PROGRESS_VALIDATION_COMPLETE = 40 PROGRESS_FAIL = 100 -# Messages for validation progress updates -MSG_INIT_VALIDATION = "Initializing dataset processing" -MSG_VALIDATION_SUCCESS = "Validation successful" -MSG_VALIDATION_FAILED = "Validation failed" -MSG_PROCESSING_STARTED = "Processing the dataset" -MSG_PROCESSING_COMPLETED = "Dataset Processing Completed" - #Status Messages for progress STATUS_MSG_VALIDATION_INIT = 'Initiating Validation' STATUS_MSG_VALIDATION_INPROGRESS = 'Validation In-Progress' diff --git a/dataset-processor/dataset_processor.py b/dataset-processor/dataset_processor.py index 3ef9f581..12dff102 100644 --- a/dataset-processor/dataset_processor.py +++ b/dataset-processor/dataset_processor.py @@ -7,16 +7,13 @@ RUUTER_PRIVATE_URL = os.getenv("RUUTER_PRIVATE_URL") GET_VALIDATION_SCHEMA = os.getenv("GET_VALIDATION_SCHEMA") -FILE_HANDLER_DOWNLOAD_JSON_URL = os.getenv("FILE_HANDLER_DOWNLOAD_JSON_URL") GET_STOPWORDS_URL = os.getenv("GET_STOPWORDS_URL") FILE_HANDLER_IMPORT_CHUNKS_URL = os.getenv("FILE_HANDLER_IMPORT_CHUNKS_URL") FILE_HANDLER_DOWNLOAD_LOCATION_JSON_URL = os.getenv("FILE_HANDLER_DOWNLOAD_LOCATION_JSON_URL") GET_PAGE_COUNT_URL = os.getenv("GET_PAGE_COUNT_URL") SAVE_JSON_AGGREGRATED_DATA_URL = os.getenv("SAVE_JSON_AGGREGRATED_DATA_URL") DOWNLOAD_CHUNK_URL = os.getenv("DOWNLOAD_CHUNK_URL") -STATUS_UPDATE_URL = os.getenv("STATUS_UPDATE_URL") FILE_HANDLER_COPY_CHUNKS_URL = os.getenv("FILE_HANDLER_COPY_CHUNKS_URL") -PARAPHRASE_API_URL = os.getenv("PARAPHRASE_API_URL") class DatasetProcessor: def __init__(self): From da35f95323e7d9f7f1016c3ccfac57aefa13170b Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 12 Aug 2024 02:03:10 +0530 Subject: [PATCH 437/582] initial const changes for file handler --- file-handler/constants.py | 29 +++++++++++++++++------ file-handler/file_handler_api.py | 40 +++++++------------------------- 2 files changed, 31 insertions(+), 38 deletions(-) diff --git a/file-handler/constants.py b/file-handler/constants.py index aa5d1445..4f8abcce 100644 --- a/file-handler/constants.py +++ b/file-handler/constants.py @@ -1,5 +1,6 @@ -# constants.py +import os +# Status messages UPLOAD_FAILED = { "upload_status": 500, "operation_successful": False, @@ -41,18 +42,32 @@ "reason": "Failed to download from S3" } +# File extensions JSON_EXT = ".json" YAML_EXT = ".yaml" YML_EXT = ".yml" XLSX_EXT = ".xlsx" -def GET_S3_FERRY_PAYLOAD(destinationFilePath:str, destinationStorageType:str, sourceFilePath:str, sourceStorageType:str): +# S3 Ferry payload +def GET_S3_FERRY_PAYLOAD(destinationFilePath: str, destinationStorageType: str, sourceFilePath: str, sourceStorageType: str): S3_FERRY_PAYLOAD = { - "destinationFilePath": destinationFilePath, - "destinationStorageType": destinationStorageType, - "sourceFilePath": sourceFilePath, - "sourceStorageType": sourceStorageType - } + "destinationFilePath": destinationFilePath, + "destinationStorageType": destinationStorageType, + "sourceFilePath": sourceFilePath, + "sourceStorageType": sourceStorageType + } return S3_FERRY_PAYLOAD +# Directories +UPLOAD_DIRECTORY = os.getenv("UPLOAD_DIRECTORY", "/shared") +CHUNK_UPLOAD_DIRECTORY = os.getenv("CHUNK_UPLOAD_DIRECTORY", "/shared/chunks") +JSON_FILE_DIRECTORY = os.path.join("..", "shared") +TEMP_COPY_FILE = "temp_copy.json" +# URLs +RUUTER_PRIVATE_URL = os.getenv("RUUTER_PRIVATE_URL") +S3_FERRY_URL = os.getenv("S3_FERRY_URL") +IMPORT_STOPWORDS_URL = os.getenv("IMPORT_STOPWORDS_URL") +DELETE_STOPWORDS_URL = os.getenv("DELETE_STOPWORDS_URL") +DATAGROUP_DELETE_CONFIRMATION_URL = os.getenv("DATAGROUP_DELETE_CONFIRMATION_URL") +DATAMODEL_DELETE_CONFIRMATION_URL = os.getenv("DATAMODEL_DELETE_CONFIRMATION_URL") diff --git a/file-handler/file_handler_api.py b/file-handler/file_handler_api.py index a9f1e97f..779e6953 100644 --- a/file-handler/file_handler_api.py +++ b/file-handler/file_handler_api.py @@ -7,10 +7,7 @@ import requests from pydantic import BaseModel from file_converter import FileConverter -from constants import ( - UPLOAD_FAILED, UPLOAD_SUCCESS, EXPORT_TYPE_ERROR, IMPORT_TYPE_ERROR, - S3_UPLOAD_FAILED, S3_DOWNLOAD_FAILED, JSON_EXT, YAML_EXT, YML_EXT, XLSX_EXT -) +from constants import * from s3_ferry import S3Ferry import yaml import pandas as pd @@ -28,14 +25,6 @@ allow_headers=["*"], ) -UPLOAD_DIRECTORY = os.getenv("UPLOAD_DIRECTORY", "/shared") -CHUNK_UPLOAD_DIRECTORY = os.getenv("CHUNK_UPLOAD_DIRECTORY", "/shared/chunks") -RUUTER_PRIVATE_URL = os.getenv("RUUTER_PRIVATE_URL") -S3_FERRY_URL = os.getenv("S3_FERRY_URL") -IMPORT_STOPWORDS_URL = os.getenv("IMPORT_STOPWORDS_URL") -DELETE_STOPWORDS_URL = os.getenv("DELETE_STOPWORDS_URL") -DATAGROUP_DELETE_CONFIRMATION_URL = os.getenv("DATAGROUP_DELETE_CONFIRMATION_URL") -DATAMODEL_DELETE_CONFIRMATION_URL = os.getenv("DATAMODEL_DELETE_CONFIRMATION_URL") s3_ferry = S3Ferry(S3_FERRY_URL) class ExportFile(BaseModel): @@ -147,7 +136,7 @@ async def download_and_convert(request: Request, exportData: ExportFile, backgro if response.status_code != 201: raise HTTPException(status_code=500, detail=S3_DOWNLOAD_FAILED) - json_file_path = os.path.join('..', 'shared', f"{local_file_name}{JSON_EXT}") + json_file_path = os.path.join(JSON_FILE_DIRECTORY, f"{local_file_name}{JSON_EXT}") file_converter = FileConverter() with open(f"{json_file_path}", 'r') as json_file: @@ -182,7 +171,7 @@ async def download_and_convert(request: Request, dgId: int, background_tasks: Ba if response.status_code != 201: raise HTTPException(status_code=500, detail=S3_DOWNLOAD_FAILED) - jsonFilePath = os.path.join('..', 'shared', f"{localFileName}{JSON_EXT}") + jsonFilePath = os.path.join(JSON_FILE_DIRECTORY, f"{localFileName}{JSON_EXT}") with open(f"{jsonFilePath}", 'r') as jsonFile: jsonData = json.load(jsonFile) @@ -193,21 +182,16 @@ async def download_and_convert(request: Request, dgId: int, background_tasks: Ba @app.get("/datasetgroup/data/download/json/location") async def download_and_convert(request: Request, saveLocation:str, background_tasks: BackgroundTasks): - print("$$$") - print(request.cookies.get("customJwtCookie")) cookie = request.cookies.get("customJwtCookie") - await authenticate_user(f'customJwtCookie={cookie}') - print(saveLocation) - localFileName = saveLocation.split("/")[-1] response = s3_ferry.transfer_file(f"{localFileName}", "FS", saveLocation, "S3") if response.status_code != 201: raise HTTPException(status_code=500, detail=S3_DOWNLOAD_FAILED) - jsonFilePath = os.path.join('..', 'shared', f"{localFileName}") + jsonFilePath = os.path.join(JSON_FILE_DIRECTORY, f"{localFileName}") with open(f"{jsonFilePath}", 'r') as jsonFile: jsonData = json.load(jsonFile) @@ -218,9 +202,6 @@ async def download_and_convert(request: Request, saveLocation:str, background_ta @app.post("/datasetgroup/data/import/chunk") async def upload_and_copy(request: Request, import_chunks: ImportChunks): - print("%") - print(request.cookies.get("customJwtCookie")) - print("$") cookie = request.cookies.get("customJwtCookie") await authenticate_user(f'customJwtCookie={cookie}') @@ -229,7 +210,7 @@ async def upload_and_copy(request: Request, import_chunks: ImportChunks): exsisting_chunks = import_chunks.exsistingChunks fileLocation = os.path.join(CHUNK_UPLOAD_DIRECTORY, f"{exsisting_chunks}.json") - s3_ferry_view_file_location= os.path.join("/chunks", f"{exsisting_chunks}.json") + s3_ferry_view_file_location = os.path.join("/chunks", f"{exsisting_chunks}.json") with open(fileLocation, 'w') as jsonFile: json.dump(chunks, jsonFile, indent=4) @@ -246,17 +227,15 @@ async def download_and_convert(request: Request, dgId: int, pageId: int, backgro try: cookie = request.cookies.get("customJwtCookie") await authenticate_user(f'customJwtCookie={cookie}') - print("$#@$@#$@#$@#$") - print(request) + save_location = f"/dataset/{dgId}/chunks/{pageId}{JSON_EXT}" local_file_name = f"group_{dgId}_chunk_{pageId}" response = s3_ferry.transfer_file(f"{local_file_name}{JSON_EXT}", "FS", save_location, "S3") if response.status_code != 201: - print("S3 Download Failed") return {} - json_file_path = os.path.join('..', 'shared', f"{local_file_name}{JSON_EXT}") + json_file_path = os.path.join(JSON_FILE_DIRECTORY, f"{local_file_name}{JSON_EXT}") with open(f"{json_file_path}", 'r') as json_file: json_data = json.load(json_file) @@ -300,12 +279,13 @@ async def upload_and_copy(request: Request, copyPayload: CopyPayload): files = copyPayload.fileLocations if len(files)>0: - local_storage_location = "temp_copy.json" + local_storage_location = TEMP_COPY_FILE else: print("Abort copying since sent file list does not have any entry.") upload_success = UPLOAD_SUCCESS.copy() upload_success["saved_file_path"] = "" return JSONResponse(status_code=200, content=upload_success) + for file in files: old_location = f"/dataset/{dg_id}/{file}" new_location = f"/dataset/{new_dg_id}/{file}" @@ -347,7 +327,6 @@ def extract_stop_words(file: UploadFile) -> List[str]: else: raise HTTPException(status_code=400, detail="Unsupported file type") - @app.post("/datasetgroup/data/import/stop-words") async def import_stop_words(request: Request, stopWordsFile: UploadFile = File(...)): try: @@ -425,4 +404,3 @@ async def delete_dataset_files(request: Request): except Exception as e: print(f"Error in delete_dataset_files: {e}") raise HTTPException(status_code=500, detail=str(e)) - From 57308ec9524c08831251bedf2770870df0343385 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Mon, 12 Aug 2024 09:44:26 +0530 Subject: [PATCH 438/582] test model uis --- GUI/src/App.tsx | 3 + .../molecules/DataModelCard/index.tsx | 2 +- GUI/src/pages/DataModels/DataModels.scss | 11 ++++ GUI/src/pages/TestModel/index.tsx | 65 +++++++++++++++++++ GUI/src/types/testModel.ts | 8 +++ GUI/src/utils/testModelUtil.ts | 10 +++ 6 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 GUI/src/pages/TestModel/index.tsx create mode 100644 GUI/src/types/testModel.ts create mode 100644 GUI/src/utils/testModelUtil.ts diff --git a/GUI/src/App.tsx b/GUI/src/App.tsx index 40bfff4f..81565814 100644 --- a/GUI/src/App.tsx +++ b/GUI/src/App.tsx @@ -16,6 +16,7 @@ import CorrectedTexts from 'pages/CorrectedTexts'; import CreateDataModel from 'pages/DataModels/CreateDataModel'; import TrainingSessions from 'pages/TrainingSessions'; import DataModels from 'pages/DataModels'; +import TestModel from 'pages/TestModel'; const App: FC = () => { useQuery<{ @@ -45,6 +46,8 @@ const App: FC = () => { } /> } /> } /> + } /> + ); diff --git a/GUI/src/components/molecules/DataModelCard/index.tsx b/GUI/src/components/molecules/DataModelCard/index.tsx index 2fb5ea97..279ae532 100644 --- a/GUI/src/components/molecules/DataModelCard/index.tsx +++ b/GUI/src/components/molecules/DataModelCard/index.tsx @@ -73,7 +73,7 @@ const DataModelCard: FC> = ({

          {dataModelName}

          - {isLatest ? : null} + {isLatest ? : null}
          diff --git a/GUI/src/pages/DataModels/DataModels.scss b/GUI/src/pages/DataModels/DataModels.scss index 37643714..81bd42f7 100644 --- a/GUI/src/pages/DataModels/DataModels.scss +++ b/GUI/src/pages/DataModels/DataModels.scss @@ -9,6 +9,17 @@ padding: 25px; } +.blue-card { + border-radius: 10px; + margin-bottom: 10px; + display: flex; + gap: 20px; + align-items: center; + background-color: #d7edff; + padding: 25px; + margin-top: 20px; +} + body { font-family: Arial, sans-serif; background-color: #f4f4f4; diff --git a/GUI/src/pages/TestModel/index.tsx b/GUI/src/pages/TestModel/index.tsx new file mode 100644 index 00000000..7c275a66 --- /dev/null +++ b/GUI/src/pages/TestModel/index.tsx @@ -0,0 +1,65 @@ +import { Button, FormSelect, FormTextarea } from 'components'; +import { FC } from 'react'; +import { useTranslation } from 'react-i18next'; +import { MdClass } from 'react-icons/md'; +import { formatPredictions } from 'utils/testModelUtil'; + +const TestModel: FC = () => { + const { t } = useTranslation(); +const testResults={ + predictedClasses:["Police","Special Agency","External","Reports","Annual Report"], + averageConfidence:89.8, + predictedProbabilities: [98,82,91,90,88] +} + return ( +
          +
          +
          +
          Test Model
          +
          +
          + +
          + +
          +

          Enter Text

          + +
          +
          + +
          + +
          +
          +
          + {`Predicted Class Hierarchy : `} +

          + { + 'Police -> Special Agency -> External -> Reports -> Annual Report' + } +

          +
          +
          +
          +
          + {`Average Confidence : `} +

          {'62%'}

          +
          +
          +
          +
          + {`Class Probabilities : `} +
            + {formatPredictions(testResults)?.map((prediction)=>{ + return(
          • {prediction}
          • ) + })} +
          +
          +
          +
          +
          +
          + ); +}; + +export default TestModel; diff --git a/GUI/src/types/testModel.ts b/GUI/src/types/testModel.ts new file mode 100644 index 00000000..4efe750e --- /dev/null +++ b/GUI/src/types/testModel.ts @@ -0,0 +1,8 @@ + +export type PredictionInput = { + predictedClasses: string[]; + averageConfidence: number; + predictedProbabilities: number[]; +}; + +export type FormattedPrediction = string; diff --git a/GUI/src/utils/testModelUtil.ts b/GUI/src/utils/testModelUtil.ts new file mode 100644 index 00000000..70de9499 --- /dev/null +++ b/GUI/src/utils/testModelUtil.ts @@ -0,0 +1,10 @@ +import { FormattedPrediction, PredictionInput } from "types/testModel"; + +export function formatPredictions(input: PredictionInput): FormattedPrediction[] { + const { predictedClasses, predictedProbabilities } = input; + + return predictedClasses.map((predictedClass, index) => { + const probability = predictedProbabilities[index]; + return `${predictedClass} - ${probability}%`; + }); + } \ No newline at end of file From e2a4eac892023a758096776cc7d9ca51da16ad4a Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Mon, 12 Aug 2024 10:24:11 +0530 Subject: [PATCH 439/582] ESCLASS-169: change train mock endpoints --- DSL/Ruuter.private/DSL/POST/classifier/datamodel/create.yml | 2 +- DSL/Ruuter.private/DSL/POST/classifier/datamodel/re-train.yml | 3 ++- DSL/Ruuter.private/DSL/POST/classifier/datamodel/update.yml | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/create.yml b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/create.yml index bc08d3b2..c296975c 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/create.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/create.yml @@ -174,7 +174,7 @@ check_connected_model_updated_status: next: assign_fail_response execute_cron_manager: - call: reflect.mock + call: http.post args: url: "[#CLASSIFIER_CRON_MANAGER]/execute/data_model/model_trainer" query: diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/re-train.yml b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/re-train.yml index 78a43453..3d39b89b 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/re-train.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/re-train.yml @@ -50,12 +50,13 @@ check_data_model_exist: next: assign_fail_response execute_cron_manager: - call: reflect.mock + call: http.post args: url: "[#CLASSIFIER_CRON_MANAGER]/execute/data_model/model_trainer" query: cookie: ${cookie} modelId: ${model_id} + new_model_id: -1 result: res next: assign_success_response diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update.yml b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update.yml index 54581eb5..1dcc50c5 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update.yml @@ -310,7 +310,7 @@ check_event_type_again: next: return_type_found execute_cron_manager: - call: reflect.mock + call: http.post args: url: "[#CLASSIFIER_CRON_MANAGER]/execute/data_model/model_trainer" query: From e3ef144059ce6f91d427030f05fe3be8fcebb690 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Mon, 12 Aug 2024 11:26:13 +0530 Subject: [PATCH 440/582] ESCLASS-169: fix get exist input data issue --- DSL/Resql/get-basic-input-metadata-by-input-id.sql | 2 ++ DSL/Resql/get-jira-input-row-data.sql | 2 +- .../DSL/GET/classifier/inference/exist.yml | 11 +++++------ 3 files changed, 8 insertions(+), 7 deletions(-) create mode 100644 DSL/Resql/get-basic-input-metadata-by-input-id.sql diff --git a/DSL/Resql/get-basic-input-metadata-by-input-id.sql b/DSL/Resql/get-basic-input-metadata-by-input-id.sql new file mode 100644 index 00000000..3a6136d7 --- /dev/null +++ b/DSL/Resql/get-basic-input-metadata-by-input-id.sql @@ -0,0 +1,2 @@ +SELECT id, input_id, platform +FROM "input" WHERE input_id =:input_id; \ No newline at end of file diff --git a/DSL/Resql/get-jira-input-row-data.sql b/DSL/Resql/get-jira-input-row-data.sql index f4d3c2c0..bc32ef30 100644 --- a/DSL/Resql/get-jira-input-row-data.sql +++ b/DSL/Resql/get-jira-input-row-data.sql @@ -1,3 +1,3 @@ -SELECT predicted_labels,corrected_labels +SELECT predicted_labels, corrected_labels FROM "input" WHERE input_id=:inputId AND platform='JIRA'; diff --git a/DSL/Ruuter.private/DSL/GET/classifier/inference/exist.yml b/DSL/Ruuter.private/DSL/GET/classifier/inference/exist.yml index 39c3cf2e..d8382f34 100644 --- a/DSL/Ruuter.private/DSL/GET/classifier/inference/exist.yml +++ b/DSL/Ruuter.private/DSL/GET/classifier/inference/exist.yml @@ -8,22 +8,22 @@ declaration: namespace: classifier allowlist: params: - - field: inferenceId - type: number + - field: inputId + type: string description: "Parameter 'inferenceId'" extract_data: assign: - inference_id: ${Number(incoming.params.inferenceId)} + input_id: ${incoming.params.inputId} exist: false next: get_input_metadata_by_id get_input_metadata_by_id: call: http.post args: - url: "[#CLASSIFIER_RESQL]/get-basic-input-metadata-by-id" + url: "[#CLASSIFIER_RESQL]/get-basic-input-metadata-by-input-id" body: - id: ${inference_id} + input_id: ${input_id} result: res_input_id next: check_input_metadata_status @@ -42,7 +42,6 @@ check_input_metadata_exist: assign_exist_data: assign: exist : true - inference_id: ${Number(incoming.params.inferenceId)} value: [{ inferenceId: '${res_input_id.response.body[0].id}', inputId: '${res_input_id.response.body[0].inputId}', From 01207a694c86d098da18d563e059e0add926bd2a Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Mon, 12 Aug 2024 12:07:16 +0530 Subject: [PATCH 441/582] data models refactoring --- .../FormElements/FormSelect/index.tsx | 36 +++---- GUI/src/components/Label/Label.scss | 2 +- .../molecules/DataModelCard/index.tsx | 95 +++++++++++++------ .../molecules/DataModelForm/index.tsx | 16 ++-- .../molecules/TrainingSessionCard/index.tsx | 12 +-- .../styles.scss | 4 +- GUI/src/pages/DatasetGroups/index.tsx | 25 +++-- GUI/src/pages/TrainingSessions/index.tsx | 40 ++++---- GUI/src/pages/ValidationSessions/index.tsx | 36 +++---- GUI/src/types/dataModels.ts | 18 +++- GUI/src/types/datasetGroups.ts | 21 +++- 11 files changed, 180 insertions(+), 125 deletions(-) diff --git a/GUI/src/components/FormElements/FormSelect/index.tsx b/GUI/src/components/FormElements/FormSelect/index.tsx index 027393bc..250b303b 100644 --- a/GUI/src/components/FormElements/FormSelect/index.tsx +++ b/GUI/src/components/FormElements/FormSelect/index.tsx @@ -14,6 +14,11 @@ import { Icon } from 'components'; import './FormSelect.scss'; import { ControllerRenderProps } from 'react-hook-form'; +type FormSelectOption = { + label: string; + value: string | { name: string; id: string }; +}; + type FormSelectProps = Partial & SelectHTMLAttributes & { label: ReactNode; @@ -21,26 +26,13 @@ type FormSelectProps = Partial & placeholder?: string; hideLabel?: boolean; direction?: 'down' | 'up'; - options: - | { - label: string; - value: string; - }[] - | { - label: string; - value: { name: string; id: string }; - }[]; - onSelectionChange?: ( - selection: { label: string; value: string } |{ - label: string; - value: { name: string; id: string }; - }| null - ) => void; + options: FormSelectOption[]; + onSelectionChange?: (selection: FormSelectOption | null) => void; error?: string; }; -const itemToString = (item: { label: string; value: string } | null) => { - return item ? item.value : ''; +const itemToString = (item: FormSelectOption | null) => { + return item ? item.value.toString() : ''; }; const FormSelect = forwardRef( @@ -62,11 +54,11 @@ const FormSelect = forwardRef( const id = useId(); const { t } = useTranslation(); const defaultSelected = - options?.find((o) => o.value === defaultValue) || options?.find((o) => o.value?.name === defaultValue) ||null; - const [selectedItem, setSelectedItem] = useState<{ - label: string; - value: string; - } | null>(defaultSelected); + options?.find((o) => o.value === defaultValue) || + options?.find((o) => typeof o.value !== 'string' && o.value?.name === defaultValue) || + null; + const [selectedItem, setSelectedItem] = useState(defaultSelected); + const { isOpen, getToggleButtonProps, diff --git a/GUI/src/components/Label/Label.scss b/GUI/src/components/Label/Label.scss index da25afdd..62f0bd19 100644 --- a/GUI/src/components/Label/Label.scss +++ b/GUI/src/components/Label/Label.scss @@ -7,7 +7,7 @@ $self: &; display: flex; padding: 1.5px 16px; - font-size: $veera-font-size-80; + font-size: 12px; font-weight: $veera-font-weight-delta; border: 2px solid; background-color: get-color(white); diff --git a/GUI/src/components/molecules/DataModelCard/index.tsx b/GUI/src/components/molecules/DataModelCard/index.tsx index 2fb5ea97..7bda9ddf 100644 --- a/GUI/src/components/molecules/DataModelCard/index.tsx +++ b/GUI/src/components/molecules/DataModelCard/index.tsx @@ -8,7 +8,7 @@ import Card from 'components/Card'; import { useTranslation } from 'react-i18next'; type DataModelCardProps = { - modelId:number; + modelId: number; dataModelName?: string | undefined; datasetGroupName?: string; version?: string; @@ -23,7 +23,7 @@ type DataModelCardProps = { results?: any; }; -const DataModelCard: FC> = ({ +const DataModelCard: FC> = ({ modelId, dataModelName, datasetGroupName, @@ -38,32 +38,60 @@ const DataModelCard: FC> = ({ setId, setView, }) => { - const { open,close } = useDialog(); + const { open, close } = useDialog(); const { t } = useTranslation(); const renderTrainingStatus = (status: string | undefined) => { if (status === TrainingStatus.RETRAINING_NEEDED) { - return ; + return ( + + ); } else if (status === TrainingStatus.TRAINED) { - return ; + return ( + + ); } else if (status === TrainingStatus.TRAINING_INPROGRESS) { - return ; + return ( + + ); } else if (status === TrainingStatus.UNTRAINABLE) { - return ; - }else if (status === TrainingStatus.NOT_TRAINED) { - return ; + return ( + + ); + } else if (status === TrainingStatus.NOT_TRAINED) { + return ; } }; const renderMaturityLabel = (status: string | undefined) => { if (status === Maturity.DEVELOPMENT) { - return ; + return ( + + ); } else if (status === Maturity.PRODUCTION) { - return ; + return ( + + ); } else if (status === Maturity.STAGING) { - return ; + return ( + + ); } else if (status === Maturity.TESTING) { - return ; + return ( + + ); } }; @@ -107,7 +135,9 @@ const DataModelCard: FC> = ({ size: 'large', content: (
          -
          Best Performing Model -
          {' '} +
          + Best Performing Model - +
          {' '} > = ({
          } > - {results ?(
          -
          - {results?.classes?.map((c) => { - return
          {c}
          ; - })} -
          -
          - {results?.accuracy?.map((c) => { - return
          {c}
          ; - })} + {results ? ( +
          +
          + {results?.classes?.map((c: string) => { + return
          {c}
          ; + })} +
          +
          + {results?.accuracy?.map((c: string) => { + return
          {c}
          ; + })} +
          +
          + {results?.f1_score?.map((c: string) => { + return
          {c}
          ; + })} +
          -
          - {results?.f1_score?.map((c) => { - return
          {c}
          ; - })} + ) : ( +
          + No training results available
          -
          ):(
          No training results available
          )} - + )}
          ), diff --git a/GUI/src/components/molecules/DataModelForm/index.tsx b/GUI/src/components/molecules/DataModelForm/index.tsx index ed076c9e..954a9b1a 100644 --- a/GUI/src/components/molecules/DataModelForm/index.tsx +++ b/GUI/src/components/molecules/DataModelForm/index.tsx @@ -10,10 +10,7 @@ import { import { formattedArray } from 'utils/commonUtilts'; import { useQuery } from '@tanstack/react-query'; import { getCreateOptions } from 'services/data-models'; -import { - customFormattedArray, - dgArrayWithVersions, -} from 'utils/dataModelsUtils'; +import { dgArrayWithVersions } from 'utils/dataModelsUtils'; import CircularSpinner from '../CircularSpinner/CircularSpinner'; type DataModelFormType = { @@ -31,8 +28,9 @@ const DataModelForm: FC = ({ }) => { const { t } = useTranslation(); - const { data: createOptions, isLoading } = useQuery(['datamodels/create-options'], () => - getCreateOptions() + const { data: createOptions, isLoading } = useQuery( + ['datamodels/create-options'], + () => getCreateOptions() ); return ( @@ -59,7 +57,7 @@ const DataModelForm: FC = ({
          )} - {createOptions && !isLoading? ( + {createOptions && !isLoading ? (
          Select Dataset Group
          @@ -117,7 +115,9 @@ const DataModelForm: FC = ({ />
          - ):()} + ) : ( + + )}
          ); }; diff --git a/GUI/src/components/molecules/TrainingSessionCard/index.tsx b/GUI/src/components/molecules/TrainingSessionCard/index.tsx index 3186c4ee..9f37189a 100644 --- a/GUI/src/components/molecules/TrainingSessionCard/index.tsx +++ b/GUI/src/components/molecules/TrainingSessionCard/index.tsx @@ -5,9 +5,6 @@ import { Card, Label } from 'components'; type TrainingSessionCardProps = { modelName: string; - dgName: string; - deployedModel: string; - lastTrained: string; isLatest: boolean; version: string; status?: string; @@ -18,10 +15,7 @@ type TrainingSessionCardProps = { }; const TrainingSessionCard: React.FC = ({ - dgName, modelName, - deployedModel, - lastTrained, version, isLatest, status, @@ -36,12 +30,8 @@ const TrainingSessionCard: React.FC = ({ -
          {modelName}
          -

          Dataset Group : {dgName}

          -

          Deployed Model : {deployedModel}

          -

          Last Trained : {lastTrained}

          -
          +
          {modelName}
          {isLatest && } {platform && }{' '} diff --git a/GUI/src/components/molecules/ViewDatasetGroupModalController/styles.scss b/GUI/src/components/molecules/ViewDatasetGroupModalController/styles.scss index 5affed21..a533536c 100644 --- a/GUI/src/components/molecules/ViewDatasetGroupModalController/styles.scss +++ b/GUI/src/components/molecules/ViewDatasetGroupModalController/styles.scss @@ -1,6 +1,4 @@ -.flex-grid { - margin-bottom: 20px; -} + .upload-progress-wrapper { text-align: center; diff --git a/GUI/src/pages/DatasetGroups/index.tsx b/GUI/src/pages/DatasetGroups/index.tsx index 0c4d3699..3a67cba2 100644 --- a/GUI/src/pages/DatasetGroups/index.tsx +++ b/GUI/src/pages/DatasetGroups/index.tsx @@ -14,6 +14,13 @@ import { datasetQueryKeys } from 'utils/queryKeys'; import { DatasetViewEnum } from 'enums/datasetEnums'; import CircularSpinner from 'components/molecules/CircularSpinner/CircularSpinner'; +type FilterData = { + datasetGroupName: string; + version: string; + validationStatus: string; + sort: 'asc' | 'desc'; +}; + const DatasetGroups: FC = () => { const { t } = useTranslation(); const navigate = useNavigate(); @@ -23,17 +30,17 @@ const DatasetGroups: FC = () => { const [enableFetch, setEnableFetch] = useState(true); const [view, setView] = useState(DatasetViewEnum.LIST); - useEffect(() => { - setEnableFetch(true); - }, [view]); - - const [filters, setFilters] = useState({ + const [filters, setFilters] = useState({ datasetGroupName: 'all', version: 'x.x.x', validationStatus: 'all', sort: 'asc', }); + useEffect(() => { + setEnableFetch(true); + }, [view]); + const { data: datasetGroupsData, isLoading } = useQuery( datasetQueryKeys.DATASET_OVERVIEW( pageIndex, @@ -59,10 +66,12 @@ const DatasetGroups: FC = () => { enabled: enableFetch, } ); + const { data: filterData } = useQuery( datasetQueryKeys.DATASET_FILTERS(), () => getFilterData() ); + const pageCount = datasetGroupsData?.response?.data?.[0]?.totalPages || 1; const handleFilterChange = (name: string, value: string) => { @@ -91,7 +100,7 @@ const DatasetGroups: FC = () => {
          @@ -100,7 +109,7 @@ const DatasetGroups: FC = () => { /> @@ -109,7 +118,7 @@ const DatasetGroups: FC = () => { /> { const { t } = useTranslation(); - const [progresses, setProgresses] = useState([]); + const [progresses, setProgresses] = useState([]); - const { data: progressData } = useQuery( + const { data: progressData } = useQuery( ['datamodels/progress'], () => getDataModelsProgress(), { @@ -24,7 +24,7 @@ const TrainingSessions: FC = () => { useEffect(() => { if (!progressData) return; - const handleUpdate = (sessionId, newData) => { + const handleUpdate = (sessionId: string, newData: SSEEventData) => { setProgresses((prevProgresses) => prevProgresses.map((progress) => progress.id === sessionId ? { ...progress, ...newData } : progress @@ -33,7 +33,7 @@ const TrainingSessions: FC = () => { }; const eventSources = progressData.map((progress) => { - return sse(`/${progress.id}`, 'model', (data) => { + return sse(`/${progress.id}`, 'model', (data: SSEEventData) => { console.log(`New data for notification ${progress.id}:`, data); handleUpdate(data.sessionId, data); }); @@ -49,22 +49,18 @@ const TrainingSessions: FC = () => {
          -
          Validation Sessions
          +
          {t('trainingSessions.title')}
          - {progresses?.map((session) => { - return ( - - ); - })} + {progresses?.map((session) => ( + + ))}
          ); diff --git a/GUI/src/pages/ValidationSessions/index.tsx b/GUI/src/pages/ValidationSessions/index.tsx index 483e25a0..52d569dc 100644 --- a/GUI/src/pages/ValidationSessions/index.tsx +++ b/GUI/src/pages/ValidationSessions/index.tsx @@ -4,17 +4,18 @@ import ValidationSessionCard from 'components/molecules/ValidationSessionCard'; import sse from 'services/sse-service'; import { useQuery } from '@tanstack/react-query'; import { getDatasetGroupsProgress } from 'services/datasets'; +import { ValidationProgressData, SSEEventData } from 'types/datasetGroups'; const ValidationSessions: FC = () => { const { t } = useTranslation(); - const [progresses, setProgresses] = useState([]); + const [progresses, setProgresses] = useState([]); - const { data: progressData } = useQuery( + const { data: progressData } = useQuery( ['datasetgroups/progress'], () => getDatasetGroupsProgress(), { onSuccess: (data) => { - setProgresses(data); + setProgresses(data); }, } ); @@ -22,7 +23,7 @@ const ValidationSessions: FC = () => { useEffect(() => { if (!progressData) return; - const handleUpdate = (sessionId, newData) => { + const handleUpdate = (sessionId: string, newData: SSEEventData) => { setProgresses((prevProgresses) => prevProgresses.map((progress) => progress.id === sessionId ? { ...progress, ...newData } : progress @@ -31,7 +32,7 @@ const ValidationSessions: FC = () => { }; const eventSources = progressData.map((progress) => { - return sse(`/${progress.id}`, 'dataset',(data) => { + return sse(`/${progress.id}`, 'dataset', (data: SSEEventData) => { console.log(`New data for notification ${progress.id}:`, data); handleUpdate(data.sessionId, data); }); @@ -47,20 +48,19 @@ const ValidationSessions: FC = () => {
          -
          Validation Sessions
          +
          {t('validationSessions.title')}
          - {progresses?.map((session) => { - return ( - - ); - })} + {progresses?.map((session) => ( + + ))}
          ); diff --git a/GUI/src/types/dataModels.ts b/GUI/src/types/dataModels.ts index 54500f01..ea789db7 100644 --- a/GUI/src/types/dataModels.ts +++ b/GUI/src/types/dataModels.ts @@ -7,4 +7,20 @@ export type DataModel = { baseModels: string[]; maturity: string; version: string; - }; \ No newline at end of file + }; + +export type TrainingProgressData = { + id: string; + modelName: string; + majorVersion: number; + minorVersion: number; + latest: boolean; + trainingStatus: string; + progressPercentage: number; +}; + +export type SSEEventData = { + sessionId: string; + trainingStatus: string; + progressPercentage: number; +}; \ No newline at end of file diff --git a/GUI/src/types/datasetGroups.ts b/GUI/src/types/datasetGroups.ts index 9fa2bce2..6536bece 100644 --- a/GUI/src/types/datasetGroups.ts +++ b/GUI/src/types/datasetGroups.ts @@ -130,4 +130,23 @@ export type DatasetDetails = { export type SelectedRowPayload = { rowId: number; -} & Record; \ No newline at end of file +} & Record; + +export type ValidationProgressData = { + id: string; + groupName: string; + majorVersion: number; + minorVersion: number; + patchVersion: number; + latest: boolean; + validationStatus: string; + validationMessage?: string; + progressPercentage: number; +}; + +export type SSEEventData = { + sessionId: string; + validationStatus: string; + validationMessage?: string; + progressPercentage: number; +}; \ No newline at end of file From 0ef8b509060f3ec398838b81de55225ae4c2d522 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 12 Aug 2024 12:41:44 +0530 Subject: [PATCH 442/582] file handler constant file changes --- file-handler/constants.py | 21 +++++++++++++-- file-handler/file_handler_api.py | 44 ++++++++++++++++---------------- 2 files changed, 41 insertions(+), 24 deletions(-) diff --git a/file-handler/constants.py b/file-handler/constants.py index 4f8abcce..f50171a9 100644 --- a/file-handler/constants.py +++ b/file-handler/constants.py @@ -42,6 +42,18 @@ "reason": "Failed to download from S3" } +DATASET_DELETION_SUCCESS = { + "status_code": 200, + "message": "Dataset deletion completed successfully.", + "files_deleted": [] +} + +DATASET_DELETION_FAILED = { + "status_code": 500, + "message": "Dataset deletion failed.", + "files_deleted": [] +} + # File extensions JSON_EXT = ".json" YAML_EXT = ".yaml" @@ -50,13 +62,12 @@ # S3 Ferry payload def GET_S3_FERRY_PAYLOAD(destinationFilePath: str, destinationStorageType: str, sourceFilePath: str, sourceStorageType: str): - S3_FERRY_PAYLOAD = { + return { "destinationFilePath": destinationFilePath, "destinationStorageType": destinationStorageType, "sourceFilePath": sourceFilePath, "sourceStorageType": sourceStorageType } - return S3_FERRY_PAYLOAD # Directories UPLOAD_DIRECTORY = os.getenv("UPLOAD_DIRECTORY", "/shared") @@ -71,3 +82,9 @@ def GET_S3_FERRY_PAYLOAD(destinationFilePath: str, destinationStorageType: str, DELETE_STOPWORDS_URL = os.getenv("DELETE_STOPWORDS_URL") DATAGROUP_DELETE_CONFIRMATION_URL = os.getenv("DATAGROUP_DELETE_CONFIRMATION_URL") DATAMODEL_DELETE_CONFIRMATION_URL = os.getenv("DATAMODEL_DELETE_CONFIRMATION_URL") + +# Dataset locations +TEMP_DATASET_LOCATION = "/dataset/{dg_id}/temp/temp_dataset.json" +PRIMARY_DATASET_LOCATION = "/dataset/{dg_id}/primary_dataset/dataset_{dg_id}_aggregated.json" +CHUNK_DATASET_LOCATION = "/dataset/{dg_id}/chunks/{chunk_id}.json" +NEW_DATASET_LOCATION = "/dataset/{new_dg_id}/" diff --git a/file-handler/file_handler_api.py b/file-handler/file_handler_api.py index 779e6953..277a0b16 100644 --- a/file-handler/file_handler_api.py +++ b/file-handler/file_handler_api.py @@ -102,7 +102,7 @@ async def upload_and_copy(request: Request, dgId: int = Form(...), dataFile: Upl with open(json_local_file_path, 'w') as json_file: json.dump(converted_data, json_file, indent=4) - save_location = f"/dataset/{dgId}/temp/temp_dataset{JSON_EXT}" + save_location = TEMP_DATASET_LOCATION.format(dg_id=dgId) source_file_path = file_name.replace(YML_EXT, JSON_EXT).replace(XLSX_EXT, JSON_EXT) response = s3_ferry.transfer_file(save_location, "S3", source_file_path, "FS") @@ -129,7 +129,7 @@ async def download_and_convert(request: Request, exportData: ExportFile, backgro if export_type not in ["xlsx", "yaml", "json"]: raise HTTPException(status_code=500, detail=EXPORT_TYPE_ERROR) - save_location = f"/dataset/{dg_id}/primary_dataset/dataset_{dg_id}_aggregated{JSON_EXT}" + save_location = PRIMARY_DATASET_LOCATION.format(dg_id=dg_id) local_file_name = f"group_{dg_id}_aggregated" response = s3_ferry.transfer_file(f"{local_file_name}{JSON_EXT}", "FS", save_location, "S3") @@ -164,41 +164,41 @@ async def download_and_convert(request: Request, dgId: int, background_tasks: Ba cookie = request.cookies.get("customJwtCookie") await authenticate_user(f'customJwtCookie={cookie}') - saveLocation = f"/dataset/{dgId}/primary_dataset/dataset_{dgId}_aggregated{JSON_EXT}" - localFileName = f"group_{dgId}_aggregated" + save_location = PRIMARY_DATASET_LOCATION.format(dg_id=dgId) + local_file_name = f"group_{dgId}_aggregated" - response = s3_ferry.transfer_file(f"{localFileName}{JSON_EXT}", "FS", saveLocation, "S3") + response = s3_ferry.transfer_file(f"{local_file_name}{JSON_EXT}", "FS", save_location, "S3") if response.status_code != 201: raise HTTPException(status_code=500, detail=S3_DOWNLOAD_FAILED) - jsonFilePath = os.path.join(JSON_FILE_DIRECTORY, f"{localFileName}{JSON_EXT}") + json_file_path = os.path.join(JSON_FILE_DIRECTORY, f"{local_file_name}{JSON_EXT}") - with open(f"{jsonFilePath}", 'r') as jsonFile: - jsonData = json.load(jsonFile) + with open(f"{json_file_path}", 'r') as json_file: + json_data = json.load(json_file) - background_tasks.add_task(os.remove, jsonFilePath) + background_tasks.add_task(os.remove, json_file_path) - return jsonData + return json_data @app.get("/datasetgroup/data/download/json/location") async def download_and_convert(request: Request, saveLocation:str, background_tasks: BackgroundTasks): cookie = request.cookies.get("customJwtCookie") await authenticate_user(f'customJwtCookie={cookie}') - localFileName = saveLocation.split("/")[-1] + local_file_name = saveLocation.split("/")[-1] - response = s3_ferry.transfer_file(f"{localFileName}", "FS", saveLocation, "S3") + response = s3_ferry.transfer_file(f"{local_file_name}", "FS", saveLocation, "S3") if response.status_code != 201: raise HTTPException(status_code=500, detail=S3_DOWNLOAD_FAILED) - jsonFilePath = os.path.join(JSON_FILE_DIRECTORY, f"{localFileName}") + json_file_path = os.path.join(JSON_FILE_DIRECTORY, f"{local_file_name}") - with open(f"{jsonFilePath}", 'r') as jsonFile: - jsonData = json.load(jsonFile) + with open(f"{json_file_path}", 'r') as json_file: + json_data = json.load(json_file) - background_tasks.add_task(os.remove, jsonFilePath) + background_tasks.add_task(os.remove, json_file_path) - return jsonData + return json_data @app.post("/datasetgroup/data/import/chunk") async def upload_and_copy(request: Request, import_chunks: ImportChunks): @@ -214,7 +214,7 @@ async def upload_and_copy(request: Request, import_chunks: ImportChunks): with open(fileLocation, 'w') as jsonFile: json.dump(chunks, jsonFile, indent=4) - saveLocation = f"/dataset/{dgID}/chunks/{exsisting_chunks}{JSON_EXT}" + saveLocation = CHUNK_DATASET_LOCATION.format(dg_id=dgID, chunk_id=exsisting_chunks) response = s3_ferry.transfer_file(saveLocation, "S3", s3_ferry_view_file_location, "FS") if response.status_code == 201: @@ -228,7 +228,7 @@ async def download_and_convert(request: Request, dgId: int, pageId: int, backgro cookie = request.cookies.get("customJwtCookie") await authenticate_user(f'customJwtCookie={cookie}') - save_location = f"/dataset/{dgId}/chunks/{pageId}{JSON_EXT}" + save_location = CHUNK_DATASET_LOCATION.format(dg_id=dgId, chunk_id=pageId) local_file_name = f"group_{dgId}_chunk_{pageId}" response = s3_ferry.transfer_file(f"{local_file_name}{JSON_EXT}", "FS", save_location, "S3") @@ -257,7 +257,7 @@ async def upload_and_copy(request: Request, importData: ImportJsonMajor): with open(fileLocation, 'w') as jsonFile: json.dump(importData.dataset, jsonFile, indent=4) - saveLocation = f"/dataset/{importData.dgId}/primary_dataset/dataset_{importData.dgId}_aggregated{JSON_EXT}" + saveLocation = PRIMARY_DATASET_LOCATION.format(dg_id=importData.dgId) sourceFilePath = fileName.replace(YML_EXT, JSON_EXT).replace(XLSX_EXT, JSON_EXT) response = s3_ferry.transfer_file(saveLocation, "S3", sourceFilePath, "FS") @@ -288,7 +288,7 @@ async def upload_and_copy(request: Request, copyPayload: CopyPayload): for file in files: old_location = f"/dataset/{dg_id}/{file}" - new_location = f"/dataset/{new_dg_id}/{file}" + new_location = NEW_DATASET_LOCATION.format(new_dg_id=new_dg_id) + file response = s3_ferry.transfer_file(local_storage_location, "FS", old_location, "S3") response = s3_ferry.transfer_file(new_location, "S3", local_storage_location, "FS") @@ -300,7 +300,7 @@ async def upload_and_copy(request: Request, copyPayload: CopyPayload): else: os.remove(local_storage_location) upload_success = UPLOAD_SUCCESS.copy() - upload_success["saved_file_path"] = f"/dataset/{new_dg_id}/" + upload_success["saved_file_path"] = NEW_DATASET_LOCATION.format(new_dg_id=new_dg_id) return JSONResponse(status_code=200, content=upload_success) def extract_stop_words(file: UploadFile) -> List[str]: From 0a9013cd1544e98b4e325ff45dcec43a5451ca90 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 12 Aug 2024 12:56:35 +0530 Subject: [PATCH 443/582] clean HTML tags before annonymization --- anonymizer/anonymizer_api.py | 5 ++++- anonymizer/html_cleaner.py | 9 +++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 anonymizer/html_cleaner.py diff --git a/anonymizer/anonymizer_api.py b/anonymizer/anonymizer_api.py index c68b9c1e..c3c986b5 100644 --- a/anonymizer/anonymizer_api.py +++ b/anonymizer/anonymizer_api.py @@ -4,6 +4,7 @@ from ner import NERProcessor from text_processing import TextProcessor from fake_replacements import FakeReplacer +from html_cleaner import HTMLCleaner app = FastAPI() @@ -16,11 +17,13 @@ class OutputText(BaseModel): status: bool ner_processor = NERProcessor() +html_cleaner = HTMLCleaner() @app.post("/process_text", response_model=OutputText) async def process_text(input_text: InputText): try: - text_chunks = TextProcessor.split_text(input_text.text, 2000) + cleaned_text = html_cleaner.remove_html_tags(input_text.text) + text_chunks = TextProcessor.split_text(cleaned_text, 2000) processed_chunks = [] for chunk in text_chunks: diff --git a/anonymizer/html_cleaner.py b/anonymizer/html_cleaner.py new file mode 100644 index 00000000..461e4fa6 --- /dev/null +++ b/anonymizer/html_cleaner.py @@ -0,0 +1,9 @@ +import re + +class HTMLCleaner: + def __init__(self): + pass + + def remove_html_tags(self, text): + clean_text = re.sub(r'<.*?>', '', text) + return clean_text \ No newline at end of file From 7ec2d36fb076e74116452f71988fb539f5c80cb1 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Mon, 12 Aug 2024 13:24:32 +0530 Subject: [PATCH 444/582] sync with user mgt bug fixes --- GUI/src/App.tsx | 72 +++++++++++++------ GUI/src/pages/LoadingScreen/LoadingScreen.tsx | 11 +++ GUI/src/pages/Unauthorized/unauthorized.scss | 30 ++++++++ GUI/src/pages/Unauthorized/unauthorized.tsx | 17 +++++ GUI/src/utils/queryKeys.ts | 4 ++ GUI/translations/en/common.json | 5 +- 6 files changed, 118 insertions(+), 21 deletions(-) create mode 100644 GUI/src/pages/LoadingScreen/LoadingScreen.tsx create mode 100644 GUI/src/pages/Unauthorized/unauthorized.scss create mode 100644 GUI/src/pages/Unauthorized/unauthorized.tsx diff --git a/GUI/src/App.tsx b/GUI/src/App.tsx index 675a8254..f2da3e38 100644 --- a/GUI/src/App.tsx +++ b/GUI/src/App.tsx @@ -1,5 +1,5 @@ -import { FC } from 'react'; -import { Navigate, Route, Routes } from 'react-router-dom'; +import { FC, useEffect, useState } from 'react'; +import { Route, Routes, useNavigate, useLocation } from 'react-router-dom'; import { Layout } from 'components'; import useStore from 'store'; import './locale/et_EE'; @@ -9,42 +9,74 @@ import DatasetGroups from 'pages/DatasetGroups'; import { useQuery } from '@tanstack/react-query'; import { UserInfo } from 'types/userInfo'; import CreateDatasetGroup from 'pages/DatasetGroups/CreateDatasetGroup'; -import ViewDatasetGroup from 'pages/DatasetGroups/ViewDatasetGroup'; import StopWords from 'pages/StopWords'; import ValidationSessions from 'pages/ValidationSessions'; +import { authQueryKeys } from 'utils/queryKeys'; +import { ROLES } from 'enums/roles'; import DataModels from 'pages/DataModels'; import CreateDataModel from 'pages/DataModels/CreateDataModel'; import TrainingSessions from 'pages/TrainingSessions'; +import LoadingScreen from 'pages/LoadingScreen/LoadingScreen'; +import Unauthorized from 'pages/Unauthorized/unauthorized'; const App: FC = () => { - - useQuery<{ - data: { custom_jwt_userinfo: UserInfo }; - }>({ - queryKey: ['auth/jwt/userinfo', 'prod'], + const navigate = useNavigate(); + const location = useLocation(); + const [hasRedirected, setHasRedirected] = useState(false); + const { isLoading, data } = useQuery({ + queryKey: authQueryKeys.USER_DETAILS(), onSuccess: (res: { response: UserInfo }) => { localStorage.setItem('exp', res.response.JWTExpirationTimestamp); - return useStore.getState().setUserInfo(res.response); + useStore.getState().setUserInfo(res.response); }, }); - + + useEffect(() => { + if (!isLoading && data && !hasRedirected && location.pathname === '/') { + const isAdmin = data.response.authorities.some( + (item) => item === ROLES.ROLE_ADMINISTRATOR + ); + if (isAdmin) { + navigate('/user-management'); + } else { + navigate('/dataset-groups'); + } + setHasRedirected(true); + } + }, [isLoading, data, navigate, hasRedirected, location.pathname]); + return ( - - }> - } /> - } /> - } /> - } /> + <> + {isLoading ? ( + + ) : ( + + }> + {data?.response.authorities.some( + (item) => item === ROLES.ROLE_ADMINISTRATOR + ) ? ( + <> + } /> + } /> + + ) : ( + <> + } /> + } /> + + )} + } /> } /> } /> } /> } /> } /> } /> - - - + + + )} + ); }; -export default App; +export default App; \ No newline at end of file diff --git a/GUI/src/pages/LoadingScreen/LoadingScreen.tsx b/GUI/src/pages/LoadingScreen/LoadingScreen.tsx new file mode 100644 index 00000000..679ec206 --- /dev/null +++ b/GUI/src/pages/LoadingScreen/LoadingScreen.tsx @@ -0,0 +1,11 @@ +import { FC } from 'react'; + +const LoadingScreen: FC = () => { + return ( +
          +

          Loading...

          +
          + ); +}; + +export default LoadingScreen; \ No newline at end of file diff --git a/GUI/src/pages/Unauthorized/unauthorized.scss b/GUI/src/pages/Unauthorized/unauthorized.scss new file mode 100644 index 00000000..60135898 --- /dev/null +++ b/GUI/src/pages/Unauthorized/unauthorized.scss @@ -0,0 +1,30 @@ +.unauthorized-container { + display: flex; + align-items: center; + justify-content: center; + height: 100vh; + background-color: #f0f2f5; + padding: 20px; + box-sizing: border-box; + } + + .unauthorized-card { + background-color: #fff; + padding: 40px; + border-radius: 8px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + text-align: center; + max-width: 400px; + width: 100%; + } + + .unauthorized-header { + font-size: 2.5em; + color: #333; + margin-bottom: 20px; + } + + .unauthorized-message { + font-size: 1.2em; + color: #555; + } \ No newline at end of file diff --git a/GUI/src/pages/Unauthorized/unauthorized.tsx b/GUI/src/pages/Unauthorized/unauthorized.tsx new file mode 100644 index 00000000..23dbc5b0 --- /dev/null +++ b/GUI/src/pages/Unauthorized/unauthorized.tsx @@ -0,0 +1,17 @@ +import { FC } from 'react'; +import './unauthorized.scss'; +import { useTranslation } from 'react-i18next'; + +const Unauthorized: FC = () => { + const { t } = useTranslation(); + return ( +
          +
          +

          {t('global.unAuthorized')}

          +

          {t('global.unAuthorizedDesc')}

          +
          +
          + ); +}; + +export default Unauthorized; \ No newline at end of file diff --git a/GUI/src/utils/queryKeys.ts b/GUI/src/utils/queryKeys.ts index ffb200af..b251d31b 100644 --- a/GUI/src/utils/queryKeys.ts +++ b/GUI/src/utils/queryKeys.ts @@ -53,3 +53,7 @@ export const datasetQueryKeys = { export const stopWordsQueryKeys = { GET_ALL_STOP_WORDS: () => [`datasetgroups/stopwords`], }; + +export const authQueryKeys = { + USER_DETAILS: () => ['auth/jwt/userinfo', 'prod'], +}; \ No newline at end of file diff --git a/GUI/translations/en/common.json b/GUI/translations/en/common.json index c95c977e..1444c612 100644 --- a/GUI/translations/en/common.json +++ b/GUI/translations/en/common.json @@ -40,7 +40,10 @@ "asc": "asc", "desc": "desc", "reset": "Reset", - "choose": "Choose" + "choose": "Choose", + "extedSession": "Extend Session", + "unAuthorized": "Unauthorized", + "unAuthorizedDesc": "You do not have permission to view this page." }, "menu": { "userManagement": "User Management", From bb3c8e5f284b396d2d9b2a85e3669e44583df9fd Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 12 Aug 2024 14:40:13 +0530 Subject: [PATCH 445/582] model inference and hirachy validation --- hierarchy_validation/Dockerfile | 16 ++ hierarchy_validation/constants.py | 23 ++ hierarchy_validation/docker-compose.yml | 47 ++++ .../hierarchy_validation_api.py | 49 ++++ hierarchy_validation/requirements.txt | 6 + hierarchy_validation/utils.py | 96 +++++++ model_inference/Dockerfile | 18 ++ model_inference/constants.py | 6 + model_inference/docker-compose.yml | 47 ++++ model_inference/inference_wrapper.py | 46 ++++ model_inference/model_inference_api.py | 242 ++++++++++++++++++ model_inference/requirements.txt | 32 +++ model_inference/s3_ferry.py | 21 ++ model_inference/utils.py | 28 ++ 14 files changed, 677 insertions(+) create mode 100644 hierarchy_validation/Dockerfile create mode 100644 hierarchy_validation/constants.py create mode 100644 hierarchy_validation/docker-compose.yml create mode 100644 hierarchy_validation/hierarchy_validation_api.py create mode 100644 hierarchy_validation/requirements.txt create mode 100644 hierarchy_validation/utils.py create mode 100644 model_inference/Dockerfile create mode 100644 model_inference/constants.py create mode 100644 model_inference/docker-compose.yml create mode 100644 model_inference/inference_wrapper.py create mode 100644 model_inference/model_inference_api.py create mode 100644 model_inference/requirements.txt create mode 100644 model_inference/s3_ferry.py create mode 100644 model_inference/utils.py diff --git a/hierarchy_validation/Dockerfile b/hierarchy_validation/Dockerfile new file mode 100644 index 00000000..029403f7 --- /dev/null +++ b/hierarchy_validation/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.9-slim +RUN addgroup --system appuser && adduser --system --ingroup appuser appuser +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY constants.py . +COPY hierarchy_validation_api.py . +COPY utils.py . + +RUN mkdir -p /shared && chown appuser:appuser /shared && chmod 770 /shared +RUN chown -R appuser:appuser /app +EXPOSE 8009 +USER appuser + +CMD ["uvicorn", "hierarchy_validation_api:app", "--host", "0.0.0.0", "--port", "8009"] diff --git a/hierarchy_validation/constants.py b/hierarchy_validation/constants.py new file mode 100644 index 00000000..8a4abc87 --- /dev/null +++ b/hierarchy_validation/constants.py @@ -0,0 +1,23 @@ +from pydantic import BaseModel +from typing import List + + +GRAPH_API_BASE_URL = "https://graph.microsoft.com/v1.0" + +class ClassHierarchy(BaseModel): + class_name: str + subclasses: List['ClassHierarchy'] = [] + + +class Folder(BaseModel): + id: str + displayName: str + childFolders: List['Folder'] = [] + + +class HierarchyCheckRequest(BaseModel): + classHierarchies: List[ClassHierarchy] + + +class FlattenedFolderHierarchy(BaseModel): + hierarchy:List[str] \ No newline at end of file diff --git a/hierarchy_validation/docker-compose.yml b/hierarchy_validation/docker-compose.yml new file mode 100644 index 00000000..a2250a09 --- /dev/null +++ b/hierarchy_validation/docker-compose.yml @@ -0,0 +1,47 @@ +version: '3.8' + +services: + init: + image: busybox + command: ["sh", "-c", "chmod -R 777 /shared"] + volumes: + - shared-volume:/shared + + receiver: + build: + context: . + dockerfile: Dockerfile + container_name: file-receiver + volumes: + - shared-volume:/shared + environment: + - UPLOAD_DIRECTORY=/shared + - JIRA_MODEL_DOWNLOAD_DIRECTORY=/shared/models/jira + - OUTLOOK_MODEL_DOWNLOAD_DIRECTORY=/shared/models/outlook + ports: + - "8009:8009" + depends_on: + - init + + api: + image: s3-ferry:latest + container_name: s3-ferry + volumes: + - shared-volume:/shared + env_file: + - config.env + environment: + - S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID} + - S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY} + ports: + - "3000:3000" + depends_on: + - receiver + - init + +volumes: + shared-volume: + +networks: + default: + driver: bridge diff --git a/hierarchy_validation/hierarchy_validation_api.py b/hierarchy_validation/hierarchy_validation_api.py new file mode 100644 index 00000000..72c06d36 --- /dev/null +++ b/hierarchy_validation/hierarchy_validation_api.py @@ -0,0 +1,49 @@ +from fastapi import FastAPI, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from constants import HierarchyCheckRequest, FlattenedFolderHierarchy +from utils import build_folder_hierarchy, validate_hierarchy, find_folder_id, get_corrected_folder_hierarchy + +app = FastAPI() + +app.add_middleware( + CORSMiddleware, + allow_origins = ["*"], + allow_credentials = True, + allow_methods = ["GET", "POST"], + allow_headers = ["*"], +) + +@app.get("/api/folder-hierarchy") +async def get_folder_hierarchy(): + try: + hierarchy = await build_folder_hierarchy() + return hierarchy + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to fetch folder hierarchy: {str(e)}") + +@app.post("/api/check-folder-hierarchy") +async def check_folder_hierarchy(request: HierarchyCheckRequest): + result = await validate_hierarchy(request.classHierarchies) + return result + +@app.post("/api/find-folder-id") +async def get_folder_id(flattened_hierarchy: FlattenedFolderHierarchy): + try: + hierarchy = await build_folder_hierarchy() + folder_id = find_folder_id(hierarchy, flattened_hierarchy.hierarchy) + return {"folder_id": folder_id} + except ValueError as e: + raise HTTPException(status_code=404, detail=str(e)) + except Exception as e: + raise HTTPException(status_code=500, detail=f"An error occurred: {str(e)}") + +@app.get("/api/get-corrected-folder-hierarchy") +async def get_hierarchy(folderId: str): + try: + hierarchy = await build_folder_hierarchy() + folder_path = get_corrected_folder_hierarchy(hierarchy, folderId) + return {"folder_hierarchy": folder_path} + except ValueError as e: + raise HTTPException(status_code=404, detail=str(e)) + except Exception as e: + raise HTTPException(status_code=500, detail=f"An error occurred: {str(e)}") \ No newline at end of file diff --git a/hierarchy_validation/requirements.txt b/hierarchy_validation/requirements.txt new file mode 100644 index 00000000..0c14a900 --- /dev/null +++ b/hierarchy_validation/requirements.txt @@ -0,0 +1,6 @@ +fastapi==0.111.1 +fastapi-cli==0.0.4 +httpx==0.27.0 +pydantic==2.8.2 +pydantic_core==2.20.1 +uvicorn==0.30.3 \ No newline at end of file diff --git a/hierarchy_validation/utils.py b/hierarchy_validation/utils.py new file mode 100644 index 00000000..1826bc1e --- /dev/null +++ b/hierarchy_validation/utils.py @@ -0,0 +1,96 @@ +from fastapi import HTTPException +import httpx +from constants import (GRAPH_API_BASE_URL, Folder, ClassHierarchy) +from typing import List + +async def fetch_folders(folder_id: str = 'root', ACCESS_TOKEN: str = 'none'): + url = f"{GRAPH_API_BASE_URL}/me/mailFolders" + if folder_id != 'root': + url = f"{GRAPH_API_BASE_URL}/me/mailFolders/{folder_id}/childFolders" + + headers = { + "Authorization": f"Bearer {ACCESS_TOKEN}", + "Content-Type": "application/json" + } + + all_folders = [] + async with httpx.AsyncClient() as client: + while url: + response = await client.get(url, headers=headers) + if response.status_code != 200: + raise HTTPException(status_code=500, detail=f"Failed to fetch folders: {response.text}") + + data = response.json() + all_folders.extend(data['value']) + url = data.get('@odata.nextLink') + + return all_folders + +async def build_folder_hierarchy(folder_id: str = 'root'): + folders = await fetch_folders(folder_id) + + async def build_hierarchy(folder): + child_folders = await build_folder_hierarchy(folder['id']) + return Folder( + id=folder['id'], + displayName=folder['displayName'], + childFolders=child_folders + ) + + return [await build_hierarchy(folder) for folder in folders] + + +async def validate_hierarchy(class_hierarchies: List[ClassHierarchy]): + errors = [] + folder_hierarchy = await build_folder_hierarchy() + + def find_folder(name: str, folders: List[Folder]): + return next((folder for folder in folders if folder.displayName == name), None) + + def check_hierarchy(classes: List[ClassHierarchy], folders: List[Folder], path: str): + for cls in classes: + folder = find_folder(cls.class_name, folders) + if not folder: + errors.append(f"Folder '{cls.class_name}' not found at path '{path}'") + return False + if cls.subclasses: + if not check_hierarchy(cls.subclasses, folder.childFolders, f"{path}/{cls.class_name}"): + return False + return True + + result = check_hierarchy(class_hierarchies, folder_hierarchy, '') + return {"isValid": result, "errors": errors} + + +def find_folder_id(hierarchy: List[Folder], path: List[str]): + current_level = hierarchy + for folder_name in path: + found = False + for folder in current_level: + if folder.displayName.lower() == folder_name.lower(): + if folder_name == path[-1]: + return folder.id + current_level = folder.childFolders + found = True + break + if not found: + raise ValueError(f"Folder '{folder_name}' not found in the given path") + raise ValueError("Path is empty or invalid") + + +def get_corrected_folder_hierarchy(hierarchy: List[Folder], final_folder_id: str): + def search_hierarchy(folders: List[Folder], target_id: str, current_path: List[str]): + for folder in folders: + new_path = current_path + [folder.displayName] + if folder.id == target_id: + return new_path + if folder.childFolders: + result = search_hierarchy(folder.childFolders, target_id, new_path) + if result: + return result + return [] + + result = search_hierarchy(hierarchy, final_folder_id, []) + if not result: + raise ValueError(f"Folder with ID '{final_folder_id}' not found in the hierarchy") + return result diff --git a/model_inference/Dockerfile b/model_inference/Dockerfile new file mode 100644 index 00000000..56a42f22 --- /dev/null +++ b/model_inference/Dockerfile @@ -0,0 +1,18 @@ +FROM python:3.9-slim +RUN addgroup --system appuser && adduser --system --ingroup appuser appuser +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY constants.py . +COPY inference_wrapper.py . +COPY model_inference_api.py . +COPY s3_ferry.py . +COPY utils.py . + +RUN mkdir -p /shared && chown appuser:appuser /shared && chmod 770 /shared +RUN chown -R appuser:appuser /app +EXPOSE 8003 +USER appuser + +CMD ["uvicorn", "model_inference_api:app", "--host", "0.0.0.0", "--port", "8003"] \ No newline at end of file diff --git a/model_inference/constants.py b/model_inference/constants.py new file mode 100644 index 00000000..59fdfbb6 --- /dev/null +++ b/model_inference/constants.py @@ -0,0 +1,6 @@ +S3_DOWNLOAD_FAILED = { + "upload_status": 500, + "operation_successful": False, + "saved_file_path": None, + "reason": "Failed to download from S3" +} diff --git a/model_inference/docker-compose.yml b/model_inference/docker-compose.yml new file mode 100644 index 00000000..bcbc604d --- /dev/null +++ b/model_inference/docker-compose.yml @@ -0,0 +1,47 @@ +version: '3.8' + +services: + init: + image: busybox + command: ["sh", "-c", "chmod -R 777 /shared"] + volumes: + - shared-volume:/shared + + receiver: + build: + context: . + dockerfile: Dockerfile + container_name: file-receiver + volumes: + - shared-volume:/shared + environment: + - UPLOAD_DIRECTORY=/shared + - JIRA_MODEL_DOWNLOAD_DIRECTORY=/shared/models/jira + - OUTLOOK_MODEL_DOWNLOAD_DIRECTORY=/shared/models/outlook + ports: + - "8003:8003" + depends_on: + - init + + api: + image: s3-ferry:latest + container_name: s3-ferry + volumes: + - shared-volume:/shared + env_file: + - config.env + environment: + - S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID} + - S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY} + ports: + - "3000:3000" + depends_on: + - receiver + - init + +volumes: + shared-volume: + +networks: + default: + driver: bridge diff --git a/model_inference/inference_wrapper.py b/model_inference/inference_wrapper.py new file mode 100644 index 00000000..d11a44c1 --- /dev/null +++ b/model_inference/inference_wrapper.py @@ -0,0 +1,46 @@ +class Inference: + def __init__(self) -> None: + pass + + def predict(text:str): + pass + +class InferenceWrapper: + + def __init__(self) -> None: + self.active_jira_model = None + self.active_outlook_model = None + + def model_swapping(self, model_path:str, best_performing_model:str, deployment_platform:str): + try: + + if(deployment_platform == "jira"): + temp_jira_model = Inference(model_path, best_performing_model) + self.active_jira_model = temp_jira_model + return True + + elif(deployment_platform == "outlook"): + temp_outlook_model = Inference(model_path, best_performing_model) + self.active_outlook_model = temp_outlook_model + return True + + except Exception as e: + raise Exception(f"Failed to instantiate the Inference Pipeline. Reason: {e}") + + def inference(self, text:str, deployment_platform:str): + result = [] + if(deployment_platform == "jira" and self.active_jira_model): + result = self.active_jira_model.predict(text) + + if(deployment_platform == "outlook" and self.active_outlook_model): + result = self.active_outlook_model.predict(text) + + return result + + def stop_model(self,deployment_platform:str): + if(deployment_platform == "jira"): + self.active_jira_model = None + if(deployment_platform == "outlook"): + self.active_outlook_model = None + + \ No newline at end of file diff --git a/model_inference/model_inference_api.py b/model_inference/model_inference_api.py new file mode 100644 index 00000000..37485e59 --- /dev/null +++ b/model_inference/model_inference_api.py @@ -0,0 +1,242 @@ +from fastapi import FastAPI,HTTPException, Request, BackgroundTasks +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse +import os +from s3_ferry import S3Ferry +from utils import unzip_file, clear_folder_contents +from pydantic import BaseModel +from constants import S3_DOWNLOAD_FAILED +import requests +from inference_wrapper import InferenceWrapper + + +app = FastAPI() + +app.add_middleware( + CORSMiddleware, + allow_origins = ["*"], + allow_credentials = True, + allow_methods = ["GET", "POST"], + allow_headers = ["*"], +) + +class UpdateRequest(BaseModel): + modelId: str + replaceDeployment:bool + replaceDeploymentPlatform:str + bestModelName:str + +class OutlookInferenceRequest(BaseModel): + inputId:int + inputText:str + isCorrected:bool + finalFolderId:int + +inference_obj = InferenceWrapper() + +S3_FERRY_URL = os.getenv("S3_FERRY_URL") +s3_ferry = S3Ferry(S3_FERRY_URL) +RUUTER_PRIVATE_URL = os.getenv("RUUTER_PRIVATE_URL") +JIRA_MODEL_DOWNLOAD_DIRECTORY = os.getenv("JIRA_MODEL_DOWNLOAD_DIRECTORY", "/shared/models/jira") +OUTLOOK_MODEL_DOWNLOAD_DIRECTORY = os.getenv("OUTLOOK_MODEL_DOWNLOAD_DIRECTORY", "/shared/models/outlook") + +if not os.path.exists(JIRA_MODEL_DOWNLOAD_DIRECTORY): + os.makedirs(JIRA_MODEL_DOWNLOAD_DIRECTORY) + +if not os.path.exists(OUTLOOK_MODEL_DOWNLOAD_DIRECTORY): + os.makedirs(OUTLOOK_MODEL_DOWNLOAD_DIRECTORY) + +async def authenticate_user(request: Request): + cookie = request.cookies.get("customJwtCookie") + if not cookie: + raise HTTPException(status_code=401, detail="No cookie found in the request") + + url = f"{RUUTER_PRIVATE_URL}/auth/jwt/userinfo" + headers = { + 'cookie': f'customJwtCookie={cookie}' + } + + response = requests.get(url, headers=headers) + if response.status_code != 200: + raise HTTPException(status_code=response.status_code, detail="Authentication failed") + +@app.post("/deployment/jira/update") +async def download_document(request: Request, modelData:UpdateRequest, backgroundTasks: BackgroundTasks): + + saveLocation = f"/models/{modelData.modelId}/{modelData.modelId}.zip" + + try: + await authenticate_user(request) + local_file_name = f"{modelData.modelId}.zip" + local_file_path = f"/models/jira/{local_file_name}" + + ## Get class hierarchy and validate it + + # Get group id from the model id + # get class hierarchy using group id + + # 1. Clear the current content inside the folder + folder_path = os.path.join("..", "shared", "models", "jira") + clear_folder_contents(folder_path) + + # 2. Download the new Model + response = s3_ferry.transfer_file(local_file_path, "FS", saveLocation, "S3") + if response.status_code != 201: + raise HTTPException(status_code = 500, detail = S3_DOWNLOAD_FAILED) + + zip_file_path = os.path.join("..", f"shared/models/jira", local_file_name) + extract_file_path = os.path.join("..", f"shared/models/jira") + + # 3. Unzip Model Content + unzip_file(zip_path=zip_file_path, extract_to=extract_file_path) + + backgroundTasks.add_task(os.remove, zip_file_path) + + + #3. TODO : Replace the content in other folder if it a replacement --> Call the delete endpoint + if(UpdateRequest.replaceDeployment): + folder_path = os.path.join("..", "shared", "models", {UpdateRequest.replaceDeploymentPlatform}) + clear_folder_contents(folder_path) + + inference_obj.stop_model(deployment_platform=UpdateRequest.replaceDeploymentPlatform) + + # 4. TODO : Instantiate Munsif's Inference Model + model_path = f"shared/models/jira/{UpdateRequest.modelId}" + best_model = UpdateRequest.bestModelName + + model_initiate = inference_obj.model_swapping(model_path, best_model, deployment_platform="jira") + + if(model_initiate): + return JSONResponse(status_code=200, content="Success") + + except Exception as e: + raise HTTPException(status_code = 500, detail=str(e)) + + + +@app.post("/deployment/outlook/update") +async def download_document(request: Request, modelData:UpdateRequest, backgroundTasks: BackgroundTasks): + + saveLocation = f"/models/{modelData.modelId}/{modelData.modelId}.zip" + + try: + await authenticate_user(request) + local_file_name = f"{modelData.modelId}.zip" + local_file_path = f"/models/jira/{local_file_name}" + + # before all + + + # 1. Clear the current content inside the folder + folder_path = os.path.join("..", "shared", "models", "outlook") + clear_folder_contents(folder_path) + + # 2. Download the new Model + response = s3_ferry.transfer_file(local_file_path, "FS", saveLocation, "S3") + if response.status_code != 201: + raise HTTPException(status_code = 500, detail = S3_DOWNLOAD_FAILED) + + zip_file_path = os.path.join("..", f"shared/models/outlook", local_file_name) + extract_file_path = os.path.join("..", f"shared/models/outlook") + + # 3. Unzip Model Content + unzip_file(zip_path=zip_file_path, extract_to=extract_file_path) + + backgroundTasks.add_task(os.remove, zip_file_path) + + + #3. TODO : Replace the content in other folder if it a replacement --> Call the delete endpoint + if(UpdateRequest.replaceDeployment): + folder_path = os.path.join("..", "shared", "models", {UpdateRequest.replaceDeploymentPlatform}) + clear_folder_contents(folder_path) + + inference_obj.stop_model(deployment_platform=UpdateRequest.replaceDeploymentPlatform) + + # 4. TODO : Instantiate Munsif's Inference Model + model_path = f"shared/models/outlook/{UpdateRequest.modelId}" + best_model = UpdateRequest.bestModelName + + model_initiate = inference_obj.model_swapping(model_path, best_model, deployment_platform="outlook") + + if(model_initiate): + return JSONResponse(status_code=200, content="Success") + + except Exception as e: + raise HTTPException(status_code = 500, detail=str(e)) + + + + +@app.post("/deployment/jira/delete") +async def delete_folder_content(request:Request): + try: + await authenticate_user(request) + folder_path = os.path.join("..", "shared", "models", "jira") + clear_folder_contents(folder_path) + + # TODO : Stop Server Functionality + inference_obj.stop_model(deployment_platform="jira") + + delete_success = {"message" : "Model Deleted Successfully!"} + return JSONResponse(status_code = 200, content = delete_success) + + except Exception as e: + raise HTTPException(status_code = 500, detail=str(e)) + + +@app.post("/deployment/outlook/delete") +async def delete_folder_content(request:Request): + try: + await authenticate_user(request) + folder_path = os.path.join("..", "shared", "models", "outlook") + clear_folder_contents(folder_path) + + # TODO : Stop Server Functionality + inference_obj.stop_model(deployment_platform="outlook") + + delete_success = {"message" : "Model Deleted Successfully!"} + return JSONResponse(status_code = 200, content = delete_success) + + except Exception as e: + raise HTTPException(status_code = 500, detail=str(e)) + + +@app.post("/classifier/deployment/outlook/inference") +async def outlook_inference(request:Request, inferenceData:OutlookInferenceRequest): + try: + await authenticate_user(request) + + print(inferenceData) + + ## Check Whether this is a corrected email or not --> if(inferenceData. isCorrected) + + if(inferenceData.isCorrected): + pass + # No Inference + ## TODO : What's the process in here? + + else: ## New Email + # Call inference + prediction = inference_obj.inference(inferenceData.inputText, deployment_platform="outlook") + + ## TODO : Call inference/create endpoint + + ## Call Kalsara's Outlook endpoint + + + + except Exception as e: + raise HTTPException(status_code = 500, detail=str(e)) + + + +@app.post("/classifier/deployment/jira/inference") +async def jira_inference(request:Request, inferenceData:OutlookInferenceRequest): + try: + await authenticate_user(request) + + print(inferenceData) + + + except Exception as e: + raise HTTPException(status_code = 500, detail=str(e)) \ No newline at end of file diff --git a/model_inference/requirements.txt b/model_inference/requirements.txt new file mode 100644 index 00000000..198d4b35 --- /dev/null +++ b/model_inference/requirements.txt @@ -0,0 +1,32 @@ +fastapi==0.111.1 +fastapi-cli==0.0.4 +httpx==0.27.0 +huggingface-hub==0.24.2 +numpy==1.26.4 +pydantic==2.8.2 +pydantic_core==2.20.1 +Pygments==2.18.0 +python-dotenv==1.0.1 +python-multipart==0.0.9 +PyYAML==6.0.1 +regex==2024.7.24 +requests==2.32.3 +rich==13.7.1 +safetensors==0.4.3 +scikit-learn==0.24.2 +sentencepiece==0.2.0 +setuptools==69.5.1 +shellingham==1.5.4 +sniffio==1.3.1 +starlette==0.37.2 +sympy==1.13.1 +tokenizers==0.19.1 +torch==2.4.0 +torchaudio==2.4.0 +torchvision==0.19.0 +tqdm==4.66.4 +transformers==4.43.3 +typer==0.12.3 +typing_extensions==4.12.2 +urllib3==2.2.2 +uvicorn==0.30.3 \ No newline at end of file diff --git a/model_inference/s3_ferry.py b/model_inference/s3_ferry.py new file mode 100644 index 00000000..54b1d46d --- /dev/null +++ b/model_inference/s3_ferry.py @@ -0,0 +1,21 @@ +import requests +from utils import get_s3_payload + +class S3Ferry: + def __init__(self, url): + self.url = url + + def transfer_file(self, destinationFilePath, destinationStorageType, sourceFilePath, sourceStorageType): + print("Transfer File Method Calling") + print(f"Destination Path :{destinationFilePath}", + f"Destination Storage :{destinationStorageType}", + f"Source File Path :{sourceFilePath}", + f"Source Storage Type :{sourceStorageType}", + sep="\n" + ) + payload = get_s3_payload(destinationFilePath, destinationStorageType, sourceFilePath, sourceStorageType) + print(payload) + print(f"url : {self.url}") + response = requests.post(self.url, json=payload) + print(response) + return response diff --git a/model_inference/utils.py b/model_inference/utils.py new file mode 100644 index 00000000..b1834442 --- /dev/null +++ b/model_inference/utils.py @@ -0,0 +1,28 @@ +import zipfile +import os +import shutil + +def unzip_file(zip_path, extract_to): + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + zip_ref.extractall(extract_to) + + +def clear_folder_contents(folder_path: str): + try: + for filename in os.listdir(folder_path): + file_path = os.path.join(folder_path, filename) + if os.path.isfile(file_path) or os.path.islink(file_path): + os.unlink(file_path) + elif os.path.isdir(file_path): + shutil.rmtree(file_path) + except Exception as e: + raise Exception(f"Failed to delete contents in {folder_path}. Reason: {e}") + +def get_s3_payload(destinationFilePath:str, destinationStorageType:str, sourceFilePath:str, sourceStorageType:str): + S3_FERRY_PAYLOAD = { + "destinationFilePath": destinationFilePath, + "destinationStorageType": destinationStorageType, + "sourceFilePath": sourceFilePath, + "sourceStorageType": sourceStorageType + } + return S3_FERRY_PAYLOAD \ No newline at end of file From 97991971b4ede4a306cb2e456c0462d4b762af53 Mon Sep 17 00:00:00 2001 From: Pamoda Dilranga <51948729+pamodaDilranga@users.noreply.github.com> Date: Mon, 12 Aug 2024 14:43:19 +0530 Subject: [PATCH 446/582] Update anonymizer_api.py with api endpoint name --- anonymizer/anonymizer_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/anonymizer/anonymizer_api.py b/anonymizer/anonymizer_api.py index c3c986b5..3d549b75 100644 --- a/anonymizer/anonymizer_api.py +++ b/anonymizer/anonymizer_api.py @@ -19,7 +19,7 @@ class OutputText(BaseModel): ner_processor = NERProcessor() html_cleaner = HTMLCleaner() -@app.post("/process_text", response_model=OutputText) +@app.post("/anonymize", response_model=OutputText) async def process_text(input_text: InputText): try: cleaned_text = html_cleaner.remove_html_tags(input_text.text) From b9831d72ce5d03e92f81baad08baa61c9ccff474 Mon Sep 17 00:00:00 2001 From: Thirunayan22 Date: Mon, 12 Aug 2024 15:10:49 +0530 Subject: [PATCH 447/582] added model trainer scripts --- .DS_Store | Bin 0 -> 6148 bytes model_trainer/Dockerfile | 17 ++ model_trainer/constants.py | 6 + model_trainer/datapipeline.py | 107 ++++++++++++ model_trainer/inference.py | 114 +++++++++++++ model_trainer/model_trainer.py | 109 ++++++++++++ model_trainer/model_upload_requirements.txt | 61 +++++++ model_trainer/requirements.txt | 2 - model_trainer/s3_ferry.py | 20 +++ model_trainer/trainingpipeline.py | 175 ++++++++++++++++++++ 10 files changed, 609 insertions(+), 2 deletions(-) create mode 100644 .DS_Store create mode 100644 model_trainer/Dockerfile create mode 100644 model_trainer/constants.py create mode 100644 model_trainer/datapipeline.py create mode 100644 model_trainer/inference.py create mode 100644 model_trainer/model_trainer.py create mode 100644 model_trainer/model_upload_requirements.txt delete mode 100644 model_trainer/requirements.txt create mode 100644 model_trainer/s3_ferry.py create mode 100644 model_trainer/trainingpipeline.py diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5783d19b6cf2f71276cbfff6611ef4c650b8a738 GIT binary patch literal 6148 zcmeHK!AiqG5S?wSO(}&Q6nYGJEm*4*ikDF94;aydN=!)5V49UQwTDv3S%1hc@q3)v z-H4^?!GlPdf!Q}ZJG0Bagq>Xg5TjXm0MG^i3ze|az~&2~and!Z7!RRPb0pwF4jK3m zuSK)tKQchyt_2g25JCi>zh8>|ESOIpgG2@x^%^`T@ig!C-bAHZ+uE*Mb*pLJxc74I z=6*h#j{NM3dgoFmL23KJMI6n&_Rfh+b3aa_OeMr&gdx|LaT?0`NY2u5s&ZXDU^T3U z*X}G9{o$Z%ANBgnuDuxcduSgHmdl2N-)XF%ISy`ADicqVg-&Wxu9F5#E z1I)lG16AE^Q~f{v{{6q2#64z!8Q3TWL~ZPjN4O= 2: + top_level_classes = [node['class'] for node in data] + models.append({model_num: top_level_classes}) + filters.append([top_level_classes]) + model_num += 1 + + def traverse(node, current_filters): + nonlocal model_num + if node.get('subclasses'): + if len(node['subclasses']) >= 2: + class_names = [subclass['class'] for subclass in node['subclasses']] + models.append({node['class']: class_names}) + filters.append(current_filters + [[node['class']]] + [class_names]) + model_num += 1 + for subclass in node['subclasses']: + traverse(subclass, current_filters + [[node['class']]]) + + for root_node in data: + traverse(root_node, []) + + return models, filters + + + def filter_dataframe_by_values(self, filters): + filtered_df = self.df.copy() + for filter_list in filters: + filtered_df = filtered_df[filtered_df.apply(lambda row: any(value in row.values for value in filter_list), axis=1)] + return filtered_df + + def create_dataframes(self): + dfs = [] + input_columns = self.extract_input_columns() + models, filters = self.models_and_filters() + for i in range(len(models)): + filtered_df = self.filter_dataframe_by_values(filters[i]) + target_column = self.find_target_column(filtered_df, filters[i][-1])[0] + filtered_df['input'] = filtered_df[input_columns].apply(lambda row: ' '.join(row.dropna().astype(str)), axis=1) + filtered_df = filtered_df.rename(columns={target_column: 'target'}) + filtered_df = filtered_df[['input', 'target']] + filtered_df = filtered_df.dropna() + dfs.append(filtered_df) + + return dfs \ No newline at end of file diff --git a/model_trainer/inference.py b/model_trainer/inference.py new file mode 100644 index 00000000..957abd88 --- /dev/null +++ b/model_trainer/inference.py @@ -0,0 +1,114 @@ +from transformers import XLMRobertaTokenizer, XLMRobertaForSequenceClassification, XLNetForSequenceClassification, XLNetTokenizer, BertForSequenceClassification, BertTokenizer +import pickle +import torch +import os +import json + +class InferencePipeline: + + def __init__(self, hierarchy_path, model_name, path_models_folder,path_label_encoder, path_classification_folder, models): + + if model_name == 'xlnet-base-cased': + self.base_model = XLNetForSequenceClassification.from_pretrained('xlnet-base-cased') + self.tokenizer = XLNetTokenizer.from_pretrained('xlnet-base-cased') + + elif model_name == 'xlm-roberta-base': + self.base_model = XLMRobertaForSequenceClassification.from_pretrained('xlm-roberta-base') + self.tokenizer = XLMRobertaTokenizer.from_pretrained('xlm-roberta-base') + + elif model_name == 'bert-base-uncased': + self.tokenizer = BertTokenizer.from_pretrained('bert-base-uncased') + self.base_model = BertForSequenceClassification.from_pretrained('bert-base-uncased') + + self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + self.models = models + self.model_name = model_name + + label_encoder_names = os.listdir(path_label_encoder) + label_encoder_names = sorted(label_encoder_names, key=lambda x: int(x.split('_')[-1].split('.')[0])) + self.label_encoder_dict = {} + for i in range(len(label_encoder_names)): + with open(os.path.join(path_label_encoder,label_encoder_names[i]), 'rb') as file: + self.label_encoder_dict[i] = pickle.load(file) + + model_names = os.listdir(path_models_folder) + model_names = sorted(model_names, key=lambda x: int(x.split('_')[-1].split('.')[0])) + self.models_dict = {} + for i in range(len(model_names)): + self.models_dict[i] = torch.load(os.path.join(path_models_folder,model_names[i])) + + classification_model_names = os.listdir(path_classification_folder) + classification_model_names = sorted(classification_model_names, key=lambda x: int(x.split('_')[-1].split('.')[0])) + self.classification_models_dict = {} + for i in range(len(classification_model_names)): + self.classification_models_dict[i] = torch.load(os.path.join(path_classification_folder,classification_model_names[i])) + + with open(hierarchy_path, 'r', encoding='utf-8') as file: + self.hierarchy_file = json.load(file) + + def find_index(self, data, search_dict): + for index, d in enumerate(data): + if d == search_dict: + return index + return None + + def predict_class(self,text_input): + inputs = self.tokenizer(text_input, truncation=True, padding=True, return_tensors='pt') + inputs.to(self.device) + inputs = {key: val.to(self.device) for key, val in inputs.items()} + predicted_classes = [] + self.base_model.to(self.device) + i = 0 + data = self.hierarchy_file['classHierarchy'] + parent = 1 + while data: + current_classes = {parent: [d['class'] for d in data]} + model_num = self.find_index(self.models, current_classes) + if model_num is None: + break + label_encoder = self.label_encoder_dict[model_num] + num_labels = len(label_encoder.classes_) + + if self.model_name == 'xlnet-base-cased': + self.base_model.classifier = XLNetForSequenceClassification.from_pretrained('xlnet-base-cased', num_labels=num_labels).classifier + + elif self.model_name == 'xlm-roberta-base': + self.base_model.classifier = XLMRobertaForSequenceClassification.from_pretrained('xlm-roberta-base', num_labels=num_labels).classifier + self.base_model.roberta.encoder.layer[-2:].load_state_dict(self.models_dict[model_num]) + + elif self.model_name == 'bert-base-uncased': + self.base_model.classifier = BertForSequenceClassification.from_pretrained('bert-base-uncased', num_labels=num_labels).classifier + self.base_model.base_model.encoder.layer[-2:].load_state_dict(self.models_dict[model_num]) + + self.base_model.classifier.load_state_dict(self.classification_models_dict[model_num]) + self.base_model.to(self.device) + with torch.no_grad(): + outputs = self.base_model(**inputs) + predictions = torch.argmax(outputs.logits, dim=1) + + predicted_label = label_encoder.inverse_transform(predictions.cpu().numpy()) + predicted_classes.append(predicted_label[0]) + + data = next((item for item in data if item['class'] == predicted_label), None) + parent = predicted_label[0] + if not data: + break + + while data['subclasses'] and len(data['subclasses']) <= 1: + if data['subclasses']: + predicted_classes.append(data['subclasses'][0]['class']) + parent = data['subclasses'][0]['class'] + data = data['subclasses'][0] + else: + data = None + break + + if not data['subclasses']: + break + + if not data: + break + + data = data['subclasses'] + + return predicted_classes \ No newline at end of file diff --git a/model_trainer/model_trainer.py b/model_trainer/model_trainer.py new file mode 100644 index 00000000..08c61669 --- /dev/null +++ b/model_trainer/model_trainer.py @@ -0,0 +1,109 @@ +from datapipeline import DataPipeline +from trainingpipeline import TrainingPipeline +import os +import requests +import torch +import pickle +import shutil +from s3_ferry import S3Ferry +from constants import URL_MODEL + +class ModelTrainer: + def __init__(self, cookie, newModelId, oldModelId = None) -> None: + + model_url = URL_MODEL + + self.newModelId = newModelId + self.oldModelId = oldModelId + cookies = {'customJwtCookie': cookie} + self.cookie = cookie + response = requests.get(model_url, params = {'modelId': newModelId}, cookies=cookies) + if response.status_code == 200: + self.model_details = response.json() + print("success") + else: + print(f"Failed with status code: {response.status_code}") + + + + def train(self): + s3_ferry = S3Ferry() + dgId = self.model_details['response']['data'][0]['connectedDgId'] + data_pipeline = DataPipeline(dgId, self.cookie) + dfs = data_pipeline.create_dataframes() + models_dets,_ = data_pipeline.models_and_filters() + models_to_train = self.model_details['response']['data'][0]['baseModels'] + + results_rt_paths = [f"results/saved_models", f"results/classifiers", f"results/saved_label_encoders"] + for path in results_rt_paths: + if not os.path.exists(path): + os.makedirs(path) + + with open('results/models_dets.pkl', 'wb') as file: + pickle.dump(models_dets, file) + + models_list = [] + classifiers_list = [] + label_encoders_list = [] + average_accuracy = [] + for i in range(len(models_to_train)): + training_pipeline = TrainingPipeline(dfs, models_to_train[i]) + metrics, models, classifiers, label_encoders = training_pipeline.train() + models_list.append(models) + classifiers_list.append(classifiers) + label_encoders_list.append(label_encoders) + average = sum(metrics[1]) / len(metrics[1]) + average_accuracy.append(average) + + max_value_index = average_accuracy.index(max(average_accuracy)) + best_models = models_list[max_value_index] + best_classifiers = classifiers_list[max_value_index] + best_label_encoders = label_encoders_list[max_value_index] + model_name = models_to_train[max_value_index] + for i, (model, classifier, label_encoder) in enumerate(zip(best_models, best_classifiers, best_label_encoders)): + if model_name == 'xlnet': + torch.save(model, f"results/saved_models/last_two_layers_dfs_{i}.pth") + elif model_name == 'roberta': + torch.save(model, f"results/saved_models/last_two_layers_dfs_{i}.pth") + elif model_name == 'bert-base-uncased': + torch.save(model, f"results/saved_models/last_two_layers_dfs_{i}.pth") + + torch.save(classifier, f"results/classifiers/classifier_{i}.pth") + + label_encoder_path = f"results/saved_label_encoders/label_encoder_{i}.pkl" + with open(label_encoder_path, 'wb') as file: + pickle.dump(label_encoder, file) + + shutil.make_archive(f"{str(self.newModelId)}", 'zip', f"results") + save_location = f"shared/models/{str(self.newModelId)}/{str(self.newModelId)}.zip" + source_location = f"{str(self.newModelId)}.zip" + response = s3_ferry.transfer_file(save_location, "S3", source_location, "FS") + if response.status_code == 201: + upload_status = {"message": "Model File Uploaded Successfully!", "saved_file_path": save_location} + + else: + upload_status = {"message": "failed to Upload Model File!"} + + DeploymentPlatform = self.model_details['response']['data'][0]['deploymentEnv'] + + url = f"http://localhost:8088/classifier/datamodel/deployment/{DeploymentPlatform}/update" + + + if self.oldModelId is not None: + + payload = { + "modelId": self.newModelId, + "replaceDeployment": True, + "replaceDeploymentPlatform":DeploymentPlatform, + "bestModelName":model_name + } + + else: + payload = { + "modelId": self.newModelId, + "replaceDeployment": False, + "replaceDeploymentPlatform": DeploymentPlatform, + "bestModelName":model_name + } + + response = requests.post(url, json=payload) \ No newline at end of file diff --git a/model_trainer/model_upload_requirements.txt b/model_trainer/model_upload_requirements.txt new file mode 100644 index 00000000..f0cf53d6 --- /dev/null +++ b/model_trainer/model_upload_requirements.txt @@ -0,0 +1,61 @@ +pandas==2.2.2 +scikit_learn==1.5.1 +pydantic==2.8.2 +annotated-types==0.7.0 +accelerate==0.33.0 +anyio==4.4.0 +certifi==2024.7.4 +charset-normalizer==3.3.2 +click==8.1.7 +colorama==0.4.6 +dnspython==2.6.1 +email_validator==2.2.0 +exceptiongroup==1.2.2 +fastapi==0.111.1 +fastapi-cli==0.0.4 +filelock==3.15.4 +fsspec==2024.6.1 +h11==0.14.0 +httpcore==1.0.5 +httptools==0.6.1 +httpx==0.27.0 +huggingface-hub==0.24.2 +idna==3.7 +Jinja2==3.1.4 +langdetect==1.0.9 +markdown-it-py==3.0.0 +MarkupSafe==2.1.5 +mdurl==0.1.2 +mpmath==1.3.0 +networkx==3.2.1 +numpy==1.24.4 +packaging==24.1 +pillow==10.2.0 +pydantic==2.8.2 +pydantic_core==2.20.1 +Pygments==2.18.0 +python-dotenv==1.0.1 +python-multipart==0.0.9 +PyYAML==6.0.1 +regex==2024.7.24 +requests==2.32.3 +rich==13.7.1 +safetensors==0.4.3 +sentencepiece==0.2.0 +shellingham==1.5.4 +six==1.16.0 +sniffio==1.3.1 +starlette==0.37.2 +sympy==1.12 +tokenizers==0.19.1 +torch==2.4.0 +torchaudio==2.4.0 +torchvision==0.19.0 +tqdm==4.66.4 +transformers==4.44.0 +typer==0.12.3 +typing_extensions==4.12.2 +urllib3==2.2.2 +uvicorn==0.30.5 +watchfiles==0.22.0 +websockets==12.0 diff --git a/model_trainer/requirements.txt b/model_trainer/requirements.txt deleted file mode 100644 index 25022cd1..00000000 --- a/model_trainer/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -fastapi==0.111.1 -uvicorn==0.30.3 \ No newline at end of file diff --git a/model_trainer/s3_ferry.py b/model_trainer/s3_ferry.py new file mode 100644 index 00000000..1b63182f --- /dev/null +++ b/model_trainer/s3_ferry.py @@ -0,0 +1,20 @@ +import requests + +class S3Ferry: + def __init__(self): + self.url = "http://s3-ferry:3000/v1/files/copy" + + def transfer_file(self, destinationFilePath, destinationStorageType, sourceFilePath, sourceStorageType): + payload = self.get_s3_ferry_payload(destinationFilePath, destinationStorageType, sourceFilePath, sourceStorageType) + + response = requests.post(self.url, json=payload) + return response + + def get_s3_ferry_payload(self, destinationFilePath:str, destinationStorageType:str, sourceFilePath:str, sourceStorageType:str): + S3_FERRY_PAYLOAD = { + "destinationFilePath": destinationFilePath, + "destinationStorageType": destinationStorageType, + "sourceFilePath": sourceFilePath, + "sourceStorageType": sourceStorageType + } + return S3_FERRY_PAYLOAD diff --git a/model_trainer/trainingpipeline.py b/model_trainer/trainingpipeline.py new file mode 100644 index 00000000..f1d72238 --- /dev/null +++ b/model_trainer/trainingpipeline.py @@ -0,0 +1,175 @@ +from transformers import XLMRobertaTokenizer, XLMRobertaForSequenceClassification,Trainer, TrainingArguments, DistilBertTokenizer, DistilBertForSequenceClassification, BertForSequenceClassification, BertTokenizer +from torch.utils.data import Dataset +from sklearn.preprocessing import LabelEncoder +from sklearn.model_selection import train_test_split +from sklearn.metrics import accuracy_score, classification_report +import torch +import shutil +import pandas as pd +import os + + +from transformers import logging +import warnings +warnings.filterwarnings("ignore", message="Some weights of the model checkpoint were not used when initializing") +logging.set_verbosity_error() + + +class CustomDataset(Dataset): + def __init__(self, encodings, labels): + self.encodings = encodings + self.labels = torch.tensor(labels, dtype=torch.long) + + def __getitem__(self, idx): + item = {key: val[idx] for key, val in self.encodings.items()} + item['labels'] = self.labels[idx] + return item + + def __len__(self): + return len(self.labels) + + +device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') +print(device) + + +class TrainingPipeline: + def __init__(self, dfs,model_name): + + self.model_name = model_name + self.dfs = dfs + self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + + + def replicate_data(self, df, target_rows): + while len(df) < target_rows: + df = pd.concat([df, df]) + return df[:target_rows] + + + def tokenize_data(self, data, tokenizer): + tokenized = tokenizer.batch_encode_plus( + data, + truncation=True, + padding=True, + return_token_type_ids=False, + return_attention_mask=True, + return_tensors='pt' + ) + return tokenized + + def data_split(self, df): + unique_values_sample = df.drop_duplicates(subset=['target']) + remaining_df = df[~df.index.isin(unique_values_sample.index)] + remaining_train_df, test_df = train_test_split(remaining_df, test_size=0.2, random_state=42) + train_df = pd.concat([remaining_train_df, unique_values_sample]) + test_df =pd.concat([test_df, unique_values_sample]) + return train_df, test_df + + def train(self): + classes = [] + accuracies = [] + f1_scores = [] + models = [] + classifiers = [] + label_encoders =[] + for i in range(len(self.dfs)): + current_df = self.dfs[i] + if len(current_df) < 10: + current_df = self.replicate_data(current_df, 50).reset_index(drop=True) + + train_df, test_df = self.data_split(current_df) + label_encoder = LabelEncoder() + train_labels = label_encoder.fit_transform(train_df['target']) + test_labels = label_encoder.transform(test_df['target']) + + + + if self.model_name == 'xlnet': + model = DistilBertForSequenceClassification.from_pretrained('distilbert-base-uncased',num_labels=len(label_encoder.classes_), force_download=True) + tokenizer = DistilBertTokenizer.from_pretrained('distilbert-base-uncased', force_download=True) + + for param in model.distilbert.parameters(): + param.requires_grad = False + + for param in model.distilbert.transformer.layer[-2:].parameters(): + param.requires_grad = True + + for param in model.classifier.parameters(): + param.requires_grad = True + + elif self.model_name == 'roberta': + model = XLMRobertaForSequenceClassification.from_pretrained('xlm-roberta-base', num_labels=len(label_encoder.classes_), force_download=True) + tokenizer = XLMRobertaTokenizer.from_pretrained('xlm-roberta-base', force_download=True) + for param in model.roberta.parameters(): + param.requires_grad = False + + for param in model.roberta.encoder.layer[-2:].parameters(): + param.requires_grad = True + + for param in model.classifier.parameters(): + param.requires_grad = True + + elif self.model_name == 'albert': + model = BertForSequenceClassification.from_pretrained('bert-base-uncased', num_labels=len(label_encoder.classes_), force_download=True) + tokenizer = BertTokenizer.from_pretrained('bert-base-uncased', force_download=True) + + for param in model.base_model.parameters(): + param.requires_grad = False + + for param in model.base_model.encoder.layer[-2:].parameters(): + param.requires_grad = True + + for param in model.classifier.parameters(): + param.requires_grad = True + + train_encodings = self.tokenize_data(train_df['input'].tolist(), tokenizer) + test_encodings = self.tokenize_data(test_df['input'].tolist(), tokenizer) + + train_dataset = CustomDataset(train_encodings, train_labels) + test_dataset = CustomDataset(test_encodings, test_labels) + + training_args = TrainingArguments( + output_dir= 'tmp', + num_train_epochs=1, + per_device_train_batch_size=16, + per_device_eval_batch_size=16, + logging_dir='./logs', + logging_steps=100, + eval_strategy='epoch', + disable_tqdm=False + ) + + trainer = Trainer( + model=model, + args=training_args, + train_dataset=train_dataset, + eval_dataset=test_dataset, + compute_metrics=lambda eval_pred: {"accuracy": accuracy_score(eval_pred.label_ids, eval_pred.predictions.argmax(axis=1))} + ) + + trainer.train() + if self.model_name == 'xlnet': + models.append(model.distilbert.transformer.layer[-2:].state_dict()) + elif self.model_name == 'roberta': + models.append(model.roberta.encoder.layer[-2:].state_dict()) + elif self.model_name == 'albert': + models.append(model.base_model.encoder.layer[-2:].state_dict()) + + + classifiers.append(model.classifier.state_dict()) + predictions, labels, _ = trainer.predict(test_dataset) + predictions = predictions.argmax(axis=-1) + report = classification_report(labels, predictions, target_names=label_encoder.classes_ ,output_dict=True, zero_division=0) + for cls in label_encoder.classes_: + classes.append(cls) + accuracies.append(report[cls]['precision']) + f1_scores.append(report[cls]['f1-score']) + + label_encoders.append(label_encoder) + # shutil.rmtree('tmp') + + metrics = (classes, accuracies, f1_scores) + return metrics, models, classifiers, label_encoders + + \ No newline at end of file From 78d123f36a0d908e4dd5bdb0670a61309eaa16cd Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 12 Aug 2024 23:50:23 +0530 Subject: [PATCH 448/582] Model trainer and inference intergration --- .../script/python_script_starter.sh | 4 +- anonymizer/anonymizer_api.py | 58 +++++++++++-------- docker-compose.yml | 26 +++++++-- model_trainer/model_trainer.py | 10 ++-- 4 files changed, 65 insertions(+), 33 deletions(-) diff --git a/DSL/CronManager/script/python_script_starter.sh b/DSL/CronManager/script/python_script_starter.sh index 046bc503..7ff9a807 100644 --- a/DSL/CronManager/script/python_script_starter.sh +++ b/DSL/CronManager/script/python_script_starter.sh @@ -1,8 +1,8 @@ #!/bin/bash VENV_DIR="/home/cronmanager/clsenv" -REQUIREMENTS="model_trainer/requirements.txt" -PYTHON_SCRIPT="model_trainer/model_handler.py" +REQUIREMENTS="model_trainer/model_upload_requirements.txt" +PYTHON_SCRIPT="model_trainer/model_trainer.py" is_package_installed() { package=$1 diff --git a/anonymizer/anonymizer_api.py b/anonymizer/anonymizer_api.py index 3d549b75..4d72c007 100644 --- a/anonymizer/anonymizer_api.py +++ b/anonymizer/anonymizer_api.py @@ -1,28 +1,29 @@ -from fastapi import FastAPI, HTTPException +from fastapi import FastAPI, Request from pydantic import BaseModel -from language_detection import LanguageDetector from ner import NERProcessor from text_processing import TextProcessor from fake_replacements import FakeReplacer from html_cleaner import HTMLCleaner +import os +import requests app = FastAPI() -class InputText(BaseModel): - text: str - -class OutputText(BaseModel): - original_text: str - processed_text: str - status: bool - ner_processor = NERProcessor() html_cleaner = HTMLCleaner() -@app.post("/anonymize", response_model=OutputText) -async def process_text(input_text: InputText): +JIRA_INFERENCE_ENDPOINT = os.getenv("RUUTER_PRIVATE_URL") +OUTLOOK_INFERENCE_ENDPOINT = os.getenv("RUUTER_PRIVATE_URL") + +@app.post("/anonymize") +async def process_text(request: Request): try: - cleaned_text = html_cleaner.remove_html_tags(input_text.text) + payload = await request.json() + + data_dict = payload.get("data", {}) + concatenated_text = " ".join(str(value) for value in data_dict.values()) + + cleaned_text = html_cleaner.remove_html_tags(concatenated_text) text_chunks = TextProcessor.split_text(cleaned_text, 2000) processed_chunks = [] @@ -33,17 +34,28 @@ async def process_text(input_text: InputText): processed_text = TextProcessor.combine_chunks(processed_chunks) - return OutputText( - original_text=input_text.text, - processed_text=processed_text, - status=True - ) + output_payload = {key: value for key, value in payload.items() if key != "data"} + output_payload["input_text"] = processed_text + output_payload["status"] = True + + platform = payload.get("platform", "").lower() + + headers = { + 'Content-Type': 'application/json' + } + + if platform == "jira": + response = requests.post(JIRA_INFERENCE_ENDPOINT, json=output_payload, headers=headers) + elif platform == "outlook": + response = requests.post(OUTLOOK_INFERENCE_ENDPOINT, json=output_payload, headers=headers) + + return output_payload except Exception as e: - return OutputText( - original_text=input_text.text, - processed_text=str(e), - status=False - ) + output_payload = {key: value for key, value in payload.items() if key != "data"} + output_payload["input_text"] = e + output_payload["status"] = False + + return output_payload if __name__ == "__main__": import uvicorn diff --git a/docker-compose.yml b/docker-compose.yml index 2f05fbff..0a73bd8c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -237,10 +237,6 @@ services: - SAVE_JSON_AGGREGRATED_DATA_URL=http://file-handler:8000/datasetgroup/data/import/json - DOWNLOAD_CHUNK_URL=http://file-handler:8000/datasetgroup/data/download/chunk - STATUS_UPDATE_URL=http://ruuter-private:8088/classifier/datasetgroup/update/preprocess/status - - S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID} - - S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY} - - S3_BUCKET_NAME=esclassifier-test - - S3_REGION_NAME=eu-west-1 - PARAPHRASE_API_URL=http://data-enrichment-api:8005/paraphrase - VALIDATION_CONFIRMATION_URL=http://ruuter-private:8088/classifier/datasetgroup/update/validation/status - GET_DATAGROUP_METADATA_URL=http://ruuter-private:8088/classifier/datasetgroup/group/metadata?groupId=dgId @@ -256,6 +252,25 @@ services: - s3-ferry - file-handler + model-inference: + build: + context: ./model_inference + dockerfile: Dockerfile + container_name: model-inference + volumes: + - shared-volume:/shared + environment: + - UPLOAD_DIRECTORY=/shared + - JIRA_MODEL_DOWNLOAD_DIRECTORY=/shared/models/jira + - OUTLOOK_MODEL_DOWNLOAD_DIRECTORY=/shared/models/outlook + - RUUTER_PRIVATE_URL=http://ruuter-private:8088 + - S3_FERRY_URL=http://s3-ferry:3000/v1/files/copy + ports: + - "8003:8003" + depends_on: + - init + - s3-ferry + opensearch-node: image: opensearchproject/opensearch:2.11.1 container_name: opensearch-node @@ -324,6 +339,9 @@ services: container_name: anonymizer ports: - "8010:8010" + environment: + - JIRA_INFERENCE_ENDPOINT=http://model-inference:8003/classifier/deployment/jira/inference + - OUTLOOK_INFERENCE_ENDPOINT=http://model-inference:8003/classifier/deployment/outlook/inference networks: - bykstack diff --git a/model_trainer/model_trainer.py b/model_trainer/model_trainer.py index 08c61669..80411d5d 100644 --- a/model_trainer/model_trainer.py +++ b/model_trainer/model_trainer.py @@ -9,7 +9,11 @@ from constants import URL_MODEL class ModelTrainer: - def __init__(self, cookie, newModelId, oldModelId = None) -> None: + def __init__(self) -> None: + + cookie = os.environ.get('COOKIE') + newModelId = os.environ.get('NEW_MODEL_ID') + oldModelId = os.environ.get('OLD_MODEL_ID') model_url = URL_MODEL @@ -24,8 +28,6 @@ def __init__(self, cookie, newModelId, oldModelId = None) -> None: else: print(f"Failed with status code: {response.status_code}") - - def train(self): s3_ferry = S3Ferry() dgId = self.model_details['response']['data'][0]['connectedDgId'] @@ -106,4 +108,4 @@ def train(self): "bestModelName":model_name } - response = requests.post(url, json=payload) \ No newline at end of file + response = requests.post(url, json=payload) From c4668d5d8fa403745cc5f5cda57c3032b9a9a93b Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Tue, 13 Aug 2024 01:07:34 +0530 Subject: [PATCH 449/582] ESCLASS-190: Add test model API's and fixed flow for outlook and jira --- .../hbs/return_jira_issue_info.handlebars | 8 +- .../hbs/return_label_mismatch.handlebars | 2 +- .../return_outlook_payload_info.handlebars | 17 +++- DSL/DMapper/lib/helpers.js | 34 ++++--- .../classifier-script-v3-integrations.sql | 1 - DSL/Resql/get-test-data-models.sql | 7 ++ DSL/Resql/insert-input-metadata.sql | 2 - .../DSL/GET/classifier/testmodel/models.yml | 63 +++++++++++++ .../DSL/POST/classifier/inference/create.yml | 7 +- .../POST/classifier/testmodel/test-data.yml | 92 +++++++++++++++++++ .../integration/jira/cloud/accept.yml | 28 +++--- .../classifier/integration/outlook/accept.yml | 58 ++++++++++-- constants.ini | 1 + 13 files changed, 275 insertions(+), 45 deletions(-) create mode 100644 DSL/Resql/get-test-data-models.sql create mode 100644 DSL/Ruuter.private/DSL/GET/classifier/testmodel/models.yml create mode 100644 DSL/Ruuter.private/DSL/POST/classifier/testmodel/test-data.yml diff --git a/DSL/DMapper/hbs/return_jira_issue_info.handlebars b/DSL/DMapper/hbs/return_jira_issue_info.handlebars index f60efd7f..adb45ef9 100644 --- a/DSL/DMapper/hbs/return_jira_issue_info.handlebars +++ b/DSL/DMapper/hbs/return_jira_issue_info.handlebars @@ -2,5 +2,11 @@ "jiraId": "{{data.key}}", "summary": "{{data.fields.summary}}", "labels": "{{data.fields.labels}}", - "projectId": "{{data.fields.project.key}}" + "projectId": "{{data.fields.project.key}}", + "description": "{{data.fields.description}}", + "attachments": [ + {{#each data.fields.attachment}} + "{{this.filename}}"{{#unless @last}},{{/unless}} + {{/each}} + ] } diff --git a/DSL/DMapper/hbs/return_label_mismatch.handlebars b/DSL/DMapper/hbs/return_label_mismatch.handlebars index 488aed0e..b7b3437f 100644 --- a/DSL/DMapper/hbs/return_label_mismatch.handlebars +++ b/DSL/DMapper/hbs/return_label_mismatch.handlebars @@ -1,3 +1,3 @@ { - "isMismatch": "{{isLabelsMismatch newLabels previousLabels}}" + "isMismatch": "{{isLabelsMismatch newLabels correctedLabels predictedLabels}}" } diff --git a/DSL/DMapper/hbs/return_outlook_payload_info.handlebars b/DSL/DMapper/hbs/return_outlook_payload_info.handlebars index 3e9639be..2e0d38d4 100644 --- a/DSL/DMapper/hbs/return_outlook_payload_info.handlebars +++ b/DSL/DMapper/hbs/return_outlook_payload_info.handlebars @@ -3,5 +3,18 @@ "subject": "{{{data.subject}}}", "parentFolderId": "{{{data.parentFolderId}}}", "categories": "{{{data.categories}}}", - "body": "{{{data.body.contentType}}}" -} + "body": "{{{jsEscape data.body.content}}}", + "fromEmailAddress": "{{{data.from.emailAddress.address}}}", + "attachments": [ + {{#each data.attachments}} + "{{{this.name}}}" + {{#unless @last}},{{/unless}} + {{/each}} + ], + "toEmailAddress": [ + {{#each data.toRecipients}} + "{{{this.emailAddress.address}}}" + {{#unless @last}},{{/unless}} + {{/each}} + ] +} \ No newline at end of file diff --git a/DSL/DMapper/lib/helpers.js b/DSL/DMapper/lib/helpers.js index f2e14ea4..2b5e581c 100644 --- a/DSL/DMapper/lib/helpers.js +++ b/DSL/DMapper/lib/helpers.js @@ -32,21 +32,27 @@ export function platformStatus(platform, data) { return platformData ? platformData.isConnect : false; } -export function isLabelsMismatch(newLabels, previousLabels) { - if ( - Array.isArray(newLabels) && - Array.isArray(previousLabels) && - newLabels.length === previousLabels.length - ) { - for (let i = 0; i < newLabels.length; i++) { - if (newLabels[i] !== previousLabels[i]) { - return true; +export function isLabelsMismatch(newLabels, correctedLabels, predictedLabels) { + function check(arr, newLabels) { + if ( + Array.isArray(newLabels) && + Array.isArray(arr) && + newLabels.length === arr.length + ) { + for (let i = 0; i < newLabels.length; i++) { + if (!arr.includes(newLabels[i])) { + return true; + } } + return false; + } else { + return true; } - return false; - } else { - return true; } + + const val1 = check(correctedLabels, newLabels); + const val2 = check(predictedLabels, newLabels); + return val1 && val2; } export function getOutlookExpirationDateTime() { @@ -119,3 +125,7 @@ export function base64Encrypt(content) { } } +export function jsEscape(str) { + return JSON.stringify(str).slice(1, -1) +} + diff --git a/DSL/Liquibase/changelog/classifier-script-v3-integrations.sql b/DSL/Liquibase/changelog/classifier-script-v3-integrations.sql index 236722af..13cd4241 100644 --- a/DSL/Liquibase/changelog/classifier-script-v3-integrations.sql +++ b/DSL/Liquibase/changelog/classifier-script-v3-integrations.sql @@ -32,7 +32,6 @@ CREATE TABLE public."input" ( average_predicted_classes_probability INT, average_corrected_classes_probability INT, primary_folder_id TEXT DEFAULT NULL, - all_class_predicted_probabilities JSONB, platform platform, CONSTRAINT input_pkey PRIMARY KEY (id), CONSTRAINT input_id_unique UNIQUE (input_id) diff --git a/DSL/Resql/get-test-data-models.sql b/DSL/Resql/get-test-data-models.sql new file mode 100644 index 00000000..6905a8d3 --- /dev/null +++ b/DSL/Resql/get-test-data-models.sql @@ -0,0 +1,7 @@ +SELECT + id AS model_id, + model_name, + major_version, + minor_version +FROM models_metadata +WHERE deployment_env = 'testing'; \ No newline at end of file diff --git a/DSL/Resql/insert-input-metadata.sql b/DSL/Resql/insert-input-metadata.sql index be0edb8d..d11d2dd7 100644 --- a/DSL/Resql/insert-input-metadata.sql +++ b/DSL/Resql/insert-input-metadata.sql @@ -5,7 +5,6 @@ INSERT INTO "input" ( predicted_labels, average_predicted_classes_probability, platform, - all_class_predicted_probabilities, primary_folder_id ) VALUES ( @@ -15,7 +14,6 @@ VALUES ( :predicted_labels::jsonb, :average_predicted_classes_probability, :platform::platform, - :all_class_predicted_probabilities::jsonb, :primary_folder_id ) RETURNING id; diff --git a/DSL/Ruuter.private/DSL/GET/classifier/testmodel/models.yml b/DSL/Ruuter.private/DSL/GET/classifier/testmodel/models.yml new file mode 100644 index 00000000..cad039c0 --- /dev/null +++ b/DSL/Ruuter.private/DSL/GET/classifier/testmodel/models.yml @@ -0,0 +1,63 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'models'" + method: get + accepts: json + returns: json + namespace: classifier + +get_test_data_models: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-test-data-models" + body: + id: ${model_id} + result: res_model + next: check_status + +check_status: + switch: + - condition: ${200 <= res_model.response.statusCodeValue && res_model.response.statusCodeValue < 300} + next: check_data_exist + next: assign_fail_response + +check_data_exist: + switch: + - condition: ${res_model.response.body.length>0} + next: assign_success_response + next: assign_empty_response + +assign_success_response: + assign: + format_res: { + operationSuccessful: true, + data: '${res_model.response.body}' + } + next: return_ok + +assign_empty_response: + assign: + format_res: { + operationSuccessful: true, + data: '${[]}' + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + operationSuccessful: false, + data: '${[]}' + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/POST/classifier/inference/create.yml b/DSL/Ruuter.private/DSL/POST/classifier/inference/create.yml index 4dadb5c6..86272416 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/inference/create.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/inference/create.yml @@ -23,9 +23,6 @@ declaration: - field: platform type: string description: "Body field 'platform'" - - field: allClassPredictedProbabilities - type: json - description: "Body field 'allClassPredictedProbabilities'" - field: primaryFolderId type: string description: "Body field 'primaryFolderId'" @@ -41,13 +38,12 @@ extract_request_data: predicted_labels_org: ${incoming.body.predictedLabels} average_predicted_classes_probability: ${incoming.body.averagePredictedClassesProbability} platform: ${incoming.body.platform} - all_class_predicted_probabilities: ${incoming.body.allClassPredictedProbabilities} primary_folder_id: ${incoming.body.primaryFolderId} next: check_for_request_data check_for_request_data: switch: - - condition: ${input_id !== null || inference_text !== null || predicted_labels_org !== null || average_predicted_classes_probability !== null || platform !== null || all_class_predicted_probabilities !== null} + - condition: ${input_id !== null || inference_text !== null || predicted_labels_org !== null || average_predicted_classes_probability !== null || platform !== null} next: check_platform_data next: return_incorrect_request @@ -79,7 +75,6 @@ create_input_metadata: predicted_labels: ${JSON.stringify(predicted_labels_org)} average_predicted_classes_probability: ${average_predicted_classes_probability} platform: ${platform} - all_class_predicted_probabilities: ${JSON.stringify(all_class_predicted_probabilities)} primary_folder_id: ${platform !== 'OUTLOOK' ? '' :primary_folder_id} result: res_input next: check_status diff --git a/DSL/Ruuter.private/DSL/POST/classifier/testmodel/test-data.yml b/DSL/Ruuter.private/DSL/POST/classifier/testmodel/test-data.yml new file mode 100644 index 00000000..cb888cd1 --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/classifier/testmodel/test-data.yml @@ -0,0 +1,92 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'TEST-DATA'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: modelId + type: number + description: "Body field 'modelId'" + - field: text + type: string + description: "Body field 'text'" + headers: + - field: cookie + type: string + description: "Cookie field" + +extract_request_data: + assign: + model_id: ${incoming.body.modelId} + text: ${incoming.headers.text} + cookie: ${incoming.headers.cookie} + next: check_for_request_data + +check_for_request_data: + switch: + - condition: ${model_id !== null && text !== null} + next: get_text_data + next: return_incorrect_request + +check_for_text_data: + switch: + - condition: ${text !== ''} + next: get_data_model_by_id + next: return_empty_request + +send_data_to_predict: + call: reflect.mock + args: + url: "[#CLASSIFIER_RESQL]/get-data-model-by-id" + body: + id: ${model_id} + result: res_model + next: check_data_model_status + +check_data_model_status: + switch: + - condition: ${200 <= res_model.response.statusCodeValue && res_model.response.statusCodeValue < 300} + next: check_data_model_exist + next: assign_fail_response + +assign_success_response: + assign: + format_res: { + modelId: '${model_id}', + data: '${res_model.response.body}', + operationSuccessful: true, + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + modelId: '${model_id}', + data: '', + operationSuccessful: false, + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_incorrect_request: + status: 400 + return: 'Missing Required Fields' + next: end + +return_empty_request: + status: 400 + return: 'Text Data Empty' + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml b/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml index 3cf43665..2519cec5 100644 --- a/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml +++ b/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml @@ -12,7 +12,7 @@ declaration: type: object description: "Body field 'headers'" - field: payload - type: object + type: json description: "Body field 'payload'" - field: issue_info type: string @@ -78,16 +78,12 @@ check_previous_labels: switch: - condition: ${res.response.body.length > 0} next: assign_previous_labels - next: assign_empty + next: get_jira_issue_info assign_previous_labels: assign: - previous_labels: ${res.response.body[0].correctedLabels} - next: validate_issue_labels - -assign_empty: - assign: - previous_labels: ${[]} + previous_corrected_labels: ${res.response.body[0].correctedLabels !==null ? JSON.parse(res.response.body[0].correctedLabels.value) :[]} + previous_predicted_labels: ${res.response.body[0].predictedLabels !==null ? JSON.parse(res.response.body[0].predictedLabels.value) :[]} next: validate_issue_labels validate_issue_labels: @@ -98,7 +94,8 @@ validate_issue_labels: type: json body: newLabels: ${issue_info.fields.labels} - previousLabels: ${previous_labels} + correctedLabels: ${previous_corrected_labels} + predictedLabels: ${previous_predicted_labels} result: label_response next: check_label_mismatch @@ -106,7 +103,7 @@ check_label_mismatch: switch: - condition: ${label_response.response.body.isMismatch === 'true'} next: get_jira_issue_info - next: end + next: return_data get_jira_issue_info: call: http.post @@ -122,11 +119,13 @@ get_jira_issue_info: send_issue_data: call: reflect.mock args: - url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/return_jira_issue_info"# need correct url + url: "[#CLASSIFIER_ANONYMIZER]/anonymize" headers: type: json body: - info: ${extract_info} + platform: 'JIRA' + key: ${issue_info.key} + data: ${extract_info} response: statusCodeValue: 200 result: res @@ -142,6 +141,11 @@ return_ok: return: "Jira data send successfully" next: end +return_data: + status: 200 + return: "Not Sent" + next: end + return_error_found: status: 400 return: "Error Found" diff --git a/DSL/Ruuter.public/DSL/POST/classifier/integration/outlook/accept.yml b/DSL/Ruuter.public/DSL/POST/classifier/integration/outlook/accept.yml index 9578e214..5e0f0194 100644 --- a/DSL/Ruuter.public/DSL/POST/classifier/integration/outlook/accept.yml +++ b/DSL/Ruuter.public/DSL/POST/classifier/integration/outlook/accept.yml @@ -42,24 +42,64 @@ assign_outlook_mail_info: assign: resource: ${payload.value[0].resource} event_type: ${payload.value[0].changeType} - next: get_token_info + next: get_refresh_token -get_token_info: - call: http.get +get_refresh_token: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-token" + body: + platform: 'OUTLOOK' + result: res + next: set_refresh_token + +set_refresh_token: + assign: + refresh_token: ${res.response.body[0].token} + next: check_refresh_token + +check_refresh_token: + switch: + - condition: ${refresh_token !== null} + next: decrypt_token + next: return_not_found + +decrypt_token: + call: http.post + args: + url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/return_decrypted_outlook_token" + headers: + type: json + body: + token: ${refresh_token} + result: token_data + next: get_access_token + +get_access_token: + call: http.post args: - url: "[#CLASSIFIER_RUUTER_PRIVATE]/classifier/integration/outlook/token" + url: "https://login.microsoftonline.com/common/oauth2/v2.0/token" + contentType: formdata + headers: + type: json + body: + client_id: "[#OUTLOOK_CLIENT_ID]" + scope: "User.Read Mail.ReadWrite MailboxSettings.ReadWrite offline_access" + refresh_token: ${token_data.response.body.token.content} + grant_type: "refresh_token" + client_secret: "[#OUTLOOK_SECRET_KEY]" result: res next: assign_access_token assign_access_token: assign: - access_token: ${res.response.body.response.access_token} + access_token: ${res.response.body.access_token} next: get_extracted_mail_info get_extracted_mail_info: call: http.get args: - url: "https://graph.microsoft.com/v1.0/${resource}" + url: "https://graph.microsoft.com/v1.0/${resource}?$expand=attachments" headers: Authorization: ${'Bearer ' + access_token} result: mail_info_data @@ -114,11 +154,13 @@ rearrange_mail_payload: send_outlook_data: call: reflect.mock args: - url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/return_jira_issue_info"# need correct url + url: "[#CLASSIFIER_ANONYMIZER]/anonymize" headers: type: json body: - info: ${outlook_body} + platform: 'OUTLOOK' + key: ${mail_info_data.id} + data: ${outlook_body} response: statusCodeValue: 200 result: res diff --git a/constants.ini b/constants.ini index 23b5dca2..47537c95 100644 --- a/constants.ini +++ b/constants.ini @@ -8,6 +8,7 @@ CLASSIFIER_TIM=http://tim:8085 CLASSIFIER_CRON_MANAGER=http://cron-manager:9010 CLASSIFIER_FILE_HANDLER=http://file-handler:8000 CLASSIFIER_NOTIFICATIONS=http://notifications-node:4040 +CLASSIFIER_ANONYMIZER=http://anonymizer:8010 CLASSIFIER_RUUTER_PUBLIC_FRONTEND_URL=value DOMAIN=localhost JIRA_API_TOKEN= value From a1cecb0f316ecfc731064d369b741b0acad6d2c3 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Tue, 13 Aug 2024 01:10:01 +0530 Subject: [PATCH 450/582] ESCLASS-190: change API path --- DSL/Ruuter.private/DSL/POST/classifier/testmodel/test-data.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/DSL/Ruuter.private/DSL/POST/classifier/testmodel/test-data.yml b/DSL/Ruuter.private/DSL/POST/classifier/testmodel/test-data.yml index cb888cd1..dd04deba 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/testmodel/test-data.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/testmodel/test-data.yml @@ -41,9 +41,10 @@ check_for_text_data: send_data_to_predict: call: reflect.mock args: - url: "[#CLASSIFIER_RESQL]/get-data-model-by-id" + url: "[#CLASSIFIER_ANONYMIZER]/anonymize" body: id: ${model_id} + text: ${text} result: res_model next: check_data_model_status From 9ad2e1f1d325d6f6b612bdcae3f50c1237da7879 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Tue, 13 Aug 2024 01:13:44 +0530 Subject: [PATCH 451/582] ESCLASS-190: validate test API --- .../POST/classifier/testmodel/test-data.yml | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/DSL/Ruuter.private/DSL/POST/classifier/testmodel/test-data.yml b/DSL/Ruuter.private/DSL/POST/classifier/testmodel/test-data.yml index dd04deba..fe390f23 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/testmodel/test-data.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/testmodel/test-data.yml @@ -29,7 +29,7 @@ extract_request_data: check_for_request_data: switch: - condition: ${model_id !== null && text !== null} - next: get_text_data + next: check_for_text_data next: return_incorrect_request check_for_text_data: @@ -38,13 +38,12 @@ check_for_text_data: next: get_data_model_by_id next: return_empty_request -send_data_to_predict: - call: reflect.mock +get_data_model_by_id: + call: http.post args: - url: "[#CLASSIFIER_ANONYMIZER]/anonymize" + url: "[#CLASSIFIER_RESQL]/get-data-model-by-id" body: id: ${model_id} - text: ${text} result: res_model next: check_data_model_status @@ -54,6 +53,30 @@ check_data_model_status: next: check_data_model_exist next: assign_fail_response +check_data_model_exist: + switch: + - condition: ${res_model.response.body.length>0} + next: send_data_to_predict + next: assign_fail_response + +send_data_to_predict: + call: reflect.mock + args: + url: "[#CLASSIFIER_ANONYMIZER]/anonymize" + body: + id: ${model_id} + text: ${text} + response: + statusCodeValue: 200 + result: res_predict + next: check_data_predict_status + +check_data_predict_status: + switch: + - condition: ${200 <= res_predict.response.statusCodeValue && res_predict.response.statusCodeValue < 300} + next: assign_success_response + next: assign_fail_response + assign_success_response: assign: format_res: { From fb60782ced09482400fc4146b1fb2ad70e561c29 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Tue, 13 Aug 2024 15:09:52 +0530 Subject: [PATCH 452/582] data models refactor --- .../FormElements/FormCheckboxes/index.tsx | 2 +- .../FormElements/FormRadios/index.tsx | 2 +- .../FormElements/FormSelect/index.tsx | 2 +- .../molecules/DataModelCard/index.tsx | 30 +- .../molecules/DataModelForm/index.tsx | 3 +- GUI/src/enums/dataModelsEnums.ts | 8 +- .../pages/DataModels/ConfigureDataModel.tsx | 68 ++-- GUI/src/pages/DataModels/CreateDataModel.tsx | 21 +- GUI/src/pages/DataModels/index.tsx | 370 +++++++++--------- GUI/src/types/dataModels.ts | 61 ++- GUI/src/utils/commonUtilts.ts | 8 +- GUI/src/utils/dataModelsUtils.ts | 19 +- GUI/translations/en/common.json | 24 ++ 13 files changed, 365 insertions(+), 253 deletions(-) diff --git a/GUI/src/components/FormElements/FormCheckboxes/index.tsx b/GUI/src/components/FormElements/FormCheckboxes/index.tsx index 50829eaf..d845ed22 100644 --- a/GUI/src/components/FormElements/FormCheckboxes/index.tsx +++ b/GUI/src/components/FormElements/FormCheckboxes/index.tsx @@ -10,7 +10,7 @@ type FormCheckboxesType = { items: { label: string; value: string; - }[]; + }[] |undefined; isStack?: boolean; error?: string; selectedValues?: string[]; // New prop for selected values diff --git a/GUI/src/components/FormElements/FormRadios/index.tsx b/GUI/src/components/FormElements/FormRadios/index.tsx index fffba69a..2b1956ac 100644 --- a/GUI/src/components/FormElements/FormRadios/index.tsx +++ b/GUI/src/components/FormElements/FormRadios/index.tsx @@ -8,7 +8,7 @@ type FormRadiosType = { items: { label: string; value: string; - }[]; + }[] |undefined; onChange: (selectedValue: string) => void; selectedValue?: string; // New prop for the selected value isStack?: boolean; diff --git a/GUI/src/components/FormElements/FormSelect/index.tsx b/GUI/src/components/FormElements/FormSelect/index.tsx index 250b303b..c3c59707 100644 --- a/GUI/src/components/FormElements/FormSelect/index.tsx +++ b/GUI/src/components/FormElements/FormSelect/index.tsx @@ -26,7 +26,7 @@ type FormSelectProps = Partial & placeholder?: string; hideLabel?: boolean; direction?: 'down' | 'up'; - options: FormSelectOption[]; + options: FormSelectOption[]|[]; onSelectionChange?: (selection: FormSelectOption | null) => void; error?: string; }; diff --git a/GUI/src/components/molecules/DataModelCard/index.tsx b/GUI/src/components/molecules/DataModelCard/index.tsx index 7bda9ddf..831ed9f3 100644 --- a/GUI/src/components/molecules/DataModelCard/index.tsx +++ b/GUI/src/components/molecules/DataModelCard/index.tsx @@ -19,7 +19,7 @@ type DataModelCardProps = { platform?: string; maturity?: string; setId: React.Dispatch>; - setView: React.Dispatch>; + setView: React.Dispatch>; results?: any; }; @@ -101,21 +101,21 @@ const DataModelCard: FC> = ({

          {dataModelName}

          - {isLatest ? : null} + {isLatest ? : null}

          - {'Dataset Group:'} + {t('dataModels.dataModelCard.datasetGroup') ?? ''}: {datasetGroupName}

          - {'Dataset Group Version:'} + {t('dataModels.dataModelCard.dgVersion') ?? ''}: {dgVersion}

          - {'Last Trained:'} - {lastTrained} + {t('dataModels.dataModelCard.lastTrained') ?? ''}: {lastTrained} +

          @@ -130,21 +130,21 @@ const DataModelCard: FC> = ({ size="s" onClick={() => { open({ - title: 'Training Results', - footer: , + title: t('dataModels.trainingResults.title') ?? '', + footer: , size: 'large', content: (
          - Best Performing Model - + {t('dataModels.trainingResults.bestPerformingModel') ?? ''} -
          {' '} -
          Classes
          -
          Accuracy
          -
          F1 Score
          +
          {t('dataModels.trainingResults.classes') ?? ''}
          +
          {t('dataModels.trainingResults.accuracy') ?? ''}
          +
          {t('dataModels.trainingResults.f1Score') ?? ''}
          } > @@ -168,7 +168,7 @@ const DataModelCard: FC> = ({
          ) : (
          - No training results available + {t('dataModels.trainingResults.noResults') ?? ''}
          )} @@ -177,7 +177,7 @@ const DataModelCard: FC> = ({ }); }} > - View Results + {t('dataModels.trainingResults.viewResults') ?? ''} Results
          diff --git a/GUI/src/components/molecules/DataModelForm/index.tsx b/GUI/src/components/molecules/DataModelForm/index.tsx index 954a9b1a..aeedbeb3 100644 --- a/GUI/src/components/molecules/DataModelForm/index.tsx +++ b/GUI/src/components/molecules/DataModelForm/index.tsx @@ -12,10 +12,11 @@ import { useQuery } from '@tanstack/react-query'; import { getCreateOptions } from 'services/data-models'; import { dgArrayWithVersions } from 'utils/dataModelsUtils'; import CircularSpinner from '../CircularSpinner/CircularSpinner'; +import { DataModel } from 'types/dataModels'; type DataModelFormType = { dataModel: any; - handleChange: (name: string, value: any) => void; + handleChange: (name: keyof DataModel, value: any) => void; errors?: Record; type: string; }; diff --git a/GUI/src/enums/dataModelsEnums.ts b/GUI/src/enums/dataModelsEnums.ts index f776ee3d..9a3da354 100644 --- a/GUI/src/enums/dataModelsEnums.ts +++ b/GUI/src/enums/dataModelsEnums.ts @@ -11,7 +11,6 @@ export enum Maturity { STAGING = 'staging', DEVELOPMENT = 'development', TESTING = 'testing', - } export enum Platform { @@ -19,5 +18,10 @@ export enum Platform { OUTLOOK = 'outlook', PINAL = 'pinal', UNDEPLOYED = 'undeployed', +} -} \ No newline at end of file +export enum UpdateType { + MAJOR = 'major', + MINOR = 'minor', + MATURITY_LABEL = 'maturityLabel', +} diff --git a/GUI/src/pages/DataModels/ConfigureDataModel.tsx b/GUI/src/pages/DataModels/ConfigureDataModel.tsx index 3a7b373b..e9c5d575 100644 --- a/GUI/src/pages/DataModels/ConfigureDataModel.tsx +++ b/GUI/src/pages/DataModels/ConfigureDataModel.tsx @@ -11,22 +11,33 @@ import { updateDataModel, } from 'services/data-models'; import DataModelForm from 'components/molecules/DataModelForm'; -import { getChangedAttributes, validateDataModel } from 'utils/dataModelsUtils'; -import { Platform } from 'enums/dataModelsEnums'; +import { getChangedAttributes } from 'utils/dataModelsUtils'; +import { Platform, UpdateType } from 'enums/dataModelsEnums'; import { ButtonAppearanceTypes } from 'enums/commonEnums'; import CircularSpinner from 'components/molecules/CircularSpinner/CircularSpinner'; +import { DataModel, UpdatedDataModelPayload } from 'types/dataModels'; type ConfigureDataModelType = { id: number; - availableProdModels?: string[] + availableProdModels?: string[]; }; -const ConfigureDataModel: FC = ({ id,availableProdModels }) => { +const ConfigureDataModel: FC = ({ + id, + availableProdModels, +}) => { const { open, close } = useDialog(); const navigate = useNavigate(); - const [enabled, setEnabled] = useState(true); - const [initialData, setInitialData] = useState({}); - const [dataModel, setDataModel] = useState({ + const [enabled, setEnabled] = useState(true); + const [initialData, setInitialData] = useState>({ + modelName: '', + dgId: '', + platform: '', + baseModels: [], + maturity: '', + version: '', + }); + const [dataModel, setDataModel] = useState({ modelId: 0, modelName: '', dgId: '', @@ -36,15 +47,14 @@ const ConfigureDataModel: FC = ({ id,availableProdModels version: '', }); - const { data: dataModelData, isLoading } = useQuery( + const { isLoading } = useQuery( ['datamodels/metadata', id], () => getMetadata(id), - { enabled, onSuccess: (data) => { setDataModel({ - modelId: data?.modelId || '', + modelId: data?.modelId || 0, modelName: data?.modelName || '', dgId: data?.connectedDgId || '', platform: data?.deploymentEnv || '', @@ -54,7 +64,7 @@ const ConfigureDataModel: FC = ({ id,availableProdModels }); setInitialData({ modelName: data?.modelName || '', - dgId: data?.connectedDgId || '', + dgId: data?.connectedDgId || 0, platform: data?.deploymentEnv || '', baseModels: data?.baseModels || [], maturity: data?.maturityLabel || '', @@ -65,7 +75,10 @@ const ConfigureDataModel: FC = ({ id,availableProdModels } ); - const handleDataModelAttributesChange = (name: string, value: any) => { + const handleDataModelAttributesChange = ( + name: keyof DataModel, + value: any + ) => { setDataModel((prevDataModel) => ({ ...prevDataModel, [name]: value, @@ -74,28 +87,26 @@ const ConfigureDataModel: FC = ({ id,availableProdModels const handleSave = () => { const payload = getChangedAttributes(initialData, dataModel); - let updateType; + let updateType: string | undefined; if (payload.dgId) { - updateType = 'major'; + updateType = UpdateType.MAJOR; } else if (payload.baseModels || payload.platform) { - updateType = 'minor'; + updateType = UpdateType.MINOR; } else if (payload.maturity) { - updateType = 'maturityLabel'; + updateType = UpdateType.MATURITY_LABEL; } const updatedPayload = { - modelId: dataModel.modelId, - connectedDgId: payload.dgId, - deploymentEnv: payload.platform, - baseModels: payload.baseModels, - maturityLabel: payload.maturity, - updateType, + modelId: dataModel.modelId?? 0, + connectedDgId: payload.dgId ?? 0, + deploymentEnv: payload.platform ?? "", + baseModels: payload.baseModels ?? [""], + maturityLabel: payload.maturity ?? "", + updateType:updateType??"", }; if (updateType) { - if ( - availableProdModels?.includes(dataModel.platform) - ) { + if (availableProdModels?.includes(dataModel.platform)) { open({ title: 'Warning: Replace Production Model', content: @@ -123,8 +134,8 @@ const ConfigureDataModel: FC = ({ id,availableProdModels }; const updateDataModelMutation = useMutation({ - mutationFn: (data) => updateDataModel(data), - onSuccess: async (response) => { + mutationFn: (data: UpdatedDataModelPayload) => updateDataModel(data), + onSuccess: async () => { open({ title: 'Changes Saved Successfully', content: ( @@ -188,7 +199,7 @@ const ConfigureDataModel: FC = ({ id,availableProdModels
          @@ -259,6 +270,7 @@ const ConfigureDataModel: FC = ({ id,availableProdModels }); }, }); + return (
          diff --git a/GUI/src/pages/DataModels/CreateDataModel.tsx b/GUI/src/pages/DataModels/CreateDataModel.tsx index 2d047a24..f45f9b64 100644 --- a/GUI/src/pages/DataModels/CreateDataModel.tsx +++ b/GUI/src/pages/DataModels/CreateDataModel.tsx @@ -12,16 +12,15 @@ import { ButtonAppearanceTypes } from 'enums/commonEnums'; import { createDataModel, getDataModelsOverview } from 'services/data-models'; import { integrationQueryKeys } from 'utils/queryKeys'; import { getIntegrationStatus } from 'services/integration'; +import { CreateDataModelPayload, DataModel } from 'types/dataModels'; const CreateDataModel: FC = () => { const { t } = useTranslation(); const { open, close } = useDialog(); - const [isModalOpen, setIsModalOpen] = useState(false); - const [modalType, setModalType] = useState(''); const navigate = useNavigate(); const [availableProdModels, setAvailableProdModels] = useState([]); - const [dataModel, setDataModel] = useState({ + const [dataModel, setDataModel] = useState>({ modelName: '', dgName: '', dgId: 0, @@ -94,15 +93,15 @@ const CreateDataModel: FC = () => { const handleCreate = () => { if (validateData()) { const payload = { - modelName: dataModel.modelName, - dgId: dataModel.dgId, - baseModels: dataModel.baseModels, - deploymentPlatform: dataModel.platform, - maturityLabel: dataModel.maturity, + modelName: dataModel.modelName ??"", + dgId: dataModel.dgId?? 0, + baseModels: dataModel.baseModels?? [""], + deploymentPlatform: dataModel.platform ??"", + maturityLabel: dataModel.maturity?? "", }; if ( - availableProdModels?.includes(dataModel.platform) + availableProdModels?.includes(dataModel.platform??"") ) { open({ title: 'Warning: Replace Production Model', @@ -136,7 +135,7 @@ const CreateDataModel: FC = () => { } }; const createDataModelMutation = useMutation({ - mutationFn: (data) => createDataModel(data), + mutationFn: (data:CreateDataModelPayload) => createDataModel(data), onSuccess: async (response) => { open({ title: 'Data Model Created and Trained', @@ -203,7 +202,7 @@ const CreateDataModel: FC = () => { background: 'white', }} > - + diff --git a/GUI/src/pages/DataModels/index.tsx b/GUI/src/pages/DataModels/index.tsx index e182d97b..1c890c3b 100644 --- a/GUI/src/pages/DataModels/index.tsx +++ b/GUI/src/pages/DataModels/index.tsx @@ -11,24 +11,26 @@ import ConfigureDataModel from './ConfigureDataModel'; import { customFormattedArray, extractedArray } from 'utils/dataModelsUtils'; import CircularSpinner from 'components/molecules/CircularSpinner/CircularSpinner'; import { ButtonAppearanceTypes } from 'enums/commonEnums'; +import { DataModelResponse, FilterData, Filters } from 'types/dataModels'; const DataModels: FC = () => { const { t } = useTranslation(); const navigate = useNavigate(); - const [pageIndex, setPageIndex] = useState(1); - const [id, setId] = useState(0); - const [enableFetch, setEnableFetch] = useState(true); - const [enableProdModelsFetch, setEnableProdModelsFetch] = useState(true); + const [pageIndex, setPageIndex] = useState(1); + const [id, setId] = useState(0); + const [enableFetch, setEnableFetch] = useState(true); + const [enableProdModelsFetch, setEnableProdModelsFetch] = + useState(true); - const [view, setView] = useState('list'); + const [view, setView] = useState<'list' | 'individual'>('list'); const [availableProdModels, setAvailableProdModels] = useState([]); useEffect(() => { setEnableFetch(true); }, [view]); - const [filters, setFilters] = useState({ + const [filters, setFilters] = useState({ modelName: 'all', version: 'x.x.x', platform: 'all', @@ -43,9 +45,9 @@ const DataModels: FC = () => { 'datamodels/overview', pageIndex, filters.modelName, - parseVersionString(filters?.version)?.major, - parseVersionString(filters?.version)?.minor, - parseVersionString(filters?.version)?.patch, + parseVersionString(filters.version)?.major, + parseVersionString(filters.version)?.minor, + parseVersionString(filters.version)?.patch, filters.platform, filters.datasetGroup, filters.trainingStatus, @@ -57,8 +59,8 @@ const DataModels: FC = () => { getDataModelsOverview( pageIndex, filters.modelName, - parseVersionString(filters?.version)?.major, - parseVersionString(filters?.version)?.minor, + parseVersionString(filters.version)?.major, + parseVersionString(filters.version)?.minor, filters.platform, filters.datasetGroup, filters.trainingStatus, @@ -71,6 +73,7 @@ const DataModels: FC = () => { enabled: enableFetch, } ); + const { data: prodDataModelsData, isLoading: isProdModelDataLoading } = useQuery( [ @@ -100,20 +103,26 @@ const DataModels: FC = () => { true ), { - onSuccess:(data)=>{ - setAvailableProdModels(extractedArray(data?.data,"deploymentEnv")) - setEnableProdModelsFetch(false) + onSuccess: (data) => { + setAvailableProdModels(extractedArray(data?.data, 'deploymentEnv')); + setEnableProdModelsFetch(false); }, keepPreviousData: true, enabled: enableProdModelsFetch, } ); - const { data: filterData } = useQuery(['datamodels/filters'], () => - getFilterData() + + const { data: filterData } = useQuery( + ['datamodels/filters'], + () => getFilterData() ); + const pageCount = dataModelsData?.data[0]?.totalPages || 1; - const handleFilterChange = (name: string, value: string) => { + const handleFilterChange = ( + name: keyof Filters, + value: string | number | undefined | { name: string; id: string } + ) => { setEnableFetch(false); setFilters((prevFilters) => ({ ...prevFilters, @@ -125,175 +134,182 @@ const DataModels: FC = () => {
          {view === 'list' && (
          - {!isModelDataLoading && !isProdModelDataLoading ?(
          -
          -
          -
          {t("dataModels.productionModels")}
          {' '} -
          - -
          - {prodDataModelsData?.data?.map((dataset, index: number) => { - return ( - - ); - })} -
          -
          + {!isModelDataLoading && !isProdModelDataLoading ? (
          -
          -
          {t("dataModels.dataModels")}
          - -
          -
          - - handleFilterChange('modelName', selection?.value ?? '') - } - defaultValue={filters?.modelName} - /> - - handleFilterChange('version', selection?.value ?? '') - } - defaultValue={filters?.version} - - /> - - handleFilterChange('platform', selection?.value ?? '') - } - defaultValue={filters?.platform} - - /> - - handleFilterChange('datasetGroup', selection?.value?.id) - } - defaultValue={filters?.datasetGroup} +
          +
          +
          + {t('dataModels.productionModels')} +
          {' '} +
          - /> - - handleFilterChange('trainingStatus', selection?.value ) - } - defaultValue={filters?.trainingStatus} - - /> - - handleFilterChange('maturity', selection?.value ?? '') - } - defaultValue={filters?.maturity} - - /> - - handleFilterChange('sort', selection?.value ?? '') - } - defaultValue={filters?.sort} - - /> - - +
          + {prodDataModelsData?.data?.map( + (dataset: DataModelResponse, index: number) => { + return ( + + ); + } + )} +
          +
          +
          +
          {t('dataModels.dataModels')}
          + +
          +
          + + handleFilterChange('modelName', selection?.value ?? '') + } + defaultValue={filters?.modelName} + /> + + handleFilterChange('version', selection?.value ?? '') + } + defaultValue={filters?.version} + /> + + handleFilterChange('platform', selection?.value ?? '') + } + defaultValue={filters?.platform} + /> + + handleFilterChange('datasetGroup', selection?.value?.id) + } + defaultValue={filters?.datasetGroup} + /> + + handleFilterChange('trainingStatus', selection?.value) + } + defaultValue={filters?.trainingStatus} + /> + + handleFilterChange('maturity', selection?.value) + } + defaultValue={filters?.maturity} + /> + + handleFilterChange('sort', selection?.value) + } + defaultValue={filters?.sort} + /> + + +
          -
          - {dataModelsData?.data?.map((dataset, index: number) => { - return ( - - ); - })} +
          + {dataModelsData?.data?.map( + (dataset: DataModelResponse, index: number) => { + return ( + + ); + } + )} +
          + 1} + canNextPage={pageIndex < 10} + onPageChange={setPageIndex} + />
          - 1} - canNextPage={pageIndex < 10} - onPageChange={setPageIndex} - /> -
          ):( - + ) : ( + )}
          )} - {view === 'individual' && } + {view === 'individual' && ( + + )}
          ); }; diff --git a/GUI/src/types/dataModels.ts b/GUI/src/types/dataModels.ts index ea789db7..74a71eca 100644 --- a/GUI/src/types/dataModels.ts +++ b/GUI/src/types/dataModels.ts @@ -1,12 +1,12 @@ export type DataModel = { - modelId: string; + modelId: number; modelName: string; - dgName: string; - dgId: string; + dgName?: string; + dgId: string |number; platform: string; baseModels: string[]; maturity: string; - version: string; + version?: string; }; export type TrainingProgressData = { @@ -23,4 +23,55 @@ export type SSEEventData = { sessionId: string; trainingStatus: string; progressPercentage: number; -}; \ No newline at end of file +}; + +export type UpdatedDataModelPayload = { + modelId: number; + connectedDgId: number | string; + deploymentEnv: string; + baseModels: string[] |string; + maturityLabel: string; + updateType: string; +}; + +export type CreateDataModelPayload={ + modelName: string; + dgId: string | number; + baseModels: string[]; + deploymentPlatform: string; + maturityLabel: string; +} + +export type FilterData = { + modelNames: string[]; + modelVersions: string[]; + deploymentsEnvs: string[]; + datasetGroups: Array<{ id: number; name: string }>; + trainingStatuses: string[]; + maturityLabels: string[]; +}; + +export type DataModelResponse = { + id: number; + modelName: string; + connectedDgName: string; + majorVersion: number; + minorVersion: number; + latest: boolean; + dgVersion: string; + lastTrained: string; + trainingStatus: string; + deploymentEnv: string; + maturityLabel: string; + trainingResults: string[]; +}; + +export type Filters = { + modelName: string; + version: string; + platform: string; + datasetGroup: number; + trainingStatus: string; + maturity: string; + sort: 'asc' | 'desc'; +}; diff --git a/GUI/src/utils/commonUtilts.ts b/GUI/src/utils/commonUtilts.ts index 7ee6dcaf..277efd10 100644 --- a/GUI/src/utils/commonUtilts.ts +++ b/GUI/src/utils/commonUtilts.ts @@ -2,13 +2,19 @@ import { rankItem } from '@tanstack/match-sorter-utils'; import { FilterFn } from '@tanstack/react-table'; import moment from 'moment'; -export const formattedArray = (data: string[]) => { +type FormattedOption = { + label: string; + value: string; +}; + +export const formattedArray = (data: string[]|undefined): FormattedOption[]|undefined => { return data?.map((name) => ({ label: name, value: name, })); }; + export const convertTimestampToDateTime = (timestamp: number) => { return moment.unix(timestamp).format('YYYY-MM-DD HH:mm:ss'); }; diff --git a/GUI/src/utils/dataModelsUtils.ts b/GUI/src/utils/dataModelsUtils.ts index 6028cd8f..d1bc7135 100644 --- a/GUI/src/utils/dataModelsUtils.ts +++ b/GUI/src/utils/dataModelsUtils.ts @@ -1,22 +1,22 @@ import { DataModel } from 'types/dataModels'; -export const validateDataModel = (dataModel) => { +export const validateDataModel = (dataModel: Partial) => { const { modelName, dgId, platform, baseModels, maturity } = dataModel; const newErrors: any = {}; - if (!modelName.trim()) newErrors.modelName = 'Model Name is required'; - if (!platform.trim()) newErrors.platform = 'Platform is required'; + if (!modelName?.trim()) newErrors.modelName = 'Model Name is required'; + if (!platform?.trim()) newErrors.platform = 'Platform is required'; if (dgId === 0) newErrors.dgId = 'Dataset group is required'; if (baseModels?.length === 0) newErrors.baseModels = 'At least one Base Model is required'; - if (!maturity.trim()) newErrors.maturity = 'Maturity is required'; + if (!maturity?.trim()) newErrors.maturity = 'Maturity is required'; return newErrors; }; export const customFormattedArray = >( - data: T[], + data: T[] |undefined, attributeName: keyof T ) => { return data?.map((item) => ({ @@ -46,18 +46,17 @@ export const dgArrayWithVersions = >( }; export const getChangedAttributes = ( - original: DataModel, - updated: DataModel + original: Partial, + updated: Partial ): Partial> => { const changes: Partial> = {}; (Object.keys(original) as (keyof DataModel)[]).forEach((key) => { if (original[key] !== updated[key]) { - changes[key] = updated[key]; - } else { - changes[key] = null; + changes[key] = updated[key] as string | null; } }); return changes; }; + diff --git a/GUI/translations/en/common.json b/GUI/translations/en/common.json index 1444c612..77743efa 100644 --- a/GUI/translations/en/common.json +++ b/GUI/translations/en/common.json @@ -334,6 +334,30 @@ "production": "Production", "staging": "Staging", "testing": "Testing" + }, + "dataModelCard": { + "datasetGroup": "Dataset Group", + "dgVersion": "Dataset Group Version", + "lastTrained": "Last Trained" + }, + "trainingResults": { + "title": "Training Results", + "bestPerformingModel": "Best Performing Model", + "classes": "Classes", + "accuracy": "Accuracy", + "f1Score": "F1 Score", + "noResults": "No training results available", + "viewResults": " View Results" + }, + "createDataModel": { + "title": "Create Data Model", + "replaceTitle": "Warning: Replace Production Model", + "replaceDesc": "Adding this model to production will replace the current production model. Are you sure you want to proceed?", + "successTitle": "Data Model Created and Trained", + "successDesc": " You have successfully created and trained the data model. You can view it on the data model dashboard.", + "viewAll": "View All Data Models", + "errorTitle": "Error Creating Data Model", + "errorDesc": " There was an issue creating or training the data model. Please try again. If the problem persists, contact support for assistance." } } } From 507600d2edd129bcfe936a2bb9802d33b8b1188e Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Tue, 13 Aug 2024 23:18:45 +0530 Subject: [PATCH 453/582] ESCLASS-190-outlook: Outlook integration API issues fixed and change payloads for anonymizer --- .../hbs/return_jira_issue_info.handlebars | 5 +- .../return_outlook_payload_info.handlebars | 2 - .../DSL/GET/classifier/testmodel/models.yml | 2 - .../integration/outlook/subscribe.yml | 2 +- .../POST/classifier/testmodel/test-data.yml | 11 +- .../integration/jira/cloud/accept.yml | 2 + .../classifier/integration/outlook/accept.yml | 48 ++++-- .../DSL/POST/internal/corrected.yml | 141 +++++++++++++++ .../DSL/POST/internal/create.yml | 160 ++++++++++++++++++ docker-compose.yml | 6 +- notification-server/src/openSearch.js | 6 +- 11 files changed, 358 insertions(+), 27 deletions(-) create mode 100644 DSL/Ruuter.public/DSL/POST/internal/corrected.yml create mode 100644 DSL/Ruuter.public/DSL/POST/internal/create.yml diff --git a/DSL/DMapper/hbs/return_jira_issue_info.handlebars b/DSL/DMapper/hbs/return_jira_issue_info.handlebars index adb45ef9..0bb1c705 100644 --- a/DSL/DMapper/hbs/return_jira_issue_info.handlebars +++ b/DSL/DMapper/hbs/return_jira_issue_info.handlebars @@ -1,8 +1,5 @@ { - "jiraId": "{{data.key}}", - "summary": "{{data.fields.summary}}", - "labels": "{{data.fields.labels}}", - "projectId": "{{data.fields.project.key}}", + "title": "{{data.fields.summary}}", "description": "{{data.fields.description}}", "attachments": [ {{#each data.fields.attachment}} diff --git a/DSL/DMapper/hbs/return_outlook_payload_info.handlebars b/DSL/DMapper/hbs/return_outlook_payload_info.handlebars index 2e0d38d4..e26ccefb 100644 --- a/DSL/DMapper/hbs/return_outlook_payload_info.handlebars +++ b/DSL/DMapper/hbs/return_outlook_payload_info.handlebars @@ -1,7 +1,5 @@ { - "outlookId": "{{{data.id}}}", "subject": "{{{data.subject}}}", - "parentFolderId": "{{{data.parentFolderId}}}", "categories": "{{{data.categories}}}", "body": "{{{jsEscape data.body.content}}}", "fromEmailAddress": "{{{data.from.emailAddress.address}}}", diff --git a/DSL/Ruuter.private/DSL/GET/classifier/testmodel/models.yml b/DSL/Ruuter.private/DSL/GET/classifier/testmodel/models.yml index cad039c0..c6b0aec4 100644 --- a/DSL/Ruuter.private/DSL/GET/classifier/testmodel/models.yml +++ b/DSL/Ruuter.private/DSL/GET/classifier/testmodel/models.yml @@ -11,8 +11,6 @@ get_test_data_models: call: http.post args: url: "[#CLASSIFIER_RESQL]/get-test-data-models" - body: - id: ${model_id} result: res_model next: check_status diff --git a/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/subscribe.yml b/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/subscribe.yml index 3c4e188f..881bf013 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/subscribe.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/subscribe.yml @@ -87,7 +87,7 @@ subscribe_outlook: body: changeType: "created,updated" notificationUrl: "[#CLASSIFIER_RUUTER_PUBLIC_FRONTEND_URL]/classifier/integration/outlook/accept" - resource: "me/mailFolders('inbox')/messages" + resource: "me/messages" expirationDateTime: ${expiration_date_time} clientState: "secretClientValue" result: res_subscribe diff --git a/DSL/Ruuter.private/DSL/POST/classifier/testmodel/test-data.yml b/DSL/Ruuter.private/DSL/POST/classifier/testmodel/test-data.yml index fe390f23..2d736dc5 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/testmodel/test-data.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/testmodel/test-data.yml @@ -22,7 +22,7 @@ declaration: extract_request_data: assign: model_id: ${incoming.body.modelId} - text: ${incoming.headers.text} + text: ${incoming.body.text} cookie: ${incoming.headers.cookie} next: check_for_request_data @@ -57,7 +57,7 @@ check_data_model_exist: switch: - condition: ${res_model.response.body.length>0} next: send_data_to_predict - next: assign_fail_response + next: return_model_not_found send_data_to_predict: call: reflect.mock @@ -81,7 +81,7 @@ assign_success_response: assign: format_res: { modelId: '${model_id}', - data: '${res_model.response.body}', + data: '${res_predict.response.body}', operationSuccessful: true, } next: return_ok @@ -105,6 +105,11 @@ return_incorrect_request: return: 'Missing Required Fields' next: end +return_model_not_found: + status: 404 + return: 'Model Not Found' + next: end + return_empty_request: status: 400 return: 'Text Data Empty' diff --git a/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml b/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml index 2519cec5..a009a4e1 100644 --- a/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml +++ b/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml @@ -126,6 +126,8 @@ send_issue_data: platform: 'JIRA' key: ${issue_info.key} data: ${extract_info} + parentFolderId: null + labels: ${issue_info.fields.labels} response: statusCodeValue: 200 result: res diff --git a/DSL/Ruuter.public/DSL/POST/classifier/integration/outlook/accept.yml b/DSL/Ruuter.public/DSL/POST/classifier/integration/outlook/accept.yml index 5e0f0194..e8d4ffa9 100644 --- a/DSL/Ruuter.public/DSL/POST/classifier/integration/outlook/accept.yml +++ b/DSL/Ruuter.public/DSL/POST/classifier/integration/outlook/accept.yml @@ -91,6 +91,12 @@ get_access_token: result: res next: assign_access_token +check_access_token: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: assign_access_token + next: return_access_token_not_found + assign_access_token: assign: access_token: ${res.response.body.access_token} @@ -108,36 +114,42 @@ get_extracted_mail_info: check_extracted_mail_info: switch: - condition: ${200 <= mail_info_data.response.statusCodeValue && mail_info_data.response.statusCodeValue < 300} - next: check_event_type - next: return_mail_info_not_found - -check_event_type: - switch: - - condition: ${event_type === 'updated'} next: get_existing_folder_id - next: rearrange_mail_payload + next: return_mail_info_not_found get_existing_folder_id: call: http.post args: url: "[#CLASSIFIER_RESQL]/get-outlook-input-row-data" body: - inputId: ${mail_info_data.id} + inputId: ${mail_info_data.response.body.internetMessageId} result: existing_outlook_info next: check_input_response check_input_response: switch: - condition: ${200 <= existing_outlook_info.response.statusCodeValue && existing_outlook_info.response.statusCodeValue < 300} - next: check_folder_id + next: check_folders_exist next: return_db_request_fail +check_folders_exist: + switch: + - condition: ${existing_outlook_info.response.body.length>0} + next: check_folder_id + next: check_event_type + check_folder_id: switch: - - condition: ${check_folder_id.response.body.primaryFolderId !== mail_info_data.parentFolderId } + - condition: ${existing_outlook_info.response.body.primaryFolderId !== mail_info_data.response.body.parentFolderId} next: rearrange_mail_payload next: end +check_event_type: + switch: + - condition: ${event_type === 'updated'} + next: return_folder_not_found + next: rearrange_mail_payload + rearrange_mail_payload: call: http.post args: @@ -159,8 +171,10 @@ send_outlook_data: type: json body: platform: 'OUTLOOK' - key: ${mail_info_data.id} + key: ${mail_info_data.response.body.internetMessageId} data: ${outlook_body} + parentFolderId: ${mail_info_data.response.body.parentFolderId} + labels: null response: statusCodeValue: 200 result: res @@ -178,10 +192,20 @@ return_ok: next: end return_mail_info_not_found: - status: 400 + status: 404 return: "Mail Info Not Found" next: end +return_access_token_not_found: + status: 404 + return: "Access Token Not Found" + next: end + +return_folder_not_found: + status: 404 + return: "Folder Data Not Found" + next: end + return_bad_request: status: 400 return: "Bad Request" diff --git a/DSL/Ruuter.public/DSL/POST/internal/corrected.yml b/DSL/Ruuter.public/DSL/POST/internal/corrected.yml new file mode 100644 index 00000000..45e7deb4 --- /dev/null +++ b/DSL/Ruuter.public/DSL/POST/internal/corrected.yml @@ -0,0 +1,141 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'CORRECTED'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: inferenceId + type: number + description: "Body field 'inferenceId'" + - field: isCorrected + type: boolean + description: "Body field 'isCorrected'" + - field: correctedLabels + type: json + description: "Body field 'correctedLabels'" + - field: averageCorrectedClassesProbability + type: int + description: "Body field 'averageCorrectedClassesProbability'" + - field: primaryFolderId + type: string + description: "Body field 'primaryFolderId'" + - field: platform + type: string + description: "Body field 'platform'" + headers: + - field: cookie + type: string + description: "Cookie field" + +extract_request_data: + assign: + inference_id: ${incoming.body.inferenceId} + is_corrected: ${incoming.body.isCorrected} + corrected_labels: ${incoming.body.correctedLabels} + average_corrected_classes_probability: ${incoming.body.averageCorrectedClassesProbability} + primary_folder_id: ${incoming.body.primaryFolderId} + platform: ${incoming.body.platform} + next: check_for_request_data + +check_for_request_data: + switch: + - condition: ${inference_id !== null || corrected_labels !== null || average_corrected_classes_probability !== null || platform !== null} + next: check_platform_data + next: return_incorrect_request + +check_platform_data: + switch: + - condition: ${platform == 'OUTLOOK'} + next: check_primary_folder_exist + next: get_input_metadata_by_id + +check_primary_folder_exist: + switch: + - condition: ${primary_folder_id !== null} + next: get_input_metadata_by_id + next: return_primary_folder_not_found + +get_input_metadata_by_id: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-input-metadata-exits-by-id" + body: + id: ${inference_id} + result: res_input_id + next: check_input_metadata_status + +check_input_metadata_status: + switch: + - condition: ${200 <= res_input_id.response.statusCodeValue && res_input_id.response.statusCodeValue < 300} + next: check_input_metadata_exist + next: assign_fail_response + +check_input_metadata_exist: + switch: + - condition: ${res_input_id.response.body.length>0} + next: update_input_metadata + next: return_inference_data_not_found + +update_input_metadata: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/update-input-metadata" + body: + id: ${inference_id} + is_corrected: ${is_corrected} + corrected_labels: ${JSON.stringify(corrected_labels)} + average_corrected_classes_probability: ${average_corrected_classes_probability} + primary_folder_id: ${platform !== 'OUTLOOK' ? '' :primary_folder_id} + result: res_input + next: check_status + +check_status: + switch: + - condition: ${200 <= res_input.response.statusCodeValue && res_input.response.statusCodeValue < 300} + next: assign_success_response + next: assign_fail_response + +assign_success_response: + assign: + format_res: { + inferenceId: '${inference_id}', + operationSuccessful: true, + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + inferenceId: '${inference_id}', + operationSuccessful: false, + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end + +return_incorrect_request: + status: 400 + return: 'Missing Required Fields' + next: end + +return_primary_folder_not_found: + status: 400 + return: 'Missing Primary Folder Id' + next: end + +return_inference_data_not_found: + status: 400 + return: 'Inference data not found' + next: end diff --git a/DSL/Ruuter.public/DSL/POST/internal/create.yml b/DSL/Ruuter.public/DSL/POST/internal/create.yml new file mode 100644 index 00000000..86272416 --- /dev/null +++ b/DSL/Ruuter.public/DSL/POST/internal/create.yml @@ -0,0 +1,160 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'CREATE'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: inputId + type: string + description: "Body field 'inputId'" + - field: inferenceText + type: string + description: "Body field 'inferenceText'" + - field: predictedLabels + type: json + description: "Body field 'predictedLabels'" + - field: averagePredictedClassesProbability + type: int + description: "Body field 'averagePredictedClassesProbability'" + - field: platform + type: string + description: "Body field 'platform'" + - field: primaryFolderId + type: string + description: "Body field 'primaryFolderId'" + headers: + - field: cookie + type: string + description: "Cookie field" + +extract_request_data: + assign: + input_id: ${incoming.body.inputId} + inference_text: ${incoming.body.inferenceText} + predicted_labels_org: ${incoming.body.predictedLabels} + average_predicted_classes_probability: ${incoming.body.averagePredictedClassesProbability} + platform: ${incoming.body.platform} + primary_folder_id: ${incoming.body.primaryFolderId} + next: check_for_request_data + +check_for_request_data: + switch: + - condition: ${input_id !== null || inference_text !== null || predicted_labels_org !== null || average_predicted_classes_probability !== null || platform !== null} + next: check_platform_data + next: return_incorrect_request + +check_platform_data: + switch: + - condition: ${platform == 'OUTLOOK'} + next: check_primary_folder_exist + next: get_epoch_date + +check_primary_folder_exist: + switch: + - condition: ${primary_folder_id !== null} + next: get_epoch_date + next: return_primary_folder_not_found + +get_epoch_date: + assign: + current_epoch: ${Date.now()} + next: create_input_metadata + +create_input_metadata: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/insert-input-metadata" + body: + input_id: ${input_id} + inference_time_stamp: ${new Date(current_epoch).toISOString()} + inference_text: ${inference_text} + predicted_labels: ${JSON.stringify(predicted_labels_org)} + average_predicted_classes_probability: ${average_predicted_classes_probability} + platform: ${platform} + primary_folder_id: ${platform !== 'OUTLOOK' ? '' :primary_folder_id} + result: res_input + next: check_status + +check_status: + switch: + - condition: ${200 <= res_input.response.statusCodeValue && res_input.response.statusCodeValue < 300} + next: check_input_type + next: assign_fail_response + +check_input_type: + switch: + - condition: ${platform == 'OUTLOOK'} + next: outlook_flow + - condition: ${platform == 'JIRA'} + next: jira_flow + next: assign_success_response + +outlook_flow: + call: http.post + args: + url: "[#CLASSIFIER_RUUTER_PRIVATE]/classifier/integration/outlook/label" + headers: + cookie: ${incoming.headers.cookie} + body: + mailId: ${input_id} + folderId: ${primary_folder_id} + result: res_label + next: check_label_status + +jira_flow: + call: http.post + args: + url: "[#CLASSIFIER_RUUTER_PRIVATE]/classifier/integration/jira/cloud/label" + headers: + cookie: ${incoming.headers.cookie} + body: + issueKey: ${input_id} + labels: ${predicted_labels_org} + result: res_label + next: check_label_status + +check_label_status: + switch: + - condition: ${200 <= res_label.response.statusCodeValue && res_label.response.statusCodeValue < 300} + next: assign_success_response + next: assign_fail_response + +assign_success_response: + assign: + format_res: { + inferenceId: '${res_input.response.body[0].id}', + operationSuccessful: true, + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + inferenceId: '', + operationSuccessful: false, + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end + +return_incorrect_request: + status: 400 + return: 'Missing Required Fields' + next: end + +return_primary_folder_not_found: + status: 400 + return: 'Missing Primary Folder Id' + next: end diff --git a/docker-compose.yml b/docker-compose.yml index 0a73bd8c..aa52ef54 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ services: environment: - application.cors.allowedOrigins=http://localhost:8086,http://localhost:3001,http://localhost:3003,http://localhost:3004,http://localhost:8080,http://localhost:8000 - application.httpCodesAllowList=200,201,202,400,401,403,500 - - application.internalRequests.allowedIPs=127.0.0.1 + - application.internalRequests.allowedIPs=127.0.0.1,172.25.0.7 - application.logging.displayRequestContent=true - application.logging.displayResponseContent=true - server.port=8086 @@ -353,5 +353,9 @@ networks: bykstack: name: bykstack driver: bridge + ipam: + config: + - subnet: 172.25.0.0/16 + gateway: 172.25.0.1 driver_opts: com.docker.network.driver.mtu: 1400 \ No newline at end of file diff --git a/notification-server/src/openSearch.js b/notification-server/src/openSearch.js index 6e649b80..21755d93 100644 --- a/notification-server/src/openSearch.js +++ b/notification-server/src/openSearch.js @@ -17,7 +17,8 @@ async function searchDatasetGroupNotification({ sessionId, connectionId, sender must_not: { match: { sentTo: connectionId } }, }, }, - sort: { timestamp: { order: "asc" } }, + sort: { timestamp: { order: "desc" } }, + size: 1, }, }); @@ -49,7 +50,8 @@ async function searchModelNotification({ sessionId, connectionId, sender }) { must_not: { match: { sentTo: connectionId } }, }, }, - sort: { timestamp: { order: "asc" } }, + sort: { timestamp: { order: "desc" } }, + size: 1, }, }); From 95a16bfa695f6e795742929ded6a2d2c2c61b341 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Tue, 13 Aug 2024 23:33:18 +0530 Subject: [PATCH 454/582] ESCLASS-190-outlook: add mail id to paylaod,it change with folder id --- .../DSL/POST/classifier/integration/jira/cloud/accept.yml | 1 + .../DSL/POST/classifier/integration/outlook/accept.yml | 1 + DSL/Ruuter.public/DSL/POST/internal/create.yml | 3 ++- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml b/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml index a009a4e1..43140166 100644 --- a/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml +++ b/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml @@ -127,6 +127,7 @@ send_issue_data: key: ${issue_info.key} data: ${extract_info} parentFolderId: null + mailId: null labels: ${issue_info.fields.labels} response: statusCodeValue: 200 diff --git a/DSL/Ruuter.public/DSL/POST/classifier/integration/outlook/accept.yml b/DSL/Ruuter.public/DSL/POST/classifier/integration/outlook/accept.yml index e8d4ffa9..7fc4d5c9 100644 --- a/DSL/Ruuter.public/DSL/POST/classifier/integration/outlook/accept.yml +++ b/DSL/Ruuter.public/DSL/POST/classifier/integration/outlook/accept.yml @@ -174,6 +174,7 @@ send_outlook_data: key: ${mail_info_data.response.body.internetMessageId} data: ${outlook_body} parentFolderId: ${mail_info_data.response.body.parentFolderId} + mailId: ${mail_info_data.response.body.id} labels: null response: statusCodeValue: 200 diff --git a/DSL/Ruuter.public/DSL/POST/internal/create.yml b/DSL/Ruuter.public/DSL/POST/internal/create.yml index 86272416..2dee7257 100644 --- a/DSL/Ruuter.public/DSL/POST/internal/create.yml +++ b/DSL/Ruuter.public/DSL/POST/internal/create.yml @@ -34,6 +34,7 @@ declaration: extract_request_data: assign: input_id: ${incoming.body.inputId} + mail_id: ${incoming.body.mailId} inference_text: ${incoming.body.inferenceText} predicted_labels_org: ${incoming.body.predictedLabels} average_predicted_classes_probability: ${incoming.body.averagePredictedClassesProbability} @@ -100,7 +101,7 @@ outlook_flow: headers: cookie: ${incoming.headers.cookie} body: - mailId: ${input_id} + mailId: ${mail_id} folderId: ${primary_folder_id} result: res_label next: check_label_status From b8c6b51d0db136d19283d283f8a65ac4a41e3f9e Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Tue, 13 Aug 2024 23:56:42 +0530 Subject: [PATCH 455/582] static ip address for all containers update --- docker-compose.yml | 65 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 19 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 0a73bd8c..21595405 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,3 +1,5 @@ +version: '3.7' + services: ruuter-public: container_name: ruuter-public @@ -15,7 +17,8 @@ services: ports: - 8086:8086 networks: - - bykstack + bykstack: + ipv4_address: 172.25.0.2 cpus: "0.5" mem_limit: "512M" @@ -37,7 +40,8 @@ services: ports: - 8088:8088 networks: - - bykstack + bykstack: + ipv4_address: 172.25.0.3 cpus: "0.5" mem_limit: "512M" @@ -55,7 +59,8 @@ services: ports: - 3000:3000 networks: - - bykstack + bykstack: + ipv4_address: 172.25.0.4 tim: container_name: tim @@ -67,7 +72,8 @@ services: ports: - 8085:8085 networks: - - bykstack + bykstack: + ipv4_address: 172.25.0.5 extra_hosts: - "host.docker.internal:host-gateway" cpus: "0.5" @@ -86,7 +92,8 @@ services: ports: - 9876:5432 networks: - - bykstack + bykstack: + ipv4_address: 172.25.0.6 gui: container_name: gui @@ -112,7 +119,8 @@ services: - /app/node_modules - ./GUI:/app networks: - - bykstack + bykstack: + ipv4_address: 172.25.0.7 cpus: "0.5" mem_limit: "1G" @@ -122,7 +130,8 @@ services: ports: - 3004:3004 networks: - - bykstack + bykstack: + ipv4_address: 172.25.0.8 resql: container_name: resql @@ -141,7 +150,8 @@ services: volumes: - ./DSL/Resql:/workspace/app/templates/classifier networks: - - bykstack + bykstack: + ipv4_address: 172.25.0.9 users_db: container_name: users_db @@ -155,7 +165,8 @@ services: volumes: - /home/ubuntu/user_db_files:/var/lib/postgresql/data networks: - - bykstack + bykstack: + ipv4_address: 172.25.0.10 restart: always cron-manager: @@ -171,13 +182,17 @@ services: ports: - 9010:8080 networks: - - bykstack + bykstack: + ipv4_address: 172.25.0.11 init: image: busybox command: ["sh", "-c", "chmod -R 777 /shared"] volumes: - shared-volume:/shared + networks: + bykstack: + ipv4_address: 172.25.0.12 file-handler: build: @@ -198,7 +213,8 @@ services: ports: - "8000:8000" networks: - - bykstack + bykstack: + ipv4_address: 172.25.0.13 depends_on: - init @@ -218,7 +234,8 @@ services: - file-handler - init networks: - - bykstack + bykstack: + ipv4_address: 172.25.0.14 dataset-processor: build: @@ -246,7 +263,8 @@ services: ports: - "8001:8001" networks: - - bykstack + bykstack: + ipv4_address: 172.25.0.15 depends_on: - init - s3-ferry @@ -267,6 +285,9 @@ services: - S3_FERRY_URL=http://s3-ferry:3000/v1/files/copy ports: - "8003:8003" + networks: + bykstack: + ipv4_address: 172.25.0.7 depends_on: - init - s3-ferry @@ -294,7 +315,8 @@ services: - 9200:9200 - 9600:9600 networks: - - bykstack + bykstack: + ipv4_address: 172.25.0.16 notifications-node: container_name: notifications-node @@ -319,7 +341,8 @@ services: - /app/node_modules - ./notification-server:/app networks: - - bykstack + bykstack: + ipv4_address: 172.25.0.17 data-enrichment-api: container_name: data-enrichment-api @@ -330,7 +353,8 @@ services: ports: - "8005:8005" networks: - - bykstack + bykstack: + ipv4_address: 172.25.0.18 anonymizer: build: @@ -343,7 +367,8 @@ services: - JIRA_INFERENCE_ENDPOINT=http://model-inference:8003/classifier/deployment/jira/inference - OUTLOOK_INFERENCE_ENDPOINT=http://model-inference:8003/classifier/deployment/outlook/inference networks: - - bykstack + bykstack: + ipv4_address: 172.25.0.19 volumes: shared-volume: @@ -353,5 +378,7 @@ networks: bykstack: name: bykstack driver: bridge - driver_opts: - com.docker.network.driver.mtu: 1400 \ No newline at end of file + ipam: + config: + - subnet: 172.25.0.0/24 + gateway: 172.25.0.1 From 4754c1bf0cb4adc24aa00e956c7d58849618c72b Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 14 Aug 2024 00:06:13 +0530 Subject: [PATCH 456/582] Progress session for validation will be created based on the newdgId --- dataset-processor/dataset_processor.py | 15 ++++++++--- dataset-processor/dataset_processor_api.py | 2 +- dataset-processor/dataset_validator.py | 29 ++++++++++++++++------ 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/dataset-processor/dataset_processor.py b/dataset-processor/dataset_processor.py index 12dff102..dd85dd4b 100644 --- a/dataset-processor/dataset_processor.py +++ b/dataset-processor/dataset_processor.py @@ -339,20 +339,25 @@ def update_preprocess_status(self,dg_id, cookie, processed_data_available, raw_d return None def process_handler(self, dgId, newDgId, cookie, updateType, savedFilePath, patchPayload): - session_id = self.get_session_id(dgId, cookie) - if not session_id: - return self.generate_response(False, MSG_FAIL) print(MSG_PROCESS_HANDLER_STARTED.format(updateType)) page_count = self.get_page_count(dgId, cookie) print(MSG_PAGE_COUNT.format(page_count)) if updateType == "minor" and page_count > 0: + session_id = self.get_session_id(newDgId, cookie) + if not session_id: + return self.generate_response(False, MSG_FAIL) updateType = "minor_append_update" elif updateType == "patch": - pass + session_id = self.get_session_id(dgId, cookie) + if not session_id: + return self.generate_response(False, MSG_FAIL) else: updateType = "minor_initial_update" + session_id = self.get_session_id(newDgId, cookie) + if not session_id: + return self.generate_response(False, MSG_FAIL) if updateType == "minor_initial_update": result = self.handle_minor_initial_update(dgId, newDgId, cookie, savedFilePath, session_id) @@ -557,6 +562,8 @@ def get_session_id(self, dgId, cookie): response = requests.get(GET_PROGRESS_SESSIONS_URL, headers=headers) response.raise_for_status() sessions = response.json().get("response", {}).get("data", []) + print("Sessions") + print(sessions) for session in sessions: if session['dgId'] == dgId: return session['id'] diff --git a/dataset-processor/dataset_processor_api.py b/dataset-processor/dataset_processor_api.py index e7841316..b8e4b994 100644 --- a/dataset-processor/dataset_processor_api.py +++ b/dataset-processor/dataset_processor_api.py @@ -62,7 +62,7 @@ async def forward_request(request: Request, response: Response): except Exception as e: raise HTTPException(status_code=400, detail=f"Invalid JSON payload: {str(e)}") - validator_response = validator.process_request(int(payload["dgId"]), payload["cookie"], payload["updateType"], payload["savedFilePath"], payload["patchPayload"]) + validator_response = validator.process_request(int(payload["dgId"]), int(payload["newDgId"]), payload["cookie"], payload["updateType"], payload["savedFilePath"], payload["patchPayload"]) forward_payload = {} forward_payload["dgId"] = int(payload["dgId"]) forward_payload["newDgId"] = int(payload["newDgId"]) diff --git a/dataset-processor/dataset_validator.py b/dataset-processor/dataset_validator.py index 11a82a2a..b9e31ed3 100644 --- a/dataset-processor/dataset_validator.py +++ b/dataset-processor/dataset_validator.py @@ -10,17 +10,29 @@ class DatasetValidator: def __init__(self): pass - def process_request(self, dgId, cookie, updateType, savedFilePath, patchPayload=None): + def process_request(self, dgId, newDgId, cookie, updateType, savedFilePath, patchPayload=None): print(MSG_PROCESS_REQUEST_STARTED) print(f"dgId: {dgId}, updateType: {updateType}, savedFilePath: {savedFilePath}") - metadata = self.get_datagroup_metadata(dgId, cookie) - if not metadata: - return self.generate_response(False, MSG_REQUEST_FAILED.format("Metadata")) - session_id = self.create_progress_session(metadata, cookie) - print(f"Progress Session ID : {session_id}") - if not session_id: - return self.generate_response(False, MSG_REQUEST_FAILED.format("Progress session creation")) + if updateType == "minor": + metadata = self.get_datagroup_metadata(newDgId, cookie) + if not metadata: + return self.generate_response(False, MSG_REQUEST_FAILED.format("Metadata")) + session_id = self.create_progress_session(metadata, cookie) + print(f"Progress Session ID : {session_id}") + if not session_id: + return self.generate_response(False, MSG_REQUEST_FAILED.format("Progress session creation")) + elif updateType == "patch": + metadata = self.get_datagroup_metadata(dgId, cookie) + if not metadata: + return self.generate_response(False, MSG_REQUEST_FAILED.format("Metadata")) + + session_id = self.create_progress_session(metadata, cookie) + print(f"Progress Session ID : {session_id}") + if not session_id: + return self.generate_response(False, MSG_REQUEST_FAILED.format("Progress session creation")) + else: + return self.generate_response(False, "Unknown update type") try: # Initializing dataset processing @@ -353,6 +365,7 @@ def get_datagroup_metadata(self, dgId, cookie): response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: + print(e) print(MSG_REQUEST_FAILED.format("Metadata fetch")) return None From 0fce120fb2849823becd46b402b9185fa18e907b Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 14 Aug 2024 00:06:59 +0530 Subject: [PATCH 457/582] duplicate container static ip change --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 21595405..bc350c99 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -120,7 +120,7 @@ services: - ./GUI:/app networks: bykstack: - ipv4_address: 172.25.0.7 + ipv4_address: 172.25.0.20 cpus: "0.5" mem_limit: "1G" From 3fcecbee7da507fd55890aed2417f81d4dd99ef1 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 14 Aug 2024 00:13:39 +0530 Subject: [PATCH 458/582] Jira token verification endpoint --- anonymizer/anonymizer_api.py | 37 +++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/anonymizer/anonymizer_api.py b/anonymizer/anonymizer_api.py index 4d72c007..e7422b10 100644 --- a/anonymizer/anonymizer_api.py +++ b/anonymizer/anonymizer_api.py @@ -1,4 +1,4 @@ -from fastapi import FastAPI, Request +from fastapi import FastAPI, Request, Header, HTTPException from pydantic import BaseModel from ner import NERProcessor from text_processing import TextProcessor @@ -6,6 +6,9 @@ from html_cleaner import HTMLCleaner import os import requests +import hmac +import hashlib +import json app = FastAPI() @@ -56,6 +59,38 @@ async def process_text(request: Request): output_payload["status"] = False return output_payload + +@app.post("/verify_signature") +async def verify_signature_endpoint(request: Request, x_hub_signature: str = Header(...)): + try: + payload = await request.json() + secret = os.getenv("SHARED_SECRET") # You should set this environment variable + headers = {"x-hub-signature": x_hub_signature} + + is_valid = verify_signature(payload, headers, secret) + + if is_valid: + return {"status": True} + else: + return {"status": False}, 401 + except Exception as e: + return {"status": False, "error": str(e)}, 500 + +def verify_signature(payload: dict, headers: dict, secret: str) -> bool: + signature = headers.get("x-hub-signature") + if not signature: + raise HTTPException(status_code=400, detail="Signature missing") + + shared_secret = secret.encode('utf-8') + payload_string = json.dumps(payload).encode('utf-8') + + hmac_obj = hmac.new(shared_secret, payload_string, hashlib.sha256) + computed_signature = hmac_obj.hexdigest() + computed_signature_prefixed = f"sha256={computed_signature}" + + is_valid = hmac.compare_digest(computed_signature_prefixed, signature) + + return is_valid if __name__ == "__main__": import uvicorn From f84ed2fe51a0657b3c4899e30a92da4fcc05a1bc Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 14 Aug 2024 09:35:41 +0530 Subject: [PATCH 459/582] removing the non needed files --- DSL/Liquibase/data/update_refresh_token.sql | 4 ---- ...refresh_token.sql\357\200\215\357\200\215" | 19 ------------------- 2 files changed, 23 deletions(-) delete mode 100644 DSL/Liquibase/data/update_refresh_token.sql delete mode 100644 "DSL/Liquibase/data/update_refresh_token.sql\357\200\215\357\200\215" diff --git a/DSL/Liquibase/data/update_refresh_token.sql b/DSL/Liquibase/data/update_refresh_token.sql deleted file mode 100644 index 4987e3af..00000000 --- a/DSL/Liquibase/data/update_refresh_token.sql +++ /dev/null @@ -1,4 +0,0 @@ --- Update the refresh token in the database -UPDATE integration_status -SET token = 'dmFsdWU=' -WHERE platform='OUTLOOK'; diff --git "a/DSL/Liquibase/data/update_refresh_token.sql\357\200\215\357\200\215" "b/DSL/Liquibase/data/update_refresh_token.sql\357\200\215\357\200\215" deleted file mode 100644 index 8e0f6a3c..00000000 --- "a/DSL/Liquibase/data/update_refresh_token.sql\357\200\215\357\200\215" +++ /dev/null @@ -1,19 +0,0 @@ --- Update the refresh token in the database -UPDATE integration_status -SET token = 'value ' -WHERE platform='OUTLOOK'; -EOF - -# Function to parse ini file and extract the value for a given key under a given section -get_ini_value() { - local file= - local key= - awk -F '=' -v key="" ' == key { gsub(/^[ \t]+|[ \t]+$/, "", ); print ; exit }' "" -} - -# Get the values from dsl_config.ini -INI_FILE="constants.ini" -DB_PASSWORD= - - -docker run --rm --network bykstack -v /mnt/d/Estonia/New folder/classifier/DSL/Liquibase/changelog:/liquibase/changelog -v /mnt/d/Estonia/New folder/classifier/DSL/Liquibase/master.yml:/liquibase/master.yml -v /mnt/d/Estonia/New folder/classifier/DSL/Liquibase/data:/liquibase/data liquibase/liquibase --defaultsFile=/liquibase/changelog/liquibase.properties --changelog-file=master.yml --url=jdbc:postgresql://users_db:5432/classifier?user=postgres --password= update \ No newline at end of file From 4a9acd6c78c1ec2e6a282631c3c0d022695f4490 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Wed, 14 Aug 2024 10:22:05 +0530 Subject: [PATCH 460/582] ESCLASS-190-outlook: sonar issue fixed --- DSL/DMapper/lib/helpers.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DSL/DMapper/lib/helpers.js b/DSL/DMapper/lib/helpers.js index 2b5e581c..1baca2f2 100644 --- a/DSL/DMapper/lib/helpers.js +++ b/DSL/DMapper/lib/helpers.js @@ -39,8 +39,8 @@ export function isLabelsMismatch(newLabels, correctedLabels, predictedLabels) { Array.isArray(arr) && newLabels.length === arr.length ) { - for (let i = 0; i < newLabels.length; i++) { - if (!arr.includes(newLabels[i])) { + for (let label of newLabels) { + if (!arr.includes(label)) { return true; } } From 5291ae1b59a89418b51967af0c5be062400073e2 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Wed, 14 Aug 2024 11:24:13 +0530 Subject: [PATCH 461/582] fixes --- GUI/src/pages/DataModels/ConfigureDataModel.tsx | 12 ++++++------ GUI/src/pages/DataModels/CreateDataModel.tsx | 12 ++++++------ GUI/translations/en/common.json | 3 ++- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/GUI/src/pages/DataModels/ConfigureDataModel.tsx b/GUI/src/pages/DataModels/ConfigureDataModel.tsx index e9c5d575..a93c16bf 100644 --- a/GUI/src/pages/DataModels/ConfigureDataModel.tsx +++ b/GUI/src/pages/DataModels/ConfigureDataModel.tsx @@ -97,12 +97,12 @@ const ConfigureDataModel: FC = ({ } const updatedPayload = { - modelId: dataModel.modelId?? 0, - connectedDgId: payload.dgId ?? 0, - deploymentEnv: payload.platform ?? "", - baseModels: payload.baseModels ?? [""], - maturityLabel: payload.maturity ?? "", - updateType:updateType??"", + modelId: dataModel.modelId, + connectedDgId: payload.dgId, + deploymentEnv: payload.platform, + baseModels: payload.baseModels, + maturityLabel: payload.maturity, + updateType:updateType, }; if (updateType) { diff --git a/GUI/src/pages/DataModels/CreateDataModel.tsx b/GUI/src/pages/DataModels/CreateDataModel.tsx index f45f9b64..96464a19 100644 --- a/GUI/src/pages/DataModels/CreateDataModel.tsx +++ b/GUI/src/pages/DataModels/CreateDataModel.tsx @@ -93,11 +93,11 @@ const CreateDataModel: FC = () => { const handleCreate = () => { if (validateData()) { const payload = { - modelName: dataModel.modelName ??"", - dgId: dataModel.dgId?? 0, - baseModels: dataModel.baseModels?? [""], - deploymentPlatform: dataModel.platform ??"", - maturityLabel: dataModel.maturity?? "", + modelName: dataModel.modelName, + dgId: dataModel.dgId, + baseModels: dataModel.baseModels, + deploymentPlatform: dataModel.platform, + maturityLabel: dataModel.maturity, }; if ( @@ -141,7 +141,7 @@ const CreateDataModel: FC = () => { title: 'Data Model Created and Trained', content: (

          - You have successfully created and trained the data model. You can + You have successfully created and started training the data model. You can view it on the data model dashboard.

          ), diff --git a/GUI/translations/en/common.json b/GUI/translations/en/common.json index 77743efa..075d7e10 100644 --- a/GUI/translations/en/common.json +++ b/GUI/translations/en/common.json @@ -43,7 +43,8 @@ "choose": "Choose", "extedSession": "Extend Session", "unAuthorized": "Unauthorized", - "unAuthorizedDesc": "You do not have permission to view this page." + "unAuthorizedDesc": "You do not have permission to view this page.", + "close":"Close" }, "menu": { "userManagement": "User Management", From 9cac8f5fb35fcf5a788a33e0720138e0ce232dc5 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Wed, 14 Aug 2024 13:16:02 +0530 Subject: [PATCH 462/582] corrected texts api int --- GUI/src/components/MainNavigation/index.tsx | 3 +- .../CorrectedTextTable.scss | 39 +++ .../CorrectedTextsTables.tsx | 297 +++++------------- GUI/src/pages/CorrectedTexts/index.tsx | 78 ++++- GUI/src/types/correctedTextTypes.ts | 12 + GUI/src/types/correctedTextsTypes.ts | 11 - GUI/src/utils/endpoints.ts | 2 +- 7 files changed, 198 insertions(+), 244 deletions(-) create mode 100644 GUI/src/components/molecules/CorrectedTextTables/CorrectedTextTable.scss create mode 100644 GUI/src/types/correctedTextTypes.ts delete mode 100644 GUI/src/types/correctedTextsTypes.ts diff --git a/GUI/src/components/MainNavigation/index.tsx b/GUI/src/components/MainNavigation/index.tsx index 7528c7fa..91b2e830 100644 --- a/GUI/src/components/MainNavigation/index.tsx +++ b/GUI/src/components/MainNavigation/index.tsx @@ -7,8 +7,7 @@ import { MdOutlineDataset, MdPeople, MdSettings, - MdSettingsBackupRestore, - MdTextFormat, + MdSettingsBackupRestore } from 'react-icons/md'; import { useQuery } from '@tanstack/react-query'; import clsx from 'clsx'; diff --git a/GUI/src/components/molecules/CorrectedTextTables/CorrectedTextTable.scss b/GUI/src/components/molecules/CorrectedTextTables/CorrectedTextTable.scss new file mode 100644 index 00000000..a8a94278 --- /dev/null +++ b/GUI/src/components/molecules/CorrectedTextTables/CorrectedTextTable.scss @@ -0,0 +1,39 @@ +.correctedHierarchy { + text-align: center; + display: flex; + align-content: center; + align-items: center; + justify-content: center; + text-wrap: wrap; + width: 100%; +} + +.hierarchyLabels { + display: flex; + align-items: center; + justify-content: center; + width: 500px; + text-wrap: wrap; +} + +.probabilityLabels { + text-align: center; + display: flex; + align-content: center; + width: 200px; + text-wrap: wrap; +} + +.inferenceTimeLabel { + text-align: right; + display: flex; + align-content: center; + align-items: center; +} + +.inferenceTimeCell { + display: flex; + align-items: center; + font-size: 14px; + flex-direction: column; +} \ No newline at end of file diff --git a/GUI/src/components/molecules/CorrectedTextTables/CorrectedTextsTables.tsx b/GUI/src/components/molecules/CorrectedTextTables/CorrectedTextsTables.tsx index 70aad829..b3e0c8a5 100644 --- a/GUI/src/components/molecules/CorrectedTextTables/CorrectedTextsTables.tsx +++ b/GUI/src/components/molecules/CorrectedTextTables/CorrectedTextsTables.tsx @@ -1,5 +1,4 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { generateDynamicColumns } from 'utils/dataTableUtils'; +import React, { useEffect, useMemo, useState } from 'react'; import SkeletonTable from '../TableSkeleton/TableSkeleton'; import DataTable from 'components/DataTable'; import { @@ -7,86 +6,47 @@ import { createColumnHelper, PaginationState, } from '@tanstack/react-table'; -import { CorrectedTextResponseType } from 'types/correctedTextsTypes'; import { useTranslation } from 'react-i18next'; -import { useQuery } from '@tanstack/react-query'; -import mockDev from '../../../services/api-mock'; -import { correctedTextEndpoints } from 'utils/endpoints'; -import { correctedData } from 'data/mockData'; import { formatDateTime } from 'utils/commonUtilts'; +import Card from 'components/Card'; +import { InferencePayload } from 'types/correctedTextTypes'; +import './CorrectedTextTable.scss'; const CorrectedTextsTables = ({ - filters, - enableFetch, + correctedTextData, + totalPages, + isLoading, + setPagination, + pagination, }: { - filters: { - platform: string; - sort: string; - }; - enableFetch: boolean; + correctedTextData: InferencePayload[]; + totalPages: number; + isLoading: boolean; + setPagination: React.Dispatch>; + pagination: PaginationState; }) => { - console.log(filters); - const columnHelper = createColumnHelper(); - const [filteredData, setFilteredData] = useState( - [] - ); - const [loading, setLoading] = useState(false); + const columnHelper = createColumnHelper(); const { t } = useTranslation(); - const [totalPages, setTotalPages] = useState(1); - const [pagination, setPagination] = useState({ - pageIndex: 0, - pageSize: 5, - }); - const [platform, setPlatform] = useState('all'); - const [sortType, setSortType] = useState('all'); - // const { data, isLoading } = useQuery({ - // queryKey: ['correctedText', platform, sortType, pagination.pageIndex], - // queryFn: async () => { - // return await mockDev.get( - // correctedTextEndpoints.GET_CORRECTED_WORDS( - // pagination.pageIndex, - // pagination.pageSize, - // platform, - // sortType - // ) - // ); - // }, - // onSuccess: () => {}, - // }); const dataColumns = useMemo( () => [ - columnHelper.accessor('inferenceTime', { + columnHelper.accessor('inferenceTimeStamp', { header: () => ( -
          +
          {t('correctedTexts.inferenceTime') ?? ''}
          ), cell: (props) => ( -
          +
          { - formatDateTime(props?.row?.original?.inferenceTime) + formatDateTime(props?.row?.original?.inferenceTimeStamp) .formattedDate } { - formatDateTime(props?.row?.original?.inferenceTime) + formatDateTime(props?.row?.original?.inferenceTimeStamp) .formattedTime } @@ -96,22 +56,12 @@ const CorrectedTextsTables = ({ columnHelper.accessor('platform', { header: t('correctedTexts.platform') ?? '', }), - columnHelper.accessor('inferencedText', { + columnHelper.accessor('inferenceText', { header: t('correctedTexts.text') ?? '', }), columnHelper.accessor('predictedLabels', { header: () => ( -
          +
          ), cell: (props) => ( -
          - {formatArray(props?.row?.original?.predictedLabels)} +
          + {props?.row?.original?.predictedLabels && + formatArray(props?.row?.original?.predictedLabels)}
          ), }), columnHelper.accessor('averagePredictedClassesProbability', { header: () => ( -
          +
          {t('correctedTexts.predictedConfidenceProbability') ?? ''}
          ), cell: (props) => ( -
          - {props.row.original.averagePredictedClassesProbability}% +
          + {props?.row?.original?.averagePredictedClassesProbability}%
          ), meta: { @@ -168,17 +95,7 @@ const CorrectedTextsTables = ({ }), columnHelper.accessor('correctedLabels', { header: () => ( -
          +
          ), cell: (props) => ( -
          - {formatArray(props?.row?.original?.correctedLabels)} +
          + {props?.row?.original?.correctedLabels && + formatArray(props?.row?.original?.correctedLabels)}
          ), }), columnHelper.accessor('averageCorrectedClassesProbability', { header: () => ( -
          +
          {t('correctedTexts.correctedConfidenceProbability') ?? ''}
          ), cell: (props) => ( -
          - {props.row.original.averageCorrectedClassesProbability}% +
          + {props?.row?.original?.averageCorrectedClassesProbability && ( + <>{props?.row?.original?.averageCorrectedClassesProbability}% + )}
          ), }), @@ -234,100 +130,69 @@ const CorrectedTextsTables = ({ [t] ); - function paginateDataset(data: any[], pageIndex: number, pageSize: number) { - const startIndex = pageIndex * pageSize; - const endIndex = startIndex + pageSize; - - const pageData = data.slice(startIndex, endIndex); - - setFilteredData(pageData); - } - - const calculateNumberOfPages = (data: any[], pageSize: number) => { - return Math.ceil(data.length / pageSize); - }; + const formatArray = (array: string | string[]) => { + let formatedArray: string[]; + if (typeof array === 'string') { + try { + const cleanedInput = array?.replace(/\s+/g, ''); + formatedArray = JSON.parse(cleanedInput); + } catch (error) { + console.error('Error parsing input string:', error); + return ''; + } + } else { + formatedArray = array; + } - const formatArray = (array: string[]) => { - return array - .map((item, index) => (index === array.length - 1 ? item : item + ' ->')) + return formatedArray + .map((item, index) => + index === formatedArray?.length - 1 ? item : item + ' ->' + ) .join(' '); }; - const filterItems = useCallback(() => { - if (!enableFetch) { - return; - } - - let newData = correctedData; - - if (filters.platform && filters.platform !== 'all') { - newData = newData.filter((item) => item.platform === filters.platform); - } - - if (filters.sort && filters.sort !== 'all') { - newData = newData.sort((a, b) => { - if (filters.sort === 'asc') { - return a.inferenceTime.localeCompare(b.inferenceTime); - } else if (filters.sort === 'desc') { - return b.inferenceTime.localeCompare(a.inferenceTime); - } - return 0; - }); - } - - setFilteredData(newData); - }, [filters, enableFetch]); - - useEffect(() => { - filterItems(); - }, [filterItems]); - - useEffect(() => { - paginateDataset(correctedData, pagination.pageIndex, pagination.pageSize); - }, []); return (
          - {/* {isLoading && } */} - {/* {!isLoading && ( + {isLoading && } + {!isLoading && correctedTextData && correctedTextData.length === 0 && ( + +
          + {t('datasetGroups.detailedView.noData') ?? ''} +
          +
          + )} + {!isLoading && correctedTextData && correctedTextData.length > 0 && ( []} pagination={pagination} setPagination={(state: PaginationState) => { if ( - state?.pageIndex === pagination?.pageIndex && - state?.pageSize === pagination?.pageSize + state.pageIndex === pagination.pageIndex && + state.pageSize === pagination.pageSize ) return; - setPagination(state); + setPagination({ + pageIndex: state.pageIndex + 1, + pageSize: state.pageSize, + }); }} - pagesCount={pagination.pageIndex} + pagesCount={totalPages} isClientSide={false} /> - )} */} - - []} - pagination={pagination} - setPagination={(state: PaginationState) => { - if ( - state?.pageIndex === pagination?.pageIndex && - state?.pageSize === pagination?.pageSize - ) - return; - setPagination(state); - }} - pagesCount={calculateNumberOfPages( - correctedData, - pagination.pageSize - )} - isClientSide={false} - /> + )}
          ); }; -export default CorrectedTextsTables; +export default CorrectedTextsTables; \ No newline at end of file diff --git a/GUI/src/pages/CorrectedTexts/index.tsx b/GUI/src/pages/CorrectedTexts/index.tsx index e07944da..54d7094a 100644 --- a/GUI/src/pages/CorrectedTexts/index.tsx +++ b/GUI/src/pages/CorrectedTexts/index.tsx @@ -1,16 +1,17 @@ -import React, { FC, useState } from 'react'; +import { FC, useState } from 'react'; import './index.scss'; import { useTranslation } from 'react-i18next'; import { ButtonAppearanceTypes } from 'enums/commonEnums'; -import { Button, FormInput, FormSelect } from 'components'; -import { useNavigate } from 'react-router-dom'; -import { formattedArray } from 'utils/commonUtilts'; +import { Button, FormSelect } from 'components'; import CorrectedTextsTables from 'components/molecules/CorrectedTextTables/CorrectedTextsTables'; +import { useQuery } from '@tanstack/react-query'; +import { correctedTextEndpoints } from 'utils/endpoints'; +import apiDev from '../../services/api-dev'; +import { InferencePayload } from 'types/correctedTextTypes'; +import { PaginationState } from '@tanstack/react-table'; const CorrectedTexts: FC = () => { const { t } = useTranslation(); - const navigate = useNavigate(); - const [enableFetch, setEnableFetch] = useState(true); const [filters, setFilters] = useState({ @@ -18,6 +19,39 @@ const CorrectedTexts: FC = () => { sort: 'asc', }); + const [totalPages, setTotalPages] = useState(1); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 5, + }); + + const { + data: correctedTextData, + isLoading, + refetch, + } = useQuery({ + queryKey: ['correctedText', pagination.pageIndex], + queryFn: async () => { + const response = await apiDev.get( + correctedTextEndpoints.GET_CORRECTED_WORDS( + pagination.pageIndex, + pagination.pageSize, + filters.platform, + filters.sort + ) + ); + + return ( + (await response?.data?.response?.data) ?? ([] as InferencePayload[]) + ); + }, + onSuccess: (data: InferencePayload[]) => { + setTotalPages(data[0]?.totalPages ?? 1); + if (enableFetch) setEnableFetch(false); + }, + enabled: enableFetch, + }); + const handleFilterChange = (name: string, value: string) => { setEnableFetch(false); setFilters((prevFilters) => ({ @@ -53,13 +87,14 @@ const CorrectedTexts: FC = () => { - handleFilterChange('platform', selection?.value ?? '') + handleFilterChange('platform', selection?.value as string) } /> { { label: t('correctedTexts.filterDesc'), value: 'desc' }, ]} onSelectionChange={(selection) => - handleFilterChange('sort', selection?.value ?? '') + handleFilterChange('sort', (selection?.value as string) ?? '') } />
          -
          - +
          ); }; -export default CorrectedTexts; +export default CorrectedTexts; \ No newline at end of file diff --git a/GUI/src/types/correctedTextTypes.ts b/GUI/src/types/correctedTextTypes.ts new file mode 100644 index 00000000..4bf2360b --- /dev/null +++ b/GUI/src/types/correctedTextTypes.ts @@ -0,0 +1,12 @@ +export type InferencePayload = { + averageCorrectedClassesProbability: number | null; + averagePredictedClassesProbability: number; + correctedLabels: string[] | null; + inferenceId: number; + inferenceText: string; + inferenceTimeStamp: string; + inputId: string; + platform: string; + predictedLabels: string[]; + totalPages: number; +}; \ No newline at end of file diff --git a/GUI/src/types/correctedTextsTypes.ts b/GUI/src/types/correctedTextsTypes.ts deleted file mode 100644 index ddf43d59..00000000 --- a/GUI/src/types/correctedTextsTypes.ts +++ /dev/null @@ -1,11 +0,0 @@ -export type CorrectedTextResponseType = { - inferenceId: number; - itemId: string; - inferenceTime: string; - inferencedText: string; - predictedLabels: string[]; - averagePredictedClassesProbability: number; - platform: string; - correctedLabels: string[]; - averageCorrectedClassesProbability: number; -}; diff --git a/GUI/src/utils/endpoints.ts b/GUI/src/utils/endpoints.ts index 61b8a886..78082f0c 100644 --- a/GUI/src/utils/endpoints.ts +++ b/GUI/src/utils/endpoints.ts @@ -43,5 +43,5 @@ export const correctedTextEndpoints = { platform: string, sortType: string ) => - `/classifier/correctedtext?pageNum=${pageNumber}&pageSize=${pageSize}&platform=${platform}&sortType=${sortType}`, + `/classifier/inference/corrected-metadata?pageNum=${pageNumber}&pageSize=${pageSize}&platform=${platform}&sortType=${sortType}`, }; From 589d5154c2d78b90baa2e1a492af84813dd09197 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 14 Aug 2024 14:04:09 +0530 Subject: [PATCH 463/582] conflict fix --- docker-compose.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index bc350c99..7cc4aa9f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -380,5 +380,7 @@ networks: driver: bridge ipam: config: - - subnet: 172.25.0.0/24 + - subnet: 172.25.0.0/16 gateway: 172.25.0.1 + driver_opts: + com.docker.network.driver.mtu: 1400 From c66ff0782ffe48684045828e6f85ae7af78d3c80 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 14 Aug 2024 14:05:58 +0530 Subject: [PATCH 464/582] revert conflict fix --- docker-compose.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 31b9baa0..078f3d79 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -380,7 +380,5 @@ networks: driver: bridge ipam: config: - - subnet: 172.25.0.0/16 + - subnet: 172.25.0.0/24 gateway: 172.25.0.1 - driver_opts: - com.docker.network.driver.mtu: 1400 From 2e1c9c7197b43e1f1bd211e126241c2cbead5c5e Mon Sep 17 00:00:00 2001 From: Thirunayan22 Date: Wed, 14 Aug 2024 15:56:52 +0530 Subject: [PATCH 465/582] update training scripts to have new modelss --- model_trainer/inference.py | 22 +++++++++++----------- model_trainer/model_trainer.py | 14 ++++++++------ model_trainer/trainingpipeline.py | 8 ++++---- 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/model_trainer/inference.py b/model_trainer/inference.py index 957abd88..86205037 100644 --- a/model_trainer/inference.py +++ b/model_trainer/inference.py @@ -1,4 +1,4 @@ -from transformers import XLMRobertaTokenizer, XLMRobertaForSequenceClassification, XLNetForSequenceClassification, XLNetTokenizer, BertForSequenceClassification, BertTokenizer +from transformers import XLMRobertaTokenizer, XLMRobertaForSequenceClassification,Trainer, TrainingArguments, DistilBertTokenizer, DistilBertForSequenceClassification, BertForSequenceClassification, BertTokenizer import pickle import torch import os @@ -8,15 +8,15 @@ class InferencePipeline: def __init__(self, hierarchy_path, model_name, path_models_folder,path_label_encoder, path_classification_folder, models): - if model_name == 'xlnet-base-cased': - self.base_model = XLNetForSequenceClassification.from_pretrained('xlnet-base-cased') - self.tokenizer = XLNetTokenizer.from_pretrained('xlnet-base-cased') + if model_name == 'distil-bert': + self.base_model = DistilBertForSequenceClassification.from_pretrained('distilbert-base-uncased') + self.tokenizer = DistilBertTokenizer.from_pretrained('distilbert-base-uncased') - elif model_name == 'xlm-roberta-base': + elif model_name == 'roberta': self.base_model = XLMRobertaForSequenceClassification.from_pretrained('xlm-roberta-base') self.tokenizer = XLMRobertaTokenizer.from_pretrained('xlm-roberta-base') - elif model_name == 'bert-base-uncased': + elif model_name == 'bert': self.tokenizer = BertTokenizer.from_pretrained('bert-base-uncased') self.base_model = BertForSequenceClassification.from_pretrained('bert-base-uncased') @@ -69,14 +69,14 @@ def predict_class(self,text_input): label_encoder = self.label_encoder_dict[model_num] num_labels = len(label_encoder.classes_) - if self.model_name == 'xlnet-base-cased': - self.base_model.classifier = XLNetForSequenceClassification.from_pretrained('xlnet-base-cased', num_labels=num_labels).classifier - - elif self.model_name == 'xlm-roberta-base': + if self.model_name == 'distil-bert': + self.base_model.classifier = DistilBertForSequenceClassification.from_pretrained('distilbert-base-uncased', num_labels=num_labels).classifier + self.base_model.distilbert.transformer.layer[-2:].load_state_dict(self.models_dict[model_num]) + elif self.model_name == 'roberta': self.base_model.classifier = XLMRobertaForSequenceClassification.from_pretrained('xlm-roberta-base', num_labels=num_labels).classifier self.base_model.roberta.encoder.layer[-2:].load_state_dict(self.models_dict[model_num]) - elif self.model_name == 'bert-base-uncased': + elif self.model_name == 'bert': self.base_model.classifier = BertForSequenceClassification.from_pretrained('bert-base-uncased', num_labels=num_labels).classifier self.base_model.base_model.encoder.layer[-2:].load_state_dict(self.models_dict[model_num]) diff --git a/model_trainer/model_trainer.py b/model_trainer/model_trainer.py index 08c61669..938c0d31 100644 --- a/model_trainer/model_trainer.py +++ b/model_trainer/model_trainer.py @@ -9,7 +9,11 @@ from constants import URL_MODEL class ModelTrainer: - def __init__(self, cookie, newModelId, oldModelId = None) -> None: + def __init__(self) -> None: + + cookie = os.environ.get('COOKIE') + newModelId = os.environ.get('NEW_MODEL_ID') + oldModelId = os.environ.get('OLD_MODEL_ID') model_url = URL_MODEL @@ -24,8 +28,6 @@ def __init__(self, cookie, newModelId, oldModelId = None) -> None: else: print(f"Failed with status code: {response.status_code}") - - def train(self): s3_ferry = S3Ferry() dgId = self.model_details['response']['data'][0]['connectedDgId'] @@ -61,11 +63,11 @@ def train(self): best_label_encoders = label_encoders_list[max_value_index] model_name = models_to_train[max_value_index] for i, (model, classifier, label_encoder) in enumerate(zip(best_models, best_classifiers, best_label_encoders)): - if model_name == 'xlnet': + if model_name == 'distil-bert': torch.save(model, f"results/saved_models/last_two_layers_dfs_{i}.pth") elif model_name == 'roberta': torch.save(model, f"results/saved_models/last_two_layers_dfs_{i}.pth") - elif model_name == 'bert-base-uncased': + elif model_name == 'bert': torch.save(model, f"results/saved_models/last_two_layers_dfs_{i}.pth") torch.save(classifier, f"results/classifiers/classifier_{i}.pth") @@ -106,4 +108,4 @@ def train(self): "bestModelName":model_name } - response = requests.post(url, json=payload) \ No newline at end of file + response = requests.post(url, json=payload) diff --git a/model_trainer/trainingpipeline.py b/model_trainer/trainingpipeline.py index f1d72238..0503b05d 100644 --- a/model_trainer/trainingpipeline.py +++ b/model_trainer/trainingpipeline.py @@ -85,7 +85,7 @@ def train(self): - if self.model_name == 'xlnet': + if self.model_name == 'distil-bert': model = DistilBertForSequenceClassification.from_pretrained('distilbert-base-uncased',num_labels=len(label_encoder.classes_), force_download=True) tokenizer = DistilBertTokenizer.from_pretrained('distilbert-base-uncased', force_download=True) @@ -110,7 +110,7 @@ def train(self): for param in model.classifier.parameters(): param.requires_grad = True - elif self.model_name == 'albert': + elif self.model_name == 'bert': model = BertForSequenceClassification.from_pretrained('bert-base-uncased', num_labels=len(label_encoder.classes_), force_download=True) tokenizer = BertTokenizer.from_pretrained('bert-base-uncased', force_download=True) @@ -149,11 +149,11 @@ def train(self): ) trainer.train() - if self.model_name == 'xlnet': + if self.model_name == 'distil-bert': models.append(model.distilbert.transformer.layer[-2:].state_dict()) elif self.model_name == 'roberta': models.append(model.roberta.encoder.layer[-2:].state_dict()) - elif self.model_name == 'albert': + elif self.model_name == 'bert': models.append(model.base_model.encoder.layer[-2:].state_dict()) From 590bb8284d4af99161ce2932ad5e59e2abb556da Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Wed, 14 Aug 2024 16:09:31 +0530 Subject: [PATCH 466/582] outlook-refresh-subscription: add new Enpoint for cronmanager and bug fixes --- DSL/CronManager/DSL/outlook.yml | 7 +- .../script/outlook_subscription_refresh.sh | 70 ++++++++++ .../get-data-model-progress-sessions.sql | 28 ++-- DSL/Resql/get-platform-subscription-id.sql | 3 + .../integration/jira/cloud/accept.yml | 8 +- .../DSL/POST/internal/create.yml | 15 +-- DSL/Ruuter.public/DSL/POST/internal/exist.yml | 78 ++++++++++++ .../DSL/POST/internal/jira/label.yml | 79 ++++++++++++ .../DSL/POST/internal/outlook/label.yml | 114 +++++++++++++++++ .../DSL/POST/internal/validate.yml | 120 ++++++++++++++++++ constants.ini | 1 + 11 files changed, 497 insertions(+), 26 deletions(-) create mode 100644 DSL/CronManager/script/outlook_subscription_refresh.sh create mode 100644 DSL/Resql/get-platform-subscription-id.sql create mode 100644 DSL/Ruuter.public/DSL/POST/internal/exist.yml create mode 100644 DSL/Ruuter.public/DSL/POST/internal/jira/label.yml create mode 100644 DSL/Ruuter.public/DSL/POST/internal/outlook/label.yml create mode 100644 DSL/Ruuter.public/DSL/POST/internal/validate.yml diff --git a/DSL/CronManager/DSL/outlook.yml b/DSL/CronManager/DSL/outlook.yml index fca4258c..3cb1572b 100644 --- a/DSL/CronManager/DSL/outlook.yml +++ b/DSL/CronManager/DSL/outlook.yml @@ -1,4 +1,9 @@ token_refresh: trigger: "0 0 0 ? * 7#1" type: exec - command: "../app/scripts/outlook_refresh_token.sh" \ No newline at end of file + command: "../app/scripts/outlook_refresh_token.sh" + +subscription_refresh: + trigger: "0 0 0 * * ?" + type: exec + command: "../app/scripts/outlook_subscription_refresh.sh" \ No newline at end of file diff --git a/DSL/CronManager/script/outlook_subscription_refresh.sh b/DSL/CronManager/script/outlook_subscription_refresh.sh new file mode 100644 index 00000000..a64f62f7 --- /dev/null +++ b/DSL/CronManager/script/outlook_subscription_refresh.sh @@ -0,0 +1,70 @@ +#!/bin/bash + +# Set the working directory to the location of the script +cd "$(dirname "$0")" + +# Source the constants from the ini file +source ../config/config.ini + +script_name=$(basename $0) +pwd + +echo $(date -u +"%Y-%m-%d %H:%M:%S.%3NZ") - $script_name started + +# Fetch the encrypted refresh token +response=$(curl -X POST -H "Content-Type: application/json" -d '{"platform":"OUTLOOK"}' "$CLASSIFIER_RESQL/get-token") +encrypted_refresh_token=$(echo $response | grep -oP '"token":"\K[^"]+') + +echo "encrypted_refresh_token: $encrypted_refresh_token" + +if [ -z "$encrypted_refresh_token" ]; then + echo "No encrypted refresh token found" + exit 1 +fi + +# Decrypt the refresh token +responseVal=$(curl -X POST -H "Content-Type: application/json" -d '{"token":"'"$encrypted_refresh_token"'"}' "http://data-mapper:3000/hbs/classifier/return_decrypted_outlook_token") +decrypted_refresh_token=$(echo "$responseVal" | grep -oP '"content":"\K[^"]+' | sed 's/\\/\\\\/g') + +echo "decrypted refresh token: $decrypted_refresh_token" + +# Fetch the subscription ID from the resql endpoint without authorization +subscription_id_response=$(curl -X POST -H "Content-Type: application/json" -d '{"platform":"OUTLOOK"}' "$CLASSIFIER_RESQL/get-platform-subscription-id") +subscription_id=$(echo "$subscription_id_response" | grep -oP '"subscriptionId":"\K[^"]+') + +echo "subscription_id: $subscription_id" + +if [ -z "$subscription_id" ]; then + echo "Failed to retrieve subscription ID" + exit 1 +fi + +# Request a new access token using the decrypted refresh token +access_token_response=$(curl -X POST \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "client_id=$OUTLOOK_CLIENT_ID&scope=$OUTLOOK_SCOPE&refresh_token=$decrypted_refresh_token&grant_type=refresh_token&client_secret=$OUTLOOK_SECRET_KEY" \ + https://login.microsoftonline.com/common/oauth2/v2.0/token) + +new_access_token=$(echo $access_token_response | grep -oP '"access_token":"\K[^"]+') + +echo "new_access_token: $new_access_token" + +if [ -z "$new_access_token" ]; then + echo "Failed to get a new access token" + exit 1 +fi + +# Calculate expiration time by adding 3 days to the current time +expiration_time=$(date -u -d "+3 days" +"%Y-%m-%dT%H:%M:%SZ") +echo "Calculated expiration time: $expiration_time" + +# Refresh the subscription in Outlook +refresh_subscription_response=$(curl -X PATCH \ + -H "Authorization: Bearer $new_access_token" \ + -H "Content-Type: application/json" \ + -d '{"expirationDateTime": "'"$expiration_time"'"}' \ + "https://graph.microsoft.com/v1.0/subscriptions/$subscription_id") + +echo "refresh_subscription_response: $refresh_subscription_response" + +echo $(date -u +"%Y-%m-%d %H:%M:%S.%3NZ") - $script_name finished diff --git a/DSL/Resql/get-data-model-progress-sessions.sql b/DSL/Resql/get-data-model-progress-sessions.sql index 76e8ff9b..9cca7730 100644 --- a/DSL/Resql/get-data-model-progress-sessions.sql +++ b/DSL/Resql/get-data-model-progress-sessions.sql @@ -1,12 +1,18 @@ SELECT - id, - model_id, - model_name, - major_version, - minor_version, - latest, - process_complete, - progress_percentage, - training_progress_status as training_status, - training_message -FROM model_progress_sessions; + mps.id, + mps.model_id, + mps.model_name, + mps.major_version, + mps.minor_version, + mps.latest, + mps.process_complete, + mps.progress_percentage, + mps.training_progress_status as training_status, + mps.training_message, + mm.maturity_label, + mm.deployment_env +FROM model_progress_sessions mps +JOIN + models_metadata mm +ON + mps.model_id = mm.id; \ No newline at end of file diff --git a/DSL/Resql/get-platform-subscription-id.sql b/DSL/Resql/get-platform-subscription-id.sql new file mode 100644 index 00000000..c30272f7 --- /dev/null +++ b/DSL/Resql/get-platform-subscription-id.sql @@ -0,0 +1,3 @@ +SELECT subscription_id +FROM integration_status +WHERE platform=:platform::platform; \ No newline at end of file diff --git a/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml b/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml index 43140166..82cdd5db 100644 --- a/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml +++ b/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml @@ -32,19 +32,19 @@ get_webhook_data: verify_jira_signature: call: http.post args: - url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/verify_signature" + url: "[#CLASSIFIER_ANONYMIZER]/verify_signature" headers: - type: json + Content-Type: "application/json" + x-hub-signature: ${headers['x-hub-signature']} body: payload: ${payload} - headers: ${headers} secret: "[#JIRA_WEBHOOK_SECRET]" result: valid_data next: assign_verification assign_verification: assign: - is_valid: true #${valid_data.response.body.valid} + is_valid: ${valid_data.response.body.status} next: validate_url_signature validate_url_signature: diff --git a/DSL/Ruuter.public/DSL/POST/internal/create.yml b/DSL/Ruuter.public/DSL/POST/internal/create.yml index 2dee7257..6262723d 100644 --- a/DSL/Ruuter.public/DSL/POST/internal/create.yml +++ b/DSL/Ruuter.public/DSL/POST/internal/create.yml @@ -11,6 +11,9 @@ declaration: - field: inputId type: string description: "Body field 'inputId'" + - field: mailId + type: string + description: "Body field 'mailId'" - field: inferenceText type: string description: "Body field 'inferenceText'" @@ -26,10 +29,6 @@ declaration: - field: primaryFolderId type: string description: "Body field 'primaryFolderId'" - headers: - - field: cookie - type: string - description: "Cookie field" extract_request_data: assign: @@ -97,9 +96,7 @@ check_input_type: outlook_flow: call: http.post args: - url: "[#CLASSIFIER_RUUTER_PRIVATE]/classifier/integration/outlook/label" - headers: - cookie: ${incoming.headers.cookie} + url: "[#CLASSIFIER_RUUTER_PUBLIC_INTERNAL]/internal/outlook/label" body: mailId: ${mail_id} folderId: ${primary_folder_id} @@ -109,9 +106,7 @@ outlook_flow: jira_flow: call: http.post args: - url: "[#CLASSIFIER_RUUTER_PRIVATE]/classifier/integration/jira/cloud/label" - headers: - cookie: ${incoming.headers.cookie} + url: "[#CLASSIFIER_RUUTER_PUBLIC_INTERNAL]/internal/jira/label" body: issueKey: ${input_id} labels: ${predicted_labels_org} diff --git a/DSL/Ruuter.public/DSL/POST/internal/exist.yml b/DSL/Ruuter.public/DSL/POST/internal/exist.yml new file mode 100644 index 00000000..d8382f34 --- /dev/null +++ b/DSL/Ruuter.public/DSL/POST/internal/exist.yml @@ -0,0 +1,78 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'EXIST'" + method: get + accepts: json + returns: json + namespace: classifier + allowlist: + params: + - field: inputId + type: string + description: "Parameter 'inferenceId'" + +extract_data: + assign: + input_id: ${incoming.params.inputId} + exist: false + next: get_input_metadata_by_id + +get_input_metadata_by_id: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-basic-input-metadata-by-input-id" + body: + input_id: ${input_id} + result: res_input_id + next: check_input_metadata_status + +check_input_metadata_status: + switch: + - condition: ${200 <= res_input_id.response.statusCodeValue && res_input_id.response.statusCodeValue < 300} + next: check_input_metadata_exist + next: assign_fail_response + +check_input_metadata_exist: + switch: + - condition: ${res_input_id.response.body.length>0} + next: assign_exist_data + next: assign_fail_response + +assign_exist_data: + assign: + exist : true + value: [{ + inferenceId: '${res_input_id.response.body[0].id}', + inputId: '${res_input_id.response.body[0].inputId}', + platform: '${res_input_id.response.body[0].platform}' + }] + next: assign_success_response + +assign_success_response: + assign: + format_res: { + operationSuccessful: true, + exist: '${exist}', + data: '${value}' + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + operationSuccessful: false, + exist: '${exist}', + data: '${[]}' + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.public/DSL/POST/internal/jira/label.yml b/DSL/Ruuter.public/DSL/POST/internal/jira/label.yml new file mode 100644 index 00000000..fb91746b --- /dev/null +++ b/DSL/Ruuter.public/DSL/POST/internal/jira/label.yml @@ -0,0 +1,79 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'LABEL'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: issueKey + type: string + description: "Body field 'issueId'" + - field: labels + type: array + description: "Body field 'labels'" + +extract_request_data: + assign: + issue_key: ${incoming.body.issueKey} + label_list: ${incoming.body.labels} + next: check_for_request_data + +check_for_request_data: + switch: + - condition: ${issue_key == null || label_list == null} + next: return_incorrect_request + next: get_auth_header + +get_auth_header: + call: http.post + args: + url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/get_auth_header" + headers: + type: json + body: + username: "[#JIRA_USERNAME]" + token: "[#JIRA_API_TOKEN]" + result: auth_header + next: set_data + +set_data: + assign: + all_labels: { + labels: '${label_list}' + } + next: update_jira_issue + +update_jira_issue: + call: http.put + args: + url: "[#JIRA_CLOUD_DOMAIN]/rest/api/3/issue/${issue_key}" + headers: + Authorization: ${auth_header.response.body.val} + body: + fields: ${all_labels} + result: res_jira + next: check_status + +check_status: + switch: + - condition: ${200 <= res_jira.response.statusCodeValue && res_jira.response.statusCodeValue < 300} + next: return_ok + next: return_bad_request + +return_ok: + status: 200 + return: "Jira Ticket Updated" + next: end + +return_bad_request: + status: 400 + return: "Bad Request" + next: end + +return_incorrect_request: + status: 400 + return: 'missing labels' + next: end \ No newline at end of file diff --git a/DSL/Ruuter.public/DSL/POST/internal/outlook/label.yml b/DSL/Ruuter.public/DSL/POST/internal/outlook/label.yml new file mode 100644 index 00000000..1b10c9b7 --- /dev/null +++ b/DSL/Ruuter.public/DSL/POST/internal/outlook/label.yml @@ -0,0 +1,114 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'Label'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: mailId + type: string + description: "Body field 'mailId'" + - field: folderId + type: string + description: "Body field 'folderId'" + +extract_request_data: + assign: + mail_id: ${incoming.body.mailId} + folder_id: ${incoming.body.folderId} + next: check_for_request_data + +check_for_request_data: + switch: + - condition: ${mail_id !== null || folder_id !== null} + next: get_token_info + next: return_incorrect_request + +get_token_info: + call: http.get + args: + url: "[#CLASSIFIER_RUUTER_PRIVATE]/classifier/integration/outlook/token" + headers: + cookie: ${incoming.headers.cookie} + result: res + next: assign_access_token + +assign_access_token: + assign: + access_token: ${res.response.body.response.access_token} + next: get_email_exist + +get_email_exist: + call: http.get + args: + url: "https://graph.microsoft.com/v1.0/me/messages/${mail_id}" + headers: + Authorization: ${'Bearer ' + access_token} + result: res + next: check_email_status + +check_email_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: get_folder_exist + next: return_email_not_found + +get_folder_exist: + call: http.get + args: + url: "https://graph.microsoft.com/v1.0/me/mailFolders/${folder_id}" + headers: + Authorization: ${'Bearer ' + access_token} + result: res + next: check_folder_status + +check_folder_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: update_mail_folder + next: return_folder_not_found + +update_mail_folder: + call: http.post + args: + url: "https://graph.microsoft.com/v1.0/me/messages/${mail_id}/move" + headers: + Authorization: ${'Bearer ' + access_token} + body: + destinationId: ${folder_id} + result: res_mail + next: check_status + +check_status: + switch: + - condition: ${200 <= res_mail.response.statusCodeValue && res_mail.response.statusCodeValue < 300} + next: return_ok + next: return_bad_request + +return_ok: + status: 200 + return: "Outlook Email Destination Updated" + next: end + +return_folder_not_found: + status: 404 + return: 'Folder Not Found' + next: end + +return_email_not_found: + status: 404 + return: 'Email Not Found' + next: end + +return_bad_request: + status: 400 + return: "Bad Request" + next: end + +return_incorrect_request: + status: 400 + return: 'missing labels' + next: end \ No newline at end of file diff --git a/DSL/Ruuter.public/DSL/POST/internal/validate.yml b/DSL/Ruuter.public/DSL/POST/internal/validate.yml new file mode 100644 index 00000000..01260256 --- /dev/null +++ b/DSL/Ruuter.public/DSL/POST/internal/validate.yml @@ -0,0 +1,120 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'VALIDATE'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: modelId + type: string + description: "Body field 'modelId'" + +extract_request: + assign: + model_id: ${incoming.body.modelId} + next: get_token_info + +get_token_info: + call: http.get + args: + url: "[#CLASSIFIER_RUUTER_PRIVATE]/classifier/integration/outlook/token" + headers: + cookie: ${incoming.headers.cookie} + result: res + next: check_token_status + +check_token_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: assign_access_token + next: assign_fail_response + +assign_access_token: + assign: + access_token: ${res.response.body.response.access_token} + next: get_dataset_group_id_by_model_id + +get_dataset_group_id_by_model_id: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-data-model-dataset-group-by-id" + body: + id: ${model_id} + result: res_model + next: check_data_model_status + +check_data_model_status: + switch: + - condition: ${200 <= res_model.response.statusCodeValue && res_model.response.statusCodeValue < 300} + next: check_data_model_exist + next: assign_fail_response + +check_data_model_exist: + switch: + - condition: ${res_model.response.body.length>0} + next: assign_dataset_group_id + next: assign_fail_response + +assign_dataset_group_id: + assign: + dg_id: ${res_model.response.body[0].connectedDgId} + next: get_dataset_group_class_hierarchy + +get_dataset_group_class_hierarchy: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-dataset-group-class-hierarchy" + body: + id: ${dg_id} + result: res_dataset + next: check_dataset_group_status + +check_dataset_group_status: + switch: + - condition: ${200 <= res_dataset.response.statusCodeValue && res_dataset.response.statusCodeValue < 300} + next: check_dataset_group_exist + next: assign_fail_response + +check_dataset_group_exist: + switch: + - condition: ${res_dataset.response.body.length>0} + next: assign_dataset_class_hierarchy + next: assign_fail_response + +assign_dataset_class_hierarchy: + assign: + class_hierarchy: ${JSON.parse(res_dataset.response.body[0].classHierarchy.value)} + next: assign_success_response + +assign_success_response: + assign: + format_res: { + modelId: '${model_id}', + outlook_access_token: '${access_token}', + class_hierarchy: '${class_hierarchy}', + operationSuccessful: true, + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + modelId: '${model_id}', + outlook_access_token: '', + class_hierarchy: '', + operationSuccessful: false, + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end \ No newline at end of file diff --git a/constants.ini b/constants.ini index 47537c95..38260914 100644 --- a/constants.ini +++ b/constants.ini @@ -2,6 +2,7 @@ CLASSIFIER_RUUTER_PUBLIC=http://ruuter-public:8086 CLASSIFIER_RUUTER_PRIVATE=http://ruuter-private:8088 +CLASSIFIER_RUUTER_PUBLIC_INTERNAL=http://localhost:8086 CLASSIFIER_DMAPPER=http://data-mapper:3000 CLASSIFIER_RESQL=http://resql:8082 CLASSIFIER_TIM=http://tim:8085 From 1f578eda1c890590c22cb786d2d210baf1b07f53 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Wed, 14 Aug 2024 16:31:44 +0530 Subject: [PATCH 467/582] validation sessions fixes --- .../molecules/ValidationSessionCard/index.tsx | 13 ++++++++----- GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx | 2 +- GUI/src/pages/ValidationSessions/index.tsx | 11 ++++++----- GUI/src/styles/generic/_base.scss | 4 ++++ GUI/translations/en/common.json | 5 +++++ 5 files changed, 24 insertions(+), 11 deletions(-) diff --git a/GUI/src/components/molecules/ValidationSessionCard/index.tsx b/GUI/src/components/molecules/ValidationSessionCard/index.tsx index 4eb3dab0..58d026ef 100644 --- a/GUI/src/components/molecules/ValidationSessionCard/index.tsx +++ b/GUI/src/components/molecules/ValidationSessionCard/index.tsx @@ -8,11 +8,11 @@ type ValidationSessionCardProps = { version:string; isLatest:boolean; status?:string; - errorMessage?: string; + validationMessage?: string; progress: number; }; -const ValidationSessionCard: React.FC = ({dgName,version,isLatest,status,errorMessage,progress}) => { +const ValidationSessionCard: React.FC = ({dgName,version,isLatest,status,validationMessage,progress}) => { const { t } = useTranslation(); return ( @@ -30,9 +30,9 @@ const ValidationSessionCard: React.FC = ({dgName,ver } >
          - {errorMessage? ( -
          - {errorMessage} + {(status==="Fail" || status==="Success") && progress===100 ? ( +
          + {validationMessage}
          ) : (
          @@ -42,6 +42,9 @@ const ValidationSessionCard: React.FC = ({dgName,ver max={100} label={`${progress}%`} /> +
          + {validationMessage} +
          )}
          diff --git a/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx b/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx index 7169f70e..6fd6da28 100644 --- a/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx +++ b/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx @@ -305,7 +305,7 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { > {t('global.cancel')} -
          diff --git a/GUI/src/pages/ValidationSessions/index.tsx b/GUI/src/pages/ValidationSessions/index.tsx index 52d569dc..a1409c11 100644 --- a/GUI/src/pages/ValidationSessions/index.tsx +++ b/GUI/src/pages/ValidationSessions/index.tsx @@ -10,7 +10,7 @@ const ValidationSessions: FC = () => { const { t } = useTranslation(); const [progresses, setProgresses] = useState([]); - const { data: progressData } = useQuery( + const { data: progressData,refetch } = useQuery( ['datasetgroups/progress'], () => getDatasetGroupsProgress(), { @@ -32,17 +32,18 @@ const ValidationSessions: FC = () => { }; const eventSources = progressData.map((progress) => { + if(progress.validationStatus !=="Success" && progress.progressPercentage!==100) return sse(`/${progress.id}`, 'dataset', (data: SSEEventData) => { console.log(`New data for notification ${progress.id}:`, data); handleUpdate(data.sessionId, data); }); }); - + // refetch(); return () => { - eventSources.forEach((eventSource) => eventSource.close()); + eventSources.forEach((eventSource) => eventSource?.close()); console.log('SSE connections closed'); }; - }, [progressData]); + }, [progressData,refetch]); return (
          @@ -57,7 +58,7 @@ const ValidationSessions: FC = () => { version={`V${session?.majorVersion}.${session?.minorVersion}.${session?.patchVersion}`} isLatest={session.latest} status={session.validationStatus} - errorMessage={session.validationMessage} + validationMessage={session.validationMessage} progress={session.progressPercentage} /> ))} diff --git a/GUI/src/styles/generic/_base.scss b/GUI/src/styles/generic/_base.scss index 93bdd0c6..b942851c 100644 --- a/GUI/src/styles/generic/_base.scss +++ b/GUI/src/styles/generic/_base.scss @@ -87,6 +87,10 @@ body { text-align: center; } +.error { + color: rgb(222, 10, 10); + } + a, input, select, diff --git a/GUI/translations/en/common.json b/GUI/translations/en/common.json index cf45145d..5df4962c 100644 --- a/GUI/translations/en/common.json +++ b/GUI/translations/en/common.json @@ -377,5 +377,10 @@ "errorTitle": "Error Creating Data Model", "errorDesc": " There was an issue creating or training the data model. Please try again. If the problem persists, contact support for assistance." } + }, + "trainingSessions": { + "title": "Training Sessions", + "inprogress": "Validation in-Progress", + "fail": "Validation failed because {{class}} class found in the {{column}} column does not exist in hierarchy" } } From 6af3c4b38d7e855e9d7e54f2ec910cebd222ec4c Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Wed, 14 Aug 2024 23:03:59 +0530 Subject: [PATCH 468/582] outlook-refresh-subscription: bug fixes in and validation status flow update --- DSL/Resql/get-validated-all-dataset-groups.sql | 2 +- .../classifier/datasetgroup/update/validation/status.yml | 5 +++++ .../DSL/POST/classifier/testmodel/test-data.yml | 3 +++ .../DSL/POST/classifier/integration/jira/cloud/accept.yml | 2 +- .../DSL/POST/classifier/integration/outlook/accept.yml | 4 +--- DSL/Ruuter.public/DSL/POST/internal/jira/label.yml | 2 +- docker-compose.yml | 3 ++- 7 files changed, 14 insertions(+), 7 deletions(-) diff --git a/DSL/Resql/get-validated-all-dataset-groups.sql b/DSL/Resql/get-validated-all-dataset-groups.sql index 2d0c0ccc..e3aa847d 100644 --- a/DSL/Resql/get-validated-all-dataset-groups.sql +++ b/DSL/Resql/get-validated-all-dataset-groups.sql @@ -1,3 +1,3 @@ SELECT id as dg_id, group_name, major_version, minor_version, patch_version FROM dataset_group_metadata -WHERE is_enabled = true AND validation_status = 'success'; +WHERE is_enabled AND validation_status = 'success'; diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/validation/status.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/validation/status.yml index c785a519..ecf7af86 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/validation/status.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/validation/status.yml @@ -29,6 +29,9 @@ declaration: - field: validationErrors type: array description: "Body field 'validationErrors'" + - field: sessionId + type: number + description: "Body field 'sessionId'" headers: - field: cookie type: string @@ -43,6 +46,7 @@ extract_request_data: save_file_path: ${incoming.body.savedFilePath} validation_status: ${incoming.body.validationStatus} validation_errors: ${incoming.body.validationErrors} + session_id: ${incoming.body.sessionId} next: check_request_type check_request_type: @@ -149,6 +153,7 @@ execute_cron_manager: updateType: ${update_type} savedFilePath: ${save_file_path} patchPayload: ${patch_payload} + sessionId: ${session_id} result: res next: assign_success_response diff --git a/DSL/Ruuter.private/DSL/POST/classifier/testmodel/test-data.yml b/DSL/Ruuter.private/DSL/POST/classifier/testmodel/test-data.yml index 2d736dc5..99d88a57 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/testmodel/test-data.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/testmodel/test-data.yml @@ -68,6 +68,9 @@ send_data_to_predict: text: ${text} response: statusCodeValue: 200 + predictedClasses: [ "Police","Special Agency","External","Reports","Annual Report" ] + averageConfidence: 89.8 + predictedProbabilities: [ 98,82,91,90,88 ] result: res_predict next: check_data_predict_status diff --git a/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml b/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml index 82cdd5db..e030e451 100644 --- a/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml +++ b/DSL/Ruuter.public/DSL/POST/classifier/integration/jira/cloud/accept.yml @@ -117,7 +117,7 @@ get_jira_issue_info: next: send_issue_data send_issue_data: - call: reflect.mock + call: http.post args: url: "[#CLASSIFIER_ANONYMIZER]/anonymize" headers: diff --git a/DSL/Ruuter.public/DSL/POST/classifier/integration/outlook/accept.yml b/DSL/Ruuter.public/DSL/POST/classifier/integration/outlook/accept.yml index 7fc4d5c9..253c9bdd 100644 --- a/DSL/Ruuter.public/DSL/POST/classifier/integration/outlook/accept.yml +++ b/DSL/Ruuter.public/DSL/POST/classifier/integration/outlook/accept.yml @@ -161,10 +161,8 @@ rearrange_mail_payload: result: outlook_body next: send_outlook_data -#check the mail id is an existing id and check categories from mail and db are same -#if different or new send to AI model send_outlook_data: - call: reflect.mock + call: http.post args: url: "[#CLASSIFIER_ANONYMIZER]/anonymize" headers: diff --git a/DSL/Ruuter.public/DSL/POST/internal/jira/label.yml b/DSL/Ruuter.public/DSL/POST/internal/jira/label.yml index fb91746b..5923114c 100644 --- a/DSL/Ruuter.public/DSL/POST/internal/jira/label.yml +++ b/DSL/Ruuter.public/DSL/POST/internal/jira/label.yml @@ -10,7 +10,7 @@ declaration: body: - field: issueKey type: string - description: "Body field 'issueId'" + description: "Body field 'issueKey'" - field: labels type: array description: "Body field 'labels'" diff --git a/docker-compose.yml b/docker-compose.yml index 078f3d79..5ea1b694 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,9 +6,10 @@ services: image: ruuter environment: - application.cors.allowedOrigins=http://localhost:8086,http://localhost:3001,http://localhost:3003,http://localhost:3004,http://localhost:8080,http://localhost:8000 - - application.httpCodesAllowList=200,201,202,400,401,403,500 + - application.httpCodesAllowList=200,201,202,204,400,401,403,404,500 - application.internalRequests.allowedIPs=127.0.0.1,172.25.0.7 - application.logging.displayRequestContent=true + - application.incomingRequests.allowedMethodTypes=POST,GET,PUT - application.logging.displayResponseContent=true - server.port=8086 volumes: From b52fcc253a09a14962a05296fb838bf5410de368 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Wed, 14 Aug 2024 23:27:06 +0530 Subject: [PATCH 469/582] refactor --- .../pages/DataModels/ConfigureDataModel.tsx | 3 +- GUI/src/pages/DataModels/CreateDataModel.tsx | 9 ++--- GUI/src/pages/DataModels/index.tsx | 18 ++++----- .../pages/DatasetGroups/ViewDatasetGroup.tsx | 2 +- GUI/src/services/data-models.ts | 28 +++++++------- GUI/src/services/datasets.ts | 2 +- GUI/src/types/dataModels.ts | 20 +++++----- GUI/src/utils/endpoints.ts | 15 +++++++- GUI/src/utils/queryKeys.ts | 38 ++++++++++++++++++- 9 files changed, 90 insertions(+), 45 deletions(-) diff --git a/GUI/src/pages/DataModels/ConfigureDataModel.tsx b/GUI/src/pages/DataModels/ConfigureDataModel.tsx index a93c16bf..42b95efb 100644 --- a/GUI/src/pages/DataModels/ConfigureDataModel.tsx +++ b/GUI/src/pages/DataModels/ConfigureDataModel.tsx @@ -16,6 +16,7 @@ import { Platform, UpdateType } from 'enums/dataModelsEnums'; import { ButtonAppearanceTypes } from 'enums/commonEnums'; import CircularSpinner from 'components/molecules/CircularSpinner/CircularSpinner'; import { DataModel, UpdatedDataModelPayload } from 'types/dataModels'; +import { dataModelsQueryKeys } from 'utils/queryKeys'; type ConfigureDataModelType = { id: number; @@ -48,7 +49,7 @@ const ConfigureDataModel: FC = ({ }); const { isLoading } = useQuery( - ['datamodels/metadata', id], + dataModelsQueryKeys.GET_META_DATA(id), () => getMetadata(id), { enabled, diff --git a/GUI/src/pages/DataModels/CreateDataModel.tsx b/GUI/src/pages/DataModels/CreateDataModel.tsx index 96464a19..f34994d7 100644 --- a/GUI/src/pages/DataModels/CreateDataModel.tsx +++ b/GUI/src/pages/DataModels/CreateDataModel.tsx @@ -10,7 +10,7 @@ import { extractedArray, validateDataModel } from 'utils/dataModelsUtils'; import DataModelForm from 'components/molecules/DataModelForm'; import { ButtonAppearanceTypes } from 'enums/commonEnums'; import { createDataModel, getDataModelsOverview } from 'services/data-models'; -import { integrationQueryKeys } from 'utils/queryKeys'; +import { dataModelsQueryKeys, integrationQueryKeys } from 'utils/queryKeys'; import { getIntegrationStatus } from 'services/integration'; import { CreateDataModelPayload, DataModel } from 'types/dataModels'; @@ -31,8 +31,7 @@ const CreateDataModel: FC = () => { }); useQuery( - [ - 'datamodels/overview', + dataModelsQueryKeys.DATA_MODELS_OVERVIEW( 0, 'all', -1, @@ -42,8 +41,8 @@ const CreateDataModel: FC = () => { 'all', 'all', 'asc', - true, - ], + true + ), () => getDataModelsOverview( 1, diff --git a/GUI/src/pages/DataModels/index.tsx b/GUI/src/pages/DataModels/index.tsx index 1c890c3b..0862aabe 100644 --- a/GUI/src/pages/DataModels/index.tsx +++ b/GUI/src/pages/DataModels/index.tsx @@ -12,6 +12,7 @@ import { customFormattedArray, extractedArray } from 'utils/dataModelsUtils'; import CircularSpinner from 'components/molecules/CircularSpinner/CircularSpinner'; import { ButtonAppearanceTypes } from 'enums/commonEnums'; import { DataModelResponse, FilterData, Filters } from 'types/dataModels'; +import { dataModelsQueryKeys } from 'utils/queryKeys'; const DataModels: FC = () => { const { t } = useTranslation(); @@ -41,20 +42,18 @@ const DataModels: FC = () => { }); const { data: dataModelsData, isLoading: isModelDataLoading } = useQuery( - [ - 'datamodels/overview', + dataModelsQueryKeys.DATA_MODELS_OVERVIEW( pageIndex, filters.modelName, parseVersionString(filters.version)?.major, parseVersionString(filters.version)?.minor, - parseVersionString(filters.version)?.patch, filters.platform, filters.datasetGroup, filters.trainingStatus, filters.maturity, filters.sort, - false, - ], + false + ), () => getDataModelsOverview( pageIndex, @@ -76,8 +75,7 @@ const DataModels: FC = () => { const { data: prodDataModelsData, isLoading: isProdModelDataLoading } = useQuery( - [ - 'datamodels/overview', + dataModelsQueryKeys.DATA_MODELS_OVERVIEW( 0, 'all', -1, @@ -87,8 +85,8 @@ const DataModels: FC = () => { 'all', 'all', 'asc', - true, - ], + true + ), () => getDataModelsOverview( 1, @@ -113,7 +111,7 @@ const DataModels: FC = () => { ); const { data: filterData } = useQuery( - ['datamodels/filters'], + dataModelsQueryKeys.DATA_MODEL_FILTERS(), () => getFilterData() ); diff --git a/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx b/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx index 6fd6da28..65feb45a 100644 --- a/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx +++ b/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx @@ -105,7 +105,7 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { ); const { data: metadata, isLoading: isMetadataLoading } = useQuery( - datasetQueryKeys.GET_MATA_DATA(dgId), + datasetQueryKeys.GET_META_DATA(dgId), () => getMetadata(dgId), { enabled: fetchEnabled } ); diff --git a/GUI/src/services/data-models.ts b/GUI/src/services/data-models.ts index 46464152..ed59d69c 100644 --- a/GUI/src/services/data-models.ts +++ b/GUI/src/services/data-models.ts @@ -1,8 +1,6 @@ +import { CreateDataModelPayload, UpdatedDataModelPayload } from 'types/dataModels'; import apiDev from './api-dev'; -import apiExternal from './api-external'; -import apiMock from './api-mock'; -import { PaginationState } from '@tanstack/react-table'; -import { DatasetGroup, Operation } from 'types/datasetGroups'; +import { dataModelsEndpoints } from 'utils/endpoints'; export async function getDataModelsOverview( pageNum: number, @@ -16,7 +14,7 @@ export async function getDataModelsOverview( sort: string, isProductionModel: boolean ) { - const { data } = await apiDev.get('classifier/datamodel/overview', { + const { data } = await apiDev.get(dataModelsEndpoints.GET_OVERVIEW(), { params: { page: pageNum, modelName, @@ -35,17 +33,17 @@ export async function getDataModelsOverview( } export async function getFilterData() { - const { data } = await apiDev.get('classifier/datamodel/overview/filters'); + const { data } = await apiDev.get(dataModelsEndpoints.GET_DATAMODELS_FILTERS()); return data?.response; } export async function getCreateOptions() { - const { data } = await apiDev.get('classifier/datamodel/create/options'); + const { data } = await apiDev.get(dataModelsEndpoints.GET_CREATE_OPTIONS()); return data?.response; } export async function getMetadata(modelId: string | number | null) { - const { data } = await apiDev.get('classifier/datamodel/metadata', { + const { data } = await apiDev.get(dataModelsEndpoints.GET_METADATA(), { params: { modelId, }, @@ -53,35 +51,35 @@ export async function getMetadata(modelId: string | number | null) { return data?.response?.data[0]; } -export async function createDataModel(dataModel) { - const { data } = await apiDev.post('classifier/datamodel/create', { +export async function createDataModel(dataModel:CreateDataModelPayload) { + const { data } = await apiDev.post(dataModelsEndpoints.CREATE_DATA_MODEL(), { ...dataModel, }); return data; } -export async function updateDataModel(dataModel) { - const { data } = await apiDev.post('classifier/datamodel/update', { +export async function updateDataModel(dataModel:UpdatedDataModelPayload) { + const { data } = await apiDev.post(dataModelsEndpoints.UPDATE_DATA_MODEL(), { ...dataModel, }); return data; } export async function deleteDataModel(modelId : number) { - const { data } = await apiDev.post('classifier/datamodel/delete', { + const { data } = await apiDev.post(dataModelsEndpoints.DELETE_DATA_MODEL(), { modelId }); return data; } export async function retrainDataModel(modelId : number) { - const { data } = await apiDev.post('classifier/datamodel/re-train', { + const { data } = await apiDev.post(dataModelsEndpoints.RETRAIN_DATA_MODEL(), { modelId }); return data; } export async function getDataModelsProgress() { - const { data } = await apiDev.get('classifier/datamodel/progress'); + const { data } = await apiDev.get(dataModelsEndpoints.GET_DATA_MODEL_PROGRESS()); return data?.response?.data; } \ No newline at end of file diff --git a/GUI/src/services/datasets.ts b/GUI/src/services/datasets.ts index 246004d6..3c546c4d 100644 --- a/GUI/src/services/datasets.ts +++ b/GUI/src/services/datasets.ts @@ -45,7 +45,7 @@ export async function enableDataset(enableData: Operation) { export async function getFilterData() { const { data } = await apiDev.get( - datasetsEndpoints.GET_DATASET_OVERVIEW_BY_FILTERS() + datasetsEndpoints.GET_DATASET_FILTERS() ); return data; } diff --git a/GUI/src/types/dataModels.ts b/GUI/src/types/dataModels.ts index 74a71eca..70458188 100644 --- a/GUI/src/types/dataModels.ts +++ b/GUI/src/types/dataModels.ts @@ -27,19 +27,19 @@ export type SSEEventData = { export type UpdatedDataModelPayload = { modelId: number; - connectedDgId: number | string; - deploymentEnv: string; - baseModels: string[] |string; - maturityLabel: string; - updateType: string; + connectedDgId: string | null | undefined; + deploymentEnv: string | null | undefined; + baseModels: string | null | undefined; + maturityLabel: string | null | undefined; + updateType: string | undefined; }; export type CreateDataModelPayload={ - modelName: string; - dgId: string | number; - baseModels: string[]; - deploymentPlatform: string; - maturityLabel: string; + modelName: string | undefined; + dgId: string | number | undefined; + baseModels: string[] | undefined; + deploymentPlatform: string | undefined; + maturityLabel: string | undefined; } export type FilterData = { diff --git a/GUI/src/utils/endpoints.ts b/GUI/src/utils/endpoints.ts index 78082f0c..c08cd4bf 100644 --- a/GUI/src/utils/endpoints.ts +++ b/GUI/src/utils/endpoints.ts @@ -15,7 +15,7 @@ export const integrationsEndPoints = { export const datasetsEndpoints = { GET_OVERVIEW: (): string => '/classifier/datasetgroup/overview', - GET_DATASET_OVERVIEW_BY_FILTERS: (): string => + GET_DATASET_FILTERS: (): string => '/classifier/datasetgroup/overview/filters', ENABLE_DATASET: (): string => `/classifier/datasetgroup/update/status`, GET_DATASETS: (): string => `/classifier/datasetgroup/group/data`, @@ -45,3 +45,16 @@ export const correctedTextEndpoints = { ) => `/classifier/inference/corrected-metadata?pageNum=${pageNumber}&pageSize=${pageSize}&platform=${platform}&sortType=${sortType}`, }; + +export const dataModelsEndpoints = { + GET_OVERVIEW: (): string => '/classifier/datamodel/overview', + GET_DATAMODELS_FILTERS: (): string => + '/classifier/datamodel/overview/filters', + GET_METADATA: (): string => `/classifier/datamodel/metadata`, + GET_CREATE_OPTIONS: (): string => `classifier/datamodel/create/options`, + CREATE_DATA_MODEL: (): string => `classifier/datamodel/create`, + UPDATE_DATA_MODEL: (): string => `classifier/datamodel/update`, + DELETE_DATA_MODEL: (): string => `classifier/datamodel/delete`, + RETRAIN_DATA_MODEL: (): string => `classifier/datamodel/retrain`, + GET_DATA_MODEL_PROGRESS: (): string => `classifier/datamodel/progress`, +}; diff --git a/GUI/src/utils/queryKeys.ts b/GUI/src/utils/queryKeys.ts index b251d31b..43760ca8 100644 --- a/GUI/src/utils/queryKeys.ts +++ b/GUI/src/utils/queryKeys.ts @@ -38,7 +38,7 @@ export const datasetQueryKeys = { sort, ].filter((val) => val !== undefined); }, - GET_MATA_DATA: function (dgId?: number) { + GET_META_DATA: function (dgId?: number) { return ['datasets/groups/metadata', `${dgId}`].filter( (val) => val !== undefined ); @@ -56,4 +56,40 @@ export const stopWordsQueryKeys = { export const authQueryKeys = { USER_DETAILS: () => ['auth/jwt/userinfo', 'prod'], +}; + +export const dataModelsQueryKeys = { + DATA_MODEL_FILTERS: (): string[] => ['datamodels/filters'], + DATA_MODELS_OVERVIEW: function ( + pageIndex?: number, + modelName?: string, + versionMajor?: number, + versionMinor?: number, + platform?: string, + datasetGroup?: number, + trainingStatus?:string, + maturity?:string, + sort?:string, + isProduction?:boolean + + ) { + return [ + 'datamodels/overview', + pageIndex, + modelName, + versionMajor, + versionMinor, + platform, + datasetGroup, + trainingStatus, + maturity, + sort, + isProduction + ].filter((val) => val !== undefined); + }, + GET_META_DATA: function (modelId?: number) { + return ['datamodels/metadata', `${modelId}`].filter( + (val) => val !== undefined + ); + } }; \ No newline at end of file From cb1ee82a3f1bf170754788f99665f4438ddf4e63 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Thu, 15 Aug 2024 00:58:23 +0530 Subject: [PATCH 470/582] Session Id will be passed with the payload to dataset processor --- DSL/CronManager/DSL/dataset_processor.yml | 2 +- DSL/CronManager/script/data_processor_exec.sh | 5 +- dataset-processor/dataset_processor.py | 22 ++------ dataset-processor/dataset_processor_api.py | 3 +- dataset-processor/dataset_validator.py | 55 ++++++++++--------- 5 files changed, 40 insertions(+), 47 deletions(-) diff --git a/DSL/CronManager/DSL/dataset_processor.yml b/DSL/CronManager/DSL/dataset_processor.yml index b57c7886..91dc6d5a 100644 --- a/DSL/CronManager/DSL/dataset_processor.yml +++ b/DSL/CronManager/DSL/dataset_processor.yml @@ -2,4 +2,4 @@ dataset_processor: trigger: off type: exec command: "../app/scripts/data_processor_exec.sh" - allowedEnvs: ["cookie","dgId", "newDgId","updateType","savedFilePath","patchPayload"] \ No newline at end of file + allowedEnvs: ["cookie","dgId", "newDgId","updateType","savedFilePath","patchPayload", "sessionId"] \ No newline at end of file diff --git a/DSL/CronManager/script/data_processor_exec.sh b/DSL/CronManager/script/data_processor_exec.sh index c7147496..ca52a66d 100644 --- a/DSL/CronManager/script/data_processor_exec.sh +++ b/DSL/CronManager/script/data_processor_exec.sh @@ -1,9 +1,9 @@ #!/bin/bash echo "Started Shell Script to process" # Ensure required environment variables are set -if [ -z "$dgId" ] || [ -z "$newDgId" ] || [ -z "$cookie" ] || [ -z "$updateType" ] || [ -z "$savedFilePath" ] || [ -z "$patchPayload" ]; then +if [ -z "$dgId" ] || [ -z "$newDgId" ] || [ -z "$cookie" ] || [ -z "$updateType" ] || [ -z "$savedFilePath" ] || [ -z "$patchPayload" ] || [ -z "$sessionId" ]; then echo "One or more environment variables are missing." - echo "Please set dgId, newDgId, cookie, updateType, savedFilePath, and patchPayload." + echo "Please set dgId, newDgId, cookie, updateType, savedFilePath, patchPayload, and sessionId." exit 1 fi @@ -16,6 +16,7 @@ payload=$(cat < 0: - session_id = self.get_session_id(newDgId, cookie) - if not session_id: - return self.generate_response(False, MSG_FAIL) - updateType = "minor_append_update" - elif updateType == "patch": - session_id = self.get_session_id(dgId, cookie) - if not session_id: - return self.generate_response(False, MSG_FAIL) - else: - updateType = "minor_initial_update" - session_id = self.get_session_id(newDgId, cookie) - if not session_id: + + if sessionId >= 0: + session_id = sessionId + if not session_id: return self.generate_response(False, MSG_FAIL) - + if updateType == "minor_initial_update": result = self.handle_minor_initial_update(dgId, newDgId, cookie, savedFilePath, session_id) elif updateType == "minor_append_update": diff --git a/dataset-processor/dataset_processor_api.py b/dataset-processor/dataset_processor_api.py index b8e4b994..7ec6b8a1 100644 --- a/dataset-processor/dataset_processor_api.py +++ b/dataset-processor/dataset_processor_api.py @@ -49,7 +49,7 @@ async def process_handler_endpoint(request: Request): await authenticate_user(request) authCookie = payload["cookie"] - result = processor.process_handler(int(payload["dgId"]), int(payload["newDgId"]), authCookie, payload["updateType"], payload["savedFilePath"], payload["patchPayload"]) + result = processor.process_handler(int(payload["dgId"]), int(payload["newDgId"]), authCookie, payload["updateType"], payload["savedFilePath"], payload["patchPayload"], payload["sessionId"]) if result: return result else: @@ -69,6 +69,7 @@ async def forward_request(request: Request, response: Response): forward_payload["updateType"] = payload["updateType"] forward_payload["patchPayload"] = payload["patchPayload"] forward_payload["savedFilePath"] = payload["savedFilePath"] + forward_payload["sessionId"] = validator_response["sessionId"] if validator_response["sessionId"] is not None else 0 headers = { 'cookie': payload["cookie"], diff --git a/dataset-processor/dataset_validator.py b/dataset-processor/dataset_validator.py index b9e31ed3..33dc9229 100644 --- a/dataset-processor/dataset_validator.py +++ b/dataset-processor/dataset_validator.py @@ -17,22 +17,22 @@ def process_request(self, dgId, newDgId, cookie, updateType, savedFilePath, patc if updateType == "minor": metadata = self.get_datagroup_metadata(newDgId, cookie) if not metadata: - return self.generate_response(False, MSG_REQUEST_FAILED.format("Metadata")) + return self.generate_response(False, MSG_REQUEST_FAILED.format("Metadata"), None) session_id = self.create_progress_session(metadata, cookie) print(f"Progress Session ID : {session_id}") if not session_id: - return self.generate_response(False, MSG_REQUEST_FAILED.format("Progress session creation")) + return self.generate_response(False, MSG_REQUEST_FAILED.format("Progress session creation"), None) elif updateType == "patch": metadata = self.get_datagroup_metadata(dgId, cookie) if not metadata: - return self.generate_response(False, MSG_REQUEST_FAILED.format("Metadata")) + return self.generate_response(False, MSG_REQUEST_FAILED.format("Metadata"), None) session_id = self.create_progress_session(metadata, cookie) print(f"Progress Session ID : {session_id}") if not session_id: - return self.generate_response(False, MSG_REQUEST_FAILED.format("Progress session creation")) + return self.generate_response(False, MSG_REQUEST_FAILED.format("Progress session creation"), None) else: - return self.generate_response(False, "Unknown update type") + return self.generate_response(False, "Unknown update type", None) try: # Initializing dataset processing @@ -43,14 +43,14 @@ def process_request(self, dgId, newDgId, cookie, updateType, savedFilePath, patc elif updateType == "patch": result = self.handle_patch_update(dgId, cookie, patchPayload, session_id) else: - result = self.generate_response(False, "Unknown update type") + result = self.generate_response(False, "Unknown update type", None) # Final progress update upon successful completion self.update_progress(cookie, PROGRESS_VALIDATION_COMPLETE, MSG_VALIDATION_SUCCESS, STATUS_MSG_SUCCESS, session_id) return result except Exception as e: self.update_progress(cookie, PROGRESS_FAIL, MSG_INTERNAL_ERROR.format(e), STATUS_MSG_FAIL, session_id) - return self.generate_response(False, MSG_INTERNAL_ERROR.format(e)) + return self.generate_response(False, MSG_INTERNAL_ERROR.format(e), None) def handle_minor_update(self, dgId, cookie, savedFilePath, session_id): try: @@ -59,38 +59,38 @@ def handle_minor_update(self, dgId, cookie, savedFilePath, session_id): data = self.get_dataset_by_location(savedFilePath, cookie) if data is None: self.update_progress(cookie, PROGRESS_FAIL, "Failed to download and load data", STATUS_MSG_FAIL, session_id) - return self.generate_response(False, "Failed to download and load data") + return self.generate_response(False, "Failed to download and load data", None) print("Data downloaded and loaded successfully") self.update_progress(cookie, 20, MSG_FETCHING_VALIDATION_CRITERIA, STATUS_MSG_VALIDATION_INPROGRESS, session_id) validation_criteria, class_hierarchy = self.get_validation_criteria(dgId, cookie) if validation_criteria is None: self.update_progress(cookie, PROGRESS_FAIL, "Failed to get validation criteria", STATUS_MSG_FAIL, session_id) - return self.generate_response(False, "Failed to get validation criteria") + return self.generate_response(False, "Failed to get validation criteria", None) print("Validation criteria retrieved successfully") self.update_progress(cookie, 30, MSG_VALIDATING_FIELDS, STATUS_MSG_VALIDATION_INPROGRESS, session_id) field_validation_result = self.validate_fields(data, validation_criteria) if not field_validation_result['success']: self.update_progress(cookie, PROGRESS_FAIL, field_validation_result['message'], STATUS_MSG_FAIL, session_id) - return self.generate_response(False, field_validation_result['message']) + return self.generate_response(False, field_validation_result['message'], None) print(MSG_VALIDATION_FIELDS_SUCCESS) self.update_progress(cookie, 35, MSG_VALIDATING_CLASS_HIERARCHY, STATUS_MSG_VALIDATION_INPROGRESS, session_id) hierarchy_validation_result = self.validate_class_hierarchy(data, validation_criteria, class_hierarchy) if not hierarchy_validation_result['success']: self.update_progress(cookie, PROGRESS_FAIL, hierarchy_validation_result['message'], STATUS_MSG_FAIL, session_id) - return self.generate_response(False, hierarchy_validation_result['message']) + return self.generate_response(False, hierarchy_validation_result['message'], None) print(MSG_CLASS_HIERARCHY_SUCCESS) print("Minor update processed successfully") self.update_progress(cookie, 40, "Minor update processed successfully", STATUS_MSG_SUCCESS, session_id) - return self.generate_response(True, "Minor update processed successfully") + return self.generate_response(True, "Minor update processed successfully", session_id) except Exception as e: print(MSG_INTERNAL_ERROR.format(e)) self.update_progress(cookie, PROGRESS_FAIL, MSG_INTERNAL_ERROR.format(e), STATUS_MSG_FAIL, session_id) - return self.generate_response(False, MSG_INTERNAL_ERROR.format(e)) + return self.generate_response(False, MSG_INTERNAL_ERROR.format(e), None) def get_dataset_by_location(self, fileLocation, custom_jwt_cookie): print(MSG_DOWNLOADING_DATASET) @@ -224,11 +224,11 @@ def handle_patch_update(self, dgId, cookie, patchPayload, session_id): validation_criteria, class_hierarchy = self.get_validation_criteria(dgId, cookie) if validation_criteria is None: self.update_progress(cookie, PROGRESS_FAIL, "Failed to get validation criteria", STATUS_MSG_FAIL, session_id) - return self.generate_response(False, "Failed to get validation criteria") + return self.generate_response(False, "Failed to get validation criteria", None) if patchPayload is None: self.update_progress(cookie, PROGRESS_FAIL, "No patch payload provided", STATUS_MSG_FAIL, session_id) - return self.generate_response(False, "No patch payload provided") + return self.generate_response(False, "No patch payload provided", None) decoded_patch_payload = urllib.parse.unquote(patchPayload) patch_payload_dict = json.loads(decoded_patch_payload) @@ -241,16 +241,16 @@ def handle_patch_update(self, dgId, cookie, patchPayload, session_id): row_id = row.pop("rowId", None) if row_id is None: self.update_progress(cookie, PROGRESS_FAIL, "Missing rowId in edited data", STATUS_MSG_FAIL, session_id) - return self.generate_response(False, "Missing rowId in edited data") + return self.generate_response(False, "Missing rowId in edited data", None) for key, value in row.items(): if key not in validation_criteria['validationRules']: self.update_progress(cookie, PROGRESS_FAIL, f"Invalid field: {key}", STATUS_MSG_FAIL, session_id) - return self.generate_response(False, f"Invalid field: {key}") + return self.generate_response(False, f"Invalid field: {key}", None) if not self.validate_value(value, validation_criteria['validationRules'][key]['type']): self.update_progress(cookie, PROGRESS_FAIL, f"Validation failed for field type '{key}' in row {row_id}", STATUS_MSG_FAIL, session_id) - return self.generate_response(False, f"Validation failed for field type '{key}' in row {row_id}") + return self.generate_response(False, f"Validation failed for field type '{key}' in row {row_id}", None) self.update_progress(cookie, 20, "Validating data class hierarchy", STATUS_MSG_VALIDATION_INPROGRESS, session_id) data_class_columns = [field for field, rules in validation_criteria['validationRules'].items() if rules.get('isDataClass', False)] @@ -259,18 +259,18 @@ def handle_patch_update(self, dgId, cookie, patchPayload, session_id): for col in data_class_columns: if row.get(col) and row[col] not in hierarchy_values: self.update_progress(cookie, PROGRESS_FAIL, f"New class '{row[col]}' does not exist in the schema hierarchy", STATUS_MSG_FAIL, session_id) - return self.generate_response(False, f"New class '{row[col]}' does not exist in the schema hierarchy") + return self.generate_response(False, f"New class '{row[col]}' does not exist in the schema hierarchy", None) self.update_progress(cookie, 30, "Downloading aggregated dataset", STATUS_MSG_VALIDATION_INPROGRESS, session_id) aggregated_data = self.get_dataset_by_location(f"/dataset/{dgId}/primary_dataset/dataset_{dgId}_aggregated.json", cookie) if aggregated_data is None: self.update_progress(cookie, PROGRESS_FAIL, "Failed to download aggregated dataset", STATUS_MSG_FAIL, session_id) - return self.generate_response(False, "Failed to download aggregated dataset") + return self.generate_response(False, "Failed to download aggregated dataset", None) self.update_progress(cookie, 35, "Checking label counts for edited data", STATUS_MSG_VALIDATION_INPROGRESS, session_id) if not self.check_label_counts(aggregated_data, edited_data, data_class_columns, min_label_value): self.update_progress(cookie, PROGRESS_FAIL, "Editing this data will cause the dataset to have insufficient data examples for one or more labels.", STATUS_MSG_FAIL, session_id) - return self.generate_response(False, "Editing this data will cause the dataset to have insufficient data examples for one or more labels.") + return self.generate_response(False, "Editing this data will cause the dataset to have insufficient data examples for one or more labels.", None) deleted_data_rows = patch_payload_dict.get("deletedDataRows", []) if deleted_data_rows: @@ -279,20 +279,20 @@ def handle_patch_update(self, dgId, cookie, patchPayload, session_id): aggregated_data = self.get_dataset_by_location(f"/dataset/{dgId}/primary_dataset/dataset_{dgId}_aggregated.json", cookie) if aggregated_data is None: self.update_progress(cookie, PROGRESS_FAIL, "Failed to download aggregated dataset", STATUS_MSG_FAIL, session_id) - return self.generate_response(False, "Failed to download aggregated dataset") + return self.generate_response(False, "Failed to download aggregated dataset", None) self.update_progress(cookie, 45, "Checking label counts after deletion", STATUS_MSG_VALIDATION_INPROGRESS, session_id) if not self.check_label_counts_after_deletion(aggregated_data, deleted_data_rows, data_class_columns, min_label_value): self.update_progress(cookie, PROGRESS_FAIL, "Deleting this data will cause the dataset to have insufficient data examples for one or more labels.", STATUS_MSG_FAIL, session_id) - return self.generate_response(False, "Deleting this data will cause the dataset to have insufficient data examples for one or more labels.") + return self.generate_response(False, "Deleting this data will cause the dataset to have insufficient data examples for one or more labels.", None) self.update_progress(cookie, PROGRESS_VALIDATION_COMPLETE, MSG_PATCH_UPDATE_SUCCESS, STATUS_MSG_SUCCESS, session_id) - return self.generate_response(True, MSG_PATCH_UPDATE_SUCCESS) + return self.generate_response(True, MSG_PATCH_UPDATE_SUCCESS, session_id) except Exception as e: print(MSG_INTERNAL_ERROR.format(e)) self.update_progress(cookie, PROGRESS_FAIL, MSG_INTERNAL_ERROR.format(e), STATUS_MSG_FAIL, session_id) - return self.generate_response(False, MSG_INTERNAL_ERROR.format(e)) + return self.generate_response(False, MSG_INTERNAL_ERROR.format(e), None) def check_label_counts(self, aggregated_data, edited_data, data_class_columns, min_label_value): @@ -348,12 +348,13 @@ def check_label_counts_after_deletion(self, aggregated_data, deleted_data_rows, return False return True - def generate_response(self, success, message): + def generate_response(self, success, message, session_id): print(MSG_GENERATING_RESPONSE.format(success, message)) return { 'response': { 'operationSuccessful': success, - 'message': message + 'message': message, + 'sessionId': session_id } } From 05feaf0eb10dc7865245f0ef52fff1c4c31d216c Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Thu, 15 Aug 2024 01:20:14 +0530 Subject: [PATCH 471/582] draft corrected text export api endpoint --- file-handler/file_handler_api.py | 38 ++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/file-handler/file_handler_api.py b/file-handler/file_handler_api.py index 277a0b16..254677c3 100644 --- a/file-handler/file_handler_api.py +++ b/file-handler/file_handler_api.py @@ -14,6 +14,7 @@ from typing import List from io import BytesIO, TextIOWrapper from dataset_deleter import DatasetDeleter +from datetime import datetime app = FastAPI() @@ -31,6 +32,10 @@ class ExportFile(BaseModel): dgId: int exportType: str +class ExportCorrectedDataFile(BaseModel): + platform: str + exportType: str + class ImportChunks(BaseModel): dg_id: int chunks: list @@ -404,3 +409,36 @@ async def delete_dataset_files(request: Request): except Exception as e: print(f"Error in delete_dataset_files: {e}") raise HTTPException(status_code=500, detail=str(e)) + +@app.post("/datamodel/data/corrected/download") +async def download_and_convert(request: Request, exportData: ExportCorrectedDataFile, backgroundTasks: BackgroundTasks): + cookie = request.cookies.get("customJwtCookie") + await authenticate_user(f'customJwtCookie={cookie}') + platform = exportData.platform + export_type = exportData.exportType + + if export_type not in ["xlsx", "yaml", "json"]: + raise HTTPException(status_code=500, detail=EXPORT_TYPE_ERROR) + + # get json payload by calling to Ruuter + json_data = {} + now = datetime.now() + formatted_time_date = now.strftime("%Y%m%d_%H%M%S") + result_string = f"corrected_text_{formatted_time_date}" + + file_converter = FileConverter() + + if export_type == "xlsx": + output_file = f"{result_string}{XLSX_EXT}" + file_converter.convert_json_to_xlsx(json_data, output_file) + elif export_type == "yaml": + output_file = f"{result_string}{YAML_EXT}" + file_converter.convert_json_to_yaml(json_data, output_file) + elif export_type == "json": + output_file = f"{result_string}{JSON_EXT}" + else: + raise HTTPException(status_code=500, detail=EXPORT_TYPE_ERROR) + + backgroundTasks.add_task(os.remove, output_file) + + return FileResponse(output_file, filename=os.path.basename(output_file)) \ No newline at end of file From ae7622ac3c934d8ae8db2cdca0b22b0c64365ff3 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Thu, 15 Aug 2024 01:48:40 +0530 Subject: [PATCH 472/582] datamodel delete endpoint --- file-handler/dataset_deleter.py | 25 ++++++++++++++++++++++++- file-handler/file_handler_api.py | 23 ++++++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/file-handler/dataset_deleter.py b/file-handler/dataset_deleter.py index 68a3034f..d56d34a3 100644 --- a/file-handler/dataset_deleter.py +++ b/file-handler/dataset_deleter.py @@ -2,6 +2,7 @@ import json import requests from s3_ferry import S3Ferry +import zipfile GET_PAGE_COUNT_URL = os.getenv("GET_PAGE_COUNT_URL") UPLOAD_DIRECTORY = os.getenv("UPLOAD_DIRECTORY", "/shared") @@ -61,4 +62,26 @@ def delete_dataset_files(self, dg_id, cookie): all_files_deleted = success_count >= len(file_locations)-2 os.remove(empty_json_path) print(f"Dataset Deletion Final : {all_files_deleted} / {success_count}") - return all_files_deleted, success_count \ No newline at end of file + return all_files_deleted, success_count + +class ModelDeleter: + def __init__(self, s3_ferry_url): + self.s3_ferry = S3Ferry(s3_ferry_url) + + def delete_model_files(self, model_id): + file_location = f"/models/{model_id}/{model_id}.zip" + + empty_zip_path = os.path.join('..', 'shared', "empty.zip") + with open(empty_zip_path, 'w') as empty_file: + json.dump({}, empty_file) + + empty_zip_path_local = "empty.zip" + + response = self.s3_ferry.transfer_file(file_location, "S3", empty_zip_path_local, "FS") + os.remove(empty_zip_path) + if response.status_code == 201: + return True + else: + print(response.status_code) + print(f"Failed to transfer file to {file_location}") + return False \ No newline at end of file diff --git a/file-handler/file_handler_api.py b/file-handler/file_handler_api.py index 254677c3..e6b696c1 100644 --- a/file-handler/file_handler_api.py +++ b/file-handler/file_handler_api.py @@ -14,6 +14,7 @@ from typing import List from io import BytesIO, TextIOWrapper from dataset_deleter import DatasetDeleter +from dataset_deleter import ModelDeleter from datetime import datetime app = FastAPI() @@ -441,4 +442,24 @@ async def download_and_convert(request: Request, exportData: ExportCorrectedData backgroundTasks.add_task(os.remove, output_file) - return FileResponse(output_file, filename=os.path.basename(output_file)) \ No newline at end of file + return FileResponse(output_file, filename=os.path.basename(output_file)) + +@app.post("/datamodel/model/delete") +async def delete_datamodels(request: Request): + try: + cookie = request.cookies.get("customJwtCookie") + await authenticate_user(f'customJwtCookie={cookie}') + + payload = await request.json() + model_id = int(payload["modelId"]) + + deleter = ModelDeleter(S3_FERRY_URL) + success = deleter.delete_model_files(model_id) + + if success: + return JSONResponse(status_code=200, content={"message": "Data model deletion completed successfully."}) + else: + return JSONResponse(status_code=500, content={"message": "Data model deletion failed."}) + except Exception as e: + print(f"Error in delete_datamodel_files: {e}") + raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file From a7db334eb0579c3f228e491be52232eeeb3bfcef Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Thu, 15 Aug 2024 02:30:21 +0530 Subject: [PATCH 473/582] Anonymizer Streamlit app --- anonymizer/anonymizer_api.py | 41 ++++++++++++++++++++++++++ anonymizer/anonymizer_streamlit_app.py | 27 +++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 anonymizer/anonymizer_streamlit_app.py diff --git a/anonymizer/anonymizer_api.py b/anonymizer/anonymizer_api.py index e7422b10..9780da28 100644 --- a/anonymizer/anonymizer_api.py +++ b/anonymizer/anonymizer_api.py @@ -9,6 +9,10 @@ import hmac import hashlib import json +from fastapi import FastAPI, File, UploadFile, Form, HTTPException +from fastapi.responses import StreamingResponse +import pandas as pd +import io app = FastAPI() @@ -92,6 +96,43 @@ def verify_signature(payload: dict, headers: dict, secret: str) -> bool: return is_valid +async def anonymize_file(file: UploadFile = File(...), columns: str = Form(...)): + try: + contents = await file.read() + df = pd.read_excel(io.BytesIO(contents)) + + columns_to_anonymize = columns.split(",") + + concatenated_text = " ".join(" ".join(str(val) for val in df[col].values) for col in columns_to_anonymize) + + cleaned_text = html_cleaner.remove_html_tags(concatenated_text) + text_chunks = TextProcessor.split_text(cleaned_text, 2000) + processed_chunks = [] + + for chunk in text_chunks: + entities = ner_processor.identify_entities(chunk) + processed_chunk = FakeReplacer.replace_entities(chunk, entities) + processed_chunks.append(processed_chunk) + + processed_text = TextProcessor.combine_chunks(processed_chunks) + anonymized_values = processed_text.split(" ") + + for col in columns_to_anonymize: + df[col] = anonymized_values[:len(df[col])] + anonymized_values = anonymized_values[len(df[col]):] + + output = io.BytesIO() + df.to_excel(output, index=False) + output.seek(0) + + return StreamingResponse(output, media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", headers={ + "Content-Disposition": f"attachment; filename=anonymized_{file.filename}" + }) + + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8010) diff --git a/anonymizer/anonymizer_streamlit_app.py b/anonymizer/anonymizer_streamlit_app.py new file mode 100644 index 00000000..6da6dbae --- /dev/null +++ b/anonymizer/anonymizer_streamlit_app.py @@ -0,0 +1,27 @@ +import streamlit as st +import pandas as pd +import requests +from io import BytesIO + +st.title("Data Anonymizer") + +uploaded_file = st.file_uploader("Upload your Excel file", type="xlsx") + +columns_to_anonymize = st.text_input("Enter the column names to anonymize (comma-separated)") + +if st.button("Anonymize"): + if uploaded_file is not None and columns_to_anonymize: + file_data = BytesIO(uploaded_file.read()) + + files = {'file': (uploaded_file.name, file_data, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')} + data = {'columns': columns_to_anonymize} + + response = requests.post("http://localhost:8010/anonymize-file", files=files, data=data) + + if response.status_code == 200: + st.success("Anonymization successful! Download the file below:") + st.download_button(label="Download Anonymized File", data=response.content, file_name=f"anonymized_{uploaded_file.name}") + else: + st.error(f"An error occurred: {response.text}") + else: + st.warning("Please upload a file and enter the column names.") From 0b35dc74901f9312e4aa7e465848891a11b6cef2 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Thu, 15 Aug 2024 02:37:28 +0530 Subject: [PATCH 474/582] data enrichment streamlit app --- data_enrichment/enrichment_streamlit_app.py | 49 +++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 data_enrichment/enrichment_streamlit_app.py diff --git a/data_enrichment/enrichment_streamlit_app.py b/data_enrichment/enrichment_streamlit_app.py new file mode 100644 index 00000000..651af048 --- /dev/null +++ b/data_enrichment/enrichment_streamlit_app.py @@ -0,0 +1,49 @@ +import streamlit as st +import pandas as pd +import requests +from io import BytesIO + +API_URL = "http://0.0.0.0:8005/paraphrase" + +def paraphrase_text(text, num_return_sequences=1, language_id=None): + response = requests.post( + API_URL, + json={"text": text, "num_return_sequences": num_return_sequences, "language_id": language_id}, + ) + if response.status_code == 200: + return response.json()["paraphrases"] + else: + st.error(f"Error: {response.status_code} - {response.json().get('detail', 'Unknown error')}") + return [] + +st.title("Syntactic Data Generator") + +uploaded_file = st.file_uploader("Upload your Excel file", type=["xlsx"]) + +if uploaded_file: + df = pd.read_excel(uploaded_file) + st.write("Original Data", df) + + columns_to_paraphrase = st.multiselect("Select columns to generate syntactic data", df.columns) + + if st.button("Generate Syntactic Data"): + new_data = df.copy() + for column in columns_to_paraphrase: + paraphrased_column = [] + for text in df[column]: + paraphrased_text = paraphrase_text(text) + paraphrased_column.append(paraphrased_text[0] if paraphrased_text else text) + new_data[column] = paraphrased_column + + output = BytesIO() + with pd.ExcelWriter(output, engine="xlsxwriter") as writer: + new_data.to_excel(writer, index=False, sheet_name="Sheet1") + output.seek(0) + + st.download_button( + label="Download Synthesized Data", + data=output, + file_name="synthesized_data.xlsx", + mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + ) + st.write("Synthesized Data", new_data) From f7d246cee25b0452775d49e16d723686f0ee0416 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Thu, 15 Aug 2024 11:06:32 +0530 Subject: [PATCH 475/582] correced texts api int --- .../CorrectedTextsTables.tsx | 31 +--- GUI/src/pages/CorrectedTexts/index.tsx | 3 +- GUI/src/pages/TestModel/index.tsx | 175 +++++++++++++----- GUI/src/pages/TestModel/testModelStyles.scss | 13 ++ GUI/src/types/testModelTypes.ts | 22 +++ GUI/src/utils/commonUtilts.ts | 21 +++ GUI/src/utils/endpoints.ts | 5 + GUI/src/utils/queryKeys.ts | 6 +- GUI/translations/en/common.json | 12 +- 9 files changed, 211 insertions(+), 77 deletions(-) create mode 100644 GUI/src/pages/TestModel/testModelStyles.scss create mode 100644 GUI/src/types/testModelTypes.ts diff --git a/GUI/src/components/molecules/CorrectedTextTables/CorrectedTextsTables.tsx b/GUI/src/components/molecules/CorrectedTextTables/CorrectedTextsTables.tsx index b3e0c8a5..2b74bfb7 100644 --- a/GUI/src/components/molecules/CorrectedTextTables/CorrectedTextsTables.tsx +++ b/GUI/src/components/molecules/CorrectedTextTables/CorrectedTextsTables.tsx @@ -7,12 +7,12 @@ import { PaginationState, } from '@tanstack/react-table'; import { useTranslation } from 'react-i18next'; -import { formatDateTime } from 'utils/commonUtilts'; +import { formatClassHierarchyArray, formatDateTime } from 'utils/commonUtilts'; import Card from 'components/Card'; import { InferencePayload } from 'types/correctedTextTypes'; import './CorrectedTextTable.scss'; -const CorrectedTextsTables = ({ +const CorrectedTextsTable = ({ correctedTextData, totalPages, isLoading, @@ -74,7 +74,7 @@ const CorrectedTextsTables = ({ cell: (props) => (
          {props?.row?.original?.predictedLabels && - formatArray(props?.row?.original?.predictedLabels)} + formatClassHierarchyArray(props?.row?.original?.predictedLabels)}
          ), }), @@ -108,7 +108,7 @@ const CorrectedTextsTables = ({ cell: (props) => (
          {props?.row?.original?.correctedLabels && - formatArray(props?.row?.original?.correctedLabels)} + formatClassHierarchyArray(props?.row?.original?.correctedLabels)}
          ), }), @@ -130,27 +130,6 @@ const CorrectedTextsTables = ({ [t] ); - const formatArray = (array: string | string[]) => { - let formatedArray: string[]; - if (typeof array === 'string') { - try { - const cleanedInput = array?.replace(/\s+/g, ''); - formatedArray = JSON.parse(cleanedInput); - } catch (error) { - console.error('Error parsing input string:', error); - return ''; - } - } else { - formatedArray = array; - } - - return formatedArray - .map((item, index) => - index === formatedArray?.length - 1 ? item : item + ' ->' - ) - .join(' '); - }; - return (
          @@ -195,4 +174,4 @@ const CorrectedTextsTables = ({ ); }; -export default CorrectedTextsTables; \ No newline at end of file +export default CorrectedTextsTable; \ No newline at end of file diff --git a/GUI/src/pages/CorrectedTexts/index.tsx b/GUI/src/pages/CorrectedTexts/index.tsx index 54d7094a..682b9fc7 100644 --- a/GUI/src/pages/CorrectedTexts/index.tsx +++ b/GUI/src/pages/CorrectedTexts/index.tsx @@ -9,6 +9,7 @@ import { correctedTextEndpoints } from 'utils/endpoints'; import apiDev from '../../services/api-dev'; import { InferencePayload } from 'types/correctedTextTypes'; import { PaginationState } from '@tanstack/react-table'; +import CorrectedTextsTable from 'components/molecules/CorrectedTextTables/CorrectedTextsTables'; const CorrectedTexts: FC = () => { const { t } = useTranslation(); @@ -135,7 +136,7 @@ const CorrectedTexts: FC = () => {
          - { const { t } = useTranslation(); -const testResults={ - predictedClasses:["Police","Special Agency","External","Reports","Annual Report"], - averageConfidence:89.8, - predictedProbabilities: [98,82,91,90,88] -} + + const [modelOptions, setModelOptions] = useState< + TestModalDropdownSelectionType[] + >([]); + + const [testModel, setTestModel] = useState({ + modelId: null, + text: '', + }); + const { isLoading } = useQuery({ + queryKey: testModelsQueryKeys.GET_TEST_MODELS(), + queryFn: async () => { + const response = await apiDev.get(testModelsEnpoinnts.GET_MODELS()); + return response?.data?.response?.data ?? ([] as TestModelType[]); + }, + onSuccess: (data: TestModelType[]) => { + if (data && data.length > 0) { + setModelOptions( + data?.map((options) => ({ + label: options.modelName, + value: options.modelId, + })) + ); + } + }, + }); + + const { + data: classifyData, + isLoading: classifyLoading, + mutate, + } = useMutation({ + mutationFn: async (data: ClassifyTestModalPayloadType) => { + const response = await apiDev.post( + testModelsEnpoinnts.CLASSIFY_TEST_MODELS(), + data + ); + return response?.data?.response?.data as ClassifyTestModalResponseType; + }, + }); + + const handleChange = (key: string, value: string | number) => { + setTestModel((prev) => ({ + ...prev, + [key]: value, + })); + }; + return (
          -
          -
          -
          Test Model
          -
          -
          - -
          - -
          -

          Enter Text

          - -
          -
          - -
          + {isLoading ? ( + + ) : ( +
          +
          +
          {t('testModels.title')}
          +
          +
          + { + handleChange('modelId', selection?.value as string); + }} + /> +
          -
          -
          -
          - {`Predicted Class Hierarchy : `} -

          - { - 'Police -> Special Agency -> External -> Reports -> Annual Report' - } -

          -
          +
          +

          {t('testModels.classifyTextLabel')}

          + handleChange('text', e.target.value)} + showMaxLength={true} + />
          -
          -
          - {`Average Confidence : `} -

          {'62%'}

          -
          +
          +
          -
          -
          - {`Class Probabilities : `} -
            - {formatPredictions(testResults)?.map((prediction)=>{ - return(
          • {prediction}
          • ) - })} -
          + + {!classifyLoading && classifyData && ( +
          +
          +
          + {t('testModels.predictedHierarchy')} +

          + {classifyData?.predictedClasses && + formatClassHierarchyArray(classifyData?.predictedClasses)} +

          +
          +
          +
          +
          + {t('testModels.averageConfidence')} +

          {classifyData?.averageConfidence ?? ''}

          +
          +
          +
          +
          + {t('testModels.classProbabilities')} +
            + {formatPredictions(classifyData)?.map((prediction) => { + return
          • {prediction}
          • ; + })} +
          +
          +
          -
          + )}
          -
          + )}
          ); }; -export default TestModel; +export default TestModel; \ No newline at end of file diff --git a/GUI/src/pages/TestModel/testModelStyles.scss b/GUI/src/pages/TestModel/testModelStyles.scss new file mode 100644 index 00000000..2025939b --- /dev/null +++ b/GUI/src/pages/TestModel/testModelStyles.scss @@ -0,0 +1,13 @@ +.testModalformTextArea { + margin-top: 30px; +} + +.testModalClassifyButton { + text-align: right; + margin-top: 20px; +} + +.testModalList { + list-style: disc; + margin-left: 30px; +} \ No newline at end of file diff --git a/GUI/src/types/testModelTypes.ts b/GUI/src/types/testModelTypes.ts new file mode 100644 index 00000000..283dd011 --- /dev/null +++ b/GUI/src/types/testModelTypes.ts @@ -0,0 +1,22 @@ +export type TestModelType = { + majorVersion: number; + minorVersion: number; + modelId: number; + modelName: string; + }; + + export type TestModalDropdownSelectionType = { + label: string; + value: number; + }; + + export type ClassifyTestModalPayloadType = { + modelId: number | null; + text: string; + }; + + export type ClassifyTestModalResponseType = { + predictedClasses: string[]; + averageConfidence: number; + predictedProbabilities: number[]; + }; \ No newline at end of file diff --git a/GUI/src/utils/commonUtilts.ts b/GUI/src/utils/commonUtilts.ts index 0b59866a..9b842f4b 100644 --- a/GUI/src/utils/commonUtilts.ts +++ b/GUI/src/utils/commonUtilts.ts @@ -58,3 +58,24 @@ export const formatDateTime = (date: string) => { formattedTime, }; }; + +export const formatClassHierarchyArray = (array: string | string[]) => { + let formatedArray: string[]; + if (typeof array === 'string') { + try { + const cleanedInput = array?.replace(/\s+/g, ''); + formatedArray = JSON.parse(cleanedInput); + } catch (error) { + console.error('Error parsing input string:', error); + return ''; + } + } else { + formatedArray = array; + } + + return formatedArray + .map((item, index) => + index === formatedArray?.length - 1 ? item : item + ' ->' + ) + .join(' '); +}; \ No newline at end of file diff --git a/GUI/src/utils/endpoints.ts b/GUI/src/utils/endpoints.ts index c08cd4bf..ca86ac0b 100644 --- a/GUI/src/utils/endpoints.ts +++ b/GUI/src/utils/endpoints.ts @@ -58,3 +58,8 @@ export const dataModelsEndpoints = { RETRAIN_DATA_MODEL: (): string => `classifier/datamodel/retrain`, GET_DATA_MODEL_PROGRESS: (): string => `classifier/datamodel/progress`, }; + +export const testModelsEnpoinnts = { + GET_MODELS: (): string => `/classifier/testmodel/models`, + CLASSIFY_TEST_MODELS: (): string => `/classifier/testmodel/test-data`, +}; \ No newline at end of file diff --git a/GUI/src/utils/queryKeys.ts b/GUI/src/utils/queryKeys.ts index 43760ca8..05369051 100644 --- a/GUI/src/utils/queryKeys.ts +++ b/GUI/src/utils/queryKeys.ts @@ -92,4 +92,8 @@ export const dataModelsQueryKeys = { (val) => val !== undefined ); } -}; \ No newline at end of file +}; + +export const testModelsQueryKeys = { + GET_TEST_MODELS: () => ['testModels'] +} \ No newline at end of file diff --git a/GUI/translations/en/common.json b/GUI/translations/en/common.json index 5df4962c..58bd0059 100644 --- a/GUI/translations/en/common.json +++ b/GUI/translations/en/common.json @@ -382,5 +382,15 @@ "title": "Training Sessions", "inprogress": "Validation in-Progress", "fail": "Validation failed because {{class}} class found in the {{column}} column does not exist in hierarchy" - } + }, + "testModels": { + "title": "Test Model", + "selectionLabel": "Model", + "placeholder": "Choose model", + "classifyTextLabel": "Enter Text", + "classify": "Classify", + "predictedHierarchy": "Predicted Class Hierarchy : ", + "averageConfidence": "Average Confidence : ", + "classProbabilities": "Class Probabilities : " + }, } From a877bfb0ce9a737c9cc49c1b0f69b97fda8df590 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Thu, 15 Aug 2024 13:14:01 +0530 Subject: [PATCH 476/582] fix dataset status update --- dataset-processor/dataset_validator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dataset-processor/dataset_validator.py b/dataset-processor/dataset_validator.py index 33dc9229..3a618dfd 100644 --- a/dataset-processor/dataset_validator.py +++ b/dataset-processor/dataset_validator.py @@ -46,7 +46,7 @@ def process_request(self, dgId, newDgId, cookie, updateType, savedFilePath, patc result = self.generate_response(False, "Unknown update type", None) # Final progress update upon successful completion - self.update_progress(cookie, PROGRESS_VALIDATION_COMPLETE, MSG_VALIDATION_SUCCESS, STATUS_MSG_SUCCESS, session_id) + self.update_progress(cookie, PROGRESS_VALIDATION_COMPLETE, MSG_VALIDATION_SUCCESS, STATUS_MSG_VALIDATION_INPROGRESS, session_id) return result except Exception as e: self.update_progress(cookie, PROGRESS_FAIL, MSG_INTERNAL_ERROR.format(e), STATUS_MSG_FAIL, session_id) @@ -84,7 +84,7 @@ def handle_minor_update(self, dgId, cookie, savedFilePath, session_id): print(MSG_CLASS_HIERARCHY_SUCCESS) print("Minor update processed successfully") - self.update_progress(cookie, 40, "Minor update processed successfully", STATUS_MSG_SUCCESS, session_id) + self.update_progress(cookie, 40, "Minor update processed successfully", STATUS_MSG_VALIDATION_INPROGRESS, session_id) return self.generate_response(True, "Minor update processed successfully", session_id) except Exception as e: @@ -286,7 +286,7 @@ def handle_patch_update(self, dgId, cookie, patchPayload, session_id): self.update_progress(cookie, PROGRESS_FAIL, "Deleting this data will cause the dataset to have insufficient data examples for one or more labels.", STATUS_MSG_FAIL, session_id) return self.generate_response(False, "Deleting this data will cause the dataset to have insufficient data examples for one or more labels.", None) - self.update_progress(cookie, PROGRESS_VALIDATION_COMPLETE, MSG_PATCH_UPDATE_SUCCESS, STATUS_MSG_SUCCESS, session_id) + self.update_progress(cookie, PROGRESS_VALIDATION_COMPLETE, MSG_PATCH_UPDATE_SUCCESS, STATUS_MSG_VALIDATION_INPROGRESS, session_id) return self.generate_response(True, MSG_PATCH_UPDATE_SUCCESS, session_id) except Exception as e: From ad755c405ac3fe61c52350ba6845029c8bda3935 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Thu, 15 Aug 2024 14:08:52 +0530 Subject: [PATCH 477/582] fixes --- .../pages/LoadingScreen/LoadingScreen.scss | 20 +++++++++++++++++++ GUI/src/pages/LoadingScreen/LoadingScreen.tsx | 5 +++-- GUI/translations/en/common.json | 2 +- 3 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 GUI/src/pages/LoadingScreen/LoadingScreen.scss diff --git a/GUI/src/pages/LoadingScreen/LoadingScreen.scss b/GUI/src/pages/LoadingScreen/LoadingScreen.scss new file mode 100644 index 00000000..c45e573a --- /dev/null +++ b/GUI/src/pages/LoadingScreen/LoadingScreen.scss @@ -0,0 +1,20 @@ +/* Loader container */ +.loader { + position: fixed; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + border: 8px solid #f3f3f3; /* Light grey */ + border-top: 8px solid #3498db; /* Blue */ + border-radius: 50%; + width: 60px; + height: 60px; + animation: spin 1.5s linear infinite; + } + + /* Spin animation */ + @keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + } + \ No newline at end of file diff --git a/GUI/src/pages/LoadingScreen/LoadingScreen.tsx b/GUI/src/pages/LoadingScreen/LoadingScreen.tsx index 679ec206..3f8add92 100644 --- a/GUI/src/pages/LoadingScreen/LoadingScreen.tsx +++ b/GUI/src/pages/LoadingScreen/LoadingScreen.tsx @@ -1,10 +1,11 @@ import { FC } from 'react'; +import './LoadingScreen.scss' const LoadingScreen: FC = () => { return (
          -

          Loading...

          -
          +
          +
          ); }; diff --git a/GUI/translations/en/common.json b/GUI/translations/en/common.json index 58bd0059..f779c297 100644 --- a/GUI/translations/en/common.json +++ b/GUI/translations/en/common.json @@ -392,5 +392,5 @@ "predictedHierarchy": "Predicted Class Hierarchy : ", "averageConfidence": "Average Confidence : ", "classProbabilities": "Class Probabilities : " - }, + } } From 5c55d3121ffb6fe0ecb4fd8ad94f0bdb22673eff Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Thu, 15 Aug 2024 15:19:43 +0530 Subject: [PATCH 478/582] adding pandas to anonymizer --- anonymizer/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/anonymizer/requirements.txt b/anonymizer/requirements.txt index 94cfdf4a..3b7b39ff 100644 --- a/anonymizer/requirements.txt +++ b/anonymizer/requirements.txt @@ -14,6 +14,7 @@ idna==3.7 langdetect==1.0.9 numpy==2.0.1 packaging==24.1 +pandas==2.2.2 pydantic==2.8.2 pydantic_core==2.20.1 python-dateutil==2.9.0.post0 From 6e39849bc2ffbcfa6b24522c2e3e2305fe186e09 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Thu, 15 Aug 2024 16:44:45 +0530 Subject: [PATCH 479/582] outlook-bug-fix: Liquibase changelog order and fix outlook update mail bugs --- DSL/Liquibase/master.yml | 23 ++++++- .../DSL/GET/internal/outlook/token.yml | 64 +++++++++++++++++++ .../classifier/integration/outlook/accept.yml | 9 ++- .../DSL/POST/internal/outlook/label.yml | 17 +++-- 4 files changed, 104 insertions(+), 9 deletions(-) create mode 100644 DSL/Ruuter.public/DSL/GET/internal/outlook/token.yml diff --git a/DSL/Liquibase/master.yml b/DSL/Liquibase/master.yml index 8e0d64de..06ef5a82 100644 --- a/DSL/Liquibase/master.yml +++ b/DSL/Liquibase/master.yml @@ -1,4 +1,21 @@ databaseChangeLog: - - includeAll: - path: changelog/ - errorIfMissingOrEmpty: true + - include: + file: changelog/classifier-script-v1-user-management.sql + - include: + file: changelog/classifier-script-v2-authority-data.xml + - include: + file: changelog/classifier-script-v3-integrations.sql + - include: + file: changelog/classifier-script-v4-configuration.sql + - include: + file: changelog/classifier-script-v5-outlook-refresh-token.xml + - include: + file: changelog/classifier-script-v6-dataset-group-metadata.sql + - include: + file: changelog/classifier-script-v7-stop-words.sql + - include: + file: changelog/classifier-script-v8-dataset-progress-sessions.sql + - include: + file: changelog/classifier-script-v9-models-metadata.sql + - include: + file: changelog/classifier-script-v10-data-model-progress-sessions.sql diff --git a/DSL/Ruuter.public/DSL/GET/internal/outlook/token.yml b/DSL/Ruuter.public/DSL/GET/internal/outlook/token.yml new file mode 100644 index 00000000..7fc61b63 --- /dev/null +++ b/DSL/Ruuter.public/DSL/GET/internal/outlook/token.yml @@ -0,0 +1,64 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'TOKEN'" + method: get + accepts: json + returns: json + namespace: classifier + +get_refresh_token: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-token" + body: + platform: 'OUTLOOK' + result: res + next: set_refresh_token + +set_refresh_token: + assign: + refresh_token: ${res.response.body[0].token} + next: check_refresh_token + +check_refresh_token: + switch: + - condition: ${refresh_token !== null} + next: decrypt_token + next: return_not_found + +decrypt_token: + call: http.post + args: + url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/return_decrypted_outlook_token" + headers: + type: json + body: + token: ${refresh_token} + result: token_data + next: get_access_token + +get_access_token: + call: http.post + args: + url: "https://login.microsoftonline.com/common/oauth2/v2.0/token" + contentType: formdata + headers: + type: json + body: + client_id: "[#OUTLOOK_CLIENT_ID]" + scope: "User.Read Mail.ReadWrite MailboxSettings.ReadWrite offline_access" + refresh_token: ${token_data.response.body.token.content} + grant_type: "refresh_token" + client_secret: "[#OUTLOOK_SECRET_KEY]" + result: res + next: return_result + +return_result: + return: ${res.response.body} + next: end + +return_not_found: + status: 404 + return: "refresh token not found" + next: end \ No newline at end of file diff --git a/DSL/Ruuter.public/DSL/POST/classifier/integration/outlook/accept.yml b/DSL/Ruuter.public/DSL/POST/classifier/integration/outlook/accept.yml index 253c9bdd..f59f2cb0 100644 --- a/DSL/Ruuter.public/DSL/POST/classifier/integration/outlook/accept.yml +++ b/DSL/Ruuter.public/DSL/POST/classifier/integration/outlook/accept.yml @@ -140,9 +140,9 @@ check_folders_exist: check_folder_id: switch: - - condition: ${existing_outlook_info.response.body.primaryFolderId !== mail_info_data.response.body.parentFolderId} + - condition: ${existing_outlook_info.response.body[0].primaryFolderId !== mail_info_data.response.body.parentFolderId} next: rearrange_mail_payload - next: end + next: return_Folder_Match check_event_type: switch: @@ -190,6 +190,11 @@ return_ok: return: "Outlook data send successfully" next: end +return_Folder_Match: + status: 200 + return: "Folder Match,No Update" + next: end + return_mail_info_not_found: status: 404 return: "Mail Info Not Found" diff --git a/DSL/Ruuter.public/DSL/POST/internal/outlook/label.yml b/DSL/Ruuter.public/DSL/POST/internal/outlook/label.yml index 1b10c9b7..931b6ecb 100644 --- a/DSL/Ruuter.public/DSL/POST/internal/outlook/label.yml +++ b/DSL/Ruuter.public/DSL/POST/internal/outlook/label.yml @@ -30,11 +30,15 @@ check_for_request_data: get_token_info: call: http.get args: - url: "[#CLASSIFIER_RUUTER_PRIVATE]/classifier/integration/outlook/token" - headers: - cookie: ${incoming.headers.cookie} + url: "[#CLASSIFIER_RUUTER_PUBLIC_INTERNAL]/internal/outlook/token" result: res - next: assign_access_token + next: check_access_token + +check_access_token: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: assign_access_token + next: return_access_token_not_found assign_access_token: assign: @@ -111,4 +115,9 @@ return_bad_request: return_incorrect_request: status: 400 return: 'missing labels' + next: end + +return_access_token_not_found: + status: 404 + return: "Access Token Not Found" next: end \ No newline at end of file From 08dbf4e15fcd67e58311498d8f329f8460c115f8 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Thu, 15 Aug 2024 17:11:48 +0530 Subject: [PATCH 480/582] bug fix --- dataset-processor/dataset_processor.py | 20 ++++++++++++++++++-- dataset-processor/dataset_processor_api.py | 15 +++++++++++++-- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/dataset-processor/dataset_processor.py b/dataset-processor/dataset_processor.py index 04c998ba..0867d25f 100644 --- a/dataset-processor/dataset_processor.py +++ b/dataset-processor/dataset_processor.py @@ -314,6 +314,7 @@ def add_row_id(self, structured_data, max_row_id): def update_preprocess_status(self,dg_id, cookie, processed_data_available, raw_data_available, preprocess_data_location, raw_data_location, enable_allowed, num_samples, num_pages): url = STATUS_UPDATE_URL + print(url) headers = { 'Content-Type': 'application/json', @@ -331,6 +332,7 @@ def update_preprocess_status(self,dg_id, cookie, processed_data_available, raw_d } try: + print(data) response = requests.post(url, json=data, headers=headers) response.raise_for_status() return response.json() @@ -339,15 +341,21 @@ def update_preprocess_status(self,dg_id, cookie, processed_data_available, raw_d return None def process_handler(self, dgId, newDgId, cookie, updateType, savedFilePath, patchPayload, sessionId): + print("IN DATASET PROCESSOR PROCESS_HANDLER") print(MSG_PROCESS_HANDLER_STARTED.format(updateType)) page_count = self.get_page_count(dgId, cookie) print(MSG_PAGE_COUNT.format(page_count)) - if sessionId >= 0: + if int(sessionId) >= 0: session_id = sessionId if not session_id: return self.generate_response(False, MSG_FAIL) + + if page_count > 0 and updateType == 'minor': + updateType = "minor_append_update" + elif page_count <= 0 and updateType == 'minor': + updateType = "minor_initial_update" if updateType == "minor_initial_update": result = self.handle_minor_initial_update(dgId, newDgId, cookie, savedFilePath, session_id) @@ -355,6 +363,8 @@ def process_handler(self, dgId, newDgId, cookie, updateType, savedFilePath, patc result = self.handle_minor_append_update(dgId, newDgId, cookie, savedFilePath, session_id) elif updateType == "patch": result = self.handle_patch_update(dgId, cookie, patchPayload, session_id) + else: + print(f"Update TYPE {updateType}") self.update_progress(cookie, PROGRESS_SUCCESS if result['response']['operationSuccessful'] else PROGRESS_FAIL, MSG_SUCCESS if result['response']['operationSuccessful'] else MSG_FAIL, STATUS_MSG_SUCCESS if result['response']['operationSuccessful'] else STATUS_MSG_FAIL, session_id) return result @@ -559,10 +569,12 @@ def get_session_id(self, dgId, cookie): return session['id'] return None except requests.exceptions.RequestException as e: + print(e) print(MSG_FAIL) return None def update_progress(self, cookie, progress, message, status, session_id): + if progress == PROGRESS_SUCCESS or progress == PROGRESS_FAIL: process_complete = True else: @@ -571,17 +583,19 @@ def update_progress(self, cookie, progress, message, status, session_id): url = UPDATE_PROGRESS_SESSION_URL headers = {'Content-Type': 'application/json', 'Cookie': cookie} payload = { - 'sessionId': session_id, + 'sessionId': int(session_id), 'validationStatus': status, 'validationMessage': message, 'progressPercentage': progress, 'processComplete': process_complete } try: + print(f"Progress Update > {payload}") response = requests.post(url, json=payload, headers=headers) response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: + print(e) print(MSG_FAIL) return None @@ -604,6 +618,7 @@ def prepare_chunk_updates(self, cleaned_patch_payload): chunk_updates[chunk_num].append(entry) return chunk_updates except Exception as e: + print(e) print(MSG_FAIL) return None @@ -613,5 +628,6 @@ def reindex_dataset(self, dataset): row['rowId'] = idx return dataset except Exception as e: + print(e) print(MSG_FAIL) return None \ No newline at end of file diff --git a/dataset-processor/dataset_processor_api.py b/dataset-processor/dataset_processor_api.py index 7ec6b8a1..77d413f9 100644 --- a/dataset-processor/dataset_processor_api.py +++ b/dataset-processor/dataset_processor_api.py @@ -45,11 +45,14 @@ async def authenticate_user(request: Request): @app.post("/init-dataset-process") async def process_handler_endpoint(request: Request): + print("STAGE 1") payload = await request.json() + print("STAGE 1.1") await authenticate_user(request) - + print("STAGE 2") authCookie = payload["cookie"] result = processor.process_handler(int(payload["dgId"]), int(payload["newDgId"]), authCookie, payload["updateType"], payload["savedFilePath"], payload["patchPayload"], payload["sessionId"]) + print("STAGE 3") if result: return result else: @@ -64,12 +67,17 @@ async def forward_request(request: Request, response: Response): validator_response = validator.process_request(int(payload["dgId"]), int(payload["newDgId"]), payload["cookie"], payload["updateType"], payload["savedFilePath"], payload["patchPayload"]) forward_payload = {} + print("@@@@") + print(payload) + print("------") + print(validator_response) + print("@@@@") forward_payload["dgId"] = int(payload["dgId"]) forward_payload["newDgId"] = int(payload["newDgId"]) forward_payload["updateType"] = payload["updateType"] forward_payload["patchPayload"] = payload["patchPayload"] forward_payload["savedFilePath"] = payload["savedFilePath"] - forward_payload["sessionId"] = validator_response["sessionId"] if validator_response["sessionId"] is not None else 0 + forward_payload["sessionId"] = validator_response['response']["sessionId"] if validator_response['response']["sessionId"] is not None else 0 headers = { 'cookie': payload["cookie"], @@ -83,6 +91,9 @@ async def forward_request(request: Request, response: Response): forward_payload["validationErrors"] = [] try: + print("#####") + print(forward_payload) + print("#####") forward_response = requests.post(VALIDATION_CONFIRMATION_URL, json=forward_payload, headers=headers) forward_response.raise_for_status() From 0edfd768ee9e854041ddc770c8ffdd01fc592f11 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Thu, 15 Aug 2024 17:29:09 +0530 Subject: [PATCH 481/582] script , change --- DSL/CronManager/script/data_processor_exec.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DSL/CronManager/script/data_processor_exec.sh b/DSL/CronManager/script/data_processor_exec.sh index ca52a66d..21b9d0c9 100644 --- a/DSL/CronManager/script/data_processor_exec.sh +++ b/DSL/CronManager/script/data_processor_exec.sh @@ -15,7 +15,7 @@ payload=$(cat < Date: Thu, 15 Aug 2024 23:27:56 +0530 Subject: [PATCH 482/582] addeding minor updates to model training endpoints --- model_trainer/constants.py | 2 + model_trainer/inference.py | 111 +++++++++++++++++++++++++++++---- model_trainer/model_trainer.py | 6 +- 3 files changed, 103 insertions(+), 16 deletions(-) diff --git a/model_trainer/constants.py b/model_trainer/constants.py index 88d832e6..29b39d1e 100644 --- a/model_trainer/constants.py +++ b/model_trainer/constants.py @@ -4,3 +4,5 @@ URL_HIERARCHY = "http://ruuter-private:8088/classifier/datasetgroup/group/metadata" URL_MODEL= "http://ruuter-private:8088/classifier/datamodel/metadata" + +URL_DEPLOY = "http://localhost:8088/classifier/datamodel/deployment/{deployment_platform}/update" diff --git a/model_trainer/inference.py b/model_trainer/inference.py index 86205037..2befa731 100644 --- a/model_trainer/inference.py +++ b/model_trainer/inference.py @@ -2,12 +2,21 @@ import pickle import torch import os -import json +import torch.nn.functional as F +from transformers import logging +import warnings +warnings.filterwarnings("ignore", message="Some weights of the model checkpoint were not used when initializing") +logging.set_verbosity_error() class InferencePipeline: - def __init__(self, hierarchy_path, model_name, path_models_folder,path_label_encoder, path_classification_folder, models): + def __init__(self, hierarchy_file, model_name, results_folder): + self.hierarchy_file = hierarchy_file + + with open(f"{results_folder}/models_dets.pkl", 'rb') as file: + self.models = pickle.load(file) + if model_name == 'distil-bert': self.base_model = DistilBertForSequenceClassification.from_pretrained('distilbert-base-uncased') self.tokenizer = DistilBertTokenizer.from_pretrained('distilbert-base-uncased') @@ -21,30 +30,26 @@ def __init__(self, hierarchy_path, model_name, path_models_folder,path_label_enc self.base_model = BertForSequenceClassification.from_pretrained('bert-base-uncased') self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') - self.models = models self.model_name = model_name - label_encoder_names = os.listdir(path_label_encoder) + label_encoder_names = os.listdir(f"{results_folder}/saved_label_encoders") label_encoder_names = sorted(label_encoder_names, key=lambda x: int(x.split('_')[-1].split('.')[0])) self.label_encoder_dict = {} for i in range(len(label_encoder_names)): - with open(os.path.join(path_label_encoder,label_encoder_names[i]), 'rb') as file: + with open(os.path.join(f"{results_folder}/saved_label_encoders",label_encoder_names[i]), 'rb') as file: self.label_encoder_dict[i] = pickle.load(file) - model_names = os.listdir(path_models_folder) + model_names = os.listdir(f"{results_folder}/saved_models") model_names = sorted(model_names, key=lambda x: int(x.split('_')[-1].split('.')[0])) self.models_dict = {} for i in range(len(model_names)): - self.models_dict[i] = torch.load(os.path.join(path_models_folder,model_names[i])) + self.models_dict[i] = torch.load(os.path.join(f"{results_folder}/saved_models",model_names[i])) - classification_model_names = os.listdir(path_classification_folder) + classification_model_names = os.listdir(f"{results_folder}/classifiers") classification_model_names = sorted(classification_model_names, key=lambda x: int(x.split('_')[-1].split('.')[0])) self.classification_models_dict = {} for i in range(len(classification_model_names)): - self.classification_models_dict[i] = torch.load(os.path.join(path_classification_folder,classification_model_names[i])) - - with open(hierarchy_path, 'r', encoding='utf-8') as file: - self.hierarchy_file = json.load(file) + self.classification_models_dict[i] = torch.load(os.path.join(f"{results_folder}/classifiers",classification_model_names[i])) def find_index(self, data, search_dict): for index, d in enumerate(data): @@ -57,6 +62,7 @@ def predict_class(self,text_input): inputs.to(self.device) inputs = {key: val.to(self.device) for key, val in inputs.items()} predicted_classes = [] + probabilities = [] self.base_model.to(self.device) i = 0 data = self.hierarchy_file['classHierarchy'] @@ -72,6 +78,7 @@ def predict_class(self,text_input): if self.model_name == 'distil-bert': self.base_model.classifier = DistilBertForSequenceClassification.from_pretrained('distilbert-base-uncased', num_labels=num_labels).classifier self.base_model.distilbert.transformer.layer[-2:].load_state_dict(self.models_dict[model_num]) + elif self.model_name == 'roberta': self.base_model.classifier = XLMRobertaForSequenceClassification.from_pretrained('xlm-roberta-base', num_labels=num_labels).classifier self.base_model.roberta.encoder.layer[-2:].load_state_dict(self.models_dict[model_num]) @@ -84,7 +91,12 @@ def predict_class(self,text_input): self.base_model.to(self.device) with torch.no_grad(): outputs = self.base_model(**inputs) + probability = F.softmax(outputs.logits, dim=1) predictions = torch.argmax(outputs.logits, dim=1) + predicted_probabilities = probability.gather(1, predictions.unsqueeze(1)).squeeze() + if int(predicted_probabilities.cpu().item()*100)<80: + return [],[] + probabilities.append(int(predicted_probabilities.cpu().item()*100)) predicted_label = label_encoder.inverse_transform(predictions.cpu().numpy()) predicted_classes.append(predicted_label[0]) @@ -111,4 +123,77 @@ def predict_class(self,text_input): data = data['subclasses'] - return predicted_classes \ No newline at end of file + return predicted_classes, probabilities + + def user_corrected_probabilities(self, text_input, user_classes): + inputs = self.tokenizer(text_input, truncation=True, padding=True, return_tensors='pt') + inputs.to(self.device) + inputs = {key: val.to(self.device) for key, val in inputs.items()} + predicted_classes = [] + user_class_probabilities = [] + real_predicted_probabilities = [] + self.base_model.to(self.device) + i = 0 + data = self.hierarchy_file['classHierarchy'] + parent = 1 + + for i in range(len(user_classes)): + current_classes = {parent: [d['class'] for d in data]} + model_num = self.find_index(self.models, current_classes) + if model_num is None: + break + label_encoder = self.label_encoder_dict[model_num] + num_labels = len(label_encoder.classes_) + + if self.model_name == 'distil-bert': + self.base_model.classifier = DistilBertForSequenceClassification.from_pretrained('distilbert-base-uncased', num_labels=num_labels).classifier + self.base_model.distilbert.transformer.layer[-2:].load_state_dict(self.models_dict[model_num]) + + elif self.model_name == 'roberta': + self.base_model.classifier = XLMRobertaForSequenceClassification.from_pretrained('xlm-roberta-base', num_labels=num_labels).classifier + self.base_model.roberta.encoder.layer[-2:].load_state_dict(self.models_dict[model_num]) + + elif self.model_name == 'bert': + self.base_model.classifier = BertForSequenceClassification.from_pretrained('bert-base-uncased', num_labels=num_labels).classifier + self.base_model.base_model.encoder.layer[-2:].load_state_dict(self.models_dict[model_num]) + + self.base_model.classifier.load_state_dict(self.classification_models_dict[model_num]) + self.base_model.to(self.device) + + with torch.no_grad(): + outputs = self.base_model(**inputs) + probability = F.softmax(outputs.logits, dim=1) + + user_class_index = label_encoder.transform([user_classes[i]])[0] + + user_class_probability = probability[:, user_class_index].item() + user_class_probabilities.append(int(user_class_probability * 100)) + + predictions = torch.argmax(outputs.logits, dim=1) + real_predicted_probabilities.append(int(probability.gather(1, predictions.unsqueeze(1)).squeeze().cpu().item() * 100)) + + predicted_label = label_encoder.inverse_transform(predictions.cpu().numpy()) + predicted_classes.append(predicted_label[0]) + + data = next((item for item in data if item['class'] == user_classes[i]), None) + parent = user_classes[i] + if not data: + break + + while data['subclasses'] and len(data['subclasses']) <= 1: + if data['subclasses']: + parent = data['subclasses'][0]['class'] + data = data['subclasses'][0] + else: + data = None + break + + if not data['subclasses']: + break + + if not data: + break + + data = data['subclasses'] + + return user_class_probabilities diff --git a/model_trainer/model_trainer.py b/model_trainer/model_trainer.py index 938c0d31..218d7e62 100644 --- a/model_trainer/model_trainer.py +++ b/model_trainer/model_trainer.py @@ -6,7 +6,7 @@ import pickle import shutil from s3_ferry import S3Ferry -from constants import URL_MODEL +from constants import URL_MODEL, URL_DEPLOY class ModelTrainer: def __init__(self) -> None: @@ -88,7 +88,7 @@ def train(self): DeploymentPlatform = self.model_details['response']['data'][0]['deploymentEnv'] - url = f"http://localhost:8088/classifier/datamodel/deployment/{DeploymentPlatform}/update" + deploy_url = URL_DEPLOY.format(deployment_platform = DeploymentPlatform) if self.oldModelId is not None: @@ -108,4 +108,4 @@ def train(self): "bestModelName":model_name } - response = requests.post(url, json=payload) + response = requests.post(deploy_url, json=payload) From 0a43bf26539bf4ed6d1041c47a5f4b303a4acd1d Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Fri, 16 Aug 2024 11:59:57 +0530 Subject: [PATCH 483/582] translations added --- .../molecules/DataModelForm/index.tsx | 21 +++- .../pages/DataModels/ConfigureDataModel.tsx | 102 +++++++---------- GUI/src/pages/DataModels/CreateDataModel.tsx | 104 +++++++++--------- GUI/translations/en/common.json | 33 +++++- 4 files changed, 138 insertions(+), 122 deletions(-) diff --git a/GUI/src/components/molecules/DataModelForm/index.tsx b/GUI/src/components/molecules/DataModelForm/index.tsx index aeedbeb3..2e8cc018 100644 --- a/GUI/src/components/molecules/DataModelForm/index.tsx +++ b/GUI/src/components/molecules/DataModelForm/index.tsx @@ -48,7 +48,8 @@ const DataModelForm: FC = ({ />
          - Model Version + {t('dataModels.dataModelForm.modelVersion')}{' '} +
          ) : ( @@ -58,9 +59,11 @@ const DataModelForm: FC = ({
          )} - {createOptions && !isLoading ? ( + {dataModel && createOptions && !isLoading ? (
          -
          Select Dataset Group
          +
          + {t('dataModels.dataModelForm.datasetGroup')}{' '} +
          = ({ />
          -
          Select Base Models
          +
          + {t('dataModels.dataModelForm.baseModels')}{' '} +
          = ({ />
          -
          Select Deployment Platform
          +
          + {t('dataModels.dataModelForm.deploymentPlatform')}{' '} +
          = ({ />
          -
          Select Maturity Label
          +
          + {t('dataModels.dataModelForm.maturityLabel')}{' '} +
          = ({ id, availableProdModels, }) => { + const { t } = useTranslation(); const { open, close } = useDialog(); const navigate = useNavigate(); const [enabled, setEnabled] = useState(true); @@ -103,27 +105,26 @@ const ConfigureDataModel: FC = ({ deploymentEnv: payload.platform, baseModels: payload.baseModels, maturityLabel: payload.maturity, - updateType:updateType, + updateType: updateType, }; if (updateType) { if (availableProdModels?.includes(dataModel.platform)) { open({ - title: 'Warning: Replace Production Model', - content: - 'Adding this model to production will replace the current production model. Are you sure you want to proceed?', + title: t('dataModels.createDataModel.replaceTitle'), + content: t('dataModels.createDataModel.replaceDesc'), footer: (
          ), @@ -138,13 +139,8 @@ const ConfigureDataModel: FC = ({ mutationFn: (data: UpdatedDataModelPayload) => updateDataModel(data), onSuccess: async () => { open({ - title: 'Changes Saved Successfully', - content: ( -

          - You have successfully saved the changes. You can view the data model - in the "All Data Models" view. -

          - ), + title: t('dataModels.configureDataModel.saveChangesTitile'), + content:

          {t('dataModels.configureDataModel.saveChangesDesc')}

          , footer: (
          {' '}
          ), @@ -170,13 +166,8 @@ const ConfigureDataModel: FC = ({ }, onError: () => { open({ - title: 'Error Updating Data Model', - content: ( -

          - There was an issue updating the data model. Please try again. If the - problem persists, contact support for assistance. -

          - ), + title: t('dataModels.configureDataModel.updateErrorTitile'), + content:

          {t('dataModels.configureDataModel.updateErrorDesc')}

          , }); }, }); @@ -188,31 +179,27 @@ const ConfigureDataModel: FC = ({ dataModel.platform === Platform.PINAL ) { open({ - title: 'Cannot Delete Model', - content: ( -

          - The model cannot be deleted because it is currently in production. - Please escalate another model to production before proceeding to - delete this model. -

          - ), + title: t('dataModels.configureDataModel.deleteErrorTitle'), + content:

          {t('dataModels.configureDataModel.deleteErrorDesc')}

          , footer: (
          + -
          ), }); } else { open({ - title: 'Are you sure?', + title: t('dataModels.configureDataModel.deleteConfirmation'), content: ( -

          Confirm that you are wish to delete the following data model

          +

          {t('dataModels.configureDataModel.deleteConfirmationDesc')}

          ), footer: (
          @@ -220,13 +207,13 @@ const ConfigureDataModel: FC = ({ appearance={ButtonAppearanceTypes.SECONDARY} onClick={close} > - Cancel + {t('global.cancel')}
          ), @@ -242,12 +229,9 @@ const ConfigureDataModel: FC = ({ }, onError: () => { open({ - title: 'Error Deleting Data Model', + title: t('dataModels.configureDataModel.deleteModalErrorTitle'), content: ( -

          - There was an issue deleting the data model. Please try again. If the - problem persists, contact support for assistance. -

          +

          {t('dataModels.configureDataModel.deleteModalErrorDesc')}

          ), }); }, @@ -261,12 +245,9 @@ const ConfigureDataModel: FC = ({ }, onError: () => { open({ - title: 'Error Deleting Data Model', + title: t('dataModels.configureDataModel.retrainDataModalErrorTitle'), content: ( -

          - There was an issue retraining the data model. Please try again. If - the problem persists, contact support for assistance. -

          +

          {t('dataModels.configureDataModel.retrainDataModalErrorDesc')}

          ), }); }, @@ -279,7 +260,9 @@ const ConfigureDataModel: FC = ({ navigate(0)}> -
          Configure Data Model
          +
          + {t('dataModels.configureDataModel.title')} +
          @@ -291,16 +274,13 @@ const ConfigureDataModel: FC = ({ }} >
          -

          - Model updated. Please initiate retraining to continue benefiting - from the latest improvements. -

          +

          {t('dataModels.configureDataModel.retrainCard')}

          @@ -328,39 +308,41 @@ const ConfigureDataModel: FC = ({ }} >
          ), }) } > - Retrain + {t('dataModels.configureDataModel.retrain')} + + -
          ); }; -export default ConfigureDataModel; +export default ConfigureDataModel; \ No newline at end of file diff --git a/GUI/src/pages/DataModels/CreateDataModel.tsx b/GUI/src/pages/DataModels/CreateDataModel.tsx index f34994d7..85d27820 100644 --- a/GUI/src/pages/DataModels/CreateDataModel.tsx +++ b/GUI/src/pages/DataModels/CreateDataModel.tsx @@ -57,8 +57,8 @@ const CreateDataModel: FC = () => { true ), { - onSuccess:(data)=>{ - setAvailableProdModels(extractedArray(data?.data,"deploymentEnv")) + onSuccess: (data) => { + setAvailableProdModels(extractedArray(data?.data, 'deploymentEnv')); }, } ); @@ -99,61 +99,60 @@ const CreateDataModel: FC = () => { maturityLabel: dataModel.maturity, }; - if ( - availableProdModels?.includes(dataModel.platform??"") - ) { - open({ - title: 'Warning: Replace Production Model', - content: -
          - Adding this model to production will replace the current production model. Are you sure you want to proceed? - {!integrationStatus[`${dataModel.platform}_connection_status`] &&
          {`${dataModel.platform} integration is currently disabled, therefore the model wouldn't recieve any inputs or make any predictions`}
          } - -
          , - footer: ( -
          - - -
          - ), - }); - }else{ + if (availableProdModels?.includes(dataModel.platform ?? '')) { + open({ + title: t('dataModels.createDataModel.replaceTitle'), + content: ( +
          + {t('dataModels.createDataModel.replaceDesc')} + {!integrationStatus[ + `${dataModel.platform}_connection_status` + ] && ( +
          + {t('dataModels.createDataModel.replaceWarning', { + platform: dataModel.platform, + })} +
          + )} +
          + ), + footer: ( +
          + + +
          + ), + }); + } else { createDataModelMutation.mutate(payload); - - } - + } } }; const createDataModelMutation = useMutation({ - mutationFn: (data:CreateDataModelPayload) => createDataModel(data), + mutationFn: (data: CreateDataModelPayload) => createDataModel(data), onSuccess: async (response) => { open({ - title: 'Data Model Created and Trained', - content: ( -

          - You have successfully created and started training the data model. You can - view it on the data model dashboard. -

          - ), + title: t('dataModels.createDataModel.successTitle'), + content:

          {t('dataModels.createDataModel.successDesc')}

          , footer: (
          - {' '} + {' '}
          ), @@ -161,13 +160,8 @@ const CreateDataModel: FC = () => { }, onError: () => { open({ - title: 'Error Creating Data Model', - content: ( -

          - There was an issue creating or training the data model. Please try - again. If the problem persists, contact support for assistance. -

          - ), + title: t('dataModels.createDataModel.errorTitle'), + content:

          {t('dataModels.createDataModel.errorDesc')}

          , }); }, }); @@ -180,7 +174,7 @@ const CreateDataModel: FC = () => { -
          Create Data Model
          +
          {t('dataModels.createDataModel.title')}
          { background: 'white', }} > - +
          ); }; -export default CreateDataModel; +export default CreateDataModel; \ No newline at end of file diff --git a/GUI/translations/en/common.json b/GUI/translations/en/common.json index f779c297..0232d36e 100644 --- a/GUI/translations/en/common.json +++ b/GUI/translations/en/common.json @@ -375,8 +375,37 @@ "successDesc": " You have successfully created and trained the data model. You can view it on the data model dashboard.", "viewAll": "View All Data Models", "errorTitle": "Error Creating Data Model", - "errorDesc": " There was an issue creating or training the data model. Please try again. If the problem persists, contact support for assistance." - } + "errorDesc": " There was an issue creating or training the data model. Please try again. If the problem persists, contact support for assistance.", + "replaceWarning": "{{platform}} integration is currently disabled, therefore the model wouldn't recieve any inputs or make any predictions" + }, + "configureDataModel": { + "saveChangesTitile": "Changes Saved Successfully", + "saveChangesDesc": "You have successfully saved the changes. You can view the data model in the \"All Data Models\" view.", + "updateErrorTitile": "Error Updating Data Model", + "updateErrorDesc": "There was an issue updating the data model. Please try again. If the problem persists, contact support for assistance.", + "deleteErrorTitle": "Cannot Delete Model", + "deleteErrorDesc": "The model cannot be deleted because it is currently in production. Please escalate another model to production before proceeding to delete this model.", + "deleteConfirmation": "Are you sure?", + "deleteConfirmationDesc": "Confirm that you are wish to delete the following data model", + "deleteModalErrorTitle": "Error Deleting Data Model", + "deleteModalErrorDesc": "There was an issue deleting the data model. Please try again. If the problem persists, contact support for assistance.", + "retrainDataModalErrorTitle": "Error retraining data model", + "retrainDataModalErrorDesc":"There was an issue retraining the data model. Please try again. If the problem persists, contact support for assistance." , + "title": "Configure Data Model", + "retrainCard": "Model updated. Please initiate retraining to continue benefiting from the latest improvements.", + "retrain": "Retrain", + "deleteModal": "Delete Model", + "confirmRetrain": "Confirm Retrain Model", + "confirmRetrainDesc": "Are you sure you want to retrain this model?", + "save": "Save Changes" + }, + "dataModelForm": { + "modelVersion": "Model Version", + "datasetGroup": "Select Dataset Group", + "baseModels": "Select Base Models", + "deploymentPlatform": "Select Deployment Platform", + "maturityLabel": "Select Maturity Label" + } }, "trainingSessions": { "title": "Training Sessions", From 3c0b8f17d50a2185b5b7789aeea54638f4b2d165 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Fri, 16 Aug 2024 12:01:41 +0530 Subject: [PATCH 484/582] scripts lf --- DSL/CronManager/script/data_processor_exec.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DSL/CronManager/script/data_processor_exec.sh b/DSL/CronManager/script/data_processor_exec.sh index ca52a66d..21b9d0c9 100644 --- a/DSL/CronManager/script/data_processor_exec.sh +++ b/DSL/CronManager/script/data_processor_exec.sh @@ -15,7 +15,7 @@ payload=$(cat < Date: Fri, 16 Aug 2024 12:11:33 +0530 Subject: [PATCH 485/582] test inference --- test_inference/Dockerfile | 19 ++++ test_inference/constants.py | 20 ++++ test_inference/docker-compose.yml | 47 ++++++++++ test_inference/requirements.txt | 32 +++++++ test_inference/s3_ferry.py | 21 +++++ test_inference/test_inference.py | 24 +++++ test_inference/test_inference_api.py | 114 +++++++++++++++++++++++ test_inference/test_inference_wrapper.py | 45 +++++++++ test_inference/utils.py | 46 +++++++++ 9 files changed, 368 insertions(+) create mode 100644 test_inference/Dockerfile create mode 100644 test_inference/constants.py create mode 100644 test_inference/docker-compose.yml create mode 100644 test_inference/requirements.txt create mode 100644 test_inference/s3_ferry.py create mode 100644 test_inference/test_inference.py create mode 100644 test_inference/test_inference_api.py create mode 100644 test_inference/test_inference_wrapper.py create mode 100644 test_inference/utils.py diff --git a/test_inference/Dockerfile b/test_inference/Dockerfile new file mode 100644 index 00000000..856367c3 --- /dev/null +++ b/test_inference/Dockerfile @@ -0,0 +1,19 @@ +FROM python:3.9-slim +RUN addgroup --system appuser && adduser --system --ingroup appuser appuser +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY constants.py . +COPY s3_ferry.py . +COPY test_inference_api.py . +COPY test_inference_wrapper.py . +COPY test_inference.py . +COPY utils.py . + +RUN mkdir -p /shared && chown appuser:appuser /shared && chmod 770 /shared +RUN chown -R appuser:appuser /app +EXPOSE 8010 +USER appuser + +CMD ["uvicorn", "test_inference_api:app", "--host", "0.0.0.0", "--port", "8010"] \ No newline at end of file diff --git a/test_inference/constants.py b/test_inference/constants.py new file mode 100644 index 00000000..c1249b8c --- /dev/null +++ b/test_inference/constants.py @@ -0,0 +1,20 @@ +from pydantic import BaseModel + +class TestDeploymentRequest(BaseModel): + replacementModelId:int + bestBaseModel:str + +class TestInferenceRequest(BaseModel): + modelId:int + text:str + +class DeleteTestRequest(BaseModel): + deleteModelId:int + +S3_DOWNLOAD_FAILED = { + "upload_status": 500, + "operation_successful": False, + "saved_file_path": None, + "reason": "Failed to download from S3" +} + diff --git a/test_inference/docker-compose.yml b/test_inference/docker-compose.yml new file mode 100644 index 00000000..6805db11 --- /dev/null +++ b/test_inference/docker-compose.yml @@ -0,0 +1,47 @@ +version: '3.8' + +services: + init: + image: busybox + command: ["sh", "-c", "chmod -R 777 /shared"] + volumes: + - shared-volume:/shared + + receiver: + build: + context: . + dockerfile: Dockerfile + container_name: file-receiver + volumes: + - shared-volume:/shared + environment: + - UPLOAD_DIRECTORY=/shared + - JIRA_MODEL_DOWNLOAD_DIRECTORY=/shared/models/jira + - OUTLOOK_MODEL_DOWNLOAD_DIRECTORY=/shared/models/outlook + ports: + - "8010:8010" + depends_on: + - init + + api: + image: s3-ferry:latest + container_name: s3-ferry + volumes: + - shared-volume:/shared + env_file: + - config.env + environment: + - S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID} + - S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY} + ports: + - "3000:3000" + depends_on: + - receiver + - init + +volumes: + shared-volume: + +networks: + default: + driver: bridge diff --git a/test_inference/requirements.txt b/test_inference/requirements.txt new file mode 100644 index 00000000..198d4b35 --- /dev/null +++ b/test_inference/requirements.txt @@ -0,0 +1,32 @@ +fastapi==0.111.1 +fastapi-cli==0.0.4 +httpx==0.27.0 +huggingface-hub==0.24.2 +numpy==1.26.4 +pydantic==2.8.2 +pydantic_core==2.20.1 +Pygments==2.18.0 +python-dotenv==1.0.1 +python-multipart==0.0.9 +PyYAML==6.0.1 +regex==2024.7.24 +requests==2.32.3 +rich==13.7.1 +safetensors==0.4.3 +scikit-learn==0.24.2 +sentencepiece==0.2.0 +setuptools==69.5.1 +shellingham==1.5.4 +sniffio==1.3.1 +starlette==0.37.2 +sympy==1.13.1 +tokenizers==0.19.1 +torch==2.4.0 +torchaudio==2.4.0 +torchvision==0.19.0 +tqdm==4.66.4 +transformers==4.43.3 +typer==0.12.3 +typing_extensions==4.12.2 +urllib3==2.2.2 +uvicorn==0.30.3 \ No newline at end of file diff --git a/test_inference/s3_ferry.py b/test_inference/s3_ferry.py new file mode 100644 index 00000000..54b1d46d --- /dev/null +++ b/test_inference/s3_ferry.py @@ -0,0 +1,21 @@ +import requests +from utils import get_s3_payload + +class S3Ferry: + def __init__(self, url): + self.url = url + + def transfer_file(self, destinationFilePath, destinationStorageType, sourceFilePath, sourceStorageType): + print("Transfer File Method Calling") + print(f"Destination Path :{destinationFilePath}", + f"Destination Storage :{destinationStorageType}", + f"Source File Path :{sourceFilePath}", + f"Source Storage Type :{sourceStorageType}", + sep="\n" + ) + payload = get_s3_payload(destinationFilePath, destinationStorageType, sourceFilePath, sourceStorageType) + print(payload) + print(f"url : {self.url}") + response = requests.post(self.url, json=payload) + print(response) + return response diff --git a/test_inference/test_inference.py b/test_inference/test_inference.py new file mode 100644 index 00000000..59022144 --- /dev/null +++ b/test_inference/test_inference.py @@ -0,0 +1,24 @@ +import requests +import os + +GET_OUTLOOK_ACCESS_TOKEN_URL=os.getenv("GET_OUTLOOK_ACCESS_TOKEN_URL") + +class TestModelInference: + def __init__(self): + pass + + def get_class_hierarchy_by_model_id(self, model_id): + try: + get_outlook_access_token_url = GET_OUTLOOK_ACCESS_TOKEN_URL + response = requests.post(get_outlook_access_token_url, json={"modelId": model_id}) + response.raise_for_status() + data = response.json() + + class_hierarchy = data["class_hierarchy"] + return class_hierarchy + except requests.exceptions.RequestException as e: + raise Exception(f"Failed to retrieve the class hierarchy Reason: {e}") + + + + diff --git a/test_inference/test_inference_api.py b/test_inference/test_inference_api.py new file mode 100644 index 00000000..20001bbb --- /dev/null +++ b/test_inference/test_inference_api.py @@ -0,0 +1,114 @@ +from fastapi import FastAPI,HTTPException, Request, BackgroundTasks +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse +import os +from s3_ferry import S3Ferry +from utils import unzip_file, calculate_average_predicted_class_probability, get_inference_success_payload, delete_folder +from constants import S3_DOWNLOAD_FAILED, TestDeploymentRequest, TestInferenceRequest, DeleteTestRequest +from test_inference_wrapper import TestInferenceWrapper +from test_inference import TestModelInference + +app = FastAPI() +testModelInference = TestModelInference() + +app.add_middleware( + CORSMiddleware, + allow_origins = ["*"], + allow_credentials = True, + allow_methods = ["GET", "POST"], + allow_headers = ["*"], +) + +inference_obj = TestInferenceWrapper() + +S3_FERRY_URL = os.getenv("S3_FERRY_URL") +s3_ferry = S3Ferry(S3_FERRY_URL) +RUUTER_PRIVATE_URL = os.getenv("RUUTER_PRIVATE_URL") +TEST_MODEL_DOWNLOAD_DIRECTORY = os.getenv("JIRA_MODEL_DOWNLOAD_DIRECTORY", "/shared/models/test") + +if not os.path.exists(TEST_MODEL_DOWNLOAD_DIRECTORY): + os.makedirs(TEST_MODEL_DOWNLOAD_DIRECTORY) + + +@app.post("/classifier/datamodel/deployment/test/update") +async def download_test_model(request: Request, modelData:TestDeploymentRequest, backgroundTasks: BackgroundTasks): + + saveLocation = f"/models/{modelData.replacementModelId}/{modelData.replacementModelId}.zip" + + try: + local_file_name = f"{modelData.replacementModelId}.zip" + local_file_path = f"/models/test/{local_file_name}" + + # 1. Download the new Model + response = s3_ferry.transfer_file(local_file_path, "FS", saveLocation, "S3") + if response.status_code != 201: + raise HTTPException(status_code = 500, detail = S3_DOWNLOAD_FAILED) + + zip_file_path = os.path.join("..", "shared/models/test", local_file_name) + extract_file_path = os.path.join("..", "shared/models/test") + + # 2. Unzip Model Content + unzip_file(zip_path=zip_file_path, extract_to=extract_file_path) + + backgroundTasks.add_task(os.remove, zip_file_path) + + # 3. Instantiate Munsif's Inference Model + class_hierarchy = testModelInference.get_class_hierarchy_by_model_id(modelData.replacementModelId) + if(class_hierarchy): + + model_path = f"shared/models/test/{modelData.replacementModelId}" + best_model = modelData.bestBaseModel + model_initiate = inference_obj.model_initiate(model_id=modelData.replacementModelId, model_path=model_path, best_performing_model=best_model, class_hierarchy=class_hierarchy) + + if(model_initiate): + return JSONResponse(status_code=200, content={"replacementStatus": 200}) + else: + raise HTTPException(status_code = 500, detail = "Failed to initiate inference object") + else: + raise HTTPException(status_code = 500, detail = "Error in obtaining the class hierarchy") + + except Exception as e: + raise HTTPException(status_code = 500, detail=str(e)) + + +@app.post("/classifier/datamodel/deployment/test/delete") +async def delete_folder_content(request:Request, modelData:DeleteTestRequest): + try: + folder_path = os.path.join("..", "shared", "models", "test", {modelData.deleteModelId}) + delete_folder(folder_path) + + # Stop the model + inference_obj.stop_model(model_id=modelData.deleteModelId) + + delete_success = {"message" : "Model Deleted Successfully!"} + return JSONResponse(status_code = 200, content = delete_success) + + except Exception as e: + raise HTTPException(status_code = 500, detail=str(e)) + + + +@app.post("/classifier/testmodel/test-data") +async def test_inference(request:Request, inferenceData:TestInferenceRequest): + try: + + # Call Inference + predicted_hierarchy, probabilities = inference_obj.inference(model_id=inferenceData.modelId, text=inferenceData.text) + + if (probabilities and predicted_hierarchy): + + # Calculate Average Predicted Class Probability + average_probability = calculate_average_predicted_class_probability(probabilities) + + # Build request payload for inference/create endpoint + inference_succcess_payload = get_inference_success_payload(predictedClasses=predicted_hierarchy, averageConfidence=average_probability, predictedProbabilities=probabilities) + + return JSONResponse(status_code=200, content={inference_succcess_payload}) + + + else: + raise HTTPException(status_code = 500, detail="Failed to call inference") + + + except Exception as e: + raise HTTPException(status_code = 500, detail=str(e)) \ No newline at end of file diff --git a/test_inference/test_inference_wrapper.py b/test_inference/test_inference_wrapper.py new file mode 100644 index 00000000..7fd56cbb --- /dev/null +++ b/test_inference/test_inference_wrapper.py @@ -0,0 +1,45 @@ +from typing import List, Dict + +class Inference: + + def __init__(self) -> None: + pass + + def predict(text:str): + pass + + def user_corrected_probabilities(text:str, corrected_labels:List[str]): + pass + + +class TestInferenceWrapper: + def __init__(self) -> None: + self.model_dictionary: Dict[int, Inference] = {} + + + + def model_initiate(self, model_id: int, model_path: str, best_performing_model: str, class_hierarchy: list) -> bool: + try: + new_model = Inference(model_path, best_performing_model, class_hierarchy) + self.model_dictionary[model_id] = new_model + return True + except Exception as e: + raise Exception(f"Failed to instantiate the Inference Pipeline. Reason: {e}") + + def inference(self, text: str, model_id: int): + try: + if model_id in self.model_dictionary: + predicted_labels = None + probabilities = None + model = self.model_dictionary[model_id] + predicted_labels, probabilities = model.predict(text) + return predicted_labels, probabilities + else: + raise Exception(f"Model with ID {model_id} not found") + except Exception as e: + raise Exception(f"Failed to call the inference. Reason: {e}") + + + def stop_model(self, model_id: int) -> None: + if model_id in self.models: + del self.models[model_id] diff --git a/test_inference/utils.py b/test_inference/utils.py new file mode 100644 index 00000000..bc7e95b2 --- /dev/null +++ b/test_inference/utils.py @@ -0,0 +1,46 @@ +import zipfile +from typing import List +import os +import shutil + +def calculate_average_predicted_class_probability(class_probabilities:List[float]): + + total_probability = sum(class_probabilities) + average_probability = total_probability / len(class_probabilities) + + return average_probability + + +def get_s3_payload(destinationFilePath:str, destinationStorageType:str, sourceFilePath:str, sourceStorageType:str): + S3_FERRY_PAYLOAD = { + "destinationFilePath": destinationFilePath, + "destinationStorageType": destinationStorageType, + "sourceFilePath": sourceFilePath, + "sourceStorageType": sourceStorageType + } + return S3_FERRY_PAYLOAD + + +def get_inference_success_payload(predictedClasses:List[str], averageConfidence:float, predictedProbabilities:List[float] ): + INFERENCE_SUCCESS_PAYLOAD = { + "predictedClasses":predictedClasses, + "averageConfidence":averageConfidence, + "predictedProbabilities": predictedProbabilities +} + + return INFERENCE_SUCCESS_PAYLOAD + + +def unzip_file(zip_path, extract_to): + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + zip_ref.extractall(extract_to) + + +def delete_folder(folder_path: str): + try: + if os.path.isdir(folder_path): + shutil.rmtree(folder_path) + else: + raise FileNotFoundError(f"The path {folder_path} is not a directory.") + except Exception as e: + raise Exception(f"Failed to delete the folder {folder_path}. Reason: {e}") \ No newline at end of file From 790a53653639609948b6fa91933a4ce72458dc60 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 16 Aug 2024 12:11:47 +0530 Subject: [PATCH 486/582] model inference updates --- model_inference/Dockerfile | 1 + model_inference/constants.py | 20 ++ model_inference/inference_wrapper.py | 61 +++++- model_inference/model_inference.py | 127 +++++++++++ model_inference/model_inference_api.py | 290 +++++++++++++++---------- model_inference/utils.py | 38 +++- 6 files changed, 412 insertions(+), 125 deletions(-) create mode 100644 model_inference/model_inference.py diff --git a/model_inference/Dockerfile b/model_inference/Dockerfile index 56a42f22..5ff35c48 100644 --- a/model_inference/Dockerfile +++ b/model_inference/Dockerfile @@ -7,6 +7,7 @@ RUN pip install --no-cache-dir -r requirements.txt COPY constants.py . COPY inference_wrapper.py . COPY model_inference_api.py . +COPY model_inference.py . COPY s3_ferry.py . COPY utils.py . diff --git a/model_inference/constants.py b/model_inference/constants.py index 59fdfbb6..4ac14f33 100644 --- a/model_inference/constants.py +++ b/model_inference/constants.py @@ -1,6 +1,26 @@ +from pydantic import BaseModel +from typing import List + S3_DOWNLOAD_FAILED = { "upload_status": 500, "operation_successful": False, "saved_file_path": None, "reason": "Failed to download from S3" } + +class UpdateRequest(BaseModel): + modelId: int + replaceDeployment:bool + replaceDeploymentPlatform:str + bestBaseModel:str + +class OutlookInferenceRequest(BaseModel): + inputId:int + inputText:str + finalFolderId:int + mailId:str + +class JiraInferenceRequest(BaseModel): + inputId:int + inputText:str + finalLabels:List[str] \ No newline at end of file diff --git a/model_inference/inference_wrapper.py b/model_inference/inference_wrapper.py index d11a44c1..ba8b3ef6 100644 --- a/model_inference/inference_wrapper.py +++ b/model_inference/inference_wrapper.py @@ -1,46 +1,85 @@ +from typing import List class Inference: def __init__(self) -> None: pass def predict(text:str): pass + + def user_corrected_probabilities(text:str, corrected_labels:List[str]): + pass class InferenceWrapper: def __init__(self) -> None: self.active_jira_model = None self.active_outlook_model = None + self.active_jira_model_id = None + self.active_outlook_model_id = None - def model_swapping(self, model_path:str, best_performing_model:str, deployment_platform:str): + def model_swapping(self, model_path:str, best_performing_model:str, deployment_platform:str, class_hierarchy:list, model_id:int): try: if(deployment_platform == "jira"): - temp_jira_model = Inference(model_path, best_performing_model) + temp_jira_model = Inference(model_path, best_performing_model, class_hierarchy) self.active_jira_model = temp_jira_model + self.active_jira_model_id = model_id return True elif(deployment_platform == "outlook"): - temp_outlook_model = Inference(model_path, best_performing_model) + temp_outlook_model = Inference(model_path, best_performing_model, class_hierarchy) self.active_outlook_model = temp_outlook_model + self.active_outlook_model_id = model_id return True except Exception as e: raise Exception(f"Failed to instantiate the Inference Pipeline. Reason: {e}") def inference(self, text:str, deployment_platform:str): - result = [] - if(deployment_platform == "jira" and self.active_jira_model): - result = self.active_jira_model.predict(text) + try: + predicted_labels = None + probabilities = None + if(deployment_platform == "jira" and self.active_jira_model): + predicted_labels, probabilities = self.active_jira_model.predict(text) - if(deployment_platform == "outlook" and self.active_outlook_model): - result = self.active_outlook_model.predict(text) - - return result + if(deployment_platform == "outlook" and self.active_outlook_model): + predicted_labels, probabilities = self.active_outlook_model.predict(text) + + return predicted_labels, probabilities + + except Exception as e: + raise Exception(f"Failed to call the inference. Reason: {e}") def stop_model(self,deployment_platform:str): if(deployment_platform == "jira"): self.active_jira_model = None + self.active_jira_model_id = None if(deployment_platform == "outlook"): self.active_outlook_model = None + self.active_outlook_model_id = None + + + def get_model_id(self, deployment_platform:str): + model_id = None + if(deployment_platform == "jira" and self.active_jira_model): + model_id = self.active_jira_model_id + + if(deployment_platform == "outlook" and self.active_outlook_model): + model_id = self.active_outlook_model_id + + return model_id + - \ No newline at end of file + def get_corrected_probabilities(self, text:str, corrected_labels:List[str] , deployment_platform:str): + try: + corrected_probabilities = None + if(deployment_platform == "jira" and self.active_jira_model): + corrected_probabilities = self.active_jira_model.user_corrected_probabilities(text, corrected_labels) + + if(deployment_platform == "outlook" and self.active_outlook_model): + corrected_probabilities = self.active_outlook_model.user_corrected_probabilities(text, corrected_labels) + + return corrected_probabilities + + except Exception as e: + raise Exception(f"Failed to retrieve corrected probabilities from the inference pipeline. Reason: {e}") \ No newline at end of file diff --git a/model_inference/model_inference.py b/model_inference/model_inference.py new file mode 100644 index 00000000..b9b556dd --- /dev/null +++ b/model_inference/model_inference.py @@ -0,0 +1,127 @@ +import requests +import os + +GET_INFERENCE_DATASET_EXIST_URL = os.getenv("GET_INFERENCE_DATASET_EXIST_URL") +CREATE_INFERENCE_URL=os.getenv("CREATE_INFERENCE_URL") +UPDATE_INFERENCE_URL=os.getenv("UPDATE_INFERENCE_URL") +CLASS_HIERARCHY_VALIDATION_URL=os.getenv("CLASS_HIERARCHY_VALIDATION_URL") +GET_OUTLOOK_ACCESS_TOKEN_URL=os.getenv("GET_OUTLOOK_ACCESS_TOKEN_URL") +BUILD_CORRECTED_FOLDER_HIERARCHY_URL = os.getenv("BUILD_CORRECTED_FOLDER_HIERARCHY_URL") +FIND_FINAL_FOLDER_ID_URL = os.getenv("FIND_FINAL_FOLDER_ID_URL") + +class ModelInference: + def __init__(self): + pass + + def get_class_hierarchy_by_model_id(self, model_id): + try: + get_outlook_access_token_url = GET_OUTLOOK_ACCESS_TOKEN_URL + response = requests.post(get_outlook_access_token_url, json={"modelId": model_id}) + response.raise_for_status() + data = response.json() + + class_hierarchy = data["class_hierarchy"] + return class_hierarchy + except requests.exceptions.RequestException as e: + raise Exception(f"Failed to retrieve the class hierarchy Reason: {e}") + + + + def validate_class_hierarchy(self, class_hierarchy, model_id): + try: + validate_class_hierarchy_url = CLASS_HIERARCHY_VALIDATION_URL + response = requests.post(validate_class_hierarchy_url, json={"class_hierarchy": class_hierarchy, "modelId": model_id}) + response.raise_for_status() + data = response.json() + + is_valid = data["isValid"] + return is_valid + except requests.exceptions.RequestException as e: + raise Exception(f"Failed to validate the class hierarchy. Reason: {e}") + + + + def get_class_hierarchy_and_validate(self, model_id): + try: + class_hierarchy = self.get_class_hierarchy_by_model_id(model_id) + if class_hierarchy: + is_valid = self.validate_class_hierarchy(class_hierarchy, model_id) + return is_valid, class_hierarchy + + return False, None + + except requests.exceptions.RequestException as e: + raise Exception(f"Failed to retrieve and validate the class hierarchy. Reason: {e}") + + + + def check_inference_data_exists(self, input_id): + try: + check_inference_data_exists_url = GET_INFERENCE_DATASET_EXIST_URL.replace("inferenceInputId",str(input_id)) + response = requests.get(check_inference_data_exists_url) + response.raise_for_status() + data = response.json() + + is_exist = data["exist"] + return is_exist + except requests.exceptions.RequestException as e: + raise Exception(f"Failed to validate the class hierarchy. Reason: {e}") + + + def build_corrected_folder_hierarchy(self, final_folder_id, model_id): + try: + build_corrected_folder_hierarchy_url = BUILD_CORRECTED_FOLDER_HIERARCHY_URL + response = requests.get(build_corrected_folder_hierarchy_url, json={"folderId": final_folder_id, "modelId": model_id}) + response.raise_for_status() + data = response.json() + + folder_hierarchy = data["folder_hierarchy"] + return folder_hierarchy + except requests.exceptions.RequestException as e: + raise Exception(f"Failed to validate the class hierarchy. Reason: {e}") + + + def find_final_folder_id(self, flattened_folder_hierarchy, model_id): + try: + find_final_folder_id_url = FIND_FINAL_FOLDER_ID_URL + response = requests.get(find_final_folder_id_url, json={"hierarchy":flattened_folder_hierarchy , "modelId": model_id}) + response.raise_for_status() + data = response.json() + + final_folder_id = data["folder_id"] + return final_folder_id + except requests.exceptions.RequestException as e: + raise Exception(f"Failed to validate the class hierarchy. Reason: {e}") + + + + def update_inference(self, payload): + try: + update_inference_url = UPDATE_INFERENCE_URL + response = requests.get(update_inference_url, json=payload) + response.raise_for_status() + data = response.json() + + is_success = data["operationSuccessful"] + return is_success + except requests.exceptions.RequestException as e: + raise Exception(f"Failed to call update inference. Reason: {e}") + + + def create_inference(self, payload): + + try: + create_inference_url = CREATE_INFERENCE_URL + response = requests.get(create_inference_url, json=payload) + response.raise_for_status() + data = response.json() + + is_success = data["operationSuccessful"] + return is_success + except requests.exceptions.RequestException as e: + raise Exception(f"Failed to call create inference. Reason: {e}") + + + + + diff --git a/model_inference/model_inference_api.py b/model_inference/model_inference_api.py index 37485e59..b52485c5 100644 --- a/model_inference/model_inference_api.py +++ b/model_inference/model_inference_api.py @@ -3,14 +3,13 @@ from fastapi.responses import JSONResponse import os from s3_ferry import S3Ferry -from utils import unzip_file, clear_folder_contents -from pydantic import BaseModel -from constants import S3_DOWNLOAD_FAILED -import requests +from utils import unzip_file, clear_folder_contents, calculate_average_predicted_class_probability, get_inference_create_payload, get_inference_update_payload +from constants import S3_DOWNLOAD_FAILED, JiraInferenceRequest, OutlookInferenceRequest, UpdateRequest from inference_wrapper import InferenceWrapper - +from model_inference import ModelInference app = FastAPI() +modelInference = ModelInference() app.add_middleware( CORSMiddleware, @@ -20,18 +19,6 @@ allow_headers = ["*"], ) -class UpdateRequest(BaseModel): - modelId: str - replaceDeployment:bool - replaceDeploymentPlatform:str - bestModelName:str - -class OutlookInferenceRequest(BaseModel): - inputId:int - inputText:str - isCorrected:bool - finalFolderId:int - inference_obj = InferenceWrapper() S3_FERRY_URL = os.getenv("S3_FERRY_URL") @@ -46,89 +33,74 @@ class OutlookInferenceRequest(BaseModel): if not os.path.exists(OUTLOOK_MODEL_DOWNLOAD_DIRECTORY): os.makedirs(OUTLOOK_MODEL_DOWNLOAD_DIRECTORY) -async def authenticate_user(request: Request): - cookie = request.cookies.get("customJwtCookie") - if not cookie: - raise HTTPException(status_code=401, detail="No cookie found in the request") - url = f"{RUUTER_PRIVATE_URL}/auth/jwt/userinfo" - headers = { - 'cookie': f'customJwtCookie={cookie}' - } - - response = requests.get(url, headers=headers) - if response.status_code != 200: - raise HTTPException(status_code=response.status_code, detail="Authentication failed") - -@app.post("/deployment/jira/update") -async def download_document(request: Request, modelData:UpdateRequest, backgroundTasks: BackgroundTasks): +@app.post("/classifier/deployment/outlook/update") +async def download_outlook_model(request: Request, modelData:UpdateRequest, backgroundTasks: BackgroundTasks): saveLocation = f"/models/{modelData.modelId}/{modelData.modelId}.zip" - try: - await authenticate_user(request) + try: local_file_name = f"{modelData.modelId}.zip" - local_file_path = f"/models/jira/{local_file_name}" + local_file_path = f"/models/outlook/{local_file_name}" ## Get class hierarchy and validate it + is_valid, class_hierarchy = modelInference.get_class_hierarchy_and_validate(modelData.modelId) - # Get group id from the model id - # get class hierarchy using group id + if(is_valid and class_hierarchy): + + # 1. Clear the current content inside the folder + folder_path = os.path.join("..", "shared", "models", "outlook") + clear_folder_contents(folder_path) - # 1. Clear the current content inside the folder - folder_path = os.path.join("..", "shared", "models", "jira") - clear_folder_contents(folder_path) + # 2. Download the new Model + response = s3_ferry.transfer_file(local_file_path, "FS", saveLocation, "S3") + if response.status_code != 201: + raise HTTPException(status_code = 500, detail = S3_DOWNLOAD_FAILED) - # 2. Download the new Model - response = s3_ferry.transfer_file(local_file_path, "FS", saveLocation, "S3") - if response.status_code != 201: - raise HTTPException(status_code = 500, detail = S3_DOWNLOAD_FAILED) - - zip_file_path = os.path.join("..", f"shared/models/jira", local_file_name) - extract_file_path = os.path.join("..", f"shared/models/jira") + zip_file_path = os.path.join("..", "shared/models/outlook", local_file_name) + extract_file_path = os.path.join("..", "shared/models/outlook") - # 3. Unzip Model Content - unzip_file(zip_path=zip_file_path, extract_to=extract_file_path) - - backgroundTasks.add_task(os.remove, zip_file_path) + # 3. Unzip Model Content + unzip_file(zip_path=zip_file_path, extract_to=extract_file_path) + backgroundTasks.add_task(os.remove, zip_file_path) - #3. TODO : Replace the content in other folder if it a replacement --> Call the delete endpoint - if(UpdateRequest.replaceDeployment): - folder_path = os.path.join("..", "shared", "models", {UpdateRequest.replaceDeploymentPlatform}) - clear_folder_contents(folder_path) - - inference_obj.stop_model(deployment_platform=UpdateRequest.replaceDeploymentPlatform) + # 3. Replace the content in other folder if it a replacement + if(modelData.replaceDeployment): + folder_path = os.path.join("..", "shared", "models", {modelData.replaceDeploymentPlatform}) + clear_folder_contents(folder_path) + inference_obj.stop_model(deployment_platform=modelData.replaceDeploymentPlatform) - # 4. TODO : Instantiate Munsif's Inference Model - model_path = f"shared/models/jira/{UpdateRequest.modelId}" - best_model = UpdateRequest.bestModelName + # 4. Instantiate Munsif's Inference Model + model_path = f"shared/models/outlook/{modelData.modelId}" + best_model = modelData.bestBaseModel - model_initiate = inference_obj.model_swapping(model_path, best_model, deployment_platform="jira") + model_initiate = inference_obj.model_swapping(model_path, best_model, deployment_platform="outlook", class_hierarchy=class_hierarchy, model_id=modelData.modelId) - if(model_initiate): - return JSONResponse(status_code=200, content="Success") + if(model_initiate): + return JSONResponse(status_code=200, content={"replacementStatus": 200}) + else: + raise HTTPException(status_code = 500, detail = "Failed to initiate inference object") + + else: + raise HTTPException(status_code = 500, detail = "Error in obtaining the class hierarchy or class hierarchy is invalid") except Exception as e: raise HTTPException(status_code = 500, detail=str(e)) -@app.post("/deployment/outlook/update") -async def download_document(request: Request, modelData:UpdateRequest, backgroundTasks: BackgroundTasks): +@app.post("/classifier/datamodel/deployment/jira/update") +async def download_jira_model(request: Request, modelData:UpdateRequest, backgroundTasks: BackgroundTasks): saveLocation = f"/models/{modelData.modelId}/{modelData.modelId}.zip" - try: - await authenticate_user(request) + try: local_file_name = f"{modelData.modelId}.zip" local_file_path = f"/models/jira/{local_file_name}" - # before all - - # 1. Clear the current content inside the folder - folder_path = os.path.join("..", "shared", "models", "outlook") + folder_path = os.path.join("..", "shared", "models", "jira") clear_folder_contents(folder_path) # 2. Download the new Model @@ -136,8 +108,8 @@ async def download_document(request: Request, modelData:UpdateRequest, backgroun if response.status_code != 201: raise HTTPException(status_code = 500, detail = S3_DOWNLOAD_FAILED) - zip_file_path = os.path.join("..", f"shared/models/outlook", local_file_name) - extract_file_path = os.path.join("..", f"shared/models/outlook") + zip_file_path = os.path.join("..", "shared/models/jira", local_file_name) + extract_file_path = os.path.join("..", "shared/models/jira") # 3. Unzip Model Content unzip_file(zip_path=zip_file_path, extract_to=extract_file_path) @@ -145,36 +117,39 @@ async def download_document(request: Request, modelData:UpdateRequest, backgroun backgroundTasks.add_task(os.remove, zip_file_path) - #3. TODO : Replace the content in other folder if it a replacement --> Call the delete endpoint - if(UpdateRequest.replaceDeployment): - folder_path = os.path.join("..", "shared", "models", {UpdateRequest.replaceDeploymentPlatform}) + #3. Replace the content in other folder if it a replacement --> Call the delete endpoint + if(modelData.replaceDeployment): + folder_path = os.path.join("..", "shared", "models", {modelData.replaceDeploymentPlatform}) clear_folder_contents(folder_path) - inference_obj.stop_model(deployment_platform=UpdateRequest.replaceDeploymentPlatform) + inference_obj.stop_model(deployment_platform=modelData.replaceDeploymentPlatform) - # 4. TODO : Instantiate Munsif's Inference Model - model_path = f"shared/models/outlook/{UpdateRequest.modelId}" - best_model = UpdateRequest.bestModelName + # 4. Instantiate Munsif's Inference Model + class_hierarchy = modelInference.get_class_hierarchy_by_model_id(modelData.modelId) + if(class_hierarchy): - model_initiate = inference_obj.model_swapping(model_path, best_model, deployment_platform="outlook") - - if(model_initiate): - return JSONResponse(status_code=200, content="Success") + model_path = f"shared/models/jira/{modelData.modelId}" + best_model = modelData.bestBaseModel + model_initiate = inference_obj.model_swapping(model_path, best_model, deployment_platform="jira", class_hierarchy=class_hierarchy, model_id=modelData.modelId) + + if(model_initiate): + return JSONResponse(status_code=200, content={"replacementStatus": 200}) + else: + raise HTTPException(status_code = 500, detail = "Failed to initiate inference object") + else: + raise HTTPException(status_code = 500, detail = "Error in obtaining the class hierarchy") except Exception as e: raise HTTPException(status_code = 500, detail=str(e)) - - -@app.post("/deployment/jira/delete") +@app.post("/classifier/datamodel/deployment/jira/delete") async def delete_folder_content(request:Request): try: - await authenticate_user(request) folder_path = os.path.join("..", "shared", "models", "jira") clear_folder_contents(folder_path) - # TODO : Stop Server Functionality + # Stop the model inference_obj.stop_model(deployment_platform="jira") delete_success = {"message" : "Model Deleted Successfully!"} @@ -184,14 +159,13 @@ async def delete_folder_content(request:Request): raise HTTPException(status_code = 500, detail=str(e)) -@app.post("/deployment/outlook/delete") +@app.post("/classifier/datamodel/deployment/outlook/delete") async def delete_folder_content(request:Request): try: - await authenticate_user(request) folder_path = os.path.join("..", "shared", "models", "outlook") clear_folder_contents(folder_path) - # TODO : Stop Server Functionality + # Stop the model inference_obj.stop_model(deployment_platform="outlook") delete_success = {"message" : "Model Deleted Successfully!"} @@ -203,39 +177,129 @@ async def delete_folder_content(request:Request): @app.post("/classifier/deployment/outlook/inference") async def outlook_inference(request:Request, inferenceData:OutlookInferenceRequest): - try: - await authenticate_user(request) + try: + model_id = inference_obj.get_model_id(deployment_platform="outlook") - print(inferenceData) + if(model_id): + # If there is a active model - ## Check Whether this is a corrected email or not --> if(inferenceData. isCorrected) - - if(inferenceData.isCorrected): - pass - # No Inference - ## TODO : What's the process in here? + # 1 . Check whether the if the Inference Exists + is_exist = modelInference.check_inference_data_exists(input_id=inferenceData.inputId) - else: ## New Email - # Call inference - prediction = inference_obj.inference(inferenceData.inputText, deployment_platform="outlook") + if(is_exist): # Update Inference Scenario + # Create Corrected Folder Hierarchy using the final folder id + corrected_folder_hierarchy = modelInference.build_corrected_folder_hierarchy(final_folder_id=inferenceData.finalFolderId, model_id=model_id) + + # Call Munsif's user_corrected_probablities + corrected_probs = inference_obj.get_corrected_probabilities(text=inferenceData.inputText, corrected_labels=corrected_folder_hierarchy, deployment_platform="outlook") + + if(corrected_probs): + # Calculate Average Predicted Class Probability + average_probability = calculate_average_predicted_class_probability(corrected_probs) + + # Build request payload for inference/update endpoint + inference_update_paylod = get_inference_update_payload(inferenceInputId=inferenceData.inputId,isCorrected=True, correctedLabels=corrected_folder_hierarchy,averagePredictedClassesProbability=average_probability, platform="OUTLOOK", primaryFolderId=inferenceData.finalFolderId) + + # Call inference/update endpoint + is_success = modelInference.update_inference(payload=inference_update_paylod) + + if(is_success): + return JSONResponse(status_code=200, content={"operationSuccessful": True}) + else: + raise HTTPException(status_code = 500, detail="Failed to call the update inference") + + else: + raise HTTPException(status_code = 500, detail="Failed to retrieve the corrected class probabilities from the inference pipeline") + + + else: # Create Inference Scenario + # Call Inference + predicted_hierarchy, probabilities = inference_obj.inference(inferenceData.inputText, deployment_platform="outlook") + + if (probabilities and predicted_hierarchy): + + # Calculate Average Predicted Class Probability + average_probability = calculate_average_predicted_class_probability(probabilities) + + # Get the final folder id of the predicted folder hierarchy + final_folder_id = modelInference.find_final_folder_id(flattened_folder_hierarchy=predicted_hierarchy, model_id=model_id) + + # Build request payload for inference/create endpoint + inference_create_payload = get_inference_create_payload(inferenceInputId=inferenceData.inputId,inferenceText=inferenceData.inputText,predictedLabels=predicted_hierarchy, averagePredictedClassesProbability=average_probability, platform="OUTLOOK", primaryFolderId=final_folder_id, mailId=inferenceData.mailId) + + # Call inference/create endpoint + is_success = modelInference.create_inference(payload=inference_create_payload) + + if(is_success): + return JSONResponse(status_code=200, content={"operationSuccessful": True}) + else: + raise HTTPException(status_code = 500, detail="Failed to call the create inference") + + else: + raise HTTPException(status_code = 500, detail="Failed to call inference") + + else: + raise HTTPException(status_code = 500, detail="No active model currently exists for the Outlook inference") - ## TODO : Call inference/create endpoint - - ## Call Kalsara's Outlook endpoint + - except Exception as e: raise HTTPException(status_code = 500, detail=str(e)) @app.post("/classifier/deployment/jira/inference") -async def jira_inference(request:Request, inferenceData:OutlookInferenceRequest): - try: - await authenticate_user(request) - - print(inferenceData) +async def jira_inference(request:Request, inferenceData:JiraInferenceRequest): + try: + # 1 . Check whether the if the Inference Exists + is_exist = modelInference.check_inference_data_exists(input_id=inferenceData.inputId) + + if(is_exist): # Update Inference Scenario + # Call Munsif's user_corrected_probablities + corrected_probs = inference_obj.get_corrected_probabilities(text=inferenceData.inputText, corrected_labels=inferenceData.finalLabels, deployment_platform="outlook") + + if(corrected_probs): + # Calculate Average Predicted Class Probability + average_probability = calculate_average_predicted_class_probability(corrected_probs) + + # Build request payload for inference/update endpoint + inference_update_paylod = get_inference_update_payload(inferenceInputId=inferenceData.inputId,isCorrected=True, correctedLabels=inferenceData.finalLabels, averagePredictedClassesProbability=average_probability, platform="JIRA", primaryFolderId=None) + + # Call inference/update endpoint + is_success = modelInference.update_inference(payload=inference_update_paylod) + + if(is_success): + return JSONResponse(status_code=200, content={"operationSuccessful": True}) + else: + raise HTTPException(status_code = 500, detail="Failed to call the update inference") + + else: + raise HTTPException(status_code = 500, detail="Failed to retrieve the corrected class probabilities from the inference pipeline") + + + else: # Create Inference Scenario + # Call Inference + predicted_hierarchy, probabilities = inference_obj.inference(inferenceData.inputText, deployment_platform="jira") + + if (probabilities and predicted_hierarchy): + + # Calculate Average Predicted Class Probability + average_probability = calculate_average_predicted_class_probability(probabilities) + + # Build request payload for inference/create endpoint + inference_create_payload = get_inference_create_payload(inferenceInputId=inferenceData.inputId,inferenceText=inferenceData.inputText,predictedLabels=predicted_hierarchy, averagePredictedClassesProbability=average_probability, platform="JIRA", primaryFolderId=None, mailId=None) + + # Call inference/create endpoint + is_success = modelInference.create_inference(payload=inference_create_payload) + + if(is_success): + return JSONResponse(status_code=200, content={"operationSuccessful": True}) + else: + raise HTTPException(status_code = 500, detail="Failed to call the create inference") + + else: + raise HTTPException(status_code = 500, detail="Failed to call inference") except Exception as e: diff --git a/model_inference/utils.py b/model_inference/utils.py index b1834442..742f8e38 100644 --- a/model_inference/utils.py +++ b/model_inference/utils.py @@ -1,6 +1,7 @@ import zipfile import os import shutil +from typing import List, Optional def unzip_file(zip_path, extract_to): with zipfile.ZipFile(zip_path, 'r') as zip_ref: @@ -25,4 +26,39 @@ def get_s3_payload(destinationFilePath:str, destinationStorageType:str, sourceFi "sourceFilePath": sourceFilePath, "sourceStorageType": sourceStorageType } - return S3_FERRY_PAYLOAD \ No newline at end of file + return S3_FERRY_PAYLOAD + +def get_inference_create_payload(inferenceInputId:str, inferenceText:str, predictedLabels:List[str], averagePredictedClassesProbability:int, platform:str, primaryFolderId: Optional[str] = None, mailId : Optional[str] = None ): + INFERENCE_CREATE_PAYLOAD = { + "inputId": inferenceInputId, + "inferenceText": inferenceText, + "predictedLabels": predictedLabels, + "averagePredictedClassesProbability": averagePredictedClassesProbability, + "platform": platform, + "primaryFolderId": primaryFolderId, + "mailId":mailId + } + + return INFERENCE_CREATE_PAYLOAD + + +def get_inference_update_payload(inferenceInputId:str, isCorrected:bool, correctedLabels:List[str], averagePredictedClassesProbability:int, platform:str, primaryFolderId: Optional[str] = None ): + INFERENCE_UPDATE_PAYLOAD = { + "inferenceId": inferenceInputId, + "isCorrected": isCorrected, + "predictedLabels": correctedLabels, + "averagePredictedClassesProbability": averagePredictedClassesProbability, + "primaryFolderId": primaryFolderId, + "platform": platform + } + + return INFERENCE_UPDATE_PAYLOAD + + +def calculate_average_predicted_class_probability(class_probabilities:List[float]): + + total_probability = sum(class_probabilities) + average_probability = total_probability / len(class_probabilities) + + return average_probability + \ No newline at end of file From d0d383784ed183cd14b52ac9293f4b49c4d5471f Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 16 Aug 2024 12:12:04 +0530 Subject: [PATCH 487/582] hirachy validation updates --- hierarchy_validation/constants.py | 18 +++++--- .../hierarchy_validation_api.py | 45 ++++++++++++------- hierarchy_validation/requirements.txt | 3 +- hierarchy_validation/utils.py | 32 ++++++++++--- 4 files changed, 67 insertions(+), 31 deletions(-) diff --git a/hierarchy_validation/constants.py b/hierarchy_validation/constants.py index 8a4abc87..bf0f503a 100644 --- a/hierarchy_validation/constants.py +++ b/hierarchy_validation/constants.py @@ -1,23 +1,29 @@ -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing import List GRAPH_API_BASE_URL = "https://graph.microsoft.com/v1.0" class ClassHierarchy(BaseModel): - class_name: str + class_name: str = Field(..., alias="class") subclasses: List['ClassHierarchy'] = [] - class Folder(BaseModel): id: str displayName: str childFolders: List['Folder'] = [] - class HierarchyCheckRequest(BaseModel): + modelId:int classHierarchies: List[ClassHierarchy] - + +class FolderHierarchyRequest(BaseModel): + modelId:int class FlattenedFolderHierarchy(BaseModel): - hierarchy:List[str] \ No newline at end of file + hierarchy:List[str] + modelId:int + +class CorrectedFolderRequest(BaseModel): + folderId:str + modelId:int \ No newline at end of file diff --git a/hierarchy_validation/hierarchy_validation_api.py b/hierarchy_validation/hierarchy_validation_api.py index 72c06d36..5b71cdb1 100644 --- a/hierarchy_validation/hierarchy_validation_api.py +++ b/hierarchy_validation/hierarchy_validation_api.py @@ -1,7 +1,9 @@ -from fastapi import FastAPI, HTTPException +from fastapi import FastAPI, HTTPException, Request from fastapi.middleware.cors import CORSMiddleware -from constants import HierarchyCheckRequest, FlattenedFolderHierarchy -from utils import build_folder_hierarchy, validate_hierarchy, find_folder_id, get_corrected_folder_hierarchy +import os +import requests +from constants import HierarchyCheckRequest, FlattenedFolderHierarchy, FolderHierarchyRequest, CorrectedFolderRequest +from utils import build_folder_hierarchy, validate_hierarchy, find_folder_id, get_corrected_folder_hierarchy,get_outlook_access_token app = FastAPI() @@ -13,23 +15,31 @@ allow_headers = ["*"], ) -@app.get("/api/folder-hierarchy") -async def get_folder_hierarchy(): +RUUTER_PRIVATE_URL = os.getenv("RUUTER_PRIVATE_URL") + +@app.post("/folder-hierarchy") +async def get_folder_hierarchy(request: Request, modelData:FolderHierarchyRequest): try: - hierarchy = await build_folder_hierarchy() + outlook_access_token = get_outlook_access_token(model_id=modelData.modelId) + hierarchy = await build_folder_hierarchy(outlook_access_token=outlook_access_token) return hierarchy except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to fetch folder hierarchy: {str(e)}") -@app.post("/api/check-folder-hierarchy") -async def check_folder_hierarchy(request: HierarchyCheckRequest): - result = await validate_hierarchy(request.classHierarchies) - return result +@app.post("/check-folder-hierarchy") +async def check_folder_hierarchy(request: Request, hierarchyData: HierarchyCheckRequest): + try: + outlook_access_token = get_outlook_access_token(model_id=HierarchyCheckRequest.modelId) + result = await validate_hierarchy(class_hierarchies=hierarchyData.classHierarchies, outlook_access_token=outlook_access_token) + return result + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to validate folder hierarchy: {str(e)}") -@app.post("/api/find-folder-id") -async def get_folder_id(flattened_hierarchy: FlattenedFolderHierarchy): +@app.post("/find-folder-id") +async def get_folder_id(request: Request, flattened_hierarchy: FlattenedFolderHierarchy): try: - hierarchy = await build_folder_hierarchy() + outlook_access_token = get_outlook_access_token(model_id=flattened_hierarchy.modelId) + hierarchy = await build_folder_hierarchy(outlook_access_token=outlook_access_token) folder_id = find_folder_id(hierarchy, flattened_hierarchy.hierarchy) return {"folder_id": folder_id} except ValueError as e: @@ -37,11 +47,12 @@ async def get_folder_id(flattened_hierarchy: FlattenedFolderHierarchy): except Exception as e: raise HTTPException(status_code=500, detail=f"An error occurred: {str(e)}") -@app.get("/api/get-corrected-folder-hierarchy") -async def get_hierarchy(folderId: str): +@app.post("/corrected-folder-hierarchy") +async def get_hierarchy(request: Request, correctedData:CorrectedFolderRequest ): try: - hierarchy = await build_folder_hierarchy() - folder_path = get_corrected_folder_hierarchy(hierarchy, folderId) + outlook_access_token = get_outlook_access_token(model_id=correctedData.modelId) + hierarchy = await build_folder_hierarchy(outlook_access_token=outlook_access_token) + folder_path = get_corrected_folder_hierarchy(hierarchy, correctedData.folderId) return {"folder_hierarchy": folder_path} except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) diff --git a/hierarchy_validation/requirements.txt b/hierarchy_validation/requirements.txt index 0c14a900..198ca45c 100644 --- a/hierarchy_validation/requirements.txt +++ b/hierarchy_validation/requirements.txt @@ -3,4 +3,5 @@ fastapi-cli==0.0.4 httpx==0.27.0 pydantic==2.8.2 pydantic_core==2.20.1 -uvicorn==0.30.3 \ No newline at end of file +uvicorn==0.30.3 +requests==2.32.3 \ No newline at end of file diff --git a/hierarchy_validation/utils.py b/hierarchy_validation/utils.py index 1826bc1e..b430c098 100644 --- a/hierarchy_validation/utils.py +++ b/hierarchy_validation/utils.py @@ -1,15 +1,18 @@ from fastapi import HTTPException import httpx +import requests +import os from constants import (GRAPH_API_BASE_URL, Folder, ClassHierarchy) +GET_OUTLOOK_ACCESS_TOKEN_URL=os.getenv("GET_OUTLOOK_ACCESS_TOKEN_URL") from typing import List -async def fetch_folders(folder_id: str = 'root', ACCESS_TOKEN: str = 'none'): +async def fetch_folders(folder_id: str = 'root', outlook_access_token:str=''): url = f"{GRAPH_API_BASE_URL}/me/mailFolders" if folder_id != 'root': url = f"{GRAPH_API_BASE_URL}/me/mailFolders/{folder_id}/childFolders" headers = { - "Authorization": f"Bearer {ACCESS_TOKEN}", + "Authorization": f"Bearer {outlook_access_token}", "Content-Type": "application/json" } @@ -26,11 +29,12 @@ async def fetch_folders(folder_id: str = 'root', ACCESS_TOKEN: str = 'none'): return all_folders -async def build_folder_hierarchy(folder_id: str = 'root'): - folders = await fetch_folders(folder_id) +async def build_folder_hierarchy(outlook_access_token:str, folder_id: str = 'root'): + + folders = await fetch_folders(folder_id, outlook_access_token=outlook_access_token) async def build_hierarchy(folder): - child_folders = await build_folder_hierarchy(folder['id']) + child_folders = await build_folder_hierarchy(outlook_access_token=outlook_access_token, folder_id=folder['id']) return Folder( id=folder['id'], displayName=folder['displayName'], @@ -40,9 +44,9 @@ async def build_hierarchy(folder): return [await build_hierarchy(folder) for folder in folders] -async def validate_hierarchy(class_hierarchies: List[ClassHierarchy]): +async def validate_hierarchy(class_hierarchies: List[ClassHierarchy], outlook_access_token:str): errors = [] - folder_hierarchy = await build_folder_hierarchy() + folder_hierarchy = await build_folder_hierarchy(outlook_access_token=outlook_access_token) def find_folder(name: str, folders: List[Folder]): return next((folder for folder in folders if folder.displayName == name), None) @@ -94,3 +98,17 @@ def search_hierarchy(folders: List[Folder], target_id: str, current_path: List[s if not result: raise ValueError(f"Folder with ID '{final_folder_id}' not found in the hierarchy") return result + + +def get_outlook_access_token(model_id:int): + try: + get_outlook_access_token_url = GET_OUTLOOK_ACCESS_TOKEN_URL + response = requests.post(get_outlook_access_token_url, json={"modelId": model_id}) + response.raise_for_status() + data = response.json() + + access_token = data["outlook_access_token"] + return access_token + except requests.exceptions.RequestException as e: + raise Exception(f"Failed to retrieve Outlook Access Token. Reason: {e}") + \ No newline at end of file From 0cf3644948ac43ddf61af416a5a26ae75794aa63 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 16 Aug 2024 12:25:03 +0530 Subject: [PATCH 488/582] version removal and new container adding --- docker-compose.yml | 61 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 5ea1b694..a757ccd3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.7' - services: ruuter-public: container_name: ruuter-public @@ -280,10 +278,20 @@ services: - shared-volume:/shared environment: - UPLOAD_DIRECTORY=/shared - - JIRA_MODEL_DOWNLOAD_DIRECTORY=/shared/models/jira - - OUTLOOK_MODEL_DOWNLOAD_DIRECTORY=/shared/models/outlook - RUUTER_PRIVATE_URL=http://ruuter-private:8088 - S3_FERRY_URL=http://s3-ferry:3000/v1/files/copy + - JIRA_MODEL_DOWNLOAD_DIRECTORY=/shared/models/jira + - OUTLOOK_MODEL_DOWNLOAD_DIRECTORY=/shared/models/outlook + - GET_INFERENCE_DATASET_EXIST_URL=http://ruuter-public:8086/internal/exist?inputId=inferenceInputId + - CREATE_INFERENCE_URL=http://ruuter-public:8086/internal/create + - UPDATE_INFERENCE_URL=http://ruuter-public:8086/internal/corrected + - GET_DATAMODEL_METADATA_BY_ID_URL=http://ruuter-private:8088/classifier/datamodel/metadata?modelId=inferenceModelId + - GET_DATASET_GROUP_METADATA_BY_ID_URL=http://ruuter-private:8088/classifier/datasetgroup/group/metadata?groupId=dataSetGroupId + - CLASS_HIERARCHY_VALIDATION_URL=http://hierarchy-validation:8009/check-folder-hierarchy + - GET_OUTLOOK_ACCESS_TOKEN_URL=http://ruuter-public:8086/internal/validate + - BUILD_CORRECTED_FOLDER_HIERARCHY_URL=http://hierarchy-validation:8009/corrected-folder-hierarchy + - FIND_FINAL_FOLDER_ID_URL=http://hierarchy-validation:8009/find-folder-id + - UPDATE_DATAMODEL_PROGRESS_URL=http://ruuter-private:8088/classifier/datamodel/progress/update ports: - "8003:8003" networks: @@ -293,6 +301,51 @@ services: - init - s3-ferry + test-inference: + build: + context: ./test_inference + dockerfile: Dockerfile + container_name: test-inference + volumes: + - shared-volume:/shared + environment: + - RUUTER_PRIVATE_URL=http://ruuter-private:8088 + - S3_FERRY_URL=http://s3-ferry:3000/v1/files/copy + - TEST_MODEL_DOWNLOAD_DIRECTORY=/shared/models/test + - GET_OUTLOOK_ACCESS_TOKEN_URL=http://ruuter-public:8086/internal/validate + - UPDATE_DATAMODEL_PROGRESS_URL=http://ruuter-private:8088/classifier/datamodel/progress/update + ports: + - "8010:8010" + networks: + bykstack: + ipv4_address: 172.25.0.21 + depends_on: + - init + - file-handler + + hierarchy-validation: + build: + context: ./hierarchy_validation + dockerfile: Dockerfile + container_name: hierarchy-validation + volumes: + - shared-volume:/shared + environment: + - UPLOAD_DIRECTORY=/shared + - RUUTER_PRIVATE_URL=http://ruuter-private:8088 + - S3_FERRY_URL=http://s3-ferry:3000/v1/files/copy + - JIRA_MODEL_DOWNLOAD_DIRECTORY=/shared/models/jira + - OUTLOOK_MODEL_DOWNLOAD_DIRECTORY=/shared/models/outlook + - GET_OUTLOOK_ACCESS_TOKEN_URL=http://ruuter-public:8086/internal/validate + ports: + - "8009:8009" + networks: + bykstack: + ipv4_address: 172.25.0.22 + depends_on: + - init + - file-handler + opensearch-node: image: opensearchproject/opensearch:2.11.1 container_name: opensearch-node From 08d2fc459277b338abd30975bef04bb2f9eab41f Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Fri, 16 Aug 2024 12:44:40 +0530 Subject: [PATCH 489/582] add env in outlook consent app --- outlook-consent-app/.env | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 outlook-consent-app/.env diff --git a/outlook-consent-app/.env b/outlook-consent-app/.env new file mode 100644 index 00000000..51e92756 --- /dev/null +++ b/outlook-consent-app/.env @@ -0,0 +1,4 @@ +NEXT_PUBLIC_CLIENT_ID=value +CLIENT_SECRET=value +REDIRECT_URI=http://localhost:3003/callback +TENANT_ID=common From b1bc08c081bfabbaffc37cfced0b5302bcd72216 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Fri, 16 Aug 2024 14:39:09 +0530 Subject: [PATCH 490/582] outlook-bug-fix: change jira webhook triggering flow --- .../DSL/POST/internal/jira/accept.yml | 133 +++ docker-compose.yml | 14 +- jira-verification/Dockerfile | 14 + jira-verification/index.js | 31 + jira-verification/package-lock.json | 767 ++++++++++++++++++ jira-verification/package.json | 17 + jira-verification/verifySignature.js | 32 + 7 files changed, 1007 insertions(+), 1 deletion(-) create mode 100644 DSL/Ruuter.public/DSL/POST/internal/jira/accept.yml create mode 100644 jira-verification/Dockerfile create mode 100644 jira-verification/index.js create mode 100644 jira-verification/package-lock.json create mode 100644 jira-verification/package.json create mode 100644 jira-verification/verifySignature.js diff --git a/DSL/Ruuter.public/DSL/POST/internal/jira/accept.yml b/DSL/Ruuter.public/DSL/POST/internal/jira/accept.yml new file mode 100644 index 00000000..a6ae24d9 --- /dev/null +++ b/DSL/Ruuter.public/DSL/POST/internal/jira/accept.yml @@ -0,0 +1,133 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'ACCEPT'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: payload + type: string + description: "Body field 'payload'" + +get_webhook_data: + assign: + payload: ${incoming.body.payload} + issue_info: ${incoming.body.payload.issue} + event_type: ${incoming.body.payload.webhookEvent} + next: check_event_type + +check_event_type: + switch: + - condition: ${event_type === 'jira:issue_updated'} + next: get_existing_labels + next: get_jira_issue_info + +get_existing_labels: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-jira-input-row-data" + body: + inputId: ${issue_info.key} + result: res + next: check_input_response + +check_input_response: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: check_previous_labels + next: return_db_request_fail + +check_previous_labels: + switch: + - condition: ${res.response.body.length > 0} + next: assign_previous_labels + next: get_jira_issue_info + +assign_previous_labels: + assign: + previous_corrected_labels: ${res.response.body[0].correctedLabels !==null ? JSON.parse(res.response.body[0].correctedLabels.value) :[]} + previous_predicted_labels: ${res.response.body[0].predictedLabels !==null ? JSON.parse(res.response.body[0].predictedLabels.value) :[]} + next: validate_issue_labels + +validate_issue_labels: + call: http.post + args: + url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/return_label_mismatch" + headers: + type: json + body: + newLabels: ${issue_info.fields.labels} + correctedLabels: ${previous_corrected_labels} + predictedLabels: ${previous_predicted_labels} + result: label_response + next: check_label_mismatch + +check_label_mismatch: + switch: + - condition: ${label_response.response.body.isMismatch === 'true'} + next: get_jira_issue_info + next: return_data + +get_jira_issue_info: + call: http.post + args: + url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/return_jira_issue_info" + headers: + type: json + body: + data: ${issue_info} + result: extract_info + next: send_issue_data + +send_issue_data: + call: http.post + args: + url: "[#CLASSIFIER_ANONYMIZER]/anonymize" + headers: + type: json + body: + platform: 'JIRA' + key: ${issue_info.key} + data: ${extract_info} + parentFolderId: null + mailId: null + labels: ${issue_info.fields.labels} + result: res + +check_response: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: return_ok + next: return_bad_request + +return_ok: + status: 200 + return: "Jira data send successfully" + next: end + +return_data: + status: 200 + return: "Not Sent" + next: end + +return_error_found: + status: 400 + return: "Error Found" + next: end + +return_db_request_fail: + status: 400 + return: "Fetch data for labels failed" + next: end + +return_bad_request: + status: 400 + return: "Bad Request" + next: end + + + + diff --git a/docker-compose.yml b/docker-compose.yml index 5ea1b694..a184082d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: environment: - application.cors.allowedOrigins=http://localhost:8086,http://localhost:3001,http://localhost:3003,http://localhost:3004,http://localhost:8080,http://localhost:8000 - application.httpCodesAllowList=200,201,202,204,400,401,403,404,500 - - application.internalRequests.allowedIPs=127.0.0.1,172.25.0.7 + - application.internalRequests.allowedIPs=127.0.0.1,172.25.0.7,172.25.0.21 - application.logging.displayRequestContent=true - application.incomingRequests.allowedMethodTypes=POST,GET,PUT - application.logging.displayResponseContent=true @@ -371,6 +371,18 @@ services: bykstack: ipv4_address: 172.25.0.19 + jira-verification: + container_name: jira-verification + image: jira-verification + build: + context: ./jira-verification + dockerfile: Dockerfile + ports: + - "3008:3008" + networks: + bykstack: + ipv4_address: 172.25.0.21 + volumes: shared-volume: opensearch-data: diff --git a/jira-verification/Dockerfile b/jira-verification/Dockerfile new file mode 100644 index 00000000..75a77fab --- /dev/null +++ b/jira-verification/Dockerfile @@ -0,0 +1,14 @@ +# Base image +FROM node:16-alpine + +WORKDIR /app + +COPY package*.json ./ + +RUN npm install --production + +COPY . . + +EXPOSE 3008 + +CMD ["node", "index.js"] diff --git a/jira-verification/index.js b/jira-verification/index.js new file mode 100644 index 00000000..2864d97c --- /dev/null +++ b/jira-verification/index.js @@ -0,0 +1,31 @@ +const express = require("express"); +const bodyParser = require("body-parser"); +const verifySignature = require("./verifySignature.js"); +const axios = require("axios"); + +const app = express(); +app.use(bodyParser.json()); + +app.post("/webhook", async (req, res) => { + const isValid = verifySignature(req.body, req.headers); + if (isValid) { + try { + const response = await axios.post("http://ruuter-public:8086/internal/jira/accept", { + payload: req.body, + }); + + console.log("Response from helper URL:", response.data); + return res.status(200).send("Webhook processed and forwarded"); + } catch (error) { + console.error("Error talking to helper URL:", error); + return res.status(500).send("Error processing webhook"); + } + } else { + return res.status(400).send("Invalid signature"); + } +}); + +const PORT = process.env.PORT || 3008; +app.listen(PORT, () => { + console.log(`Server is running on port ${PORT}`); +}); diff --git a/jira-verification/package-lock.json b/jira-verification/package-lock.json new file mode 100644 index 00000000..2c392831 --- /dev/null +++ b/jira-verification/package-lock.json @@ -0,0 +1,767 @@ +{ + "name": "jira-webhook-verification", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "jira-webhook-verification", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "axios": "^1.7.2", + "body-parser": "^1.20.2", + "express": "^4.19.2" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "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==", + "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/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "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==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "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==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/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==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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==", + "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/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + } + } +} diff --git a/jira-verification/package.json b/jira-verification/package.json new file mode 100644 index 00000000..2653ddc6 --- /dev/null +++ b/jira-verification/package.json @@ -0,0 +1,17 @@ +{ + "name": "jira-webhook-verification", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "axios": "^1.7.2", + "body-parser": "^1.20.2", + "express": "^4.19.2" + } +} diff --git a/jira-verification/verifySignature.js b/jira-verification/verifySignature.js new file mode 100644 index 00000000..de4bff99 --- /dev/null +++ b/jira-verification/verifySignature.js @@ -0,0 +1,32 @@ +const { createHmac, timingSafeEqual } = require('crypto'); + +function verifySignature(payload, headers) { + console.log("verifySignature method start"); + + const signature = headers['x-hub-signature']; // Ensure this is the correct header + if (!signature) { + console.error('Signature missing'); + return false; + } + console.log("Received signature: " + signature); + console.log("payload------------: " + payload); + + const SHARED_SECRET = 'f5EPbhg7u31ooo0YryeX'; // Replace with your actual shared secret + const hmac = createHmac('sha256', Buffer.from(SHARED_SECRET, 'utf8')); + + const payloadString = JSON.stringify(payload); + hmac.update(Buffer.from(payloadString, 'utf8')); // Explicitly use UTF-8 encoding + + const computedSignature = hmac.digest('hex'); + console.log("Computed signature: " + computedSignature); + + const computedSignaturePrefixed = "sha256=" + computedSignature; + console.log("Computed signature with prefix: " + computedSignaturePrefixed); + + const isValid = timingSafeEqual(Buffer.from(computedSignaturePrefixed, 'utf8'), Buffer.from(signature, 'utf8')); + console.log("Signature valid: " + isValid); + + return isValid; +} + +module.exports = verifySignature; From aa84b4081e8641f134ae6b60f884acd40a73059b Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Fri, 16 Aug 2024 15:05:23 +0530 Subject: [PATCH 491/582] outlook-bug-fix: change jira webhook secret read as env variables --- docker-compose.yml | 2 ++ jira-verification/verifySignature.js | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index a184082d..fa4d08c5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -379,6 +379,8 @@ services: dockerfile: Dockerfile ports: - "3008:3008" + environment: + JIRA_WEBHOOK_SECRET: value networks: bykstack: ipv4_address: 172.25.0.21 diff --git a/jira-verification/verifySignature.js b/jira-verification/verifySignature.js index de4bff99..9bd3c8fc 100644 --- a/jira-verification/verifySignature.js +++ b/jira-verification/verifySignature.js @@ -11,7 +11,8 @@ function verifySignature(payload, headers) { console.log("Received signature: " + signature); console.log("payload------------: " + payload); - const SHARED_SECRET = 'f5EPbhg7u31ooo0YryeX'; // Replace with your actual shared secret + const SHARED_SECRET = process.env.JIRA_WEBHOOK_SECRET; // Replace with your actual shared secret + console.log("SHARED_SECRET------------: " + SHARED_SECRET); const hmac = createHmac('sha256', Buffer.from(SHARED_SECRET, 'utf8')); const payloadString = JSON.stringify(payload); From 0589e1f81e116f7637842df994380cb9b41fdd71 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Fri, 16 Aug 2024 15:32:25 +0530 Subject: [PATCH 492/582] outlook-bug-fix: sonar issues fixed --- docker-compose.yml | 1 + jira-verification/.env | 2 ++ jira-verification/Dockerfile | 9 ++++----- jira-verification/index.js | 11 +++++++---- jira-verification/{ => src}/verifySignature.js | 0 5 files changed, 14 insertions(+), 9 deletions(-) create mode 100644 jira-verification/.env rename jira-verification/{ => src}/verifySignature.js (100%) diff --git a/docker-compose.yml b/docker-compose.yml index fa4d08c5..240b65a9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -381,6 +381,7 @@ services: - "3008:3008" environment: JIRA_WEBHOOK_SECRET: value + RUUTER_PUBLIC_JIRA_URL: http://ruuter-public:8086/internal/jira/accept networks: bykstack: ipv4_address: 172.25.0.21 diff --git a/jira-verification/.env b/jira-verification/.env new file mode 100644 index 00000000..f4db2ff9 --- /dev/null +++ b/jira-verification/.env @@ -0,0 +1,2 @@ +JIRA_WEBHOOK_SECRET=value +RUUTER_PUBLIC_JIRA_URL=http://ruuter-public:8086/internal/jira/accept diff --git a/jira-verification/Dockerfile b/jira-verification/Dockerfile index 75a77fab..db935fed 100644 --- a/jira-verification/Dockerfile +++ b/jira-verification/Dockerfile @@ -1,13 +1,12 @@ -# Base image -FROM node:16-alpine +FROM node:22.0.0-alpine WORKDIR /app -COPY package*.json ./ +COPY package.json package-lock.json /app/ -RUN npm install --production +RUN npm install -COPY . . +COPY . /app/ EXPOSE 3008 diff --git a/jira-verification/index.js b/jira-verification/index.js index 2864d97c..ee4fe05b 100644 --- a/jira-verification/index.js +++ b/jira-verification/index.js @@ -1,6 +1,6 @@ const express = require("express"); const bodyParser = require("body-parser"); -const verifySignature = require("./verifySignature.js"); +const verifySignature = require("./src/verifySignature.js"); const axios = require("axios"); const app = express(); @@ -10,9 +10,12 @@ app.post("/webhook", async (req, res) => { const isValid = verifySignature(req.body, req.headers); if (isValid) { try { - const response = await axios.post("http://ruuter-public:8086/internal/jira/accept", { - payload: req.body, - }); + const response = await axios.post( + process.env.RUUTER_PUBLIC_JIRA_URL, + { + payload: req.body, + } + ); console.log("Response from helper URL:", response.data); return res.status(200).send("Webhook processed and forwarded"); diff --git a/jira-verification/verifySignature.js b/jira-verification/src/verifySignature.js similarity index 100% rename from jira-verification/verifySignature.js rename to jira-verification/src/verifySignature.js From fa8c9f29d0d554b26794fe88d18ba6cfb7a29b8a Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Fri, 16 Aug 2024 15:58:31 +0530 Subject: [PATCH 493/582] outlook-bug-fix: sonar issues fixed --- jira-verification/Dockerfile | 2 +- jira-verification/index.js | 2 ++ jira-verification/package.json | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/jira-verification/Dockerfile b/jira-verification/Dockerfile index db935fed..06a17b40 100644 --- a/jira-verification/Dockerfile +++ b/jira-verification/Dockerfile @@ -4,7 +4,7 @@ WORKDIR /app COPY package.json package-lock.json /app/ -RUN npm install +RUN npm install --ignore-scripts COPY . /app/ diff --git a/jira-verification/index.js b/jira-verification/index.js index ee4fe05b..fab24ccb 100644 --- a/jira-verification/index.js +++ b/jira-verification/index.js @@ -2,9 +2,11 @@ const express = require("express"); const bodyParser = require("body-parser"); const verifySignature = require("./src/verifySignature.js"); const axios = require("axios"); +const helmet = require("helmet"); const app = express(); app.use(bodyParser.json()); +app.use(helmet.hidePoweredBy()); app.post("/webhook", async (req, res) => { const isValid = verifySignature(req.body, req.headers); diff --git a/jira-verification/package.json b/jira-verification/package.json index 2653ddc6..3f9f97cf 100644 --- a/jira-verification/package.json +++ b/jira-verification/package.json @@ -12,6 +12,7 @@ "dependencies": { "axios": "^1.7.2", "body-parser": "^1.20.2", - "express": "^4.19.2" + "express": "^4.19.2", + "helmet": "^7.1.0" } } From 0e122b8611b70cd7535ced1f53009aa0f860fdfe Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 16 Aug 2024 16:18:41 +0530 Subject: [PATCH 494/582] file handler api bug fix --- file-handler/file_handler_api.py | 56 +++++++++++++++++--------------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/file-handler/file_handler_api.py b/file-handler/file_handler_api.py index e6b696c1..3f507860 100644 --- a/file-handler/file_handler_api.py +++ b/file-handler/file_handler_api.py @@ -277,37 +277,41 @@ async def upload_and_copy(request: Request, importData: ImportJsonMajor): @app.post("/datasetgroup/data/copy") async def upload_and_copy(request: Request, copyPayload: CopyPayload): - cookie = request.cookies.get("customJwtCookie") - await authenticate_user(f'customJwtCookie={cookie}') + try: + cookie = request.cookies.get("customJwtCookie") + await authenticate_user(f'customJwtCookie={cookie}') - dg_id = copyPayload.dgId - new_dg_id = copyPayload.newDgId - files = copyPayload.fileLocations + dg_id = copyPayload.dgId + new_dg_id = copyPayload.newDgId + files = copyPayload.fileLocations - if len(files)>0: - local_storage_location = TEMP_COPY_FILE - else: - print("Abort copying since sent file list does not have any entry.") - upload_success = UPLOAD_SUCCESS.copy() - upload_success["saved_file_path"] = "" - return JSONResponse(status_code=200, content=upload_success) + if len(files)>0: + local_storage_location = TEMP_COPY_FILE + else: + print("Abort copying since sent file list does not have any entry.") + upload_success = UPLOAD_SUCCESS.copy() + upload_success["saved_file_path"] = "" + return JSONResponse(status_code=200, content=upload_success) - for file in files: - old_location = f"/dataset/{dg_id}/{file}" - new_location = NEW_DATASET_LOCATION.format(new_dg_id=new_dg_id) + file - response = s3_ferry.transfer_file(local_storage_location, "FS", old_location, "S3") - response = s3_ferry.transfer_file(new_location, "S3", local_storage_location, "FS") + for file in files: + old_location = f"/dataset/{dg_id}/{file}" + new_location = NEW_DATASET_LOCATION.format(new_dg_id=new_dg_id) + file + response = s3_ferry.transfer_file(local_storage_location, "FS", old_location, "S3") + response = s3_ferry.transfer_file(new_location, "S3", local_storage_location, "FS") - if response.status_code == 201: - print(f"Copying completed : {file}") + if response.status_code == 201: + print(f"Copying completed : {file}") + else: + print(f"Copying failed : {file}") + raise HTTPException(status_code=500, detail=S3_UPLOAD_FAILED) else: - print(f"Copying failed : {file}") - raise HTTPException(status_code=500, detail=S3_UPLOAD_FAILED) - else: - os.remove(local_storage_location) - upload_success = UPLOAD_SUCCESS.copy() - upload_success["saved_file_path"] = NEW_DATASET_LOCATION.format(new_dg_id=new_dg_id) - return JSONResponse(status_code=200, content=upload_success) + os.remove(f"../shared/{local_storage_location}") + upload_success = UPLOAD_SUCCESS.copy() + upload_success["saved_file_path"] = NEW_DATASET_LOCATION.format(new_dg_id=new_dg_id) + return JSONResponse(status_code=200, content=upload_success) + except Exception as e: + print(f"Error in /datasetgroup/data/copy : {e}") + return JSONResponse(status_code=200, content="File Copy Failed") def extract_stop_words(file: UploadFile) -> List[str]: file_converter = FileConverter() From d0534b657eecb20875073667fe2d3650a5a18f79 Mon Sep 17 00:00:00 2001 From: Thirunayan22 Date: Fri, 16 Aug 2024 17:28:51 +0530 Subject: [PATCH 495/582] updated docker-compose.yml --- docker-compose.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 5ea1b694..e7f9d2b3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -164,7 +164,9 @@ services: ports: - 5433:5432 volumes: - - /home/ubuntu/user_db_files:/var/lib/postgresql/data + # - /home/ubuntu/user_db_files:/var/lib/postgresql/data + - ~/est_classifier/user_db_files:/var/lib/postgresql/data #For Mac Users + networks: bykstack: ipv4_address: 172.25.0.10 @@ -226,9 +228,6 @@ services: - shared-volume:/shared env_file: - config.env - environment: - - S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID} - - S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY} ports: - "3006:3000" depends_on: From a4803b817686e68dcb70551fd27802b50ef21933 Mon Sep 17 00:00:00 2001 From: Thirunayan22 Date: Fri, 16 Aug 2024 17:29:45 +0530 Subject: [PATCH 496/582] updated outlook accept.yml --- .../DSL/POST/classifier/integration/outlook/accept.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DSL/Ruuter.public/DSL/POST/classifier/integration/outlook/accept.yml b/DSL/Ruuter.public/DSL/POST/classifier/integration/outlook/accept.yml index 253c9bdd..caa0505b 100644 --- a/DSL/Ruuter.public/DSL/POST/classifier/integration/outlook/accept.yml +++ b/DSL/Ruuter.public/DSL/POST/classifier/integration/outlook/accept.yml @@ -2,7 +2,7 @@ declaration: call: declare version: 0.1 description: "Description placeholder for 'ACCEPT'" - method: get + method: post accepts: json returns: text/* namespace: classifier From e58eb8ad9f809d2eeb05d2f384dee9e0b5c09ef3 Mon Sep 17 00:00:00 2001 From: Thirunayan22 Date: Mon, 19 Aug 2024 17:02:37 +0530 Subject: [PATCH 497/582] fixed training pipeline to save and store models after training. Integrated S3 ferry with Cron Manager. refactored config and constants .ini files. --- .gitignore | 10 +- DSL/CronManager/DSL/data_model.yml | 4 +- DSL/CronManager/script/data_processor_exec.sh | 0 DSL/CronManager/script/data_validator_exec.sh | 0 .../script/dataset_deletion_exec.sh | 0 ..._completed_data_model_progress_sessions.sh | 0 ...ete_completed_dataset_progress_sessions.sh | 0 .../script/outlook_refresh_token.sh | 0 .../script/outlook_subscription_refresh.sh | 0 ...rter.sh => python_train_script_starter.sh} | 28 ++- DSL/OpenSearch/deploy-opensearch.sh | 0 .../DSL/POST/classifier/datamodel/create.yml | 6 +- GUI/entrypoint.sh | 0 GUI/nginx/scripts/env.sh | 0 GUI/rebuild.sh | 0 README.md | 20 +- config.env | 12 +- constants.ini | 6 +- create-migration.sh | 0 docker-compose.yml | 23 ++- migrate.sh | 0 model_trainer/Dockerfile | 17 -- model_trainer/constants.py | 29 ++- model_trainer/datapipeline.py | 48 +++-- model_trainer/main.py | 24 +++ model_trainer/model_handler.py | 28 --- model_trainer/model_trainer.py | 180 ++++++++++++------ ...nts.txt => model_trainer_requirements.txt} | 1 + model_trainer/s3_ferry.py | 18 +- model_trainer/trainingpipeline.py | 8 +- src/unitTesting.sh | 0 token.sh | 0 32 files changed, 284 insertions(+), 178 deletions(-) mode change 100644 => 100755 DSL/CronManager/script/data_processor_exec.sh mode change 100644 => 100755 DSL/CronManager/script/data_validator_exec.sh mode change 100644 => 100755 DSL/CronManager/script/dataset_deletion_exec.sh mode change 100644 => 100755 DSL/CronManager/script/delete_completed_data_model_progress_sessions.sh mode change 100644 => 100755 DSL/CronManager/script/delete_completed_dataset_progress_sessions.sh mode change 100644 => 100755 DSL/CronManager/script/outlook_refresh_token.sh mode change 100644 => 100755 DSL/CronManager/script/outlook_subscription_refresh.sh rename DSL/CronManager/script/{python_script_starter.sh => python_train_script_starter.sh} (61%) mode change 100644 => 100755 mode change 100644 => 100755 DSL/OpenSearch/deploy-opensearch.sh mode change 100644 => 100755 GUI/entrypoint.sh mode change 100644 => 100755 GUI/nginx/scripts/env.sh mode change 100644 => 100755 GUI/rebuild.sh mode change 100644 => 100755 create-migration.sh mode change 100644 => 100755 migrate.sh delete mode 100644 model_trainer/Dockerfile create mode 100644 model_trainer/main.py delete mode 100644 model_trainer/model_handler.py rename model_trainer/{model_upload_requirements.txt => model_trainer_requirements.txt} (98%) mode change 100644 => 100755 src/unitTesting.sh mode change 100644 => 100755 token.sh diff --git a/.gitignore b/.gitignore index 3525077b..28a112d4 100644 --- a/.gitignore +++ b/.gitignore @@ -398,4 +398,12 @@ FodyWeavers.xsd *.sln.iml /tim-db -/data \ No newline at end of file +/data + +.env +.sonarlint +.vscode +.DS_Store +DSL/Liquibase/data/update_refresh_token.sql +model_trainer/results +protected_configs/ \ No newline at end of file diff --git a/DSL/CronManager/DSL/data_model.yml b/DSL/CronManager/DSL/data_model.yml index b9101422..ee160229 100644 --- a/DSL/CronManager/DSL/data_model.yml +++ b/DSL/CronManager/DSL/data_model.yml @@ -1,5 +1,5 @@ model_trainer: trigger: off type: exec - command: "../app/scripts/python_script_starter.sh" - allowedEnvs: ['cookie', 'oldModelId', 'newModelId'] \ No newline at end of file + command: "../app/scripts/python_train_script_starter.sh" + allowedEnvs: ['cookie', 'modelId', 'newModelId','updateType'] \ No newline at end of file diff --git a/DSL/CronManager/script/data_processor_exec.sh b/DSL/CronManager/script/data_processor_exec.sh old mode 100644 new mode 100755 diff --git a/DSL/CronManager/script/data_validator_exec.sh b/DSL/CronManager/script/data_validator_exec.sh old mode 100644 new mode 100755 diff --git a/DSL/CronManager/script/dataset_deletion_exec.sh b/DSL/CronManager/script/dataset_deletion_exec.sh old mode 100644 new mode 100755 diff --git a/DSL/CronManager/script/delete_completed_data_model_progress_sessions.sh b/DSL/CronManager/script/delete_completed_data_model_progress_sessions.sh old mode 100644 new mode 100755 diff --git a/DSL/CronManager/script/delete_completed_dataset_progress_sessions.sh b/DSL/CronManager/script/delete_completed_dataset_progress_sessions.sh old mode 100644 new mode 100755 diff --git a/DSL/CronManager/script/outlook_refresh_token.sh b/DSL/CronManager/script/outlook_refresh_token.sh old mode 100644 new mode 100755 diff --git a/DSL/CronManager/script/outlook_subscription_refresh.sh b/DSL/CronManager/script/outlook_subscription_refresh.sh old mode 100644 new mode 100755 diff --git a/DSL/CronManager/script/python_script_starter.sh b/DSL/CronManager/script/python_train_script_starter.sh old mode 100644 new mode 100755 similarity index 61% rename from DSL/CronManager/script/python_script_starter.sh rename to DSL/CronManager/script/python_train_script_starter.sh index 7ff9a807..02611c37 --- a/DSL/CronManager/script/python_script_starter.sh +++ b/DSL/CronManager/script/python_train_script_starter.sh @@ -1,28 +1,26 @@ #!/bin/bash -VENV_DIR="/home/cronmanager/clsenv" -REQUIREMENTS="model_trainer/model_upload_requirements.txt" -PYTHON_SCRIPT="model_trainer/model_trainer.py" +REQUIREMENTS_FILE="/app/model_trainer/model_trainer_requirements.txt" +PYTHON_SCRIPT="/app/model_trainer/main.py" is_package_installed() { package=$1 pip show "$package" > /dev/null 2>&1 } -if [ -d "$VENV_DIR" ]; then - echo "Virtual environment already exists. Activating..." -else - echo "Virtual environment does not exist. Creating..." - python3.12 -m venv $VENV_DIR -fi +echo "cookie - $cookie" +echo "old model id - $modelId" +echo "new model id - $newModelId" +echo "update type - $updateType" -. $VENV_DIR/bin/activate +echo "Activating Python Environment" +source "/app/python_virtual_env/bin/activate" echo "Python executable in use: $(which python3)" -if [ -f "$REQUIREMENTS" ]; then +if [ -f "$REQUIREMENTS_FILE" ]; then echo "Checking if required packages are installed..." - + while IFS= read -r requirement || [ -n "$requirement" ]; do package_name=$(echo "$requirement" | cut -d '=' -f 1 | tr -d '[:space:]') @@ -32,15 +30,15 @@ if [ -f "$REQUIREMENTS" ]; then echo "Package '$package_name' is not installed. Installing..." pip install "$requirement" fi - done < "$REQUIREMENTS" + done < "$REQUIREMENTS_FILE" else - echo "Requirements file not found: $REQUIREMENTS" + echo "Requirements file not found: $REQUIREMENTS_FILE" fi # Check if the Python script exists if [ -f "$PYTHON_SCRIPT" ]; then echo "Running the Python script: $PYTHON_SCRIPT" - python "$PYTHON_SCRIPT" + python3 "$PYTHON_SCRIPT" else echo "Python script not found: $PYTHON_SCRIPT" fi diff --git a/DSL/OpenSearch/deploy-opensearch.sh b/DSL/OpenSearch/deploy-opensearch.sh old mode 100644 new mode 100755 diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/create.yml b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/create.yml index c296975c..047353a2 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/create.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/create.yml @@ -178,9 +178,9 @@ execute_cron_manager: args: url: "[#CLASSIFIER_CRON_MANAGER]/execute/data_model/model_trainer" query: - cookie: ${incoming.headers.cookie} - model_id: ${model_id} - new_model_id: ${model_id} + cookie: ${incoming.headers.cookie.replace('customJwtCookie=','')} #Removing the customJwtCookie phrase from payload to to send cookie token only + modelId: ${model_id} + newModelId: ${model_id} updateType: 'major' result: res next: assign_success_response diff --git a/GUI/entrypoint.sh b/GUI/entrypoint.sh old mode 100644 new mode 100755 diff --git a/GUI/nginx/scripts/env.sh b/GUI/nginx/scripts/env.sh old mode 100644 new mode 100755 diff --git a/GUI/rebuild.sh b/GUI/rebuild.sh old mode 100644 new mode 100755 diff --git a/README.md b/README.md index 272e7786..0df1599c 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,13 @@ This repo will primarily contain: 2. Docker Compose file to set up and run Classifier as a fully functional service; 3. You can view the UI designs for this project in this [Figma file](https://www.figma.com/design/VWoZu2s7auo7YTw49RqNtV/Estonian-Classifier-English-Version?node-id=712-1695&t=cx6ZZVuEkfWqlbZB-1) + +## Instructions for MacOs Users +#### TODO - Include instructions for MacOS users to delete specific parts of the base image to make the build work + +## Version +##### TODO - Talk about programming language versions and framework versions used in the project here + ## Dev setup - Clone [Ruuter](https://github.com/buerokratt/Ruuter) @@ -21,9 +28,14 @@ This repo will primarily contain: - Navigate to TIM and build the image `docker build -t tim .` - Clone [Authentication Layer](https://github.com/buerokratt/Authentication-layer) - Navigate to Authentication Layer and build the image `docker build -f Dockerfile.dev -t authentication-layer .` -- Clone [Cron Manager](https://github.com/buerokratt/CronManager.git) -- Navigate to Cron Manager dev branch and build the image `docker build -t cron-manager .` +- Clone [S3 Ferry](https://github.com/buerokratt/S3-Ferry) +- Navigate to S3-Ferry and build the image `docker build -t s3-ferry .` +- Clone [Cron Manager](https://github.com/rootcodelabs/CronManager) (This is a forked repo of the original Buerokratt CronManager with a Python environment included) +- Navigate to Cron Manager dev branch and build the cron-manager-python image `docker build -f Dockerfile.python -t cron-manager-python .` +## Give execution permission for all mounted shell scripts +- Navigate to the parent folder of the classifier project and run the below command to make the shell files executable +- `find classifier -type f -name "*.sh" -exec chmod +x {} \;` ### Refresh Token setup @@ -70,6 +82,7 @@ This repo will primarily contain: - JIRA_CLOUD_DOMAIN - JIRA_WEBHOOK_ID + ### Notes -To get Jira webhook id ,can use below CURL request with valid credentials @@ -85,3 +98,6 @@ JIRA_CLOUD_DOMAIN/rest/webhooks/1.0/webhook` ##### Ruuter Internal Requests - When running ruuter either on local or in an environment make sure to adjust `- application.internalRequests.allowedIPs=127.0.0.1,{YOUR_IPS}` under ruuter environments + +## Ngrok setup for local testing +#### Explain about Setting up ngrok to test in localhost diff --git a/config.env b/config.env index 675c35cf..ffcbe12d 100644 --- a/config.env +++ b/config.env @@ -1,7 +1,9 @@ API_CORS_ORIGIN=* API_DOCUMENTATION_ENABLED=true -S3_REGION=eu-west-1 -S3_ENDPOINT_URL=https://s3.eu-west-1.amazonaws.com -S3_DATA_BUCKET_NAME=esclassifier-test -S3_DATA_BUCKET_PATH=data/ -FS_DATA_DIRECTORY_PATH=/shared +S3_REGION=value +S3_ENDPOINT_URL=value +S3_DATA_BUCKET_NAME=value +S3_DATA_BUCKET_PATH=value +FS_DATA_DIRECTORY_PATH=value +S3_ACCESS_KEY_ID=value +S3_SECRET_ACCESS_KEY=value diff --git a/constants.ini b/constants.ini index 38260914..0e50b4dd 100644 --- a/constants.ini +++ b/constants.ini @@ -12,9 +12,9 @@ CLASSIFIER_NOTIFICATIONS=http://notifications-node:4040 CLASSIFIER_ANONYMIZER=http://anonymizer:8010 CLASSIFIER_RUUTER_PUBLIC_FRONTEND_URL=value DOMAIN=localhost -JIRA_API_TOKEN= value -JIRA_USERNAME= value -JIRA_CLOUD_DOMAIN= value +JIRA_API_TOKEN=value +JIRA_USERNAME=value +JIRA_CLOUD_DOMAIN=value JIRA_WEBHOOK_ID=value JIRA_WEBHOOK_SECRET=value OUTLOOK_CLIENT_ID=value diff --git a/create-migration.sh b/create-migration.sh old mode 100644 new mode 100755 diff --git a/docker-compose.yml b/docker-compose.yml index 5ea1b694..46598fb3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -170,6 +170,16 @@ services: ipv4_address: 172.25.0.10 restart: always + + init: + image: busybox + command: ["sh", "-c", "chmod -R 777 /shared"] + volumes: + - shared-volume:/shared + networks: + bykstack: + ipv4_address: 172.25.0.12 + cron-manager: container_name: cron-manager image: cron-manager-python @@ -178,6 +188,7 @@ services: - ./DSL/CronManager/script:/app/scripts - ./DSL/CronManager/config:/app/config - ./model_trainer:/app/model_trainer + - shared-volume:/shared environment: - server.port=9010 ports: @@ -185,15 +196,9 @@ services: networks: bykstack: ipv4_address: 172.25.0.11 - - init: - image: busybox - command: ["sh", "-c", "chmod -R 777 /shared"] - volumes: - - shared-volume:/shared - networks: - bykstack: - ipv4_address: 172.25.0.12 + depends_on: + - init + - s3-ferry file-handler: build: diff --git a/migrate.sh b/migrate.sh old mode 100644 new mode 100755 diff --git a/model_trainer/Dockerfile b/model_trainer/Dockerfile deleted file mode 100644 index e4aab18d..00000000 --- a/model_trainer/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -FROM python:3.10-slim - -RUN addgroup --system appuser && adduser --system --ingroup appuser appuser -WORKDIR /app - -COPY model_upload_requirements.txt . -RUN pip install --default-timeout=200 --no-cache-dir -r model_upload_requirements.txt - -COPY . . - -ENV HF_HOME=/app/cache/ -RUN mkdir -p /shared && chown appuser:appuser /shared && chmod 770 /shared -RUN chown -R appuser:appuser /app -EXPOSE 8345 -USER appuser - -CMD ["uvicorn", "trainer_api:app", "--host", "0.0.0.0", "--port", "8345"] \ No newline at end of file diff --git a/model_trainer/constants.py b/model_trainer/constants.py index 29b39d1e..3dddafc2 100644 --- a/model_trainer/constants.py +++ b/model_trainer/constants.py @@ -1,8 +1,29 @@ -URL_DATA = "http://file-handler:8000/datasetgroup/data/download/json" +#TODO - REFACTOR CODE TO CREATE A GENERIC FUNCTION HERE WHICH WILL CONSTRUCT AND RETURN THE CONSTANTS IN A DICTIONARY WHICH CAN BE REFERENCED IN ALL PARTS OF THE CODE -URL_HIERARCHY = "http://ruuter-private:8088/classifier/datasetgroup/group/metadata" +DATA_DOWNLOAD_ENDPOINT = "http://file-handler:8000/datasetgroup/data/download/json" + +GET_DATASET_METADATA_ENDPOINT = "http://ruuter-private:8088/classifier/datasetgroup/group/metadata" + +GET_MODEL_METADATA_ENDPOINT= "http://ruuter-private:8088/classifier/datamodel/metadata" + +DEPLOYMENT_ENDPOINT = "http://ruuter-private:8088/classifier/datamodel/deployment/{deployment_platform}/update" + +TRAINING_LOGS_PATH = "/app/model_trainer/training_logs.log" + +MODEL_RESULTS_PATH = "/shared/model_trainer/results" #stored in the shared folder which is connected to s3-ferry + +LOCAL_BASEMODEL_TRAINED_LAYERS_SAVE_PATH = "/shared/model_trainer/results/{model_id}/trained_base_model_layers" #stored in the shared folder which is connected to s3-ferry + +LOCAL_CLASSIFICATION_LAYER_SAVE_PATH = "/shared/model_trainer/results/{model_id}/classifier_layers" #stored in the shared folder which is connected to s3-ferry + +LOCAL_LABEL_ENCODER_SAVE_PATH = "/shared/model_trainer/results/{model_id}/label_encoders" #stored in the shared folder which is connected to s3-ferry + +S3_FERRY_MODEL_STORAGE_PATH = "/models" #folder path in s3 bucket + + +BASE_MODEL_FILENAME = "base_model_trainable_layers_{model_id}" + +CLASSIFIER_MODEL_FILENAME = "classifier_{model_id}.pth" -URL_MODEL= "http://ruuter-private:8088/classifier/datamodel/metadata" -URL_DEPLOY = "http://localhost:8088/classifier/datamodel/deployment/{deployment_platform}/update" diff --git a/model_trainer/datapipeline.py b/model_trainer/datapipeline.py index 818caed2..89014ef2 100644 --- a/model_trainer/datapipeline.py +++ b/model_trainer/datapipeline.py @@ -1,41 +1,46 @@ import pandas as pd import requests -from constants import * - +from constants import DATA_DOWNLOAD_ENDPOINT,GET_DATASET_METADATA_ENDPOINT,TRAINING_LOGS_PATH +from loguru import logger +logger.add(sink=TRAINING_LOGS_PATH) class DataPipeline: - def __init__(self, dgId,cookie): - print("start __init__ DataPipeline") + def __init__(self, dg_id,cookie): + + - url_data = URL_DATA - url_hierarchy = URL_HIERARCHY + logger.info(f"DOWNLOADING DATASET WITH DGID - {dg_id}") cookies = {'customJwtCookie': cookie} - response_data = requests.get(url_data, params={'dgId': dgId}, cookies=cookies) - if response_data.status_code == 200: - print("success data successfully loaded") - data = response_data.json() + response = requests.get(DATA_DOWNLOAD_ENDPOINT, params={'dgId': dg_id}, cookies=cookies) + + if response.status_code == 200: + logger.info("DATA DOWNLOAD SUCCESSFUL") + data = response.json() df = pd.DataFrame(data) df = df.drop('rowId', axis=1) self.df = df else: - print(f"Failed with status code response_data: {response_data.status_code}") - print(response_data.text) + logger.error(f"DATA DOWNLOAD FAILED WITH ERROR CODE: {response.status_code}") + logger.error(f"RESPONSE: {response.text}") - + raise RuntimeError(f"ERROR RESPONSE {response.text}") - response_hierarchy = requests.get(url_hierarchy, params={'groupId': dgId}, cookies=cookies) + + response_hierarchy = requests.get(GET_DATASET_METADATA_ENDPOINT, params={'groupId': dg_id}, cookies=cookies) if response_hierarchy.status_code == 200: - print("success hierarchy successfully loaded") + logger.info("DATASET HIERARCHY RETREIVAL SUCCESSFUL") hierarchy = response_hierarchy.json() + self.hierarchy = hierarchy['response']['data'][0] + else: - print(f"Failed with status code response_hierarchy: {response_hierarchy.status_code}") - print(response_hierarchy.text) + logger.error(f"DATASET HIERARCHY RETRIEVAL FAILED: {response_hierarchy.status_code}") + logger.error(f"RESPONSE: {response.text}") + raise RuntimeError(f"ERROR RESPONSE\n {response_hierarchy.text}") - self.hierarchy_file = hierarchy['response']['data'][0] def find_target_column(self,df , filter_list): @@ -51,13 +56,13 @@ def find_target_column(self,df , filter_list): def extract_input_columns(self): - validationRules = self.hierarchy_file['validationCriteria']['validationRules'] + validationRules = self.hierarchy['validationCriteria']['validationRules'] input_columns = [key for key, value in validationRules.items() if value['isDataClass'] == False] return input_columns def models_and_filters(self): - data = self.hierarchy_file['classHierarchy'] + data = self.hierarchy['classHierarchy'] models = [] filters = [] model_num = 1 @@ -92,6 +97,9 @@ def filter_dataframe_by_values(self, filters): return filtered_df def create_dataframes(self): + + logger.info("CREATING DATAFRAME") + dfs = [] input_columns = self.extract_input_columns() models, filters = self.models_and_filters() diff --git a/model_trainer/main.py b/model_trainer/main.py new file mode 100644 index 00000000..daef88cf --- /dev/null +++ b/model_trainer/main.py @@ -0,0 +1,24 @@ +import os +from loguru import logger +from model_trainer import ModelTrainer +from constants import TRAINING_LOGS_PATH + +logger.add(sink=TRAINING_LOGS_PATH) + +cookie = os.environ.get('cookie') +new_model_id = os.environ.get('newModelId') +old_model_id = os.environ.get('modelId') + +logger.info(f"COOKIE - {cookie}") +logger.info(f"OLD MODEL ID {old_model_id}") +logger.info(f"NEW MODEL ID - {new_model_id}") + +logger.info(f"ENTERING MODEL TRAINER SCRIPT FOR MODEL ID - {old_model_id}") + + + +trainer = ModelTrainer(cookie=cookie,new_model_id=new_model_id,old_model_id=old_model_id) +trainer.train() + +logger.info("TRAINING SCRIPT COMPLETED") + diff --git a/model_trainer/model_handler.py b/model_trainer/model_handler.py deleted file mode 100644 index 4ab42579..00000000 --- a/model_trainer/model_handler.py +++ /dev/null @@ -1,28 +0,0 @@ -import time -import random -import string -from datetime import datetime - -class EnvironmentPrinter: - def __init__(self): - self.generated_id = self.generate_random_id() - - def generate_random_id(self): - return ''.join(random.choices(string.ascii_letters + string.digits, k=8)) - - def print_id_with_timestamp(self): - current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - print(f'Generated ID: {self.generated_id}, Timestamp: {current_time}') - - def execute(self): - self.print_id_with_timestamp() - - for _ in range(2): - time.sleep(15) - self.print_id_with_timestamp() - - return True - -if __name__ == "__main__": - env_printer = EnvironmentPrinter() - env_printer.execute() diff --git a/model_trainer/model_trainer.py b/model_trainer/model_trainer.py index 218d7e62..c0fd3a16 100644 --- a/model_trainer/model_trainer.py +++ b/model_trainer/model_trainer.py @@ -6,106 +6,166 @@ import pickle import shutil from s3_ferry import S3Ferry -from constants import URL_MODEL, URL_DEPLOY +from constants import GET_MODEL_METADATA_ENDPOINT, DEPLOYMENT_ENDPOINT,TRAINING_LOGS_PATH, MODEL_RESULTS_PATH, \ + LOCAL_BASEMODEL_TRAINED_LAYERS_SAVE_PATH,LOCAL_CLASSIFICATION_LAYER_SAVE_PATH, \ + LOCAL_LABEL_ENCODER_SAVE_PATH, S3_FERRY_MODEL_STORAGE_PATH +from loguru import logger + +logger.add(sink=TRAINING_LOGS_PATH) + +#TODO - REFACTOR CODE TO CREATE A GENERIC FUNCTION HERE WHICH WILL CONSTRUCT AND RETURN THE CONSTANTS IN A DICTIONARY WHICH CAN BE REFERENCED IN ALL PARTS OF THE CODE class ModelTrainer: - def __init__(self) -> None: - - cookie = os.environ.get('COOKIE') - newModelId = os.environ.get('NEW_MODEL_ID') - oldModelId = os.environ.get('OLD_MODEL_ID') + def __init__(self, cookie, new_model_id,old_model_id) -> None: - model_url = URL_MODEL + model_url = GET_MODEL_METADATA_ENDPOINT - self.newModelId = newModelId - self.oldModelId = oldModelId - cookies = {'customJwtCookie': cookie} + self.new_model_id = new_model_id + self.old_model_id = old_model_id self.cookie = cookie - response = requests.get(model_url, params = {'modelId': newModelId}, cookies=cookies) + + cookies = {'customJwtCookie': cookie} + + logger.info("GETTING MODEL METADATA") + + response = requests.get(model_url, params = {'modelId': self.new_model_id}, cookies=cookies) + if response.status_code == 200: self.model_details = response.json() - print("success") + logger.info("SUCCESSFULLY RECIEVED MODEL DETAILS") else: - print(f"Failed with status code: {response.status_code}") + + logger.error(f"FAILED WITH STATUS CODE: {response.status_code}") + logger.error(f"RESPONSE: {response.text}") + + raise RuntimeError(f"RESPONSE : {response.text}") + + @staticmethod + def create_training_folders(folder_paths): + + logger.info("CREATING FOLDER PATHS") + + try: + + for folder_path in folder_paths: + if not os.path.exists(folder_path): + os.makedirs(folder_path) + + logger.success(f"SUCCESSFULLY CREATED MODEL FOLDER PATHS : {folder_paths}") + + except Exception as e: + + logger.error(f"FAILED TO CREATE MODEL FOLDER PATHS : {folder_paths}") + raise RuntimeError(e) def train(self): + s3_ferry = S3Ferry() - dgId = self.model_details['response']['data'][0]['connectedDgId'] - data_pipeline = DataPipeline(dgId, self.cookie) + dg_id = self.model_details['response']['data'][0]['connectedDgId'] + data_pipeline = DataPipeline(dg_id, self.cookie) dfs = data_pipeline.create_dataframes() - models_dets,_ = data_pipeline.models_and_filters() + models_inference_metadata,_ = data_pipeline.models_and_filters() models_to_train = self.model_details['response']['data'][0]['baseModels'] - results_rt_paths = [f"results/saved_models", f"results/classifiers", f"results/saved_label_encoders"] - for path in results_rt_paths: - if not os.path.exists(path): - os.makedirs(path) + local_basemodel_layers_save_path = LOCAL_BASEMODEL_TRAINED_LAYERS_SAVE_PATH.format(model_id=self.new_model_id) + local_classification_layer_save_path = LOCAL_CLASSIFICATION_LAYER_SAVE_PATH.format(model_id=self.new_model_id) + local_label_encoder_save_path = LOCAL_LABEL_ENCODER_SAVE_PATH.format(model_id=self.new_model_id) + + + ModelTrainer.create_training_folders([local_basemodel_layers_save_path, + local_classification_layer_save_path, + local_label_encoder_save_path]) - with open('results/models_dets.pkl', 'wb') as file: - pickle.dump(models_dets, file) + with open(f'{MODEL_RESULTS_PATH}/{self.new_model_id}/models_dets.pkl', 'wb') as file: + pickle.dump(models_inference_metadata, file) - models_list = [] - classifiers_list = [] - label_encoders_list = [] + selected_models = [] + selected_classifiers = [] + selected_label_encoders = [] average_accuracy = [] + logger.info(f"MODELS TO BE TRAINED: {models_to_train}") + for i in range(len(models_to_train)): training_pipeline = TrainingPipeline(dfs, models_to_train[i]) metrics, models, classifiers, label_encoders = training_pipeline.train() - models_list.append(models) - classifiers_list.append(classifiers) - label_encoders_list.append(label_encoders) + selected_models.append(models) + selected_classifiers.append(classifiers) + selected_label_encoders.append(label_encoders) average = sum(metrics[1]) / len(metrics[1]) average_accuracy.append(average) - + max_value_index = average_accuracy.index(max(average_accuracy)) - best_models = models_list[max_value_index] - best_classifiers = classifiers_list[max_value_index] - best_label_encoders = label_encoders_list[max_value_index] - model_name = models_to_train[max_value_index] - for i, (model, classifier, label_encoder) in enumerate(zip(best_models, best_classifiers, best_label_encoders)): - if model_name == 'distil-bert': - torch.save(model, f"results/saved_models/last_two_layers_dfs_{i}.pth") - elif model_name == 'roberta': - torch.save(model, f"results/saved_models/last_two_layers_dfs_{i}.pth") - elif model_name == 'bert': - torch.save(model, f"results/saved_models/last_two_layers_dfs_{i}.pth") - - torch.save(classifier, f"results/classifiers/classifier_{i}.pth") - - label_encoder_path = f"results/saved_label_encoders/label_encoder_{i}.pkl" - with open(label_encoder_path, 'wb') as file: - pickle.dump(label_encoder, file) + best_model_base = selected_models[max_value_index] + best_model_classifier = selected_classifiers[max_value_index] + best_model_label_encoder = selected_label_encoders[max_value_index] + best_model_name = models_to_train[max_value_index] + + logger.info("TRAINING COMPLETE") + logger.info(f"THE BEST PERFORMING MODEL IS {best_model_name}") + + torch.save(best_model_base, f"{local_basemodel_layers_save_path}/base_model_trainable_layers_{self.new_model_id}.pth") + torch.save(best_model_classifier, f"{local_classification_layer_save_path}/classifier_{self.new_model_id}.pth") + + label_encoder_path = f"{local_label_encoder_save_path}/label_encoder_{self.new_model_id}.pkl" + with open(label_encoder_path, 'wb') as file: + pickle.dump(best_model_label_encoder, file) + + + model_zip_path = f"{MODEL_RESULTS_PATH}/{str(self.new_model_id)}" + + shutil.make_archive(base_name=model_zip_path, root_dir=model_zip_path, format="zip") + + save_location = f"{S3_FERRY_MODEL_STORAGE_PATH}/{str(self.new_model_id)}/{str(self.new_model_id)}.zip" + source_location = f"{MODEL_RESULTS_PATH.replace('/shared/','')}/{str(self.new_model_id)}.zip" # Removing 'shared/' path here so that S3 ferry source file path works without any issue + + logger.info("INITIATING MODEL UPLOAD TO S3") + logger.info(f"SOURCE LOCATION - {source_location}") + logger.info(f"S3 SAVE LOCATION - {save_location}") - shutil.make_archive(f"{str(self.newModelId)}", 'zip', f"results") - save_location = f"shared/models/{str(self.newModelId)}/{str(self.newModelId)}.zip" - source_location = f"{str(self.newModelId)}.zip" response = s3_ferry.transfer_file(save_location, "S3", source_location, "FS") + if response.status_code == 201: - upload_status = {"message": "Model File Uploaded Successfully!", "saved_file_path": save_location} + logger.info(f"MODEL FILE UPLOADED SUCCESSFULLY TO {save_location}") else: - upload_status = {"message": "failed to Upload Model File!"} + logger.error(f"MODEL FILE UPLOAD TO {save_location} FAILED") + logger.error(f"RESPONSE: {response.text}") + raise RuntimeError(f"RESPONSE STATUS: {response.text}") - DeploymentPlatform = self.model_details['response']['data'][0]['deploymentEnv'] + + deployment_platform = self.model_details['response']['data'][0]['deploymentEnv'] - deploy_url = URL_DEPLOY.format(deployment_platform = DeploymentPlatform) + logger.info(f"INITIATING DEPLOYMENT TO {deployment_platform}") + + deploy_url = DEPLOYMENT_ENDPOINT.format(deployment_platform = deployment_platform) - if self.oldModelId is not None: + ## CODE SHOULD BE UPDATED TO CHECK WHETHER old_model_id == new_model_id (because that is how ruuter sends the request if it's a model create operation) + if self.old_model_id is not None: payload = { - "modelId": self.newModelId, + "modelId": self.new_model_id, "replaceDeployment": True, - "replaceDeploymentPlatform":DeploymentPlatform, - "bestModelName":model_name + "replaceDeploymentPlatform":deployment_platform, + "bestModelName":best_model_name } else: payload = { - "modelId": self.newModelId, + "modelId": self.new_model_id, "replaceDeployment": False, - "replaceDeploymentPlatform": DeploymentPlatform, - "bestModelName":model_name + "replaceDeploymentPlatform": deployment_platform, + "bestModelName":best_model_name } response = requests.post(deploy_url, json=payload) + + if response.status_code == 201 or response.status_code == 200: + logger.info(f"{deployment_platform} DEPLOYMENT SUCCESSFUL") + + else: + logger.error(f"{deployment_platform} DEPLOYMENT FAILED") + logger.info(f"RESPONSE: {response.text}") + + raise RuntimeError(f"RESPONSE : {response.text}") + diff --git a/model_trainer/model_upload_requirements.txt b/model_trainer/model_trainer_requirements.txt similarity index 98% rename from model_trainer/model_upload_requirements.txt rename to model_trainer/model_trainer_requirements.txt index f0cf53d6..7a52b9b3 100644 --- a/model_trainer/model_upload_requirements.txt +++ b/model_trainer/model_trainer_requirements.txt @@ -59,3 +59,4 @@ urllib3==2.2.2 uvicorn==0.30.5 watchfiles==0.22.0 websockets==12.0 +loguru==0.7.2 diff --git a/model_trainer/s3_ferry.py b/model_trainer/s3_ferry.py index 1b63182f..9e91bb13 100644 --- a/model_trainer/s3_ferry.py +++ b/model_trainer/s3_ferry.py @@ -1,20 +1,24 @@ import requests +from loguru import logger +from constants import TRAINING_LOGS_PATH + +logger.add(sink=TRAINING_LOGS_PATH) class S3Ferry: def __init__(self): self.url = "http://s3-ferry:3000/v1/files/copy" - def transfer_file(self, destinationFilePath, destinationStorageType, sourceFilePath, sourceStorageType): - payload = self.get_s3_ferry_payload(destinationFilePath, destinationStorageType, sourceFilePath, sourceStorageType) + def transfer_file(self, destination_file_path, destination_storage_type, source_file_path, source_storage_type): + payload = self.get_s3_ferry_payload(destination_file_path, destination_storage_type, source_file_path, source_storage_type) response = requests.post(self.url, json=payload) return response - def get_s3_ferry_payload(self, destinationFilePath:str, destinationStorageType:str, sourceFilePath:str, sourceStorageType:str): + def get_s3_ferry_payload(self, destination_file_path:str, destination_storage_type:str, source_file_path:str, source_storage_type:str): S3_FERRY_PAYLOAD = { - "destinationFilePath": destinationFilePath, - "destinationStorageType": destinationStorageType, - "sourceFilePath": sourceFilePath, - "sourceStorageType": sourceStorageType + "destinationFilePath": destination_file_path, + "destinationStorageType": destination_storage_type, + "sourceFilePath": source_file_path, + "sourceStorageType": source_storage_type } return S3_FERRY_PAYLOAD diff --git a/model_trainer/trainingpipeline.py b/model_trainer/trainingpipeline.py index 0503b05d..3e5d9b8c 100644 --- a/model_trainer/trainingpipeline.py +++ b/model_trainer/trainingpipeline.py @@ -7,13 +7,15 @@ import shutil import pandas as pd import os - +from constants import TRAINING_LOGS_PATH +from loguru import logger from transformers import logging import warnings warnings.filterwarnings("ignore", message="Some weights of the model checkpoint were not used when initializing") logging.set_verbosity_error() +logger.add(sink=TRAINING_LOGS_PATH) class CustomDataset(Dataset): def __init__(self, encodings, labels): @@ -30,7 +32,7 @@ def __len__(self): device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') -print(device) +logger.info(f"TRAINING HARDWARE {device}") class TrainingPipeline: @@ -73,6 +75,8 @@ def train(self): models = [] classifiers = [] label_encoders =[] + + logger.info(f"INITIATING TRAINING FOR {self.model_name} MODEL") for i in range(len(self.dfs)): current_df = self.dfs[i] if len(current_df) < 10: diff --git a/src/unitTesting.sh b/src/unitTesting.sh old mode 100644 new mode 100755 diff --git a/token.sh b/token.sh old mode 100644 new mode 100755 From 1efb14b1f24690d50905f9204c14464de51079be Mon Sep 17 00:00:00 2001 From: Thirunayan22 Date: Mon, 19 Aug 2024 17:08:52 +0530 Subject: [PATCH 498/582] corrected validationRules variable from camelCase to snake_case in datapipeline.py --- model_trainer/datapipeline.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/model_trainer/datapipeline.py b/model_trainer/datapipeline.py index 89014ef2..11490a6d 100644 --- a/model_trainer/datapipeline.py +++ b/model_trainer/datapipeline.py @@ -56,9 +56,9 @@ def find_target_column(self,df , filter_list): def extract_input_columns(self): - validationRules = self.hierarchy['validationCriteria']['validationRules'] + validation_rules = self.hierarchy['validationCriteria']['validationRules'] - input_columns = [key for key, value in validationRules.items() if value['isDataClass'] == False] + input_columns = [key for key, value in validation_rules.items() if value['isDataClass'] == False] return input_columns def models_and_filters(self): From aeab210939ec44a41be054493f79bee3b9c00dc9 Mon Sep 17 00:00:00 2001 From: Thirunayan22 Date: Mon, 19 Aug 2024 18:52:07 +0530 Subject: [PATCH 499/582] updated DSL/Liquibase/changelog/classifier-script-v9-models-metadata.sql to in-progress as ENUM --- .gitignore | 8 ++++++- .../classifier-script-v9-models-metadata.sql | 2 +- config.env | 9 ------- constants.ini | 24 ------------------- model_trainer/datapipeline.py | 2 -- 5 files changed, 8 insertions(+), 37 deletions(-) delete mode 100644 config.env delete mode 100644 constants.ini diff --git a/.gitignore b/.gitignore index 28a112d4..828c8c15 100644 --- a/.gitignore +++ b/.gitignore @@ -406,4 +406,10 @@ FodyWeavers.xsd .DS_Store DSL/Liquibase/data/update_refresh_token.sql model_trainer/results -protected_configs/ \ No newline at end of file +protected_configs/ + +./config.env +./constants.ini + +config.env +constants.ini diff --git a/DSL/Liquibase/changelog/classifier-script-v9-models-metadata.sql b/DSL/Liquibase/changelog/classifier-script-v9-models-metadata.sql index 01cb5580..5f1ae114 100644 --- a/DSL/Liquibase/changelog/classifier-script-v9-models-metadata.sql +++ b/DSL/Liquibase/changelog/classifier-script-v9-models-metadata.sql @@ -7,7 +7,7 @@ CREATE TYPE Maturity_Label AS ENUM ('development', 'staging', 'production ready' CREATE TYPE Deployment_Env AS ENUM ('jira', 'outlook', 'pinal', 'testing', 'undeployed'); -- changeset kalsara Magamage:classifier-script-v9-changeset3 -CREATE TYPE Training_Status AS ENUM ('not trained', 'training in progress', 'trained', 'retraining needed', 'untrainable'); +CREATE TYPE Training_Status AS ENUM ('not trained', 'training in-progress', 'trained', 'retraining needed', 'untrainable'); -- changeset kalsara Magamage:classifier-script-v9-changeset4 CREATE TYPE Base_Models AS ENUM ('distil-bert', 'roberta', 'bert'); diff --git a/config.env b/config.env deleted file mode 100644 index ffcbe12d..00000000 --- a/config.env +++ /dev/null @@ -1,9 +0,0 @@ -API_CORS_ORIGIN=* -API_DOCUMENTATION_ENABLED=true -S3_REGION=value -S3_ENDPOINT_URL=value -S3_DATA_BUCKET_NAME=value -S3_DATA_BUCKET_PATH=value -FS_DATA_DIRECTORY_PATH=value -S3_ACCESS_KEY_ID=value -S3_SECRET_ACCESS_KEY=value diff --git a/constants.ini b/constants.ini deleted file mode 100644 index 0e50b4dd..00000000 --- a/constants.ini +++ /dev/null @@ -1,24 +0,0 @@ -[DSL] - -CLASSIFIER_RUUTER_PUBLIC=http://ruuter-public:8086 -CLASSIFIER_RUUTER_PRIVATE=http://ruuter-private:8088 -CLASSIFIER_RUUTER_PUBLIC_INTERNAL=http://localhost:8086 -CLASSIFIER_DMAPPER=http://data-mapper:3000 -CLASSIFIER_RESQL=http://resql:8082 -CLASSIFIER_TIM=http://tim:8085 -CLASSIFIER_CRON_MANAGER=http://cron-manager:9010 -CLASSIFIER_FILE_HANDLER=http://file-handler:8000 -CLASSIFIER_NOTIFICATIONS=http://notifications-node:4040 -CLASSIFIER_ANONYMIZER=http://anonymizer:8010 -CLASSIFIER_RUUTER_PUBLIC_FRONTEND_URL=value -DOMAIN=localhost -JIRA_API_TOKEN=value -JIRA_USERNAME=value -JIRA_CLOUD_DOMAIN=value -JIRA_WEBHOOK_ID=value -JIRA_WEBHOOK_SECRET=value -OUTLOOK_CLIENT_ID=value -OUTLOOK_SECRET_KEY=value -OUTLOOK_REFRESH_KEY=value -OUTLOOK_SCOPE=User.Read Mail.ReadWrite MailboxSettings.ReadWrite offline_access -DB_PASSWORD=value \ No newline at end of file diff --git a/model_trainer/datapipeline.py b/model_trainer/datapipeline.py index 11490a6d..d8aa3959 100644 --- a/model_trainer/datapipeline.py +++ b/model_trainer/datapipeline.py @@ -8,8 +8,6 @@ class DataPipeline: def __init__(self, dg_id,cookie): - - logger.info(f"DOWNLOADING DATASET WITH DGID - {dg_id}") From a11f8fc831caaf1dd6ab5e51d1c689745472b4f6 Mon Sep 17 00:00:00 2001 From: Thirunayan22 Date: Mon, 19 Aug 2024 19:08:14 +0530 Subject: [PATCH 500/582] updated database filepath --- docker-compose.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 92091acb..cd735d42 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -164,8 +164,7 @@ services: ports: - 5433:5432 volumes: - # - /home/ubuntu/user_db_files:/var/lib/postgresql/data - - ~/est_classifier/user_db_files:/var/lib/postgresql/data #For Mac Users + - ~/buerokratt_classifier/db_files:/var/lib/postgresql/data networks: bykstack: From b115a76410d1ac3c6720815a465fab95088957ea Mon Sep 17 00:00:00 2001 From: Thirunayan22 Date: Tue, 20 Aug 2024 00:56:12 +0530 Subject: [PATCH 501/582] completed model training, storage and status update; hot fix pending for model_trainer progress status calling issue --- DSL/Resql/update-data-model-training-data.sql | 3 +- .../datamodel/update/training/status.yml | 5 + model_trainer/constants.py | 37 ++++ model_trainer/model_trainer.py | 166 ++++++++++++++++-- 4 files changed, 197 insertions(+), 14 deletions(-) diff --git a/DSL/Resql/update-data-model-training-data.sql b/DSL/Resql/update-data-model-training-data.sql index 9fad3c72..1dace3b4 100644 --- a/DSL/Resql/update-data-model-training-data.sql +++ b/DSL/Resql/update-data-model-training-data.sql @@ -3,5 +3,6 @@ SET training_status = :training_status::Training_Status, model_s3_location = :model_s3_location, last_trained_timestamp = :last_trained_timestamp::timestamp with time zone, - training_results = :training_results::jsonb + training_results = :training_results::jsonb, + inference_routes = :inference_routes::jsonb WHERE id = :id; diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update/training/status.yml b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update/training/status.yml index 70430734..354d9052 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update/training/status.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update/training/status.yml @@ -23,6 +23,9 @@ declaration: - field: trainingResults type: json description: "Body field 'trainingResults'" + - field: inferenceRoutes + type: json + description: "Body field 'inferenceRoutes'" extract_request_data: assign: @@ -31,6 +34,7 @@ extract_request_data: model_s3_location: ${incoming.body.modelS3Location} last_trained_timestamp: ${incoming.body.lastTrainedTimestamp} training_results: ${incoming.body.trainingResults} + inference_routes: ${incoming.body.inferenceRoutes} next: check_for_request_data check_for_request_data: @@ -70,6 +74,7 @@ update_training_data: model_s3_location: ${model_s3_location} last_trained_timestamp: ${new Date(last_trained_timestamp).toISOString()} training_results: ${JSON.stringify(training_results)} + inference_routes: ${JSON.stringify(inference_routes)} result: res_update next: check_data_model_update_status diff --git a/model_trainer/constants.py b/model_trainer/constants.py index 3dddafc2..e099a278 100644 --- a/model_trainer/constants.py +++ b/model_trainer/constants.py @@ -7,6 +7,12 @@ GET_MODEL_METADATA_ENDPOINT= "http://ruuter-private:8088/classifier/datamodel/metadata" +UPDATE_MODEL_TRAINING_STATUS_ENDPOINT = "http://ruuter-private:8088/classifier/datamodel/update/training/status" + +CREATE_TRAINING_PROGRESS_SESSION_ENDPOINT = "http://ruuter-private:8088/classifier/datamodel/progress/create" + +UPDATE_TRAINING_PROGRESS_SESSION_ENDPOINT = "http://ruuter-private:8088/classifier/datamodel/progress/update" + DEPLOYMENT_ENDPOINT = "http://ruuter-private:8088/classifier/datamodel/deployment/{deployment_platform}/update" TRAINING_LOGS_PATH = "/app/model_trainer/training_logs.log" @@ -26,4 +32,35 @@ CLASSIFIER_MODEL_FILENAME = "classifier_{model_id}.pth" +MODEL_TRAINING_IN_PROGRESS = "training in-progress" + +MODEL_TRAINING_SUCCESSFUL = "trained" + + +# MODEL TRAINING PROGRESS SESSION CONSTANTS + +INITIATING_TRAINING_PROGRESS_STATUS = "Initiating Training" + +TRAINING_IN_PROGRESS_PROGRESS_STATUS = "Training In-Progress" + +DEPLOYING_MODEL_PROGRESS_STATUS = "Deploying Model" + +MODEL_TRAINED_AND_DEPLOYED_PROGRESS_STATUS = "Model Trained And Deployed" + + +INITIATING_TRAINING_PROGRESS_MESSAGE = "Download and preparing dataset" + +TRAINING_IN_PROGRESS_PROGRESS_MESSAGE = "The dataset is being trained on all selected models" + +DEPLOYING_MODEL_PROGRESS_MESSAGE = "Model training complete. The trained model is now being deployed to the {deployment_environment} enivronment" + +MODEL_TRAINED_AND_DEPLOYED_PROGRESS_MESSAGE = "The model was trained and deployed successfully to the {deployment_environment} environment" + + +INITIATING_TRAINING_PROGRESS_PERCENTAGE=20 + +TRAINING_IN_PROGRESS_PROGRESS_PERCENTAGE=50 + +DEPLOYING_MODEL_PROGRESS_PERCENTAGE=80 +MODEL_TRAINED_AND_DEPLOYED_PROGRESS_PERCENTAGE=100 diff --git a/model_trainer/model_trainer.py b/model_trainer/model_trainer.py index c0fd3a16..dc6f7e06 100644 --- a/model_trainer/model_trainer.py +++ b/model_trainer/model_trainer.py @@ -5,10 +5,14 @@ import torch import pickle import shutil +from datetime import datetime from s3_ferry import S3Ferry -from constants import GET_MODEL_METADATA_ENDPOINT, DEPLOYMENT_ENDPOINT,TRAINING_LOGS_PATH, MODEL_RESULTS_PATH, \ +from constants import GET_MODEL_METADATA_ENDPOINT, DEPLOYMENT_ENDPOINT, UPDATE_MODEL_TRAINING_STATUS_ENDPOINT, CREATE_TRAINING_PROGRESS_SESSION_ENDPOINT, UPDATE_TRAINING_PROGRESS_SESSION_ENDPOINT, TRAINING_LOGS_PATH, MODEL_RESULTS_PATH, \ LOCAL_BASEMODEL_TRAINED_LAYERS_SAVE_PATH,LOCAL_CLASSIFICATION_LAYER_SAVE_PATH, \ - LOCAL_LABEL_ENCODER_SAVE_PATH, S3_FERRY_MODEL_STORAGE_PATH + LOCAL_LABEL_ENCODER_SAVE_PATH, S3_FERRY_MODEL_STORAGE_PATH, MODEL_TRAINING_IN_PROGRESS, MODEL_TRAINING_SUCCESSFUL, \ + INITIATING_TRAINING_PROGRESS_STATUS, TRAINING_IN_PROGRESS_PROGRESS_STATUS, DEPLOYING_MODEL_PROGRESS_STATUS, MODEL_TRAINED_AND_DEPLOYED_PROGRESS_STATUS, \ + INITIATING_TRAINING_PROGRESS_MESSAGE, TRAINING_IN_PROGRESS_PROGRESS_MESSAGE, DEPLOYING_MODEL_PROGRESS_MESSAGE, MODEL_TRAINED_AND_DEPLOYED_PROGRESS_MESSAGE, \ + INITIATING_TRAINING_PROGRESS_PERCENTAGE, TRAINING_IN_PROGRESS_PROGRESS_PERCENTAGE, DEPLOYING_MODEL_PROGRESS_PERCENTAGE, MODEL_TRAINED_AND_DEPLOYED_PROGRESS_PERCENTAGE from loguru import logger logger.add(sink=TRAINING_LOGS_PATH) @@ -24,11 +28,11 @@ def __init__(self, cookie, new_model_id,old_model_id) -> None: self.old_model_id = old_model_id self.cookie = cookie - cookies = {'customJwtCookie': cookie} + self.cookies_payload = {'customJwtCookie': cookie} logger.info("GETTING MODEL METADATA") - response = requests.get(model_url, params = {'modelId': self.new_model_id}, cookies=cookies) + response = requests.get(model_url, params = {'modelId': self.new_model_id}, cookies=self.cookies_payload) if response.status_code == 200: self.model_details = response.json() @@ -57,8 +61,132 @@ def create_training_folders(folder_paths): logger.error(f"FAILED TO CREATE MODEL FOLDER PATHS : {folder_paths}") raise RuntimeError(e) + + + def update_model_db_training_status(self,training_status, model_s3_location, + last_trained_time_stamp,training_results, inference_routes): + + payload = {} + payload["modelId"] = int(self.new_model_id) + payload["trainingStatus"] = training_status + payload["modelS3Location"] = model_s3_location + payload["lastTrainedTimestamp"] = last_trained_time_stamp + payload["trainingResults"] = training_results + payload["inferenceRoutes"] = {"inference_routes":inference_routes} + + logger.info(f"{training_status} UPLOAD PAYLOAD - \n {payload}") + + response = requests.post( url=UPDATE_MODEL_TRAINING_STATUS_ENDPOINT, + json=payload, cookies=self.cookies_payload) + + if response.status_code==200: + logger.info(f"REQUEST TO UPDATE MODEL TRAINING STATUS TO {training_status} SUCCESSFUL") + + else: + logger.error(f"REQUEST TO UPDATE MODEL TRAINING STATUS TO {training_status} FAILED") + logger.error(f"ERROR RESPONSE {response.text}") + raise RuntimeError(response.text) + + def create_model_training_progress_session(self): + + payload = {} + session_id = None + model_details = self.model_details['response']['data'][0] + payload["modelId"] = self.new_model_id + payload["modelName"] = model_details["modelName"] + payload["majorVersion"] = model_details["majorVersion"] + payload["minorVersion"] = model_details["minorVersion"] + payload["latest"] = model_details["latest"] + + + logger.info(f"Create training progress session for model id - {self.new_model_id} payload \n {payload}") + + response = requests.post( url=CREATE_TRAINING_PROGRESS_SESSION_ENDPOINT, + json=payload, cookies=self.cookies_payload) + + + if response.status_code==200: + + logger.info(f"REQUEST TO CREATE TRAINING PROGRESS SESSION FOR MODEL ID {self.new_model_id} SUCCESSFUL") + logger.info(f"RESPONSE PAYLOAD \n {response.json()}") + session_id = response.json()["sessionId"] + + + else: + logger.error(f"REQUEST TO CREATE TRAINING PROGRESS SESSION FOR MODEL ID {self.new_model_id} FAILED") + logger.error(f"ERROR RESPONSE JSON {response.json()}") + logger.error(f"ERROR RESPONSE TEXT {response.text}") + raise RuntimeError(response.text) + + + return session_id + + + def update_model_training_progress_session(self,session_id,training_status, + training_progress_update_message, training_progress_percentage, + process_complete): + + payload = {} + + payload["sessionId"] = session_id + payload["trainingStatus"] = training_status + payload["trainingMessage"] = training_progress_update_message + payload["progressPercentage"] = training_progress_percentage + payload["processComplete"] = process_complete + + logger.info(f"Update training progress session for model id - {self.new_model_id} payload \n {payload}") + + response=requests.post( url=UPDATE_TRAINING_PROGRESS_SESSION_ENDPOINT, + json=payload, cookies=self.cookies_payload) + + if response.status_code==200: + + logger.info(f"REQUEST TO UPEQ%# TRAINING PROGRESS SESSION FOR MODEL ID {self.new_model_id} SUCCESSFUL") + logger.info(f"RESPONSE PAYLOAD \n {response.json()}") + session_id = response.json()["sessionId"] + + + else: + logger.error(f"REQUEST TO UPDATE TRAINING PROGRESS SESSION FOR MODEL ID {self.new_model_id} FAILED") + logger.error(f"ERROR RESPONSE JSON {response.json()}") + logger.error(f"ERROR RESPONSE TEXT {response.text}") + raise RuntimeError(response.text) + + + return session_id + + + + def train(self): + + #updating model training status to in-progress + current_timestamp = int(datetime.now().timestamp()) + self.update_model_db_training_status(training_status=MODEL_TRAINING_IN_PROGRESS, + model_s3_location="", + last_trained_time_stamp=current_timestamp, + training_results={}, + inference_routes={}) + + + ############### TODO - THIS IS A TEST CALLING OF THE PROGRESS SESSION CREATE AND UPDATE ENDPOINTS - THE ACTUAL CALLING REFERENCES SHOULD BE PLACED IN THE RIGHT PLACED WITHIN THIS FUNCTION AND THE INFERENCE ENDPOINTS + + deployment_platform = self.model_details['response']['data'][0]['deploymentEnv'] + + session_id = self.create_model_training_progress_session() + + self.update_model_training_progress_session(session_id=session_id, + training_status=INITIATING_TRAINING_PROGRESS_STATUS, + training_progress_update_message=INITIATING_TRAINING_PROGRESS_MESSAGE.format(deployment_platform=deployment_platform), + training_progress_percentage=INITIATING_TRAINING_PROGRESS_PERCENTAGE, + process_complete=False + ) + + + ############### TODO - THIS IS A TEST CALLING OF THE PROGRESS SESSION CREATE AND UPDATE ENDPOINTS - THE ACTUAL CALLING REFERENCES SHOULD BE PLACED IN THE RIGHT PLACED WITHIN THIS FUNCTION AND THE INFERENCE ENDPOINTS + + s3_ferry = S3Ferry() dg_id = self.model_details['response']['data'][0]['connectedDgId'] @@ -76,9 +204,12 @@ def train(self): local_classification_layer_save_path, local_label_encoder_save_path]) + + with open(f'{MODEL_RESULTS_PATH}/{self.new_model_id}/models_dets.pkl', 'wb') as file: pickle.dump(models_inference_metadata, file) + selected_models = [] selected_classifiers = [] selected_label_encoders = [] @@ -115,25 +246,34 @@ def train(self): shutil.make_archive(base_name=model_zip_path, root_dir=model_zip_path, format="zip") - save_location = f"{S3_FERRY_MODEL_STORAGE_PATH}/{str(self.new_model_id)}/{str(self.new_model_id)}.zip" - source_location = f"{MODEL_RESULTS_PATH.replace('/shared/','')}/{str(self.new_model_id)}.zip" # Removing 'shared/' path here so that S3 ferry source file path works without any issue + s3_save_location = f"{S3_FERRY_MODEL_STORAGE_PATH}/{str(self.new_model_id)}/{str(self.new_model_id)}.zip" + local_source_location = f"{MODEL_RESULTS_PATH.replace('/shared/','')}/{str(self.new_model_id)}.zip" # Removing 'shared/' path here so that S3 ferry source file path works without any issue logger.info("INITIATING MODEL UPLOAD TO S3") - logger.info(f"SOURCE LOCATION - {source_location}") - logger.info(f"S3 SAVE LOCATION - {save_location}") + logger.info(f"SOURCE LOCATION - {local_source_location}") + logger.info(f"S3 SAVE LOCATION - {s3_save_location}") - response = s3_ferry.transfer_file(save_location, "S3", source_location, "FS") + response = s3_ferry.transfer_file(s3_save_location, "S3", local_source_location, "FS") if response.status_code == 201: - logger.info(f"MODEL FILE UPLOADED SUCCESSFULLY TO {save_location}") + logger.info(f"MODEL FILE UPLOADED SUCCESSFULLY TO {s3_save_location}") else: - logger.error(f"MODEL FILE UPLOAD TO {save_location} FAILED") + logger.error(f"MODEL FILE UPLOAD TO {s3_save_location} FAILED") logger.error(f"RESPONSE: {response.text}") raise RuntimeError(f"RESPONSE STATUS: {response.text}") - - deployment_platform = self.model_details['response']['data'][0]['deploymentEnv'] + + ## Updating model training status to 'trained' and training results with the model training stats + ## TODO - The training results payload should be corrected formatted here to be sent to the database + current_timestamp = int(datetime.now().timestamp()) + self.update_model_db_training_status(training_status=MODEL_TRAINING_SUCCESSFUL, + model_s3_location=s3_save_location, + last_trained_time_stamp=current_timestamp, + training_results={}, + inference_routes=models_inference_metadata) + + logger.info(f"INITIATING DEPLOYMENT TO {deployment_platform}") From 1971a52bea10c6544ce1467a2c601985c822fdf1 Mon Sep 17 00:00:00 2001 From: Thirunayan22 Date: Tue, 20 Aug 2024 01:15:31 +0530 Subject: [PATCH 502/582] added progress update API call bugfix --- model_trainer/model_trainer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/model_trainer/model_trainer.py b/model_trainer/model_trainer.py index dc6f7e06..dae85369 100644 --- a/model_trainer/model_trainer.py +++ b/model_trainer/model_trainer.py @@ -92,7 +92,7 @@ def create_model_training_progress_session(self): payload = {} session_id = None model_details = self.model_details['response']['data'][0] - payload["modelId"] = self.new_model_id + payload["modelId"] = int(self.new_model_id) payload["modelName"] = model_details["modelName"] payload["majorVersion"] = model_details["majorVersion"] payload["minorVersion"] = model_details["minorVersion"] @@ -109,7 +109,7 @@ def create_model_training_progress_session(self): logger.info(f"REQUEST TO CREATE TRAINING PROGRESS SESSION FOR MODEL ID {self.new_model_id} SUCCESSFUL") logger.info(f"RESPONSE PAYLOAD \n {response.json()}") - session_id = response.json()["sessionId"] + session_id = response.json()["response"]["sessionId"] else: @@ -144,7 +144,7 @@ def update_model_training_progress_session(self,session_id,training_status, logger.info(f"REQUEST TO UPEQ%# TRAINING PROGRESS SESSION FOR MODEL ID {self.new_model_id} SUCCESSFUL") logger.info(f"RESPONSE PAYLOAD \n {response.json()}") - session_id = response.json()["sessionId"] + session_id = response.json()["response"]["sessionId"] else: From 237b23ea73fad5e23008f8151343633681c3d5d2 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Tue, 20 Aug 2024 11:17:57 +0530 Subject: [PATCH 503/582] new updates --- docker-compose.yml | 6 +++--- hierarchy_validation/utils.py | 6 +++--- model_inference/model_inference.py | 6 +++--- model_inference/model_inference_api.py | 8 ++++---- test_inference/test_inference.py | 6 +++--- test_inference/test_inference_api.py | 2 +- test_inference/test_inference_wrapper.py | 6 +----- test_inference/utils.py | 4 ---- 8 files changed, 18 insertions(+), 26 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index a757ccd3..7d9e7eef 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -288,7 +288,7 @@ services: - GET_DATAMODEL_METADATA_BY_ID_URL=http://ruuter-private:8088/classifier/datamodel/metadata?modelId=inferenceModelId - GET_DATASET_GROUP_METADATA_BY_ID_URL=http://ruuter-private:8088/classifier/datasetgroup/group/metadata?groupId=dataSetGroupId - CLASS_HIERARCHY_VALIDATION_URL=http://hierarchy-validation:8009/check-folder-hierarchy - - GET_OUTLOOK_ACCESS_TOKEN_URL=http://ruuter-public:8086/internal/validate + - OUTLOOK_ACCESS_TOKEN_API_URL=http://ruuter-public:8086/internal/validate - BUILD_CORRECTED_FOLDER_HIERARCHY_URL=http://hierarchy-validation:8009/corrected-folder-hierarchy - FIND_FINAL_FOLDER_ID_URL=http://hierarchy-validation:8009/find-folder-id - UPDATE_DATAMODEL_PROGRESS_URL=http://ruuter-private:8088/classifier/datamodel/progress/update @@ -312,7 +312,7 @@ services: - RUUTER_PRIVATE_URL=http://ruuter-private:8088 - S3_FERRY_URL=http://s3-ferry:3000/v1/files/copy - TEST_MODEL_DOWNLOAD_DIRECTORY=/shared/models/test - - GET_OUTLOOK_ACCESS_TOKEN_URL=http://ruuter-public:8086/internal/validate + - OUTLOOK_ACCESS_TOKEN_API_URL=http://ruuter-public:8086/internal/validate - UPDATE_DATAMODEL_PROGRESS_URL=http://ruuter-private:8088/classifier/datamodel/progress/update ports: - "8010:8010" @@ -336,7 +336,7 @@ services: - S3_FERRY_URL=http://s3-ferry:3000/v1/files/copy - JIRA_MODEL_DOWNLOAD_DIRECTORY=/shared/models/jira - OUTLOOK_MODEL_DOWNLOAD_DIRECTORY=/shared/models/outlook - - GET_OUTLOOK_ACCESS_TOKEN_URL=http://ruuter-public:8086/internal/validate + - OUTLOOK_ACCESS_TOKEN_API_URL=http://ruuter-public:8086/internal/validate ports: - "8009:8009" networks: diff --git a/hierarchy_validation/utils.py b/hierarchy_validation/utils.py index b430c098..184fa464 100644 --- a/hierarchy_validation/utils.py +++ b/hierarchy_validation/utils.py @@ -3,7 +3,7 @@ import requests import os from constants import (GRAPH_API_BASE_URL, Folder, ClassHierarchy) -GET_OUTLOOK_ACCESS_TOKEN_URL=os.getenv("GET_OUTLOOK_ACCESS_TOKEN_URL") +OUTLOOK_ACCESS_TOKEN_API_URL=os.getenv("OUTLOOK_ACCESS_TOKEN_API_URL") from typing import List async def fetch_folders(folder_id: str = 'root', outlook_access_token:str=''): @@ -102,8 +102,8 @@ def search_hierarchy(folders: List[Folder], target_id: str, current_path: List[s def get_outlook_access_token(model_id:int): try: - get_outlook_access_token_url = GET_OUTLOOK_ACCESS_TOKEN_URL - response = requests.post(get_outlook_access_token_url, json={"modelId": model_id}) + outlook_access_token_url = OUTLOOK_ACCESS_TOKEN_API_URL + response = requests.post(outlook_access_token_url, json={"modelId": model_id}) response.raise_for_status() data = response.json() diff --git a/model_inference/model_inference.py b/model_inference/model_inference.py index b9b556dd..a2b3cbb3 100644 --- a/model_inference/model_inference.py +++ b/model_inference/model_inference.py @@ -5,7 +5,7 @@ CREATE_INFERENCE_URL=os.getenv("CREATE_INFERENCE_URL") UPDATE_INFERENCE_URL=os.getenv("UPDATE_INFERENCE_URL") CLASS_HIERARCHY_VALIDATION_URL=os.getenv("CLASS_HIERARCHY_VALIDATION_URL") -GET_OUTLOOK_ACCESS_TOKEN_URL=os.getenv("GET_OUTLOOK_ACCESS_TOKEN_URL") +OUTLOOK_ACCESS_TOKEN_API_URL=os.getenv("OUTLOOK_ACCESS_TOKEN_API_URL") BUILD_CORRECTED_FOLDER_HIERARCHY_URL = os.getenv("BUILD_CORRECTED_FOLDER_HIERARCHY_URL") FIND_FINAL_FOLDER_ID_URL = os.getenv("FIND_FINAL_FOLDER_ID_URL") @@ -15,8 +15,8 @@ def __init__(self): def get_class_hierarchy_by_model_id(self, model_id): try: - get_outlook_access_token_url = GET_OUTLOOK_ACCESS_TOKEN_URL - response = requests.post(get_outlook_access_token_url, json={"modelId": model_id}) + outlook_access_token_url = OUTLOOK_ACCESS_TOKEN_API_URL + response = requests.post(outlook_access_token_url, json={"modelId": model_id}) response.raise_for_status() data = response.json() diff --git a/model_inference/model_inference_api.py b/model_inference/model_inference_api.py index b52485c5..84d4645d 100644 --- a/model_inference/model_inference_api.py +++ b/model_inference/model_inference_api.py @@ -71,7 +71,7 @@ async def download_outlook_model(request: Request, modelData:UpdateRequest, back clear_folder_contents(folder_path) inference_obj.stop_model(deployment_platform=modelData.replaceDeploymentPlatform) - # 4. Instantiate Munsif's Inference Model + # 4. Instantiate Inference Model model_path = f"shared/models/outlook/{modelData.modelId}" best_model = modelData.bestBaseModel @@ -124,7 +124,7 @@ async def download_jira_model(request: Request, modelData:UpdateRequest, backgro inference_obj.stop_model(deployment_platform=modelData.replaceDeploymentPlatform) - # 4. Instantiate Munsif's Inference Model + # 4. Instantiate Inference Model class_hierarchy = modelInference.get_class_hierarchy_by_model_id(modelData.modelId) if(class_hierarchy): @@ -190,7 +190,7 @@ async def outlook_inference(request:Request, inferenceData:OutlookInferenceReque # Create Corrected Folder Hierarchy using the final folder id corrected_folder_hierarchy = modelInference.build_corrected_folder_hierarchy(final_folder_id=inferenceData.finalFolderId, model_id=model_id) - # Call Munsif's user_corrected_probablities + # Call user_corrected_probablities corrected_probs = inference_obj.get_corrected_probabilities(text=inferenceData.inputText, corrected_labels=corrected_folder_hierarchy, deployment_platform="outlook") if(corrected_probs): @@ -256,7 +256,7 @@ async def jira_inference(request:Request, inferenceData:JiraInferenceRequest): is_exist = modelInference.check_inference_data_exists(input_id=inferenceData.inputId) if(is_exist): # Update Inference Scenario - # Call Munsif's user_corrected_probablities + # Call user_corrected_probablities corrected_probs = inference_obj.get_corrected_probabilities(text=inferenceData.inputText, corrected_labels=inferenceData.finalLabels, deployment_platform="outlook") if(corrected_probs): diff --git a/test_inference/test_inference.py b/test_inference/test_inference.py index 59022144..2773679b 100644 --- a/test_inference/test_inference.py +++ b/test_inference/test_inference.py @@ -1,7 +1,7 @@ import requests import os -GET_OUTLOOK_ACCESS_TOKEN_URL=os.getenv("GET_OUTLOOK_ACCESS_TOKEN_URL") +OUTLOOK_ACCESS_TOKEN_API_URL=os.getenv("OUTLOOK_ACCESS_TOKEN_API_URL") class TestModelInference: def __init__(self): @@ -9,8 +9,8 @@ def __init__(self): def get_class_hierarchy_by_model_id(self, model_id): try: - get_outlook_access_token_url = GET_OUTLOOK_ACCESS_TOKEN_URL - response = requests.post(get_outlook_access_token_url, json={"modelId": model_id}) + outlook_access_token_url = OUTLOOK_ACCESS_TOKEN_API_URL + response = requests.post(outlook_access_token_url, json={"modelId": model_id}) response.raise_for_status() data = response.json() diff --git a/test_inference/test_inference_api.py b/test_inference/test_inference_api.py index 20001bbb..fc452b89 100644 --- a/test_inference/test_inference_api.py +++ b/test_inference/test_inference_api.py @@ -52,7 +52,7 @@ async def download_test_model(request: Request, modelData:TestDeploymentRequest, backgroundTasks.add_task(os.remove, zip_file_path) - # 3. Instantiate Munsif's Inference Model + # 3. Instantiate Inference Model class_hierarchy = testModelInference.get_class_hierarchy_by_model_id(modelData.replacementModelId) if(class_hierarchy): diff --git a/test_inference/test_inference_wrapper.py b/test_inference/test_inference_wrapper.py index 7fd56cbb..92dfe0f9 100644 --- a/test_inference/test_inference_wrapper.py +++ b/test_inference/test_inference_wrapper.py @@ -1,7 +1,6 @@ from typing import List, Dict -class Inference: - +class Inference: def __init__(self) -> None: pass @@ -16,8 +15,6 @@ class TestInferenceWrapper: def __init__(self) -> None: self.model_dictionary: Dict[int, Inference] = {} - - def model_initiate(self, model_id: int, model_path: str, best_performing_model: str, class_hierarchy: list) -> bool: try: new_model = Inference(model_path, best_performing_model, class_hierarchy) @@ -39,7 +36,6 @@ def inference(self, text: str, model_id: int): except Exception as e: raise Exception(f"Failed to call the inference. Reason: {e}") - def stop_model(self, model_id: int) -> None: if model_id in self.models: del self.models[model_id] diff --git a/test_inference/utils.py b/test_inference/utils.py index bc7e95b2..d1dd330f 100644 --- a/test_inference/utils.py +++ b/test_inference/utils.py @@ -10,7 +10,6 @@ def calculate_average_predicted_class_probability(class_probabilities:List[float return average_probability - def get_s3_payload(destinationFilePath:str, destinationStorageType:str, sourceFilePath:str, sourceStorageType:str): S3_FERRY_PAYLOAD = { "destinationFilePath": destinationFilePath, @@ -20,7 +19,6 @@ def get_s3_payload(destinationFilePath:str, destinationStorageType:str, sourceFi } return S3_FERRY_PAYLOAD - def get_inference_success_payload(predictedClasses:List[str], averageConfidence:float, predictedProbabilities:List[float] ): INFERENCE_SUCCESS_PAYLOAD = { "predictedClasses":predictedClasses, @@ -30,12 +28,10 @@ def get_inference_success_payload(predictedClasses:List[str], averageConfidence: return INFERENCE_SUCCESS_PAYLOAD - def unzip_file(zip_path, extract_to): with zipfile.ZipFile(zip_path, 'r') as zip_ref: zip_ref.extractall(extract_to) - def delete_folder(folder_path: str): try: if os.path.isdir(folder_path): From 10ccd0cf72d1054b2265a5c78962be2d40e384d9 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Tue, 20 Aug 2024 14:00:25 +0530 Subject: [PATCH 504/582] timestamp change --- DSL/OpenSearch/mock/dataset_progress_sessions.json | 12 ++++++------ notification-server/src/openSearch.js | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/DSL/OpenSearch/mock/dataset_progress_sessions.json b/DSL/OpenSearch/mock/dataset_progress_sessions.json index c0ed6e30..bd703747 100644 --- a/DSL/OpenSearch/mock/dataset_progress_sessions.json +++ b/DSL/OpenSearch/mock/dataset_progress_sessions.json @@ -1,13 +1,13 @@ {"index":{"_id":"1"}} -{"sessionId": "1","validationStatus": "Initiating Validation","validationMessage": "","progressPercentage": 1,"timestamp": "1801371325497", "sentTo": []} +{"sessionId": "100","validationStatus": "Initiating Validation","validationMessage": "","progressPercentage": 1,"timestamp": "1801371325497", "sentTo": []} {"index":{"_id":"2"}} -{"sessionId": "2","validationStatus": "Validation In-Progress","validationMessage": "","progressPercentage": 26,"timestamp": "1801371325597", "sentTo": []} +{"sessionId": "200","validationStatus": "Validation In-Progress","validationMessage": "","progressPercentage": 26,"timestamp": "1801371325597", "sentTo": []} {"index":{"_id":"3"}} -{"sessionId": "3","validationStatus": "Cleaning Dataset","validationMessage": "","progressPercentage": 52,"timestamp": "1801371325697", "sentTo": []} +{"sessionId": "300","validationStatus": "Cleaning Dataset","validationMessage": "","progressPercentage": 52,"timestamp": "1801371325697", "sentTo": []} {"index":{"_id":"4"}} -{"sessionId": "4","validationStatus": "Generating Data","validationMessage": "","progressPercentage": 97,"timestamp": "1801371325797", "sentTo": []} +{"sessionId": "400","validationStatus": "Generating Data","validationMessage": "","progressPercentage": 97,"timestamp": "1801371325797", "sentTo": []} {"index":{"_id":"5"}} -{"sessionId": "5","validationStatus": "Success","validationMessage": "","progressPercentage": 100,"timestamp": "1801371325797", "sentTo": []} +{"sessionId": "500","validationStatus": "Success","validationMessage": "","progressPercentage": 100,"timestamp": "1801371325797", "sentTo": []} {"index":{"_id":"6"}} -{"sessionId": "6","validationStatus": "Fail","validationMessage": "Validation failed because class called 'complaints' in 'police' column doesn't exist in the dataset`","progressPercentage": 100,"timestamp": "1801371325797", "sentTo": []} +{"sessionId": "600","validationStatus": "Fail","validationMessage": "Validation failed because class called 'complaints' in 'police' column doesn't exist in the dataset`","progressPercentage": 100,"timestamp": "1801371325797", "sentTo": []} diff --git a/notification-server/src/openSearch.js b/notification-server/src/openSearch.js index 21755d93..99116105 100644 --- a/notification-server/src/openSearch.js +++ b/notification-server/src/openSearch.js @@ -104,7 +104,7 @@ async function updateDatasetGroupProgress( validationStatus, progressPercentage, validationMessage, - timestamp: new Date(), + timestamp: Date.now(), }, }); } From b225717bc963ddd1223ffc23e4c92762e5c561c3 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Tue, 20 Aug 2024 15:22:01 +0530 Subject: [PATCH 505/582] sonar cloud fixes --- GUI/src/assets/BackArrowButton.tsx | 45 +++-- GUI/src/assets/Jira.tsx | 10 +- GUI/src/assets/Pinal.tsx | 2 +- GUI/src/components/DataTable/index.tsx | 16 -- GUI/src/components/FileUpload/index.tsx | 46 +++-- .../FormCheckbox/FormCheckbox.scss | 1 - .../FormElements/FormInput/FormInput.scss | 5 - .../FormElements/FormSelect/FormSelect.scss | 5 +- .../FormElements/FormSelect/index.tsx | 2 +- .../components/FormElements/Switch/index.tsx | 7 +- GUI/src/components/Header/index.tsx | 158 +++++++++------ .../MainNavigation/MainNavigation.scss | 4 +- GUI/src/components/MainNavigation/index.tsx | 11 +- .../TreeNode/ClassHeirarchyTreeNode.tsx | 32 ++-- .../molecules/ClassHeirarchy/index.tsx | 1 + .../CreateDatasetGroupModal.tsx | 46 ++--- .../molecules/DataModelCard/index.tsx | 39 ++-- .../DatasetGroupCard/DatasetGroupCard.scss | 7 - .../molecules/DatasetGroupCard/index.tsx | 7 +- .../IntegrationModals/IntegrationModals.tsx | 4 +- .../UserManagementActionButtons.tsx | 91 +++++++++ .../ValidationCriteria/CardsView.tsx | 6 +- .../DraggableItem/DragableItemStyle.scss | 11 ++ .../DraggableItem/DraggableItem.tsx | 68 ++++--- .../ValidationCriteria/RowViewStyle.scss | 18 ++ .../molecules/ValidationCriteria/RowsView.tsx | 180 ++++++++++-------- .../molecules/ValidationSessionCard/index.tsx | 5 +- .../ViewDatasetGroupModalController.tsx | 2 +- GUI/src/context/DialogContext.tsx | 153 ++++++++------- GUI/src/enums/datasetEnums.ts | 9 +- GUI/src/hoc/with-authorization.tsx | 25 +-- .../DatasetGroups/CreateDatasetGroup.tsx | 146 ++++++++------ .../pages/DatasetGroups/ViewDatasetGroup.tsx | 4 +- GUI/src/pages/DatasetGroups/index.tsx | 37 ++-- GUI/src/pages/LoadingSxreen/LoadingScreen.tsx | 11 ++ GUI/src/pages/StopWords/index.tsx | 19 +- GUI/src/pages/TestModel/index.tsx | 6 +- GUI/src/pages/Unauthorized/unauthorized.scss | 2 +- GUI/src/pages/Unauthorized/unauthorized.tsx | 2 +- GUI/src/pages/UserManagement/UserModal.tsx | 2 +- GUI/src/pages/UserManagement/index.tsx | 111 +++-------- GUI/src/pages/ValidationSessions/index.tsx | 5 +- GUI/src/types/datasetGroups.ts | 2 +- GUI/src/utils/datasetGroupsUtils.ts | 4 + GUI/src/utils/endpoints.ts | 9 +- GUI/src/utils/queryKeys.ts | 7 +- GUI/translations/en/common.json | 15 +- 47 files changed, 809 insertions(+), 589 deletions(-) create mode 100644 GUI/src/components/molecules/UserManagementActionButtons/UserManagementActionButtons.tsx create mode 100644 GUI/src/components/molecules/ValidationCriteria/DraggableItem/DragableItemStyle.scss create mode 100644 GUI/src/components/molecules/ValidationCriteria/RowViewStyle.scss create mode 100644 GUI/src/pages/LoadingSxreen/LoadingScreen.tsx diff --git a/GUI/src/assets/BackArrowButton.tsx b/GUI/src/assets/BackArrowButton.tsx index e7ea4432..e8e60eb8 100644 --- a/GUI/src/assets/BackArrowButton.tsx +++ b/GUI/src/assets/BackArrowButton.tsx @@ -1,20 +1,31 @@ - - const BackArrowButton = () => { - return ( - - - - - - + return ( + + + + + + - + - - - ); - }; - - export default BackArrowButton; - \ No newline at end of file + + + ); +}; + +export default BackArrowButton; diff --git a/GUI/src/assets/Jira.tsx b/GUI/src/assets/Jira.tsx index 457aab56..37088791 100644 --- a/GUI/src/assets/Jira.tsx +++ b/GUI/src/assets/Jira.tsx @@ -7,7 +7,7 @@ const Jira = () => { fill="none" xmlns="http://www.w3.org/2000/svg" > - + { y2="47.1041" gradientUnits="userSpaceOnUse" > - - + + { y2="62.7351" gradientUnits="userSpaceOnUse" > - - + + diff --git a/GUI/src/assets/Pinal.tsx b/GUI/src/assets/Pinal.tsx index ac74d104..b815c32d 100644 --- a/GUI/src/assets/Pinal.tsx +++ b/GUI/src/assets/Pinal.tsx @@ -19,7 +19,7 @@ const Pinal = () => { d="M3.75195 8.98172V46.3167L32.1535 52.2723V3.48438L3.75195 8.98172ZM22.7583 33.9611C22.227 34.7101 21.5206 35.3178 20.7006 35.7311C19.8806 36.1445 18.9719 36.3509 18.0538 36.3326C17.1588 36.3485 16.2732 36.148 15.4723 35.7481C14.6714 35.3483 13.9789 34.761 13.4538 34.036C12.2083 32.2971 11.5852 30.1892 11.6852 28.0526C11.5793 25.8112 12.2136 23.5972 13.4904 21.752C14.0284 20.9887 14.7459 20.3694 15.5797 19.9489C16.4135 19.5283 17.338 19.3194 18.2716 19.3404C19.1601 19.3226 20.0394 19.5232 20.8323 19.9245C21.6253 20.3258 22.3075 20.9156 22.8193 21.6422C24.0524 23.4188 24.6648 25.5526 24.5617 27.7128C24.6704 29.9379 24.0359 32.1361 22.7583 33.9611Z" fill="#0072C6" /> - + = (
          )} - {/*
          - - -
          */}
          )}
          diff --git a/GUI/src/components/FileUpload/index.tsx b/GUI/src/components/FileUpload/index.tsx index ba0ae071..4738a471 100644 --- a/GUI/src/components/FileUpload/index.tsx +++ b/GUI/src/components/FileUpload/index.tsx @@ -1,16 +1,15 @@ import { FormInput } from 'components/FormElements'; import React, { - useState, ChangeEvent, forwardRef, useImperativeHandle, Ref, + useRef, } from 'react'; type FileUploadProps = { - label?: string; - onFileSelect: (file: File | null) => void; - accept?: string; + onFileSelect: (file: File | undefined) => void; + accept?: string | string[]; disabled?: boolean; }; @@ -20,26 +19,34 @@ export type FileUploadHandle = { const FileUpload = forwardRef( (props: FileUploadProps, ref: Ref) => { - const { onFileSelect, accept,disabled } = props; - const [selectedFile, setSelectedFile] = useState(null); + const { onFileSelect, accept, disabled } = props; + const fileInputRef = useRef(null); useImperativeHandle(ref, () => ({ clearFile() { - setSelectedFile(null); - onFileSelect(null); + onFileSelect(undefined); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } }, })); const handleFileChange = (e: ChangeEvent) => { - const file = e.target.files ? e.target.files[0] : null; - setSelectedFile(file); + const file = e.target.files ? e.target.files[0] : undefined; onFileSelect(file); }; - const restrictFormat = (accept: string) => { - if (accept === 'json') return '.json'; - else if (accept === 'xlsx') return '.xlsx'; - else if (accept === 'yaml') return '.yaml, .yml'; + const restrictFormat = (accept: string | string[]) => { + if (typeof accept === 'string') { + console.log("hii") + if (accept === 'json') return '.json'; + else if (accept === 'xlsx') return '.xlsx'; + else if (accept === 'yaml') return '.yaml, .yml'; + return ''; + } else { + return accept.map(ext => `.${ext}`).join(', '); + } + }; return ( @@ -50,9 +57,18 @@ const FileUpload = forwardRef( onChange={handleFileChange} accept={restrictFormat(accept ?? '')} disabled={disabled} + ref={fileInputRef} /> -
          diff --git a/GUI/src/components/FormElements/FormCheckbox/FormCheckbox.scss b/GUI/src/components/FormElements/FormCheckbox/FormCheckbox.scss index 613f4e6a..8bdf863d 100644 --- a/GUI/src/components/FormElements/FormCheckbox/FormCheckbox.scss +++ b/GUI/src/components/FormElements/FormCheckbox/FormCheckbox.scss @@ -17,7 +17,6 @@ } &__item { - margin-top: 6px; input[type=checkbox] { display: none; diff --git a/GUI/src/components/FormElements/FormInput/FormInput.scss b/GUI/src/components/FormElements/FormInput/FormInput.scss index 3d019010..c010c478 100644 --- a/GUI/src/components/FormElements/FormInput/FormInput.scss +++ b/GUI/src/components/FormElements/FormInput/FormInput.scss @@ -91,11 +91,6 @@ &--disabled & { input { background-color: get-color(black-coral-0); - } - } - - &--disabled & { - input { border: solid 1px get-color(jasper-10); } } diff --git a/GUI/src/components/FormElements/FormSelect/FormSelect.scss b/GUI/src/components/FormElements/FormSelect/FormSelect.scss index d151870d..b6b4f434 100644 --- a/GUI/src/components/FormElements/FormSelect/FormSelect.scss +++ b/GUI/src/components/FormElements/FormSelect/FormSelect.scss @@ -112,20 +112,17 @@ } &[aria-selected=true] { - background-color: get-color(sapphire-blue-10); - color: get-color(white); + background-color: #DDEBFF; &:hover, &:focus { background-color: get-color(sapphire-blue-10); - color: get-color(white); } } &:hover, &:focus { background-color: get-color(black-coral-0); - color: get-color(black-coral-20); } } } diff --git a/GUI/src/components/FormElements/FormSelect/index.tsx b/GUI/src/components/FormElements/FormSelect/index.tsx index c3c59707..462a46ea 100644 --- a/GUI/src/components/FormElements/FormSelect/index.tsx +++ b/GUI/src/components/FormElements/FormSelect/index.tsx @@ -79,7 +79,7 @@ const FormSelect = forwardRef( const selectClasses = clsx('select', disabled && 'select--disabled'); - const placeholderValue = placeholder || t('global.choose'); + const placeholderValue = placeholder || t('datasetGroups.createDataset.selectPlaceholder'); return (
          diff --git a/GUI/src/components/FormElements/Switch/index.tsx b/GUI/src/components/FormElements/Switch/index.tsx index 7cf1b755..ed414c7e 100644 --- a/GUI/src/components/FormElements/Switch/index.tsx +++ b/GUI/src/components/FormElements/Switch/index.tsx @@ -14,7 +14,7 @@ type SwitchProps = Partial & { checked?: boolean; defaultChecked?: boolean; hideLabel?: boolean; - onCheckedChange?: (checked: boolean) => void|any; + onCheckedChange?: (checked: boolean) => void; }; const Switch = forwardRef( @@ -38,7 +38,10 @@ const Switch = forwardRef( const offValueLabel = offLabel || t('global.off'); return ( -
          +
          {label && !hideLabel && (
          diff --git a/GUI/src/components/molecules/CreateDatasetGroupModals/CreateDatasetGroupModal.tsx b/GUI/src/components/molecules/CreateDatasetGroupModals/CreateDatasetGroupModal.tsx index 01915ffe..f235d662 100644 --- a/GUI/src/components/molecules/CreateDatasetGroupModals/CreateDatasetGroupModal.tsx +++ b/GUI/src/components/molecules/CreateDatasetGroupModals/CreateDatasetGroupModal.tsx @@ -1,54 +1,38 @@ -import { CreateDatasetGroupModals } from 'enums/datasetEnums'; +import { + CreateDatasetGroupModals, + ValidationErrorTypes, +} from 'enums/datasetEnums'; import { Button, Dialog } from 'components'; import { ButtonAppearanceTypes } from 'enums/commonEnums'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; -import { useDialog } from 'hooks/useDialog'; const CreateDatasetGroupModalController = ({ modalType, isModalOpen, setIsModalOpen, + valiadationErrorType, }: { modalType: CreateDatasetGroupModals; isModalOpen: boolean; setIsModalOpen: React.Dispatch>; + valiadationErrorType?: ValidationErrorTypes; }) => { const { t } = useTranslation(); const navigate = useNavigate(); - const { open, close } = useDialog(); - const opneValidationErrorModal = (modalType: CreateDatasetGroupModals) => { - open({ - title: t('datasetGroups.modals.columnInsufficientHeader') ?? "", - content: ( -

          - {t('datasetGroups.modals.columnInsufficientDescription')} -

          - ), - footer: ( -
          - - -
          - ) - }) - } return ( <> {modalType === CreateDatasetGroupModals.VALIDATION_ERROR && ( +
          )} {modalType === CreateDatasetGroupModals.SUCCESS && ( @@ -70,7 +56,7 @@ const CreateDatasetGroupModalController = ({ isOpen={isModalOpen} title={t('datasetGroups.modals.createDatasetSuccessTitle')} footer={ -
          +
          , + footer: ( + + ), size: 'large', content: (
          - {t('dataModels.trainingResults.bestPerformingModel') ?? ''} - + {t('dataModels.trainingResults.bestPerformingModel') ?? + ''}{' '} + -
          {' '} -
          {t('dataModels.trainingResults.classes') ?? ''}
          -
          {t('dataModels.trainingResults.accuracy') ?? ''}
          -
          {t('dataModels.trainingResults.f1Score') ?? ''}
          +
          + {' '} + {t('dataModels.trainingResults.classes') ?? ''} +
          +
          + {t('dataModels.trainingResults.accuracy') ?? ''} +
          +
          + {t('dataModels.trainingResults.f1Score') ?? ''} +
          } > @@ -177,7 +190,7 @@ const DataModelCard: FC> = ({ }); }} > - {t('dataModels.trainingResults.viewResults') ?? ''} Results + {t('dataModels.trainingResults.viewResults') ?? ''} Results + + +
          + ), + }); + }} + > + } /> + {t('global.delete')} + +
          + ); +}; + +export default ActionButtons; diff --git a/GUI/src/components/molecules/ValidationCriteria/CardsView.tsx b/GUI/src/components/molecules/ValidationCriteria/CardsView.tsx index a781c8e1..e91ef334 100644 --- a/GUI/src/components/molecules/ValidationCriteria/CardsView.tsx +++ b/GUI/src/components/molecules/ValidationCriteria/CardsView.tsx @@ -5,6 +5,7 @@ import Button from 'components/Button'; import { ValidationRule } from 'types/datasetGroups'; import { v4 as uuidv4 } from 'uuid'; import DraggableItem from './DraggableItem/DraggableItem'; +import { useTranslation } from 'react-i18next'; type ValidationRulesProps = { validationRules?: ValidationRule[]; @@ -20,6 +21,7 @@ const ValidationCriteriaCardsView: FC< setValidationRuleError, validationRuleError, }) => { + const { t } = useTranslation(); const moveItem = (fromIndex: number, toIndex: number) => { const updatedItems = Array.from(validationRules ?? []); const [movedItem] = updatedItems.splice(fromIndex, 1); @@ -38,7 +40,7 @@ const ValidationCriteriaCardsView: FC< return ( -
          Create Validation Rule
          +
          {t('datasetGroups.createDataset.validationCriteria')}
          {validationRules && validationRules?.map((item, index) => ( ))}
          - +
          ); diff --git a/GUI/src/components/molecules/ValidationCriteria/DraggableItem/DragableItemStyle.scss b/GUI/src/components/molecules/ValidationCriteria/DraggableItem/DragableItemStyle.scss new file mode 100644 index 00000000..696fe446 --- /dev/null +++ b/GUI/src/components/molecules/ValidationCriteria/DraggableItem/DragableItemStyle.scss @@ -0,0 +1,11 @@ +.dragabbleCardContainer { + display: flex; + gap: 20px; + align-items: center; +} + +.dragabbleButtonWrapper { + display: flex; + gap: 10px; + justify-content: end; +} diff --git a/GUI/src/components/molecules/ValidationCriteria/DraggableItem/DraggableItem.tsx b/GUI/src/components/molecules/ValidationCriteria/DraggableItem/DraggableItem.tsx index 5265333e..b9aee410 100644 --- a/GUI/src/components/molecules/ValidationCriteria/DraggableItem/DraggableItem.tsx +++ b/GUI/src/components/molecules/ValidationCriteria/DraggableItem/DraggableItem.tsx @@ -1,12 +1,14 @@ import React, { useCallback } from 'react'; import { useDrag, useDrop } from 'react-dnd'; import dataTypes from '../../../../config/dataTypesConfig.json'; -import { MdDehaze, MdDelete } from 'react-icons/md'; +import { MdDehaze, MdDeleteOutline } from 'react-icons/md'; import Card from 'components/Card'; import { FormCheckbox, FormInput, FormSelect } from 'components/FormElements'; import { ValidationRule } from 'types/datasetGroups'; import { Link } from 'react-router-dom'; import { isFieldNameExisting } from 'utils/datasetGroupsUtils'; +import './DragableItemStyle.scss'; +import { useTranslation } from 'react-i18next'; const ItemTypes = { ITEM: 'item', @@ -27,6 +29,7 @@ const DraggableItem = ({ setValidationRules: React.Dispatch>; validationRuleError?: boolean; }) => { + const { t } = useTranslation(); const [, ref] = useDrag({ type: ItemTypes.ITEM, item: { index }, @@ -34,10 +37,8 @@ const DraggableItem = ({ const [, drop] = useDrop({ accept: ItemTypes.ITEM, - hover: (draggedItem: { - index: number - }) => { - if (draggedItem?.index !== index) { + hover: (draggedItem: { index: number }) => { + if (draggedItem.index !== index) { moveItem(draggedItem.index, index); draggedItem.index = index; } @@ -72,63 +73,76 @@ const DraggableItem = ({ ); updatedItems && setValidationRules(updatedItems); }; + + const getErrorMessage = (item: ValidationRule) => { + if (validationRuleError) { + if (!item.fieldName) { + return t('datasetGroups.detailedView.fieldName'); + } + if (item.fieldName.toLowerCase() === 'rowid') { + return t('datasetGroups.detailedView.fieldNameError', { + name: item.fieldName, + }); + } + if (isFieldNameExisting(validationRules, item.fieldName)) { + return t('datasetGroups.detailedView.fieldNameExist', { + name: item.fieldName, + }); + } + } + return ''; + }; + return (
          ref(drop(node))}> -
          +
          handleChange(item.id, e.target.value)} - error={ - validationRuleError && !item.fieldName - ? 'Enter a field name' - : validationRuleError && - item.fieldName && - item?.fieldName.toString().toLocaleLowerCase() === 'rowid' - ? `${item?.fieldName} cannot be used as a field name` - : item.fieldName && - isFieldNameExisting(validationRules, item?.fieldName) - ? `${item?.fieldName} alreday exist as field name` - : '' - } + error={getErrorMessage(item)} /> - changeDataType(item.id, selection?.value ?? "") + changeDataType(item.id, selection?.value as string) } error={ validationRuleError && !item.dataType ? 'Select a data type' : '' } /> -
          +
          deleteItem(item.id)} className="link" > - - Delete + + {t('global.delete')} setIsDataClass(item.id, item.isDataClass)} - style={{ width: '150px' }} + style={{ + width: '150px', + }} /> - +
          diff --git a/GUI/src/components/molecules/ValidationCriteria/RowViewStyle.scss b/GUI/src/components/molecules/ValidationCriteria/RowViewStyle.scss new file mode 100644 index 00000000..e46d9e57 --- /dev/null +++ b/GUI/src/components/molecules/ValidationCriteria/RowViewStyle.scss @@ -0,0 +1,18 @@ +.rowViewContentWrapper { + display: flex; + gap: 20px; + align-items: center; + padding: 25px; +} + +.rowViewButtonWrapper { + display: flex; + justify-content: end; + gap: 10px; +} + +.rowViewButton { + display: flex; + align-items: center; + gap: 10px; +} \ No newline at end of file diff --git a/GUI/src/components/molecules/ValidationCriteria/RowsView.tsx b/GUI/src/components/molecules/ValidationCriteria/RowsView.tsx index fef02edd..d7e42d34 100644 --- a/GUI/src/components/molecules/ValidationCriteria/RowsView.tsx +++ b/GUI/src/components/molecules/ValidationCriteria/RowsView.tsx @@ -6,55 +6,61 @@ import { ValidationRule } from 'types/datasetGroups'; import { Link } from 'react-router-dom'; import { isFieldNameExisting } from 'utils/datasetGroupsUtils'; import { v4 as uuidv4 } from 'uuid'; +import { useTranslation } from 'react-i18next'; +import './RowViewStyle.scss'; type ValidationRulesProps = { validationRules?: ValidationRule[]; - setValidationRules: React.Dispatch>; + setValidationRules: React.Dispatch< + React.SetStateAction + >; validationRuleError?: boolean; setValidationRuleError: React.Dispatch>; }; -const ValidationCriteriaRowsView: FC> = ({ + +const ValidationCriteriaRowsView: FC< + PropsWithChildren +> = ({ validationRules, setValidationRules, setValidationRuleError, validationRuleError, - }) => { + const { t } = useTranslation(); const setIsDataClass = (id: string | number, isDataClass: boolean) => { const updatedItems = validationRules?.map((item) => - item?.id === id ? { ...item, isDataClass: !isDataClass } : item + item.id === id ? { ...item, isDataClass: !isDataClass } : item ); updatedItems && setValidationRules(updatedItems); }; const changeName = (id: number | string, newValue: string) => { - setValidationRules((prevData) => prevData?.map((item) => - item?.id === id ? { ...item, fieldName: newValue } : item + item.id === id ? { ...item, fieldName: newValue } : item ) ); - if(isFieldNameExisting(validationRuleError,newValue)) + if (isFieldNameExisting(validationRules, newValue)) { setValidationRuleError(true); - else - setValidationRuleError(false) - } + } else { + setValidationRuleError(false); + } + }; const changeDataType = (id: number | string, value: string) => { const updatedItems = validationRules?.map((item) => - item?.id === id ? { ...item, dataType: value } : item + item.id === id ? { ...item, dataType: value } : item ); setValidationRules(updatedItems); }; const addNewClass = () => { - setValidationRuleError(false) + setValidationRuleError(false); const updatedItems = [ - ...validationRules ?? [], + ...(validationRules ?? []), { id: uuidv4(), fieldName: '', dataType: '', isDataClass: false }, ]; - setValidationRules(updatedItems); }; @@ -66,79 +72,93 @@ const ValidationCriteriaRowsView: FC> = setValidationRules(updatedItems); }; + const getErrorMessage = (item: ValidationRule) => { + if (validationRuleError) { + if (!item.fieldName) { + return t('datasetGroups.detailedView.fieldName'); + } + if (item.fieldName.toLowerCase() === 'rowid') { + return t('datasetGroups.detailedView.fieldNameError', { + name: item.fieldName, + }); + } + if (isFieldNameExisting(validationRules, item.fieldName)) { + return t('datasetGroups.detailedView.fieldNameExist', { + name: item.fieldName, + }); + } + } + return ''; + }; + + const getBackgroundColor = (index: number) => { + if (index % 2 === 1) return '#F9F9F9'; + else return '#FFFFFF'; + }; + return ( -
          +
          {validationRules?.map((item, index) => ( -
          - changeName(item.id, e.target.value)} - error={ - validationRuleError && !item.fieldName - ? 'Enter a field name' - : validationRuleError && - item.fieldName && - item?.fieldName.toString().toLocaleLowerCase() === 'rowid' - ? `${item?.fieldName} cannot be used as a field name` - : item.fieldName && isFieldNameExisting(validationRules,item?.fieldName)?`${item?.fieldName} alreday exist as field name` - : '' - } - /> - - changeDataType(item.id, selection?.value ?? "") - } - error={ - validationRuleError && !item.dataType - ? 'Select a data type' - : '' - } - />
          - addNewClass()} - className='link' - > - - Add - - deleteItem(item.id)} - className='link' - > - - Delete - - setIsDataClass(item.id, item.isDataClass)} - style={{width:"150px"}} + changeName(item.id, e.target.value)} + error={getErrorMessage(item)} + /> + + changeDataType(item.id, (selection?.value as string) ?? '') + } + error={ + validationRuleError && !item.dataType + ? t('datasetGroups.detailedView.selectDataType') ?? '' + : '' + } /> +
          + addNewClass()} + className="rowViewButton" + > + + {t('global.add')} + + deleteItem(item.id)} + className="rowViewButton" + > + + {t('global.delete')} + + setIsDataClass(item.id, item.isDataClass)} + style={{ width: '150px' }} + /> +
          -
          ))} - -
          ); }; diff --git a/GUI/src/components/molecules/ValidationSessionCard/index.tsx b/GUI/src/components/molecules/ValidationSessionCard/index.tsx index 58d026ef..c92b61e9 100644 --- a/GUI/src/components/molecules/ValidationSessionCard/index.tsx +++ b/GUI/src/components/molecules/ValidationSessionCard/index.tsx @@ -1,4 +1,3 @@ -import { FC, useState } from 'react'; import { useTranslation } from 'react-i18next'; import ProgressBar from 'components/ProgressBar'; import { Card, Label } from 'components'; @@ -21,10 +20,10 @@ const ValidationSessionCard: React.FC = ({dgName,ver
          {dgName} {isLatest &&( - + )} {status==="Fail" &&( - + )}
          } diff --git a/GUI/src/components/molecules/ViewDatasetGroupModalController/ViewDatasetGroupModalController.tsx b/GUI/src/components/molecules/ViewDatasetGroupModalController/ViewDatasetGroupModalController.tsx index a6ecd179..b06fe2de 100644 --- a/GUI/src/components/molecules/ViewDatasetGroupModalController/ViewDatasetGroupModalController.tsx +++ b/GUI/src/components/molecules/ViewDatasetGroupModalController/ViewDatasetGroupModalController.tsx @@ -32,7 +32,7 @@ const ViewDatasetGroupModalController = ({ exportFormat, }: { setImportStatus: React.Dispatch>; - handleFileSelect: (file: File | null) => void; + handleFileSelect: (file: File | undefined) => void; fileUploadRef: RefObject; handleImport: () => void; importStatus: string; diff --git a/GUI/src/context/DialogContext.tsx b/GUI/src/context/DialogContext.tsx index 737e114e..3141273a 100644 --- a/GUI/src/context/DialogContext.tsx +++ b/GUI/src/context/DialogContext.tsx @@ -1,76 +1,83 @@ import React, { - createContext, - FC, - PropsWithChildren, - ReactNode, - useMemo, - useState, - useCallback, - } from 'react'; - import * as RadixDialog from '@radix-ui/react-dialog'; - import { MdOutlineClose } from 'react-icons/md'; - import clsx from 'clsx'; - import '../components/Dialog/Dialog.scss'; - import Icon from 'components/Icon'; - import Track from 'components/Track'; - - type DialogProps = { - title?: string | null; - footer?: ReactNode; - size?: 'default' | 'large'; - content: ReactNode; + createContext, + FC, + PropsWithChildren, + ReactNode, + useMemo, + useState, +} from 'react'; +import * as RadixDialog from '@radix-ui/react-dialog'; +import { MdOutlineClose } from 'react-icons/md'; +import clsx from 'clsx'; +import '../components/Dialog/Dialog.scss'; +import Icon from 'components/Icon'; +import Track from 'components/Track'; + +type DialogProps = { + title?: string | null; + footer?: ReactNode; + size?: 'default' | 'large'; + content: ReactNode; +}; + +type DialogContextType = { + open: (dialog: DialogProps) => void; + close: () => void; +}; + +export const DialogContext = createContext(null!); + +export const DialogProvider: FC> = ({ children }) => { + const [isOpen, setIsOpen] = useState(false); + const [dialogProps, setDialogProps] = useState(null); + + const open = (dialog: DialogProps) => { + setDialogProps(dialog); + setIsOpen(true); }; - - type DialogContextType = { - open: (dialog: DialogProps) => void; - close: () => void; + + const close = () => { + setIsOpen(false); + setDialogProps(null); }; - - export const DialogContext = createContext(null!); - - export const DialogProvider: FC> = ({ children }) => { - const [isOpen, setIsOpen] = useState(false); - const [dialogProps, setDialogProps] = useState(null); - - const open = (dialog: DialogProps) => { - setDialogProps(dialog); - setIsOpen(true); - }; - - const close = () => { - setIsOpen(false); - setDialogProps(null); - }; - - const contextValue = useMemo(() => ({ open, close }), []); - - return ( - - {children} - {dialogProps && ( - - - - - {dialogProps.title && ( -
          - {dialogProps.title} - - - -
          - )} -
          {dialogProps.content}
          - {dialogProps.footer && ( - {dialogProps.footer} - )} -
          -
          -
          - )} -
          - ); - }; - \ No newline at end of file + + const contextValue = useMemo(() => ({ open, close }), []); + + return ( + + {children} + {dialogProps && ( + + + + + {dialogProps.title && ( +
          + + {dialogProps.title} + + + + +
          + )} +
          {dialogProps.content}
          + {dialogProps.footer && ( + + {dialogProps.footer} + + )} +
          +
          +
          + )} +
          + ); +}; diff --git a/GUI/src/enums/datasetEnums.ts b/GUI/src/enums/datasetEnums.ts index 9b5cf334..c10868b8 100644 --- a/GUI/src/enums/datasetEnums.ts +++ b/GUI/src/enums/datasetEnums.ts @@ -40,4 +40,11 @@ export enum ImportExportDataTypes { export enum StopWordImportOptions { ADD = 'add', DELETE = 'delete', -} \ No newline at end of file +} + +export enum ValidationErrorTypes { + NAME = 'NAME', + CLASS_HIERARCHY = 'CLASS_HIERARCHY', + VALIDATION_CRITERIA = 'VALIDATION_CRITERIA', + NULL = 'NULL', +} diff --git a/GUI/src/hoc/with-authorization.tsx b/GUI/src/hoc/with-authorization.tsx index 59947ffc..9874ffa9 100644 --- a/GUI/src/hoc/with-authorization.tsx +++ b/GUI/src/hoc/with-authorization.tsx @@ -4,25 +4,26 @@ import useStore from 'store'; function withAuthorization

          ( WrappedComponent: React.ComponentType

          , - allowedRoles: ROLES[] = [], + allowedRoles: ROLES[] = [] ): React.FC

          { const CheckRoles: React.FC

          = ({ ...props }: P) => { - - const userInfo = useStore(x => x.userInfo); - const allowed = allowedRoles?.some(x => userInfo?.authorities.includes(x)); + const userInfo = useStore((x) => x.userInfo); + const allowed = allowedRoles?.some((x) => + userInfo?.authorities.includes(x) + ); - if(!userInfo) { - return Loading... + if (!userInfo) { + return Loading...; } - - if(!allowed) { - return Unauthorized Access + + if (!allowed) { + return Unauthorized Access; } - return ; + return ; }; - + return CheckRoles; -}; +} export default withAuthorization; diff --git a/GUI/src/pages/DatasetGroups/CreateDatasetGroup.tsx b/GUI/src/pages/DatasetGroups/CreateDatasetGroup.tsx index c709cf11..051a5be7 100644 --- a/GUI/src/pages/DatasetGroups/CreateDatasetGroup.tsx +++ b/GUI/src/pages/DatasetGroups/CreateDatasetGroup.tsx @@ -17,7 +17,10 @@ import ValidationCriteriaCardsView from 'components/molecules/ValidationCriteria import { useMutation } from '@tanstack/react-query'; import { createDatasetGroup } from 'services/datasets'; import { useDialog } from 'hooks/useDialog'; -import { CreateDatasetGroupModals } from 'enums/datasetEnums'; +import { + CreateDatasetGroupModals, + ValidationErrorTypes, +} from 'enums/datasetEnums'; import CreateDatasetGroupModalController from 'components/molecules/CreateDatasetGroupModals/CreateDatasetGroupModal'; import { ButtonAppearanceTypes } from 'enums/commonEnums'; @@ -33,6 +36,7 @@ const CreateDatasetGroup: FC = () => { const initialClass = [ { id: uuidv4(), fieldName: '', level: 0, children: [] }, + { id: uuidv4(), fieldName: '', level: 0, children: [] }, ]; const [isModalOpen, setIsModalOpen] = useState(false); @@ -47,6 +51,8 @@ const CreateDatasetGroup: FC = () => { const [validationRuleError, setValidationRuleError] = useState(false); const [nodes, setNodes] = useState(initialClass); const [nodesError, setNodesError] = useState(false); + const [valiadationErrorType, setValidationErrorType] = + useState(ValidationErrorTypes.NULL); const validateData = useCallback(() => { setNodesError(validateClassHierarchy(nodes)); @@ -60,6 +66,11 @@ const CreateDatasetGroup: FC = () => { if (!isValidationRulesSatisfied(validationRules)) { setIsModalOpen(true); setModalType(CreateDatasetGroupModals.VALIDATION_ERROR); + setValidationErrorType(ValidationErrorTypes.VALIDATION_CRITERIA); + } else if (nodes.length < 2) { + setIsModalOpen(true); + setModalType(CreateDatasetGroupModals.VALIDATION_ERROR); + setValidationErrorType(ValidationErrorTypes.CLASS_HIERARCHY); } else { const payload: DatasetGroup = { groupName: datasetName, @@ -88,71 +99,82 @@ const CreateDatasetGroup: FC = () => { return (

          -
          -
          {t('datasetGroups.createDataset.title')}
          -
          -
          - -
          - setDatasetName(e.target.value)} - error={ - !datasetName && datasetNameError - ? t( - 'datasetGroups.createDataset.datasetInputPlaceholder' - ) ?? '' - : '' - } - /> +
          +
          +
          + {t('datasetGroups.createDataset.title')}
          - +
          +
          + +
          + setDatasetName(e.target.value)} + error={ + !datasetName && datasetNameError + ? t( + 'datasetGroups.createDataset.datasetInputPlaceholder' + ) ?? '' + : '' + } + /> +
          +
          - - -
          Class Hierarchy
          - - {' '} - - -
          - - -
          - - +
          Class Hierarchy
          + + {' '} + + +
          + + + +
          + + +
          diff --git a/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx b/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx index 65feb45a..21341ee5 100644 --- a/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx +++ b/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx @@ -249,8 +249,8 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { }, }); - const handleFileSelect = (file: File | null) => { - if (file) setFile(file); + const handleFileSelect = (file: File | undefined) => { + setFile(file) }; const handleImport = () => { diff --git a/GUI/src/pages/DatasetGroups/index.tsx b/GUI/src/pages/DatasetGroups/index.tsx index 3a67cba2..d93cbba7 100644 --- a/GUI/src/pages/DatasetGroups/index.tsx +++ b/GUI/src/pages/DatasetGroups/index.tsx @@ -104,8 +104,15 @@ const DatasetGroups: FC = () => { placeholder={t('datasetGroups.table.group') ?? ''} options={formattedArray(filterData?.response?.dgNames) ?? []} onSelectionChange={(selection) => - handleFilterChange('datasetGroupName', selection?.value ?? '') + handleFilterChange( + 'datasetGroupName', + (selection?.value as string) ?? '' + ) } + value={{ + label: filters.datasetGroupName, + value: filters.datasetGroupName, + }} /> { placeholder={t('datasetGroups.table.version') ?? ''} options={formattedArray(filterData?.response?.dgVersions) ?? []} onSelectionChange={(selection) => - handleFilterChange('version', selection?.value ?? '') + handleFilterChange( + 'version', + (selection?.value as string) ?? '' + ) } /> { [] } onSelectionChange={(selection) => - handleFilterChange('validationStatus', selection?.value ?? '') + handleFilterChange( + 'validationStatus', + (selection?.value as string) ?? '' + ) } /> { { label: 'Z-A', value: 'desc' }, ]} onSelectionChange={(selection) => - handleFilterChange('sort', selection?.value ?? '') + handleFilterChange('sort', (selection?.value as string) ?? '') } />
          {isLoading && ( -
          +
          )} -
          +
          {datasetGroupsData?.response?.data?.map( (dataset: SingleDatasetType, index: number) => { return ( { + return ( +
          +

          Loading...

          +
          + ); +}; + +export default LoadingScreen; \ No newline at end of file diff --git a/GUI/src/pages/StopWords/index.tsx b/GUI/src/pages/StopWords/index.tsx index bf0fcee3..eb7f8047 100644 --- a/GUI/src/pages/StopWords/index.tsx +++ b/GUI/src/pages/StopWords/index.tsx @@ -17,7 +17,6 @@ import { stopWordsQueryKeys } from 'utils/queryKeys'; import { ButtonAppearanceTypes } from 'enums/commonEnums'; import { StopWordImportOptions } from 'enums/datasetEnums'; import { useDialog } from 'hooks/useDialog'; -import { AxiosError } from 'axios'; const StopWords: FC = () => { const { t } = useTranslation(); @@ -28,11 +27,10 @@ const StopWords: FC = () => { const [importOption, setImportOption] = useState(''); const [file, setFile] = useState(); const fileUploadRef = useRef(null); - const [addedStopWord, setAddedStopWord] = useState(''); const { register, setValue, watch } = useForm({ defaultValues: { - stopWord: addedStopWord, + stopWord: '', }, }); @@ -49,7 +47,7 @@ const StopWords: FC = () => { const addStopWordMutation = useMutation({ mutationFn: (data: { stopWords: string[] }) => addStopWord(data), - onSuccess: async (res) => { + onSuccess: async () => { await queryClient.invalidateQueries( stopWordsQueryKeys.GET_ALL_STOP_WORDS() ); @@ -59,7 +57,7 @@ const StopWords: FC = () => { const deleteStopWordMutation = useMutation({ mutationFn: (data: { stopWords: string[] }) => deleteStopWord(data), - onSuccess: async (res) => { + onSuccess: async () => { await queryClient.invalidateQueries( stopWordsQueryKeys.GET_ALL_STOP_WORDS() ); @@ -89,7 +87,7 @@ const StopWords: FC = () => { setIsModalOpen(false); importMutationSuccessFunc(); }, - onError: async (error: AxiosError) => { + onError: async () => { setIsModalOpen(true); open({ title: t('stopWords.importModal.unsuccessTitle') ?? '', @@ -104,7 +102,7 @@ const StopWords: FC = () => { setIsModalOpen(false); importMutationSuccessFunc(); }, - onError: async (error: AxiosError) => { + onError: async () => { setIsModalOpen(true); open({ title: t('stopWords.importModal.unsuccessTitle') ?? '', @@ -113,8 +111,8 @@ const StopWords: FC = () => { }, }); - const handleFileSelect = (file: File | null) => { - if (file) setFile(file); + const handleFileSelect = (file: File | undefined) => { + setFile(file); }; const handleStopWordFileOperations = () => { @@ -231,6 +229,7 @@ const StopWords: FC = () => { ref={fileUploadRef} disabled={importOption === ''} onFileSelect={handleFileSelect} + accept={['xlsx', 'json', 'yaml', 'txt']} />
          @@ -240,4 +239,4 @@ const StopWords: FC = () => { ); }; -export default StopWords; \ No newline at end of file +export default StopWords; diff --git a/GUI/src/pages/TestModel/index.tsx b/GUI/src/pages/TestModel/index.tsx index f0b50bfa..97cb82da 100644 --- a/GUI/src/pages/TestModel/index.tsx +++ b/GUI/src/pages/TestModel/index.tsx @@ -11,7 +11,7 @@ import { TestModelType, } from 'types/testModelTypes'; import { formatClassHierarchyArray } from 'utils/commonUtilts'; -import { testModelsEnpoinnts } from 'utils/endpoints'; +import { testModelsEndpoints } from 'utils/endpoints'; import { testModelsQueryKeys } from 'utils/queryKeys'; import { formatPredictions } from 'utils/testModelUtil'; import './testModelStyles.scss'; @@ -30,7 +30,7 @@ const TestModel: FC = () => { const { isLoading } = useQuery({ queryKey: testModelsQueryKeys.GET_TEST_MODELS(), queryFn: async () => { - const response = await apiDev.get(testModelsEnpoinnts.GET_MODELS()); + const response = await apiDev.get(testModelsEndpoints.GET_MODELS()); return response?.data?.response?.data ?? ([] as TestModelType[]); }, onSuccess: (data: TestModelType[]) => { @@ -52,7 +52,7 @@ const TestModel: FC = () => { } = useMutation({ mutationFn: async (data: ClassifyTestModalPayloadType) => { const response = await apiDev.post( - testModelsEnpoinnts.CLASSIFY_TEST_MODELS(), + testModelsEndpoints.CLASSIFY_TEST_MODELS(), data ); return response?.data?.response?.data as ClassifyTestModalResponseType; diff --git a/GUI/src/pages/Unauthorized/unauthorized.scss b/GUI/src/pages/Unauthorized/unauthorized.scss index 60135898..3c1bb0ff 100644 --- a/GUI/src/pages/Unauthorized/unauthorized.scss +++ b/GUI/src/pages/Unauthorized/unauthorized.scss @@ -27,4 +27,4 @@ .unauthorized-message { font-size: 1.2em; color: #555; - } \ No newline at end of file + } diff --git a/GUI/src/pages/Unauthorized/unauthorized.tsx b/GUI/src/pages/Unauthorized/unauthorized.tsx index 23dbc5b0..088fd80b 100644 --- a/GUI/src/pages/Unauthorized/unauthorized.tsx +++ b/GUI/src/pages/Unauthorized/unauthorized.tsx @@ -14,4 +14,4 @@ const Unauthorized: FC = () => { ); }; -export default Unauthorized; \ No newline at end of file +export default Unauthorized; diff --git a/GUI/src/pages/UserManagement/UserModal.tsx b/GUI/src/pages/UserManagement/UserModal.tsx index 49f6cedf..e3efcfb1 100644 --- a/GUI/src/pages/UserManagement/UserModal.tsx +++ b/GUI/src/pages/UserManagement/UserModal.tsx @@ -17,7 +17,7 @@ import { FaChevronDown, FaChevronUp } from 'react-icons/fa'; type UserModalProps = { onClose: () => void; - user?: User | undefined; + user?: User; isModalOpen?: boolean; }; diff --git a/GUI/src/pages/UserManagement/index.tsx b/GUI/src/pages/UserManagement/index.tsx index 8ee757d1..3edafcee 100644 --- a/GUI/src/pages/UserManagement/index.tsx +++ b/GUI/src/pages/UserManagement/index.tsx @@ -1,5 +1,5 @@ import { FC, useMemo, useState } from 'react'; -import { Button, DataTable, Icon } from '../../components'; +import { Button, DataTable } from '../../components'; import { PaginationState, Row, @@ -7,22 +7,18 @@ import { createColumnHelper, } from '@tanstack/react-table'; import { User } from '../../types/user'; -import { MdOutlineDeleteOutline, MdOutlineEdit } from 'react-icons/md'; import './UserManagement.scss'; import { useTranslation } from 'react-i18next'; -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; - -import { deleteUser } from 'services/users'; -import { useToast } from 'hooks/useToast'; -import { AxiosError } from 'axios'; +import { useQuery } from '@tanstack/react-query'; import apiDev from 'services/api-dev'; import UserModal from './UserModal'; import { userManagementQueryKeys } from 'utils/queryKeys'; import { userManagementEndpoints } from 'utils/endpoints'; -import { ButtonAppearanceTypes, ToastTypes } from 'enums/commonEnums'; +import { ButtonAppearanceTypes } from 'enums/commonEnums'; import { useDialog } from 'hooks/useDialog'; import SkeletonTable from 'components/molecules/TableSkeleton/TableSkeleton'; import CircularSpinner from 'components/molecules/CircularSpinner/CircularSpinner'; +import ActionButtons from 'components/molecules/UserManagementActionButtons/UserManagementActionButtons'; const UserManagement: FC = () => { const columnHelper = createColumnHelper(); @@ -35,20 +31,19 @@ const UserManagement: FC = () => { }); const [sorting, setSorting] = useState([]); const { t } = useTranslation(); - const toast = useToast(); - const queryClient = useQueryClient(); - const { open, close } = useDialog(); + const getSortString = (length: number) => { + if (length === 0) return 'name asc'; + else + return `${sorting[0]?.id} ${ + sorting[0]?.desc ? t('global.desc') : t('global.asc') + }`; + }; const fetchUsers = async ( pagination: PaginationState, sorting: SortingState ) => { - const sort = - sorting?.length === 0 - ? 'name asc' - : sorting[0]?.id + - ' ' + - (sorting[0]?.desc ? t('global.desc') : t('global.asc')); + const sort = getSortString(sorting?.length); const { data } = await apiDev.post(userManagementEndpoints.FETCH_USERS(), { page: pagination?.pageIndex + 1, page_size: pagination?.pageSize, @@ -58,57 +53,13 @@ const UserManagement: FC = () => { }; const { data: users, isLoading } = useQuery({ - queryKey: userManagementQueryKeys.getAllEmployees(), + queryKey: userManagementQueryKeys.getAllEmployees(pagination, sorting), queryFn: () => fetchUsers(pagination, sorting), onSuccess: (data) => { - setTotalPages( data[0]?.totalPages) - } + setTotalPages(data[0]?.totalPages); + }, }); - const ActionButtons: FC<{ row: User }> = ({ row }) => ( -
          - - - -
          - ), - }); - }} - > - } /> - {t('global.delete')} - -
          - ); - const usersColumns = useMemo( () => [ columnHelper.accessor( @@ -153,7 +104,12 @@ const UserManagement: FC = () => { {t('userManagement.table.actions') ?? ''}
          ), - cell: (props) => , + cell: (props) => ( + + ), meta: { size: '1%', }, @@ -162,31 +118,8 @@ const UserManagement: FC = () => { [t] ); - const deleteUserMutation = useMutation({ - mutationFn: ({ id }: { id: string | number }) => deleteUser(id), - onSuccess: async () => { - close(); - await queryClient.invalidateQueries( - userManagementQueryKeys.getAllEmployees() - ); - toast.open({ - type: ToastTypes.SUCCESS, - title: t('global.notification'), - message: t('toast.success.userDeleted'), - }); - }, - onError: (error: AxiosError) => { - toast.open({ - type: ToastTypes.ERROR, - title: t('global.notificationError'), - message: error?.message ?? '', - }); - }, - }); - if (isLoading) return ; - console.log('users ', users); return (
          @@ -251,4 +184,4 @@ const UserManagement: FC = () => { ); }; -export default UserManagement; \ No newline at end of file +export default UserManagement; diff --git a/GUI/src/pages/ValidationSessions/index.tsx b/GUI/src/pages/ValidationSessions/index.tsx index a1409c11..b92b1410 100644 --- a/GUI/src/pages/ValidationSessions/index.tsx +++ b/GUI/src/pages/ValidationSessions/index.tsx @@ -5,13 +5,14 @@ import sse from 'services/sse-service'; import { useQuery } from '@tanstack/react-query'; import { getDatasetGroupsProgress } from 'services/datasets'; import { ValidationProgressData, SSEEventData } from 'types/datasetGroups'; +import { datasetQueryKeys } from 'utils/queryKeys'; const ValidationSessions: FC = () => { const { t } = useTranslation(); const [progresses, setProgresses] = useState([]); - const { data: progressData,refetch } = useQuery( - ['datasetgroups/progress'], + const { data: progressData, refetch } = useQuery( + datasetQueryKeys.GET_DATASET_GROUP_PROGRESS(), () => getDatasetGroupsProgress(), { onSuccess: (data) => { diff --git a/GUI/src/types/datasetGroups.ts b/GUI/src/types/datasetGroups.ts index 6536bece..aa4ae240 100644 --- a/GUI/src/types/datasetGroups.ts +++ b/GUI/src/types/datasetGroups.ts @@ -11,7 +11,7 @@ export interface Class { id: string; fieldName: string; level: number; - children: Class[] | any; + children: Class[]; } export interface LinkedModel { diff --git a/GUI/src/utils/datasetGroupsUtils.ts b/GUI/src/utils/datasetGroupsUtils.ts index 2ecd392a..fd6b1598 100644 --- a/GUI/src/utils/datasetGroupsUtils.ts +++ b/GUI/src/utils/datasetGroupsUtils.ts @@ -66,16 +66,20 @@ export const transformObjectToArray = (data: Record { + console.log("data ", data) for (let item of data) { if (item.fieldName === '') { + console.log("data s") return true; } if (item.children && item.children.length > 0) { if (validateClassHierarchy(item.children)) { + console.log("data 2") return true; } } } + console.log("data 4") return false; }; diff --git a/GUI/src/utils/endpoints.ts b/GUI/src/utils/endpoints.ts index ca86ac0b..b850ea90 100644 --- a/GUI/src/utils/endpoints.ts +++ b/GUI/src/utils/endpoints.ts @@ -46,6 +46,11 @@ export const correctedTextEndpoints = { `/classifier/inference/corrected-metadata?pageNum=${pageNumber}&pageSize=${pageSize}&platform=${platform}&sortType=${sortType}`, }; +export const authEndpoints = { + GET_EXTENDED_COOKIE: () :string => `/auth/jwt/extend`, + LOGOUT: (): string => `/accounts/logout` +} + export const dataModelsEndpoints = { GET_OVERVIEW: (): string => '/classifier/datamodel/overview', GET_DATAMODELS_FILTERS: (): string => @@ -59,7 +64,7 @@ export const dataModelsEndpoints = { GET_DATA_MODEL_PROGRESS: (): string => `classifier/datamodel/progress`, }; -export const testModelsEnpoinnts = { +export const testModelsEndpoints = { GET_MODELS: (): string => `/classifier/testmodel/models`, CLASSIFY_TEST_MODELS: (): string => `/classifier/testmodel/test-data`, -}; \ No newline at end of file +}; diff --git a/GUI/src/utils/queryKeys.ts b/GUI/src/utils/queryKeys.ts index 05369051..d5c4f745 100644 --- a/GUI/src/utils/queryKeys.ts +++ b/GUI/src/utils/queryKeys.ts @@ -5,7 +5,9 @@ export const userManagementQueryKeys = { pagination?: PaginationState, sorting?: SortingState ) { - return ['accounts/users', pagination, sorting]; + return ['accounts/users', pagination, sorting].filter( + (val) => val !== undefined + ); }, }; @@ -48,6 +50,7 @@ export const datasetQueryKeys = { (val) => val !== undefined ); }, + GET_DATASET_GROUP_PROGRESS: () => ['datasetgroups/progress'], }; export const stopWordsQueryKeys = { @@ -96,4 +99,4 @@ export const dataModelsQueryKeys = { export const testModelsQueryKeys = { GET_TEST_MODELS: () => ['testModels'] -} \ No newline at end of file +} diff --git a/GUI/translations/en/common.json b/GUI/translations/en/common.json index 0232d36e..2e47a447 100644 --- a/GUI/translations/en/common.json +++ b/GUI/translations/en/common.json @@ -44,6 +44,10 @@ "extedSession": "Extend Session", "unAuthorized": "Unauthorized", "unAuthorizedDesc": "You do not have permission to view this page.", + "latest": "Latest", + "failed": "Failed", + "sessionTimeOutTitle": "You session has been ended!", + "sessionTimeOutDesc": "Extend your session or signout from application in {{seconds}}", "close":"Close" }, "menu": { @@ -112,7 +116,7 @@ "integrationSuccessDesc": "You have successfully connected with {{channel}}! Your integration is now complete, and you can start working with {{channel}} seamlessly.", "confirmationModalTitle": "Are you sure?", "disconnectConfirmationModalDesc": "Are you sure you want to disconnect the {{channel}} integration? This action cannot be undone and may affect your workflow and linked issues.", - "connectConfirmationModalDesc": "Are you sure you want to connect the {{channel}} integration?", + "connectConfirmationModalDesc": "Are you sure you want to connect the {{channel}} integration? This action cannot be undone and may affect your workflow and linked issues.", "disconnectErrorTi/tle": "Disconnection Unsuccessful", "disconnectErrorDesc": "Failed to disconnect {{channel}}. Please check your settings and try again. If the problem persists, contact support for assistance.", "addUserButton": " Add a user", @@ -177,7 +181,8 @@ "typeNumbers": "Numbers", "typeDateTime": "DateTime", "addClassButton": "Add class", - "addNowButton": "Add now" + "addNowButton": "Add now", + "selectPlaceholder": "- Select -" }, "classHierarchy": { "title": "Class Hierarchy", @@ -191,6 +196,8 @@ "deleteClaassDesc": "Confirm that you are wish to delete the following record", "columnInsufficientHeader": "Insufficient Columns in Dataset", "columnInsufficientDescription": "The dataset must have at least 2 columns. Additionally, there needs to be at least one column designated as a data class and one column that is not a data class. Please adjust your dataset accordingly.", + "classsesInsufficientHeader": "Insufficient Classes in Dataset", + "classsesInsufficientDescription": "The dataset must have at least 2 main classes in the class hierarchy", "createDatasetSuccessTitle": "Dataset Group Created Successfully", "createDatasetUnsuccessTitle": "Dataset Group Creation Unsuccessful", "createDatasetSucceessDesc": "You have successfully created the dataset group. In the detailed view, you can now see and edit the dataset as needed.", @@ -231,6 +238,10 @@ "validationInitiatedTitle": "Dataset uploaded and validation initiated", "validationInitiatedDesc": "The dataset file was successfully uploaded. The validation and preprocessing is now initiated", "viewValidations": "View Validation Sessions", + "fieldName": "Enter a field name", + "fieldNameError": "{{name}} cannot be used as a field name", + "fieldNameExist": "{{name}} already exists as a field name", + "selectDataType": "Select a data type", "table": { "id": "rowId", "data": "Data", From 81852129bc98502265e58b8262deec399131e09d Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Tue, 20 Aug 2024 19:22:24 +0530 Subject: [PATCH 506/582] progress-bar-issue: fix progress bar issue and add create-migration script code implementation --- .../mock/data_model_progress_sessions.json | 8 +-- .../mock/dataset_progress_sessions.json | 12 ++-- .../DSL/GET/classifier/datamodel/overview.yml | 2 +- .../integration/jira/cloud/subscribe.yml | 31 ++++++++-- .../integration/outlook/subscribe.yml | 4 +- .../integration/toggle-platform.yml | 2 +- .../classifier/integration/outlook/accept.yml | 2 +- README.md | 1 + create-migration.sh | 55 ++++++++++++++++-- notification-server/src/openSearch.js | 56 ++++++++++--------- 10 files changed, 124 insertions(+), 49 deletions(-) diff --git a/DSL/OpenSearch/mock/data_model_progress_sessions.json b/DSL/OpenSearch/mock/data_model_progress_sessions.json index b60ca8e1..c4e63318 100644 --- a/DSL/OpenSearch/mock/data_model_progress_sessions.json +++ b/DSL/OpenSearch/mock/data_model_progress_sessions.json @@ -1,8 +1,8 @@ {"index":{"_id":"1"}} -{"sessionId": "101","trainingStatus": "Initiating Training","trainingMessage": "","progressPercentage": 1,"timestamp": "1801371325497", "sentTo": []} +{"sessionId": "10001000","trainingStatus": "Initiating Training","trainingMessage": "","progressPercentage": 1,"timestamp": "1801371325497", "sentTo": []} {"index":{"_id":"2"}} -{"sessionId": "102","trainingStatus": "Training In-Progress","trainingMessage": "","progressPercentage": 26,"timestamp": "1801371325597", "sentTo": []} +{"sessionId": "10002000","trainingStatus": "Training In-Progress","trainingMessage": "","progressPercentage": 26,"timestamp": "1801371325597", "sentTo": []} {"index":{"_id":"3"}} -{"sessionId": "103","trainingStatus": "Deploying Model","trainingMessage": "","progressPercentage": 52,"timestamp": "1801371325697", "sentTo": []} +{"sessionId": "10003000","trainingStatus": "Deploying Model","trainingMessage": "","progressPercentage": 52,"timestamp": "1801371325697", "sentTo": []} {"index":{"_id":"4"}} -{"sessionId": "104","trainingStatus": "Model Trained And Deployed","trainingMessage": "","progressPercentage": 97,"timestamp": "1801371325797", "sentTo": []} +{"sessionId": "100040000","trainingStatus": "Model Trained And Deployed","trainingMessage": "","progressPercentage": 97,"timestamp": "1801371325797", "sentTo": []} diff --git a/DSL/OpenSearch/mock/dataset_progress_sessions.json b/DSL/OpenSearch/mock/dataset_progress_sessions.json index c0ed6e30..4e400a37 100644 --- a/DSL/OpenSearch/mock/dataset_progress_sessions.json +++ b/DSL/OpenSearch/mock/dataset_progress_sessions.json @@ -1,13 +1,13 @@ {"index":{"_id":"1"}} -{"sessionId": "1","validationStatus": "Initiating Validation","validationMessage": "","progressPercentage": 1,"timestamp": "1801371325497", "sentTo": []} +{"sessionId": "10001000","validationStatus": "Initiating Validation","validationMessage": "","progressPercentage": 1,"timestamp": "1801371325497", "sentTo": []} {"index":{"_id":"2"}} -{"sessionId": "2","validationStatus": "Validation In-Progress","validationMessage": "","progressPercentage": 26,"timestamp": "1801371325597", "sentTo": []} +{"sessionId": "20001000","validationStatus": "Validation In-Progress","validationMessage": "","progressPercentage": 26,"timestamp": "1801371325597", "sentTo": []} {"index":{"_id":"3"}} -{"sessionId": "3","validationStatus": "Cleaning Dataset","validationMessage": "","progressPercentage": 52,"timestamp": "1801371325697", "sentTo": []} +{"sessionId": "30001000","validationStatus": "Cleaning Dataset","validationMessage": "","progressPercentage": 52,"timestamp": "1801371325697", "sentTo": []} {"index":{"_id":"4"}} -{"sessionId": "4","validationStatus": "Generating Data","validationMessage": "","progressPercentage": 97,"timestamp": "1801371325797", "sentTo": []} +{"sessionId": "40001000","validationStatus": "Generating Data","validationMessage": "","progressPercentage": 97,"timestamp": "1801371325797", "sentTo": []} {"index":{"_id":"5"}} -{"sessionId": "5","validationStatus": "Success","validationMessage": "","progressPercentage": 100,"timestamp": "1801371325797", "sentTo": []} +{"sessionId": "50001000","validationStatus": "Success","validationMessage": "","progressPercentage": 100,"timestamp": "1801371325797", "sentTo": []} {"index":{"_id":"6"}} -{"sessionId": "6","validationStatus": "Fail","validationMessage": "Validation failed because class called 'complaints' in 'police' column doesn't exist in the dataset`","progressPercentage": 100,"timestamp": "1801371325797", "sentTo": []} +{"sessionId": "60001000","validationStatus": "Fail","validationMessage": "Validation failed because class called 'complaints' in 'police' column doesn't exist in the dataset`","progressPercentage": 100,"timestamp": "1801371325797", "sentTo": []} diff --git a/DSL/Ruuter.private/DSL/GET/classifier/datamodel/overview.yml b/DSL/Ruuter.private/DSL/GET/classifier/datamodel/overview.yml index 36ffa833..58b47340 100644 --- a/DSL/Ruuter.private/DSL/GET/classifier/datamodel/overview.yml +++ b/DSL/Ruuter.private/DSL/GET/classifier/datamodel/overview.yml @@ -59,7 +59,7 @@ extract_data: check_production_model_status: switch: - - condition: ${is_production_model == 'true'} + - condition: ${is_production_model === 'true'} next: get_production_data_model_meta_data_overview next: get_data_model_meta_data_overview diff --git a/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/subscribe.yml b/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/subscribe.yml index 52b4c8bd..99b42524 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/subscribe.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/integration/jira/cloud/subscribe.yml @@ -8,16 +8,34 @@ declaration: namespace: classifier allowlist: body: - - field: is_connect + - field: isConnect type: boolean description: "Body field 'isConnect'" extract_request_data: assign: - is_connect: ${incoming.body.is_connect} - next: get_auth_header + is_connect: ${incoming.body.isConnect} + next: get_platform_integration_status -#check already subcribe or not from db +get_platform_integration_status: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-platform-integration-status" + body: + platform: 'JIRA' + result: res + next: assign_db_platform_integration_data + +assign_db_platform_integration_data: + assign: + db_platform_status: ${res.response.body[0].isConnect} + next: validate_request + +validate_request: + switch: + - condition: ${db_platform_status !== is_connect} + next: get_auth_header + next: return_already_request get_auth_header: call: http.post @@ -92,4 +110,9 @@ remove_subscription_data: return_result: return: res.response.body + next: end + +return_already_request: + status: 400 + return: "Already Requested-Bad Request" next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/subscribe.yml b/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/subscribe.yml index 881bf013..0e57c2c8 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/subscribe.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/integration/outlook/subscribe.yml @@ -8,7 +8,7 @@ declaration: namespace: classifier allowlist: body: - - field: is_connect + - field: isConnect type: boolean description: "Body field 'isConnect'" headers: @@ -18,7 +18,7 @@ declaration: extract_request_data: assign: - is_connect: ${incoming.body.is_connect} + is_connect: ${incoming.body.isConnect} next: get_platform_integration_status get_platform_integration_status: diff --git a/DSL/Ruuter.private/DSL/POST/classifier/integration/toggle-platform.yml b/DSL/Ruuter.private/DSL/POST/classifier/integration/toggle-platform.yml index 4911ce3a..4ec63ae0 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/integration/toggle-platform.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/integration/toggle-platform.yml @@ -77,7 +77,7 @@ route_to_platform: type: json cookie: ${cookie} body: - is_connect: ${is_connect} + isConnect: ${is_connect} result: res next: check_platform_response_status diff --git a/DSL/Ruuter.public/DSL/POST/classifier/integration/outlook/accept.yml b/DSL/Ruuter.public/DSL/POST/classifier/integration/outlook/accept.yml index c75d9050..ce9d7ae1 100644 --- a/DSL/Ruuter.public/DSL/POST/classifier/integration/outlook/accept.yml +++ b/DSL/Ruuter.public/DSL/POST/classifier/integration/outlook/accept.yml @@ -9,7 +9,7 @@ declaration: allowlist: params: - field: validationToken - type: boolean + type: string description: "parameter 'validationToken'" body: - field: payload diff --git a/README.md b/README.md index 272e7786..613ea29a 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ This repo will primarily contain: - For setting up the database initially, run helper script `./token.sh` - Then setup database password in constant.ini under the key DB_PASSWORD - Run migrations added in this repository by running the helper script `./migrate.sh`(consider db properties before run the script) +- When creating new migrations, use the helper `./create-migration.sh name-of-migration sql` which will create a new file in the correct directory and add the required headers, pass file name(ex: data-model-sessions) and the format(sql or xml) as inputs ### Open Search diff --git a/create-migration.sh b/create-migration.sh index f91fe720..0e2a3643 100644 --- a/create-migration.sh +++ b/create-migration.sh @@ -1,9 +1,54 @@ #!/bin/bash -if [[ $# -eq 0 ]] ; then - echo 'specify descriptive name for migration file' - exit 0 +# Variables +folder_path="DSL/Liquibase/changelog" +liquibase_folder="DSL/Liquibase" +user_input_name="$1" # User input for the name (e.g., user-given name) +file_extension="$2" # User input for the file extension (e.g., "sql" or "xml") + +# Function to increment version +increment_version() { + local version=$1 + local prefix=$(echo "$version" | grep -oP '\d+') + local new_version=$((prefix + 1)) + echo "v${new_version}" +} + +# Find the file with the highest version number matching the format classifier-script-vXX.* +max_version_file=$(ls "$folder_path"/classifier-script-v*.* 2>/dev/null | grep -oP "$folder_path/classifier-script-v\d+" | sort -t 'v' -k 2,2 -n | tail -n 1) + +if [[ -z "$max_version_file" ]]; then + # No files found, start with version v1 + current_version="v0" +else + # Extract the current version + current_version=$(echo "$max_version_file" | grep -oP 'v\d+') +fi + +# Increment the version +new_version=$(increment_version "$current_version") + +# Form new file name with the incremented version and user input name +new_file="classifier-script-${new_version}-${user_input_name}.${file_extension}" + +# Create the new file with the appropriate header +if [ "$file_extension" = "sql" ]; then + echo "-- liquibase formatted sql" > "$folder_path/$new_file" +elif [ "$file_extension" = "xml" ]; then + echo '' > "$folder_path/$new_file" +else + echo "Unsupported file extension." + exit 1 fi -echo "-- liquibase formatted sql" > "DSL/Liquibase/changelog/`date '+%s'`-$1.sql" -echo "-- changeset $(git config user.name):`date '+%s'`" >> "DSL/Liquibase/changelog/`date '+%s'`-$1.sql" +echo "Created new file: $new_file with header" + +# Path to the master.yml file in the Liquibase folder +master_yml_file="$liquibase_folder/master.yml" + +# Add entry to Liquibase master.yml +echo " - include:" >> "$master_yml_file" +echo " file: changelog/$new_file" >> "$master_yml_file" +echo "Updated $master_yml_file with file: changelog/$new_file" + +git add "$folder_path/$new_file" \ No newline at end of file diff --git a/notification-server/src/openSearch.js b/notification-server/src/openSearch.js index 21755d93..6699f0df 100644 --- a/notification-server/src/openSearch.js +++ b/notification-server/src/openSearch.js @@ -6,7 +6,11 @@ const client = new Client({ ssl: openSearchConfig.ssl, }); -async function searchDatasetGroupNotification({ sessionId, connectionId, sender }) { +async function searchDatasetGroupNotification({ + sessionId, + connectionId, + sender, +}) { try { const response = await client.search({ index: openSearchConfig.datasetGroupProgress, @@ -14,24 +18,25 @@ async function searchDatasetGroupNotification({ sessionId, connectionId, sender query: { bool: { must: { match: { sessionId } }, - must_not: { match: { sentTo: connectionId } }, }, }, - sort: { timestamp: { order: "desc" } }, - size: 1, + sort: { timestamp: { order: "desc" } }, + size: 1, }, }); for (const hit of response.body.hits.hits) { - console.log(`hit: ${JSON.stringify(hit)}`); - const sessionJson = { - sessionId: hit._source.sessionId, - progressPercentage: hit._source.progressPercentage, - validationStatus: hit._source.validationStatus, - validationMessage: hit._source.validationMessage, - }; - await sender(sessionJson); - await markAsSent(hit, connectionId); + if (!hit._source.sentTo?.includes(connectionId)) { + console.log(`hit: ${JSON.stringify(hit)}`); + const sessionJson = { + sessionId: hit._source.sessionId, + progressPercentage: hit._source.progressPercentage, + validationStatus: hit._source.validationStatus, + validationMessage: hit._source.validationMessage, + }; + await sender(sessionJson); + await markAsSent(hit, connectionId); + } } } catch (e) { console.error(e); @@ -47,7 +52,6 @@ async function searchModelNotification({ sessionId, connectionId, sender }) { query: { bool: { must: { match: { sessionId } }, - must_not: { match: { sentTo: connectionId } }, }, }, sort: { timestamp: { order: "desc" } }, @@ -56,15 +60,17 @@ async function searchModelNotification({ sessionId, connectionId, sender }) { }); for (const hit of response.body.hits.hits) { - console.log(`hit: ${JSON.stringify(hit)}`); - const sessionJson = { - sessionId: hit._source.sessionId, - progressPercentage: hit._source.progressPercentage, - trainingStatus: hit._source.trainingStatus, - trainingMessage: hit._source.trainingMessage, - }; - await sender(sessionJson); - await markAsSent(hit, connectionId); + if (!hit._source.sentTo?.includes(connectionId)) { + console.log(`hit: ${JSON.stringify(hit)}`); + const sessionJson = { + sessionId: hit._source.sessionId, + progressPercentage: hit._source.progressPercentage, + trainingStatus: hit._source.trainingStatus, + trainingMessage: hit._source.trainingMessage, + }; + await sender(sessionJson); + await markAsSent(hit, connectionId); + } } } catch (e) { console.error(e); @@ -104,7 +110,7 @@ async function updateDatasetGroupProgress( validationStatus, progressPercentage, validationMessage, - timestamp: new Date(), + timestamp: Date.now(), }, }); } @@ -122,7 +128,7 @@ async function updateModelProgress( trainingStatus, progressPercentage, trainingMessage, - timestamp: new Date(), + timestamp: Date.now(), }, }); } From 670f11fd3d321cc164fbc59cc6cf00f04a8ad6cf Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Tue, 20 Aug 2024 21:03:17 +0530 Subject: [PATCH 507/582] no data placeholders added --- .../molecules/CircularSpinner/Spinner.scss | 2 +- .../CorrectedTextsTables.tsx | 4 +- .../components/molecules/NoDataView/index.tsx | 21 ++++ GUI/src/pages/DataModels/index.tsx | 110 ++++++++++-------- GUI/src/pages/DatasetGroups/index.tsx | 50 ++++---- GUI/src/pages/TrainingSessions/index.tsx | 1 + GUI/translations/en/common.json | 7 +- notification-server/src/openSearch.js | 35 +++--- 8 files changed, 140 insertions(+), 90 deletions(-) create mode 100644 GUI/src/components/molecules/NoDataView/index.tsx diff --git a/GUI/src/components/molecules/CircularSpinner/Spinner.scss b/GUI/src/components/molecules/CircularSpinner/Spinner.scss index d6e2abcc..d2297dea 100644 --- a/GUI/src/components/molecules/CircularSpinner/Spinner.scss +++ b/GUI/src/components/molecules/CircularSpinner/Spinner.scss @@ -2,7 +2,7 @@ display: flex; justify-content: center; align-items: center; - height: 100vh; + height: 80vh; } .spinner { diff --git a/GUI/src/components/molecules/CorrectedTextTables/CorrectedTextsTables.tsx b/GUI/src/components/molecules/CorrectedTextTables/CorrectedTextsTables.tsx index 2b74bfb7..5a9ba7b5 100644 --- a/GUI/src/components/molecules/CorrectedTextTables/CorrectedTextsTables.tsx +++ b/GUI/src/components/molecules/CorrectedTextTables/CorrectedTextsTables.tsx @@ -11,6 +11,7 @@ import { formatClassHierarchyArray, formatDateTime } from 'utils/commonUtilts'; import Card from 'components/Card'; import { InferencePayload } from 'types/correctedTextTypes'; import './CorrectedTextTable.scss'; +import NoDataView from '../NoDataView'; const CorrectedTextsTable = ({ correctedTextData, @@ -145,7 +146,8 @@ const CorrectedTextsTable = ({ justifyContent: 'center', }} > - {t('datasetGroups.detailedView.noData') ?? ''} + +
          )} diff --git a/GUI/src/components/molecules/NoDataView/index.tsx b/GUI/src/components/molecules/NoDataView/index.tsx new file mode 100644 index 00000000..4a994000 --- /dev/null +++ b/GUI/src/components/molecules/NoDataView/index.tsx @@ -0,0 +1,21 @@ +import React, { ReactNode } from 'react'; +import { MdDashboard } from 'react-icons/md'; +import { useTranslation } from 'react-i18next'; + +interface NoDataViewProps { + text?: string; + icon?: ReactNode; +} + +const NoDataView: React.FC = ({ text, icon }) => { + const { t } = useTranslation(); + + return ( +
          + +
          {text}
          +
          + ); +}; + +export default NoDataView; \ No newline at end of file diff --git a/GUI/src/pages/DataModels/index.tsx b/GUI/src/pages/DataModels/index.tsx index 0862aabe..4c8fca55 100644 --- a/GUI/src/pages/DataModels/index.tsx +++ b/GUI/src/pages/DataModels/index.tsx @@ -13,6 +13,7 @@ import CircularSpinner from 'components/molecules/CircularSpinner/CircularSpinne import { ButtonAppearanceTypes } from 'enums/commonEnums'; import { DataModelResponse, FilterData, Filters } from 'types/dataModels'; import { dataModelsQueryKeys } from 'utils/queryKeys'; +import NoDataView from 'components/molecules/NoDataView'; const DataModels: FC = () => { const { t } = useTranslation(); @@ -140,31 +141,37 @@ const DataModels: FC = () => { {t('dataModels.productionModels')}
          {' '}
          - -
          - {prodDataModelsData?.data?.map( - (dataset: DataModelResponse, index: number) => { - return ( - - ); - } - )} -
          + {prodDataModelsData?.data?.length > 0 ? ( +
          + {prodDataModelsData?.data?.map( + (dataset: DataModelResponse, index: number) => { + return ( + + ); + } + )} +
          + ) : ( + + )}
          @@ -187,6 +194,7 @@ const DataModels: FC = () => { handleFilterChange('modelName', selection?.value ?? '') } defaultValue={filters?.modelName} + style={{fontSize:"1rem"}} /> {
          -
          - {dataModelsData?.data?.map( - (dataset: DataModelResponse, index: number) => { - return ( - - ); - } - )} -
          + {dataModelsData?.data?.length > 0 ? ( +
          + {dataModelsData?.data?.map( + (dataset: DataModelResponse, index: number) => { + return ( + + ); + } + )} +
          + ) : ( + + )}
          {
          )} -
          - {datasetGroupsData?.response?.data?.map( - (dataset: SingleDatasetType, index: number) => { - return ( - - ); - } - )} -
          + {datasetGroupsData?.response?.data?.length > 0 ? ( +
          + {datasetGroupsData?.response?.data?.map( + (dataset: SingleDatasetType, index: number) => { + return ( + + ); + } + )} +
          + ) : ( + + )} + { }; const eventSources = progressData.map((progress) => { + if(progress.validationStatus !=="Success" && progress.progressPercentage!==100) return sse(`/${progress.id}`, 'model', (data: SSEEventData) => { console.log(`New data for notification ${progress.id}:`, data); handleUpdate(data.sessionId, data); diff --git a/GUI/translations/en/common.json b/GUI/translations/en/common.json index 2e47a447..a8b86bd8 100644 --- a/GUI/translations/en/common.json +++ b/GUI/translations/en/common.json @@ -149,6 +149,7 @@ "datasetGroups": { "title": "Dataset Groups", "createDatasetGroupButton": "Create Dataset Group", + "noDatasets":"No data sets available", "table": { "group": "Dataset Group", "version": "Version", @@ -214,7 +215,7 @@ "import": "Import Dataset", "unsavedChangesWarning": "You have made changes to the ataset which are not saved. Please save the changes to apply", "insufficientExamplesDesc": "Insufficient examples - at least 10 examples are needed to activate the dataset group", - "noData": "No Data Available", + "noData": "No Corrected Texts Available", "noDataDesc": "You have created the dataset group, but there are no datasets available to show here. You can upload a dataset to view it in this space. Once added, you can edit or delete the data as needed.", "importExamples": "Import Examples", "importNewData": "Import New Data", @@ -342,6 +343,8 @@ "productionModels": "Production Models", "dataModels": "Data Models", "createModel": "Create Model", + "noProdModels":"No production models available", + "noModels":"No models available", "filters": { "modelName": "Model Name", "version": "Version", @@ -383,7 +386,7 @@ "replaceTitle": "Warning: Replace Production Model", "replaceDesc": "Adding this model to production will replace the current production model. Are you sure you want to proceed?", "successTitle": "Data Model Created and Trained", - "successDesc": " You have successfully created and trained the data model. You can view it on the data model dashboard.", + "successDesc": " You have successfully created and started training the data model. You can view it on the data model dashboard.", "viewAll": "View All Data Models", "errorTitle": "Error Creating Data Model", "errorDesc": " There was an issue creating or training the data model. Please try again. If the problem persists, contact support for assistance.", diff --git a/notification-server/src/openSearch.js b/notification-server/src/openSearch.js index 99116105..d930e916 100644 --- a/notification-server/src/openSearch.js +++ b/notification-server/src/openSearch.js @@ -6,7 +6,11 @@ const client = new Client({ ssl: openSearchConfig.ssl, }); -async function searchDatasetGroupNotification({ sessionId, connectionId, sender }) { +async function searchDatasetGroupNotification({ + sessionId, + connectionId, + sender, +}) { try { const response = await client.search({ index: openSearchConfig.datasetGroupProgress, @@ -14,24 +18,25 @@ async function searchDatasetGroupNotification({ sessionId, connectionId, sender query: { bool: { must: { match: { sessionId } }, - must_not: { match: { sentTo: connectionId } }, }, }, - sort: { timestamp: { order: "desc" } }, - size: 1, + sort: { timestamp: { order: "desc" } }, + size: 1, }, }); - for (const hit of response.body.hits.hits) { - console.log(`hit: ${JSON.stringify(hit)}`); - const sessionJson = { - sessionId: hit._source.sessionId, - progressPercentage: hit._source.progressPercentage, - validationStatus: hit._source.validationStatus, - validationMessage: hit._source.validationMessage, - }; - await sender(sessionJson); - await markAsSent(hit, connectionId); + console.log(`connectionId: ${connectionId}`); + if (!hit._source.sentTo || !hit._source.sentTo?.includes(connectionId)) { + console.log(`hit: ${JSON.stringify(hit)}`); + const sessionJson = { + sessionId: hit._source.sessionId, + progressPercentage: hit._source.progressPercentage, + validationStatus: hit._source.validationStatus, + validationMessage: hit._source.validationMessage, + }; + await sender(sessionJson); + await markAsSent(hit, connectionId); + } } } catch (e) { console.error(e); @@ -132,4 +137,4 @@ module.exports = { searchModelNotification, updateDatasetGroupProgress, updateModelProgress, -}; +}; \ No newline at end of file From 8026cbf858e0e31dbc7c28c9bc7a6dc2105efce2 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Tue, 20 Aug 2024 21:32:12 +0530 Subject: [PATCH 508/582] fixes --- GUI/src/components/Header/index.tsx | 2 -- GUI/src/components/MainNavigation/index.tsx | 9 ++----- .../TreeNode/ClassHeirarchyTreeNode.tsx | 26 +++++++++++++++++-- .../CreateDatasetGroupModal.tsx | 8 +++--- .../DatasetGroups/CreateDatasetGroup.tsx | 11 +++----- GUI/src/pages/UserManagement/index.tsx | 1 - 6 files changed, 33 insertions(+), 24 deletions(-) diff --git a/GUI/src/components/Header/index.tsx b/GUI/src/components/Header/index.tsx index be7cb857..826e5adb 100644 --- a/GUI/src/components/Header/index.tsx +++ b/GUI/src/components/Header/index.tsx @@ -136,7 +136,6 @@ const Header: FC = () => { {sessionTimeOutModalOpened && ( - <> setSessionTimeOutModalOpened(false)} isOpen={sessionTimeOutModalOpened} @@ -167,7 +166,6 @@ const Header: FC = () => { }) ?? ''}

          - )}
          ); diff --git a/GUI/src/components/MainNavigation/index.tsx b/GUI/src/components/MainNavigation/index.tsx index c77693f3..06fd2bae 100644 --- a/GUI/src/components/MainNavigation/index.tsx +++ b/GUI/src/components/MainNavigation/index.tsx @@ -1,11 +1,6 @@ -import { FC, MouseEvent, useEffect, useState } from 'react'; +import { FC, MouseEvent, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { - NavLink, - useLocation, - useNavigate, - useNavigation, -} from 'react-router-dom'; +import { NavLink, useLocation, useNavigate } from 'react-router-dom'; import { MdApps, MdKeyboardArrowDown, diff --git a/GUI/src/components/molecules/ClassHeirarchy/TreeNode/ClassHeirarchyTreeNode.tsx b/GUI/src/components/molecules/ClassHeirarchy/TreeNode/ClassHeirarchyTreeNode.tsx index 8db04288..060b971d 100644 --- a/GUI/src/components/molecules/ClassHeirarchy/TreeNode/ClassHeirarchyTreeNode.tsx +++ b/GUI/src/components/molecules/ClassHeirarchy/TreeNode/ClassHeirarchyTreeNode.tsx @@ -22,7 +22,6 @@ const ClassHeirarchyTreeNode = ({ nodesError?: boolean; }) => { const { t } = useTranslation(); - const [fieldName, setFieldName] = useState(node.fieldName); const handleChange = (e: ChangeEvent) => { @@ -33,6 +32,16 @@ const ClassHeirarchyTreeNode = ({ else setNodesError(false); }; + const handleKeyPress = ( + event: React.KeyboardEvent, + callback: () => void + ) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + callback(); + } + }; + const errorMessage = nodesError && !fieldName ? t('datasetGroups.classHierarchy.fieldHint') ?? '' @@ -59,15 +68,28 @@ const ClassHeirarchyTreeNode = ({ />
          onAddSubClass(node?.id)} + onKeyPress={(event) => handleKeyPress(event, () => onAddSubClass(node?.id))} className="link" style={{ textDecoration: 'underline', + cursor: 'pointer', }} > {t('datasetGroups.classHierarchy.addSubClass')}
          -
          onDelete(node?.id)} className="link"> +
          onDelete(node?.id)} + onKeyPress={(event) => handleKeyPress(event, () => onDelete(node?.id))} + className="link" + style={{ + cursor: 'pointer', + }} + > {t('global.delete')}
          diff --git a/GUI/src/components/molecules/CreateDatasetGroupModals/CreateDatasetGroupModal.tsx b/GUI/src/components/molecules/CreateDatasetGroupModals/CreateDatasetGroupModal.tsx index f235d662..8f01dffa 100644 --- a/GUI/src/components/molecules/CreateDatasetGroupModals/CreateDatasetGroupModal.tsx +++ b/GUI/src/components/molecules/CreateDatasetGroupModals/CreateDatasetGroupModal.tsx @@ -11,12 +11,12 @@ const CreateDatasetGroupModalController = ({ modalType, isModalOpen, setIsModalOpen, - valiadationErrorType, + validationErrorType, }: { modalType: CreateDatasetGroupModals; isModalOpen: boolean; setIsModalOpen: React.Dispatch>; - valiadationErrorType?: ValidationErrorTypes; + validationErrorType?: ValidationErrorTypes; }) => { const { t } = useTranslation(); const navigate = useNavigate(); @@ -27,7 +27,7 @@ const CreateDatasetGroupModalController = ({ setIsModalOpen(false)} > - {valiadationErrorType === ValidationErrorTypes.VALIDATION_CRITERIA + {validationErrorType === ValidationErrorTypes.VALIDATION_CRITERIA ? t('datasetGroups.modals.columnInsufficientDescription') : t('datasetGroups.modals.classsesInsufficientDescription')} diff --git a/GUI/src/pages/DatasetGroups/CreateDatasetGroup.tsx b/GUI/src/pages/DatasetGroups/CreateDatasetGroup.tsx index 051a5be7..5a93fb80 100644 --- a/GUI/src/pages/DatasetGroups/CreateDatasetGroup.tsx +++ b/GUI/src/pages/DatasetGroups/CreateDatasetGroup.tsx @@ -39,6 +39,7 @@ const CreateDatasetGroup: FC = () => { { id: uuidv4(), fieldName: '', level: 0, children: [] }, ]; + // Properly destructure useState calls into value and setter pairs const [isModalOpen, setIsModalOpen] = useState(false); const [modalType, setModalType] = useState( CreateDatasetGroupModals.NULL @@ -51,7 +52,7 @@ const CreateDatasetGroup: FC = () => { const [validationRuleError, setValidationRuleError] = useState(false); const [nodes, setNodes] = useState(initialClass); const [nodesError, setNodesError] = useState(false); - const [valiadationErrorType, setValidationErrorType] = + const [validationErrorType, setValidationErrorType] = useState(ValidationErrorTypes.NULL); const validateData = useCallback(() => { @@ -153,17 +154,11 @@ const CreateDatasetGroup: FC = () => { modalType={modalType} isModalOpen={isModalOpen} setIsModalOpen={setIsModalOpen} - valiadationErrorType={valiadationErrorType} + validationErrorType={validationErrorType} />
          )} - {dataModel && createOptions && !isLoading ? ( + {((type === 'configure' && dataModel?.dgId > 0) || type === 'create') && + createOptions && + !isLoading ? (
          {t('dataModels.dataModelForm.datasetGroup')}{' '} diff --git a/GUI/src/pages/DataModels/ConfigureDataModel.tsx b/GUI/src/pages/DataModels/ConfigureDataModel.tsx index 054faf7e..af9e870f 100644 --- a/GUI/src/pages/DataModels/ConfigureDataModel.tsx +++ b/GUI/src/pages/DataModels/ConfigureDataModel.tsx @@ -34,7 +34,7 @@ const ConfigureDataModel: FC = ({ const [enabled, setEnabled] = useState(true); const [initialData, setInitialData] = useState>({ modelName: '', - dgId: '', + dgId: 0, platform: '', baseModels: [], maturity: '', @@ -43,7 +43,7 @@ const ConfigureDataModel: FC = ({ const [dataModel, setDataModel] = useState({ modelId: 0, modelName: '', - dgId: '', + dgId: 0, platform: '', baseModels: [], maturity: '', @@ -59,7 +59,7 @@ const ConfigureDataModel: FC = ({ setDataModel({ modelId: data?.modelId || 0, modelName: data?.modelName || '', - dgId: data?.connectedDgId || '', + dgId: data?.connectedDgId || 0, platform: data?.deploymentEnv || '', baseModels: data?.baseModels || [], maturity: data?.maturityLabel || '', @@ -185,11 +185,12 @@ const ConfigureDataModel: FC = ({
          -
          diff --git a/GUI/src/pages/DataModels/index.tsx b/GUI/src/pages/DataModels/index.tsx index 4c8fca55..0b441379 100644 --- a/GUI/src/pages/DataModels/index.tsx +++ b/GUI/src/pages/DataModels/index.tsx @@ -156,8 +156,8 @@ const DataModels: FC = () => { datasetGroupName={dataset?.connectedDgName} version={`V${dataset?.majorVersion}.${dataset?.minorVersion}`} isLatest={dataset.latest} - dgVersion={dataset?.dgVersion} - lastTrained={dataset?.lastTrained} + dgVersion={`V${dataset?.connectedDgMajorVersion}.${dataset?.connectedDgMinorVersion}${dataset?.connectedDgPatchVersion}`} + lastTrained={dataset?.lastTrainedTimestamp} trainingStatus={dataset.trainingStatus} platform={dataset?.deploymentEnv} maturity={dataset?.maturityLabel} @@ -287,8 +287,8 @@ const DataModels: FC = () => { datasetGroupName={dataset?.connectedDgName} version={`V${dataset?.majorVersion}.${dataset?.minorVersion}`} isLatest={dataset.latest} - dgVersion={dataset?.dgVersion} - lastTrained={dataset?.lastTrained} + dgVersion={`V${dataset?.connectedDgMajorVersion}.${dataset?.connectedDgMinorVersion}${dataset?.connectedDgPatchVersion}`} + lastTrained={dataset?.lastTrainedTimestamp} trainingStatus={dataset.trainingStatus} platform={dataset?.deploymentEnv} maturity={dataset?.maturityLabel} diff --git a/GUI/src/pages/TestModel/index.tsx b/GUI/src/pages/TestModel/index.tsx index 97cb82da..220a0b9e 100644 --- a/GUI/src/pages/TestModel/index.tsx +++ b/GUI/src/pages/TestModel/index.tsx @@ -87,7 +87,7 @@ const TestModel: FC = () => { />
          -
          +

          {t('testModels.classifyTextLabel')}

          {
          } diff --git a/GUI/src/components/molecules/CorrectedTextTables/CorrectedTextsTables.tsx b/GUI/src/components/molecules/CorrectedTextTables/CorrectedTextsTables.tsx index 5a9ba7b5..7c0b4758 100644 --- a/GUI/src/components/molecules/CorrectedTextTables/CorrectedTextsTables.tsx +++ b/GUI/src/components/molecules/CorrectedTextTables/CorrectedTextsTables.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useMemo } from 'react'; import SkeletonTable from '../TableSkeleton/TableSkeleton'; import DataTable from 'components/DataTable'; import { diff --git a/GUI/src/components/molecules/DataModelCard/index.tsx b/GUI/src/components/molecules/DataModelCard/index.tsx index 4dc06832..1668ffa8 100644 --- a/GUI/src/components/molecules/DataModelCard/index.tsx +++ b/GUI/src/components/molecules/DataModelCard/index.tsx @@ -10,7 +10,7 @@ import { TrainingResults } from 'types/dataModels'; type DataModelCardProps = { modelId: number; - dataModelName?: string | undefined; + dataModelName?: string; datasetGroupName?: string; version?: string; isLatest?: boolean; @@ -166,17 +166,17 @@ const DataModelCard: FC> = ({
          {results?.trainingResults?.classes?.map((c: string) => { - return
          {c}
          ; + return
          {c}
          ; })}
          {results?.trainingResults?.accuracy?.map((c: string) => { - return
          {c}
          ; + return
          {c}
          ; })}
          {results?.trainingResults?.f1_score?.map((c: string) => { - return
          {c}
          ; + return
          {c}
          ; })}
          diff --git a/GUI/src/components/molecules/TrainingSessionCard/index.tsx b/GUI/src/components/molecules/TrainingSessionCard/index.tsx index 9f37189a..e85f6c11 100644 --- a/GUI/src/components/molecules/TrainingSessionCard/index.tsx +++ b/GUI/src/components/molecules/TrainingSessionCard/index.tsx @@ -1,4 +1,3 @@ -import { FC, useState } from 'react'; import { useTranslation } from 'react-i18next'; import ProgressBar from 'components/ProgressBar'; import { Card, Label } from 'components'; @@ -32,11 +31,11 @@ const TrainingSessionCard: React.FC = ({
          {modelName}
          - {isLatest && } + {isLatest && } {platform && }{' '} {maturity && } - {status === 'Fail' && } + {status === 'Fail' && }
          } diff --git a/GUI/src/pages/CorrectedTexts/index.tsx b/GUI/src/pages/CorrectedTexts/index.tsx index 682b9fc7..3babed50 100644 --- a/GUI/src/pages/CorrectedTexts/index.tsx +++ b/GUI/src/pages/CorrectedTexts/index.tsx @@ -3,7 +3,6 @@ import './index.scss'; import { useTranslation } from 'react-i18next'; import { ButtonAppearanceTypes } from 'enums/commonEnums'; import { Button, FormSelect } from 'components'; -import CorrectedTextsTables from 'components/molecules/CorrectedTextTables/CorrectedTextsTables'; import { useQuery } from '@tanstack/react-query'; import { correctedTextEndpoints } from 'utils/endpoints'; import apiDev from '../../services/api-dev'; @@ -68,7 +67,6 @@ const CorrectedTexts: FC = () => { appearance={ButtonAppearanceTypes.PRIMARY} size="m" onClick={() => { - // setNewUserModal(true); }} > {t('correctedTexts.export')} diff --git a/GUI/translations/en/common.json b/GUI/translations/en/common.json index 39f6b4b5..09261050 100644 --- a/GUI/translations/en/common.json +++ b/GUI/translations/en/common.json @@ -41,7 +41,7 @@ "desc": "desc", "reset": "Reset", "choose": "Choose", - "extedSession": "Extend Session", + "extendSession": "Extend Session", "unAuthorized": "Unauthorized", "unAuthorizedDesc": "You do not have permission to view this page.", "latest": "Latest", From a2f391bc1b154aceeca401026110488d5e38214a Mon Sep 17 00:00:00 2001 From: Thirunayan22 Date: Fri, 23 Aug 2024 19:52:31 +0530 Subject: [PATCH 524/582] added fix in anonymizer --- anonymizer/anonymizer_api.py | 12 +++++++++++- anonymizer/webhook_request_retention.py | 16 ++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 anonymizer/webhook_request_retention.py diff --git a/anonymizer/anonymizer_api.py b/anonymizer/anonymizer_api.py index 1139f6f3..569b79f6 100644 --- a/anonymizer/anonymizer_api.py +++ b/anonymizer/anonymizer_api.py @@ -3,6 +3,7 @@ from ner import NERProcessor from text_processing import TextProcessor from fake_replacements import FakeReplacer +from webhook_request_retention import RequestRetentionList from html_cleaner import HTMLCleaner import os import requests @@ -13,9 +14,11 @@ from fastapi.responses import StreamingResponse, JSONResponse import pandas as pd import io +import uvicorn app = FastAPI() +request_validator = RequestRetentionList() ner_processor = NERProcessor() html_cleaner = HTMLCleaner() @@ -24,6 +27,12 @@ def anonymizer_functions(payload): try: + if(payload.get("platform", "").lower()=="outlook"): + orginal_request = request_validator.add_email(payload.get("mailId", "")+payload.get("parentFolderId", "")) + if not orginal_request: + return False + + data_dict = payload.get("data", {}) if len(data_dict["attachments"]) <= 0: @@ -78,6 +87,7 @@ def anonymizer_functions(payload): return output_payload except Exception as e: print(f"Error while annonymizing the data : {e}") + return False @app.post("/anonymize") async def process_text(request: Request, background_tasks: BackgroundTasks): @@ -165,5 +175,5 @@ async def anonymize_file(file: UploadFile = File(...), columns: str = Form(...)) if __name__ == "__main__": - import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8010) diff --git a/anonymizer/webhook_request_retention.py b/anonymizer/webhook_request_retention.py new file mode 100644 index 00000000..8fd94625 --- /dev/null +++ b/anonymizer/webhook_request_retention.py @@ -0,0 +1,16 @@ +import os +from collections import deque + +class RequestRetentionList: + def __init__(self): + self.retention_limit = int(os.getenv('EMAIL_ID_RETENTION_LIMIT', '100')) + self.email_list = deque(maxlen=self.retention_limit) + + def add_email(self, email_id: str) -> bool: + if email_id in self.email_list: + print(self.email_list) + return False + else: + self.email_list.append(email_id) + print(self.email_list) + return True \ No newline at end of file From 8345900bc7496c24a05ecf18131d0a4dbcb2cca4 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 23 Aug 2024 19:54:20 +0530 Subject: [PATCH 525/582] conflict fix --- DSL/CronManager/script/data_processor_exec.sh | 120 ++++++++++++------ DSL/CronManager/script/data_validator_exec.sh | 119 +++++++++++------ docker-compose.yml | 45 +++++-- 3 files changed, 196 insertions(+), 88 deletions(-) diff --git a/DSL/CronManager/script/data_processor_exec.sh b/DSL/CronManager/script/data_processor_exec.sh index d6b87644..12a8feb4 100644 --- a/DSL/CronManager/script/data_processor_exec.sh +++ b/DSL/CronManager/script/data_processor_exec.sh @@ -1,47 +1,93 @@ #!/bin/bash -echo "Started Shell Script to process" -# Ensure required environment variables are set -if [ -z "$dgId" ] || [ -z "$newDgId" ] || [ -z "$cookie" ] || [ -z "$updateType" ] || [ -z "$savedFilePath" ] || [ -z "$patchPayload" ] || [ -z "$sessionId" ]; then - echo "One or more environment variables are missing." - echo "Please set dgId, newDgId, cookie, updateType, savedFilePath, patchPayload, and sessionId." - exit 1 -fi +VENV_DIR="/home/cronmanager/clsenv" +REQUIREMENTS="dataset_processor/requirements.txt" +PYTHON_SCRIPT="dataset_processor/invoke_dataset_processor.py" -# Construct the payload using here document -payload=$(cat < /dev/null 2>&1 } -EOF -) -# Set the forward URL -forward_url="http://dataset-processor:8001/init-dataset-process" +if [ -d "$VENV_DIR" ]; then + echo "Virtual environment already exists. Activating..." +else + echo "Virtual environment does not exist. Creating..." + python3.12 -m venv $VENV_DIR +fi -# Send the request -response=$(curl -s -w "\nHTTP_STATUS_CODE:%{http_code}" -X POST "$forward_url" \ - -H "Content-Type: application/json" \ - -H "Cookie: $cookie" \ - -d "$payload") +. $VENV_DIR/bin/activate -# Extract the HTTP status code from the response -http_status=$(echo "$response" | grep "HTTP_STATUS_CODE" | awk -F: '{print $2}' | tr -d '[:space:]') +echo "Python executable in use: $(which python3)" -# Extract the body from the response -http_body=$(echo "$response" | grep -v "HTTP_STATUS_CODE") +if [ -f "$REQUIREMENTS" ]; then + echo "Checking if required packages are installed..." + + while IFS= read -r requirement || [ -n "$requirement" ]; do + package_name=$(echo "$requirement" | cut -d '=' -f 1 | tr -d '[:space:]') + + if is_package_installed "$package_name"; then + echo "Package '$package_name' is already installed." + else + echo "Package '$package_name' is not installed. Installing..." + pip install "$requirement" + fi + done < "$REQUIREMENTS" +else + echo "Requirements file not found: $REQUIREMENTS" +fi -# Check if the request was successful -if [ "$http_status" -ge 200 ] && [ "$http_status" -lt 300 ]; then - echo "Request successful." - echo "Response: $http_body" +# Check if the Python script exists +if [ -f "$PYTHON_SCRIPT" ]; then + echo "Running the Python script: $PYTHON_SCRIPT" + python "$PYTHON_SCRIPT" else - echo "Request failed with status code $http_status." - echo "Response: $http_body" - exit \ No newline at end of file + echo "Python script not found: $PYTHON_SCRIPT" +fi + + +# echo "Started Shell Script to process" +# # Ensure required environment variables are set +# if [ -z "$dgId" ] || [ -z "$newDgId" ] || [ -z "$cookie" ] || [ -z "$updateType" ] || [ -z "$savedFilePath" ] || [ -z "$patchPayload" ] || [ -z "$sessionId" ]; then +# echo "One or more environment variables are missing." +# echo "Please set dgId, newDgId, cookie, updateType, savedFilePath, patchPayload, and sessionId." +# exit 1 +# fi + +# # Construct the payload using here document +# payload=$(cat < /dev/null 2>&1 } -EOF -) -# Set the forward URL -forward_url="http://dataset-processor:8001/datasetgroup/update/validation/status" +if [ -d "$VENV_DIR" ]; then + echo "Virtual environment already exists. Activating..." +else + echo "Virtual environment does not exist. Creating..." + python3.12 -m venv $VENV_DIR +fi -# Send the request -response=$(curl -s -w "\nHTTP_STATUS_CODE:%{http_code}" -X POST "$forward_url" \ - -H "Content-Type: application/json" \ - -H "Cookie: $cookie" \ - -d "$payload") +. $VENV_DIR/bin/activate -# Extract the HTTP status code from the response -http_status=$(echo "$response" | grep "HTTP_STATUS_CODE" | awk -F: '{print $2}' | tr -d '[:space:]') +echo "Python executable in use: $(which python3)" -# Extract the body from the response -http_body=$(echo "$response" | grep -v "HTTP_STATUS_CODE") +if [ -f "$REQUIREMENTS" ]; then + echo "Checking if required packages are installed..." + + while IFS= read -r requirement || [ -n "$requirement" ]; do + package_name=$(echo "$requirement" | cut -d '=' -f 1 | tr -d '[:space:]') + + if is_package_installed "$package_name"; then + echo "Package '$package_name' is already installed." + else + echo "Package '$package_name' is not installed. Installing..." + pip install "$requirement" + fi + done < "$REQUIREMENTS" +else + echo "Requirements file not found: $REQUIREMENTS" +fi -# Check if the request was successful -if [ "$http_status" -ge 200 ] && [ "$http_status" -lt 300 ]; then - echo "Request successful." - echo "Response: $http_body" +# Check if the Python script exists +if [ -f "$PYTHON_SCRIPT" ]; then + echo "Running the Python script: $PYTHON_SCRIPT" + python "$PYTHON_SCRIPT" else - echo "Request failed with status code $http_status." - echo "Response: $http_body" - exit 1 -fi \ No newline at end of file + echo "Python script not found: $PYTHON_SCRIPT" +fi + +# echo "Started Shell Script to validator" +# # Ensure required environment variables are set +# if [ -z "$dgId" ] || [ -z "$newDgId" ] || [ -z "$cookie" ] || [ -z "$updateType" ] || [ -z "$savedFilePath" ] || [ -z "$patchPayload" ]; then +# echo "One or more environment variables are missing." +# echo "Please set dgId, newDgId, cookie, updateType, savedFilePath, and patchPayload." +# exit 1 +# fi + +# # Construct the payload using here document +# payload=$(cat < Date: Fri, 23 Aug 2024 19:59:13 +0530 Subject: [PATCH 526/582] realignment with dev --- docker-compose.yml | 43 ++++++++++++------------------------------- 1 file changed, 12 insertions(+), 31 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 1001bcbb..6ae02de6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,8 +4,8 @@ services: image: ruuter environment: - application.cors.allowedOrigins=http://localhost:8086,http://localhost:3001,http://localhost:3003,http://localhost:3004,http://localhost:8080,http://localhost:8000 - - application.http CodesAllowList=200,201,202,204,400,401,403,404,500 - - application.internalRequests.allowedIPs=127.0.0.1,172.25.0.7,172.25.0.21,172.25.0.22 + - application.httpCodesAllowList=200,201,202,204,400,401,403,404,500 + - application.internalRequests.allowedIPs=127.0.0.1,172.25.0.7,172.25.0.21 - application.logging.displayRequestContent=true - application.incomingRequests.allowedMethodTypes=POST,GET,PUT - application.logging.displayResponseContent=true @@ -169,16 +169,6 @@ services: ipv4_address: 172.25.0.10 restart: always - - init: - image: busybox - command: ["sh", "-c", "chmod -R 777 /shared"] - volumes: - - shared-volume:/shared - networks: - bykstack: - ipv4_address: 172.25.0.12 - cron-manager: container_name: cron-manager image: cron-manager-python @@ -195,9 +185,15 @@ services: networks: bykstack: ipv4_address: 172.25.0.11 - depends_on: - - init - - s3-ferry + + init: + image: busybox + command: ["sh", "-c", "chmod -R 777 /shared"] + volumes: + - shared-volume:/shared + networks: + bykstack: + ipv4_address: 172.25.0.12 file-handler: build: @@ -274,7 +270,7 @@ services: model-inference: build: - context: ./model-inference + context: ./model_inference dockerfile: Dockerfile container_name: model-inference volumes: @@ -446,21 +442,6 @@ volumes: shared-volume: opensearch-data: - -# Uncomment below container if you wish to debug progress bar sessions in opensearch dashboard -# opensearch-dashboards: -# image: opensearchproject/opensearch-dashboards:2.11.1 -# container_name: opensearch-dashboards -# environment: -# - OPENSEARCH_HOSTS=http://opensearch-node:9200 -# - DISABLE_SECURITY_DASHBOARDS_PLUGIN=true -# ports: -# - 5601:5601 -# networks: -# bykstack: -# ipv4_address: 172.25.0.24 - - networks: bykstack: name: bykstack From 753fe28520b7c7d027ce9b1e3b9aa1324dacc693 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Fri, 23 Aug 2024 20:06:37 +0530 Subject: [PATCH 527/582] bug fixes --- .../pages/DatasetGroups/ViewDatasetGroup.tsx | 27 ++++++++++++++++--- GUI/src/pages/UserManagement/index.tsx | 3 ++- GUI/src/services/datasets.ts | 12 ++++++--- GUI/src/utils/endpoints.ts | 1 + GUI/translations/en/common.json | 6 +++-- .../components/CopyableTextField/index.tsx | 14 +++++----- 6 files changed, 48 insertions(+), 15 deletions(-) diff --git a/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx b/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx index 21341ee5..a607455b 100644 --- a/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx +++ b/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx @@ -14,6 +14,7 @@ import { } from 'types/datasetGroups'; import { useNavigate } from 'react-router-dom'; import { + deleteDatasetGroup, exportDataset, getDatasets, getMetadata, @@ -277,6 +278,8 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { handleCloseModals(); }, onError: () => { + handleCloseModals(); + setImportStatus('ABORTED'); open({ title: t('datasetGroups.detailedView.ImportDataUnsucessTitle') ?? '', content: ( @@ -413,8 +416,26 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { }, onError: () => { open({ - title: 'Dataset Group Update Unsuccessful', - content:

          Something went wrong. Please try again.

          , + title: t('datasetGroups.detailedView.modals.edit.error'), + content:

          { t('datasetGroups.modals.delete.errorDesc') }

          , + }); + }, + }); + + const handleDeleteDataset = () => { + deleteDatasetMutation.mutate(dgId); + }; + + const deleteDatasetMutation = useMutation({ + mutationFn: (dgId: number) => deleteDatasetGroup(dgId), + onSuccess: async () => { + navigate(0); + close(); + }, + onError: () => { + open({ + title: t('datasetGroups.detailedView.modals.delete.error'), + content:

          { t('datasetGroups.modals.delete.errorDesc') }

          , }); }, }); @@ -483,7 +504,7 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { diff --git a/GUI/src/pages/UserManagement/index.tsx b/GUI/src/pages/UserManagement/index.tsx index bc7ca405..a665981e 100644 --- a/GUI/src/pages/UserManagement/index.tsx +++ b/GUI/src/pages/UserManagement/index.tsx @@ -42,7 +42,7 @@ const UserManagement: FC = () => { pagination: PaginationState, sorting: SortingState ) => { - const sort = getSortString(sorting?.length); + const sort = getSortString(sorting?.length); const { data } = await apiDev.post(userManagementEndpoints.FETCH_USERS(), { page: pagination?.pageIndex + 1, page_size: pagination?.pageSize, @@ -69,6 +69,7 @@ const UserManagement: FC = () => { } ), columnHelper.accessor('useridcode', { + id :"idCode", header: t('userManagement.table.personalId') ?? '', }), columnHelper.accessor('csaTitle', { diff --git a/GUI/src/services/datasets.ts b/GUI/src/services/datasets.ts index 3c546c4d..438f37ad 100644 --- a/GUI/src/services/datasets.ts +++ b/GUI/src/services/datasets.ts @@ -44,9 +44,7 @@ export async function enableDataset(enableData: Operation) { } export async function getFilterData() { - const { data } = await apiDev.get( - datasetsEndpoints.GET_DATASET_FILTERS() - ); + const { data } = await apiDev.get(datasetsEndpoints.GET_DATASET_FILTERS()); return data; } @@ -133,6 +131,14 @@ export async function majorUpdate(updatedData: DatasetGroup) { return data; } +export async function deleteDatasetGroup(dgId: number) { + const { data } = await apiDev.post( + datasetsEndpoints.DELETE_DATASET_GROUP(), + { dgId } + ); + return data; +} + export async function getStopWords() { const { data } = await apiDev.get(datasetsEndpoints.GET_STOP_WORDS()); return data?.response?.stopWords; diff --git a/GUI/src/utils/endpoints.ts b/GUI/src/utils/endpoints.ts index 9148c924..0e7b08e7 100644 --- a/GUI/src/utils/endpoints.ts +++ b/GUI/src/utils/endpoints.ts @@ -29,6 +29,7 @@ export const datasetsEndpoints = { `/classifier/datasetgroup/update/minor`, DATASET_GROUP_MAJOR_UPDATE: (): string => `/classifier/datasetgroup/update/major`, + DELETE_DATASET_GROUP:(): string =>`classifier/datasetgroup/delete`, GET_STOP_WORDS: (): string => `/classifier/datasetgroup/stop-words`, POST_STOP_WORDS: (): string => `/classifier/datasetgroup/update/stop-words`, DELETE_STOP_WORD: (): string => `/classifier/datasetgroup/delete/stop-words`, diff --git a/GUI/translations/en/common.json b/GUI/translations/en/common.json index 09261050..ddec71bd 100644 --- a/GUI/translations/en/common.json +++ b/GUI/translations/en/common.json @@ -276,13 +276,15 @@ }, "delete": { "title": "Are you sure?", - "description": "Once you delete the dataset all models connected to this model will become untrainable. Are you sure you want to proceed?" + "description": "Once you delete the dataset all models connected to this model will become untrainable. Are you sure you want to proceed?", + "error":"Dataset Group Deletion Unsuccessful" }, "edit": { "title": "Edit", "data": "Data", "label": "Label", - "update": "Update" + "update": "Update", + "error":"Dataset Group Update Unsuccessful" }, "upload": { "title": "Data upload successful", diff --git a/outlook-consent-app/src/app/components/CopyableTextField/index.tsx b/outlook-consent-app/src/app/components/CopyableTextField/index.tsx index 7826e24f..45d5fad6 100644 --- a/outlook-consent-app/src/app/components/CopyableTextField/index.tsx +++ b/outlook-consent-app/src/app/components/CopyableTextField/index.tsx @@ -1,17 +1,19 @@ import React, { useRef, useState } from 'react'; -import "../../page.module.css" import styles from "../../page.module.css"; const CopyableTextField: React.FC<{ value: string }> = ({ value }) => { const [copied, setCopied] = useState(false); const textFieldRef = useRef(null); - const copyToClipboard = () => { + const copyToClipboard = async () => { if (textFieldRef.current) { - textFieldRef.current.select(); - document.execCommand('copy'); - setCopied(true); - setTimeout(() => setCopied(false), 2000); + try { + await navigator.clipboard.writeText(textFieldRef.current.value); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch (err) { + console.error('Failed to copy text: ', err); + } } }; From 2475758dccde9541ec38b2bf00a18ed48a43911f Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Fri, 23 Aug 2024 20:08:22 +0530 Subject: [PATCH 528/582] model _ to dash change --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 6ae02de6..d85e7862 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -270,7 +270,7 @@ services: model-inference: build: - context: ./model_inference + context: ./model-inference dockerfile: Dockerfile container_name: model-inference volumes: From 34c2742dc56de2f48cd34ecf00b4c1e6c742906e Mon Sep 17 00:00:00 2001 From: Thirunayan22 Date: Sat, 24 Aug 2024 01:57:22 +0530 Subject: [PATCH 529/582] fixed outlook corrected texts pipeline --- DSL/Resql/get-input-metadata-exits-by-id.sql | 4 +- DSL/Resql/update-input-metadata.sql | 2 +- .../classifier/inference/exist.yml | 8 +- .../DSL/POST/internal/corrected.yml | 7 +- .../DSL/{GET => POST}/internal/exist.yml | 8 +- docker-compose.yml | 4 +- model-inference/inference_pipeline.py | 10 +- model-inference/inference_wrapper.py | 67 +++++-- model-inference/model_inference.py | 41 +++-- model-inference/model_inference_api.py | 169 ++++++++++-------- model-inference/utils.py | 4 +- 11 files changed, 205 insertions(+), 119 deletions(-) rename DSL/Ruuter.private/DSL/{GET => POST}/classifier/inference/exist.yml (93%) rename DSL/Ruuter.public/DSL/{GET => POST}/internal/exist.yml (93%) diff --git a/DSL/Resql/get-input-metadata-exits-by-id.sql b/DSL/Resql/get-input-metadata-exits-by-id.sql index 8de7e2e9..2996f006 100644 --- a/DSL/Resql/get-input-metadata-exits-by-id.sql +++ b/DSL/Resql/get-input-metadata-exits-by-id.sql @@ -1,2 +1,2 @@ -SELECT id -FROM "input" WHERE id =:id; \ No newline at end of file +SELECT input_id +FROM "input" WHERE input_id =:id; \ No newline at end of file diff --git a/DSL/Resql/update-input-metadata.sql b/DSL/Resql/update-input-metadata.sql index d1d1f695..bd4f1ef3 100644 --- a/DSL/Resql/update-input-metadata.sql +++ b/DSL/Resql/update-input-metadata.sql @@ -4,4 +4,4 @@ SET corrected_labels = :corrected_labels::JSONB, average_corrected_classes_probability = :average_corrected_classes_probability, primary_folder_id = :primary_folder_id -WHERE id = :id; +WHERE input_id = :id; diff --git a/DSL/Ruuter.private/DSL/GET/classifier/inference/exist.yml b/DSL/Ruuter.private/DSL/POST/classifier/inference/exist.yml similarity index 93% rename from DSL/Ruuter.private/DSL/GET/classifier/inference/exist.yml rename to DSL/Ruuter.private/DSL/POST/classifier/inference/exist.yml index d8382f34..0e7d40b2 100644 --- a/DSL/Ruuter.private/DSL/GET/classifier/inference/exist.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/inference/exist.yml @@ -2,19 +2,19 @@ declaration: call: declare version: 0.1 description: "Description placeholder for 'EXIST'" - method: get + method: post accepts: json returns: json namespace: classifier allowlist: - params: + body: - field: inputId type: string - description: "Parameter 'inferenceId'" + description: "Body field 'inputId'" extract_data: assign: - input_id: ${incoming.params.inputId} + input_id: ${incoming.body.inputId} exist: false next: get_input_metadata_by_id diff --git a/DSL/Ruuter.public/DSL/POST/internal/corrected.yml b/DSL/Ruuter.public/DSL/POST/internal/corrected.yml index 45e7deb4..dd8bb444 100644 --- a/DSL/Ruuter.public/DSL/POST/internal/corrected.yml +++ b/DSL/Ruuter.public/DSL/POST/internal/corrected.yml @@ -9,7 +9,7 @@ declaration: allowlist: body: - field: inferenceId - type: number + type: string description: "Body field 'inferenceId'" - field: isCorrected type: boolean @@ -26,10 +26,7 @@ declaration: - field: platform type: string description: "Body field 'platform'" - headers: - - field: cookie - type: string - description: "Cookie field" + extract_request_data: assign: diff --git a/DSL/Ruuter.public/DSL/GET/internal/exist.yml b/DSL/Ruuter.public/DSL/POST/internal/exist.yml similarity index 93% rename from DSL/Ruuter.public/DSL/GET/internal/exist.yml rename to DSL/Ruuter.public/DSL/POST/internal/exist.yml index d8382f34..0e7d40b2 100644 --- a/DSL/Ruuter.public/DSL/GET/internal/exist.yml +++ b/DSL/Ruuter.public/DSL/POST/internal/exist.yml @@ -2,19 +2,19 @@ declaration: call: declare version: 0.1 description: "Description placeholder for 'EXIST'" - method: get + method: post accepts: json returns: json namespace: classifier allowlist: - params: + body: - field: inputId type: string - description: "Parameter 'inferenceId'" + description: "Body field 'inputId'" extract_data: assign: - input_id: ${incoming.params.inputId} + input_id: ${incoming.body.inputId} exist: false next: get_input_metadata_by_id diff --git a/docker-compose.yml b/docker-compose.yml index 1001bcbb..0f56221c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -285,11 +285,9 @@ services: - S3_FERRY_URL=http://s3-ferry:3000/v1/files/copy - JIRA_MODEL_DOWNLOAD_DIRECTORY=/shared/models/jira - OUTLOOK_MODEL_DOWNLOAD_DIRECTORY=/shared/models/outlook - - GET_INFERENCE_DATASET_EXIST_URL=http://ruuter-public:8086/internal/exist?inputId=inferenceInputId + - GET_INFERENCE_DATASET_EXIST_URL=http://ruuter-public:8086/internal/exist - CREATE_INFERENCE_URL=http://ruuter-public:8086/internal/create - UPDATE_INFERENCE_URL=http://ruuter-public:8086/internal/corrected - - GET_DATAMODEL_METADATA_BY_ID_URL=http://ruuter-private:8088/classifier/datamodel/metadata?modelId=inferenceModelId - - GET_DATASET_GROUP_METADATA_BY_ID_URL=http://ruuter-private:8088/classifier/datasetgroup/group/metadata?groupId=dataSetGroupId - CLASS_HIERARCHY_VALIDATION_URL=http://hierarchy-validation:8009/check-folder-hierarchy - OUTLOOK_ACCESS_TOKEN_API_URL=http://ruuter-public:8086/internal/validate - BUILD_CORRECTED_FOLDER_HIERARCHY_URL=http://hierarchy-validation:8009/corrected-folder-hierarchy diff --git a/model-inference/inference_pipeline.py b/model-inference/inference_pipeline.py index d739288a..2bc467a8 100644 --- a/model-inference/inference_pipeline.py +++ b/model-inference/inference_pipeline.py @@ -157,6 +157,9 @@ def predict_class(self,text_input): return predicted_classes, probabilities def user_corrected_probabilities(self, text_input, user_classes): + + logger.info(f"USER CLASSES - {user_classes}") + logger.info(f"TEXT INPUT - {text_input}") inputs = self.tokenizer(text_input, truncation=True, padding=True, return_tensors='pt') inputs.to(self.device) @@ -166,9 +169,11 @@ def user_corrected_probabilities(self, text_input, user_classes): real_predicted_probabilities = [] self.base_model.to(self.device) i = 0 - data = self.hierarchy_file['classHierarchy'] + data = self.hierarchy_file parent = 1 + logger.info("ENTERING LOOP IN user_corrected_probabilities") + for i in range(len(user_classes)): current_classes = {parent: [d['class'] for d in data]} model_num = self.find_index(self.models, current_classes) @@ -199,6 +204,9 @@ def user_corrected_probabilities(self, text_input, user_classes): user_class_index = label_encoder.transform([user_classes[i]])[0] user_class_probability = probability[:, user_class_index].item() + + logger.info(f"USER CLASS PROBABILITY {user_class_probability}") + user_class_probabilities.append(int(user_class_probability * 100)) predictions = torch.argmax(outputs.logits, dim=1) diff --git a/model-inference/inference_wrapper.py b/model-inference/inference_wrapper.py index faba1c09..f8b45e0d 100644 --- a/model-inference/inference_wrapper.py +++ b/model-inference/inference_wrapper.py @@ -94,17 +94,15 @@ def stop_model(self,deployment_platform:str): self.active_outlook_model_id = None - def get_model_id(self, deployment_platform:str): - logger.info("Get Model Id Calling") - logger.info(f"Platform : {deployment_platform}") + def get_outlook_model_id(self): + logger.info("Get Outlook Model Id Calling") - - logger.info(f"Model Exists : {'Yes' if self.active_outlook_model else 'No'}") - model_id = None + logger.info(f"Outlook Model Exists : {'Yes' if self.active_outlook_model else 'No'}") + outlook_model_id = None if not self.active_outlook_model : file_location = "/shared/models/outlook/outlook_inference_metadata.json" - logger.info("RETRIEVING DATA FROM JSON FILE IN get_model_id function ") + logger.info("RETRIEVING DATA FROM JSON FILE IN get_outlook_model_id function ") if os.path.exists(file_location): with open(file_location, 'r') as json_file: data = json.load(json_file) @@ -118,29 +116,64 @@ def get_model_id(self, deployment_platform:str): self.model_swapping(model_path=model_path, best_performing_model=best_model, deployment_platform=deployment_platform,class_hierarchy=class_hierarchy, model_id=model_id) else: - return None - if(deployment_platform == "jira" and self.active_jira_model): - model_id = self.active_jira_model_id + return None + + if(self.active_outlook_model): + outlook_model_id= self.active_outlook_model_id + logger.info(f"Outlook Model Id : {outlook_model_id}") + return outlook_model_id + - if(deployment_platform == "outlook" and self.active_outlook_model): - model_id = self.active_outlook_model_id - logger.info(f"Outlook Model Id : {model_id}") + def get_jira_model_id(self): + logger.info("Get Jira Model Id Calling") + logger.info(f"Jira Model Exists : {'Yes' if self.active_jira_model else 'No'}") + jira_model_id = None + + if not self.active_jira_model : + file_location = "/shared/models/outlook/jira_inference_metadata.json" + logger.info("RETRIEVING DATA FROM JSON FILE IN get_jira_model_id function ") + if os.path.exists(file_location): + with open(file_location, 'r') as json_file: + data = json.load(json_file) + + model_path = data.get("model_path") + best_model = data.get("best_model") + deployment_platform = data.get("deployment_platform") + class_hierarchy = data.get("class_hierarchy") + model_id = data.get("model_id") + + self.model_swapping(model_path=model_path, best_performing_model=best_model, deployment_platform=deployment_platform,class_hierarchy=class_hierarchy, model_id=model_id) + + else: + return None + + if(self.active_jira_model): + jira_model_id= self.active_jira_model_id + logger.info(f"Jira Model Id : {jira_model_id}") + + return jira_model_id - return model_id - def get_corrected_probabilities(self, text, corrected_labels , deployment_platform): try: + + logger.info(f"TEXT IN get_corrected_probabilities - {text}") + logger.info(f"CORRECTED LABELS IN get_corrected_probabilities - {corrected_labels}") + logger.info(f"DEPLOYMENT PLATFORM IN get_corrected_probabilities - {deployment_platform}") + corrected_probabilities = None if(deployment_platform == "jira" and self.active_jira_model): corrected_probabilities = self.active_jira_model.user_corrected_probabilities(text_input=text, user_classes=corrected_labels) if(deployment_platform == "outlook" and self.active_outlook_model): corrected_probabilities = self.active_outlook_model.user_corrected_probabilities(text_input=text, user_classes=corrected_labels) - + + + logger.info(f"CORRECTED PROBABILITIES - {corrected_probabilities}") return corrected_probabilities except Exception as e: - raise Exception(f"Failed to retrieve corrected probabilities from the inference pipeline. Reason: {e}") \ No newline at end of file + logger.info(f"ERROR IN get_corrected_probabilities - {corrected_probabilities}") + raise RuntimeError(f"Failed to retrieve corrected probabilities from the inference pipeline. Reason: {e}") \ No newline at end of file diff --git a/model-inference/model_inference.py b/model-inference/model_inference.py index 0c71a15e..049469a6 100644 --- a/model-inference/model_inference.py +++ b/model-inference/model_inference.py @@ -2,6 +2,7 @@ import os from loguru import logger from constants import INFERENCE_LOGS_PATH +import urllib.parse logger.add(sink=INFERENCE_LOGS_PATH) @@ -81,9 +82,13 @@ def check_inference_data_exists(self, input_id): logger.info("Check Inference Data Exists Function Calling") logger.info(f"Input ID : {input_id}") try: - check_inference_data_exists_url = GET_INFERENCE_DATASET_EXIST_URL.replace("inferenceInputId",str(input_id)) + + check_inference_data_exists_url = GET_INFERENCE_DATASET_EXIST_URL logger.info(f"Check Inference URL : {check_inference_data_exists_url}") - response = requests.get(check_inference_data_exists_url) + + payload = {} + payload["inputId"] = input_id + response = requests.post(check_inference_data_exists_url,json=payload) data = response.json() logger.info(f"Response from check_inference_data_exists: {data}") @@ -98,15 +103,24 @@ def check_inference_data_exists(self, input_id): def build_corrected_folder_hierarchy(self, final_folder_id, model_id): try: build_corrected_folder_hierarchy_url = BUILD_CORRECTED_FOLDER_HIERARCHY_URL - response = requests.get(build_corrected_folder_hierarchy_url, json={"folderId": final_folder_id, "modelId": model_id}) + response = requests.post(build_corrected_folder_hierarchy_url, json={"folderId": final_folder_id, "modelId": model_id}) + + logger.info(f"build_corrected_folder_hierarchy response status code {response.status_code}") + response.raise_for_status() data = response.json() + logger.info(f"build_corrected_folder_hierarchy response status code {data}") + folder_hierarchy = data["folder_hierarchy"] + + logger.info(f"build_corrected_folder_hierarchy folder hierarchy - {data}") + return folder_hierarchy except Exception as e: - raise Exception(f"Failed to validate the class hierarchy. Reason: {e}") - + logger.info(f"EXCEPTION IN build_corrected_folder_hierarchy - {e}") + raise RuntimeError(f"Failed to validate the class hierarchy. Reason: {e}") + def find_final_folder_id(self, flattened_folder_hierarchy, model_id): try: @@ -133,15 +147,22 @@ def find_final_folder_id(self, flattened_folder_hierarchy, model_id): def update_inference(self, payload): try: + + logger.info(f"PAYLOAD IN update_inference - {payload}") + update_inference_url = UPDATE_INFERENCE_URL - response = requests.get(update_inference_url, json=payload) - response.raise_for_status() - data = response.json() + response = requests.post(update_inference_url, json=payload) - is_success = data["operationSuccessful"] + data = response.json() + + logger.info(f"DATA IN UPDATE INFERENCE - {data}") + is_success = data["response"]["operationSuccessful"] return is_success + except Exception as e: - raise Exception(f"Failed to call update inference. Reason: {e}") + + logger.info(f"Failed to call update inference. Reason: {e}") + raise RuntimeError(f"Failed to call update inference. Reason: {e}") def create_inference(self, payload): diff --git a/model-inference/model_inference_api.py b/model-inference/model_inference_api.py index 32a56dd9..458a98ee 100644 --- a/model-inference/model_inference_api.py +++ b/model-inference/model_inference_api.py @@ -39,7 +39,7 @@ @app.post("/classifier/datamodel/deployment/outlook/update") -async def download_outlook_model(request: Request, model_data:UpdateRequest, background_tasks: BackgroundTasks): +async def download_outlook_model(request: Request, model_data:UpdateRequest): save_location = f"/models/{model_data.modelId}/{model_data.modelId}.zip" @@ -114,12 +114,12 @@ async def download_outlook_model(request: Request, model_data:UpdateRequest, bac @app.post("/classifier/datamodel/deployment/jira/update") -async def download_jira_model(request: Request, modelData:UpdateRequest, backgroundTasks: BackgroundTasks): +async def download_jira_model(request: Request, model_data:UpdateRequest): - saveLocation = f"/models/{modelData.modelId}/{modelData.modelId}.zip" + save_location = f"/models/{model_data.modelId}/{model_data.modelId}.zip" try: - local_file_name = f"{modelData.modelId}.zip" + local_file_name = f"{model_data.modelId}.zip" local_file_path = f"/models/jira/{local_file_name}" # 1. Clear the current content inside the folder @@ -127,7 +127,7 @@ async def download_jira_model(request: Request, modelData:UpdateRequest, backgro clear_folder_contents(folder_path) # 2. Download the new Model - response = s3_ferry.transfer_file(local_file_path, "FS", saveLocation, "S3") + response = s3_ferry.transfer_file(local_file_path, "FS", save_location, "S3") if response.status_code != 201: raise HTTPException(status_code = 500, detail = S3_DOWNLOAD_FAILED) @@ -137,23 +137,35 @@ async def download_jira_model(request: Request, modelData:UpdateRequest, backgro # 3. Unzip Model Content unzip_file(zip_path=zip_file_path, extract_to=extract_file_path) - backgroundTasks.add_task(os.remove, zip_file_path) - + os.remove(zip_file_path) #3. Replace the content in other folder if it a replacement --> Call the delete endpoint - if(modelData.replaceDeployment): - folder_path = os.path.join("..", "shared", "models", {modelData.replaceDeploymentPlatform}) + if(model_data.replaceDeployment): + folder_path = os.path.join("..", "shared", "models", {model_data.replaceDeploymentPlatform}) clear_folder_contents(folder_path) - inference_obj.stop_model(deployment_platform=modelData.replaceDeploymentPlatform) + inference_obj.stop_model(deployment_platform=model_data.replaceDeploymentPlatform) # 4. Instantiate Inference Model - class_hierarchy = modelInference.get_class_hierarchy_by_model_id(modelData.modelId) + class_hierarchy = modelInference.get_class_hierarchy_by_model_id(model_data.modelId) if(class_hierarchy): - model_path = f"shared/models/jira/{modelData.modelId}" - best_model = modelData.bestBaseModel - model_initiate = inference_obj.model_swapping(model_path, best_model, deployment_platform="jira", class_hierarchy=class_hierarchy, model_id=modelData.modelId) + model_path = f"shared/models/jira/{model_data.modelId}" + best_model = model_data.bestBaseModel + + data = { + "model_path" : model_path, + "best_model":best_model, + "deployment_platform":"jira", + "class_hierarchy": class_hierarchy, + "model_id": model_data.modelId + } + + meta_data_save_location = '/shared/models/jira/jira_inference_metadata.json' + with open(meta_data_save_location, 'w') as json_file: + json.dump(data, json_file, indent=4) + + model_initiate = inference_obj.model_swapping(model_path, best_model, deployment_platform="jira", class_hierarchy=class_hierarchy, model_id=model_data.modelId) if(model_initiate): return JSONResponse(status_code=200, content={"replacementStatus": 200}) @@ -199,38 +211,49 @@ async def delete_folder_content(request:Request): @app.post("/classifier/deployment/outlook/inference") -async def outlook_inference(request:Request, inferenceData:OutlookInferenceRequest): +async def outlook_inference(request:Request, inference_data:OutlookInferenceRequest): try: - logger.info(f"Inference Endpoint Calling") - logger.info(f"Inference Data : {inferenceData}") + logger.info("Inference Endpoint Calling") + logger.info(f"Inference Data : {inference_data}") - model_id = inference_obj.get_model_id(deployment_platform="outlook") + model_id = inference_obj.get_outlook_model_id() logger.info(f"Model Id : {model_id}") if(model_id): # If there is a active model # 1 . Check whether the if the Inference Exists - is_exist = modelInference.check_inference_data_exists(input_id=inferenceData.inputId) + is_exist = modelInference.check_inference_data_exists(input_id=inference_data.inputId) logger.info(f"Inference Exists : {is_exist}") if(is_exist): # Update Inference Scenario # Create Corrected Folder Hierarchy using the final folder id - corrected_folder_hierarchy = modelInference.build_corrected_folder_hierarchy(final_folder_id=inferenceData.finalFolderId, model_id=model_id) + corrected_folder_hierarchy = modelInference.build_corrected_folder_hierarchy(final_folder_id=inference_data.finalFolderId, model_id=model_id) + logger.info(f"CORRECTED FOLDER HIERARCHY - {corrected_folder_hierarchy}") # Call user_corrected_probablities - corrected_probs = inference_obj.get_corrected_probabilities(text=inferenceData.inputText, corrected_labels=corrected_folder_hierarchy, deployment_platform="outlook") + corrected_probs = inference_obj.get_corrected_probabilities(text=inference_data.inputText, corrected_labels=corrected_folder_hierarchy, deployment_platform="outlook") + logger.info(f"CORRECTED PROBABILITIES IN MODEL INFERENCE API - {corrected_probs}") + if(corrected_probs): # Calculate Average Predicted Class Probability average_probability = calculate_average_predicted_class_probability(corrected_probs) + logger.info(f"AVERAGE PROBABILITY - {average_probability}") # Build request payload for inference/update endpoint - inference_update_paylod = get_inference_update_payload(inferenceInputId=inferenceData.inputId,isCorrected=True, correctedLabels=corrected_folder_hierarchy,averagePredictedClassesProbability=average_probability, platform="OUTLOOK", primaryFolderId=inferenceData.finalFolderId) - + inference_update_payload = get_inference_update_payload(inferenceInputId=inference_data.inputId,isCorrected=True, + correctedLabels=corrected_folder_hierarchy, + averagePredictedClassesProbability=average_probability, + platform="OUTLOOK", + primaryFolderId=inference_data.finalFolderId) + + logger.info(f"INFERENCE PAYLOAD - {inference_update_payload}") # Call inference/update endpoint - is_success = modelInference.update_inference(payload=inference_update_paylod) + is_success = modelInference.update_inference(payload=inference_update_payload) + logger.info(f"IS SUCCESS - {is_success}") + if(is_success): return JSONResponse(status_code=200, content={"operationSuccessful": True}) else: @@ -243,7 +266,7 @@ async def outlook_inference(request:Request, inferenceData:OutlookInferenceReque else: # Create Inference Scenario # Call Inference logger.info("CREATE INFERENCE SCENARIO OUTLOOK") - predicted_hierarchy, probabilities = inference_obj.inference(inferenceData.inputText, deployment_platform="outlook") + predicted_hierarchy, probabilities = inference_obj.inference(inference_data.inputText, deployment_platform="outlook") logger.info(f"PREDICTED HIERARCHIES AND PROBABILITIES {predicted_hierarchy}") logger.info(f"PROBABILITIES {probabilities}") @@ -260,7 +283,7 @@ async def outlook_inference(request:Request, inferenceData:OutlookInferenceReque # Build request payload for inference/create endpoint - inference_create_payload = get_inference_create_payload(inferenceInputId=inferenceData.inputId,inferenceText=inferenceData.inputText,predictedLabels=predicted_hierarchy, averagePredictedClassesProbability=average_probability, platform="OUTLOOK", primaryFolderId=final_folder_id, mailId=inferenceData.mailId) + inference_create_payload = get_inference_create_payload(inferenceInputId=inference_data.inputId,inferenceText=inference_data.inputText,predictedLabels=predicted_hierarchy, averagePredictedClassesProbability=average_probability, platform="OUTLOOK", primaryFolderId=final_folder_id, mailId=inference_data.mailId) logger.info(f"INFERENCE CREATE PAYLOAD - {inference_create_payload}") # Call inference/create endpoint is_success = modelInference.create_inference(payload=inference_create_payload) @@ -291,59 +314,65 @@ async def outlook_inference(request:Request, inferenceData:OutlookInferenceReque raise HTTPException(status_code = 500, detail=str(e)) - @app.post("/classifier/deployment/jira/inference") async def jira_inference(request:Request, inferenceData:JiraInferenceRequest): - try: - # 1 . Check whether the if the Inference Exists - is_exist = modelInference.check_inference_data_exists(input_id=inferenceData.inputId) + try: - if(is_exist): # Update Inference Scenario - # Call user_corrected_probablities - corrected_probs = inference_obj.get_corrected_probabilities(text=inferenceData.inputText, corrected_labels=inferenceData.finalLabels, deployment_platform="outlook") - - if(corrected_probs): - # Calculate Average Predicted Class Probability - average_probability = calculate_average_predicted_class_probability(corrected_probs) + model_id = inference_obj.get_jira_model_id() + + if(model_id): + # 1 . Check whether the if the Inference Exists + is_exist = modelInference.check_inference_data_exists(input_id=inferenceData.inputId) - # Build request payload for inference/update endpoint - inference_update_paylod = get_inference_update_payload(inferenceInputId=inferenceData.inputId,isCorrected=True, correctedLabels=inferenceData.finalLabels, averagePredictedClassesProbability=average_probability, platform="JIRA", primaryFolderId=None) - - # Call inference/update endpoint - is_success = modelInference.update_inference(payload=inference_update_paylod) + if(is_exist): # Update Inference Scenario + # Call user_corrected_probablities + corrected_probs = inference_obj.get_corrected_probabilities(text=inferenceData.inputText, corrected_labels=inferenceData.finalLabels, deployment_platform="outlook") - if(is_success): - return JSONResponse(status_code=200, content={"operationSuccessful": True}) - else: - raise HTTPException(status_code = 500, detail="Failed to call the update inference") - - else: - raise HTTPException(status_code = 500, detail="Failed to retrieve the corrected class probabilities from the inference pipeline") - + if(corrected_probs): + # Calculate Average Predicted Class Probability + average_probability = calculate_average_predicted_class_probability(corrected_probs) - else: # Create Inference Scenario - # Call Inference - predicted_hierarchy, probabilities = inference_obj.inference(inferenceData.inputText, deployment_platform="jira") - - if (probabilities and predicted_hierarchy): + # Build request payload for inference/update endpoint + inference_update_paylod = get_inference_update_payload(inferenceInputId=inferenceData.inputId,isCorrected=True, correctedLabels=inferenceData.finalLabels, averagePredictedClassesProbability=average_probability, platform="JIRA", primaryFolderId=None) + + # Call inference/update endpoint + is_success = modelInference.update_inference(payload=inference_update_paylod) + + if(is_success): + return JSONResponse(status_code=200, content={"operationSuccessful": True}) + else: + raise HTTPException(status_code = 500, detail="Failed to call the update inference") - # Calculate Average Predicted Class Probability - average_probability = calculate_average_predicted_class_probability(probabilities) + else: + raise HTTPException(status_code = 500, detail="Failed to retrieve the corrected class probabilities from the inference pipeline") - # Build request payload for inference/create endpoint - inference_create_payload = get_inference_create_payload(inferenceInputId=inferenceData.inputId,inferenceText=inferenceData.inputText,predictedLabels=predicted_hierarchy, averagePredictedClassesProbability=average_probability, platform="JIRA", primaryFolderId=None, mailId=None) + + else: # Create Inference Scenario + # Call Inference + predicted_hierarchy, probabilities = inference_obj.inference(inferenceData.inputText, deployment_platform="jira") - # Call inference/create endpoint - is_success = modelInference.create_inference(payload=inference_create_payload) + if (probabilities and predicted_hierarchy): + + # Calculate Average Predicted Class Probability + average_probability = calculate_average_predicted_class_probability(probabilities) + + # Build request payload for inference/create endpoint + inference_create_payload = get_inference_create_payload(inferenceInputId=inferenceData.inputId,inferenceText=inferenceData.inputText,predictedLabels=predicted_hierarchy, averagePredictedClassesProbability=average_probability, platform="JIRA", primaryFolderId=None, mailId=None) + + # Call inference/create endpoint + is_success = modelInference.create_inference(payload=inference_create_payload) + + if(is_success): + return JSONResponse(status_code=200, content={"operationSuccessful": True}) + else: + raise HTTPException(status_code = 500, detail="Failed to call the create inference") - if(is_success): - return JSONResponse(status_code=200, content={"operationSuccessful": True}) else: - raise HTTPException(status_code = 500, detail="Failed to call the create inference") - - else: - raise HTTPException(status_code = 500, detail="Failed to call inference") - - + raise HTTPException(status_code = 500, detail="Failed to call inference") + + else: + raise HTTPException(status_code = 500, detail="No active model currently exists for the Jira inference") + except Exception as e: - raise HTTPException(status_code = 500, detail=str(e)) \ No newline at end of file + raise HTTPException(status_code = 500, detail=str(e)) + diff --git a/model-inference/utils.py b/model-inference/utils.py index 241b6019..3de0b7f3 100644 --- a/model-inference/utils.py +++ b/model-inference/utils.py @@ -46,8 +46,8 @@ def get_inference_update_payload(inferenceInputId:str, isCorrected:bool, correct INFERENCE_UPDATE_PAYLOAD = { "inferenceId": inferenceInputId, "isCorrected": isCorrected, - "predictedLabels": correctedLabels, - "averagePredictedClassesProbability": averagePredictedClassesProbability, + "correctedLabels": correctedLabels, + "averageCorrectedClassesProbability": averagePredictedClassesProbability, "primaryFolderId": primaryFolderId, "platform": platform } From dd29721b9da794ffeb27022b20df15407bb2b86d Mon Sep 17 00:00:00 2001 From: Thirunayan22 Date: Sat, 24 Aug 2024 18:06:51 +0530 Subject: [PATCH 530/582] pushing outlook fixes --- DSL/Resql/get-input-metadata-exits-by-id.sql | 4 +- DSL/Resql/update-input-metadata.sql | 2 +- .../DSL/POST/internal/corrected.yml | 2 +- docker-compose.yml | 2 +- model-inference/inference_pipeline.py | 139 ++++++++++-------- model-inference/inference_wrapper.py | 8 +- model-inference/model_inference.py | 19 ++- model-inference/model_inference_api.py | 77 +++++++--- model-inference/utils.py | 36 ++--- 9 files changed, 175 insertions(+), 114 deletions(-) diff --git a/DSL/Resql/get-input-metadata-exits-by-id.sql b/DSL/Resql/get-input-metadata-exits-by-id.sql index 2996f006..8de7e2e9 100644 --- a/DSL/Resql/get-input-metadata-exits-by-id.sql +++ b/DSL/Resql/get-input-metadata-exits-by-id.sql @@ -1,2 +1,2 @@ -SELECT input_id -FROM "input" WHERE input_id =:id; \ No newline at end of file +SELECT id +FROM "input" WHERE id =:id; \ No newline at end of file diff --git a/DSL/Resql/update-input-metadata.sql b/DSL/Resql/update-input-metadata.sql index bd4f1ef3..d1d1f695 100644 --- a/DSL/Resql/update-input-metadata.sql +++ b/DSL/Resql/update-input-metadata.sql @@ -4,4 +4,4 @@ SET corrected_labels = :corrected_labels::JSONB, average_corrected_classes_probability = :average_corrected_classes_probability, primary_folder_id = :primary_folder_id -WHERE input_id = :id; +WHERE id = :id; diff --git a/DSL/Ruuter.public/DSL/POST/internal/corrected.yml b/DSL/Ruuter.public/DSL/POST/internal/corrected.yml index dd8bb444..f5c89903 100644 --- a/DSL/Ruuter.public/DSL/POST/internal/corrected.yml +++ b/DSL/Ruuter.public/DSL/POST/internal/corrected.yml @@ -9,7 +9,7 @@ declaration: allowlist: body: - field: inferenceId - type: string + type: number description: "Body field 'inferenceId'" - field: isCorrected type: boolean diff --git a/docker-compose.yml b/docker-compose.yml index 0f56221c..d139dffd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -434,7 +434,7 @@ services: ports: - "3008:3008" environment: - JIRA_WEBHOOK_SECRET: value + JIRA_WEBHOOK_SECRET: FM8P4YU8lY5uWMDCZukG RUUTER_PUBLIC_JIRA_URL: http://ruuter-public:8086/internal/jira/accept networks: bykstack: diff --git a/model-inference/inference_pipeline.py b/model-inference/inference_pipeline.py index 2bc467a8..394505bf 100644 --- a/model-inference/inference_pipeline.py +++ b/model-inference/inference_pipeline.py @@ -60,6 +60,10 @@ def __init__(self, hierarchy_file, model_name, results_folder): self.classification_models_dict[i] = torch.load(os.path.join(f"{results_folder}/{CLASSIFIER_LAYERS_FOLDER}",classification_model_names[i])) def find_index(self, data, search_dict): + + logger.info(f"DATA - {data}") + logger.info(f"SEARCH DICT - {search_dict}") + for index, d in enumerate(data): if d == search_dict: return index @@ -91,6 +95,9 @@ def predict_class(self,text_input): while data: current_classes = {parent: [d['class'] for d in data]} + + logger.info(f"CURRENT CLASSES - {current_classes}") + model_num = self.find_index(self.models, current_classes) if model_num is None: break @@ -157,83 +164,95 @@ def predict_class(self,text_input): return predicted_classes, probabilities def user_corrected_probabilities(self, text_input, user_classes): - - logger.info(f"USER CLASSES - {user_classes}") - logger.info(f"TEXT INPUT - {text_input}") + try: + logger.info(f"USER CLASSES - {user_classes}") + logger.info(f"TEXT INPUT - {text_input}") + + inputs = self.tokenizer(text_input, truncation=True, padding=True, return_tensors='pt') + inputs.to(self.device) + inputs = {key: val.to(self.device) for key, val in inputs.items()} + predicted_classes = [] + user_class_probabilities = [] + real_predicted_probabilities = [] + self.base_model.to(self.device) + i = 0 + data = self.hierarchy_file + parent = 1 - inputs = self.tokenizer(text_input, truncation=True, padding=True, return_tensors='pt') - inputs.to(self.device) - inputs = {key: val.to(self.device) for key, val in inputs.items()} - predicted_classes = [] - user_class_probabilities = [] - real_predicted_probabilities = [] - self.base_model.to(self.device) - i = 0 - data = self.hierarchy_file - parent = 1 + #TODO - CREATE A FUNCTION HERE THAT LOOPS THROUGH THE DATA (HIERARCHY FILE) TO FIND OUT WHETHER USERCLASSES EXIST IN THE HIERARCHY + logger.info("ENTERING LOOP IN user_corrected_probabilities") - logger.info("ENTERING LOOP IN user_corrected_probabilities") + for i in range(len(user_classes)): + current_classes = {parent: [d['class'] for d in data]} + + logger.info(f"CURRENT CLASSES - {current_classes}") - for i in range(len(user_classes)): - current_classes = {parent: [d['class'] for d in data]} - model_num = self.find_index(self.models, current_classes) - if model_num is None: - break - label_encoder = self.label_encoder_dict[model_num] - num_labels = len(label_encoder.classes_) + model_num = self.find_index(self.models, current_classes) - if self.model_name == DISTIL_BERT: - self.base_model.classifier = DistilBertForSequenceClassification.from_pretrained('distilbert-base-uncased', num_labels=num_labels).classifier - self.base_model.distilbert.transformer.layer[-2:].load_state_dict(self.models_dict[model_num]) + logger.info(f"MODEL NUM - {model_num}") + if model_num is None: - elif self.model_name == ROBERTA: - self.base_model.classifier = XLMRobertaForSequenceClassification.from_pretrained('xlm-roberta-base', num_labels=num_labels).classifier - self.base_model.roberta.encoder.layer[-2:].load_state_dict(self.models_dict[model_num]) + logger.info(f"MODEL DOES NOT EXIST FOR CLASS - {current_classes}") + break + label_encoder = self.label_encoder_dict[model_num] + num_labels = len(label_encoder.classes_) - elif self.model_name == BERT: - self.base_model.classifier = BertForSequenceClassification.from_pretrained('bert-base-uncased', num_labels=num_labels).classifier - self.base_model.base_model.encoder.layer[-2:].load_state_dict(self.models_dict[model_num]) + if self.model_name == DISTIL_BERT: + self.base_model.classifier = DistilBertForSequenceClassification.from_pretrained('distilbert-base-uncased', num_labels=num_labels).classifier + self.base_model.distilbert.transformer.layer[-2:].load_state_dict(self.models_dict[model_num]) - self.base_model.classifier.load_state_dict(self.classification_models_dict[model_num]) - self.base_model.to(self.device) + elif self.model_name == ROBERTA: + self.base_model.classifier = XLMRobertaForSequenceClassification.from_pretrained('xlm-roberta-base', num_labels=num_labels).classifier + self.base_model.roberta.encoder.layer[-2:].load_state_dict(self.models_dict[model_num]) - with torch.no_grad(): - outputs = self.base_model(**inputs) - probability = F.softmax(outputs.logits, dim=1) + elif self.model_name == BERT: + self.base_model.classifier = BertForSequenceClassification.from_pretrained('bert-base-uncased', num_labels=num_labels).classifier + self.base_model.base_model.encoder.layer[-2:].load_state_dict(self.models_dict[model_num]) - user_class_index = label_encoder.transform([user_classes[i]])[0] + self.base_model.classifier.load_state_dict(self.classification_models_dict[model_num]) + self.base_model.to(self.device) - user_class_probability = probability[:, user_class_index].item() + with torch.no_grad(): + outputs = self.base_model(**inputs) + probability = F.softmax(outputs.logits, dim=1) - logger.info(f"USER CLASS PROBABILITY {user_class_probability}") + user_class_index = label_encoder.transform([user_classes[i]])[0] - user_class_probabilities.append(int(user_class_probability * 100)) + user_class_probability = probability[:, user_class_index].item() - predictions = torch.argmax(outputs.logits, dim=1) - real_predicted_probabilities.append(int(probability.gather(1, predictions.unsqueeze(1)).squeeze().cpu().item() * 100)) + logger.info(f"USER CLASS PROBABILITY {user_class_probability}") - predicted_label = label_encoder.inverse_transform(predictions.cpu().numpy()) - predicted_classes.append(predicted_label[0]) + user_class_probabilities.append(int(user_class_probability * 100)) + + predictions = torch.argmax(outputs.logits, dim=1) + real_predicted_probabilities.append(int(probability.gather(1, predictions.unsqueeze(1)).squeeze().cpu().item() * 100)) - data = next((item for item in data if item['class'] == user_classes[i]), None) - parent = user_classes[i] - if not data: - break - - while data['subclasses'] and len(data['subclasses']) <= 1: - if data['subclasses']: - parent = data['subclasses'][0]['class'] - data = data['subclasses'][0] - else: - data = None + predicted_label = label_encoder.inverse_transform(predictions.cpu().numpy()) + predicted_classes.append(predicted_label[0]) + + data = next((item for item in data if item['class'] == user_classes[i]), None) + parent = user_classes[i] + if not data: break - if not data['subclasses']: - break + while data['subclasses'] and len(data['subclasses']) <= 1: + if data['subclasses']: + parent = data['subclasses'][0]['class'] + data = data['subclasses'][0] + else: + data = None + break + + if not data['subclasses']: + break - if not data: - break + if not data: + break - data = data['subclasses'] + data = data['subclasses'] - return user_class_probabilities + return user_class_probabilities + + except Exception as e: + logger.info(f"ERROR in user_corrected_probabilities - {e}") + raise RuntimeError(f"ERROR in user_corrected_probabilities - {e}") diff --git a/model-inference/inference_wrapper.py b/model-inference/inference_wrapper.py index f8b45e0d..22b190ab 100644 --- a/model-inference/inference_wrapper.py +++ b/model-inference/inference_wrapper.py @@ -16,7 +16,7 @@ def __init__(self) -> None: self.active_jira_model_id = None self.active_outlook_model_id = None - def model_swapping(self, model_path:str, best_performing_model:str, deployment_platform:str, class_hierarchy:list, model_id:int): + def load_model(self, model_path:str, best_performing_model:str, deployment_platform:str, class_hierarchy:list, model_id:int): try: logger.info("LOGGING INSIDE model_swapping") @@ -113,7 +113,7 @@ def get_outlook_model_id(self): class_hierarchy = data.get("class_hierarchy") model_id = data.get("model_id") - self.model_swapping(model_path=model_path, best_performing_model=best_model, deployment_platform=deployment_platform,class_hierarchy=class_hierarchy, model_id=model_id) + self.load_model(model_path=model_path, best_performing_model=best_model, deployment_platform=deployment_platform,class_hierarchy=class_hierarchy, model_id=model_id) else: return None @@ -144,7 +144,7 @@ def get_jira_model_id(self): class_hierarchy = data.get("class_hierarchy") model_id = data.get("model_id") - self.model_swapping(model_path=model_path, best_performing_model=best_model, deployment_platform=deployment_platform,class_hierarchy=class_hierarchy, model_id=model_id) + self.load_model(model_path=model_path, best_performing_model=best_model, deployment_platform=deployment_platform,class_hierarchy=class_hierarchy, model_id=model_id) else: return None @@ -176,4 +176,4 @@ def get_corrected_probabilities(self, text, corrected_labels , deployment_platfo except Exception as e: logger.info(f"ERROR IN get_corrected_probabilities - {corrected_probabilities}") - raise RuntimeError(f"Failed to retrieve corrected probabilities from the inference pipeline. Reason: {e}") \ No newline at end of file + raise RuntimeError(f"Failed to retrieve corrected probabilities from the inference pipeline. Reason: {e}") diff --git a/model-inference/model_inference.py b/model-inference/model_inference.py index 049469a6..6ded632d 100644 --- a/model-inference/model_inference.py +++ b/model-inference/model_inference.py @@ -21,8 +21,10 @@ def __init__(self): def get_class_hierarchy_by_model_id(self, model_id): try: + logger.info(f"get_class_hierarchy_by_model_id - {model_id}") outlook_access_token_url = OUTLOOK_ACCESS_TOKEN_API_URL + logger.info(f"OUTLOOK ACCESS TOKEN URL - {outlook_access_token_url}") response = requests.post(outlook_access_token_url, json={"modelId": model_id}) data = response.json() @@ -36,7 +38,6 @@ def get_class_hierarchy_by_model_id(self, model_id): logger.error(f"Failed to retrieve the class hierarchy Reason: {e}") raise RuntimeError(f"Failed to retrieve the class hierarchy Reason: {e}") - def validate_class_hierarchy(self, class_hierarchy, model_id): @@ -82,7 +83,8 @@ def check_inference_data_exists(self, input_id): logger.info("Check Inference Data Exists Function Calling") logger.info(f"Input ID : {input_id}") try: - + is_exist = None + inference_id = None check_inference_data_exists_url = GET_INFERENCE_DATASET_EXIST_URL logger.info(f"Check Inference URL : {check_inference_data_exists_url}") @@ -94,11 +96,16 @@ def check_inference_data_exists(self, input_id): logger.info(f"Response from check_inference_data_exists: {data}") is_exist = data["response"]["exist"] - return is_exist - except Exception as e: - logger.info(f"Failed to validate the class hierarchy. Reason: {e}") - raise Exception(f"Failed to validate the class hierarchy. Reason: {e}") + + if (len(data["response"]["data"]) > 0): + + inference_id=data["response"]["data"][0]["inferenceId"] + return is_exist, inference_id + except Exception as e: + logger.info(f"check_inference_data_exists failed. Reason: {e}") + raise RuntimeError(f"check_inference_data_exists failed. Reason: {e}") + def build_corrected_folder_hierarchy(self, final_folder_id, model_id): try: diff --git a/model-inference/model_inference_api.py b/model-inference/model_inference_api.py index 458a98ee..4a9f0a3e 100644 --- a/model-inference/model_inference_api.py +++ b/model-inference/model_inference_api.py @@ -96,7 +96,7 @@ async def download_outlook_model(request: Request, model_data:UpdateRequest): json.dump(data, json_file, indent=4) - model_initiate = inference_obj.model_swapping(model_path, best_model, deployment_platform="outlook", class_hierarchy=class_hierarchy, model_id=model_data.modelId) + model_initiate = inference_obj.load_model(model_path, best_model, deployment_platform="outlook", class_hierarchy=class_hierarchy, model_id=model_data.modelId) logger.info(f"MODEL INITIATE - {model_initiate}") @@ -122,9 +122,10 @@ async def download_jira_model(request: Request, model_data:UpdateRequest): local_file_name = f"{model_data.modelId}.zip" local_file_path = f"/models/jira/{local_file_name}" + logger.info(f"MODEL DATA - {model_data}") # 1. Clear the current content inside the folder folder_path = os.path.join("..", "shared", "models", "jira") - clear_folder_contents(folder_path) + clear_folder_contents(folder_path) # 2. Download the new Model response = s3_ferry.transfer_file(local_file_path, "FS", save_location, "S3") @@ -137,20 +138,31 @@ async def download_jira_model(request: Request, model_data:UpdateRequest): # 3. Unzip Model Content unzip_file(zip_path=zip_file_path, extract_to=extract_file_path) - os.remove(zip_file_path) + os.remove(zip_file_path) #3. Replace the content in other folder if it a replacement --> Call the delete endpoint + logger.info("JUST ABOUT TO ENTER - if(model_data.replaceDeployment):") + if(model_data.replaceDeployment): + + logger.info("INSIDE REPLACE DEPLOYMENT") + folder_path = os.path.join("..", "shared", "models", {model_data.replaceDeploymentPlatform}) clear_folder_contents(folder_path) inference_obj.stop_model(deployment_platform=model_data.replaceDeploymentPlatform) # 4. Instantiate Inference Model + + logger.info(f"JUST ABOUT TO ENTER get_class_hierarchy_by_model_id") + class_hierarchy = modelInference.get_class_hierarchy_by_model_id(model_data.modelId) + + logger.info(f"JIRA UPDATE CLASS HIERARCHY - {class_hierarchy}") + if(class_hierarchy): - - model_path = f"shared/models/jira/{model_data.modelId}" + + model_path = "/shared/models/jira" best_model = model_data.bestBaseModel data = { @@ -165,9 +177,11 @@ async def download_jira_model(request: Request, model_data:UpdateRequest): with open(meta_data_save_location, 'w') as json_file: json.dump(data, json_file, indent=4) - model_initiate = inference_obj.model_swapping(model_path, best_model, deployment_platform="jira", class_hierarchy=class_hierarchy, model_id=model_data.modelId) + model_initiate = inference_obj.load_model(model_path, best_model, deployment_platform="jira", class_hierarchy=class_hierarchy, model_id=model_data.modelId) if(model_initiate): + logger.info(f"MODEL INITIATE - {model_initiate}") + logger.info("JIRA DEPLOYMENT SUCCESSFUL") return JSONResponse(status_code=200, content={"replacementStatus": 200}) else: raise HTTPException(status_code = 500, detail = "Failed to initiate inference object") @@ -222,17 +236,21 @@ async def outlook_inference(request:Request, inference_data:OutlookInferenceRequ # If there is a active model # 1 . Check whether the if the Inference Exists - is_exist = modelInference.check_inference_data_exists(input_id=inference_data.inputId) + is_exist, inference_id = modelInference.check_inference_data_exists(input_id=inference_data.inputId) logger.info(f"Inference Exists : {is_exist}") + logger.info(f"Inference ID - {inference_id}") if(is_exist): # Update Inference Scenario # Create Corrected Folder Hierarchy using the final folder id - corrected_folder_hierarchy = modelInference.build_corrected_folder_hierarchy(final_folder_id=inference_data.finalFolderId, model_id=model_id) + corrected_folder_hierarchy = modelInference.build_corrected_folder_hierarchy(final_folder_id=inference_data.finalFolderId, + model_id=model_id) logger.info(f"CORRECTED FOLDER HIERARCHY - {corrected_folder_hierarchy}") # Call user_corrected_probablities - corrected_probs = inference_obj.get_corrected_probabilities(text=inference_data.inputText, corrected_labels=corrected_folder_hierarchy, deployment_platform="outlook") + corrected_probs = inference_obj.get_corrected_probabilities(text=inference_data.inputText, + corrected_labels=corrected_folder_hierarchy, + deployment_platform="outlook") logger.info(f"CORRECTED PROBABILITIES IN MODEL INFERENCE API - {corrected_probs}") @@ -242,11 +260,11 @@ async def outlook_inference(request:Request, inference_data:OutlookInferenceRequ logger.info(f"AVERAGE PROBABILITY - {average_probability}") # Build request payload for inference/update endpoint - inference_update_payload = get_inference_update_payload(inferenceInputId=inference_data.inputId,isCorrected=True, - correctedLabels=corrected_folder_hierarchy, - averagePredictedClassesProbability=average_probability, + inference_update_payload = get_inference_update_payload(inference_id=inference_id,is_corrected=True, + corrected_labels=corrected_folder_hierarchy, + average_predicted_classes_probability=average_probability, platform="OUTLOOK", - primaryFolderId=inference_data.finalFolderId) + primary_folder_id=inference_data.finalFolderId) logger.info(f"INFERENCE PAYLOAD - {inference_update_payload}") # Call inference/update endpoint @@ -283,7 +301,7 @@ async def outlook_inference(request:Request, inference_data:OutlookInferenceRequ # Build request payload for inference/create endpoint - inference_create_payload = get_inference_create_payload(inferenceInputId=inference_data.inputId,inferenceText=inference_data.inputText,predictedLabels=predicted_hierarchy, averagePredictedClassesProbability=average_probability, platform="OUTLOOK", primaryFolderId=final_folder_id, mailId=inference_data.mailId) + inference_create_payload = get_inference_create_payload(inference_input_id=inference_data.inputId,inference_text=inference_data.inputText,predicted_labels=predicted_hierarchy, average_predicted_classes_probability=average_probability, platform="OUTLOOK", primary_folder_id=final_folder_id, mail_id=inference_data.mailId) logger.info(f"INFERENCE CREATE PAYLOAD - {inference_create_payload}") # Call inference/create endpoint is_success = modelInference.create_inference(payload=inference_create_payload) @@ -317,28 +335,40 @@ async def outlook_inference(request:Request, inference_data:OutlookInferenceRequ @app.post("/classifier/deployment/jira/inference") async def jira_inference(request:Request, inferenceData:JiraInferenceRequest): try: - + + + logger.info(f"INFERENCE DATA IN JIRA INFERENCE - {inferenceData}") + model_id = inference_obj.get_jira_model_id() + if(model_id): # 1 . Check whether the if the Inference Exists - is_exist = modelInference.check_inference_data_exists(input_id=inferenceData.inputId) + is_exist, inference_id = modelInference.check_inference_data_exists(input_id=inferenceData.inputId) - if(is_exist): # Update Inference Scenario + logger.info(f"LOGGING IS EXIST IN JIRA IN JIRA UPDATE INFERENCE - {is_exist}") + if(is_exist): # Update Inference Scenario # Call user_corrected_probablities corrected_probs = inference_obj.get_corrected_probabilities(text=inferenceData.inputText, corrected_labels=inferenceData.finalLabels, deployment_platform="outlook") + logger.info(f"CORRECT PROBS IN JIRA UPDATE INFERENCE - {corrected_probs}") if(corrected_probs): # Calculate Average Predicted Class Probability average_probability = calculate_average_predicted_class_probability(corrected_probs) # Build request payload for inference/update endpoint - inference_update_paylod = get_inference_update_payload(inferenceInputId=inferenceData.inputId,isCorrected=True, correctedLabels=inferenceData.finalLabels, averagePredictedClassesProbability=average_probability, platform="JIRA", primaryFolderId=None) - + inference_update_payload = get_inference_update_payload(inference_id=inference_id,is_corrected=True, corrected_labels=inferenceData.finalLabels, average_predicted_classes_probability=average_probability, platform="JIRA", primary_folder_id=None) + + logger.info(f"INFERENCE UPDATE PAYLOAD - {inference_update_payload}") # Call inference/update endpoint - is_success = modelInference.update_inference(payload=inference_update_paylod) + is_success = modelInference.update_inference(payload=inference_update_payload) + + logger.info(f"IS SUCCESS IN JIRA UPDATE INFERENCE - {is_success} ") + if(is_success): + + logger.info("JIRA UPDATE INFERENCE SUCCESSFUL") return JSONResponse(status_code=200, content={"operationSuccessful": True}) else: raise HTTPException(status_code = 500, detail="Failed to call the update inference") @@ -351,18 +381,23 @@ async def jira_inference(request:Request, inferenceData:JiraInferenceRequest): # Call Inference predicted_hierarchy, probabilities = inference_obj.inference(inferenceData.inputText, deployment_platform="jira") + logger.info(f"JIRA PREDICTED HIERARCHY - {predicted_hierarchy}") + logger.info(f"JIRA PROBABILITIES - {probabilities}") + if (probabilities and predicted_hierarchy): # Calculate Average Predicted Class Probability average_probability = calculate_average_predicted_class_probability(probabilities) # Build request payload for inference/create endpoint - inference_create_payload = get_inference_create_payload(inferenceInputId=inferenceData.inputId,inferenceText=inferenceData.inputText,predictedLabels=predicted_hierarchy, averagePredictedClassesProbability=average_probability, platform="JIRA", primaryFolderId=None, mailId=None) + inference_create_payload = get_inference_create_payload(inference_input_id=inferenceData.inputId,inference_text=inferenceData.inputText,predicted_labels=predicted_hierarchy, average_predicted_classes_probability=average_probability, platform="JIRA", primary_folder_id=None, mail_id=None) # Call inference/create endpoint is_success = modelInference.create_inference(payload=inference_create_payload) + logger.info(f"JIRA inference is_success - {is_success}") if(is_success): + logger.info("JIRA CREATE INFERENCE SUCCESSFUL") return JSONResponse(status_code=200, content={"operationSuccessful": True}) else: raise HTTPException(status_code = 500, detail="Failed to call the create inference") diff --git a/model-inference/utils.py b/model-inference/utils.py index 3de0b7f3..379eb3fa 100644 --- a/model-inference/utils.py +++ b/model-inference/utils.py @@ -19,36 +19,36 @@ def clear_folder_contents(folder_path: str): except Exception as e: raise Exception(f"Failed to delete contents in {folder_path}. Reason: {e}") -def get_s3_payload(destinationFilePath:str, destinationStorageType:str, sourceFilePath:str, sourceStorageType:str): +def get_s3_payload(destination_file_path:str, destination_storage_type:str, source_file_path:str, source_storage_type:str): S3_FERRY_PAYLOAD = { - "destinationFilePath": destinationFilePath, - "destinationStorageType": destinationStorageType, - "sourceFilePath": sourceFilePath, - "sourceStorageType": sourceStorageType + "destinationFilePath": destination_file_path, + "destinationStorageType": destination_storage_type, + "sourceFilePath": source_file_path, + "sourceStorageType": source_storage_type } return S3_FERRY_PAYLOAD -def get_inference_create_payload(inferenceInputId:str, inferenceText:str, predictedLabels:list, averagePredictedClassesProbability:int, platform:str, primaryFolderId: Optional[str] = None, mailId : Optional[str] = None ): +def get_inference_create_payload(inference_input_id:str, inference_text:str, predicted_labels:list, average_predicted_classes_probability:int, platform:str, primary_folder_id: Optional[str] = None, mail_id : Optional[str] = None ): INFERENCE_CREATE_PAYLOAD = { - "inputId": inferenceInputId, - "inferenceText": inferenceText, - "predictedLabels": predictedLabels, - "averagePredictedClassesProbability": averagePredictedClassesProbability, + "inputId": inference_input_id, + "inferenceText": inference_text, + "predictedLabels": predicted_labels, + "averagePredictedClassesProbability": average_predicted_classes_probability, "platform": platform, - "primaryFolderId": primaryFolderId, - "mailId":mailId + "primaryFolderId": primary_folder_id, + "mailId":mail_id } return INFERENCE_CREATE_PAYLOAD -def get_inference_update_payload(inferenceInputId:str, isCorrected:bool, correctedLabels:list, averagePredictedClassesProbability:int, platform:str, primaryFolderId: Optional[str] = None ): +def get_inference_update_payload(inference_id:str, is_corrected:bool, corrected_labels:list, average_predicted_classes_probability:int, platform:str, primary_folder_id: Optional[str] = None ): INFERENCE_UPDATE_PAYLOAD = { - "inferenceId": inferenceInputId, - "isCorrected": isCorrected, - "correctedLabels": correctedLabels, - "averageCorrectedClassesProbability": averagePredictedClassesProbability, - "primaryFolderId": primaryFolderId, + "inferenceId": inference_id, + "isCorrected": is_corrected, + "correctedLabels": corrected_labels, + "averageCorrectedClassesProbability": average_predicted_classes_probability, + "primaryFolderId": primary_folder_id, "platform": platform } From 304cbc7f8023a97999453250efd0c5fb89710d07 Mon Sep 17 00:00:00 2001 From: Thirunayan22 Date: Sun, 25 Aug 2024 17:05:10 +0530 Subject: [PATCH 531/582] fixed issues in model replacement --- .../DSL/POST/classifier/datamodel/update.yml | 6 +- docker-compose.yml | 1 + model-inference/constants.py | 13 ++ model-inference/inference_wrapper.py | 2 +- model-inference/model_inference.py | 63 +++++++++- model-inference/model_inference_api.py | 117 ++++++++++++------ model_trainer/model_trainer.py | 3 + 7 files changed, 164 insertions(+), 41 deletions(-) diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update.yml b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update.yml index 1dcc50c5..8f408657 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update.yml @@ -314,9 +314,9 @@ execute_cron_manager: args: url: "[#CLASSIFIER_CRON_MANAGER]/execute/data_model/model_trainer" query: - cookie: ${incoming.headers.cookie} - model_id: ${model_id} - new_model_id: ${new_model_id} + cookie: ${incoming.headers.cookie.replace('customJwtCookie=','')} #Removing the customJwtCookie phrase from payload to to send cookie token only + modelId: ${model_id} + newModelId: ${new_model_id} updateType: ${update_type} result: res next: assign_success_response diff --git a/docker-compose.yml b/docker-compose.yml index d139dffd..c0fbbb44 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -293,6 +293,7 @@ services: - BUILD_CORRECTED_FOLDER_HIERARCHY_URL=http://hierarchy-validation:8009/corrected-folder-hierarchy - FIND_FINAL_FOLDER_ID_URL=http://hierarchy-validation:8009/find-folder-id - UPDATE_DATAMODEL_PROGRESS_URL=http://ruuter-private:8088/classifier/datamodel/progress/update + - UPDATE_MODEL_TRAINING_STATUS_ENDPOINT=http://ruuter-private:8088/classifier/datamodel/update/training/status ports: - "8003:8003" networks: diff --git a/model-inference/constants.py b/model-inference/constants.py index aa845196..d18d0888 100644 --- a/model-inference/constants.py +++ b/model-inference/constants.py @@ -18,6 +18,19 @@ BERT = "bert" +OUTLOOK_MODELS_FOLDER_PATH = "/shared/models/outlook" + +JIRA_MODELS_FOLDER_PATH = "/shared/models/jira" + +SHARED_MODELS_ROOT_FOLDER = "/shared/models" + +MODEL_TRAINED_AND_DEPLOYED_PROGRESS_PERCENTAGE=100 + +MODEL_TRAINED_AND_DEPLOYED_PROGRESS_MESSAGE = "The model was trained and deployed successfully to the {deployment_environment} environment" + +MODEL_TRAINED_AND_DEPLOYED_PROGRESS_STATUS = "Model Trained And Deployed" + + S3_DOWNLOAD_FAILED = { "upload_status": 500, diff --git a/model-inference/inference_wrapper.py b/model-inference/inference_wrapper.py index 22b190ab..19dcdc5b 100644 --- a/model-inference/inference_wrapper.py +++ b/model-inference/inference_wrapper.py @@ -132,7 +132,7 @@ def get_jira_model_id(self): jira_model_id = None if not self.active_jira_model : - file_location = "/shared/models/outlook/jira_inference_metadata.json" + file_location = "/shared/models/jira/jira_inference_metadata.json" logger.info("RETRIEVING DATA FROM JSON FILE IN get_jira_model_id function ") if os.path.exists(file_location): with open(file_location, 'r') as json_file: diff --git a/model-inference/model_inference.py b/model-inference/model_inference.py index 6ded632d..61c5f8b1 100644 --- a/model-inference/model_inference.py +++ b/model-inference/model_inference.py @@ -1,8 +1,11 @@ import requests import os from loguru import logger -from constants import INFERENCE_LOGS_PATH +from constants import INFERENCE_LOGS_PATH, MODEL_TRAINED_AND_DEPLOYED_PROGRESS_PERCENTAGE, \ +MODEL_TRAINED_AND_DEPLOYED_PROGRESS_MESSAGE, MODEL_TRAINED_AND_DEPLOYED_PROGRESS_STATUS import urllib.parse +from fastapi import FastAPI,HTTPException, Request, BackgroundTasks + logger.add(sink=INFERENCE_LOGS_PATH) @@ -13,6 +16,9 @@ OUTLOOK_ACCESS_TOKEN_API_URL=os.getenv("OUTLOOK_ACCESS_TOKEN_API_URL") BUILD_CORRECTED_FOLDER_HIERARCHY_URL = os.getenv("BUILD_CORRECTED_FOLDER_HIERARCHY_URL") FIND_FINAL_FOLDER_ID_URL = os.getenv("FIND_FINAL_FOLDER_ID_URL") +UPDATE_DATAMODEL_PROGRESS_URL = os.getenv("UPDATE_DATAMODEL_PROGRESS_URL") +UPDATE_MODEL_TRAINING_STATUS_ENDPOINT = os.getenv("UPDATE_MODEL_TRAINING_STATUS_ENDPOINT") +RUUTER_PRIVATE_URL = os.getenv("RUUTER_PRIVATE_URL") class ModelInference: def __init__(self): @@ -38,7 +44,60 @@ def get_class_hierarchy_by_model_id(self, model_id): logger.error(f"Failed to retrieve the class hierarchy Reason: {e}") raise RuntimeError(f"Failed to retrieve the class hierarchy Reason: {e}") + + async def authenticate_user(self, cookie: str): + try: + if not cookie: + raise HTTPException(status_code=401, detail="No cookie found in the request") + + url = f"{RUUTER_PRIVATE_URL}/auth/jwt/userinfo" + headers = { + 'cookie': cookie + } + + response = requests.get(url, headers=headers) + + if response.status_code != 200: + raise HTTPException(status_code=response.status_code, detail="Authentication failed") + except Exception as e: + print(f"Error in file handler authentication : {e}") + + raise HTTPException(status_code=500, detail="Authentication failed") + + def update_model_training_progress_session(self,session_id,model_id,cookie): + + payload = {} + cookies_payload = {'customJwtCookie': cookie} + + payload["sessionId"] = session_id + payload["trainingStatus"] = MODEL_TRAINED_AND_DEPLOYED_PROGRESS_STATUS + payload["trainingMessage"] = MODEL_TRAINED_AND_DEPLOYED_PROGRESS_MESSAGE + payload["progressPercentage"] = MODEL_TRAINED_AND_DEPLOYED_PROGRESS_PERCENTAGE + payload["processComplete"] = True + + logger.info(f"Update training progress session for model id - {model_id} payload \n {payload}") + + response=requests.post( url=UPDATE_DATAMODEL_PROGRESS_URL, + json=payload, cookies=cookies_payload) + + if response.status_code==200: + + logger.info(f"REQUEST TO UPDATE TRAINING PROGRESS SESSION FOR MODEL ID {model_id} SUCCESSFUL") + logger.info(f"RESPONSE PAYLOAD \n {response.json()}") + session_id = response.json()["response"]["sessionId"] + + + else: + logger.error(f"REQUEST TO UPDATE TRAINING PROGRESS SESSION FOR MODEL ID {model_id} FAILED") + logger.error(f"ERROR RESPONSE JSON {response.json()}") + logger.error(f"ERROR RESPONSE TEXT {response.text}") + raise RuntimeError(response.text) + + + return session_id + + def validate_class_hierarchy(self, class_hierarchy, model_id): logger.info(f"CLASS HIERARCHY - {class_hierarchy}") @@ -190,7 +249,7 @@ def create_inference(self, payload): except Exception as e: logger.info(f"Failed to call create inference. Reason: {e}") - raise Exception(f"Failed to call create inference. Reason: {e}") + raise RuntimeError(f"Failed to call create inference. Reason: {e}") diff --git a/model-inference/model_inference_api.py b/model-inference/model_inference_api.py index 4a9f0a3e..c2faa9a3 100644 --- a/model-inference/model_inference_api.py +++ b/model-inference/model_inference_api.py @@ -4,7 +4,9 @@ import os from s3_ferry import S3Ferry from utils import unzip_file, clear_folder_contents, calculate_average_predicted_class_probability, get_inference_create_payload, get_inference_update_payload -from constants import S3_DOWNLOAD_FAILED, INFERENCE_LOGS_PATH, JiraInferenceRequest, OutlookInferenceRequest, UpdateRequest +from constants import S3_DOWNLOAD_FAILED, INFERENCE_LOGS_PATH, JiraInferenceRequest, \ + OutlookInferenceRequest, UpdateRequest, OUTLOOK_MODELS_FOLDER_PATH, JIRA_MODELS_FOLDER_PATH,\ + SHARED_MODELS_ROOT_FOLDER from inference_wrapper import InferenceWrapper from model_inference import ModelInference from loguru import logger @@ -13,7 +15,7 @@ logger.add(sink=INFERENCE_LOGS_PATH) app = FastAPI() -modelInference = ModelInference() +model_inference = ModelInference() app.add_middleware( CORSMiddleware, @@ -41,16 +43,23 @@ @app.post("/classifier/datamodel/deployment/outlook/update") async def download_outlook_model(request: Request, model_data:UpdateRequest): - save_location = f"/models/{model_data.modelId}/{model_data.modelId}.zip" - + save_location = f"/models/{model_data.modelId}/{model_data.modelId}.zip" logger.info(f"MODEL DATA PAYLOAD - {model_data}") try: + + ## Authenticating User Cookie + cookie = request.cookies.get("customJwtCookie") + await model_inference.authenticate_user(f'customJwtCookie={cookie}') + + local_file_name = f"{model_data.modelId}.zip" local_file_path = f"/models/outlook/{local_file_name}" + model_progress_session_id = model_data.progressSessionId + ## Get class hierarchy and validate it - is_valid, class_hierarchy = modelInference.get_class_hierarchy_and_validate(model_data.modelId) + is_valid, class_hierarchy = model_inference.get_class_hierarchy_and_validate(model_data.modelId) logger.info(f"IS VALID VALUE : {is_valid}") logger.info(f"CLASS HIERARCHY VALUE : {class_hierarchy}") @@ -58,25 +67,28 @@ async def download_outlook_model(request: Request, model_data:UpdateRequest): if(is_valid and class_hierarchy): # 1. Clear the current content inside the folder - folder_path = os.path.join("..", "shared", "models", "outlook") - clear_folder_contents(folder_path) + outlook_models_folder_path = OUTLOOK_MODELS_FOLDER_PATH + clear_folder_contents(outlook_models_folder_path) # 2. Download the new Model response = s3_ferry.transfer_file(local_file_path, "FS", save_location, "S3") if response.status_code != 201: raise HTTPException(status_code = 500, detail = S3_DOWNLOAD_FAILED) - zip_file_path = os.path.join("..", "shared/models/outlook", local_file_name) - extract_file_path = os.path.join("..", "shared/models/outlook") + zip_file_path = f"{outlook_models_folder_path}/{local_file_name}" + extract_file_path = outlook_models_folder_path # 3. Unzip Model Content unzip_file(zip_path=zip_file_path, extract_to=extract_file_path) + shared_models_root_folder = SHARED_MODELS_ROOT_FOLDER os.remove(zip_file_path) # 3. Replace the content in other folder if it a replacement if(model_data.replaceDeployment): - folder_path = os.path.join("..", "shared", "models", {model_data.replaceDeploymentPlatform}) - clear_folder_contents(folder_path) + # replace_deployment_folder_path = f"{shared_models_root_folder}/{model_data.replaceDeploymentPlatform}" # TODO - THIS NEEDS TO BE CHANGED AND REPLACED + replace_deployment_folder_path = f"{shared_models_root_folder}/jira" + logger.info(f"REPLACE DEPLOYMENT FOLDER PATH - {replace_deployment_folder_path}") + clear_folder_contents(replace_deployment_folder_path) inference_obj.stop_model(deployment_platform=model_data.replaceDeploymentPlatform) # 4. Instantiate Inference Model @@ -99,8 +111,18 @@ async def download_outlook_model(request: Request, model_data:UpdateRequest): model_initiate = inference_obj.load_model(model_path, best_model, deployment_platform="outlook", class_hierarchy=class_hierarchy, model_id=model_data.modelId) logger.info(f"MODEL INITIATE - {model_initiate}") + + if(model_initiate): + + #TODO - Add update_training_status db to update training status to deployed in models metadata DB + + model_inference.update_model_training_progress_session(session_id=model_progress_session_id, + model_id=model_data.modelId, + cookie=cookie) + + logger.info(f"OUTLOOK MODEL UPDATE SUCCESSFUL FOR MODEL ID - {model_data.modelId}") return JSONResponse(status_code=200, content={"replacementStatus": 200}) else: raise HTTPException(status_code = 500, detail = "Failed to initiate inference object") @@ -117,23 +139,33 @@ async def download_outlook_model(request: Request, model_data:UpdateRequest): async def download_jira_model(request: Request, model_data:UpdateRequest): save_location = f"/models/{model_data.modelId}/{model_data.modelId}.zip" + logger.info(f"JIRA MODEL DATA PAYLOAD - {model_data}") try: + + ## Authenticating User Cookie + cookie = request.cookies.get("customJwtCookie") + await model_inference.authenticate_user(f'customJwtCookie={cookie}') + + local_file_name = f"{model_data.modelId}.zip" local_file_path = f"/models/jira/{local_file_name}" + + model_progress_session_id = model_data.progressSessionId logger.info(f"MODEL DATA - {model_data}") # 1. Clear the current content inside the folder - folder_path = os.path.join("..", "shared", "models", "jira") - clear_folder_contents(folder_path) + jira_models_folder_path = JIRA_MODELS_FOLDER_PATH + clear_folder_contents(jira_models_folder_path) # 2. Download the new Model response = s3_ferry.transfer_file(local_file_path, "FS", save_location, "S3") if response.status_code != 201: raise HTTPException(status_code = 500, detail = S3_DOWNLOAD_FAILED) - zip_file_path = os.path.join("..", "shared/models/jira", local_file_name) - extract_file_path = os.path.join("..", "shared/models/jira") + + zip_file_path = f"{jira_models_folder_path}/{local_file_name}" + extract_file_path = jira_models_folder_path # 3. Unzip Model Content unzip_file(zip_path=zip_file_path, extract_to=extract_file_path) @@ -143,26 +175,30 @@ async def download_jira_model(request: Request, model_data:UpdateRequest): #3. Replace the content in other folder if it a replacement --> Call the delete endpoint logger.info("JUST ABOUT TO ENTER - if(model_data.replaceDeployment):") + shared_models_root_folder = SHARED_MODELS_ROOT_FOLDER + if(model_data.replaceDeployment): logger.info("INSIDE REPLACE DEPLOYMENT") + # replace_deployment_folder_path = f"{shared_models_root_folder}/{model_data.replaceDeploymentPlatform}" + replace_deployment_folder_path = f"{shared_models_root_folder}/outlook" # TODO - THIS NEEDS TO BE CHANGED AND REPLACED - folder_path = os.path.join("..", "shared", "models", {model_data.replaceDeploymentPlatform}) - clear_folder_contents(folder_path) + logger.info(f"REPLACE DEPLOYMENT FOLDER PATH - {replace_deployment_folder_path}") + clear_folder_contents(replace_deployment_folder_path) inference_obj.stop_model(deployment_platform=model_data.replaceDeploymentPlatform) # 4. Instantiate Inference Model - logger.info(f"JUST ABOUT TO ENTER get_class_hierarchy_by_model_id") + logger.info("JUST ABOUT TO ENTER get_class_hierarchy_by_model_id") - class_hierarchy = modelInference.get_class_hierarchy_by_model_id(model_data.modelId) + class_hierarchy = model_inference.get_class_hierarchy_by_model_id(model_data.modelId) logger.info(f"JIRA UPDATE CLASS HIERARCHY - {class_hierarchy}") if(class_hierarchy): - model_path = "/shared/models/jira" + model_path = JIRA_MODELS_FOLDER_PATH best_model = model_data.bestBaseModel data = { @@ -173,14 +209,25 @@ async def download_jira_model(request: Request, model_data:UpdateRequest): "model_id": model_data.modelId } - meta_data_save_location = '/shared/models/jira/jira_inference_metadata.json' + + meta_data_save_location = f"{model_path}/jira_inference_metadata.json" with open(meta_data_save_location, 'w') as json_file: json.dump(data, json_file, indent=4) model_initiate = inference_obj.load_model(model_path, best_model, deployment_platform="jira", class_hierarchy=class_hierarchy, model_id=model_data.modelId) - + logger.info(f"JIRA MODEL INITITATE - {model_initiate}") + if(model_initiate): logger.info(f"MODEL INITIATE - {model_initiate}") + + #TODO - Add update_training_status db to update training status to deployed in models metadata DB + + model_inference.update_model_training_progress_session(session_id=model_progress_session_id, + model_id=model_data.modelId, + cookie=cookie) + + logger.info(f"OUTLOOK MODEL UPDATE SUCCESSFUL FOR MODEL ID - {model_data.modelId}") + logger.info("JIRA DEPLOYMENT SUCCESSFUL") return JSONResponse(status_code=200, content={"replacementStatus": 200}) else: @@ -195,8 +242,8 @@ async def download_jira_model(request: Request, model_data:UpdateRequest): @app.post("/classifier/datamodel/deployment/jira/delete") async def delete_folder_content(request:Request): try: - folder_path = os.path.join("..", "shared", "models", "jira") - clear_folder_contents(folder_path) + jira_models_folder_path = JIRA_MODELS_FOLDER_PATH + clear_folder_contents(jira_models_folder_path) # Stop the model inference_obj.stop_model(deployment_platform="jira") @@ -211,8 +258,8 @@ async def delete_folder_content(request:Request): @app.post("/classifier/datamodel/deployment/outlook/delete") async def delete_folder_content(request:Request): try: - folder_path = os.path.join("..", "shared", "models", "outlook") - clear_folder_contents(folder_path) + outlook_models_folder_path = OUTLOOK_MODELS_FOLDER_PATH + clear_folder_contents(outlook_models_folder_path) # Stop the model inference_obj.stop_model(deployment_platform="outlook") @@ -236,14 +283,14 @@ async def outlook_inference(request:Request, inference_data:OutlookInferenceRequ # If there is a active model # 1 . Check whether the if the Inference Exists - is_exist, inference_id = modelInference.check_inference_data_exists(input_id=inference_data.inputId) + is_exist, inference_id = model_inference.check_inference_data_exists(input_id=inference_data.inputId) logger.info(f"Inference Exists : {is_exist}") logger.info(f"Inference ID - {inference_id}") if(is_exist): # Update Inference Scenario # Create Corrected Folder Hierarchy using the final folder id - corrected_folder_hierarchy = modelInference.build_corrected_folder_hierarchy(final_folder_id=inference_data.finalFolderId, + corrected_folder_hierarchy = model_inference.build_corrected_folder_hierarchy(final_folder_id=inference_data.finalFolderId, model_id=model_id) logger.info(f"CORRECTED FOLDER HIERARCHY - {corrected_folder_hierarchy}") @@ -268,7 +315,7 @@ async def outlook_inference(request:Request, inference_data:OutlookInferenceRequ logger.info(f"INFERENCE PAYLOAD - {inference_update_payload}") # Call inference/update endpoint - is_success = modelInference.update_inference(payload=inference_update_payload) + is_success = model_inference.update_inference(payload=inference_update_payload) logger.info(f"IS SUCCESS - {is_success}") @@ -296,7 +343,7 @@ async def outlook_inference(request:Request, inference_data:OutlookInferenceRequ logger.info(f"average probability - {average_probability}") # Get the final folder id of the predicted folder hierarchy - final_folder_id = modelInference.find_final_folder_id(flattened_folder_hierarchy=predicted_hierarchy, model_id=model_id) + final_folder_id = model_inference.find_final_folder_id(flattened_folder_hierarchy=predicted_hierarchy, model_id=model_id) logger.info(f"final folder id - {final_folder_id}") @@ -304,7 +351,7 @@ async def outlook_inference(request:Request, inference_data:OutlookInferenceRequ inference_create_payload = get_inference_create_payload(inference_input_id=inference_data.inputId,inference_text=inference_data.inputText,predicted_labels=predicted_hierarchy, average_predicted_classes_probability=average_probability, platform="OUTLOOK", primary_folder_id=final_folder_id, mail_id=inference_data.mailId) logger.info(f"INFERENCE CREATE PAYLOAD - {inference_create_payload}") # Call inference/create endpoint - is_success = modelInference.create_inference(payload=inference_create_payload) + is_success = model_inference.create_inference(payload=inference_create_payload) logger.info(f"IS SUCCESS - {is_success}") if(is_success): @@ -344,12 +391,12 @@ async def jira_inference(request:Request, inferenceData:JiraInferenceRequest): if(model_id): # 1 . Check whether the if the Inference Exists - is_exist, inference_id = modelInference.check_inference_data_exists(input_id=inferenceData.inputId) + is_exist, inference_id = model_inference.check_inference_data_exists(input_id=inferenceData.inputId) logger.info(f"LOGGING IS EXIST IN JIRA IN JIRA UPDATE INFERENCE - {is_exist}") if(is_exist): # Update Inference Scenario # Call user_corrected_probablities - corrected_probs = inference_obj.get_corrected_probabilities(text=inferenceData.inputText, corrected_labels=inferenceData.finalLabels, deployment_platform="outlook") + corrected_probs = inference_obj.get_corrected_probabilities(text=inferenceData.inputText, corrected_labels=inferenceData.finalLabels, deployment_platform="jira") logger.info(f"CORRECT PROBS IN JIRA UPDATE INFERENCE - {corrected_probs}") if(corrected_probs): @@ -361,7 +408,7 @@ async def jira_inference(request:Request, inferenceData:JiraInferenceRequest): logger.info(f"INFERENCE UPDATE PAYLOAD - {inference_update_payload}") # Call inference/update endpoint - is_success = modelInference.update_inference(payload=inference_update_payload) + is_success = model_inference.update_inference(payload=inference_update_payload) logger.info(f"IS SUCCESS IN JIRA UPDATE INFERENCE - {is_success} ") @@ -393,7 +440,7 @@ async def jira_inference(request:Request, inferenceData:JiraInferenceRequest): inference_create_payload = get_inference_create_payload(inference_input_id=inferenceData.inputId,inference_text=inferenceData.inputText,predicted_labels=predicted_hierarchy, average_predicted_classes_probability=average_probability, platform="JIRA", primary_folder_id=None, mail_id=None) # Call inference/create endpoint - is_success = modelInference.create_inference(payload=inference_create_payload) + is_success = model_inference.create_inference(payload=inference_create_payload) logger.info(f"JIRA inference is_success - {is_success}") if(is_success): diff --git a/model_trainer/model_trainer.py b/model_trainer/model_trainer.py index a049d683..4719e712 100644 --- a/model_trainer/model_trainer.py +++ b/model_trainer/model_trainer.py @@ -29,6 +29,9 @@ def __init__(self, cookie, new_model_id,old_model_id) -> None: self.cookies_payload = {'customJwtCookie': cookie} + + logger.info(f"COOKIES PAYLOAD - {self.cookies_payload}") + logger.info("GETTING MODEL METADATA") response = requests.get(model_url, params = {'modelId': self.new_model_id}, cookies=self.cookies_payload) From 999ef6b97df440f84551b646b0ace804ae756c2f Mon Sep 17 00:00:00 2001 From: Thirunayan22 Date: Sun, 25 Aug 2024 17:19:31 +0530 Subject: [PATCH 532/582] created docker compose file and jira_env --- docker-compose.yml | 3 ++- jira_config.env | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 jira_config.env diff --git a/docker-compose.yml b/docker-compose.yml index c0fbbb44..f933c0f5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -434,8 +434,9 @@ services: dockerfile: Dockerfile ports: - "3008:3008" + env_file: + - jira_config.env environment: - JIRA_WEBHOOK_SECRET: FM8P4YU8lY5uWMDCZukG RUUTER_PUBLIC_JIRA_URL: http://ruuter-public:8086/internal/jira/accept networks: bykstack: diff --git a/jira_config.env b/jira_config.env new file mode 100644 index 00000000..e0aa7892 --- /dev/null +++ b/jira_config.env @@ -0,0 +1 @@ +JIRA_WEBHOOK_SECRET=value From 7650dc7696ce7a06f9d05a4dd353fba6d592db6e Mon Sep 17 00:00:00 2001 From: Thirunayan22 Date: Sun, 25 Aug 2024 17:20:10 +0530 Subject: [PATCH 533/582] updated jira-config.env --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 828c8c15..265817a8 100644 --- a/.gitignore +++ b/.gitignore @@ -413,3 +413,4 @@ protected_configs/ config.env constants.ini +jira_config.env \ No newline at end of file From 0ae4ee0a4f5b32d6407e678f37135247dadb9c8e Mon Sep 17 00:00:00 2001 From: Thirunayan22 Date: Sun, 25 Aug 2024 17:21:30 +0530 Subject: [PATCH 534/582] ignored jira_config.env --- jira_config.env | 1 - 1 file changed, 1 deletion(-) delete mode 100644 jira_config.env diff --git a/jira_config.env b/jira_config.env deleted file mode 100644 index e0aa7892..00000000 --- a/jira_config.env +++ /dev/null @@ -1 +0,0 @@ -JIRA_WEBHOOK_SECRET=value From 95cb02d8c8757fc37304505dc9a9a4cca7d7dbf0 Mon Sep 17 00:00:00 2001 From: Thirunayan22 Date: Sun, 25 Aug 2024 17:42:31 +0530 Subject: [PATCH 535/582] updated readme.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 440bf67d..24c1f573 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,8 @@ This repo will primarily contain: - JIRA_USERNAME - JIRA_CLOUD_DOMAIN - JIRA_WEBHOOK_ID +- Create a .env file called jira_config.env and add the jira webhook secret as shown below + - JIRA_WEBHOOK_SECRET=<> ### Notes From f9232133b80c94b21f670e33911520f5144d002a Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Mon, 26 Aug 2024 09:50:08 +0530 Subject: [PATCH 536/582] remove pinal refs --- GUI/src/assets/Pinal.tsx | 42 ------------------- GUI/src/enums/dataModelsEnums.ts | 1 - .../pages/DataModels/ConfigureDataModel.tsx | 4 +- GUI/src/pages/Integrations/index.tsx | 7 ---- 4 files changed, 1 insertion(+), 53 deletions(-) delete mode 100644 GUI/src/assets/Pinal.tsx diff --git a/GUI/src/assets/Pinal.tsx b/GUI/src/assets/Pinal.tsx deleted file mode 100644 index b815c32d..00000000 --- a/GUI/src/assets/Pinal.tsx +++ /dev/null @@ -1,42 +0,0 @@ -const Pinal = () => { - return ( - - - - - - - - - - - - - - ); -}; - -export default Pinal; diff --git a/GUI/src/enums/dataModelsEnums.ts b/GUI/src/enums/dataModelsEnums.ts index 9a3da354..38264c63 100644 --- a/GUI/src/enums/dataModelsEnums.ts +++ b/GUI/src/enums/dataModelsEnums.ts @@ -16,7 +16,6 @@ export enum Maturity { export enum Platform { JIRA = 'jira', OUTLOOK = 'outlook', - PINAL = 'pinal', UNDEPLOYED = 'undeployed', } diff --git a/GUI/src/pages/DataModels/ConfigureDataModel.tsx b/GUI/src/pages/DataModels/ConfigureDataModel.tsx index af9e870f..5f343aaa 100644 --- a/GUI/src/pages/DataModels/ConfigureDataModel.tsx +++ b/GUI/src/pages/DataModels/ConfigureDataModel.tsx @@ -175,9 +175,7 @@ const ConfigureDataModel: FC = ({ const handleDelete = () => { if ( dataModel.platform === Platform.JIRA || - dataModel.platform === Platform.OUTLOOK || - dataModel.platform === Platform.PINAL - ) { + dataModel.platform === Platform.OUTLOOK) { open({ title: t('dataModels.configureDataModel.deleteErrorTitle'), content:

          {t('dataModels.configureDataModel.deleteErrorDesc')}

          , diff --git a/GUI/src/pages/Integrations/index.tsx b/GUI/src/pages/Integrations/index.tsx index dac7c6eb..4ec4d965 100644 --- a/GUI/src/pages/Integrations/index.tsx +++ b/GUI/src/pages/Integrations/index.tsx @@ -3,7 +3,6 @@ import './Integrations.scss'; import { useTranslation } from 'react-i18next'; import IntegrationCard from 'components/molecules/IntegrationCard'; import Outlook from 'assets/Outlook'; -import Pinal from 'assets/Pinal'; import Jira from 'assets/Jira'; import { useQuery } from '@tanstack/react-query'; import { getIntegrationStatus } from 'services/integration'; @@ -36,12 +35,6 @@ const Integrations: FC = () => { channelDescription={t('integration.outlookDesc') ?? ''} isActive={integrationStatus?.outlook_connection_status} /> - } - channel={t('integration.outlookAndPinal') ?? ''} - channelDescription={t('integration.pinalDesc') ?? ''} - isActive={integrationStatus?.pinal_connection_status} - />
          From 3c27af675737a8087dcd7cc139a19350548d9e33 Mon Sep 17 00:00:00 2001 From: Thirunayan22 Date: Mon, 26 Aug 2024 10:55:54 +0530 Subject: [PATCH 537/582] fixed retraining --- DSL/CronManager/DSL/data_model.yml | 2 +- .../script/python_train_script_starter.sh | 1 + .../update-undeployed-previous-model.sql | 4 +++ .../DSL/POST/classifier/datamodel/create.yml | 1 + .../datamodel/{re-train.yml => retrain.yml} | 6 ++-- .../DSL/POST/classifier/datamodel/update.yml | 18 +++++++++++- docker-compose.yml | 2 +- model-inference/constants.py | 3 +- model-inference/model_inference_api.py | 14 +++++---- model_trainer/main.py | 8 ++++- model_trainer/model_trainer.py | 29 ++++++++++++------- 11 files changed, 65 insertions(+), 23 deletions(-) create mode 100644 DSL/Resql/update-undeployed-previous-model.sql rename DSL/Ruuter.private/DSL/POST/classifier/datamodel/{re-train.yml => retrain.yml} (89%) diff --git a/DSL/CronManager/DSL/data_model.yml b/DSL/CronManager/DSL/data_model.yml index ee160229..69108b37 100644 --- a/DSL/CronManager/DSL/data_model.yml +++ b/DSL/CronManager/DSL/data_model.yml @@ -2,4 +2,4 @@ model_trainer: trigger: off type: exec command: "../app/scripts/python_train_script_starter.sh" - allowedEnvs: ['cookie', 'modelId', 'newModelId','updateType'] \ No newline at end of file + allowedEnvs: ['cookie', 'modelId', 'newModelId','updateType','previousDeploymentEnv'] \ No newline at end of file diff --git a/DSL/CronManager/script/python_train_script_starter.sh b/DSL/CronManager/script/python_train_script_starter.sh index 02611c37..c0892ab6 100755 --- a/DSL/CronManager/script/python_train_script_starter.sh +++ b/DSL/CronManager/script/python_train_script_starter.sh @@ -12,6 +12,7 @@ echo "cookie - $cookie" echo "old model id - $modelId" echo "new model id - $newModelId" echo "update type - $updateType" +echo "previous deployment env - $previousDeploymentEnv" echo "Activating Python Environment" source "/app/python_virtual_env/bin/activate" diff --git a/DSL/Resql/update-undeployed-previous-model.sql b/DSL/Resql/update-undeployed-previous-model.sql new file mode 100644 index 00000000..dec08e30 --- /dev/null +++ b/DSL/Resql/update-undeployed-previous-model.sql @@ -0,0 +1,4 @@ +UPDATE models_metadata +SET + deployment_env = 'undeployed' +WHERE id = :id \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/create.yml b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/create.yml index 047353a2..47530f7b 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/create.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/create.yml @@ -182,6 +182,7 @@ execute_cron_manager: modelId: ${model_id} newModelId: ${model_id} updateType: 'major' + prevDeploymentEnv: 'None' result: res next: assign_success_response diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/re-train.yml b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/retrain.yml similarity index 89% rename from DSL/Ruuter.private/DSL/POST/classifier/datamodel/re-train.yml rename to DSL/Ruuter.private/DSL/POST/classifier/datamodel/retrain.yml index 3d39b89b..484a6f35 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/re-train.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/retrain.yml @@ -54,9 +54,11 @@ execute_cron_manager: args: url: "[#CLASSIFIER_CRON_MANAGER]/execute/data_model/model_trainer" query: - cookie: ${cookie} + cookie: ${cookie.replace('customJwtCookie=','')} #Removing the customJwtCookie phrase from payload to to send cookie token only modelId: ${model_id} - new_model_id: -1 + newModelId: ${model_id} + updateType: 'retrain' + prevDeploymentEnv: 'None' result: res next: assign_success_response diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update.yml b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update.yml index 8f408657..9dd0e200 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/datamodel/update.yml @@ -39,7 +39,7 @@ extract_request_data: base_models: ${incoming.body.baseModels} maturity_label: ${incoming.body.maturityLabel} update_type: ${incoming.body.updateType} - next: check_event_type + next: check_for_request_data check_for_request_data: switch: @@ -125,6 +125,21 @@ update_latest_in_old_versions: next: check_latest_status check_latest_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: update_undeployed_in_previous_model + next: assign_fail_response + +update_undeployed_in_previous_model: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/update-undeployed-previous-model" + body: + id: ${model_id} + result: res + next: check_undeployed_previous_status + +check_undeployed_previous_status: switch: - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} next: set_deployment_env @@ -318,6 +333,7 @@ execute_cron_manager: modelId: ${model_id} newModelId: ${new_model_id} updateType: ${update_type} + previousDeploymentEnv: ${deployment_env_prev} result: res next: assign_success_response diff --git a/docker-compose.yml b/docker-compose.yml index f933c0f5..96b37da7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ services: image: ruuter environment: - application.cors.allowedOrigins=http://localhost:8086,http://localhost:3001,http://localhost:3003,http://localhost:3004,http://localhost:8080,http://localhost:8000 - - application.http CodesAllowList=200,201,202,204,400,401,403,404,500 + - application.httpCodesAllowList=200,201,202,204,400,401,403,404,500 - application.internalRequests.allowedIPs=127.0.0.1,172.25.0.7,172.25.0.21,172.25.0.22 - application.logging.displayRequestContent=true - application.incomingRequests.allowedMethodTypes=POST,GET,PUT diff --git a/model-inference/constants.py b/model-inference/constants.py index d18d0888..74b96363 100644 --- a/model-inference/constants.py +++ b/model-inference/constants.py @@ -42,8 +42,9 @@ class UpdateRequest(BaseModel): modelId: int replaceDeployment:bool - replaceDeploymentPlatform:str + replaceDeploymentPlatform:Optional[str] = None bestBaseModel:str + updateType: Optional[str] = None progressSessionId: int class OutlookInferenceRequest(BaseModel): diff --git a/model-inference/model_inference_api.py b/model-inference/model_inference_api.py index c2faa9a3..bcbc35fb 100644 --- a/model-inference/model_inference_api.py +++ b/model-inference/model_inference_api.py @@ -14,6 +14,9 @@ logger.add(sink=INFERENCE_LOGS_PATH) + +logger.info("ENTERING MODEL INFERENCE API") + app = FastAPI() model_inference = ModelInference() @@ -84,9 +87,9 @@ async def download_outlook_model(request: Request, model_data:UpdateRequest): os.remove(zip_file_path) # 3. Replace the content in other folder if it a replacement - if(model_data.replaceDeployment): - # replace_deployment_folder_path = f"{shared_models_root_folder}/{model_data.replaceDeploymentPlatform}" # TODO - THIS NEEDS TO BE CHANGED AND REPLACED - replace_deployment_folder_path = f"{shared_models_root_folder}/jira" + if(model_data.replaceDeployment and model_data.replaceDeploymentPlatform!="undeployed" and model_data.updateType!="retrain"): + + replace_deployment_folder_path = f"{shared_models_root_folder}/{model_data.replaceDeploymentPlatform}" logger.info(f"REPLACE DEPLOYMENT FOLDER PATH - {replace_deployment_folder_path}") clear_folder_contents(replace_deployment_folder_path) inference_obj.stop_model(deployment_platform=model_data.replaceDeploymentPlatform) @@ -177,11 +180,10 @@ async def download_jira_model(request: Request, model_data:UpdateRequest): shared_models_root_folder = SHARED_MODELS_ROOT_FOLDER - if(model_data.replaceDeployment): + if(model_data.replaceDeployment and model_data.replaceDeploymentPlatform!="undeployed" and model_data.updateType!="retrain"): logger.info("INSIDE REPLACE DEPLOYMENT") - # replace_deployment_folder_path = f"{shared_models_root_folder}/{model_data.replaceDeploymentPlatform}" - replace_deployment_folder_path = f"{shared_models_root_folder}/outlook" # TODO - THIS NEEDS TO BE CHANGED AND REPLACED + replace_deployment_folder_path = f"{shared_models_root_folder}/{model_data.replaceDeploymentPlatform}" logger.info(f"REPLACE DEPLOYMENT FOLDER PATH - {replace_deployment_folder_path}") clear_folder_contents(replace_deployment_folder_path) diff --git a/model_trainer/main.py b/model_trainer/main.py index daef88cf..d602c322 100644 --- a/model_trainer/main.py +++ b/model_trainer/main.py @@ -8,16 +8,22 @@ cookie = os.environ.get('cookie') new_model_id = os.environ.get('newModelId') old_model_id = os.environ.get('modelId') +update_type = os.environ.get('updateType') +prev_deployment_env = os.environ.get('previousDeploymentEnv') logger.info(f"COOKIE - {cookie}") logger.info(f"OLD MODEL ID {old_model_id}") logger.info(f"NEW MODEL ID - {new_model_id}") +logger.info(f"UPDATE TYPE - {update_type}") +logger.info(f"PREVIOUSE DEPLOYMENT ENV - {prev_deployment_env}") logger.info(f"ENTERING MODEL TRAINER SCRIPT FOR MODEL ID - {old_model_id}") -trainer = ModelTrainer(cookie=cookie,new_model_id=new_model_id,old_model_id=old_model_id) +trainer = ModelTrainer(cookie=cookie,new_model_id=new_model_id, + old_model_id=old_model_id, prev_deployment_env=prev_deployment_env, + update_type=update_type) trainer.train() logger.info("TRAINING SCRIPT COMPLETED") diff --git a/model_trainer/model_trainer.py b/model_trainer/model_trainer.py index 4719e712..78ce141f 100644 --- a/model_trainer/model_trainer.py +++ b/model_trainer/model_trainer.py @@ -19,37 +19,42 @@ logger.add(sink=TRAINING_LOGS_PATH) class ModelTrainer: - def __init__(self, cookie, new_model_id,old_model_id) -> None: + def __init__(self, cookie, new_model_id,old_model_id,prev_deployment_env,update_type) -> None: model_url = GET_MODEL_METADATA_ENDPOINT self.new_model_id = int(new_model_id) self.old_model_id = int(old_model_id) + self.prev_deployment_env = prev_deployment_env self.cookie = cookie + self.update_type = update_type self.cookies_payload = {'customJwtCookie': cookie} - logger.info(f"COOKIES PAYLOAD - {self.cookies_payload}") logger.info("GETTING MODEL METADATA") + if self.update_type == "retrain": + logger.info(f"ENTERING INTO RETRAIN SEQUENCE FOR MODELID - {self.new_model_id}") + response = requests.get(model_url, params = {'modelId': self.new_model_id}, cookies=self.cookies_payload) - + #only for model create and retrain operations old_model_id=new_model_id if self.old_model_id==self.new_model_id: self.replace_deployment = False - + else: self.replace_deployment = True if response.status_code == 200: self.model_details = response.json() - self.deployment_platform = self.model_details['response']['data'][0]['deploymentEnv'] + self.current_deployment_platform = self.model_details['response']['data'][0]['deploymentEnv'] logger.info("SUCCESSFULLY RECIEVED MODEL DETAILS") + logger.info(f"MODEL DETAILS - {self.model_details}") else: logger.error(f"FAILED WITH STATUS CODE: {response.status_code}") @@ -183,27 +188,31 @@ def deploy_model(self, best_model_name, progress_session_id): payload = {} payload["modelId"] = self.new_model_id payload["replaceDeployment"] = self.replace_deployment - payload["replaceDeploymentPlatform"] = self.deployment_platform + payload["replaceDeploymentPlatform"] = self.prev_deployment_env payload["bestBaseModel"] = best_model_name payload["progressSessionId"] = progress_session_id + payload["updateType"] = self.update_type + + if self.update_type == "retrain": + payload["replaceDeploymentPlatform"] = self.current_deployment_platform logger.info(f"SENDING MODEL DEPLOYMENT REQUEST FOR MODEL ID - {self.new_model_id}") logger.info(f"MODEL DEPLOYMENT PAYLOAD - {payload}") deployment_url = None - if self.deployment_platform == JIRA: + if self.current_deployment_platform == JIRA: deployment_url = JIRA_DEPLOYMENT_ENDPOINT - elif self.deployment_platform == OUTLOOK: + elif self.current_deployment_platform == OUTLOOK: deployment_url = OUTLOOK_DEPLOYMENT_ENDPOINT else: - logger.info(f"UNRECOGNIZED DEPLOYMENT PLATFORM - {self.deployment_platform}") - raise RuntimeError(f"RUNTIME ERROR - UNRECOGNIZED DEPLOYMENT PLATFORM - {self.deployment_platform}") + logger.info(f"UNRECOGNIZED DEPLOYMENT PLATFORM - {self.current_deployment_platform}") + raise RuntimeError(f"RUNTIME ERROR - UNRECOGNIZED DEPLOYMENT PLATFORM - {self.current_deployment_platform}") response = requests.post( url=deployment_url, From 5418fe7256a85f823f25e3ce3f701e8f8e1eb12b Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 26 Aug 2024 13:50:23 +0530 Subject: [PATCH 538/582] docker compose updates for delete models --- docker-compose.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index f933c0f5..2aac57f6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -215,6 +215,10 @@ services: - GET_PAGE_COUNT_URL=http://ruuter-private:8088/classifier/datasetgroup/group/page-count?groupId=dgId - DATAGROUP_DELETE_CONFIRMATION_URL= http://ruuter-private:8088/classifier/datasetgroup/metadata/delete - DATAMODEL_DELETE_CONFIRMATION_URL= http://ruuter-private:8088/classifier/datamodel/update/dataset-group + - JIRA_ACTIVE_MODEL_DELETE_URL=http://model-inference:8003/classifier/datamodel/deployment/jira/delete + - OUTLOOK_ACTIVE_MODEL_DELETE_URL=http://model-inference:8003/classifier/datamodel/deployment/outlook/delete + - TEST_MODEL_DELETE_URL=http://test-inference:8011/classifier/datamodel/deployment/test/delete + - MODEL_METADATA_DELETE_URL=http://ruuter-private:8088/classifier/datamodel/metadata/delete ports: - "8000:8000" networks: From c26d469adf91fa666b7e4e28c2e42ddee1b04abd Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 26 Aug 2024 13:50:38 +0530 Subject: [PATCH 539/582] model delete ruuter and cron manger updates --- DSL/CronManager/DSL/datamodel_processing.yml | 5 ++ .../script/datamodel_deletion_exec.sh | 57 +++++++++++++++++++ .../DSL/POST/classifier/datamodel/delete.yml | 4 +- 3 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 DSL/CronManager/DSL/datamodel_processing.yml create mode 100644 DSL/CronManager/script/datamodel_deletion_exec.sh diff --git a/DSL/CronManager/DSL/datamodel_processing.yml b/DSL/CronManager/DSL/datamodel_processing.yml new file mode 100644 index 00000000..a27f6f7a --- /dev/null +++ b/DSL/CronManager/DSL/datamodel_processing.yml @@ -0,0 +1,5 @@ +datamodel_deletion: + trigger: off + type: exec + command: "../app/scripts/datamodel_deletion_exec.sh" + allowedEnvs: ["cookie","modelId"] \ No newline at end of file diff --git a/DSL/CronManager/script/datamodel_deletion_exec.sh b/DSL/CronManager/script/datamodel_deletion_exec.sh new file mode 100644 index 00000000..07a35c08 --- /dev/null +++ b/DSL/CronManager/script/datamodel_deletion_exec.sh @@ -0,0 +1,57 @@ +#!/bin/bash +echo "Started Shell Script to delete models" +# Ensure required environment variables are set +if [ -z "$modelId" ] || [ -z "$cookie" ]; then + echo "One or more environment variables are missing." + echo "Please set modelId and cookie." + exit 1 +fi + +# Set the API URL to get metadata based on the modelId +api_url="http://ruuter-private:8088/classifier/datamodel/metadata?modelId=$modelId" + +# Send the request to the API and capture the output +api_response=$(curl -s -H "Cookie: $cookie" -X GET "$api_url") + +# Check if the API response is valid +if [ -z "$api_response" ]; then + echo "API request failed to get the model metadata." + exit 1 +fi + +deployment_env=$(echo "$api_response" | jq -r '.deploymentEnv') + +# Construct the payload using here document +payload=$(cat < Date: Mon, 26 Aug 2024 13:50:47 +0530 Subject: [PATCH 540/582] model delete file handler updates --- file-handler/constants.py | 4 ++++ file-handler/file_handler_api.py | 33 ++++++++++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/file-handler/constants.py b/file-handler/constants.py index f50171a9..9fd2197b 100644 --- a/file-handler/constants.py +++ b/file-handler/constants.py @@ -82,6 +82,10 @@ def GET_S3_FERRY_PAYLOAD(destinationFilePath: str, destinationStorageType: str, DELETE_STOPWORDS_URL = os.getenv("DELETE_STOPWORDS_URL") DATAGROUP_DELETE_CONFIRMATION_URL = os.getenv("DATAGROUP_DELETE_CONFIRMATION_URL") DATAMODEL_DELETE_CONFIRMATION_URL = os.getenv("DATAMODEL_DELETE_CONFIRMATION_URL") +JIRA_ACTIVE_MODEL_DELETE_URL = os.getenv("JIRA_ACTIVE_MODEL_DELETE_URL") +OUTLOOK_ACTIVE_MODEL_DELETE_URL = os.getenv("OUTLOOK_ACTIVE_MODEL_DELETE_URL") +TEST_MODEL_DELETE_URL = os.getenv("TEST_MODEL_DELETE_URL") +MODEL_METADATA_DELETE_URL = os.getenv("MODEL_METADATA_DELETE_URL") # Dataset locations TEMP_DATASET_LOCATION = "/dataset/{dg_id}/temp/temp_dataset.json" diff --git a/file-handler/file_handler_api.py b/file-handler/file_handler_api.py index 3bf66cf4..46f917aa 100644 --- a/file-handler/file_handler_api.py +++ b/file-handler/file_handler_api.py @@ -200,7 +200,8 @@ async def download_and_convert(request: Request, saveLocation:str, background_ta json_file_path = os.path.join(JSON_FILE_DIRECTORY, f"{local_file_name}") with open(f"{json_file_path}", 'r') as json_file: - json_data = json.load(json_file) + json_str = json_file.read().replace('NaN', 'null') + json_data = json.loads(json_str) background_tasks.add_task(os.remove, json_file_path) @@ -456,12 +457,40 @@ async def delete_datamodels(request: Request): payload = await request.json() model_id = int(payload["modelId"]) + deployment_env = payload["deploymentEnv"] deleter = ModelDeleter(S3_FERRY_URL) success = deleter.delete_model_files(model_id) if success: - return JSONResponse(status_code=200, content={"message": "Data model deletion completed successfully."}) + headers = { + 'Content-Type': 'application/json', + 'Cookie': f'customJwtCookie={cookie}' + } + active_models_deleted = False + if deployment_env.lower() == "jira": + response = requests.post(JIRA_ACTIVE_MODEL_DELETE_URL, headers=headers, json={"modelId": model_id}) + if response.status_code == 200: + active_models_deleted = True + elif deployment_env.lower() == "outlook": + response = requests.post(OUTLOOK_ACTIVE_MODEL_DELETE_URL, headers=headers, json={"modelId": model_id}) + if response.status_code == 200: + active_models_deleted = True + elif deployment_env.lower() == "test": + response = requests.post(TEST_MODEL_DELETE_URL, headers=headers, json={"deleteModelId": model_id}) + if response.status_code == 200: + active_models_deleted = True + else: + active_models_deleted = True + if active_models_deleted: + response = requests.post(MODEL_METADATA_DELETE_URL, headers=headers, json={"deleteModelId": model_id}) + if response.status_code == 200: + active_models_deleted = True + return JSONResponse(status_code=200, content={"message": "Data model deletion completed successfully."}) + else: + return JSONResponse(status_code=500, content={"message": "Data model metadata deletion failed."}) + else: + return JSONResponse(status_code=500, content={"message": "Active data model deletion failed."}) else: return JSONResponse(status_code=500, content={"message": "Data model deletion failed."}) except Exception as e: From d53b08a667877b0f5c232225db55efd8f2c47f79 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 26 Aug 2024 14:22:05 +0530 Subject: [PATCH 541/582] dataset validator fails for None values bug fix --- dataset-processor/dataset_validator.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/dataset-processor/dataset_validator.py b/dataset-processor/dataset_validator.py index 3a618dfd..13c615a1 100644 --- a/dataset-processor/dataset_validator.py +++ b/dataset-processor/dataset_validator.py @@ -103,6 +103,7 @@ def get_dataset_by_location(self, fileLocation, custom_jwt_cookie): return response.json() except requests.exceptions.RequestException as e: print(MSG_REQUEST_FAILED.format("Dataset download")) + print(e) return None def get_validation_criteria(self, dgId, cookie): @@ -136,8 +137,9 @@ def validate_fields(self, data, validation_criteria): if field in row: value = row[field] if not self.validate_value(value, rules['type']): - print(MSG_VALIDATION_FIELD_FAIL.format(field, idx + 1)) - return {'success': False, 'message': MSG_VALIDATION_FIELD_FAIL.format(field, idx + 1)} + if value is not None: + print(MSG_VALIDATION_FIELD_FAIL.format(field, idx + 1)) + return {'success': False, 'message': MSG_VALIDATION_FIELD_FAIL.format(field, idx + 1)} print(MSG_VALIDATION_FIELDS_SUCCESS) return {'success': True, 'message': MSG_VALIDATION_FIELDS_SUCCESS} except Exception as e: @@ -174,6 +176,7 @@ def validate_class_hierarchy(self, data, validation_criteria, class_hierarchy): data_values = self.extract_data_class_values(data, data_class_columns) missing_in_hierarchy = data_values - hierarchy_values + missing_in_hierarchy = [item for item in missing_in_hierarchy if item is not None] missing_in_data = hierarchy_values - data_values if missing_in_hierarchy: From cfa34a4aa1636e0df6d72b7a2db3d01f856e38bd Mon Sep 17 00:00:00 2001 From: Thirunayan22 Date: Mon, 26 Aug 2024 14:46:10 +0530 Subject: [PATCH 542/582] updated for test inference --- docker-compose.yml | 23 +--- model-inference/Dockerfile | 4 +- model-inference/constants.py | 10 +- model-inference/model_inference_api.py | 161 ++++++++++++++++++++-- model-inference/s3_ferry.py | 26 ++-- model-inference/test_inference_wrapper.py | 81 +++++++++++ model_trainer/constants.py | 4 + model_trainer/model_trainer.py | 8 +- test_inference/Dockerfile | 19 --- test_inference/constants.py | 20 --- test_inference/docker-compose.yml | 47 ------- test_inference/requirements.txt | 32 ----- test_inference/s3_ferry.py | 21 --- test_inference/test_inference.py | 24 ---- test_inference/test_inference_api.py | 114 --------------- test_inference/test_inference_wrapper.py | 41 ------ test_inference/utils.py | 42 ------ 17 files changed, 265 insertions(+), 412 deletions(-) create mode 100644 model-inference/test_inference_wrapper.py delete mode 100644 test_inference/Dockerfile delete mode 100644 test_inference/constants.py delete mode 100644 test_inference/docker-compose.yml delete mode 100644 test_inference/requirements.txt delete mode 100644 test_inference/s3_ferry.py delete mode 100644 test_inference/test_inference.py delete mode 100644 test_inference/test_inference_api.py delete mode 100644 test_inference/test_inference_wrapper.py delete mode 100644 test_inference/utils.py diff --git a/docker-compose.yml b/docker-compose.yml index 96b37da7..4215dbd1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -285,6 +285,7 @@ services: - S3_FERRY_URL=http://s3-ferry:3000/v1/files/copy - JIRA_MODEL_DOWNLOAD_DIRECTORY=/shared/models/jira - OUTLOOK_MODEL_DOWNLOAD_DIRECTORY=/shared/models/outlook + - TEST_MODEL_DOWNLOAD_ROOT_DIRECTORY=/shared/models/testing - GET_INFERENCE_DATASET_EXIST_URL=http://ruuter-public:8086/internal/exist - CREATE_INFERENCE_URL=http://ruuter-public:8086/internal/create - UPDATE_INFERENCE_URL=http://ruuter-public:8086/internal/corrected @@ -303,28 +304,6 @@ services: - init - s3-ferry - test-inference: - build: - context: ./test_inference - dockerfile: Dockerfile - container_name: test-inference - volumes: - - shared-volume:/shared - environment: - - RUUTER_PRIVATE_URL=http://ruuter-private:8088 - - S3_FERRY_URL=http://s3-ferry:3000/v1/files/copy - - TEST_MODEL_DOWNLOAD_DIRECTORY=/shared/models/test - - OUTLOOK_ACCESS_TOKEN_API_URL=http://ruuter-public:8086/internal/validate - - UPDATE_DATAMODEL_PROGRESS_URL=http://ruuter-private:8088/classifier/datamodel/progress/update - ports: - - "8011:8011" - networks: - bykstack: - ipv4_address: 172.25.0.23 - depends_on: - - init - - file-handler - hierarchy-validation: build: context: ./hierarchy_validation diff --git a/model-inference/Dockerfile b/model-inference/Dockerfile index 5458ea57..4074686c 100644 --- a/model-inference/Dockerfile +++ b/model-inference/Dockerfile @@ -11,10 +11,12 @@ COPY model_inference.py . COPY inference_pipeline.py . COPY s3_ferry.py . COPY utils.py . +COPY test_inference_wrapper.py . ENV HF_HOME=/app/cache/ -RUN mkdir -p /shared && chown appuser:appuser /shared && chmod 770 /shared +RUN mkdir -p /shared && chown appuser:appuser /shared && chmod 777 /shared +RUN mkdir -p /shared/models/testing && chown appuser:appuser /shared/models/testing && chmod 777 /shared/models/testing RUN chown -R appuser:appuser /app EXPOSE 8003 USER appuser diff --git a/model-inference/constants.py b/model-inference/constants.py index 74b96363..7497d2f0 100644 --- a/model-inference/constants.py +++ b/model-inference/constants.py @@ -31,7 +31,6 @@ MODEL_TRAINED_AND_DEPLOYED_PROGRESS_STATUS = "Model Trained And Deployed" - S3_DOWNLOAD_FAILED = { "upload_status": 500, "operation_successful": False, @@ -56,4 +55,11 @@ class OutlookInferenceRequest(BaseModel): class JiraInferenceRequest(BaseModel): inputId:str inputText:str - finalLabels:Optional[List[str]] = None \ No newline at end of file + finalLabels:Optional[List[str]] = None + +class TestInferenceRequest(BaseModel): + modelId:int + text:str + +class DeleteTestRequest(BaseModel): + deleteModelId:int diff --git a/model-inference/model_inference_api.py b/model-inference/model_inference_api.py index bcbc35fb..9db67f1b 100644 --- a/model-inference/model_inference_api.py +++ b/model-inference/model_inference_api.py @@ -8,6 +8,7 @@ OutlookInferenceRequest, UpdateRequest, OUTLOOK_MODELS_FOLDER_PATH, JIRA_MODELS_FOLDER_PATH,\ SHARED_MODELS_ROOT_FOLDER from inference_wrapper import InferenceWrapper +from test_inference_wrapper import TestInferenceWrapper from model_inference import ModelInference from loguru import logger import json @@ -28,13 +29,15 @@ allow_headers = ["*"], ) -inference_obj = InferenceWrapper() +model_inference_wrapper = InferenceWrapper() +test_inference_wrapper = TestInferenceWrapper() S3_FERRY_URL = os.getenv("S3_FERRY_URL") s3_ferry = S3Ferry(S3_FERRY_URL) RUUTER_PRIVATE_URL = os.getenv("RUUTER_PRIVATE_URL") JIRA_MODEL_DOWNLOAD_DIRECTORY = os.getenv("JIRA_MODEL_DOWNLOAD_DIRECTORY", "/shared/models/jira") OUTLOOK_MODEL_DOWNLOAD_DIRECTORY = os.getenv("OUTLOOK_MODEL_DOWNLOAD_DIRECTORY", "/shared/models/outlook") +TEST_MODEL_DOWNLOAD_ROOT_DIRECTORY = os.getenv("TEST_MODEL_DOWNLOAD_DIRECTORY", "/shared/models/testing") if not os.path.exists(JIRA_MODEL_DOWNLOAD_DIRECTORY): os.makedirs(JIRA_MODEL_DOWNLOAD_DIRECTORY) @@ -42,6 +45,8 @@ if not os.path.exists(OUTLOOK_MODEL_DOWNLOAD_DIRECTORY): os.makedirs(OUTLOOK_MODEL_DOWNLOAD_DIRECTORY) +if not os.path.exists(TEST_MODEL_DOWNLOAD_ROOT_DIRECTORY): + os.makedirs(TEST_MODEL_DOWNLOAD_ROOT_DIRECTORY) @app.post("/classifier/datamodel/deployment/outlook/update") async def download_outlook_model(request: Request, model_data:UpdateRequest): @@ -92,7 +97,7 @@ async def download_outlook_model(request: Request, model_data:UpdateRequest): replace_deployment_folder_path = f"{shared_models_root_folder}/{model_data.replaceDeploymentPlatform}" logger.info(f"REPLACE DEPLOYMENT FOLDER PATH - {replace_deployment_folder_path}") clear_folder_contents(replace_deployment_folder_path) - inference_obj.stop_model(deployment_platform=model_data.replaceDeploymentPlatform) + model_inference_wrapper.stop_model(deployment_platform=model_data.replaceDeploymentPlatform) # 4. Instantiate Inference Model model_path = "/shared/models/outlook" @@ -111,7 +116,7 @@ async def download_outlook_model(request: Request, model_data:UpdateRequest): json.dump(data, json_file, indent=4) - model_initiate = inference_obj.load_model(model_path, best_model, deployment_platform="outlook", class_hierarchy=class_hierarchy, model_id=model_data.modelId) + model_initiate = model_inference_wrapper.load_model(model_path, best_model, deployment_platform="outlook", class_hierarchy=class_hierarchy, model_id=model_data.modelId) logger.info(f"MODEL INITIATE - {model_initiate}") @@ -188,7 +193,7 @@ async def download_jira_model(request: Request, model_data:UpdateRequest): logger.info(f"REPLACE DEPLOYMENT FOLDER PATH - {replace_deployment_folder_path}") clear_folder_contents(replace_deployment_folder_path) - inference_obj.stop_model(deployment_platform=model_data.replaceDeploymentPlatform) + model_inference_wrapper.stop_model(deployment_platform=model_data.replaceDeploymentPlatform) # 4. Instantiate Inference Model @@ -216,7 +221,7 @@ async def download_jira_model(request: Request, model_data:UpdateRequest): with open(meta_data_save_location, 'w') as json_file: json.dump(data, json_file, indent=4) - model_initiate = inference_obj.load_model(model_path, best_model, deployment_platform="jira", class_hierarchy=class_hierarchy, model_id=model_data.modelId) + model_initiate = model_inference_wrapper.load_model(model_path, best_model, deployment_platform="jira", class_hierarchy=class_hierarchy, model_id=model_data.modelId) logger.info(f"JIRA MODEL INITITATE - {model_initiate}") if(model_initiate): @@ -228,7 +233,7 @@ async def download_jira_model(request: Request, model_data:UpdateRequest): model_id=model_data.modelId, cookie=cookie) - logger.info(f"OUTLOOK MODEL UPDATE SUCCESSFUL FOR MODEL ID - {model_data.modelId}") + logger.info(f"JIRA MODEL UPDATE SUCCESSFUL FOR MODEL ID - {model_data.modelId}") logger.info("JIRA DEPLOYMENT SUCCESSFUL") return JSONResponse(status_code=200, content={"replacementStatus": 200}) @@ -240,6 +245,134 @@ async def download_jira_model(request: Request, model_data:UpdateRequest): except Exception as e: raise HTTPException(status_code = 500, detail=str(e)) +@app.post("/classifier/datamodel/deployment/test/update") +async def download_test_model(request: Request, model_data:UpdateRequest): + + save_location = f"/models/{model_data.modelId}/{model_data.modelId}.zip" + logger.info(f"TEST MODEL DATA PAYLOAD - {model_data}") + + try: + + ## Authenticating User Cookie + cookie = request.cookies.get("customJwtCookie") + await model_inference.authenticate_user(f'customJwtCookie={cookie}') + + + local_file_name = f"{model_data.modelId}.zip" + + # This path is actually under /shared since the root directory of s3 ferry is anyways /shared we don't use it when referring the local filepath + local_file_path = f"/models/testing/{model_data.modelId}/{local_file_name}" + + model_progress_session_id = model_data.progressSessionId + + logger.info(f"MODEL DATA - {model_data}") + # 1. Clear the current content inside the folder + test_models_folder_path = f"/shared/models/testing/{model_data.modelId}" + + if not os.path.exists(test_models_folder_path): + logger.info("CREATING FOLDER INSIDE MODEL EXIST") + os.makedirs(test_models_folder_path) + + if os.path.exists(test_models_folder_path): + logger.info("CLEARING TEST MODEL CONTAINERS") + clear_folder_contents(test_models_folder_path) + + # 2. Download the new Model + response = s3_ferry.transfer_file(destination_file_path=local_file_path, + destination_storage_type="FS", + source_file_path=save_location, + source_storage_type="S3") + + logger.info(f"S3 Ferry Response - {response.json()}") + + if response.status_code != 201: + raise HTTPException(status_code = 500, detail = S3_DOWNLOAD_FAILED) + + + zip_file_path = f"{test_models_folder_path}/{local_file_name}" + extract_file_path = test_models_folder_path + + # 3. Unzip Model Content + unzip_file(zip_path=zip_file_path, extract_to=extract_file_path) + + os.remove(zip_file_path) + + #3. Replace the content in other folder if it a replacement --> Call the delete endpoint + logger.info("JUST ABOUT TO ENTER - if(model_data.replaceDeployment):") + + shared_models_root_folder = SHARED_MODELS_ROOT_FOLDER + + if(model_data.replaceDeployment and model_data.replaceDeploymentPlatform!="undeployed" and model_data.updateType!="retrain"): + + logger.info("INSIDE REPLACE DEPLOYMENT") + replace_deployment_folder_path = f"{shared_models_root_folder}/{model_data.replaceDeploymentPlatform}" + + logger.info(f"REPLACE DEPLOYMENT FOLDER PATH - {replace_deployment_folder_path}") + clear_folder_contents(replace_deployment_folder_path) + + model_inference_wrapper.stop_model(deployment_platform=model_data.replaceDeploymentPlatform) + + # 4. Instantiate Inference Model + + logger.info("JUST ABOUT TO ENTER get_class_hierarchy_by_model_id") + + class_hierarchy = model_inference.get_class_hierarchy_by_model_id(model_data.modelId) + + logger.info(f"TEST UPDATE CLASS HIERARCHY - {class_hierarchy}") + + if(class_hierarchy): + + model_path = test_models_folder_path + best_model = model_data.bestBaseModel + + new_metadata = { + "model_path": model_path, + "best_model": best_model, + "deployment_platform": "testing", + "class_hierarchy": class_hierarchy, + "model_id": model_data.modelId + } + + meta_data_save_location = '/shared/models/testing/test_inference_metadata.json' + + + if os.path.exists(meta_data_save_location): + with open(meta_data_save_location, 'r') as json_file: + existing_data = json.load(json_file) + else: + existing_data = [] + + existing_data.append(new_metadata) + + with open(meta_data_save_location, 'w') as json_file: + json.dump(existing_data, json_file, indent=4) + + model_initiate = test_inference_wrapper.load_model(model_path=model_path, best_performing_model=best_model, + deployment_platform="jira", class_hierarchy=class_hierarchy, + model_id=model_data.modelId) + + logger.info(f"TEST MODEL INITITATE - {model_initiate}") + + if(model_initiate): + logger.info(f"TEST MODEL INITIATE - {model_initiate}") + + #TODO - Add update_training_status db to update training status to deployed in models metadata DB + + model_inference.update_model_training_progress_session(session_id=model_progress_session_id, + model_id=model_data.modelId, + cookie=cookie) + + logger.info(f"TEST MODEL UPDATE SUCCESSFUL FOR MODEL ID - {model_data.modelId}") + + logger.info("TEST DEPLOYMENT SUCCESSFUL") + return JSONResponse(status_code=200, content={"replacementStatus": 200}) + else: + raise HTTPException(status_code = 500, detail = "Failed to initiate inference object") + else: + raise HTTPException(status_code = 500, detail = "Error in obtaining the class hierarchy") + + except Exception as e: + raise HTTPException(status_code = 500, detail=str(e)) @app.post("/classifier/datamodel/deployment/jira/delete") async def delete_folder_content(request:Request): @@ -248,7 +381,7 @@ async def delete_folder_content(request:Request): clear_folder_contents(jira_models_folder_path) # Stop the model - inference_obj.stop_model(deployment_platform="jira") + model_inference_wrapper.stop_model(deployment_platform="jira") delete_success = {"message" : "Model Deleted Successfully!"} return JSONResponse(status_code = 200, content = delete_success) @@ -264,7 +397,7 @@ async def delete_folder_content(request:Request): clear_folder_contents(outlook_models_folder_path) # Stop the model - inference_obj.stop_model(deployment_platform="outlook") + model_inference_wrapper.stop_model(deployment_platform="outlook") delete_success = {"message" : "Model Deleted Successfully!"} return JSONResponse(status_code = 200, content = delete_success) @@ -279,7 +412,7 @@ async def outlook_inference(request:Request, inference_data:OutlookInferenceRequ logger.info("Inference Endpoint Calling") logger.info(f"Inference Data : {inference_data}") - model_id = inference_obj.get_outlook_model_id() + model_id = model_inference_wrapper.get_outlook_model_id() logger.info(f"Model Id : {model_id}") if(model_id): # If there is a active model @@ -297,7 +430,7 @@ async def outlook_inference(request:Request, inference_data:OutlookInferenceRequ logger.info(f"CORRECTED FOLDER HIERARCHY - {corrected_folder_hierarchy}") # Call user_corrected_probablities - corrected_probs = inference_obj.get_corrected_probabilities(text=inference_data.inputText, + corrected_probs = model_inference_wrapper.get_corrected_probabilities(text=inference_data.inputText, corrected_labels=corrected_folder_hierarchy, deployment_platform="outlook") @@ -333,7 +466,7 @@ async def outlook_inference(request:Request, inference_data:OutlookInferenceRequ else: # Create Inference Scenario # Call Inference logger.info("CREATE INFERENCE SCENARIO OUTLOOK") - predicted_hierarchy, probabilities = inference_obj.inference(inference_data.inputText, deployment_platform="outlook") + predicted_hierarchy, probabilities = model_inference_wrapper.inference(inference_data.inputText, deployment_platform="outlook") logger.info(f"PREDICTED HIERARCHIES AND PROBABILITIES {predicted_hierarchy}") logger.info(f"PROBABILITIES {probabilities}") @@ -388,7 +521,7 @@ async def jira_inference(request:Request, inferenceData:JiraInferenceRequest): logger.info(f"INFERENCE DATA IN JIRA INFERENCE - {inferenceData}") - model_id = inference_obj.get_jira_model_id() + model_id = model_inference_wrapper.get_jira_model_id() if(model_id): @@ -398,7 +531,7 @@ async def jira_inference(request:Request, inferenceData:JiraInferenceRequest): logger.info(f"LOGGING IS EXIST IN JIRA IN JIRA UPDATE INFERENCE - {is_exist}") if(is_exist): # Update Inference Scenario # Call user_corrected_probablities - corrected_probs = inference_obj.get_corrected_probabilities(text=inferenceData.inputText, corrected_labels=inferenceData.finalLabels, deployment_platform="jira") + corrected_probs = model_inference_wrapper.get_corrected_probabilities(text=inferenceData.inputText, corrected_labels=inferenceData.finalLabels, deployment_platform="jira") logger.info(f"CORRECT PROBS IN JIRA UPDATE INFERENCE - {corrected_probs}") if(corrected_probs): @@ -428,7 +561,7 @@ async def jira_inference(request:Request, inferenceData:JiraInferenceRequest): else: # Create Inference Scenario # Call Inference - predicted_hierarchy, probabilities = inference_obj.inference(inferenceData.inputText, deployment_platform="jira") + predicted_hierarchy, probabilities = model_inference_wrapper.inference(inferenceData.inputText, deployment_platform="jira") logger.info(f"JIRA PREDICTED HIERARCHY - {predicted_hierarchy}") logger.info(f"JIRA PROBABILITIES - {probabilities}") diff --git a/model-inference/s3_ferry.py b/model-inference/s3_ferry.py index 54b1d46d..d4487788 100644 --- a/model-inference/s3_ferry.py +++ b/model-inference/s3_ferry.py @@ -1,21 +1,25 @@ import requests from utils import get_s3_payload +from loguru import logger +from constants import INFERENCE_LOGS_PATH + +logger.add(sink=INFERENCE_LOGS_PATH) class S3Ferry: def __init__(self, url): self.url = url - def transfer_file(self, destinationFilePath, destinationStorageType, sourceFilePath, sourceStorageType): - print("Transfer File Method Calling") - print(f"Destination Path :{destinationFilePath}", - f"Destination Storage :{destinationStorageType}", - f"Source File Path :{sourceFilePath}", - f"Source Storage Type :{sourceStorageType}", - sep="\n" - ) - payload = get_s3_payload(destinationFilePath, destinationStorageType, sourceFilePath, sourceStorageType) - print(payload) - print(f"url : {self.url}") + def transfer_file(self, destination_file_path, destination_storage_type, source_file_path, source_storage_type): + + logger.info("Transfer File Method Calling") + logger.info(f"Destination Path :{destination_file_path}") + logger.info(f"Destination Storage :{destination_storage_type}") + logger.info(f"Source File Path :{source_file_path}") + logger.info(f"Source Storage Type :{source_storage_type}") + + payload = get_s3_payload(destination_file_path, destination_storage_type, source_file_path, source_storage_type) + logger.info(payload) + logger.info(f"url : {self.url}") response = requests.post(self.url, json=payload) print(response) return response diff --git a/model-inference/test_inference_wrapper.py b/model-inference/test_inference_wrapper.py new file mode 100644 index 00000000..5ac4247b --- /dev/null +++ b/model-inference/test_inference_wrapper.py @@ -0,0 +1,81 @@ +from typing import List, Dict +from inference_pipeline import InferencePipeline +from loguru import logger +from constants import INFERENCE_LOGS_PATH +import json +import os + +logger.add(sink=INFERENCE_LOGS_PATH) + + +class TestInferenceWrapper: + def __init__(self) -> None: + self.model_dictionary: Dict[int, InferencePipeline] = {} + + def load_model(self, model_id: int, model_path: str, best_performing_model: str, class_hierarchy: list) -> bool: + try: + new_model = InferencePipeline(model_path, best_performing_model, class_hierarchy) + self.model_dictionary[model_id] = new_model + return True + except Exception as e: + logger.info(f"Failed to instantiate the TEST Inference Pipeline. Reason: {e}") + raise Exception(f"Failed to instantiate the TEST Inference Pipeline. Reason: {e}") + + def inference(self, text: str, model_id: int): + try: + if not self.model_dictionary: + self.load_models_from_metadata() + + if model_id in self.model_dictionary: + predicted_labels = None + probabilities = None + model = self.model_dictionary[model_id] + predicted_labels, probabilities = model.predict(text) + return predicted_labels, probabilities + else: + raise Exception(f"Model with ID {model_id} not found") + except Exception as e: + raise Exception(f"Failed to call the inference. Reason: {e}") + + def stop_model(self, model_id: int): + if model_id in self.model_dictionary: + del self.model_dictionary[model_id] + + try: + meta_data_save_location = '/shared/models/testing/test_inference_metadata.json' + + + if os.path.exists(meta_data_save_location): + with open(meta_data_save_location, 'r') as json_file: + metadata_array = json.load(json_file) + + updated_metadata_array = [metadata for metadata in metadata_array if metadata["model_id"] != model_id] + + with open(meta_data_save_location, 'w') as json_file: + json.dump(updated_metadata_array, json_file, indent=4) + + except Exception as e: + raise Exception(f"Failed to remove model metadata from JSON file. Reason: {e}") + + + def load_models_from_metadata(self): + try: + meta_data_save_location = '/shared/models/testing/test_inference_metadata.json' + + if os.path.exists(meta_data_save_location): + + with open(meta_data_save_location, 'r') as json_file: + metadata_array = json.load(json_file) + + for metadata in metadata_array: + model_id = metadata["model_id"] + model_path = metadata["model_path"] + best_performing_model = metadata["best_model"] + class_hierarchy = metadata["class_hierarchy"] + + self.load_model(model_id, model_path, best_performing_model, class_hierarchy) + else: + raise Exception("Unable to find test models meta data file : No active test models exists") + + except Exception as e: + raise Exception(f"Failed to load models from metadata. Reason: {e}") \ No newline at end of file diff --git a/model_trainer/constants.py b/model_trainer/constants.py index b682c910..c23bf22a 100644 --- a/model_trainer/constants.py +++ b/model_trainer/constants.py @@ -17,6 +17,8 @@ JIRA_DEPLOYMENT_ENDPOINT = "http://172.25.0.7:8003/classifier/datamodel/deployment/jira/update" +TEST_DEPLOYMENT_ENDPOINT = "http://172.25.0.7:8003/classifier/datamodel/deployment/test/update" + TRAINING_LOGS_PATH = "/app/model_trainer/training_logs.log" MODEL_RESULTS_PATH = "/shared/model_trainer/results" #stored in the shared folder which is connected to s3-ferry @@ -70,3 +72,5 @@ OUTLOOK = "outlook" JIRA="jira" + +TESTING="testing" diff --git a/model_trainer/model_trainer.py b/model_trainer/model_trainer.py index 78ce141f..b77b741c 100644 --- a/model_trainer/model_trainer.py +++ b/model_trainer/model_trainer.py @@ -7,13 +7,13 @@ import shutil from datetime import datetime from s3_ferry import S3Ferry -from constants import GET_MODEL_METADATA_ENDPOINT, OUTLOOK_DEPLOYMENT_ENDPOINT, JIRA_DEPLOYMENT_ENDPOINT, UPDATE_MODEL_TRAINING_STATUS_ENDPOINT, CREATE_TRAINING_PROGRESS_SESSION_ENDPOINT, UPDATE_TRAINING_PROGRESS_SESSION_ENDPOINT, TRAINING_LOGS_PATH, MODEL_RESULTS_PATH, \ +from constants import GET_MODEL_METADATA_ENDPOINT, OUTLOOK_DEPLOYMENT_ENDPOINT, JIRA_DEPLOYMENT_ENDPOINT, TEST_DEPLOYMENT_ENDPOINT ,UPDATE_MODEL_TRAINING_STATUS_ENDPOINT, CREATE_TRAINING_PROGRESS_SESSION_ENDPOINT, UPDATE_TRAINING_PROGRESS_SESSION_ENDPOINT, TRAINING_LOGS_PATH, MODEL_RESULTS_PATH, \ LOCAL_BASEMODEL_TRAINED_LAYERS_SAVE_PATH,LOCAL_CLASSIFICATION_LAYER_SAVE_PATH, \ LOCAL_LABEL_ENCODER_SAVE_PATH, S3_FERRY_MODEL_STORAGE_PATH, MODEL_TRAINING_IN_PROGRESS, MODEL_TRAINING_SUCCESSFUL, \ INITIATING_TRAINING_PROGRESS_STATUS, TRAINING_IN_PROGRESS_PROGRESS_STATUS, DEPLOYING_MODEL_PROGRESS_STATUS, \ INITIATING_TRAINING_PROGRESS_MESSAGE, TRAINING_IN_PROGRESS_PROGRESS_MESSAGE, DEPLOYING_MODEL_PROGRESS_MESSAGE, \ INITIATING_TRAINING_PROGRESS_PERCENTAGE, TRAINING_IN_PROGRESS_PROGRESS_PERCENTAGE, DEPLOYING_MODEL_PROGRESS_PERCENTAGE, \ - OUTLOOK, JIRA + OUTLOOK, JIRA, TESTING from loguru import logger logger.add(sink=TRAINING_LOGS_PATH) @@ -209,6 +209,10 @@ def deploy_model(self, best_model_name, progress_session_id): deployment_url = OUTLOOK_DEPLOYMENT_ENDPOINT + elif self.current_deployment_platform == TESTING: + + deployment_url = TEST_DEPLOYMENT_ENDPOINT + else: logger.info(f"UNRECOGNIZED DEPLOYMENT PLATFORM - {self.current_deployment_platform}") diff --git a/test_inference/Dockerfile b/test_inference/Dockerfile deleted file mode 100644 index 856367c3..00000000 --- a/test_inference/Dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -FROM python:3.9-slim -RUN addgroup --system appuser && adduser --system --ingroup appuser appuser -WORKDIR /app -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt - -COPY constants.py . -COPY s3_ferry.py . -COPY test_inference_api.py . -COPY test_inference_wrapper.py . -COPY test_inference.py . -COPY utils.py . - -RUN mkdir -p /shared && chown appuser:appuser /shared && chmod 770 /shared -RUN chown -R appuser:appuser /app -EXPOSE 8010 -USER appuser - -CMD ["uvicorn", "test_inference_api:app", "--host", "0.0.0.0", "--port", "8010"] \ No newline at end of file diff --git a/test_inference/constants.py b/test_inference/constants.py deleted file mode 100644 index c1249b8c..00000000 --- a/test_inference/constants.py +++ /dev/null @@ -1,20 +0,0 @@ -from pydantic import BaseModel - -class TestDeploymentRequest(BaseModel): - replacementModelId:int - bestBaseModel:str - -class TestInferenceRequest(BaseModel): - modelId:int - text:str - -class DeleteTestRequest(BaseModel): - deleteModelId:int - -S3_DOWNLOAD_FAILED = { - "upload_status": 500, - "operation_successful": False, - "saved_file_path": None, - "reason": "Failed to download from S3" -} - diff --git a/test_inference/docker-compose.yml b/test_inference/docker-compose.yml deleted file mode 100644 index 6805db11..00000000 --- a/test_inference/docker-compose.yml +++ /dev/null @@ -1,47 +0,0 @@ -version: '3.8' - -services: - init: - image: busybox - command: ["sh", "-c", "chmod -R 777 /shared"] - volumes: - - shared-volume:/shared - - receiver: - build: - context: . - dockerfile: Dockerfile - container_name: file-receiver - volumes: - - shared-volume:/shared - environment: - - UPLOAD_DIRECTORY=/shared - - JIRA_MODEL_DOWNLOAD_DIRECTORY=/shared/models/jira - - OUTLOOK_MODEL_DOWNLOAD_DIRECTORY=/shared/models/outlook - ports: - - "8010:8010" - depends_on: - - init - - api: - image: s3-ferry:latest - container_name: s3-ferry - volumes: - - shared-volume:/shared - env_file: - - config.env - environment: - - S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID} - - S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY} - ports: - - "3000:3000" - depends_on: - - receiver - - init - -volumes: - shared-volume: - -networks: - default: - driver: bridge diff --git a/test_inference/requirements.txt b/test_inference/requirements.txt deleted file mode 100644 index 198d4b35..00000000 --- a/test_inference/requirements.txt +++ /dev/null @@ -1,32 +0,0 @@ -fastapi==0.111.1 -fastapi-cli==0.0.4 -httpx==0.27.0 -huggingface-hub==0.24.2 -numpy==1.26.4 -pydantic==2.8.2 -pydantic_core==2.20.1 -Pygments==2.18.0 -python-dotenv==1.0.1 -python-multipart==0.0.9 -PyYAML==6.0.1 -regex==2024.7.24 -requests==2.32.3 -rich==13.7.1 -safetensors==0.4.3 -scikit-learn==0.24.2 -sentencepiece==0.2.0 -setuptools==69.5.1 -shellingham==1.5.4 -sniffio==1.3.1 -starlette==0.37.2 -sympy==1.13.1 -tokenizers==0.19.1 -torch==2.4.0 -torchaudio==2.4.0 -torchvision==0.19.0 -tqdm==4.66.4 -transformers==4.43.3 -typer==0.12.3 -typing_extensions==4.12.2 -urllib3==2.2.2 -uvicorn==0.30.3 \ No newline at end of file diff --git a/test_inference/s3_ferry.py b/test_inference/s3_ferry.py deleted file mode 100644 index 54b1d46d..00000000 --- a/test_inference/s3_ferry.py +++ /dev/null @@ -1,21 +0,0 @@ -import requests -from utils import get_s3_payload - -class S3Ferry: - def __init__(self, url): - self.url = url - - def transfer_file(self, destinationFilePath, destinationStorageType, sourceFilePath, sourceStorageType): - print("Transfer File Method Calling") - print(f"Destination Path :{destinationFilePath}", - f"Destination Storage :{destinationStorageType}", - f"Source File Path :{sourceFilePath}", - f"Source Storage Type :{sourceStorageType}", - sep="\n" - ) - payload = get_s3_payload(destinationFilePath, destinationStorageType, sourceFilePath, sourceStorageType) - print(payload) - print(f"url : {self.url}") - response = requests.post(self.url, json=payload) - print(response) - return response diff --git a/test_inference/test_inference.py b/test_inference/test_inference.py deleted file mode 100644 index 2773679b..00000000 --- a/test_inference/test_inference.py +++ /dev/null @@ -1,24 +0,0 @@ -import requests -import os - -OUTLOOK_ACCESS_TOKEN_API_URL=os.getenv("OUTLOOK_ACCESS_TOKEN_API_URL") - -class TestModelInference: - def __init__(self): - pass - - def get_class_hierarchy_by_model_id(self, model_id): - try: - outlook_access_token_url = OUTLOOK_ACCESS_TOKEN_API_URL - response = requests.post(outlook_access_token_url, json={"modelId": model_id}) - response.raise_for_status() - data = response.json() - - class_hierarchy = data["class_hierarchy"] - return class_hierarchy - except requests.exceptions.RequestException as e: - raise Exception(f"Failed to retrieve the class hierarchy Reason: {e}") - - - - diff --git a/test_inference/test_inference_api.py b/test_inference/test_inference_api.py deleted file mode 100644 index fc452b89..00000000 --- a/test_inference/test_inference_api.py +++ /dev/null @@ -1,114 +0,0 @@ -from fastapi import FastAPI,HTTPException, Request, BackgroundTasks -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse -import os -from s3_ferry import S3Ferry -from utils import unzip_file, calculate_average_predicted_class_probability, get_inference_success_payload, delete_folder -from constants import S3_DOWNLOAD_FAILED, TestDeploymentRequest, TestInferenceRequest, DeleteTestRequest -from test_inference_wrapper import TestInferenceWrapper -from test_inference import TestModelInference - -app = FastAPI() -testModelInference = TestModelInference() - -app.add_middleware( - CORSMiddleware, - allow_origins = ["*"], - allow_credentials = True, - allow_methods = ["GET", "POST"], - allow_headers = ["*"], -) - -inference_obj = TestInferenceWrapper() - -S3_FERRY_URL = os.getenv("S3_FERRY_URL") -s3_ferry = S3Ferry(S3_FERRY_URL) -RUUTER_PRIVATE_URL = os.getenv("RUUTER_PRIVATE_URL") -TEST_MODEL_DOWNLOAD_DIRECTORY = os.getenv("JIRA_MODEL_DOWNLOAD_DIRECTORY", "/shared/models/test") - -if not os.path.exists(TEST_MODEL_DOWNLOAD_DIRECTORY): - os.makedirs(TEST_MODEL_DOWNLOAD_DIRECTORY) - - -@app.post("/classifier/datamodel/deployment/test/update") -async def download_test_model(request: Request, modelData:TestDeploymentRequest, backgroundTasks: BackgroundTasks): - - saveLocation = f"/models/{modelData.replacementModelId}/{modelData.replacementModelId}.zip" - - try: - local_file_name = f"{modelData.replacementModelId}.zip" - local_file_path = f"/models/test/{local_file_name}" - - # 1. Download the new Model - response = s3_ferry.transfer_file(local_file_path, "FS", saveLocation, "S3") - if response.status_code != 201: - raise HTTPException(status_code = 500, detail = S3_DOWNLOAD_FAILED) - - zip_file_path = os.path.join("..", "shared/models/test", local_file_name) - extract_file_path = os.path.join("..", "shared/models/test") - - # 2. Unzip Model Content - unzip_file(zip_path=zip_file_path, extract_to=extract_file_path) - - backgroundTasks.add_task(os.remove, zip_file_path) - - # 3. Instantiate Inference Model - class_hierarchy = testModelInference.get_class_hierarchy_by_model_id(modelData.replacementModelId) - if(class_hierarchy): - - model_path = f"shared/models/test/{modelData.replacementModelId}" - best_model = modelData.bestBaseModel - model_initiate = inference_obj.model_initiate(model_id=modelData.replacementModelId, model_path=model_path, best_performing_model=best_model, class_hierarchy=class_hierarchy) - - if(model_initiate): - return JSONResponse(status_code=200, content={"replacementStatus": 200}) - else: - raise HTTPException(status_code = 500, detail = "Failed to initiate inference object") - else: - raise HTTPException(status_code = 500, detail = "Error in obtaining the class hierarchy") - - except Exception as e: - raise HTTPException(status_code = 500, detail=str(e)) - - -@app.post("/classifier/datamodel/deployment/test/delete") -async def delete_folder_content(request:Request, modelData:DeleteTestRequest): - try: - folder_path = os.path.join("..", "shared", "models", "test", {modelData.deleteModelId}) - delete_folder(folder_path) - - # Stop the model - inference_obj.stop_model(model_id=modelData.deleteModelId) - - delete_success = {"message" : "Model Deleted Successfully!"} - return JSONResponse(status_code = 200, content = delete_success) - - except Exception as e: - raise HTTPException(status_code = 500, detail=str(e)) - - - -@app.post("/classifier/testmodel/test-data") -async def test_inference(request:Request, inferenceData:TestInferenceRequest): - try: - - # Call Inference - predicted_hierarchy, probabilities = inference_obj.inference(model_id=inferenceData.modelId, text=inferenceData.text) - - if (probabilities and predicted_hierarchy): - - # Calculate Average Predicted Class Probability - average_probability = calculate_average_predicted_class_probability(probabilities) - - # Build request payload for inference/create endpoint - inference_succcess_payload = get_inference_success_payload(predictedClasses=predicted_hierarchy, averageConfidence=average_probability, predictedProbabilities=probabilities) - - return JSONResponse(status_code=200, content={inference_succcess_payload}) - - - else: - raise HTTPException(status_code = 500, detail="Failed to call inference") - - - except Exception as e: - raise HTTPException(status_code = 500, detail=str(e)) \ No newline at end of file diff --git a/test_inference/test_inference_wrapper.py b/test_inference/test_inference_wrapper.py deleted file mode 100644 index 92dfe0f9..00000000 --- a/test_inference/test_inference_wrapper.py +++ /dev/null @@ -1,41 +0,0 @@ -from typing import List, Dict - -class Inference: - def __init__(self) -> None: - pass - - def predict(text:str): - pass - - def user_corrected_probabilities(text:str, corrected_labels:List[str]): - pass - - -class TestInferenceWrapper: - def __init__(self) -> None: - self.model_dictionary: Dict[int, Inference] = {} - - def model_initiate(self, model_id: int, model_path: str, best_performing_model: str, class_hierarchy: list) -> bool: - try: - new_model = Inference(model_path, best_performing_model, class_hierarchy) - self.model_dictionary[model_id] = new_model - return True - except Exception as e: - raise Exception(f"Failed to instantiate the Inference Pipeline. Reason: {e}") - - def inference(self, text: str, model_id: int): - try: - if model_id in self.model_dictionary: - predicted_labels = None - probabilities = None - model = self.model_dictionary[model_id] - predicted_labels, probabilities = model.predict(text) - return predicted_labels, probabilities - else: - raise Exception(f"Model with ID {model_id} not found") - except Exception as e: - raise Exception(f"Failed to call the inference. Reason: {e}") - - def stop_model(self, model_id: int) -> None: - if model_id in self.models: - del self.models[model_id] diff --git a/test_inference/utils.py b/test_inference/utils.py deleted file mode 100644 index d1dd330f..00000000 --- a/test_inference/utils.py +++ /dev/null @@ -1,42 +0,0 @@ -import zipfile -from typing import List -import os -import shutil - -def calculate_average_predicted_class_probability(class_probabilities:List[float]): - - total_probability = sum(class_probabilities) - average_probability = total_probability / len(class_probabilities) - - return average_probability - -def get_s3_payload(destinationFilePath:str, destinationStorageType:str, sourceFilePath:str, sourceStorageType:str): - S3_FERRY_PAYLOAD = { - "destinationFilePath": destinationFilePath, - "destinationStorageType": destinationStorageType, - "sourceFilePath": sourceFilePath, - "sourceStorageType": sourceStorageType - } - return S3_FERRY_PAYLOAD - -def get_inference_success_payload(predictedClasses:List[str], averageConfidence:float, predictedProbabilities:List[float] ): - INFERENCE_SUCCESS_PAYLOAD = { - "predictedClasses":predictedClasses, - "averageConfidence":averageConfidence, - "predictedProbabilities": predictedProbabilities -} - - return INFERENCE_SUCCESS_PAYLOAD - -def unzip_file(zip_path, extract_to): - with zipfile.ZipFile(zip_path, 'r') as zip_ref: - zip_ref.extractall(extract_to) - -def delete_folder(folder_path: str): - try: - if os.path.isdir(folder_path): - shutil.rmtree(folder_path) - else: - raise FileNotFoundError(f"The path {folder_path} is not a directory.") - except Exception as e: - raise Exception(f"Failed to delete the folder {folder_path}. Reason: {e}") \ No newline at end of file From 76a53de0cb12d443f8ee420504ec44e331d7e75e Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 26 Aug 2024 18:38:55 +0530 Subject: [PATCH 543/582] dataset validator and processor not invoking temp bug fix --- DSL/CronManager/script/data_processor_exec.sh | 120 ++++++------------ DSL/CronManager/script/data_validator_exec.sh | 119 ++++++----------- 2 files changed, 74 insertions(+), 165 deletions(-) diff --git a/DSL/CronManager/script/data_processor_exec.sh b/DSL/CronManager/script/data_processor_exec.sh index 12a8feb4..d6b87644 100755 --- a/DSL/CronManager/script/data_processor_exec.sh +++ b/DSL/CronManager/script/data_processor_exec.sh @@ -1,93 +1,47 @@ #!/bin/bash -VENV_DIR="/home/cronmanager/clsenv" -REQUIREMENTS="dataset_processor/requirements.txt" -PYTHON_SCRIPT="dataset_processor/invoke_dataset_processor.py" +echo "Started Shell Script to process" +# Ensure required environment variables are set +if [ -z "$dgId" ] || [ -z "$newDgId" ] || [ -z "$cookie" ] || [ -z "$updateType" ] || [ -z "$savedFilePath" ] || [ -z "$patchPayload" ] || [ -z "$sessionId" ]; then + echo "One or more environment variables are missing." + echo "Please set dgId, newDgId, cookie, updateType, savedFilePath, patchPayload, and sessionId." + exit 1 +fi -is_package_installed() { - package=$1 - pip show "$package" > /dev/null 2>&1 +# Construct the payload using here document +payload=$(cat < /dev/null 2>&1 +# Construct the payload using here document +payload=$(cat < Date: Mon, 26 Aug 2024 18:43:10 +0530 Subject: [PATCH 544/582] json download none and null bug fix --- file-handler/file_handler_api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/file-handler/file_handler_api.py b/file-handler/file_handler_api.py index 3bf66cf4..7a24abff 100644 --- a/file-handler/file_handler_api.py +++ b/file-handler/file_handler_api.py @@ -200,7 +200,8 @@ async def download_and_convert(request: Request, saveLocation:str, background_ta json_file_path = os.path.join(JSON_FILE_DIRECTORY, f"{local_file_name}") with open(f"{json_file_path}", 'r') as json_file: - json_data = json.load(json_file) + json_str = json_file.read().replace('NaN', 'null') + json_data = json.loads(json_str) background_tasks.add_task(os.remove, json_file_path) From 66c039d505a00438ad81a33c39fc8dd59f860c28 Mon Sep 17 00:00:00 2001 From: Thirunayan22 Date: Mon, 26 Aug 2024 18:55:23 +0530 Subject: [PATCH 545/582] fixed test model re-training issue --- .../POST/classifier/testmodel/test-data.yml | 13 ++- anonymizer/anonymizer_api.py | 2 +- model-inference/model_inference_api.py | 88 +++++++++++++++++-- model-inference/s3_ferry.py | 3 +- model-inference/test_inference_wrapper.py | 4 +- model-inference/utils.py | 12 ++- model_trainer/constants.py | 2 +- 7 files changed, 102 insertions(+), 22 deletions(-) diff --git a/DSL/Ruuter.private/DSL/POST/classifier/testmodel/test-data.yml b/DSL/Ruuter.private/DSL/POST/classifier/testmodel/test-data.yml index 99d88a57..171c13ef 100644 --- a/DSL/Ruuter.private/DSL/POST/classifier/testmodel/test-data.yml +++ b/DSL/Ruuter.private/DSL/POST/classifier/testmodel/test-data.yml @@ -60,17 +60,14 @@ check_data_model_exist: next: return_model_not_found send_data_to_predict: - call: reflect.mock + call: http.post args: - url: "[#CLASSIFIER_ANONYMIZER]/anonymize" + url: "[#CLASSIFIER_MODEL_INFERENCE]/classifier/deployment/testing/inference" + headers: + cookie: ${cookie} body: - id: ${model_id} + modelId: ${model_id} text: ${text} - response: - statusCodeValue: 200 - predictedClasses: [ "Police","Special Agency","External","Reports","Annual Report" ] - averageConfidence: 89.8 - predictedProbabilities: [ 98,82,91,90,88 ] result: res_predict next: check_data_predict_status diff --git a/anonymizer/anonymizer_api.py b/anonymizer/anonymizer_api.py index 569b79f6..05f45b34 100644 --- a/anonymizer/anonymizer_api.py +++ b/anonymizer/anonymizer_api.py @@ -79,7 +79,7 @@ def anonymizer_functions(payload): print(f"Output payload : {output_payload}") response = requests.post(OUTLOOK_INFERENCE_ENDPOINT, json=output_payload, headers=headers) else: - print("Playform not recognized... ") + print("Platform not recognized... ") response = None print(f"Response from {platform} : {response}") diff --git a/model-inference/model_inference_api.py b/model-inference/model_inference_api.py index 9db67f1b..b1729d23 100644 --- a/model-inference/model_inference_api.py +++ b/model-inference/model_inference_api.py @@ -3,10 +3,10 @@ from fastapi.responses import JSONResponse import os from s3_ferry import S3Ferry -from utils import unzip_file, clear_folder_contents, calculate_average_predicted_class_probability, get_inference_create_payload, get_inference_update_payload +from utils import unzip_file, clear_folder_contents, calculate_average_predicted_class_probability, get_inference_create_payload, get_inference_update_payload, get_test_inference_success_payload from constants import S3_DOWNLOAD_FAILED, INFERENCE_LOGS_PATH, JiraInferenceRequest, \ OutlookInferenceRequest, UpdateRequest, OUTLOOK_MODELS_FOLDER_PATH, JIRA_MODELS_FOLDER_PATH,\ - SHARED_MODELS_ROOT_FOLDER + SHARED_MODELS_ROOT_FOLDER, TestInferenceRequest, DeleteTestRequest from inference_wrapper import InferenceWrapper from test_inference_wrapper import TestInferenceWrapper from model_inference import ModelInference @@ -47,6 +47,9 @@ if not os.path.exists(TEST_MODEL_DOWNLOAD_ROOT_DIRECTORY): os.makedirs(TEST_MODEL_DOWNLOAD_ROOT_DIRECTORY) + logger.info("GIVING PERMISSIONS") + os.chmod(TEST_MODEL_DOWNLOAD_ROOT_DIRECTORY,mode=0o777) + @app.post("/classifier/datamodel/deployment/outlook/update") async def download_outlook_model(request: Request, model_data:UpdateRequest): @@ -245,7 +248,7 @@ async def download_jira_model(request: Request, model_data:UpdateRequest): except Exception as e: raise HTTPException(status_code = 500, detail=str(e)) -@app.post("/classifier/datamodel/deployment/test/update") +@app.post("/classifier/datamodel/deployment/testing/update") async def download_test_model(request: Request, model_data:UpdateRequest): save_location = f"/models/{model_data.modelId}/{model_data.modelId}.zip" @@ -272,6 +275,8 @@ async def download_test_model(request: Request, model_data:UpdateRequest): if not os.path.exists(test_models_folder_path): logger.info("CREATING FOLDER INSIDE MODEL EXIST") os.makedirs(test_models_folder_path) + logger.info("GIVING PERMISSIONS") + os.chmod(test_models_folder_path,mode=0o777) if os.path.exists(test_models_folder_path): logger.info("CLEARING TEST MODEL CONTAINERS") @@ -283,15 +288,21 @@ async def download_test_model(request: Request, model_data:UpdateRequest): source_file_path=save_location, source_storage_type="S3") - logger.info(f"S3 Ferry Response - {response.json()}") + logger.info("ZIP FILE DOWNLOADED") - if response.status_code != 201: + if response.status_code!=201: raise HTTPException(status_code = 500, detail = S3_DOWNLOAD_FAILED) + + zip_file_path = f"{test_models_folder_path}/{local_file_name}" + extract_file_path = test_models_folder_path - + + logger.info(f"TESTING LOG FILE PATH - {zip_file_path}") + logger.info(f"EXTRACT FILE PATH - {extract_file_path}") + # 3. Unzip Model Content unzip_file(zip_path=zip_file_path, extract_to=extract_file_path) @@ -347,8 +358,9 @@ async def download_test_model(request: Request, model_data:UpdateRequest): with open(meta_data_save_location, 'w') as json_file: json.dump(existing_data, json_file, indent=4) - model_initiate = test_inference_wrapper.load_model(model_path=model_path, best_performing_model=best_model, - deployment_platform="jira", class_hierarchy=class_hierarchy, + model_initiate = test_inference_wrapper.load_model(model_path=model_path, + best_performing_model=best_model, + class_hierarchy=class_hierarchy, model_id=model_data.modelId) logger.info(f"TEST MODEL INITITATE - {model_initiate}") @@ -593,3 +605,63 @@ async def jira_inference(request:Request, inferenceData:JiraInferenceRequest): except Exception as e: raise HTTPException(status_code = 500, detail=str(e)) + + +@app.post("/classifier/deployment/testing/inference") +async def test_inference(request:Request, inference_data:TestInferenceRequest): + try: + + # Call Inference + + logger.info("ENTERING INTO TESTING INFERENCE") + cookie = request.cookies.get("customJwtCookie") + + logger.info(f"COOKIE - {cookie}") + await model_inference.authenticate_user(f'customJwtCookie={cookie}') + + + predicted_hierarchy, probabilities = test_inference_wrapper.inference(model_id=inference_data.modelId, text=inference_data.text) + logger.info(f"PREDICTED HIERARCHY - {predicted_hierarchy}") + logger.info(f"PROBABILITIEs - {probabilities}") + + + if (probabilities and predicted_hierarchy): + + # Calculate Average Predicted Class Probability + average_probability = calculate_average_predicted_class_probability(probabilities) + + logger.info(f"AVERAGE PROBABILITY - {average_probability}") + # Build request payload for inference/create endpoint + inference_success_payload = get_test_inference_success_payload(predicted_classes=predicted_hierarchy, average_confidence=average_probability, predicted_probabilities=probabilities) + + logger.info(f"INFERENCE PAYLOAD - {inference_success_payload}") + return JSONResponse(status_code=200, content=inference_success_payload) + + else: + + logger.info("PREDICTION FAILED IN TESTING") + raise HTTPException(status_code = 500, detail="Failed to call inference") + + + except Exception as e: + + logger.info(f"crash happened in model inference testing - {e}") + raise RuntimeError(f"crash happened in model inference testing - {e}") + + +# @app.post("/classifier/datamodel/deployment/test/delete") +# async def delete_folder_content(request:Request, modelData:DeleteTestRequest): +# try: +# folder_path = os.path.join("..", "shared", "models", "test", {modelData.deleteModelId}) +# delete_folder(folder_path) + +# # Stop the model +# inference_obj.stop_model(model_id=modelData.deleteModelId) + +# delete_success = {"message" : "Model Deleted Successfully!"} +# return JSONResponse(status_code = 200, content = delete_success) + +# except Exception as e: +# raise HTTPException(status_code = 500, detail=str(e)) + + diff --git a/model-inference/s3_ferry.py b/model-inference/s3_ferry.py index d4487788..cf283c05 100644 --- a/model-inference/s3_ferry.py +++ b/model-inference/s3_ferry.py @@ -21,5 +21,6 @@ def transfer_file(self, destination_file_path, destination_storage_type, source_ logger.info(payload) logger.info(f"url : {self.url}") response = requests.post(self.url, json=payload) - print(response) + logger.info(f"RESPONSE STATUS CODE INSIDE TRANSFER FILE - {response.status_code}") + return response diff --git a/model-inference/test_inference_wrapper.py b/model-inference/test_inference_wrapper.py index 5ac4247b..a8d0ca4b 100644 --- a/model-inference/test_inference_wrapper.py +++ b/model-inference/test_inference_wrapper.py @@ -14,7 +14,7 @@ def __init__(self) -> None: def load_model(self, model_id: int, model_path: str, best_performing_model: str, class_hierarchy: list) -> bool: try: - new_model = InferencePipeline(model_path, best_performing_model, class_hierarchy) + new_model = InferencePipeline(hierarchy_file=class_hierarchy,model_name=best_performing_model,results_folder=model_path) self.model_dictionary[model_id] = new_model return True except Exception as e: @@ -30,7 +30,7 @@ def inference(self, text: str, model_id: int): predicted_labels = None probabilities = None model = self.model_dictionary[model_id] - predicted_labels, probabilities = model.predict(text) + predicted_labels, probabilities = model.predict_class(text_input=text) return predicted_labels, probabilities else: raise Exception(f"Model with ID {model_id} not found") diff --git a/model-inference/utils.py b/model-inference/utils.py index 379eb3fa..11ff4c30 100644 --- a/model-inference/utils.py +++ b/model-inference/utils.py @@ -60,5 +60,15 @@ def calculate_average_predicted_class_probability(class_probabilities): total_probability = sum(class_probabilities) average_probability = total_probability / len(class_probabilities) - return average_probability + return average_probability + +def get_test_inference_success_payload(predicted_classes:list, average_confidence:float, predicted_probabilities:list ): + + TEST_INFERENCE_SUCCESS_PAYLOAD = { + "predictedClasses":predicted_classes, + "averageConfidence":average_confidence, + "predictedProbabilities": predicted_probabilities + } + + return TEST_INFERENCE_SUCCESS_PAYLOAD \ No newline at end of file diff --git a/model_trainer/constants.py b/model_trainer/constants.py index c23bf22a..a933b57c 100644 --- a/model_trainer/constants.py +++ b/model_trainer/constants.py @@ -17,7 +17,7 @@ JIRA_DEPLOYMENT_ENDPOINT = "http://172.25.0.7:8003/classifier/datamodel/deployment/jira/update" -TEST_DEPLOYMENT_ENDPOINT = "http://172.25.0.7:8003/classifier/datamodel/deployment/test/update" +TEST_DEPLOYMENT_ENDPOINT = "http://172.25.0.7:8003/classifier/datamodel/deployment/testing/update" TRAINING_LOGS_PATH = "/app/model_trainer/training_logs.log" From 6772b38d7be8423adb28f5cc291addb37ff00f10 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Mon, 26 Aug 2024 22:44:33 +0530 Subject: [PATCH 546/582] Re-Train-And-Last-Train-Models: Implement API's for update last train model and all model which need re-train --- .../return_connected_models_ids.handlebars | 5 + .../get-dataset-group-connected-models.sql | 2 + .../update-data-models-re-training-needed.sql | 4 + ...date-last-train-model-in-dataset-group.sql | 4 + .../update/training/last-train-model.yml | 112 ++++++++++++++ .../datasetgroup/update/training/status.yml | 138 ++++++++++++++++++ 6 files changed, 265 insertions(+) create mode 100644 DSL/DMapper/hbs/return_connected_models_ids.handlebars create mode 100644 DSL/Resql/get-dataset-group-connected-models.sql create mode 100644 DSL/Resql/update-data-models-re-training-needed.sql create mode 100644 DSL/Resql/update-last-train-model-in-dataset-group.sql create mode 100644 DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/training/last-train-model.yml create mode 100644 DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/training/status.yml diff --git a/DSL/DMapper/hbs/return_connected_models_ids.handlebars b/DSL/DMapper/hbs/return_connected_models_ids.handlebars new file mode 100644 index 00000000..6f680e58 --- /dev/null +++ b/DSL/DMapper/hbs/return_connected_models_ids.handlebars @@ -0,0 +1,5 @@ +{{#each data}} + {{#if @first}}[{{/if}} + {{~modelId}}{{#unless @last}}, {{/unless~}} + {{#if @last}}]{{/if}} +{{/each}} \ No newline at end of file diff --git a/DSL/Resql/get-dataset-group-connected-models.sql b/DSL/Resql/get-dataset-group-connected-models.sql new file mode 100644 index 00000000..9ed7f83c --- /dev/null +++ b/DSL/Resql/get-dataset-group-connected-models.sql @@ -0,0 +1,2 @@ +SELECT connected_models +FROM dataset_group_metadata WHERE id = :id; \ No newline at end of file diff --git a/DSL/Resql/update-data-models-re-training-needed.sql b/DSL/Resql/update-data-models-re-training-needed.sql new file mode 100644 index 00000000..a0d38043 --- /dev/null +++ b/DSL/Resql/update-data-models-re-training-needed.sql @@ -0,0 +1,4 @@ +UPDATE models_metadata +SET + training_status = 'retraining needed' +WHERE id = ANY (ARRAY[:ids]); \ No newline at end of file diff --git a/DSL/Resql/update-last-train-model-in-dataset-group.sql b/DSL/Resql/update-last-train-model-in-dataset-group.sql new file mode 100644 index 00000000..ed3ec836 --- /dev/null +++ b/DSL/Resql/update-last-train-model-in-dataset-group.sql @@ -0,0 +1,4 @@ +UPDATE dataset_group_metadata +SET + last_model_trained =:last_model_trained +WHERE id = :id; \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/training/last-train-model.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/training/last-train-model.yml new file mode 100644 index 00000000..5f9362c2 --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/training/last-train-model.yml @@ -0,0 +1,112 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'STATUS'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: dgId + type: number + description: "Body field 'dgId'" + - field: lastTrainModel + type: string + description: "Body field 'lastTrainModel'" + +extract_request_data: + assign: + dg_id: ${incoming.body.dgId} + last_train_model: ${incoming.body.lastTrainModel} + next: check_for_request_data + +check_for_request_data: + switch: + - condition: ${dg_id !== null && last_train_model !== null} + next: get_dataset_group_by_id + next: return_incorrect_request + +get_dataset_group_by_id: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-dataset-group-by-id" + body: + id: ${dg_id} + result: res_dataset + next: check_dataset_status + +check_dataset_status: + switch: + - condition: ${200 <= res_dataset.response.statusCodeValue && res_dataset.response.statusCodeValue < 300} + next: check_dataset_exist + next: assign_fail_response + +check_dataset_exist: + switch: + - condition: ${res_dataset.response.body.length>0} + next: update_last_train_model + next: assign_fail_response + +update_last_train_model: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/update-last-train-model-in-dataset-group" + body: + id: ${dg_id} + last_model_trained: ${last_train_model} + result: res_update + next: check_data_model_update_status + +check_data_model_update_status: + switch: + - condition: ${200 <= res_update.response.statusCodeValue && res_update.response.statusCodeValue < 300} + next: assign_success_response + next: assign_fail_response + +assign_success_response: + assign: + format_res: { + dgId: '${dg_id}', + lastTrainModel: '${last_train_model}', + operationSuccessful: true, + } + next: return_ok + +assign_empty_response: + assign: + format_res: { + dgId: '${dg_id}', + lastTrainModel: '${last_train_model}', + operationSuccessful: true, + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + dgId: '${dg_id}', + lastTrainModel: '${last_train_model}', + operationSuccessful: false, + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_incorrect_request: + status: 400 + return: 'Missing Required Fields' + next: end + +return_not_found: + status: 404 + return: "Data Group Not Found" + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/training/status.yml b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/training/status.yml new file mode 100644 index 00000000..fb68cc32 --- /dev/null +++ b/DSL/Ruuter.private/DSL/POST/classifier/datasetgroup/update/training/status.yml @@ -0,0 +1,138 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'STATUS'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: dgId + type: number + description: "Body field 'dgId'" + +extract_request_data: + assign: + dg_id: ${incoming.body.dgId} + next: check_for_request_data + +check_for_request_data: + switch: + - condition: ${dg_id !== null} + next: get_dataset_group + next: return_incorrect_request + +get_dataset_group: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/get-dataset-group-connected-models" + body: + id: ${dg_id} + result: res + next: check_dataset_group_status + +check_dataset_group_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: check_dataset_group_exist + next: assign_fail_response + +check_dataset_group_exist: + switch: + - condition: ${res.response.body.length>0} + next: check_connected_model_exist + next: return_not_found + +check_connected_model_exist: + switch: + - condition: ${res.response.body[0].connectedModels !== null} + next: get_connected_dg_group_model_ids + next: assign_empty_response + +get_connected_dg_group_model_ids: + call: http.post + args: + url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/return_connected_models_ids" + headers: + type: json + body: + data: ${JSON.parse(res.response.body[0].connectedModels.value)} + result: res_models + next: check_connected_dg_group_model_ids_status + +check_connected_dg_group_model_ids_status: + switch: + - condition: ${200 <= res_models.response.statusCodeValue && res_models.response.statusCodeValue < 300} + next: assign_model_ids + next: assign_fail_response + +assign_model_ids: + assign: + model_ids: ${res_models.response.body} + next: update_training_status + +update_training_status: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/update-data-models-re-training-needed" + body: + ids: ${model_ids} + result: res_update + next: check_data_model_update_status + +check_data_model_update_status: + switch: + - condition: ${200 <= res_update.response.statusCodeValue && res_update.response.statusCodeValue < 300} + next: assign_success_response + next: assign_fail_response + +assign_success_response: + assign: + format_res: { + dgId: '${dg_id}', + modelIds: '${model_ids}', + trainingStatus: 'retraining needed', + operationSuccessful: true, + } + next: return_ok + +assign_empty_response: + assign: + format_res: { + dgId: '${dg_id}', + modelIds: '', + trainingStatus: 'retraining needed', + operationSuccessful: true, + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + dgId: '${dg_id}', + modelIds: '', + trainingStatus: 'retraining needed', + operationSuccessful: false, + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_incorrect_request: + status: 400 + return: 'Missing Required Fields' + next: end + +return_not_found: + status: 404 + return: "Data Group Not Found" + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end From 43c3e364976cde35354688ab8c0d114e56d54344 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Mon, 26 Aug 2024 23:14:13 +0530 Subject: [PATCH 547/582] python updates for the status update for model --- dataset-processor/constants.py | 1 + dataset-processor/dataset_processor.py | 23 +++++++++++++++++++++++ docker-compose.yml | 1 + 3 files changed, 25 insertions(+) diff --git a/dataset-processor/constants.py b/dataset-processor/constants.py index af471fb5..1e8c74b0 100644 --- a/dataset-processor/constants.py +++ b/dataset-processor/constants.py @@ -19,6 +19,7 @@ DOWNLOAD_CHUNK_URL = os.getenv("DOWNLOAD_CHUNK_URL") STATUS_UPDATE_URL = os.getenv("STATUS_UPDATE_URL") FILE_HANDLER_COPY_CHUNKS_URL = os.getenv("FILE_HANDLER_COPY_CHUNKS_URL") +DATASET_MODEL_STATUS_UPDATE_URL = os.getenv("DATASET_MODEL_STATUS_UPDATE_URL") # Messages MSG_PROCESS_HANDLER_STARTED = "Process handler started with updateType: {}" diff --git a/dataset-processor/dataset_processor.py b/dataset-processor/dataset_processor.py index 0867d25f..7ebefee5 100644 --- a/dataset-processor/dataset_processor.py +++ b/dataset-processor/dataset_processor.py @@ -339,7 +339,29 @@ def update_preprocess_status(self,dg_id, cookie, processed_data_available, raw_d except requests.exceptions.RequestException as e: print(f"An error occurred: {e}") return None + + def update_dataset_model_status(self,dg_id, cookie): + url = DATASET_MODEL_STATUS_UPDATE_URL + print(url) + headers = { + 'Content-Type': 'application/json', + 'Cookie': cookie + } + data = { + "dgId": dg_id + } + + try: + print(data) + response = requests.post(url, json=data, headers=headers) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + print(f"An error occurred: {e}") + return None + + def process_handler(self, dgId, newDgId, cookie, updateType, savedFilePath, patchPayload, sessionId): print("IN DATASET PROCESSOR PROCESS_HANDLER") @@ -554,6 +576,7 @@ def handle_patch_update(self, dgId, cookie, patchPayload, session_id): if not save_result_delete: return self.generate_response(False, MSG_FAIL) + update_dataset_model_response = self.update_dataset_model_status(dgId, cookie) return self.generate_response(True, MSG_PROCESS_COMPLETE) def get_session_id(self, dgId, cookie): diff --git a/docker-compose.yml b/docker-compose.yml index 4215dbd1..e2a81c99 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -262,6 +262,7 @@ services: - CREATE_PROGRESS_SESSION_URL=http://ruuter-private:8088/classifier/datasetgroup/progress/create - UPDATE_PROGRESS_SESSION_URL=http://ruuter-private:8088/classifier/datasetgroup/progress/update - GET_PROGRESS_SESSIONS_URL=http://ruuter-private:8088/classifier/datasetgroup/progress + - DATASET_MODEL_STATUS_UPDATE_URL=http://ruuter-private:8088/classifier/datasetgroup/update/training/status ports: - "8001:8001" networks: From 7c0d034d37a08a06a87e2ee4dbab534714fa8034 Mon Sep 17 00:00:00 2001 From: Thirunayan22 Date: Tue, 27 Aug 2024 00:00:19 +0530 Subject: [PATCH 548/582] fixed all primary model flows except delete --- model-inference/constants.py | 1 + model-inference/inference_pipeline.py | 2 +- model-inference/model_inference_api.py | 64 +++++++++++++++++++------- model-inference/utils.py | 12 ++++- model_trainer/model_trainer.py | 1 + 5 files changed, 60 insertions(+), 20 deletions(-) diff --git a/model-inference/constants.py b/model-inference/constants.py index 7497d2f0..dd40ec7e 100644 --- a/model-inference/constants.py +++ b/model-inference/constants.py @@ -40,6 +40,7 @@ class UpdateRequest(BaseModel): modelId: int + oldModelId: int replaceDeployment:bool replaceDeploymentPlatform:Optional[str] = None bestBaseModel:str diff --git a/model-inference/inference_pipeline.py b/model-inference/inference_pipeline.py index 394505bf..019f8e77 100644 --- a/model-inference/inference_pipeline.py +++ b/model-inference/inference_pipeline.py @@ -82,7 +82,7 @@ def predict_class(self,text_input): self.base_model.to(self.device) logger.info(f"CLASS HIERARCHY FILE {self.hierarchy_file}") - logger.info(f"INPUTS - {inputs}") + data = self.hierarchy_file diff --git a/model-inference/model_inference_api.py b/model-inference/model_inference_api.py index b1729d23..a894c463 100644 --- a/model-inference/model_inference_api.py +++ b/model-inference/model_inference_api.py @@ -3,7 +3,7 @@ from fastapi.responses import JSONResponse import os from s3_ferry import S3Ferry -from utils import unzip_file, clear_folder_contents, calculate_average_predicted_class_probability, get_inference_create_payload, get_inference_update_payload, get_test_inference_success_payload +from utils import unzip_file, clear_folder_contents, calculate_average_predicted_class_probability, get_inference_create_payload, get_inference_update_payload, get_test_inference_success_payload, delete_folder from constants import S3_DOWNLOAD_FAILED, INFERENCE_LOGS_PATH, JiraInferenceRequest, \ OutlookInferenceRequest, UpdateRequest, OUTLOOK_MODELS_FOLDER_PATH, JIRA_MODELS_FOLDER_PATH,\ SHARED_MODELS_ROOT_FOLDER, TestInferenceRequest, DeleteTestRequest @@ -96,11 +96,25 @@ async def download_outlook_model(request: Request, model_data:UpdateRequest): os.remove(zip_file_path) # 3. Replace the content in other folder if it a replacement if(model_data.replaceDeployment and model_data.replaceDeploymentPlatform!="undeployed" and model_data.updateType!="retrain"): - - replace_deployment_folder_path = f"{shared_models_root_folder}/{model_data.replaceDeploymentPlatform}" - logger.info(f"REPLACE DEPLOYMENT FOLDER PATH - {replace_deployment_folder_path}") - clear_folder_contents(replace_deployment_folder_path) - model_inference_wrapper.stop_model(deployment_platform=model_data.replaceDeploymentPlatform) + + logger.info("INSIDE REPLACE DEPLOYMENT") + # Special Scenario - Handle swapping from Testing + if(model_data.replaceDeploymentPlatform =="testing"): + + # Clear the testing model folder + logger.info("DELETING OLD MODEL FROM TESTING") + folder_path = f"{TEST_MODEL_DOWNLOAD_ROOT_DIRECTORY}/{model_data.oldModelId}" + delete_folder(folder_path) + + # Stop the testing model + test_inference_wrapper.stop_model(model_id=model_data.oldModelId) + + else: + + replace_deployment_folder_path = f"{shared_models_root_folder}/{model_data.replaceDeploymentPlatform}" + logger.info(f"REPLACE DEPLOYMENT FOLDER PATH - {replace_deployment_folder_path}") + clear_folder_contents(replace_deployment_folder_path) + model_inference_wrapper.stop_model(deployment_platform=model_data.replaceDeploymentPlatform) # 4. Instantiate Inference Model model_path = "/shared/models/outlook" @@ -190,6 +204,21 @@ async def download_jira_model(request: Request, model_data:UpdateRequest): if(model_data.replaceDeployment and model_data.replaceDeploymentPlatform!="undeployed" and model_data.updateType!="retrain"): + if(model_data.replaceDeploymentPlatform =="testing"): + # Clear the testing model folder + logger.info("DELETING OLD MODEL FROM TESTING") + folder_path = f"{TEST_MODEL_DOWNLOAD_ROOT_DIRECTORY}/{model_data.oldModelId}" + delete_folder(folder_path) + + # Stop the testing model + test_inference_wrapper.stop_model(model_id=model_data.oldModelId) + + else: + replace_deployment_folder_path = f"{shared_models_root_folder}/{model_data.replaceDeploymentPlatform}" + logger.info(f"REPLACE DEPLOYMENT FOLDER PATH - {replace_deployment_folder_path}") + clear_folder_contents(replace_deployment_folder_path) + model_inference_wrapper.stop_model(deployment_platform=model_data.replaceDeploymentPlatform) + logger.info("INSIDE REPLACE DEPLOYMENT") replace_deployment_folder_path = f"{shared_models_root_folder}/{model_data.replaceDeploymentPlatform}" @@ -649,19 +678,20 @@ async def test_inference(request:Request, inference_data:TestInferenceRequest): raise RuntimeError(f"crash happened in model inference testing - {e}") -# @app.post("/classifier/datamodel/deployment/test/delete") -# async def delete_folder_content(request:Request, modelData:DeleteTestRequest): -# try: -# folder_path = os.path.join("..", "shared", "models", "test", {modelData.deleteModelId}) -# delete_folder(folder_path) +@app.post("/classifier/datamodel/deployment/testing/delete") +async def delete_folder_content(request:Request, model_data:DeleteTestRequest): + try: + + folder_path = f"{TEST_MODEL_DOWNLOAD_ROOT_DIRECTORY}/{model_data.deleteModelId}" + delete_folder(folder_path) -# # Stop the model -# inference_obj.stop_model(model_id=modelData.deleteModelId) + # Stop the model + test_inference_wrapper.stop_model(model_id=model_data.deleteModelId) -# delete_success = {"message" : "Model Deleted Successfully!"} -# return JSONResponse(status_code = 200, content = delete_success) + delete_success = {"message" : "Model Deleted Successfully!"} + return JSONResponse(status_code = 200, content = delete_success) -# except Exception as e: -# raise HTTPException(status_code = 500, detail=str(e)) + except Exception as e: + raise HTTPException(status_code = 500, detail=str(e)) diff --git a/model-inference/utils.py b/model-inference/utils.py index 11ff4c30..f232b50e 100644 --- a/model-inference/utils.py +++ b/model-inference/utils.py @@ -1,7 +1,7 @@ import zipfile import os import shutil -from typing import List, Optional +from typing import Optional def unzip_file(zip_path, extract_to): with zipfile.ZipFile(zip_path, 'r') as zip_ref: @@ -71,4 +71,12 @@ def get_test_inference_success_payload(predicted_classes:list, average_confidenc } return TEST_INFERENCE_SUCCESS_PAYLOAD - \ No newline at end of file + +def delete_folder(folder_path: str): + try: + if os.path.isdir(folder_path): + shutil.rmtree(folder_path) + else: + raise FileNotFoundError(f"The path {folder_path} is not a directory.") + except Exception as e: + raise Exception(f"Failed to delete the folder {folder_path}. Reason: {e}") \ No newline at end of file diff --git a/model_trainer/model_trainer.py b/model_trainer/model_trainer.py index b77b741c..7d5f92e6 100644 --- a/model_trainer/model_trainer.py +++ b/model_trainer/model_trainer.py @@ -187,6 +187,7 @@ def deploy_model(self, best_model_name, progress_session_id): payload = {} payload["modelId"] = self.new_model_id + payload["oldModelId"] = self.old_model_id payload["replaceDeployment"] = self.replace_deployment payload["replaceDeploymentPlatform"] = self.prev_deployment_env payload["bestBaseModel"] = best_model_name From f3b44a5b897f8012a0940f4939e033f2cbd84b95 Mon Sep 17 00:00:00 2001 From: Thirunayan22 Date: Tue, 27 Aug 2024 00:50:46 +0530 Subject: [PATCH 549/582] minor fixes for delete model --- DSL/CronManager/script/datamodel_deletion_exec.sh | 0 docker-compose.yml | 2 +- file-handler/file_handler_api.py | 6 +++--- 3 files changed, 4 insertions(+), 4 deletions(-) mode change 100644 => 100755 DSL/CronManager/script/datamodel_deletion_exec.sh diff --git a/DSL/CronManager/script/datamodel_deletion_exec.sh b/DSL/CronManager/script/datamodel_deletion_exec.sh old mode 100644 new mode 100755 diff --git a/docker-compose.yml b/docker-compose.yml index 1e7ba77f..7c9f09f5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -217,7 +217,7 @@ services: - DATAMODEL_DELETE_CONFIRMATION_URL= http://ruuter-private:8088/classifier/datamodel/update/dataset-group - JIRA_ACTIVE_MODEL_DELETE_URL=http://model-inference:8003/classifier/datamodel/deployment/jira/delete - OUTLOOK_ACTIVE_MODEL_DELETE_URL=http://model-inference:8003/classifier/datamodel/deployment/outlook/delete - - TEST_MODEL_DELETE_URL=http://test-inference:8011/classifier/datamodel/deployment/test/delete + - TEST_MODEL_DELETE_URL=http://model-inference:8003/classifier/datamodel/deployment/testing/delete - MODEL_METADATA_DELETE_URL=http://ruuter-private:8088/classifier/datamodel/metadata/delete ports: - "8000:8000" diff --git a/file-handler/file_handler_api.py b/file-handler/file_handler_api.py index 46f917aa..1faba930 100644 --- a/file-handler/file_handler_api.py +++ b/file-handler/file_handler_api.py @@ -469,14 +469,14 @@ async def delete_datamodels(request: Request): } active_models_deleted = False if deployment_env.lower() == "jira": - response = requests.post(JIRA_ACTIVE_MODEL_DELETE_URL, headers=headers, json={"modelId": model_id}) + response = requests.post(JIRA_ACTIVE_MODEL_DELETE_URL, headers=headers) if response.status_code == 200: active_models_deleted = True elif deployment_env.lower() == "outlook": - response = requests.post(OUTLOOK_ACTIVE_MODEL_DELETE_URL, headers=headers, json={"modelId": model_id}) + response = requests.post(OUTLOOK_ACTIVE_MODEL_DELETE_URL, headers=headers) if response.status_code == 200: active_models_deleted = True - elif deployment_env.lower() == "test": + elif deployment_env.lower() == "testing": response = requests.post(TEST_MODEL_DELETE_URL, headers=headers, json={"deleteModelId": model_id}) if response.status_code == 200: active_models_deleted = True From 995939d8ff31fd36b2950a435e4ed3918667611b Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Tue, 27 Aug 2024 11:36:21 +0530 Subject: [PATCH 550/582] Re-Train-And-Last-Train-Models:production model overview changes --- DSL/Ruuter.private/DSL/GET/classifier/datamodel/overview.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DSL/Ruuter.private/DSL/GET/classifier/datamodel/overview.yml b/DSL/Ruuter.private/DSL/GET/classifier/datamodel/overview.yml index 58b47340..5e589383 100644 --- a/DSL/Ruuter.private/DSL/GET/classifier/datamodel/overview.yml +++ b/DSL/Ruuter.private/DSL/GET/classifier/datamodel/overview.yml @@ -54,12 +54,12 @@ extract_data: dataset_group: ${Number(incoming.params.datasetGroup)} training_status: ${incoming.params.trainingStatus} deployment_maturity: ${incoming.params.deploymentMaturity} - is_production_model: ${incoming.params.isProductionModel} + is_production_model: ${JSON.parse(incoming.params.isProductionModel)} next: check_production_model_status check_production_model_status: switch: - - condition: ${is_production_model === 'true'} + - condition: ${is_production_model === true} next: get_production_data_model_meta_data_overview next: get_data_model_meta_data_overview From f89bf37808e7777267e290a123d2a63aafeeab6d Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Tue, 27 Aug 2024 11:51:08 +0530 Subject: [PATCH 551/582] bug fixes and improvements --- GUI/src/assets/DataModelsIcon.tsx | 20 ++ GUI/src/assets/DatabaseIcon.tsx | 37 +++ GUI/src/assets/IncomingTextsIcon.tsx | 20 ++ GUI/src/assets/IntegrationIcon.tsx | 42 +++ GUI/src/assets/TestModelIcon.tsx | 32 +++ GUI/src/assets/UserIcon.tsx | 30 ++ GUI/src/components/DataTable/DataTable.scss | 1 + GUI/src/components/FileUpload/index.tsx | 1 - .../FormElements/FormInput/index.tsx | 1 - .../FormElements/FormSelect/index.tsx | 19 +- GUI/src/components/Header/index.tsx | 34 ++- GUI/src/components/MainNavigation/index.tsx | 45 +-- .../TreeNode/ClassHeirarchyTreeNode.tsx | 6 +- .../molecules/ClassHeirarchy/index.tsx | 2 +- .../CorrectedTextsTables.tsx | 2 +- .../molecules/DataModelCard/index.tsx | 39 ++- .../DatasetDetailedViewTable.tsx | 44 +-- GUI/src/pages/CorrectedTexts/index.tsx | 4 +- .../pages/DataModels/ConfigureDataModel.tsx | 4 - GUI/src/pages/DataModels/CreateDataModel.tsx | 29 +- GUI/src/pages/DataModels/index.tsx | 260 ++++++++++-------- .../DatasetGroups/CreateDatasetGroup.tsx | 4 +- .../pages/DatasetGroups/ViewDatasetGroup.tsx | 76 +++-- GUI/src/pages/DatasetGroups/index.tsx | 13 +- GUI/src/pages/StopWords/index.tsx | 74 ++--- GUI/src/pages/TestModel/index.tsx | 4 +- GUI/src/pages/TrainingSessions/index.tsx | 3 +- .../pages/UserManagement/SettingsUsers.scss | 4 + GUI/src/pages/UserManagement/UserModal.tsx | 37 ++- GUI/src/pages/ValidationSessions/index.tsx | 16 +- GUI/src/styles/generic/_base.scss | 4 +- GUI/src/types/dataModels.ts | 11 +- GUI/src/types/datasetGroups.ts | 8 + GUI/src/utils/datasetGroupsUtils.ts | 67 +++-- GUI/translations/en/common.json | 24 +- 35 files changed, 686 insertions(+), 331 deletions(-) create mode 100644 GUI/src/assets/DataModelsIcon.tsx create mode 100644 GUI/src/assets/DatabaseIcon.tsx create mode 100644 GUI/src/assets/IncomingTextsIcon.tsx create mode 100644 GUI/src/assets/IntegrationIcon.tsx create mode 100644 GUI/src/assets/TestModelIcon.tsx create mode 100644 GUI/src/assets/UserIcon.tsx diff --git a/GUI/src/assets/DataModelsIcon.tsx b/GUI/src/assets/DataModelsIcon.tsx new file mode 100644 index 00000000..855dd73f --- /dev/null +++ b/GUI/src/assets/DataModelsIcon.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +const DataModelsIcon = () => { + return ( + + + + ); +}; + +export default DataModelsIcon; diff --git a/GUI/src/assets/DatabaseIcon.tsx b/GUI/src/assets/DatabaseIcon.tsx new file mode 100644 index 00000000..6db9fa78 --- /dev/null +++ b/GUI/src/assets/DatabaseIcon.tsx @@ -0,0 +1,37 @@ +import React from 'react'; + +const DatabaseIcon = () => { + return ( + + + + + + ); +}; + +export default DatabaseIcon; diff --git a/GUI/src/assets/IncomingTextsIcon.tsx b/GUI/src/assets/IncomingTextsIcon.tsx new file mode 100644 index 00000000..fb6ccb9d --- /dev/null +++ b/GUI/src/assets/IncomingTextsIcon.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +const IncomingTextsIcon = () => { + return ( + + + + ); +}; + +export default IncomingTextsIcon; diff --git a/GUI/src/assets/IntegrationIcon.tsx b/GUI/src/assets/IntegrationIcon.tsx new file mode 100644 index 00000000..e8af72b4 --- /dev/null +++ b/GUI/src/assets/IntegrationIcon.tsx @@ -0,0 +1,42 @@ +import React from 'react'; + +const IntegrationIcon = () => { + return ( + + + + + + + + + + + + ); +}; + +export default IntegrationIcon; diff --git a/GUI/src/assets/TestModelIcon.tsx b/GUI/src/assets/TestModelIcon.tsx new file mode 100644 index 00000000..2a2bf638 --- /dev/null +++ b/GUI/src/assets/TestModelIcon.tsx @@ -0,0 +1,32 @@ +import React from 'react'; + +const TestModelIcon = () => { + return ( + + + + + + + + + + + ); +}; + +export default TestModelIcon; diff --git a/GUI/src/assets/UserIcon.tsx b/GUI/src/assets/UserIcon.tsx new file mode 100644 index 00000000..9593dc0a --- /dev/null +++ b/GUI/src/assets/UserIcon.tsx @@ -0,0 +1,30 @@ +import React from 'react'; + +const UserIcon = () => { + return ( + + + + + ); +}; + +export default UserIcon; diff --git a/GUI/src/components/DataTable/DataTable.scss b/GUI/src/components/DataTable/DataTable.scss index 933abdf1..95b089f4 100644 --- a/GUI/src/components/DataTable/DataTable.scss +++ b/GUI/src/components/DataTable/DataTable.scss @@ -196,5 +196,6 @@ } .data-table__scrollWrapper { + min-height: 150px !important; padding: 10px 20px; } \ No newline at end of file diff --git a/GUI/src/components/FileUpload/index.tsx b/GUI/src/components/FileUpload/index.tsx index 4738a471..2ee593bb 100644 --- a/GUI/src/components/FileUpload/index.tsx +++ b/GUI/src/components/FileUpload/index.tsx @@ -38,7 +38,6 @@ const FileUpload = forwardRef( const restrictFormat = (accept: string | string[]) => { if (typeof accept === 'string') { - console.log("hii") if (accept === 'json') return '.json'; else if (accept === 'xlsx') return '.xlsx'; else if (accept === 'yaml') return '.yaml, .yml'; diff --git a/GUI/src/components/FormElements/FormInput/index.tsx b/GUI/src/components/FormElements/FormInput/index.tsx index dad9bda3..dd8df673 100644 --- a/GUI/src/components/FormElements/FormInput/index.tsx +++ b/GUI/src/components/FormElements/FormInput/index.tsx @@ -1,7 +1,6 @@ import { forwardRef, InputHTMLAttributes, PropsWithChildren, useId } from 'react'; import clsx from 'clsx'; import './FormInput.scss'; -import { CHAT_INPUT_LENGTH } from 'constants/config'; import { DefaultTFuncReturn } from 'i18next'; type InputProps = PropsWithChildren> & { diff --git a/GUI/src/components/FormElements/FormSelect/index.tsx b/GUI/src/components/FormElements/FormSelect/index.tsx index 462a46ea..43562b4b 100644 --- a/GUI/src/components/FormElements/FormSelect/index.tsx +++ b/GUI/src/components/FormElements/FormSelect/index.tsx @@ -4,6 +4,7 @@ import { SelectHTMLAttributes, useId, useState, + useEffect, } from 'react'; import { useSelect } from 'downshift'; import clsx from 'clsx'; @@ -26,9 +27,10 @@ type FormSelectProps = Partial & placeholder?: string; hideLabel?: boolean; direction?: 'down' | 'up'; - options: FormSelectOption[]|[]; + options: FormSelectOption[]; onSelectionChange?: (selection: FormSelectOption | null) => void; error?: string; + defaultValue?: string | { name: string; id: string }; }; const itemToString = (item: FormSelectOption | null) => { @@ -53,11 +55,20 @@ const FormSelect = forwardRef( ) => { const id = useId(); const { t } = useTranslation(); - const defaultSelected = + + const [selectedItem, setSelectedItem] = useState( options?.find((o) => o.value === defaultValue) || options?.find((o) => typeof o.value !== 'string' && o.value?.name === defaultValue) || - null; - const [selectedItem, setSelectedItem] = useState(defaultSelected); + null + ); + + useEffect(() => { + const newSelectedItem = + options?.find((o) => o.value === defaultValue) || + options?.find((o) => typeof o.value !== 'string' && o.value?.name === defaultValue) || + null; + setSelectedItem(newSelectedItem); + }, [defaultValue, options]); const { isOpen, diff --git a/GUI/src/components/Header/index.tsx b/GUI/src/components/Header/index.tsx index 90576df6..6902cd2c 100644 --- a/GUI/src/components/Header/index.tsx +++ b/GUI/src/components/Header/index.tsx @@ -1,11 +1,10 @@ import { FC, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQuery } from '@tanstack/react-query'; import { AxiosError } from 'axios'; import { Track, Button, Dialog } from 'components'; import useStore from 'store'; -import { ReactComponent as BykLogo } from 'assets/logo.svg'; import { useToast } from 'hooks/useToast'; import apiDev from 'services/api-dev'; import { useCookies } from 'react-cookie'; @@ -13,6 +12,8 @@ import './Header.scss'; import { useDialog } from 'hooks/useDialog'; import { ButtonAppearanceTypes } from 'enums/commonEnums'; import { authEndpoints } from 'utils/endpoints'; +import { authQueryKeys } from 'utils/queryKeys'; +import { UserInfo } from 'types/userInfo'; const Header: FC = () => { const { t } = useTranslation(); @@ -40,11 +41,12 @@ const Header: FC = () => { const expirationDate = new Date(parseInt(expirationTimeStamp) ?? ''); const currentDate = new Date(Date.now()); if ( - expirationDate < currentDate && - expirationDate.getTime() - currentDate.getTime() <= 120000 + expirationDate.getTime() - currentDate.getTime() <= 240000 ) { - setSessionTimeOutModalOpened(true); - setSessionTimeOutDuration(30); + if (!sessionTimeOutModalOpened) { + setSessionTimeOutModalOpened(true); + setSessionTimeOutDuration(30); + } } } }, 2000); @@ -91,12 +93,21 @@ const Header: FC = () => { setSessionTimeOutDuration(30); setSessionTimeOutModalOpened(false); setSessionExtentionInProgress(false); + refetch() }, onError: (error: AxiosError) => { handleLogout(); }, }); + const { refetch } = useQuery({ + queryKey: authQueryKeys.USER_DETAILS(), + onSuccess: (res: { response: UserInfo }) => { + localStorage.setItem('exp', res.response.JWTExpirationTimestamp); + useStore.getState().setUserInfo(res.response); + }, + enabled: false + }); const logoutMutation = useMutation({ mutationFn: () => apiDev.get(authEndpoints.LOGOUT()), onSuccess() { @@ -118,9 +129,14 @@ const Header: FC = () => { }; return (
          -
          +
          - {userInfo && (
            @@ -149,7 +153,12 @@ const MainNavigation: FC = () => { ) : ( {' '} - + {menuItem?.label} )} diff --git a/GUI/src/components/molecules/ClassHeirarchy/TreeNode/ClassHeirarchyTreeNode.tsx b/GUI/src/components/molecules/ClassHeirarchy/TreeNode/ClassHeirarchyTreeNode.tsx index 8bf8f82b..20b359b5 100644 --- a/GUI/src/components/molecules/ClassHeirarchy/TreeNode/ClassHeirarchyTreeNode.tsx +++ b/GUI/src/components/molecules/ClassHeirarchy/TreeNode/ClassHeirarchyTreeNode.tsx @@ -1,7 +1,7 @@ import { FormInput } from 'components/FormElements'; import React, { ChangeEvent, useState } from 'react'; import { TreeNode } from 'types/datasetGroups'; -import { isClassHierarchyDuplicated } from 'utils/datasetGroupsUtils'; +import { isClassHierarchyDuplicatedAtSameLevel } from 'utils/datasetGroupsUtils'; import { MdDeleteOutline } from 'react-icons/md'; import { useTranslation } from 'react-i18next'; import './index.css'; @@ -28,7 +28,7 @@ const ClassHeirarchyTreeNode = ({ const handleChange = (e: ChangeEvent) => { setFieldName(e.target.value); node.fieldName = e.target.value; - if (isClassHierarchyDuplicated(nodes, e.target.value)) setNodesError(true); + if (isClassHierarchyDuplicatedAtSameLevel(nodes, e.target.value)) setNodesError(true); else setNodesError(false); }; @@ -50,7 +50,7 @@ const ClassHeirarchyTreeNode = ({ error={ nodesError && !fieldName ? t('datasetGroups.classHierarchy.fieldHint') ?? '' - : fieldName && isClassHierarchyDuplicated(nodes, fieldName) + : fieldName && isClassHierarchyDuplicatedAtSameLevel(nodes, fieldName) ? t('datasetGroups.classHierarchy.filedHintIfExists') ?? '' : '' } diff --git a/GUI/src/components/molecules/ClassHeirarchy/index.tsx b/GUI/src/components/molecules/ClassHeirarchy/index.tsx index 76bc694d..eba5df78 100644 --- a/GUI/src/components/molecules/ClassHeirarchy/index.tsx +++ b/GUI/src/components/molecules/ClassHeirarchy/index.tsx @@ -124,7 +124,7 @@ const ClassHierarchy: FC> = ({ isOpen={isModalOpen} title={t('datasetGroups.modals.deleteClassTitle') ?? ''} footer={ -
            +
            diff --git a/GUI/src/components/molecules/DataModelCard/index.tsx b/GUI/src/components/molecules/DataModelCard/index.tsx index 1668ffa8..e0f13cef 100644 --- a/GUI/src/components/molecules/DataModelCard/index.tsx +++ b/GUI/src/components/molecules/DataModelCard/index.tsx @@ -7,6 +7,7 @@ import { Maturity, TrainingStatus } from 'enums/dataModelsEnums'; import Card from 'components/Card'; import { useTranslation } from 'react-i18next'; import { TrainingResults } from 'types/dataModels'; +import { formatDate } from 'utils/commonUtilts'; type DataModelCardProps = { modelId: number; @@ -21,7 +22,7 @@ type DataModelCardProps = { maturity?: string; setId: React.Dispatch>; setView: React.Dispatch>; - results?: TrainingResults; + results: string | null; }; const DataModelCard: FC> = ({ @@ -41,6 +42,7 @@ const DataModelCard: FC> = ({ }) => { const { open, close } = useDialog(); const { t } = useTranslation(); + const resultsJsonData: TrainingResults = JSON.parse(results ?? '{}'); const renderTrainingStatus = (status: string | undefined) => { if (status === TrainingStatus.RETRAINING_NEEDED) { @@ -118,7 +120,8 @@ const DataModelCard: FC> = ({ {t('dataModels.dataModelCard.dgVersion') ?? ''}:{dgVersion}

            - {t('dataModels.dataModelCard.lastTrained') ?? ''}: {lastTrained} + {t('dataModels.dataModelCard.lastTrained') ?? ''}:{' '} + {lastTrained && formatDate(new Date(lastTrained), 'D.M.yy-H:m')}

            @@ -165,19 +168,33 @@ const DataModelCard: FC> = ({ {results ? (
            - {results?.trainingResults?.classes?.map((c: string) => { - return
            {c}
            ; - })} + {resultsJsonData?.trainingResults?.classes?.map( + (c: string, index: number) => { + return
            {c}
            ; + } + )}
            - {results?.trainingResults?.accuracy?.map((c: string) => { - return
            {c}
            ; - })} + {resultsJsonData?.trainingResults?.accuracy?.map( + (c: string, index: number) => { + return ( +
            + {parseFloat(c)?.toFixed(2)} +
            + ); + } + )}
            - {results?.trainingResults?.f1_score?.map((c: string) => { - return
            {c}
            ; - })} + {resultsJsonData?.trainingResults?.f1_score?.map( + (c: string, index: number) => { + return ( +
            + {parseFloat(c)?.toFixed(2)} +
            + ); + } + )}
            ) : ( diff --git a/GUI/src/components/molecules/DatasetDetailedViewTable/DatasetDetailedViewTable.tsx b/GUI/src/components/molecules/DatasetDetailedViewTable/DatasetDetailedViewTable.tsx index 787af49a..0f503569 100644 --- a/GUI/src/components/molecules/DatasetDetailedViewTable/DatasetDetailedViewTable.tsx +++ b/GUI/src/components/molecules/DatasetDetailedViewTable/DatasetDetailedViewTable.tsx @@ -28,6 +28,7 @@ const DatasetDetailedViewTable = ({ pagination, setPagination, dgId, + isMetadataLoading, }: { metadata: MetaData[]; handleOpenModals: (context: ViewDatasetGroupModalContexts) => void; @@ -41,6 +42,7 @@ const DatasetDetailedViewTable = ({ pagination: PaginationState; setPagination: React.Dispatch>; dgId: number; + isMetadataLoading: boolean; }) => { const { t } = useTranslation(); const navigate = useNavigate(); @@ -80,7 +82,8 @@ const DatasetDetailedViewTable = ({ return ( <> - {metadata && ( + {isMetadataLoading && } + {metadata && !isMetadataLoading && (
            handleOpenModals(ViewDatasetGroupModalContexts.EXPORT_MODAL) } + disabled={datasets?.numPages===0} > {t('datasetGroups.detailedView.export') ?? ''} @@ -172,24 +176,26 @@ const DatasetDetailedViewTable = ({
            )} - {datasets && datasets?.numPages !== 0 && datasets?.numPages <= 2 && ( -
            -

            - {t( - 'datasetGroups.detailedView.insufficientExamplesDesc' - ) ?? ''} -

            - -
            - )} + {datasets && + datasets?.numPages !== 0 && + datasets?.numPages <= 2 && ( +
            +

            + {t( + 'datasetGroups.detailedView.insufficientExamplesDesc' + ) ?? ''} +

            + +
            + )}
            )} diff --git a/GUI/src/pages/CorrectedTexts/index.tsx b/GUI/src/pages/CorrectedTexts/index.tsx index 3babed50..760550e5 100644 --- a/GUI/src/pages/CorrectedTexts/index.tsx +++ b/GUI/src/pages/CorrectedTexts/index.tsx @@ -1,5 +1,4 @@ import { FC, useState } from 'react'; -import './index.scss'; import { useTranslation } from 'react-i18next'; import { ButtonAppearanceTypes } from 'enums/commonEnums'; import { Button, FormSelect } from 'components'; @@ -66,8 +65,7 @@ const CorrectedTexts: FC = () => { diff --git a/GUI/src/pages/DataModels/ConfigureDataModel.tsx b/GUI/src/pages/DataModels/ConfigureDataModel.tsx index 5f343aaa..f36ff5ba 100644 --- a/GUI/src/pages/DataModels/ConfigureDataModel.tsx +++ b/GUI/src/pages/DataModels/ConfigureDataModel.tsx @@ -187,10 +187,6 @@ const ConfigureDataModel: FC = ({ > {t('global.cancel')} -
          ), }); diff --git a/GUI/src/pages/DataModels/CreateDataModel.tsx b/GUI/src/pages/DataModels/CreateDataModel.tsx index 85d27820..7c21181d 100644 --- a/GUI/src/pages/DataModels/CreateDataModel.tsx +++ b/GUI/src/pages/DataModels/CreateDataModel.tsx @@ -12,7 +12,7 @@ import { ButtonAppearanceTypes } from 'enums/commonEnums'; import { createDataModel, getDataModelsOverview } from 'services/data-models'; import { dataModelsQueryKeys, integrationQueryKeys } from 'utils/queryKeys'; import { getIntegrationStatus } from 'services/integration'; -import { CreateDataModelPayload, DataModel } from 'types/dataModels'; +import { CreateDataModelPayload, DataModel, ErrorsType } from 'types/dataModels'; const CreateDataModel: FC = () => { const { t } = useTranslation(); @@ -73,16 +73,37 @@ const CreateDataModel: FC = () => { ...prevFilters, [name]: value, })); + + setErrors((prevErrors) => { + const updatedErrors = { ...prevErrors }; + + if (name === 'modelName' && value !== '') { + delete updatedErrors.modelName; + } + if (name === 'platform' && value !== '') { + delete updatedErrors.platform; + } + if (name === 'baseModels' && value !== '') { + delete updatedErrors.baseModels; + } + if (name === 'maturity' && value !== '') { + delete updatedErrors.maturity; + } + if (name === 'dgId') { + delete updatedErrors.dgId; + } + + return updatedErrors; + }); }; - const [errors, setErrors] = useState({ + const [errors, setErrors] = useState({ modelName: '', dgName: '', platform: '', baseModels: '', maturity: '', }); - const validateData = () => { const validationErrors = validateDataModel(dataModel); setErrors(validationErrors); @@ -137,7 +158,7 @@ const CreateDataModel: FC = () => { }; const createDataModelMutation = useMutation({ mutationFn: (data: CreateDataModelPayload) => createDataModel(data), - onSuccess: async (response) => { + onSuccess: async () => { open({ title: t('dataModels.createDataModel.successTitle'), content:

          {t('dataModels.createDataModel.successDesc')}

          , diff --git a/GUI/src/pages/DataModels/index.tsx b/GUI/src/pages/DataModels/index.tsx index 0b441379..89808dc8 100644 --- a/GUI/src/pages/DataModels/index.tsx +++ b/GUI/src/pages/DataModels/index.tsx @@ -147,10 +147,10 @@ const DataModels: FC = () => { style={{ margin: '30px 0px' }} > {prodDataModelsData?.data?.map( - (dataset: DataModelResponse, index: number) => { + (dataset: DataModelResponse) => { return ( { trainingStatus={dataset.trainingStatus} platform={dataset?.deploymentEnv} maturity={dataset?.maturityLabel} - results={dataset?.trainingResults} + results={dataset?.trainingResults ?? null} setId={setId} setView={setView} /> @@ -184,122 +184,156 @@ const DataModels: FC = () => { {t('dataModels.createModel')}
          -
          - - handleFilterChange('modelName', selection?.value ?? '') - } - defaultValue={filters?.modelName} - style={{fontSize:"1rem"}} - /> - - handleFilterChange('version', selection?.value ?? '') - } - defaultValue={filters?.version} - /> - - handleFilterChange('platform', selection?.value ?? '') - } - defaultValue={filters?.platform} - /> - - handleFilterChange('datasetGroup', selection?.value?.id) - } - defaultValue={filters?.datasetGroup} - /> - - handleFilterChange('trainingStatus', selection?.value) - } - defaultValue={filters?.trainingStatus} - /> - - handleFilterChange('maturity', selection?.value) - } - defaultValue={filters?.maturity} - /> - - handleFilterChange('sort', selection?.value) - } - defaultValue={filters?.sort} - /> - - + + handleFilterChange('modelName', selection?.value ?? '') + } + defaultValue={filters?.modelName} + style={{ fontSize: '1rem', width: '200px' }} + /> + + handleFilterChange('version', selection?.value ?? '') + } + defaultValue={filters?.version} + style={{ width: 'auto' }} + /> + + handleFilterChange('platform', selection?.value ?? '') + } + defaultValue={filters?.platform} + style={{ width: 'auto' }} + /> + + handleFilterChange('datasetGroup', selection?.value?.id) + } + defaultValue={filters?.datasetGroup} + style={{ width: '200px' }} + /> + + handleFilterChange('trainingStatus', selection?.value) + } + defaultValue={filters?.trainingStatus} + style={{ width: '150px' }} + /> + + handleFilterChange('maturity', selection?.value) + } + defaultValue={filters?.maturity} + style={{ width: '150px' }} + /> + + handleFilterChange('sort', selection?.value) + } + defaultValue={filters?.sort} + style={{ width: 'auto' }} + /> +
          +
          + + +
          {dataModelsData?.data?.length > 0 ? (
          - {dataModelsData?.data?.map( - (dataset: DataModelResponse, index: number) => { - return ( - - ); - } - )} -
          + {dataModelsData?.data?.map( + (dataset: DataModelResponse, index: number) => { + return ( + + ); + } + )} +
          ) : ( )} diff --git a/GUI/src/pages/DatasetGroups/CreateDatasetGroup.tsx b/GUI/src/pages/DatasetGroups/CreateDatasetGroup.tsx index 5a93fb80..812690d5 100644 --- a/GUI/src/pages/DatasetGroups/CreateDatasetGroup.tsx +++ b/GUI/src/pages/DatasetGroups/CreateDatasetGroup.tsx @@ -56,13 +56,15 @@ const CreateDatasetGroup: FC = () => { useState(ValidationErrorTypes.NULL); const validateData = useCallback(() => { + setNodesError(validateClassHierarchy(nodes)); setDatasetNameError(!datasetName); setValidationRuleError(validateValidationRules(validationRules)); if ( !validateClassHierarchy(nodes) && datasetName && - !validateValidationRules(validationRules) + !validateValidationRules(validationRules) && !nodesError && + !validationRuleError ) { if (!isValidationRulesSatisfied(validationRules)) { setIsModalOpen(true); diff --git a/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx b/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx index a607455b..cb428e59 100644 --- a/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx +++ b/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx @@ -251,7 +251,7 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { }); const handleFileSelect = (file: File | undefined) => { - setFile(file) + setFile(file); }; const handleImport = () => { @@ -308,7 +308,12 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { > {t('global.cancel')} -
          @@ -337,21 +342,12 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { }; const datasetGroupUpdate = () => { - const classHierarchyError = validateClassHierarchy(nodes); + const classHierarchyError = validateClassHierarchy(nodes) || nodesError; const validationRulesError = validateValidationRules(validationRules); setNodesError(classHierarchyError); setValidationRuleError(validationRulesError); - if ( - classHierarchyError || - validationRulesError || - nodesError || - validationRuleError - ) { - return; - } - const isMajorUpdateDetected = isMajorUpdate( { validationRules: metadata?.[0]?.validationCriteria?.validationRules, @@ -386,24 +382,28 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { }); }; - if (isMajorUpdateDetected) { - openConfirmationModal( - t('datasetGroups.detailedView.confirmMajorUpdatesDesc'), - t('datasetGroups.detailedView.confirmMajorUpdatesTitle'), - handleMajorUpdate - ); - } else if (minorPayload) { - openConfirmationModal( - t('datasetGroups.detailedView.confirmMinorUpdatesDesc'), - t('datasetGroups.detailedView.confirmMinorUpdatesTitle'), - () => minorUpdateMutation.mutate(minorPayload) - ); - } else if (patchPayload) { - openConfirmationModal( - t('datasetGroups.detailedView.confirmPatchUpdatesDesc'), - t('datasetGroups.detailedView.confirmPatchUpdatesTitle'), - () => patchUpdateMutation.mutate(patchPayload) - ); + if (classHierarchyError || validationRulesError || nodesError) { + return; + } else { + if (isMajorUpdateDetected) { + openConfirmationModal( + t('datasetGroups.detailedView.confirmMajorUpdatesDesc'), + t('datasetGroups.detailedView.confirmMajorUpdatesTitle'), + handleMajorUpdate + ); + } else if (minorPayload) { + openConfirmationModal( + t('datasetGroups.detailedView.confirmMinorUpdatesDesc'), + t('datasetGroups.detailedView.confirmMinorUpdatesTitle'), + () => minorUpdateMutation.mutate(minorPayload) + ); + } else if (patchPayload) { + openConfirmationModal( + t('datasetGroups.detailedView.confirmPatchUpdatesDesc'), + t('datasetGroups.detailedView.confirmPatchUpdatesTitle'), + () => patchUpdateMutation.mutate(patchPayload) + ); + } } }; @@ -417,7 +417,7 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { onError: () => { open({ title: t('datasetGroups.detailedView.modals.edit.error'), - content:

          { t('datasetGroups.modals.delete.errorDesc') }

          , + content:

          {t('datasetGroups.modals.delete.errorDesc')}

          , }); }, }); @@ -435,7 +435,7 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { onError: () => { open({ title: t('datasetGroups.detailedView.modals.delete.error'), - content:

          { t('datasetGroups.modals.delete.errorDesc') }

          , + content:

          {t('datasetGroups.modals.delete.errorDesc')}

          , }); }, }); @@ -456,6 +456,7 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => {
          > = ({ dgId, setView }) => { > {t('datasetGroups.detailedView.delete') ?? ''} -
          @@ -553,4 +547,4 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { ); }; -export default ViewDatasetGroup; \ No newline at end of file +export default ViewDatasetGroup; diff --git a/GUI/src/pages/DatasetGroups/index.tsx b/GUI/src/pages/DatasetGroups/index.tsx index ecf9e8c7..2c52783c 100644 --- a/GUI/src/pages/DatasetGroups/index.tsx +++ b/GUI/src/pages/DatasetGroups/index.tsx @@ -110,10 +110,7 @@ const DatasetGroups: FC = () => { (selection?.value as string) ?? '' ) } - value={{ - label: filters.datasetGroupName, - value: filters.datasetGroupName, - }} + defaultValue={filters.datasetGroupName} /> { (selection?.value as string) ?? '' ) } + defaultValue={filters.version} + /> { (selection?.value as string) ?? '' ) } + defaultValue={filters.validationStatus} + /> { onSelectionChange={(selection) => handleFilterChange('sort', (selection?.value as string) ?? '') } + defaultValue={filters.sort} + /> @@ -138,6 +140,7 @@ const CorrectedTexts: FC = () => { isLoading={isLoading} setPagination={setPagination} pagination={pagination} + setEnableFetch={setEnableFetch} /> From 14501bd1ab3cae6f7e177a9393f0bdb371eb2161 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Tue, 27 Aug 2024 12:37:05 +0530 Subject: [PATCH 553/582] sonar cloud fixes --- GUI/src/assets/DatabaseIcon.tsx | 18 +++++++++--------- GUI/src/assets/IntegrationIcon.tsx | 14 +++++++------- GUI/src/assets/UserIcon.tsx | 12 ++++++------ 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/GUI/src/assets/DatabaseIcon.tsx b/GUI/src/assets/DatabaseIcon.tsx index 6db9fa78..5ab9d3b5 100644 --- a/GUI/src/assets/DatabaseIcon.tsx +++ b/GUI/src/assets/DatabaseIcon.tsx @@ -12,23 +12,23 @@ const DatabaseIcon = () => { ); diff --git a/GUI/src/assets/IntegrationIcon.tsx b/GUI/src/assets/IntegrationIcon.tsx index e8af72b4..5553ea54 100644 --- a/GUI/src/assets/IntegrationIcon.tsx +++ b/GUI/src/assets/IntegrationIcon.tsx @@ -9,20 +9,20 @@ const IntegrationIcon = () => { fill="none" xmlns="http://www.w3.org/2000/svg" > - + diff --git a/GUI/src/assets/UserIcon.tsx b/GUI/src/assets/UserIcon.tsx index 9593dc0a..83c84c0c 100644 --- a/GUI/src/assets/UserIcon.tsx +++ b/GUI/src/assets/UserIcon.tsx @@ -12,16 +12,16 @@ const UserIcon = () => { ); From 21d03d927c2604db35be6c1cb88016d91cde6192 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Tue, 27 Aug 2024 12:40:42 +0530 Subject: [PATCH 554/582] sonar cloud fixes --- GUI/src/assets/TestModelIcon.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GUI/src/assets/TestModelIcon.tsx b/GUI/src/assets/TestModelIcon.tsx index 2a2bf638..6b9c45f6 100644 --- a/GUI/src/assets/TestModelIcon.tsx +++ b/GUI/src/assets/TestModelIcon.tsx @@ -9,7 +9,7 @@ const TestModelIcon = () => { fill="none" xmlns="http://www.w3.org/2000/svg" > - + Date: Tue, 27 Aug 2024 13:19:40 +0530 Subject: [PATCH 555/582] anonymizer verfication removal and streamlit app bug fix --- anonymizer/anonymizer_api.py | 80 +++++++++++------------------------- anonymizer/requirements.txt | 2 + 2 files changed, 27 insertions(+), 55 deletions(-) diff --git a/anonymizer/anonymizer_api.py b/anonymizer/anonymizer_api.py index 05f45b34..3df90efc 100644 --- a/anonymizer/anonymizer_api.py +++ b/anonymizer/anonymizer_api.py @@ -104,75 +104,45 @@ async def process_text(request: Request, background_tasks: BackgroundTasks): except Exception as e: return JSONResponse(status_code=200, content={"status":False, "detail":"Anonymizing process failed.", "error":e}) - -@app.post("/verify_signature") -async def verify_signature_endpoint(request: Request, x_hub_signature: str = Header(...)): - try: - payload = await request.json() - secret = os.getenv("SHARED_SECRET") # You should set this environment variable - headers = {"x-hub-signature": x_hub_signature} - - is_valid = verify_signature(payload, headers, secret) - - if is_valid: - return {"status": True} - else: - return {"status": False}, 401 - except Exception as e: - return {"status": False, "error": str(e)}, 500 - -def verify_signature(payload: dict, headers: dict, secret: str) -> bool: - signature = headers.get("x-hub-signature") - if not signature: - raise HTTPException(status_code=400, detail="Signature missing") - - shared_secret = secret.encode('utf-8') - payload_string = json.dumps(payload).encode('utf-8') - - hmac_obj = hmac.new(shared_secret, payload_string, hashlib.sha256) - computed_signature = hmac_obj.hexdigest() - computed_signature_prefixed = f"sha256={computed_signature}" - - is_valid = hmac.compare_digest(computed_signature_prefixed, signature) - - return is_valid +@app.post("/anonymize-file") async def anonymize_file(file: UploadFile = File(...), columns: str = Form(...)): try: contents = await file.read() df = pd.read_excel(io.BytesIO(contents)) - - columns_to_anonymize = columns.split(",") - - concatenated_text = " ".join(" ".join(str(val) for val in df[col].values) for col in columns_to_anonymize) - - cleaned_text = html_cleaner.remove_html_tags(concatenated_text) - text_chunks = TextProcessor.split_text(cleaned_text, 2000) - processed_chunks = [] - - for chunk in text_chunks: - entities = ner_processor.identify_entities(chunk) - processed_chunk = FakeReplacer.replace_entities(chunk, entities) - processed_chunks.append(processed_chunk) - - processed_text = TextProcessor.combine_chunks(processed_chunks) - anonymized_values = processed_text.split(" ") + column_names = columns.split(',') - for col in columns_to_anonymize: - df[col] = anonymized_values[:len(df[col])] - anonymized_values = anonymized_values[len(df[col]):] + for column in column_names: + if column in df.columns: + df[column] = df[column].apply(lambda text: process_text(text)) output = io.BytesIO() df.to_excel(output, index=False) output.seek(0) - return StreamingResponse(output, media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", headers={ - "Content-Disposition": f"attachment; filename=anonymized_{file.filename}" - }) - + return StreamingResponse( + output, + media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + headers={ + "Content-Disposition": f"attachment; filename=anonymized_{file.filename}" + } + ) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) +def process_text(text): + text_chunks = TextProcessor.split_text(text, 2000) + processed_chunks = [] + + for chunk in text_chunks: + entities = ner_processor.identify_entities(chunk) + processed_chunk = FakeReplacer.replace_entities(chunk, entities) + processed_chunks.append(processed_chunk) + + processed_text = TextProcessor.combine_chunks(processed_chunks) + + return processed_text + if __name__ == "__main__": diff --git a/anonymizer/requirements.txt b/anonymizer/requirements.txt index 3b7b39ff..c768d635 100644 --- a/anonymizer/requirements.txt +++ b/anonymizer/requirements.txt @@ -13,12 +13,14 @@ huggingface-hub==0.24.5 idna==3.7 langdetect==1.0.9 numpy==2.0.1 +openpyxl==3.1.5 packaging==24.1 pandas==2.2.2 pydantic==2.8.2 pydantic_core==2.20.1 python-dateutil==2.9.0.post0 PyYAML==6.0.1 +python-multipart==0.0.9 regex==2024.7.24 requests==2.32.3 safetensors==0.4.4 From d815e0ec0d908112b8003dc060de62577a0bc82d Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Tue, 27 Aug 2024 14:05:28 +0530 Subject: [PATCH 556/582] est-character-issue: estonian character issue fixed --- .../DSL/POST/internal/jira/accept.yml | 23 +++++++------------ jira-verification/index.js | 15 +++++++----- jira-verification/src/extractPayload.js | 19 +++++++++++++++ 3 files changed, 36 insertions(+), 21 deletions(-) create mode 100644 jira-verification/src/extractPayload.js diff --git a/DSL/Ruuter.public/DSL/POST/internal/jira/accept.yml b/DSL/Ruuter.public/DSL/POST/internal/jira/accept.yml index 34f3c0b2..adbafe22 100644 --- a/DSL/Ruuter.public/DSL/POST/internal/jira/accept.yml +++ b/DSL/Ruuter.public/DSL/POST/internal/jira/accept.yml @@ -11,10 +11,14 @@ declaration: - field: payload type: string description: "Body field 'payload'" + - field: extractData + type: json + description: "Body field 'extractData'" get_webhook_data: assign: payload: ${incoming.body.payload} + extract_data: ${incoming.body.extractData} issue_info: ${incoming.body.payload.issue} event_type: ${incoming.body.payload.webhookEvent} next: check_event_type @@ -23,7 +27,7 @@ check_event_type: switch: - condition: ${event_type === 'jira:issue_updated'} next: get_existing_labels - next: get_jira_issue_info + next: send_issue_data get_existing_labels: call: http.post @@ -44,7 +48,7 @@ check_previous_labels: switch: - condition: ${res.response.body.length > 0} next: assign_previous_labels - next: get_jira_issue_info + next: send_issue_data assign_previous_labels: assign: @@ -68,20 +72,9 @@ validate_issue_labels: check_label_mismatch: switch: - condition: ${label_response.response.body.isMismatch === 'true'} - next: get_jira_issue_info + next: send_issue_data next: return_data -get_jira_issue_info: - call: http.post - args: - url: "[#CLASSIFIER_DMAPPER]/hbs/classifier/return_jira_issue_info" - headers: - type: json - body: - data: ${issue_info} - result: extract_info - next: send_issue_data - send_issue_data: call: http.post args: @@ -91,7 +84,7 @@ send_issue_data: body: platform: 'JIRA' key: ${issue_info.key} - data: ${extract_info.response.body} + data: ${extract_data} parentFolderId: None mailId: None labels: ${issue_info.fields.labels} diff --git a/jira-verification/index.js b/jira-verification/index.js index fab24ccb..2fdf454d 100644 --- a/jira-verification/index.js +++ b/jira-verification/index.js @@ -1,6 +1,7 @@ const express = require("express"); const bodyParser = require("body-parser"); const verifySignature = require("./src/verifySignature.js"); +const extractPayload = require("./src/extractPayload.js"); const axios = require("axios"); const helmet = require("helmet"); @@ -11,13 +12,15 @@ app.use(helmet.hidePoweredBy()); app.post("/webhook", async (req, res) => { const isValid = verifySignature(req.body, req.headers); if (isValid) { + const extractData = extractPayload(req.body); + + console.log("Response from extract data:", extractData); + try { - const response = await axios.post( - process.env.RUUTER_PUBLIC_JIRA_URL, - { - payload: req.body, - } - ); + const response = await axios.post(process.env.RUUTER_PUBLIC_JIRA_URL, { + payload: req.body, + extractData: extractData, + }); console.log("Response from helper URL:", response.data); return res.status(200).send("Webhook processed and forwarded"); diff --git a/jira-verification/src/extractPayload.js b/jira-verification/src/extractPayload.js new file mode 100644 index 00000000..24ce92ff --- /dev/null +++ b/jira-verification/src/extractPayload.js @@ -0,0 +1,19 @@ +function extractPayload(payload) { + const issueFields = payload.issue.fields; + + const title = issueFields.summary; + + const description = issueFields.description + .replace(/\n/g, " ") + .replace(/\+\s*/g, ""); + + const attachments = issueFields.attachment.map((att) => att.filename); + + return { + title: title, + description: description, + attachments: attachments, + }; +} + +module.exports = extractPayload; From 5bd6f1ce6b45cef26c9cbd3e6f99a59353b96b43 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Tue, 27 Aug 2024 14:10:35 +0530 Subject: [PATCH 557/582] bug fixes and improvements --- GUI/src/components/Button/index.tsx | 17 ++++++++++++++++- .../ViewDatasetGroupModalController.tsx | 7 +++++-- .../pages/DatasetGroups/ViewDatasetGroup.tsx | 6 ++++-- GUI/src/pages/StopWords/index.tsx | 1 + GUI/src/pages/TestModel/index.tsx | 3 ++- 5 files changed, 28 insertions(+), 6 deletions(-) diff --git a/GUI/src/components/Button/index.tsx b/GUI/src/components/Button/index.tsx index 7b520d83..b35cd8c3 100644 --- a/GUI/src/components/Button/index.tsx +++ b/GUI/src/components/Button/index.tsx @@ -7,6 +7,7 @@ type ButtonProps = ButtonHTMLAttributes & { appearance?: 'primary' | 'secondary' | 'text' | 'icon' | 'error' | 'success'; size?: 'm' | 's'; disabledWithoutStyle?: boolean; + showLoadingIcon?: boolean; }; const Button: FC> = ({ @@ -15,6 +16,7 @@ const Button: FC> = ({ disabled, disabledWithoutStyle = false, children, + showLoadingIcon = false, ...rest }) => { const ref = useRef(null); @@ -34,8 +36,21 @@ const Button: FC> = ({ {...rest} > {children} + {showLoadingIcon && ( +
          + {' '} +
          +
          + )} ); }; -export default Button; +export default Button; \ No newline at end of file diff --git a/GUI/src/components/molecules/ViewDatasetGroupModalController/ViewDatasetGroupModalController.tsx b/GUI/src/components/molecules/ViewDatasetGroupModalController/ViewDatasetGroupModalController.tsx index b06fe2de..85610c39 100644 --- a/GUI/src/components/molecules/ViewDatasetGroupModalController/ViewDatasetGroupModalController.tsx +++ b/GUI/src/components/molecules/ViewDatasetGroupModalController/ViewDatasetGroupModalController.tsx @@ -30,6 +30,7 @@ const ViewDatasetGroupModalController = ({ deleteRow, file, exportFormat, + isImportDataLoading }: { setImportStatus: React.Dispatch>; handleFileSelect: (file: File | undefined) => void; @@ -52,6 +53,7 @@ const ViewDatasetGroupModalController = ({ deleteRow: (dataRow: any) => void; file: File | undefined; exportFormat: string; + isImportDataLoading: boolean }) => { const { close } = useDialog(); const { t } = useTranslation(); @@ -79,7 +81,8 @@ const ViewDatasetGroupModalController = ({ @@ -226,4 +229,4 @@ const ViewDatasetGroupModalController = ({ ); }; -export default ViewDatasetGroupModalController; +export default ViewDatasetGroupModalController; \ No newline at end of file diff --git a/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx b/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx index cb428e59..add85f2a 100644 --- a/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx +++ b/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx @@ -384,7 +384,8 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { if (classHierarchyError || validationRulesError || nodesError) { return; - } else { + } + if (isMajorUpdateDetected) { openConfirmationModal( t('datasetGroups.detailedView.confirmMajorUpdatesDesc'), @@ -404,7 +405,7 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { () => patchUpdateMutation.mutate(patchPayload) ); } - } + }; const majorUpdateDatasetGroupMutation = useMutation({ @@ -542,6 +543,7 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { deleteRow={deleteRow} file={file} exportFormat={exportFormat} + isImportDataLoading={importDataMutation.isLoading} /> ); diff --git a/GUI/src/pages/StopWords/index.tsx b/GUI/src/pages/StopWords/index.tsx index 9ead20da..98b3f8d6 100644 --- a/GUI/src/pages/StopWords/index.tsx +++ b/GUI/src/pages/StopWords/index.tsx @@ -211,6 +211,7 @@ const StopWords: FC = () => { importStopWordsMutation.isLoading || deleteStopWordMutation.isLoading } + showLoadingIcon={importStopWordsMutation.isLoading} > {t('stopWords.importModal.importButton') ?? ''} diff --git a/GUI/src/pages/TestModel/index.tsx b/GUI/src/pages/TestModel/index.tsx index 77d631be..59588215 100644 --- a/GUI/src/pages/TestModel/index.tsx +++ b/GUI/src/pages/TestModel/index.tsx @@ -99,8 +99,9 @@ const TestModel: FC = () => {
          From f6bc41bdb0e86d1bb0d454c0608666bcbb00bc5d Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Wed, 28 Aug 2024 10:10:11 +0530 Subject: [PATCH 558/582] sonar cloud fixes --- .../ValidationCriteria/CardsView.tsx | 31 ++++++++++--------- GUI/src/pages/CorrectedTexts/index.scss | 0 GUI/src/pages/CorrectedTexts/index.tsx | 1 - 3 files changed, 17 insertions(+), 15 deletions(-) delete mode 100644 GUI/src/pages/CorrectedTexts/index.scss diff --git a/GUI/src/components/molecules/ValidationCriteria/CardsView.tsx b/GUI/src/components/molecules/ValidationCriteria/CardsView.tsx index e91ef334..65e4f9ac 100644 --- a/GUI/src/components/molecules/ValidationCriteria/CardsView.tsx +++ b/GUI/src/components/molecules/ValidationCriteria/CardsView.tsx @@ -40,21 +40,24 @@ const ValidationCriteriaCardsView: FC< return ( -
          {t('datasetGroups.createDataset.validationCriteria')}
          - {validationRules && - validationRules?.map((item, index) => ( - - ))} +
          + {t('datasetGroups.createDataset.validationCriteria')} +
          + {validationRules?.map((item, index) => ( + + ))}
          - +
          ); diff --git a/GUI/src/pages/CorrectedTexts/index.scss b/GUI/src/pages/CorrectedTexts/index.scss deleted file mode 100644 index e69de29b..00000000 diff --git a/GUI/src/pages/CorrectedTexts/index.tsx b/GUI/src/pages/CorrectedTexts/index.tsx index 138b7aaa..bdfed0c6 100644 --- a/GUI/src/pages/CorrectedTexts/index.tsx +++ b/GUI/src/pages/CorrectedTexts/index.tsx @@ -1,5 +1,4 @@ import { FC, useState } from 'react'; -import './index.scss'; import { useTranslation } from 'react-i18next'; import { ButtonAppearanceTypes } from 'enums/commonEnums'; import { Button, FormSelect } from 'components'; From 3b4a9fb22084cc66d3a8c26b530c85e97c838c04 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Wed, 28 Aug 2024 10:27:51 +0530 Subject: [PATCH 559/582] enrichment url fix --- data_enrichment/enrichment_streamlit_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data_enrichment/enrichment_streamlit_app.py b/data_enrichment/enrichment_streamlit_app.py index 651af048..77ea31ba 100644 --- a/data_enrichment/enrichment_streamlit_app.py +++ b/data_enrichment/enrichment_streamlit_app.py @@ -3,7 +3,7 @@ import requests from io import BytesIO -API_URL = "http://0.0.0.0:8005/paraphrase" +API_URL = "http://localhost:8005/paraphrase" def paraphrase_text(text, num_return_sequences=1, language_id=None): response = requests.post( From 96205259f90ffa3c005069b36f3da9e89d6252f7 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Wed, 28 Aug 2024 11:15:14 +0530 Subject: [PATCH 560/582] est-character-issue: export corrected text with filters --- DSL/Resql/export-corrected-input-metadata.sql | 16 ++++ .../inference/corrected-metadata-export.yml | 78 +++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 DSL/Resql/export-corrected-input-metadata.sql create mode 100644 DSL/Ruuter.private/DSL/GET/classifier/inference/corrected-metadata-export.yml diff --git a/DSL/Resql/export-corrected-input-metadata.sql b/DSL/Resql/export-corrected-input-metadata.sql new file mode 100644 index 00000000..435ecb0c --- /dev/null +++ b/DSL/Resql/export-corrected-input-metadata.sql @@ -0,0 +1,16 @@ +SELECT id AS inference_id, + input_id, + inference_time_stamp, + inference_text, + jsonb_pretty(predicted_labels) AS predicted_labels, + jsonb_pretty(corrected_labels) AS corrected_labels, + average_predicted_classes_probability, + average_corrected_classes_probability, + platform +FROM "input" +WHERE + (is_corrected = true) + AND (:platform = 'all' OR platform = :platform::platform) +ORDER BY + CASE WHEN :sorting = 'asc' THEN inference_time_stamp END ASC, + CASE WHEN :sorting = 'desc' THEN inference_time_stamp END DESC; \ No newline at end of file diff --git a/DSL/Ruuter.private/DSL/GET/classifier/inference/corrected-metadata-export.yml b/DSL/Ruuter.private/DSL/GET/classifier/inference/corrected-metadata-export.yml new file mode 100644 index 00000000..c5c0324d --- /dev/null +++ b/DSL/Ruuter.private/DSL/GET/classifier/inference/corrected-metadata-export.yml @@ -0,0 +1,78 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'CORRECTED-METADATA-EXPORT'" + method: get + accepts: json + returns: json + namespace: classifier + allowlist: + params: + - field: platform + type: string + description: "Parameter 'platform'" + - field: sortType + type: string + description: "Parameter 'sortType'" + +extract_data: + assign: + platform: ${incoming.params.platform} + sort_type: ${incoming.params.sortType} + next: get_corrected_input_metadata + +get_corrected_input_metadata: + call: http.post + args: + url: "[#CLASSIFIER_RESQL]/export-corrected-input-metadata" + body: + platform: ${platform} + sorting: ${sort_type} + result: res_corrected + next: check_input_metadata_status + +check_input_metadata_status: + switch: + - condition: ${200 <= res_corrected.response.statusCodeValue && res_corrected.response.statusCodeValue < 300} + next: check_input_metadata_exist + next: assign_fail_response + +check_input_metadata_exist: + switch: + - condition: ${res_corrected.response.body.length>0} + next: assign_success_response + next: assign_empty_response + +assign_success_response: + assign: + format_res: { + operationSuccessful: true, + data: '${res_corrected.response.body}' + } + next: return_ok + +assign_empty_response: + assign: + format_res: { + operationSuccessful: true, + data: '${[]}' + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + operationSuccessful: false, + data: '${[]}' + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end \ No newline at end of file From bcd8e7bcd98c44b1001c2f07c096ed4e2b570087 Mon Sep 17 00:00:00 2001 From: Thirunayan22 Date: Wed, 28 Aug 2024 11:26:21 +0530 Subject: [PATCH 561/582] updated inference pipeline --- .DS_Store | Bin 6148 -> 0 bytes model-inference/inference_pipeline.py | 11 ++++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) delete mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 5783d19b6cf2f71276cbfff6611ef4c650b8a738..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK!AiqG5S?wSO(}&Q6nYGJEm*4*ikDF94;aydN=!)5V49UQwTDv3S%1hc@q3)v z-H4^?!GlPdf!Q}ZJG0Bagq>Xg5TjXm0MG^i3ze|az~&2~and!Z7!RRPb0pwF4jK3m zuSK)tKQchyt_2g25JCi>zh8>|ESOIpgG2@x^%^`T@ig!C-bAHZ+uE*Mb*pLJxc74I z=6*h#j{NM3dgoFmL23KJMI6n&_Rfh+b3aa_OeMr&gdx|LaT?0`NY2u5s&ZXDU^T3U z*X}G9{o$Z%ANBgnuDuxcduSgHmdl2N-)XF%ISy`ADicqVg-&Wxu9F5#E z1I)lG16AE^Q~f{v{{6q2#64z!8Q3TWL~ZPjN4O Date: Thu, 29 Aug 2024 00:29:28 +0530 Subject: [PATCH 562/582] fixed model deletion --- DSL/CronManager/script/datamodel_deletion_exec.sh | 14 ++++++++++++-- file-handler/file_handler_api.py | 15 ++++++++++++++- file-handler/requirements.txt | 1 + 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/DSL/CronManager/script/datamodel_deletion_exec.sh b/DSL/CronManager/script/datamodel_deletion_exec.sh index 07a35c08..35681d36 100755 --- a/DSL/CronManager/script/datamodel_deletion_exec.sh +++ b/DSL/CronManager/script/datamodel_deletion_exec.sh @@ -10,27 +10,37 @@ fi # Set the API URL to get metadata based on the modelId api_url="http://ruuter-private:8088/classifier/datamodel/metadata?modelId=$modelId" +echo $api_url # Send the request to the API and capture the output api_response=$(curl -s -H "Cookie: $cookie" -X GET "$api_url") +echo $api_response + # Check if the API response is valid if [ -z "$api_response" ]; then echo "API request failed to get the model metadata." exit 1 fi -deployment_env=$(echo "$api_response" | jq -r '.deploymentEnv') +deployment_env=$(echo $api_response | jq -r '.response.data[0].deploymentEnv') + +echo "API RESPONSE" +echo $api_response +echo "DEPLOYMENT ENV" +echo $deployment_env # Construct the payload using here document payload=$(cat < Date: Thu, 29 Aug 2024 10:52:29 +0530 Subject: [PATCH 563/582] sonar cloud fixes --- GUI/src/components/DataTable/DataTable.scss | 8 +--- GUI/src/hooks/useValidationSessions.tsx | 50 +++++++++++++++++++++ GUI/src/pages/ValidationSessions/index.tsx | 40 +---------------- GUI/src/types/testModel.ts | 4 +- GUI/src/utils/endpoints.ts | 2 +- GUI/src/utils/testModelUtil.ts | 4 +- 6 files changed, 58 insertions(+), 50 deletions(-) create mode 100644 GUI/src/hooks/useValidationSessions.tsx diff --git a/GUI/src/components/DataTable/DataTable.scss b/GUI/src/components/DataTable/DataTable.scss index 95b089f4..fcf13b37 100644 --- a/GUI/src/components/DataTable/DataTable.scss +++ b/GUI/src/components/DataTable/DataTable.scss @@ -11,10 +11,11 @@ &__scrollWrapper { height: 100%; + min-height: 150px !important; + padding: 10px 20px; overflow-x: auto; white-space: nowrap; display: block; - padding: 5px; background-color: white; border-radius: 10px; border: solid 1px get-color(black-coral-1); @@ -194,8 +195,3 @@ } } } - -.data-table__scrollWrapper { - min-height: 150px !important; - padding: 10px 20px; -} \ No newline at end of file diff --git a/GUI/src/hooks/useValidationSessions.tsx b/GUI/src/hooks/useValidationSessions.tsx new file mode 100644 index 00000000..37470564 --- /dev/null +++ b/GUI/src/hooks/useValidationSessions.tsx @@ -0,0 +1,50 @@ +import { useQuery } from "@tanstack/react-query"; +import { useEffect, useState } from "react"; +import { getDatasetGroupsProgress } from "services/datasets"; +import sse from "services/sse-service"; +import { SSEEventData, ValidationProgressData } from "types/datasetGroups"; +import { datasetQueryKeys } from "utils/queryKeys"; + +export const useValidationSessions = () => { + const [progresses, setProgresses] = useState([]); + + const { data: progressData, refetch } = useQuery( + datasetQueryKeys.GET_DATASET_GROUP_PROGRESS(), + getDatasetGroupsProgress, + { + onSuccess: (data) => setProgresses(data), + } + ); + + const handleUpdate = (sessionId: string, newData: SSEEventData) => { + setProgresses((prevProgresses) => + prevProgresses.map((progress) => + progress.id === sessionId ? { ...progress, ...newData } : progress + ) + ); + }; + + useEffect(() => { + if (!progressData) return; + + const eventSources = progressData + .filter( + (progress) => + progress.validationStatus !== 'Success' && + progress.progressPercentage !== 100 + ) + .map((progress) => + sse(`/${progress.id}`, 'dataset', (data: SSEEventData) => { + console.log(`New data for notification ${progress.id}:`, data); + handleUpdate(data.sessionId, data); + }) + ); + + return () => { + eventSources.forEach((eventSource) => eventSource?.close()); + console.log('SSE connections closed'); + }; + }, [progressData, refetch]); + + return progresses; + }; \ No newline at end of file diff --git a/GUI/src/pages/ValidationSessions/index.tsx b/GUI/src/pages/ValidationSessions/index.tsx index adf245d8..ed1e5ef5 100644 --- a/GUI/src/pages/ValidationSessions/index.tsx +++ b/GUI/src/pages/ValidationSessions/index.tsx @@ -6,47 +6,11 @@ import { useQuery } from '@tanstack/react-query'; import { getDatasetGroupsProgress } from 'services/datasets'; import { ValidationProgressData, SSEEventData } from 'types/datasetGroups'; import { datasetQueryKeys } from 'utils/queryKeys'; +import { useValidationSessions } from 'hooks/useValidationSessions'; const ValidationSessions: FC = () => { const { t } = useTranslation(); - const [progresses, setProgresses] = useState([]); - - const { data: progressData, refetch } = useQuery( - datasetQueryKeys.GET_DATASET_GROUP_PROGRESS(), - () => getDatasetGroupsProgress(), - { - onSuccess: (data) => { - setProgresses(data); - }, - } - ); - - useEffect(() => { - if (!progressData) return; - - const handleUpdate = (sessionId: string, newData: SSEEventData) => { - setProgresses((prevProgresses) => - prevProgresses.map((progress) => - progress.id === sessionId ? { ...progress, ...newData } : progress - ) - ); - }; - - const eventSources = progressData.map((progress) => { - if ( - progress.validationStatus !== 'Success' && - progress.progressPercentage !== 100 - ) - return sse(`/${progress.id}`, 'dataset', (data: SSEEventData) => { - console.log(`New data for notification ${progress.id}:`, data); - handleUpdate(data.sessionId, data); - }); - }); - return () => { - eventSources.forEach((eventSource) => eventSource?.close()); - console.log('SSE connections closed'); - }; - }, [progressData, refetch]); + const progresses = useValidationSessions(); return (
          diff --git a/GUI/src/types/testModel.ts b/GUI/src/types/testModel.ts index 4efe750e..096ea869 100644 --- a/GUI/src/types/testModel.ts +++ b/GUI/src/types/testModel.ts @@ -3,6 +3,4 @@ export type PredictionInput = { predictedClasses: string[]; averageConfidence: number; predictedProbabilities: number[]; -}; - -export type FormattedPrediction = string; +}; \ No newline at end of file diff --git a/GUI/src/utils/endpoints.ts b/GUI/src/utils/endpoints.ts index 0e7b08e7..af995179 100644 --- a/GUI/src/utils/endpoints.ts +++ b/GUI/src/utils/endpoints.ts @@ -61,7 +61,7 @@ export const dataModelsEndpoints = { CREATE_DATA_MODEL: (): string => `classifier/datamodel/create`, UPDATE_DATA_MODEL: (): string => `classifier/datamodel/update`, DELETE_DATA_MODEL: (): string => `classifier/datamodel/delete`, - RETRAIN_DATA_MODEL: (): string => `classifier/datamodel/re-train`, + RETRAIN_DATA_MODEL: (): string => `classifier/datamodel/retrain`, GET_DATA_MODEL_PROGRESS: (): string => `classifier/datamodel/progress`, }; diff --git a/GUI/src/utils/testModelUtil.ts b/GUI/src/utils/testModelUtil.ts index 70de9499..bcc0bed7 100644 --- a/GUI/src/utils/testModelUtil.ts +++ b/GUI/src/utils/testModelUtil.ts @@ -1,6 +1,6 @@ -import { FormattedPrediction, PredictionInput } from "types/testModel"; +import { PredictionInput } from "types/testModel"; -export function formatPredictions(input: PredictionInput): FormattedPrediction[] { +export function formatPredictions(input: PredictionInput): string[] { const { predictedClasses, predictedProbabilities } = input; return predictedClasses.map((predictedClass, index) => { From e52e601a4e576ff7e29e77ff7186c982e4581a12 Mon Sep 17 00:00:00 2001 From: pamodaDilranga Date: Thu, 29 Aug 2024 16:10:18 +0530 Subject: [PATCH 564/582] export corrected text python update --- docker-compose.yml | 1 + file-handler/constants.py | 1 + file-handler/file_handler_api.py | 18 +++++++++++++++--- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 7c9f09f5..7278bcf9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -219,6 +219,7 @@ services: - OUTLOOK_ACTIVE_MODEL_DELETE_URL=http://model-inference:8003/classifier/datamodel/deployment/outlook/delete - TEST_MODEL_DELETE_URL=http://model-inference:8003/classifier/datamodel/deployment/testing/delete - MODEL_METADATA_DELETE_URL=http://ruuter-private:8088/classifier/datamodel/metadata/delete + - CORRECTED_TEXT_EXPORT=http://ruuter-private:8088/classifier/inference/corrected-metadata-export.yml ports: - "8000:8000" networks: diff --git a/file-handler/constants.py b/file-handler/constants.py index 9fd2197b..8e3fbbf4 100644 --- a/file-handler/constants.py +++ b/file-handler/constants.py @@ -86,6 +86,7 @@ def GET_S3_FERRY_PAYLOAD(destinationFilePath: str, destinationStorageType: str, OUTLOOK_ACTIVE_MODEL_DELETE_URL = os.getenv("OUTLOOK_ACTIVE_MODEL_DELETE_URL") TEST_MODEL_DELETE_URL = os.getenv("TEST_MODEL_DELETE_URL") MODEL_METADATA_DELETE_URL = os.getenv("MODEL_METADATA_DELETE_URL") +CORRECTED_TEXT_EXPORT = os.getenv("CORRECTED_TEXT_EXPORT") # Dataset locations TEMP_DATASET_LOCATION = "/dataset/{dg_id}/temp/temp_dataset.json" diff --git a/file-handler/file_handler_api.py b/file-handler/file_handler_api.py index f2c27a36..73d1949c 100644 --- a/file-handler/file_handler_api.py +++ b/file-handler/file_handler_api.py @@ -417,7 +417,7 @@ async def delete_dataset_files(request: Request): print(f"Error in delete_dataset_files: {e}") raise HTTPException(status_code=500, detail=str(e)) -@app.post("/datamodel/data/corrected/download") #Download Filtered +@app.post("/datamodel/data/corrected/download") async def download_and_convert(request: Request, exportData: ExportCorrectedDataFile, backgroundTasks: BackgroundTasks): cookie = request.cookies.get("customJwtCookie") await authenticate_user(f'customJwtCookie={cookie}') @@ -427,8 +427,20 @@ async def download_and_convert(request: Request, exportData: ExportCorrectedData if export_type not in ["xlsx", "yaml", "json"]: raise HTTPException(status_code=500, detail=EXPORT_TYPE_ERROR) - # get json payload by calling to Ruuter - json_data = {} + headers = { + 'Content-Type': 'application/json', + 'Cookie': f'customJwtCookie={cookie}' + } + + payload = { + "platform" : platform, + "sortType" : "asc" + } + + response = requests.post(CORRECTED_TEXT_EXPORT, headers=headers, json=payload) + + response_data = response.json() + json_data = response_data["data"] now = datetime.now() formatted_time_date = now.strftime("%Y%m%d_%H%M%S") result_string = f"corrected_text_{formatted_time_date}" From cc96f8a273bd281e09c58f41372dc675af2a4633 Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Thu, 29 Aug 2024 16:20:29 +0530 Subject: [PATCH 565/582] timestamp-filter: add sort form current timestamp --- DSL/Resql/get-paginated-data-model-metadata.sql | 6 ++++-- DSL/Resql/get-paginated-dataset-group-metadata.sql | 6 ++++-- .../DSL/GET/classifier/datamodel/overview.yml | 5 +++++ .../DSL/GET/classifier/datasetgroup/overview.yml | 5 +++++ 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/DSL/Resql/get-paginated-data-model-metadata.sql b/DSL/Resql/get-paginated-data-model-metadata.sql index c780be20..b397460e 100644 --- a/DSL/Resql/get-paginated-data-model-metadata.sql +++ b/DSL/Resql/get-paginated-data-model-metadata.sql @@ -29,7 +29,9 @@ WHERE AND (:platform = 'all' OR dt.deployment_env = :platform::Deployment_Env) AND (:dataset_group = -1 OR dt.connected_dg_id = :dataset_group) ORDER BY - CASE WHEN :sort_type = 'asc' THEN dt.model_name END ASC, - CASE WHEN :sort_type = 'desc' THEN dt.model_name END DESC + CASE WHEN :sort_by = 'name' AND :sort_type = 'asc' THEN dt.model_name END ASC, + CASE WHEN :sort_by = 'name' AND :sort_type = 'desc' THEN dt.model_name END DESC, + CASE WHEN :sort_by = 'created_timestamp' AND :sort_type = 'desc' THEN dt.created_timestamp END ASC, + CASE WHEN :sort_by = 'created_timestamp' AND :sort_type = 'desc' THEN dt.created_timestamp END DESC OFFSET ((GREATEST(:page, 1) - 1) * :page_size) LIMIT :page_size; diff --git a/DSL/Resql/get-paginated-dataset-group-metadata.sql b/DSL/Resql/get-paginated-dataset-group-metadata.sql index e8d9270c..c14cb51f 100644 --- a/DSL/Resql/get-paginated-dataset-group-metadata.sql +++ b/DSL/Resql/get-paginated-dataset-group-metadata.sql @@ -20,6 +20,8 @@ WHERE AND (:validation_status = 'all' OR dt.validation_status = :validation_status::Validation_Status) AND (:group_name = 'all' OR dt.group_name = :group_name) ORDER BY - CASE WHEN :sorting = 'asc' THEN dt.group_name END ASC, - CASE WHEN :sorting = 'dsc' THEN dt.group_name END DESC + CASE WHEN :sort_by = 'name' AND :sorting = 'asc' THEN dt.group_name END ASC, + CASE WHEN :sort_by = 'name' AND :sorting = 'desc' THEN dt.group_name END DESC, + CASE WHEN :sort_by = 'created_timestamp' AND :sorting = 'asc' THEN dt.created_timestamp END ASC, + CASE WHEN :sort_by = 'created_timestamp' AND :sorting = 'desc' THEN dt.created_timestamp END DESC OFFSET ((GREATEST(:page, 1) - 1) * :page_size) LIMIT :page_size; diff --git a/DSL/Ruuter.private/DSL/GET/classifier/datamodel/overview.yml b/DSL/Ruuter.private/DSL/GET/classifier/datamodel/overview.yml index 58b47340..2b69d707 100644 --- a/DSL/Ruuter.private/DSL/GET/classifier/datamodel/overview.yml +++ b/DSL/Ruuter.private/DSL/GET/classifier/datamodel/overview.yml @@ -41,6 +41,9 @@ declaration: - field: isProductionModel type: boolean description: "Parameter 'isProductionModel'" + - field: sortBy + type: string + description: "Parameter 'sortBy'" extract_data: assign: @@ -55,6 +58,7 @@ extract_data: training_status: ${incoming.params.trainingStatus} deployment_maturity: ${incoming.params.deploymentMaturity} is_production_model: ${incoming.params.isProductionModel} + sort_by: ${incoming.params.sortBy} next: check_production_model_status check_production_model_status: @@ -86,6 +90,7 @@ get_data_model_meta_data_overview: training_status: ${training_status} deployment_maturity: ${deployment_maturity} sort_type: ${sort_type} + sort_by: ${sort_by} result: res_model next: check_status diff --git a/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/overview.yml b/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/overview.yml index ae3324a2..1930564e 100644 --- a/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/overview.yml +++ b/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/overview.yml @@ -32,6 +32,9 @@ declaration: - field: validationStatus type: string description: "Parameter field 'validationStatus'" + - field: sortBy + type: string + description: "Parameter field 'sortBy'" extract_data: assign: @@ -43,6 +46,7 @@ extract_data: group_name: ${incoming.params.groupName} validation_status: ${incoming.params.validationStatus} sort_type: ${incoming.params.sortType} + sort_by: ${incoming.params.sortBy} next: get_dataset_meta_data_overview get_dataset_meta_data_overview: @@ -58,6 +62,7 @@ get_dataset_meta_data_overview: patch_version: ${patch_version} group_name: ${group_name} validation_status: ${validation_status} + sort_by: ${sort_by} result: res_dataset next: check_status From 45fb8b65b8c9a0620e3f3bbcd9a9eb6835c4ea6d Mon Sep 17 00:00:00 2001 From: "kalsara.magamage_roo" Date: Thu, 29 Aug 2024 17:53:10 +0530 Subject: [PATCH 566/582] timestamp-filter: add sort by validations --- .../DSL/GET/classifier/datamodel/overview.yml | 11 +++++++++++ .../DSL/GET/classifier/datasetgroup/overview.yml | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/DSL/Ruuter.private/DSL/GET/classifier/datamodel/overview.yml b/DSL/Ruuter.private/DSL/GET/classifier/datamodel/overview.yml index 2b69d707..f50e6a4f 100644 --- a/DSL/Ruuter.private/DSL/GET/classifier/datamodel/overview.yml +++ b/DSL/Ruuter.private/DSL/GET/classifier/datamodel/overview.yml @@ -59,6 +59,17 @@ extract_data: deployment_maturity: ${incoming.params.deploymentMaturity} is_production_model: ${incoming.params.isProductionModel} sort_by: ${incoming.params.sortBy} + next: check_sort_by + +check_sort_by: + switch: + - condition: ${sort_by === 'name' || sort_by === 'created_timestamp'} + next: check_production_model_status + next: set_sort_by + +set_sort_by: + assign: + sort_by: 'name' next: check_production_model_status check_production_model_status: diff --git a/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/overview.yml b/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/overview.yml index 1930564e..a0324162 100644 --- a/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/overview.yml +++ b/DSL/Ruuter.private/DSL/GET/classifier/datasetgroup/overview.yml @@ -47,6 +47,17 @@ extract_data: validation_status: ${incoming.params.validationStatus} sort_type: ${incoming.params.sortType} sort_by: ${incoming.params.sortBy} + next: check_sort_by + +check_sort_by: + switch: + - condition: ${sort_by === 'name' || sort_by === 'created_timestamp'} + next: get_dataset_meta_data_overview + next: set_sort_by + +set_sort_by: + assign: + sort_by: 'name' next: get_dataset_meta_data_overview get_dataset_meta_data_overview: From ed6eeae93a5d6a5c31e2f338c2ef63f6956a23be Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Thu, 29 Aug 2024 20:24:24 +0530 Subject: [PATCH 567/582] bug fixes --- DSL/Ruuter.private/DSL/GET/.guard | 2 +- DSL/Ruuter.private/DSL/POST/.guard | 2 +- DSL/Ruuter.private/DSL/POST/accounts/.guard | 2 +- .../TEMPLATES/check-user-authority-admin.yml | 2 +- .../DSL/TEMPLATES/check-user-authority.yml | 2 +- .../FormElements/FormSelect/index.tsx | 10 +- .../DatasetDetailedViewTable.tsx | 7 +- .../IntegrationModals/IntegrationModals.tsx | 6 +- .../ViewDatasetGroupModalController.tsx | 69 +++++- GUI/src/enums/datasetEnums.ts | 1 + GUI/src/pages/CorrectedTexts/index.tsx | 90 +++++++- .../pages/DataModels/ConfigureDataModel.tsx | 204 ++++++++++++------ GUI/src/pages/DataModels/CreateDataModel.tsx | 10 +- .../DatasetGroups/CreateDatasetGroup.tsx | 2 + .../pages/DatasetGroups/ViewDatasetGroup.tsx | 136 ++++++------ GUI/src/pages/UserManagement/UserModal.tsx | 3 + GUI/src/services/api-dev.ts | 3 + GUI/src/services/datasets.ts | 20 +- GUI/src/utils/endpoints.ts | 2 + GUI/translations/en/common.json | 7 +- 20 files changed, 414 insertions(+), 166 deletions(-) diff --git a/DSL/Ruuter.private/DSL/GET/.guard b/DSL/Ruuter.private/DSL/GET/.guard index 9dfc1e3c..44e4f6ec 100644 --- a/DSL/Ruuter.private/DSL/GET/.guard +++ b/DSL/Ruuter.private/DSL/GET/.guard @@ -24,5 +24,5 @@ guard_success: guard_fail: return: "unauthorized" - status: 400 + status: 401 next: end diff --git a/DSL/Ruuter.private/DSL/POST/.guard b/DSL/Ruuter.private/DSL/POST/.guard index 9dfc1e3c..44e4f6ec 100644 --- a/DSL/Ruuter.private/DSL/POST/.guard +++ b/DSL/Ruuter.private/DSL/POST/.guard @@ -24,5 +24,5 @@ guard_success: guard_fail: return: "unauthorized" - status: 400 + status: 401 next: end diff --git a/DSL/Ruuter.private/DSL/POST/accounts/.guard b/DSL/Ruuter.private/DSL/POST/accounts/.guard index fae02a67..ac8cec0d 100644 --- a/DSL/Ruuter.private/DSL/POST/accounts/.guard +++ b/DSL/Ruuter.private/DSL/POST/accounts/.guard @@ -24,5 +24,5 @@ guard_success: guard_fail: return: "unauthorized" - status: 400 + status: 401 next: end diff --git a/DSL/Ruuter.private/DSL/TEMPLATES/check-user-authority-admin.yml b/DSL/Ruuter.private/DSL/TEMPLATES/check-user-authority-admin.yml index 6fa923f1..0e8f64f8 100644 --- a/DSL/Ruuter.private/DSL/TEMPLATES/check-user-authority-admin.yml +++ b/DSL/Ruuter.private/DSL/TEMPLATES/check-user-authority-admin.yml @@ -42,7 +42,7 @@ return_authorized: next: end return_unauthorized: - status: 400 + status: 401 return: false next: end diff --git a/DSL/Ruuter.private/DSL/TEMPLATES/check-user-authority.yml b/DSL/Ruuter.private/DSL/TEMPLATES/check-user-authority.yml index 496199e6..d1d06588 100644 --- a/DSL/Ruuter.private/DSL/TEMPLATES/check-user-authority.yml +++ b/DSL/Ruuter.private/DSL/TEMPLATES/check-user-authority.yml @@ -42,7 +42,7 @@ return_authorized: next: end return_unauthorized: - status: 200 + status: 401 return: false next: end diff --git a/GUI/src/components/FormElements/FormSelect/index.tsx b/GUI/src/components/FormElements/FormSelect/index.tsx index 43562b4b..7c250ab7 100644 --- a/GUI/src/components/FormElements/FormSelect/index.tsx +++ b/GUI/src/components/FormElements/FormSelect/index.tsx @@ -58,16 +58,18 @@ const FormSelect = forwardRef( const [selectedItem, setSelectedItem] = useState( options?.find((o) => o.value === defaultValue) || - options?.find((o) => typeof o.value !== 'string' && o.value?.name === defaultValue) || + options?.find((o) => typeof o.value === 'object' && o.value?.name === defaultValue) || null ); useEffect(() => { const newSelectedItem = - options?.find((o) => o.value === defaultValue) || - options?.find((o) => typeof o.value !== 'string' && o.value?.name === defaultValue) || + options?.find((o) => o.value === defaultValue) || + options?.find( + (o) => typeof o.value === 'object' && o.value?.name && o.value?.name === defaultValue + ) || null; - setSelectedItem(newSelectedItem); + newSelectedItem && setSelectedItem(newSelectedItem); }, [defaultValue, options]); const { diff --git a/GUI/src/components/molecules/DatasetDetailedViewTable/DatasetDetailedViewTable.tsx b/GUI/src/components/molecules/DatasetDetailedViewTable/DatasetDetailedViewTable.tsx index 0f503569..c639f945 100644 --- a/GUI/src/components/molecules/DatasetDetailedViewTable/DatasetDetailedViewTable.tsx +++ b/GUI/src/components/molecules/DatasetDetailedViewTable/DatasetDetailedViewTable.tsx @@ -89,7 +89,12 @@ const DatasetDetailedViewTable = ({ isHeaderLight={false} header={
          -
          +
          navigate(0)}> diff --git a/GUI/src/components/molecules/IntegrationModals/IntegrationModals.tsx b/GUI/src/components/molecules/IntegrationModals/IntegrationModals.tsx index e3c96cc7..588bec31 100644 --- a/GUI/src/components/molecules/IntegrationModals/IntegrationModals.tsx +++ b/GUI/src/components/molecules/IntegrationModals/IntegrationModals.tsx @@ -96,6 +96,8 @@ const IntegrationModals = ({ platform: channel?.toLowerCase(), }); }} + disabled={platformDisableMutation.isLoading} + showLoadingIcon={platformDisableMutation.isLoading} > {t('global.disconnect')} @@ -128,6 +130,8 @@ const IntegrationModals = ({ platform: channel?.toLowerCase(), }); }} + disabled={platformEnableMutation.isLoading} + showLoadingIcon={platformEnableMutation.isLoading} > {t('global.connect')} @@ -154,4 +158,4 @@ const IntegrationModals = ({ ); }; -export default IntegrationModals; +export default IntegrationModals; \ No newline at end of file diff --git a/GUI/src/components/molecules/ViewDatasetGroupModalController/ViewDatasetGroupModalController.tsx b/GUI/src/components/molecules/ViewDatasetGroupModalController/ViewDatasetGroupModalController.tsx index 85610c39..87978835 100644 --- a/GUI/src/components/molecules/ViewDatasetGroupModalController/ViewDatasetGroupModalController.tsx +++ b/GUI/src/components/molecules/ViewDatasetGroupModalController/ViewDatasetGroupModalController.tsx @@ -30,7 +30,15 @@ const ViewDatasetGroupModalController = ({ deleteRow, file, exportFormat, - isImportDataLoading + isImportDataLoading, + confirmationModalTitle, + confirmationModalDesc, + onConfirmationConfirm, + majorUpdateLoading, + patchUpdateLoading, + minorUpdateLoading, + confirmationFLow, + deleteDatasetMutationLoading }: { setImportStatus: React.Dispatch>; handleFileSelect: (file: File | undefined) => void; @@ -53,7 +61,15 @@ const ViewDatasetGroupModalController = ({ deleteRow: (dataRow: any) => void; file: File | undefined; exportFormat: string; - isImportDataLoading: boolean + isImportDataLoading: boolean; + confirmationModalTitle: string; + confirmationModalDesc: string; + onConfirmationConfirm: () => any; + majorUpdateLoading: boolean; + patchUpdateLoading: boolean; + minorUpdateLoading: boolean; + deleteDatasetMutationLoading: boolean; + confirmationFLow: string; }) => { const { close } = useDialog(); const { t } = useTranslation(); @@ -115,8 +131,8 @@ const ViewDatasetGroupModalController = ({ disabled={!importFormat} /> {importStatus === 'STARTED' && ( -
          -
          +
          +
          {t( 'datasetGroups.detailedView.modals.import.uploadInProgress' )} @@ -225,6 +241,51 @@ const ViewDatasetGroupModalController = ({ {t('datasetGroups.detailedView.modals.delete.description')} )} + {isModalOpen && + openedModalContext === + ViewDatasetGroupModalContexts.CONFIRMATION_MODAL && ( + + + {confirmationFLow === 'update' ? ( + + ) : ( + + )} +
          + } + > + {confirmationModalDesc} + + )} ); }; diff --git a/GUI/src/enums/datasetEnums.ts b/GUI/src/enums/datasetEnums.ts index c10868b8..89e5fd30 100644 --- a/GUI/src/enums/datasetEnums.ts +++ b/GUI/src/enums/datasetEnums.ts @@ -21,6 +21,7 @@ export enum ViewDatasetGroupModalContexts { IMPORT_MODAL = 'IMPORT_MODAL', PATCH_UPDATE_MODAL = 'PATCH_UPDATE_MODAL', DELETE_ROW_MODAL = 'DELETE_ROW_MODAL', + CONFIRMATION_MODAL = 'CONFIRMATION_MODAL', NULL = 'NULL', } diff --git a/GUI/src/pages/CorrectedTexts/index.tsx b/GUI/src/pages/CorrectedTexts/index.tsx index bdfed0c6..c502168b 100644 --- a/GUI/src/pages/CorrectedTexts/index.tsx +++ b/GUI/src/pages/CorrectedTexts/index.tsx @@ -1,13 +1,16 @@ import { FC, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ButtonAppearanceTypes } from 'enums/commonEnums'; -import { Button, FormSelect } from 'components'; -import { useQuery } from '@tanstack/react-query'; +import { Button, Dialog, FormRadios, FormSelect } from 'components'; +import { useMutation, useQuery } from '@tanstack/react-query'; import { correctedTextEndpoints } from 'utils/endpoints'; import apiDev from '../../services/api-dev'; import { InferencePayload } from 'types/correctedTextTypes'; import { PaginationState } from '@tanstack/react-table'; import CorrectedTextsTable from 'components/molecules/CorrectedTextTables/CorrectedTextsTables'; +import formats from '../../config/formatsConfig.json'; +import { handleDownload } from 'utils/datasetGroupsUtils'; +import { exportCorrectedTexts } from 'services/datasets'; const CorrectedTexts: FC = () => { const { t } = useTranslation(); @@ -23,7 +26,11 @@ const CorrectedTexts: FC = () => { pageIndex: 0, pageSize: 5, }); - + const [modalTitle, setModalTitle] = useState(''); + const [modalDiscription, setModalDiscription] = useState(''); + const [modalType, setModalType] = useState(''); + const [exportFormat, setExportFormat] = useState(''); + const [isModalOpen, setIsModalOpen] = useState(false); const { data: correctedTextData, isLoading, @@ -58,6 +65,30 @@ const CorrectedTexts: FC = () => { [name]: value, })); }; + + const handleExport = () => { + mutate(); + }; + + const { mutate, isLoading: downloadLoading } = useMutation({ + mutationFn: async () => + await exportCorrectedTexts(filters.platform, exportFormat), + onSuccess: async (response) => { + handleDownload(response, exportFormat); + setIsModalOpen(true); + setModalTitle(t('correctedTexts.exportSuccessTitle') ?? ''); + setModalDiscription(t('correctedTexts.exportDataUnsucessDesc') ?? ''); + setModalType('success'); + }, + onError: async () => { + setIsModalOpen(true); + setModalTitle(t('correctedTexts.exportDataUnsucessTitle') ?? ''); + setModalDiscription(t('correctedTexts.exportDataUnsucessDesc') ?? ''); + setModalType('error'); + }, + }); + + console.log(filters); return (
          @@ -66,6 +97,11 @@ const CorrectedTexts: FC = () => { appearance={ButtonAppearanceTypes.PRIMARY} size="m" onClick={() => { + setIsModalOpen(true); + setModalType('export'); + setModalTitle( + t('datasetGroups.detailedView.modals.export.export') ?? '' + ); }} > {t('correctedTexts.export')} @@ -142,6 +178,54 @@ const CorrectedTexts: FC = () => { setEnableFetch={setEnableFetch} />
          + + setIsModalOpen(false)} + isOpen={isModalOpen} + title={modalTitle} + footer={ + modalType === 'export' && ( +
          + + +
          + ) + } + > + {modalType === 'export' ? ( +
          +

          + {t('datasetGroups.detailedView.modals.export.fileFormatlabel')} +

          +
          + +
          +
          + ) : ( + <> +

          {modalDiscription}

          + + )} +
          ); }; diff --git a/GUI/src/pages/DataModels/ConfigureDataModel.tsx b/GUI/src/pages/DataModels/ConfigureDataModel.tsx index f36ff5ba..7b854c6d 100644 --- a/GUI/src/pages/DataModels/ConfigureDataModel.tsx +++ b/GUI/src/pages/DataModels/ConfigureDataModel.tsx @@ -1,7 +1,7 @@ -import { FC, useState } from 'react'; +import { FC, useRef, useState } from 'react'; import { useMutation, useQuery } from '@tanstack/react-query'; import { Link, useNavigate } from 'react-router-dom'; -import { Button, Card } from 'components'; +import { Button, Card, Dialog } from 'components'; import { useDialog } from 'hooks/useDialog'; import BackArrowButton from 'assets/BackArrowButton'; import { @@ -49,7 +49,11 @@ const ConfigureDataModel: FC = ({ maturity: '', version: '', }); - + const [modalOpen, setModalOpen] = useState(false); + const [modalType, setModalType] = useState(''); + const [modalTitle, setModalTitle] = useState(''); + const [modalDiscription, setModalDiscription] = useState(''); + const modalFunciton = useRef(() => {}); const { isLoading } = useQuery( dataModelsQueryKeys.GET_META_DATA(id), () => getMetadata(id), @@ -110,25 +114,31 @@ const ConfigureDataModel: FC = ({ if (updateType) { if (availableProdModels?.includes(dataModel.platform)) { - open({ - title: t('dataModels.createDataModel.replaceTitle'), - content: t('dataModels.createDataModel.replaceDesc'), - footer: ( -
          - - -
          - ), - }); + openModal( + t('dataModels.createDataModel.replaceDesc'), + t('dataModels.createDataModel.replaceTitle'), + () => updateDataModelMutation.mutate(updatedPayload), + 'replace' + ); + // open({ + // title: t('dataModels.createDataModel.replaceTitle'), + // content: t('dataModels.createDataModel.replaceDesc'), + // footer: ( + //
          + // + // + //
          + // ), + // }); } else { updateDataModelMutation.mutate(updatedPayload); } @@ -175,7 +185,8 @@ const ConfigureDataModel: FC = ({ const handleDelete = () => { if ( dataModel.platform === Platform.JIRA || - dataModel.platform === Platform.OUTLOOK) { + dataModel.platform === Platform.OUTLOOK + ) { open({ title: t('dataModels.configureDataModel.deleteErrorTitle'), content:

          {t('dataModels.configureDataModel.deleteErrorDesc')}

          , @@ -191,28 +202,34 @@ const ConfigureDataModel: FC = ({ ), }); } else { - open({ - title: t('dataModels.configureDataModel.deleteConfirmation'), - content: ( -

          {t('dataModels.configureDataModel.deleteConfirmationDesc')}

          - ), - footer: ( -
          - - -
          - ), - }); + openModal( + t('dataModels.configureDataModel.deleteConfirmationDesc'), + t('dataModels.configureDataModel.deleteConfirmation'), + () => deleteDataModelMutation.mutate(dataModel.modelId), + 'delete' + ); + // open({ + // title: t('dataModels.configureDataModel.deleteConfirmation'), + // content: ( + //

          {t('dataModels.configureDataModel.deleteConfirmationDesc')}

          + // ), + // footer: ( + //
          + // + // + //
          + // ), + // }); } }; @@ -237,6 +254,7 @@ const ConfigureDataModel: FC = ({ onSuccess: async () => { close(); navigate(0); + setModalOpen(false) }, onError: () => { open({ @@ -248,6 +266,18 @@ const ConfigureDataModel: FC = ({ }, }); + const openModal = ( + content: string, + title: string, + onConfirm: () => void, + modalType: string + ) => { + setModalOpen(true); + setModalType(modalType); + setModalDiscription(content); + setModalTitle(title); + modalFunciton.current = onConfirm; + }; return (
          @@ -302,40 +332,78 @@ const ConfigureDataModel: FC = ({ background: 'white', }} > - - -
          - ), - }) + openModal( + t('dataModels.configureDataModel.confirmRetrainDesc'), + t('dataModels.configureDataModel.retrain'), + () => retrainDataModelMutation.mutate(dataModel.modelId), + 'retrain' + ) } > {t('dataModels.configureDataModel.retrain')} -
          + + setModalOpen(false)} + isOpen={modalOpen} + title={modalTitle} + footer={ +
          + + {modalType === 'retrain' ? ( + + ) : modalType === 'delete' ? ( + + ) : ( + + )} +
          + } + > +
          {modalDiscription}
          +
          ); }; diff --git a/GUI/src/pages/DataModels/CreateDataModel.tsx b/GUI/src/pages/DataModels/CreateDataModel.tsx index 7c21181d..c5bf4438 100644 --- a/GUI/src/pages/DataModels/CreateDataModel.tsx +++ b/GUI/src/pages/DataModels/CreateDataModel.tsx @@ -12,7 +12,11 @@ import { ButtonAppearanceTypes } from 'enums/commonEnums'; import { createDataModel, getDataModelsOverview } from 'services/data-models'; import { dataModelsQueryKeys, integrationQueryKeys } from 'utils/queryKeys'; import { getIntegrationStatus } from 'services/integration'; -import { CreateDataModelPayload, DataModel, ErrorsType } from 'types/dataModels'; +import { + CreateDataModelPayload, + DataModel, + ErrorsType, +} from 'types/dataModels'; const CreateDataModel: FC = () => { const { t } = useTranslation(); @@ -216,7 +220,9 @@ const CreateDataModel: FC = () => { background: 'white', }} > - diff --git a/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx b/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx index add85f2a..4177f58a 100644 --- a/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx +++ b/GUI/src/pages/DatasetGroups/ViewDatasetGroup.tsx @@ -77,12 +77,14 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { const [updatePriority, setUpdatePriority] = useState( UpdatePriority.NULL ); + const [confirmationTitle, setConfirmationTitle] = useState(''); + const [confirmationDesc, setConfirmationDesc] = useState(''); const [openedModalContext, setOpenedModalContext] = useState(ViewDatasetGroupModalContexts.NULL); const [isModalOpen, setIsModalOpen] = useState(false); - const navigate = useNavigate(); - + const changeConfirmationFunction = useRef(() => {}); + const [confirmationFLow, setConfirmationFlow] = useState(''); useEffect(() => { setFetchEnabled(false); }, []); @@ -341,6 +343,20 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { majorUpdateDatasetGroupMutation.mutate(payload); }; + const openConfirmationModal = ( + content: string, + title: string, + onConfirm: () => void, + flow: string + ) => { + setOpenedModalContext(ViewDatasetGroupModalContexts.CONFIRMATION_MODAL); + setIsModalOpen(true); + setConfirmationDesc(content); + setConfirmationTitle(title); + changeConfirmationFunction.current = onConfirm; + setConfirmationFlow(flow); + }; + const datasetGroupUpdate = () => { const classHierarchyError = validateClassHierarchy(nodes) || nodesError; const validationRulesError = validateValidationRules(validationRules); @@ -360,52 +376,32 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { } ); - const openConfirmationModal = ( - content: string, - title: string, - onConfirm: () => void - ) => { - open({ - content, - title, - footer: ( -
          - - -
          - ), - }); - }; - if (classHierarchyError || validationRulesError || nodesError) { return; - } - - if (isMajorUpdateDetected) { - openConfirmationModal( - t('datasetGroups.detailedView.confirmMajorUpdatesDesc'), - t('datasetGroups.detailedView.confirmMajorUpdatesTitle'), - handleMajorUpdate - ); - } else if (minorPayload) { - openConfirmationModal( - t('datasetGroups.detailedView.confirmMinorUpdatesDesc'), - t('datasetGroups.detailedView.confirmMinorUpdatesTitle'), - () => minorUpdateMutation.mutate(minorPayload) - ); - } else if (patchPayload) { - openConfirmationModal( - t('datasetGroups.detailedView.confirmPatchUpdatesDesc'), - t('datasetGroups.detailedView.confirmPatchUpdatesTitle'), - () => patchUpdateMutation.mutate(patchPayload) - ); - } - + } + + if (isMajorUpdateDetected) { + openConfirmationModal( + t('datasetGroups.detailedView.confirmMajorUpdatesDesc'), + t('datasetGroups.detailedView.confirmMajorUpdatesTitle'), + handleMajorUpdate, + 'update' + ); + } else if (minorPayload) { + openConfirmationModal( + t('datasetGroups.detailedView.confirmMinorUpdatesDesc'), + t('datasetGroups.detailedView.confirmMinorUpdatesTitle'), + () => minorUpdateMutation.mutate(minorPayload), + 'update' + ); + } else if (patchPayload) { + openConfirmationModal( + t('datasetGroups.detailedView.confirmPatchUpdatesDesc'), + t('datasetGroups.detailedView.confirmPatchUpdatesTitle'), + () => patchUpdateMutation.mutate(patchPayload), + 'update' + ); + } }; const majorUpdateDatasetGroupMutation = useMutation({ @@ -486,38 +482,20 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { - -
          - ), - }) + openConfirmationModal( + t('datasetGroups.detailedView.modals.delete.title'), + t('datasetGroups.detailedView.modals.delete.title'), + () => handleDeleteDataset(), + 'delete' + ) } > {t('datasetGroups.detailedView.delete') ?? ''} -
          @@ -544,9 +522,17 @@ const ViewDatasetGroup: FC> = ({ dgId, setView }) => { file={file} exportFormat={exportFormat} isImportDataLoading={importDataMutation.isLoading} + confirmationModalTitle={confirmationTitle} + confirmationModalDesc={confirmationDesc} + onConfirmationConfirm={() => changeConfirmationFunction.current()} + majorUpdateLoading={majorUpdateDatasetGroupMutation.isLoading} + patchUpdateLoading={patchUpdateMutation.isLoading} + minorUpdateLoading={minorUpdateMutation.isLoading} + confirmationFLow={confirmationFLow} + deleteDatasetMutationLoading={deleteDatasetMutation.isLoading} />
          ); }; -export default ViewDatasetGroup; +export default ViewDatasetGroup; \ No newline at end of file diff --git a/GUI/src/pages/UserManagement/UserModal.tsx b/GUI/src/pages/UserManagement/UserModal.tsx index 897e947f..43235b72 100644 --- a/GUI/src/pages/UserManagement/UserModal.tsx +++ b/GUI/src/pages/UserManagement/UserModal.tsx @@ -186,6 +186,9 @@ const UserModal: FC = ({ onClose, user, isModalOpen }) => { (!user && !isValidIdentification) || (user && !hasChangedFields()) } + showLoadingIcon={ + userEditMutation.isLoading || userCreateMutation.isLoading + } > {t('global.confirm')} diff --git a/GUI/src/services/api-dev.ts b/GUI/src/services/api-dev.ts index c3e746f8..d85bd9a8 100644 --- a/GUI/src/services/api-dev.ts +++ b/GUI/src/services/api-dev.ts @@ -14,6 +14,9 @@ instance.interceptors.response.use( return axiosResponse; }, (error: AxiosError) => { + if (error.response?.status === 401) { + window.location.href = import.meta.env.REACT_APP_CUSTOMER_SERVICE_LOGIN + } return Promise.reject(new Error(error.message)); } ); diff --git a/GUI/src/services/datasets.ts b/GUI/src/services/datasets.ts index 438f37ad..0e19482d 100644 --- a/GUI/src/services/datasets.ts +++ b/GUI/src/services/datasets.ts @@ -1,4 +1,4 @@ -import { datasetsEndpoints } from 'utils/endpoints'; +import { correctedTextEndpoints, datasetsEndpoints } from 'utils/endpoints'; import apiDev from './api-dev'; import apiExternal from './api-external'; import { PaginationState } from '@tanstack/react-table'; @@ -182,3 +182,21 @@ export async function deleteStopWords(file: File) { ); return data; } + +export async function exportCorrectedTexts( + platform: string, + exportType: string +) { + const headers = { + 'Content-Type': 'application/json', + }; + const { data } = await apiExternal.post( + correctedTextEndpoints.EXPORT_CORRECTED_TEXTS(), + { + platform, + exportType, + }, + { headers, responseType: 'blob' } + ); + return data; +} \ No newline at end of file diff --git a/GUI/src/utils/endpoints.ts b/GUI/src/utils/endpoints.ts index af995179..87197b93 100644 --- a/GUI/src/utils/endpoints.ts +++ b/GUI/src/utils/endpoints.ts @@ -45,6 +45,7 @@ export const correctedTextEndpoints = { sortType: string ) => `/classifier/inference/corrected-metadata?pageNum=${pageNumber}&pageSize=${pageSize}&platform=${platform}&sortType=${sortType}`, + EXPORT_CORRECTED_TEXTS: () => `/datamodel/data/corrected/download` }; export const authEndpoints = { @@ -69,3 +70,4 @@ export const testModelsEndpoints = { GET_MODELS: (): string => `/classifier/testmodel/models`, CLASSIFY_TEST_MODELS: (): string => `/classifier/testmodel/test-data`, }; + diff --git a/GUI/translations/en/common.json b/GUI/translations/en/common.json index 4272bfc4..02c768d0 100644 --- a/GUI/translations/en/common.json +++ b/GUI/translations/en/common.json @@ -337,9 +337,12 @@ "predictedHierarchy": "Predicted Class Hierarchy", "predictedConfidenceProbability": "Predicted Classes Average Confidence Probability", "correctedHierarchy": "Corrected Class Hierarchy", - "correctedConfidenceProbability": "Corrected Classes Average Confidence Probability" + "correctedConfidenceProbability": "Corrected Classes Average Confidence Probability", + "exportSuccessTitle": "Data export was successful", + "exportSuccessDesc": "Your data has been successfully exported.", + "exportDataUnsucessTitle": "Data Export Unsuccessful", + "exportDataUnsucessDesc": "Something went wrong. Please try again." }, - "dataModels": { "productionModels": "Production Models", "dataModels": "Data Models", From 3cebf266289649be63ec6d676eafba6d90ce6336 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Thu, 29 Aug 2024 22:03:49 +0530 Subject: [PATCH 568/582] add sort by date option --- GUI/src/pages/DatasetGroups/index.tsx | 15 ++++++++------- GUI/src/services/datasets.ts | 3 ++- GUI/translations/en/common.json | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/GUI/src/pages/DatasetGroups/index.tsx b/GUI/src/pages/DatasetGroups/index.tsx index 2c52783c..a4622a90 100644 --- a/GUI/src/pages/DatasetGroups/index.tsx +++ b/GUI/src/pages/DatasetGroups/index.tsx @@ -19,7 +19,7 @@ type FilterData = { datasetGroupName: string; version: string; validationStatus: string; - sort: 'asc' | 'desc'; + sort: 'created_timestamp desc' | 'created_timestamp asc' | 'name asc' | 'name desc'; }; const DatasetGroups: FC = () => { @@ -35,7 +35,7 @@ const DatasetGroups: FC = () => { datasetGroupName: 'all', version: 'x.x.x', validationStatus: 'all', - sort: 'asc', + sort: 'created_timestamp desc', }); useEffect(() => { @@ -147,13 +147,14 @@ const DatasetGroups: FC = () => { label="" name="sort" placeholder={ - t('datasetGroups.table.sortBy', { - sortOrder: filters?.sort === 'asc' ? 'A-Z' : 'Z-A', - }) ?? '' + t('datasetGroups.table.sortBy') ?? '' } options={[ - { label: 'A-Z', value: 'asc' }, - { label: 'Z-A', value: 'desc' }, + { label: 'Dataset Group Name A-Z', value: 'name asc' }, + { label: 'Dataset Group Name Z-A', value: 'name desc' }, + { label: 'Create Date Latest First', value: 'created_timestamp desc' }, + { label: 'Create Date Oldest First', value: 'created_timestamp asc' }, + ]} onSelectionChange={(selection) => handleFilterChange('sort', (selection?.value as string) ?? '') diff --git a/GUI/src/services/datasets.ts b/GUI/src/services/datasets.ts index 0e19482d..e4a93d3d 100644 --- a/GUI/src/services/datasets.ts +++ b/GUI/src/services/datasets.ts @@ -28,7 +28,8 @@ export async function getDatasetsOverview( minorVersion, patchVersion, validationStatus, - sortType: sort, + sortBy:sort?.split(" ")?.[0], + sortType: sort?.split(" ")?.[1], pageSize: 12, }, }); diff --git a/GUI/translations/en/common.json b/GUI/translations/en/common.json index 02c768d0..061d3186 100644 --- a/GUI/translations/en/common.json +++ b/GUI/translations/en/common.json @@ -152,7 +152,7 @@ "group": "Dataset Group", "version": "Version", "validationStatus": "Validation Status", - "sortBy": "Sort by name ({{sortOrder}})", + "sortBy": "Sort by", "email": "Email", "actions": "Actions" }, From e30cd064cb3920c162bc8e9a295b20595ccae1a3 Mon Sep 17 00:00:00 2001 From: erangi-ar <111747955+erangi-ar@users.noreply.github.com> Date: Thu, 29 Aug 2024 23:28:55 +0530 Subject: [PATCH 569/582] sort by date filter added --- GUI/src/config/menuConfig.json | 34 -------- GUI/src/config/users.json | 115 -------------------------- GUI/src/pages/DataModels/index.tsx | 52 ++++++++---- GUI/src/pages/DatasetGroups/index.tsx | 39 +++++---- GUI/src/services/data-models.ts | 3 +- GUI/src/types/dataModels.ts | 4 +- GUI/src/types/datasetGroups.ts | 8 ++ GUI/translations/en/common.json | 12 +++ 8 files changed, 79 insertions(+), 188 deletions(-) delete mode 100644 GUI/src/config/menuConfig.json delete mode 100644 GUI/src/config/users.json diff --git a/GUI/src/config/menuConfig.json b/GUI/src/config/menuConfig.json deleted file mode 100644 index e13da714..00000000 --- a/GUI/src/config/menuConfig.json +++ /dev/null @@ -1,34 +0,0 @@ -[ - { - "title": "User Management", - "icon": "MdPeople" - }, - { - "title": "Integration", - "icon": "MdSettings" - }, - { - "title": "Dataset", - "icon": "MdDashboard" - }, - { - "title": "Data Models", - "icon": "MdDashboard" - }, - { - "title": "Classes", - "icon": "MdSettings", - "submenu": [ - { "title": "Classes", "icon": "MdPersonAdd" }, - { "title": "Stop Words", "icon": "MdList" } - ] - }, - { - "title": "Incoming Texts", - "icon": "MdTextFields" - }, - { - "title": "Test Model", - "icon": "MdDashboard" - } -] diff --git a/GUI/src/config/users.json b/GUI/src/config/users.json deleted file mode 100644 index 7a72b470..00000000 --- a/GUI/src/config/users.json +++ /dev/null @@ -1,115 +0,0 @@ -[ - { - "login": "EE40404049985", - "firstName": "admin", - "lastName": "admin", - "userIdCode": "EE40404049985", - "displayName": "admin", - "csaTitle": "admin", - "csaEmail": "admin@admin.ee", - "authorities": [ - "ROLE_ADMINISTRATOR" - ], - "customerSupportStatus": "offline", - "totalPages": 1 - }, - { - "login": "EE38807130279", - "firstName": "Jaanus", - "lastName": "Kääp", - "userIdCode": "EE38807130279", - "displayName": "Jaanus", - "csaTitle": "tester", - "csaEmail": "jaanus@clarifiedsecurity.com", - "authorities": [ - "ROLE_ADMINISTRATOR", - "ROLE_MODEL_TRAINER" - ], - "customerSupportStatus": "offline", - "totalPages": 1 - }, - { - "login": "EE30303039816", - "firstName": "kolmas", - "lastName": "admin", - "userIdCode": "EE30303039816", - "displayName": "kolmas", - "csaTitle": "kolmas", - "csaEmail": "kolmas@admin.ee", - "authorities": [ - "ROLE_MODEL_TRAINER" - ], - "customerSupportStatus": "offline", - "totalPages": 1 - }, - { - "login": "EE30303039914", - "firstName": "Kustuta", - "lastName": "Kasutaja", - "userIdCode": "EE30303039914", - "displayName": "Kustutamiseks", - "csaTitle": "", - "csaEmail": "kustutamind@mail.ee", - "authorities": [ - "ROLE_ADMINISTRATOR" - ], - "customerSupportStatus": "offline", - "totalPages": 1 - }, - { - "login": "EE50001029996", - "firstName": "Nipi", - "lastName": "Tiri", - "userIdCode": "EE50001029996", - "displayName": "Nipi", - "csaTitle": "Dr", - "csaEmail": "nipi@tiri.ee", - "authorities": [ - "ROLE_ADMINISTRATOR" - ], - "customerSupportStatus": "offline", - "totalPages": 1 - }, - { - "login": "EE40404049996", - "firstName": "teine", - "lastName": "admin", - "userIdCode": "EE40404049996", - "displayName": "teine admin", - "csaTitle": "teine admin", - "csaEmail": "Teine@admin.ee", - "authorities": [ - "ROLE_ADMINISTRATOR" - ], - "customerSupportStatus": "offline", - "totalPages": 1 - }, - { - "login": "EE50701019992", - "firstName": "Valter", - "lastName": "Aro", - "userIdCode": "EE50701019992", - "displayName": "Valter", - "csaTitle": "Mister", - "csaEmail": "valter.aro@ria.ee", - "authorities": [ - "ROLE_ADMINISTRATOR" - ], - "customerSupportStatus": "offline", - "totalPages": 1 - }, - { - "login": "EE38104266023", - "firstName": "Varmo", - "lastName": "", - "userIdCode": "EE38104266023", - "displayName": "Varmo", - "csaTitle": "MISTER", - "csaEmail": "mail@mail.ee", - "authorities": [ - "ROLE_ADMINISTRATOR" - ], - "customerSupportStatus": "online", - "totalPages": 1 - } -] \ No newline at end of file diff --git a/GUI/src/pages/DataModels/index.tsx b/GUI/src/pages/DataModels/index.tsx index 89808dc8..18d0d35a 100644 --- a/GUI/src/pages/DataModels/index.tsx +++ b/GUI/src/pages/DataModels/index.tsx @@ -11,7 +11,11 @@ import ConfigureDataModel from './ConfigureDataModel'; import { customFormattedArray, extractedArray } from 'utils/dataModelsUtils'; import CircularSpinner from 'components/molecules/CircularSpinner/CircularSpinner'; import { ButtonAppearanceTypes } from 'enums/commonEnums'; -import { DataModelResponse, FilterData, Filters } from 'types/dataModels'; +import { + DataModelResponse, + DataModelsFilters, + FilterData, +} from 'types/dataModels'; import { dataModelsQueryKeys } from 'utils/queryKeys'; import NoDataView from 'components/molecules/NoDataView'; @@ -32,14 +36,14 @@ const DataModels: FC = () => { setEnableFetch(true); }, [view]); - const [filters, setFilters] = useState({ + const [filters, setFilters] = useState({ modelName: 'all', version: 'x.x.x', platform: 'all', datasetGroup: -1, trainingStatus: 'all', maturity: 'all', - sort: 'asc', + sort: 'created_timestamp desc', }); const { data: dataModelsData, isLoading: isModelDataLoading } = useQuery( @@ -119,7 +123,7 @@ const DataModels: FC = () => { const pageCount = dataModelsData?.data[0]?.totalPages || 1; const handleFilterChange = ( - name: keyof Filters, + name: keyof DataModelsFilters, value: string | number | undefined | { name: string; id: string } ) => { setEnableFetch(false); @@ -273,8 +277,22 @@ const DataModels: FC = () => { name="" placeholder={t('dataModels.filters.sort') ?? ''} options={[ - { label: 'A-Z', value: 'asc' }, - { label: 'Z-A', value: 'desc' }, + { + label: t('dataModels.sortOptions.dataModelAsc'), + value: 'name asc', + }, + { + label: t('dataModels.sortOptions.dataModelDesc'), + value: 'name desc', + }, + { + label: t('dataModels.sortOptions.createdDateDesc'), + value: 'created_timestamp desc', + }, + { + label: t('dataModels.sortOptions.createdDateAsc'), + value: 'created_timestamp asc', + }, ]} onSelectionChange={(selection) => handleFilterChange('sort', selection?.value) @@ -293,15 +311,17 @@ const DataModels: FC = () => { {t('global.search') ?? ''}
          } >
          - {errorMessage ? ( -
          {errorMessage}
          - ) : ( -
          -
          {status}
          - -
          - )} + {(status==="failed" || status==="deployed") && progress===100 ? ( +
          + {trainingMessage} +
          + ) : ( +
          +
          {status}
          + +
          + {trainingMessage} +
          +
          + )}
          ); diff --git a/GUI/src/components/molecules/ValidationCriteria/DraggableItem/DraggableItem.tsx b/GUI/src/components/molecules/ValidationCriteria/DraggableItem/DraggableItem.tsx index b9aee410..46e6c3de 100644 --- a/GUI/src/components/molecules/ValidationCriteria/DraggableItem/DraggableItem.tsx +++ b/GUI/src/components/molecules/ValidationCriteria/DraggableItem/DraggableItem.tsx @@ -9,6 +9,7 @@ import { Link } from 'react-router-dom'; import { isFieldNameExisting } from 'utils/datasetGroupsUtils'; import './DragableItemStyle.scss'; import { useTranslation } from 'react-i18next'; +import useOptionLists from 'hooks/useOptionLists'; const ItemTypes = { ITEM: 'item', @@ -35,6 +36,8 @@ const DraggableItem = ({ item: { index }, }); + const { dataTypesConfigs: dataTypes } = useOptionLists(); + const [, drop] = useDrop({ accept: ItemTypes.ITEM, hover: (draggedItem: { index: number }) => { diff --git a/GUI/src/hooks/useOptionLists.tsx b/GUI/src/hooks/useOptionLists.tsx new file mode 100644 index 00000000..41753bfe --- /dev/null +++ b/GUI/src/hooks/useOptionLists.tsx @@ -0,0 +1,25 @@ +import { useTranslation } from 'react-i18next'; + +const useOptionLists = () => { + const { t } = useTranslation(); + + const dataTypesConfigs = [ + { label: t('optionLists.text'), value: 'text' }, + { label: t('optionLists.numbers'), value: 'numbers' }, + { label: t('optionLists.dateTimes'), value: 'datetime' }, + { label: t('optionLists.email'), value: 'email' }, + { label: t('optionLists.fileAttachements'), value: 'file_attachments' }, + ]; + + const importOptionsConfigs = [ + { label: t('optionLists.importToAdd'), value: 'add' }, + { label: t('optionLists.importToDelete'), value: 'delete' }, + ]; + + return { + dataTypesConfigs, + importOptionsConfigs, + }; +}; + +export default useOptionLists; diff --git a/GUI/src/hooks/useTrainingSessions.tsx b/GUI/src/hooks/useTrainingSessions.tsx new file mode 100644 index 00000000..7d683de8 --- /dev/null +++ b/GUI/src/hooks/useTrainingSessions.tsx @@ -0,0 +1,51 @@ +import { useQuery } from "@tanstack/react-query"; +import { useEffect, useState } from "react"; +import { getDatasetGroupsProgress } from "services/datasets"; +import sse from "services/sse-service"; +import { TrainingProgressData } from "types/dataModels"; +import { SSEEventData } from "types/datasetGroups"; +import { datasetQueryKeys } from "utils/queryKeys"; + +export const useTrainingSessions = () => { + const [progresses, setProgresses] = useState([]); + + const { data: progressData, refetch } = useQuery( + datasetQueryKeys.GET_DATASET_GROUP_PROGRESS(), + getDatasetGroupsProgress, + { + onSuccess: (data) => setProgresses(data), + } + ); + + const handleUpdate = (sessionId: string, newData: SSEEventData) => { + setProgresses((prevProgresses) => + prevProgresses.map((progress) => + progress.id === sessionId ? { ...progress, ...newData } : progress + ) + ); + }; + + useEffect(() => { + if (!progressData) return; + + const eventSources = progressData + .filter( + (progress) => + !(progress.trainingStatus === 'deployed'||progress.trainingStatus === 'deployed') && + progress.progressPercentage !== 100 + ) + .map((progress) => + sse(`/${progress.id}`, 'dataset', (data: SSEEventData) => { + console.log(`New data for notification ${progress.id}:`, data); + handleUpdate(data.sessionId, data); + }) + ); + + return () => { + eventSources.forEach((eventSource) => eventSource?.close()); + console.log('SSE connections closed'); + }; + }, [progressData, refetch]); + + return progresses; +}; diff --git a/GUI/src/hooks/useValidationSessions.tsx b/GUI/src/hooks/useValidationSessions.tsx index 37470564..0f1155ae 100644 --- a/GUI/src/hooks/useValidationSessions.tsx +++ b/GUI/src/hooks/useValidationSessions.tsx @@ -30,7 +30,7 @@ export const useValidationSessions = () => { const eventSources = progressData .filter( (progress) => - progress.validationStatus !== 'Success' && + !(progress.validationStatus === 'Success'|| progress.validationStatus === 'Fail') && progress.progressPercentage !== 100 ) .map((progress) => diff --git a/GUI/src/pages/CorrectedTexts/index.tsx b/GUI/src/pages/CorrectedTexts/index.tsx index fdabf10b..2a3e00c6 100644 --- a/GUI/src/pages/CorrectedTexts/index.tsx +++ b/GUI/src/pages/CorrectedTexts/index.tsx @@ -121,7 +121,7 @@ const CorrectedTexts: FC = () => { { onSelectionChange={(selection) => handleFilterChange('sort', (selection?.value as string) ?? '') } + defaultValue={filters.sort} />