diff --git a/.deepsource.toml b/.deepsource.toml deleted file mode 100644 index df03426d04..0000000000 --- a/.deepsource.toml +++ /dev/null @@ -1,23 +0,0 @@ -version = 1 - -[[analyzers]] -name = "python" -enabled = true - - [analyzers.meta] - runtime_version = "3.x.x" - -[[analyzers]] -name = "javascript" -enabled = true - - [analyzers.meta] - environment = [ - "nodejs", - "browser", - "jest", - "jquery" - ] - plugins = ["react"] - style_guide = "standard" - dialect = "typescript" diff --git a/.dockerignore b/.dockerignore index ff294ced2f..48958f540f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,8 @@ node_modules/ README.md +.github +.husky +.lintstagedrc +LICENSE +.vscode/ +.prettierrc.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a1acaaf89..9b9577eaa8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,14 +18,21 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + - name: Use Node.js uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' - cache: 'yarn' - name: Install dependencies - run: yarn install + run: pnpm install + + - name: Build everything + run: pnpm build:prod - - name: Check linting issues - run: yarn lint + - name: Check compilation issues + run: pnpm lint diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml deleted file mode 100644 index cc892472d0..0000000000 --- a/.github/workflows/playwright.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Playwright Tests -on: - push: - branches: [ main, master ] - pull_request: - branches: [ main, master ] -jobs: - test: - timeout-minutes: 60 - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 16 - - name: Install dependencies - run: yarn install - - name: Install Playwright Browsers - run: npx playwright install --with-deps - - name: Run Playwright tests - run: npx playwright test - - uses: actions/upload-artifact@v4 - if: always() - with: - name: playwright-report - path: playwright-report/ - retention-days: 30 diff --git a/.github/workflows/test-coverage.yaml b/.github/workflows/test-coverage.yaml deleted file mode 100644 index 6537d24679..0000000000 --- a/.github/workflows/test-coverage.yaml +++ /dev/null @@ -1,22 +0,0 @@ -name: Test Coverage -on: pull_request -jobs: - test-coverage: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Jest coverage report - uses: ArtiomTr/jest-coverage-report-action@v2.1.2 - id: coverage - with: - package-manager: yarn - annotations: failed-tests - test-script: yarn test:ci - output: report-markdown - - name: Comment on pull request - uses: marocchino/sticky-pull-request-comment@v2 - if: always() - with: - recreate: true - message: ${{ steps.coverage.outputs.report }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index dfca7bfe8d..190e1015d5 100755 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,16 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies -/node_modules -/.pnp +node_modules/ +.pnp/ .pnp.js # testing -/coverage +coverage/ # production -/build -/dist +build/ +dist/ # misc .DS_Store @@ -29,13 +29,17 @@ yarn-error.log /public/env-config.js env-config.js .idea/ -/test-results/ -/playwright-report/ -/playwright/.cache/ +# TODO: delete unnecessary stuff +test-results/ +playwright-report/ +playwright/.cache/ playwright/.auth/ .npmrc package-lock.json *storybook.log -storybook-static +storybook-static/ + +pnpm-lock.yaml +fe-internal-setup.sh diff --git a/.husky/pre-commit b/.husky/pre-commit index 50e1243735..5861fe52dc 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -17,5 +17,5 @@ . "$(dirname "$0")/_/husky.sh" -yarn tsc --noEmit -yarn lint-staged +pnpm -r compile +pnpm -r lint-staged diff --git a/Dockerfile b/Dockerfile index 717955f51d..915e1dda91 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,29 +1,30 @@ FROM node:20-alpine AS builder +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +RUN corepack enable WORKDIR /app -COPY package.json . -COPY yarn.lock . -RUN yarn install --network-timeout 600000 - -COPY src/ src -COPY nginx.conf . -COPY tsconfig.json . -COPY vite.config.mts . COPY . . RUN echo `git rev-parse --short HEAD` > health.html -RUN yarn build + +RUN if [ -d "./devtron-fe-lib" ]; then rm -rf packages/devtron-fe-lib; cp -r ./devtron-fe-lib packages/devtron-fe-lib; fi + +RUN pnpm i +RUN pnpm run build:prod FROM nginx:stable RUN useradd -ms /bin/bash devtron -COPY --from=builder /app/dist/ /usr/share/nginx/html -COPY ./nginx.conf /etc/nginx/nginx.conf -COPY ./nginx-default.conf /etc/nginx/conf.d/default.conf + +COPY --from=builder /app/apps/web/dist/ /usr/share/nginx/html +COPY --from=builder /app/apps/web/nginx.conf /etc/nginx/nginx.conf +COPY --from=builder /app/apps/web/nginx-default.conf /etc/nginx/conf.d/default.conf + WORKDIR /usr/share/nginx/html -COPY --from=builder /app/./env.sh . -COPY --from=builder /app/.env . +COPY --from=builder /app/apps/web/./env.sh . +COPY --from=builder /app/apps/web/.env . COPY --from=builder /app/health.html . RUN chown -R devtron:devtron /usr/share/nginx/html diff --git a/Dockerfile.storybook b/Dockerfile.storybook index 2272d8b80d..666625a92d 100644 --- a/Dockerfile.storybook +++ b/Dockerfile.storybook @@ -1,18 +1,19 @@ FROM node:20-alpine AS builder +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +RUN corepack enable WORKDIR /app -COPY package.json . -COPY yarn.lock . - -RUN yarn install --network-timeout 600000 COPY . . -RUN yarn build-storybook +RUN pnpm i + +RUN pnpm build:storybook FROM nginx:stable -COPY --from=builder /app/storybook-static/ /usr/share/nginx/html +COPY --from=builder /app/apps/web/storybook-static/ /usr/share/nginx/html WORKDIR /usr/share/nginx/html CMD ["/bin/bash", "-c", "nginx -g \"daemon off;\""] diff --git a/README.md b/README.md index e88833e4ff..d57d31ef8c 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,33 @@ -This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). +# Dashboard -## Available Scripts +

+ +Join Discord +

-In the project directory, you can run: +This is the client side web app for [devtron](https://github.com/devtron-labs/devtron). +This web app is written in [React](https://react.dev/) frontend-library. Uses a typescript + vite for setup. -### `yarn start` +## How to run? -Runs the app in the development mode.
-Open [http://localhost:3000](http://localhost:3000) to view it in the browser. +This project uses `pnpm` as package manager. To install all dependencies and initialize the project just run the command `pnpm install`. -The page will reload if you make edits.
-You will also see any lint errors in the console. +To start a dev server run `pnpm start`. +The above command will start a dev vite server at [http://localhost:3000](http://localhost:3000). -### `yarn test` +By default the backend pointed to by this dev server will be [https://preview.devtron.ai](https://preview.devtron.ai). +Check out our amazing project at [https://preview.devtron.ai](https://preview.devtron.ai). -Launches the test runner in the interactive watch mode.
-See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. +If you have a local devtron instance running through minikube/kind/microK8s, you can have that be your backend by changing the `TARGET_URL` +in `/apps/web/vite.config.mts` to that instance's url. -### `yarn run build` - -Builds the app for production to the `build` folder.
-It correctly bundles React in production mode and optimizes the build for the best performance. - -The build is minified and the filenames include the hashes.
-Your app is ready to be deployed! - -See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. - -### `yarn run eject` - -**Note: this is a one-way operation. Once you `eject`, you can’t go back!** - -If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. - -Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. - -You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. +Check out `package.json` for more scripts. ### `commit process` -- Now before commiting lint will run and check for errors. -- It will not allow to commit if there are lint errors. -- To fix them run `yarn lint-fix` (check package.json). It is not capable of fixing everything so some fixes has to be done manually. +- We use husky to run compile and lint-staged before commit staged changes +- If husky finds any errors during either the compilation stage or linting stage, the attempt to commit will fail +- If you see fixable issues. You can run `pnpm -r lint-fix`. None auto-fixable issues will have to be resolved by you manually ### Do's @@ -62,109 +48,6 @@ You don’t have to ever use `eject`. The curated feature set is suitable for sm - Don't use float use display instead, use display flex-box, inline, inline-block. - Don't use mix type methods in a class -### Sentry Config - -- SENTRY_AUTH_TOKEN="" -- SENTRY_ORG="devtron-labs" -- SENTRY_PROJECT="dashboard" -- DSN="" -- SENTRY_TRACES_SAMPLE_RATE="0.2" - -### Sentry sourcemap upload - -```console -foo@bar:~$ sh sentry.sh -``` - -### Set custom sentry environment during production deployment, default is staging - -```console -foo@bar~$ docker run -p 3000:80 -e SENTRY_ENV=my-custom-env -t artifact/tag -``` - -### Disable sentry error logging during production deployment, default enabled - -```console -foo@bar~$ docker run -p 3000:80 -e ENTRY_ERROR_ENABLED=false -t artifact/tag -``` - -### Disable sentry performance monitoring during production deployment, default enabled - -```console -foo@bar~$ docker run -p 3000:80 -e SENTRY_PERFORMANCE_ENABLED=false -t artifact/tag -``` - -### Enable Hotjar during production deployment, default disabled - -```console -foo@bar~$ docker run -p 3000:80 -e HOTJAR_ENABLED=false -t artifact/tag -``` - -### Enable google analytics during production deployment, default disabled - -```console -foo@bar~$ docker run -p 3000:80 -e GA_ENABLED=true -t artifact/tag -``` - -### Create test coverage report and save summary in report.txt - -```console -foo@bar~$ npm run test -- --coverage --watchAll=false > report.txt -``` - -### Upload Summary on slack - -```console -foo@bar~$ python uploadTestReport.py -``` - -### Run Following Scripts after release - -```console -foo@bar~$ sh sentry.sh -foo@bar~$ npm run test -- --coverage --watchAll=false > report.txt -foo@bar~$ python uploadTestReport.py -``` - -### Development setup with proxy. - -#### **`vite.config.ts`** -Update the `vite.config.ts` file to include the proxy configuration. -In proxy object, add the target URL of the orchestrator and grafana. - -```js -server: { - port: 3000, - proxy: { - '/orchestrator': { - target: 'https://preview.devtron.ai/', - changeOrigin: true, - }, - '/grafana': 'https://preview.devtron.ai/', - }, - } -``` - -#### **`.env.development`** - -```console -VITE_GRAFANA_ORG_ID=2 -REACT_APP_EDITOR=code -VITE_ORCHESTRATOR_ROOT=/orchestrator -REACT_APP_PASSWORD=argocd-server-74b7b94945-nxxnh -``` - -### Development setup without proxy. - -#### **`.env.development`** - -```console -VITE_GRAFANA_ORG_ID=2 -REACT_APP_EDITOR=code -VITE_ORCHESTRATOR_ROOT=http://demo.devtron.info:32080/orchestrator -REACT_APP_PASSWORD=argocd-server-74b7b94945-nxxnh -``` - ## How do I make a contribution? Never made an open source contribution before? Wondering how contributions work in our project? Here's a quick rundown! @@ -202,10 +85,4 @@ First you need to have the backend project up and running and the dashboard repo ## Code walkthrough/Project structure -We have a `src` folder at the root level which holds everything related to the dashboard - -- `src/assets` have all the image folders like logo, icons, gif etc. These folders have, the related files -- `src/components` have all the components used in the project further divided into folder component specific folders. Specific component folders hold their local CSS file specific to that component, service file specific to that component, and some required child component.tsx as well -- `src/config` has config files like constants, route, etc which holds all the constants, route path constants respectively -- `src/css has` the common CSS files -- `src/services` have the common services used across projects +This is a monorepo. Apps that run are located in `apps/` folder while supporting libraries are placed under `packages/`. diff --git a/.env b/apps/web/.env similarity index 100% rename from .env rename to apps/web/.env diff --git a/.env.development b/apps/web/.env.development similarity index 100% rename from .env.development rename to apps/web/.env.development diff --git a/.env.k8sApp b/apps/web/.env.k8sApp similarity index 100% rename from .env.k8sApp rename to apps/web/.env.k8sApp diff --git a/.env.production b/apps/web/.env.production similarity index 100% rename from .env.production rename to apps/web/.env.production diff --git a/.eslintignore b/apps/web/.eslintignore similarity index 100% rename from .eslintignore rename to apps/web/.eslintignore diff --git a/.eslintrc.js b/apps/web/.eslintrc.js similarity index 96% rename from .eslintrc.js rename to apps/web/.eslintrc.js index a052fdc9b4..86aa187265 100644 --- a/.eslintrc.js +++ b/apps/web/.eslintrc.js @@ -14,6 +14,8 @@ * limitations under the License. */ +const { resolve } = require("path"); + module.exports = { parser: '@typescript-eslint/parser', plugins: ['@typescript-eslint', 'react', 'prettier', 'import'], @@ -27,7 +29,7 @@ module.exports = { ecmaFeatures: { jsx: true, }, - project: ['./tsconfig.json'], + project: [resolve(__dirname, 'tsconfig.json')] }, globals: { JSX: true, @@ -139,5 +141,5 @@ module.exports = { }, 'import/ignore': ['\\.png$', '\\.jpg$', '\\.svg$'], }, - ignorePatterns: ['.eslintrc.cjs', 'vite.config.mts'], + ignorePatterns: ['.eslintrc.js', 'vite.config.mts'], } diff --git a/.lintstagedrc b/apps/web/.lintstagedrc similarity index 62% rename from .lintstagedrc rename to apps/web/.lintstagedrc index 049b25fcc1..cc5be6a41f 100644 --- a/.lintstagedrc +++ b/apps/web/.lintstagedrc @@ -1,5 +1,5 @@ { "*.{js,jsx,ts,tsx}": [ - "yarn eslint" + "pnpm eslint" ] } diff --git a/.storybook/main.ts b/apps/web/.storybook/main.ts similarity index 100% rename from .storybook/main.ts rename to apps/web/.storybook/main.ts diff --git a/.storybook/preview-head.html b/apps/web/.storybook/preview-head.html similarity index 100% rename from .storybook/preview-head.html rename to apps/web/.storybook/preview-head.html diff --git a/.storybook/preview.tsx b/apps/web/.storybook/preview.tsx similarity index 100% rename from .storybook/preview.tsx rename to apps/web/.storybook/preview.tsx diff --git a/config.md b/apps/web/config.md similarity index 100% rename from config.md rename to apps/web/config.md diff --git a/env.sh b/apps/web/env.sh similarity index 100% rename from env.sh rename to apps/web/env.sh diff --git a/favicon.ico b/apps/web/favicon.ico similarity index 100% rename from favicon.ico rename to apps/web/favicon.ico diff --git a/index.html b/apps/web/index.html similarity index 100% rename from index.html rename to apps/web/index.html diff --git a/manifest.json b/apps/web/manifest.json similarity index 100% rename from manifest.json rename to apps/web/manifest.json diff --git a/nginx-default.conf b/apps/web/nginx-default.conf similarity index 100% rename from nginx-default.conf rename to apps/web/nginx-default.conf diff --git a/nginx.conf b/apps/web/nginx.conf similarity index 100% rename from nginx.conf rename to apps/web/nginx.conf diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 0000000000..4c02fde4e1 --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,161 @@ +{ + "name": "web", + "version": "0.2.0", + "private": true, + "homepage": "/dashboard", + "dependencies": { + "@devtron-labs/devtron-fe-common-lib": "workspace:*", + "@devtron-labs/devtron-fe-lib": "workspace:*", + "@esbuild-plugins/node-globals-polyfill": "0.2.3", + "@rjsf/core": "^5.13.3", + "@rjsf/utils": "^5.13.3", + "@rjsf/validator-ajv8": "^5.13.3", + "@sentry/browser": "7.50.0", + "@sentry/integrations": "7.50.0", + "@sentry/tracing": "7.50.0", + "@tippyjs/react": "4.2.6", + "@typeform/embed-react": "2.20.0", + "@types/marked": "4.0.8", + "@vitejs/plugin-react": "4.3.1", + "command-line-parser": "^0.2.10", + "compute-histogram": "^0.9.11", + "dayjs": "^1.11.8", + "dompurify": "^3.0.2", + "fast-json-patch": "^3.1.1", + "flexsearch": "^0.6.32", + "jsonpath-plus": "^9.0.0", + "marked": "4.3.0", + "moment": "^2.29.4", + "monaco-editor": "0.44.0", + "monaco-yaml": "5.1.1", + "query-string": "^7.1.1", + "react": "^17.0.2", + "react-csv": "^2.2.2", + "react-dates": "^21.8.0", + "react-dom": "^17.0.2", + "react-draggable": "^4.4.5", + "react-ga4": "^1.4.1", + "react-gtm-module": "^2.0.11", + "react-keybind": "^0.9.4", + "react-mde": "^11.5.0", + "react-monaco-editor": "^0.55.0", + "react-router-dom": "^5.3.4", + "react-select": "5.8.0", + "react-virtualized": "^9.22.5", + "recharts": "^2.1.9", + "rollup-plugin-node-polyfills": "0.2.1", + "rxjs": "^7.5.4", + "sockjs-client": "1.6.1", + "tippy.js": "^6.3.7", + "vite": "5.4.6", + "vite-plugin-require-transform": "1.0.21", + "vite-plugin-svgr": "^2.4.0", + "xterm": "^4.19.0", + "xterm-addon-fit": "^0.5.0", + "xterm-addon-search": "^0.9.0", + "xterm-webfont": "^2.0.0", + "yaml": "^2.4.1" + }, + "scripts": { + "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}' --max-warnings 0", + "lint-fix": "eslint 'src/**/*.{js,jsx,ts,tsx}' --fix", + "start": "vite --open", + "build": "NODE_OPTIONS=--max_old_space_size=4096 vite build --sourcemap", + "serve": "vite preview", + "build-light": "NODE_OPTIONS=--max_old_space_size=4096 GENERATE_SOURCEMAP=false vite build", + "build-k8s-app": "NODE_OPTIONS=--max_old_space_size=4096 GENERATE_SOURCEMAP=false VITE_K8S_CLIENT=true vite build", + "test": "vitest test", + "test-coverage:watch": "npm run test -- --coverage", + "test-coverage": "npm run test -- --coverage --watchAll=false", + "test:ci": "jest --watchAll=false --ci --json --coverage --testLocationInResults --outputFile=report.json", + "jest": "jest", + "storybook": "IS_STORYBOOK=true storybook dev -p 6006", + "build-storybook": "IS_STORYBOOK=true storybook build", + "lint-staged": "lint-staged", + "compile": "tsc --noEmit" + }, + "browserslist": [ + ">0.2%", + "not dead", + "not ie <= 11", + "not op_mini all" + ], + "devDependencies": { + "@chromatic-com/storybook": "^1.8.0", + "@playwright/test": "^1.32.1", + "@sentry/cli": "^2.2.0", + "@storybook/addon-a11y": "^8.2.9", + "@storybook/addon-actions": "^8.2.9", + "@storybook/addon-essentials": "^8.2.9", + "@storybook/addon-interactions": "^8.2.9", + "@storybook/addon-links": "^8.2.9", + "@storybook/blocks": "^8.2.9", + "@storybook/react": "^8.2.9", + "@storybook/react-vite": "^8.2.9", + "@storybook/test": "^8.2.9", + "@testing-library/jest-dom": "^5.16.2", + "@testing-library/react": "^12.1.4", + "@types/jest": "^27.4.1", + "@types/node": "20.11.0", + "@types/react": "17.0.39", + "@types/react-csv": "^1.1.3", + "@types/react-dom": "17.0.13", + "@types/react-router-dom": "^5.3.3", + "@types/react-transition-group": "^4.4.4", + "@types/recharts": "^1.8.23", + "@types/recompose": "^0.30.10", + "@typescript-eslint/eslint-plugin": "8.3.0", + "@typescript-eslint/parser": "8.3.0", + "env-cmd": "10.1.0", + "eslint": "^8.56.0", + "eslint-config-airbnb": "^19.0.4", + "eslint-config-prettier": "^9.1.0", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-jsx-a11y": "^6.8.0", + "eslint-plugin-prettier": "^5.1.2", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-storybook": "^0.8.0", + "jest-extended": "^2.0.0", + "jest-junit": "^13.0.0", + "jsdom-worker": "^0.2.1", + "lint-staged": "^12.5.0", + "mock-socket": "^9.2.1", + "prettier": "^3.1.1", + "react-test-render": "^1.1.2", + "sass": "^1.69.7", + "storybook": "^8.2.9", + "ts-jest": "29.2.5", + "ts-node": "10.9.2", + "typescript": "5.5.4", + "vite-plugin-pwa": "0.20.2", + "vite-tsconfig-paths": "5.0.1", + "vitest": "2.0.5", + "workbox-core": "7.1.0", + "workbox-navigation-preload": "7.1.0", + "workbox-precaching": "7.1.0", + "workbox-routing": "7.1.0", + "workbox-strategies": "7.1.0", + "workbox-window": "7.1.0", + "worker-loader": "3.0.8" + }, + "jest": { + "collectCoverageFrom": [ + "**/*.{ts,tsx,js,jsx}", + "!**/?(*.)+(mock).+(ts|js)", + "!**/node_modules/**", + "!**/coverage/**", + "!**/serviceWorker.ts", + "!**/index.tsx", + "!**/setupProxy.js" + ], + "setupFilesAfterEnv": [ + "jest-extended" + ], + "moduleNameMapper": { + "monaco-editor": "/node_modules/react-monaco-editor", + "monaco-yaml": "/node_modules/react-monaco-editor" + } + } +} diff --git a/src/App.tsx b/apps/web/src/App.tsx similarity index 100% rename from src/App.tsx rename to apps/web/src/App.tsx diff --git a/src/Pages/App/Details/ExternalFlux/ExternalFluxAppDetails.tsx b/apps/web/src/Pages/App/Details/ExternalFlux/ExternalFluxAppDetails.tsx similarity index 100% rename from src/Pages/App/Details/ExternalFlux/ExternalFluxAppDetails.tsx rename to apps/web/src/Pages/App/Details/ExternalFlux/ExternalFluxAppDetails.tsx diff --git a/src/Pages/App/Details/ExternalFlux/ExternalFluxAppDetailsRoute.tsx b/apps/web/src/Pages/App/Details/ExternalFlux/ExternalFluxAppDetailsRoute.tsx similarity index 100% rename from src/Pages/App/Details/ExternalFlux/ExternalFluxAppDetailsRoute.tsx rename to apps/web/src/Pages/App/Details/ExternalFlux/ExternalFluxAppDetailsRoute.tsx diff --git a/src/Pages/App/Details/ExternalFlux/index.tsx b/apps/web/src/Pages/App/Details/ExternalFlux/index.tsx similarity index 100% rename from src/Pages/App/Details/ExternalFlux/index.tsx rename to apps/web/src/Pages/App/Details/ExternalFlux/index.tsx diff --git a/src/Pages/App/Details/ExternalFlux/service.tsx b/apps/web/src/Pages/App/Details/ExternalFlux/service.tsx similarity index 100% rename from src/Pages/App/Details/ExternalFlux/service.tsx rename to apps/web/src/Pages/App/Details/ExternalFlux/service.tsx diff --git a/src/Pages/App/Details/ExternalFlux/types.tsx b/apps/web/src/Pages/App/Details/ExternalFlux/types.tsx similarity index 100% rename from src/Pages/App/Details/ExternalFlux/types.tsx rename to apps/web/src/Pages/App/Details/ExternalFlux/types.tsx diff --git a/src/Pages/App/Details/ExternalFlux/utils.tsx b/apps/web/src/Pages/App/Details/ExternalFlux/utils.tsx similarity index 100% rename from src/Pages/App/Details/ExternalFlux/utils.tsx rename to apps/web/src/Pages/App/Details/ExternalFlux/utils.tsx diff --git a/src/Pages/Applications/DevtronApps/Details/AppConfigurations/AppConfig.constants.ts b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/AppConfig.constants.ts similarity index 100% rename from src/Pages/Applications/DevtronApps/Details/AppConfigurations/AppConfig.constants.ts rename to apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/AppConfig.constants.ts diff --git a/src/Pages/Applications/DevtronApps/Details/AppConfigurations/AppConfig.service.ts b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/AppConfig.service.ts similarity index 100% rename from src/Pages/Applications/DevtronApps/Details/AppConfigurations/AppConfig.service.ts rename to apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/AppConfig.service.ts diff --git a/src/Pages/Applications/DevtronApps/Details/AppConfigurations/AppConfig.tsx b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/AppConfig.tsx similarity index 100% rename from src/Pages/Applications/DevtronApps/Details/AppConfigurations/AppConfig.tsx rename to apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/AppConfig.tsx diff --git a/src/Pages/Applications/DevtronApps/Details/AppConfigurations/AppConfig.types.ts b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/AppConfig.types.ts similarity index 94% rename from src/Pages/Applications/DevtronApps/Details/AppConfigurations/AppConfig.types.ts rename to apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/AppConfig.types.ts index e51b5ded97..2f8cd529f0 100644 --- a/src/Pages/Applications/DevtronApps/Details/AppConfigurations/AppConfig.types.ts +++ b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/AppConfig.types.ts @@ -19,12 +19,12 @@ import { CollapsibleListItem, AppEnvDeploymentConfigType, EnvResourceType, + AppEnvironment, } from '@devtron-labs/devtron-fe-common-lib' import { ViewType } from '@Config/constants' import { WorkflowResult } from '@Components/app/details/triggerView/types' import { UserRoleType } from '@Pages/GlobalConfigurations/Authorization/constants' -import { AppEnvironment } from '@Services/service.types' import { ResourceConfig, ResourceConfigState } from '../../service.types' @@ -224,10 +224,18 @@ export interface EnvConfigurationsNavProps { environments: EnvironmentOptionType[] paramToCheck?: 'appId' | 'envId' goBackURL: string + /** + * The base URL to be appended before the Compare View route. + * Compare View route structure: `compareWithURL/URLS.APP_ENV_CONFIG_COMPARE/:compareTo?/:resourceType/:resourceName?` + * + * @note This can represent either a route path or a complete link(route params resolved). + */ + compareWithURL: string showComparison?: boolean showBaseConfigurations?: boolean showDeploymentTemplate?: boolean isCMSecretLocked?: boolean + hideEnvSelector?: boolean } export interface EnvConfigRouteParams { @@ -237,7 +245,7 @@ export interface EnvConfigRouteParams { } export interface ExtendedCollapsibleListItem - extends Pick { + extends Pick, 'title' | 'subtitle' | 'href' | 'iconConfig'> { configState: ResourceConfigState } @@ -255,6 +263,7 @@ export type DeploymentConfigCompareProps = { goBackURL?: string isBaseConfigProtected?: boolean getNavItemHref: (resourceType: EnvResourceType, resourceName: string) => string + overwriteNavHeading?: string } & ( | { type: 'appGroup' diff --git a/src/Pages/Applications/DevtronApps/Details/AppConfigurations/AppConfig.utils.ts b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/AppConfig.utils.ts similarity index 100% rename from src/Pages/Applications/DevtronApps/Details/AppConfigurations/AppConfig.utils.ts rename to apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/AppConfig.utils.ts diff --git a/src/Pages/Applications/DevtronApps/Details/AppConfigurations/AppConfiguration.provider.tsx b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/AppConfiguration.provider.tsx similarity index 100% rename from src/Pages/Applications/DevtronApps/Details/AppConfigurations/AppConfiguration.provider.tsx rename to apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/AppConfiguration.provider.tsx diff --git a/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/AppComposeRouter.tsx b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/AppComposeRouter.tsx similarity index 80% rename from src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/AppComposeRouter.tsx rename to apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/AppComposeRouter.tsx index a8dd8a7225..bf61e6231b 100644 --- a/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/AppComposeRouter.tsx +++ b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/AppComposeRouter.tsx @@ -23,7 +23,7 @@ import { ReactComponent as Next } from '@Icons/ic-arrow-forward.svg' import { URLS } from '@Config/index' import { ErrorBoundary, importComponentFromFELibrary, useAppContext } from '@Components/common' import ExternalLinks from '@Components/externalLinks/ExternalLinks' -import { CMSecretComponentType } from '@Pages/Shared/ConfigMapSecret/ConfigMapSecret.types' +import { CMSecretComponentType } from '@Pages/Shared/ConfigMapSecret/types' import { ConfigMapSecretWrapper } from '@Pages/Shared/ConfigMapSecret/ConfigMapSecret.wrapper' import { NextButtonProps, STAGE_NAME } from '../AppConfig.types' @@ -34,13 +34,12 @@ import { DeploymentConfigCompare } from './DeploymentConfigCompare' const MaterialList = lazy(() => import('@Components/material/MaterialList')) const CIConfig = lazy(() => import('@Components/ciConfig/CIConfig')) -const DeploymentConfig = lazy(() => import('@Components/deploymentConfig/DeploymentConfig')) +const DeploymentTemplate = lazy(() => import('./DeploymentTemplate/DeploymentTemplate')) const WorkflowEdit = lazy(() => import('@Components/workflowEditor/workflowEditor')) const EnvironmentOverride = lazy(() => import('@Pages/Shared/EnvironmentOverride/EnvironmentOverride')) const UserGitRepoConfiguration = lazy(() => import('@Components/gitOps/UserGitRepConfiguration')) const ConfigProtectionView = importComponentFromFELibrary('ConfigProtectionView') -const CompareWithButton = importComponentFromFELibrary('CompareWithButton', null, 'function') const NextButton: React.FC = ({ isCiPipeline, navItems, currentStageName, isDisabled }) => { const history = useHistory() @@ -202,13 +201,13 @@ const AppComposeRouter = () => { )} {isUnlocked.deploymentTemplate && ( - )} @@ -291,49 +290,41 @@ const AppComposeRouter = () => { /> )} , - ...(CompareWithButton - ? [ - - {({ match }) => { - const basePath = generatePath(path, match.params) - const envOverridePath = match.params.envId - ? `/${URLS.APP_ENV_OVERRIDE_CONFIG}/${match.params.envId}` - : '' - // Set the resourceTypePath based on the resourceType from the URL parameters. - // If the resourceType is 'Manifest', use 'deployment-template' as the back URL. - // Otherwise, use the actual resourceType from the URL, which could be 'deployment-template', 'configmap', or 'secrets'. - const resourceTypePath = `/${match.params.resourceType === EnvResourceType.Manifest ? EnvResourceType.DeploymentTemplate : match.params.resourceType}` - const resourceNamePath = match.params.resourceName - ? `/${match.params.resourceName}` - : '' + + {({ match }) => { + const basePath = generatePath(path, match.params) + const envOverridePath = match.params.envId + ? `/${URLS.APP_ENV_OVERRIDE_CONFIG}/${match.params.envId}` + : '' + // Set the resourceTypePath based on the resourceType from the URL parameters. + // If the resourceType is 'Manifest' or 'PipelineStrategy', use 'deployment-template' as the back URL. + // Otherwise, use the actual resourceType from the URL, which could be 'deployment-template', 'configmap', or 'secrets'. + const resourceTypePath = `/${match.params.resourceType === EnvResourceType.Manifest || match.params.resourceType === EnvResourceType.PipelineStrategy ? EnvResourceType.DeploymentTemplate : match.params.resourceType}` + const resourceNamePath = match.params.resourceName ? `/${match.params.resourceName}` : '' - const goBackURL = `${basePath}${envOverridePath}${resourceTypePath}${resourceNamePath}` + const goBackURL = `${basePath}${envOverridePath}${resourceTypePath}${resourceNamePath}` - return ( - ({ - id: environmentId, - isProtected, - name: environmentName, - }), - )} - isBaseConfigProtected={isBaseConfigProtected} - goBackURL={goBackURL} - getNavItemHref={(resourceType, resourceName) => - `${generatePath(match.path, { ...match.params, resourceType, resourceName })}${location.search}` - } - /> - ) - }} - , - ] - : []), + return ( + ({ + id: environmentId, + isProtected, + name: environmentName, + }))} + isBaseConfigProtected={isBaseConfigProtected} + goBackURL={goBackURL} + getNavItemHref={(resourceType, resourceName) => + `${generatePath(match.path, { ...match.params, resourceType, resourceName })}${location.search}` + } + /> + ) + }} + , ]} {/* Redirect route is there when current path url has something after /edit */} {location.pathname !== url && } diff --git a/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/BaseConfigurationNavigation.tsx b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/BaseConfigurationNavigation.tsx new file mode 100644 index 0000000000..c19317d011 --- /dev/null +++ b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/BaseConfigurationNavigation.tsx @@ -0,0 +1,24 @@ +import { + Button, + ButtonComponentType, + ButtonStyleType, + ButtonVariantType, + ComponentSizeType, +} from '@devtron-labs/devtron-fe-common-lib' +import { BaseConfigurationNavigationProps } from './types' + +const BaseConfigurationNavigation = ({ baseConfigurationURL }: BaseConfigurationNavigationProps) => ( + + ) +} + +const ConfigHeader = ({ + configHeaderTab, + handleTabChange, + isDisabled, + areChangesPresent, + isOverridable, + showNoOverride, + parsingError, + restoreLastSavedYAML, + hideDryRunTab = false, +}: ConfigHeaderProps) => { + const validTabKeys = isOverridable + ? CONFIG_HEADER_TAB_VALUES.OVERRIDE + : CONFIG_HEADER_TAB_VALUES.BASE_DEPLOYMENT_TEMPLATE + + const filteredTabKeys = validTabKeys.filter( + (currentTab: ConfigHeaderTabType) => !hideDryRunTab || currentTab !== ConfigHeaderTabType.DRY_RUN, + ) + + const activeTabIndex = filteredTabKeys.indexOf(configHeaderTab) + + return ( +
+ {filteredTabKeys.map((currentTab: ConfigHeaderTabType, index: number) => ( + +
+ +
+
+ ))} + +
= 0 && activeTabIndex === filteredTabKeys.length - 1 ? 'dc__border-left' : ''}`} + /> +
+ ) +} + +export default ConfigHeader diff --git a/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/ConfigToolbar.tsx b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/ConfigToolbar.tsx new file mode 100644 index 0000000000..5e4c23fa2e --- /dev/null +++ b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/ConfigToolbar.tsx @@ -0,0 +1,364 @@ +import { Fragment } from 'react' +import { + Button, + ButtonStyleType, + ButtonVariantType, + ConfigHeaderTabType, + ProtectConfigTabsType, + PopupMenu, + BaseURLParams, + ComponentSizeType, + InvalidYAMLTippyWrapper, +} from '@devtron-labs/devtron-fe-common-lib' +import { useParams } from 'react-router-dom' +import { importComponentFromFELibrary } from '@Components/common' +import { ReactComponent as ICMore } from '@Icons/ic-more-option.svg' +import { ReactComponent as ICBookOpen } from '@Icons/ic-book-open.svg' +import { ReactComponent as ICInfoOutlineGrey } from '@Icons/ic-info-outline-grey.svg' +import BaseConfigurationNavigation from './BaseConfigurationNavigation' +import ToggleResolveScopedVariables from './ToggleResolveScopedVariables' +import { PopupMenuItem } from './utils' +import { ConfigToolbarProps } from './types' +import SelectMergeStrategy from './SelectMergeStrategy' + +const ProtectionViewTabGroup = importComponentFromFELibrary('ProtectionViewTabGroup', null, 'function') +const MergePatchWithTemplateCheckbox = importComponentFromFELibrary('MergePatchWithTemplateCheckbox', null, 'function') +const ConfigApproversInfoTippy = importComponentFromFELibrary('ConfigApproversInfoTippy', null, 'function') +const ProtectConfigShowCommentsButton = importComponentFromFELibrary( + 'ProtectConfigShowCommentsButton', + null, + 'function', +) + +/** + * 1. Base template view (without protect config): + - ToggleEditorView (LHS) + - Chart selectors + - Readme(RHS) + - Scoped variables + - Popup menu (Locked keys (in case of OSS no popup)) +2. DRY RUN: No toolbar +3. INHERITED: + - Inherited from base config info tile + - Chart selectors (disabled) + - Readme + - Scoped variables + - Popup menu (Locked keys (in case of OSS no popup)) +4. In case of override: + 1. In case of no override - nothing +5. In case of protect config + 1. In case of edit draft + - protected tabs + - ToggleEditorView + - ChartSelectors + - Merge strategy + - Approver tippy (RHS) + - comments view + - Readme + - Scoped variables + - Popup menu (Locked keys, Delete override, Discard draft - only in case of saved as draft) + 2. In compare: + - protected tabs (NOTE: in case of approve tab name is Compare & Approve) + - show merged template button (in case at least on strategy is patch) + - Approver tippy (RHS) + - comments view + - Readme + - Scoped variables + - In popup (Locked keys, Delete override, discard draft) + */ +const ConfigToolbar = ({ + baseConfigurationURL, + + selectedProtectionViewTab, + handleProtectionViewTabChange, + + handleToggleCommentsView, + areCommentsPresent, + + showMergePatchesButton, + shouldMergeTemplateWithPatches, + handleToggleShowTemplateMergedWithPatch, + + mergeStrategy, + handleMergeStrategyChange, + + showEnableReadMeButton, + handleEnableReadmeView, + + children, + + popupConfig, + + handleToggleScopedVariablesView, + resolveScopedVariables, + + configHeaderTab, + isProtected = false, + isApprovalPending, + isDraftPresent, + approvalUsers, + disableAllActions = false, + parsingError = '', + restoreLastSavedYAML, + isPublishedConfigPresent = true, + headerMessage, + showDeleteOverrideDraftEmptyState, +}: ConfigToolbarProps) => { + const { envId } = useParams() + const isDisabled = disableAllActions || !!parsingError + + if (configHeaderTab === ConfigHeaderTabType.DRY_RUN) { + return null + } + + const isCompareView = !!( + isProtected && + configHeaderTab === ConfigHeaderTabType.VALUES && + selectedProtectionViewTab === ProtectConfigTabsType.COMPARE + ) + + const isPublishedValuesView = !!( + configHeaderTab === ConfigHeaderTabType.VALUES && + selectedProtectionViewTab === ProtectConfigTabsType.PUBLISHED && + isProtected && + isDraftPresent + ) + + const isEditView = !!( + configHeaderTab === ConfigHeaderTabType.VALUES && + (isProtected && isDraftPresent ? selectedProtectionViewTab === ProtectConfigTabsType.EDIT_DRAFT : true) + ) + + const showProtectedTabs = + isProtected && isDraftPresent && configHeaderTab === ConfigHeaderTabType.VALUES && !!ProtectionViewTabGroup + + const getLHSActionNodes = (): JSX.Element => { + if (configHeaderTab === ConfigHeaderTabType.INHERITED) { + return ( +
+ + Inherited from + +
+ ) + } + + return ( + <> + {headerMessage && configHeaderTab === ConfigHeaderTabType.VALUES && !showProtectedTabs && ( +
+ + {headerMessage} +
+ )} + {showProtectedTabs && ( +
+ {ProtectionViewTabGroup && ( + <> + + +
+ + )} + + {/* Data should always be valid in case we are in approval view */} + {isCompareView && MergePatchWithTemplateCheckbox && showMergePatchesButton && ( + +
+ +
+
+ )} +
+ )} + + ) + } + + const renderProtectedConfigActions = () => { + const shouldRenderApproverInfoTippy = !!isDraftPresent && isApprovalPending && ConfigApproversInfoTippy + const shouldRenderCommentsView = !!isDraftPresent + const hasNothingToRender = !shouldRenderApproverInfoTippy && !shouldRenderCommentsView + + if (!isProtected || hasNothingToRender) { + return null + } + + return ( + <> + {shouldRenderApproverInfoTippy && } + {shouldRenderCommentsView && ( + + )} + + ) + } + + const renderReadmeAndScopedVariablesBlock = () => { + const shouldRenderScopedVariables = window._env_.ENABLE_SCOPED_VARIABLES + const hasNothingToRender = !showEnableReadMeButton && !shouldRenderScopedVariables + + if (hasNothingToRender) { + return null + } + + return ( + <> + {showEnableReadMeButton && ( + +
+
+
+ )} + + {shouldRenderScopedVariables && ( + +
+ +
+
+ )} + + ) + } + + const renderSelectMergeStrategy = () => { + if (!envId || !isEditView || showDeleteOverrideDraftEmptyState) { + return null + } + + return ( + <> + {!!children &&
} + +
+ +
+
+ + ) + } + + const popupConfigGroups = Object.keys(popupConfig?.menuConfig ?? {}) + + const renderPopupMenu = () => { + if (!popupConfigGroups.length) { + return null + } + + if (popupConfig.popupNodeType) { + return popupConfig.popupMenuNode + } + + return ( + + + + + + +
+ {popupConfigGroups.map((groupName, index) => { + const groupItems = popupConfig.menuConfig[groupName] ?? [] + + return ( + + {index !== 0 &&
} + +
+ {groupItems.map(({ text, onClick, dataTestId, disabled, icon, variant }) => ( + + ))} +
+ + ) + })} +
+ + + ) + } + + return ( +
+
+ {getLHSActionNodes()} + + {children} + + {renderSelectMergeStrategy()} +
+ + {isPublishedValuesView && !isPublishedConfigPresent ? null : ( +
+ {renderProtectedConfigActions()} + + {renderReadmeAndScopedVariablesBlock()} + + {renderPopupMenu()} +
+ )} +
+ ) +} + +export default ConfigToolbar diff --git a/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/DeploymentConfigCompare/DeploymentConfigCompare.tsx b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/DeploymentConfigCompare/DeploymentConfigCompare.tsx similarity index 87% rename from src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/DeploymentConfigCompare/DeploymentConfigCompare.tsx rename to apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/DeploymentConfigCompare/DeploymentConfigCompare.tsx index 1e690e8d61..dcd57500a3 100644 --- a/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/DeploymentConfigCompare/DeploymentConfigCompare.tsx +++ b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/DeploymentConfigCompare/DeploymentConfigCompare.tsx @@ -16,8 +16,7 @@ import { EnvResourceType, } from '@devtron-labs/devtron-fe-common-lib' -import { getChartReferencesForAppAndEnv } from '@Services/service' -import { getOptions } from '@Components/deploymentConfig/service' +import { getTemplateOptions, getChartReferencesForAppAndEnv } from '@Services/service' import { BASE_CONFIGURATIONS } from '../../AppConfig.constants' import { @@ -42,6 +41,7 @@ import { isConfigTypePublished, } from './utils' import { getConfigDiffData, getDeploymentTemplateData, getManifestData } from './service.utils' +import { DeploymentConfigComparisonDataType } from './types' export const DeploymentConfigCompare = ({ environments, @@ -49,6 +49,7 @@ export const DeploymentConfigCompare = ({ envName, type = 'app', goBackURL = '', + overwriteNavHeading, isBaseConfigProtected = false, getNavItemHref, }: DeploymentConfigCompareProps) => { @@ -114,8 +115,12 @@ export const DeploymentConfigCompare = ({ // ASYNC CALLS // Load options for dropdown menus of previous deployments and default versions const [optionsLoader, options, optionsErr, reloadOptions] = useAsync( - () => Promise.all([getOptions(compareToAppId, compareToEnvId), getOptions(compareWithAppId, compareWithEnvId)]), - [compareToAppId, compareToEnvId, compareWithAppId, compareWithEnvId], + () => + Promise.all([ + getTemplateOptions(compareToAppId, compareToEnvId), + getTemplateOptions(compareWithAppId, compareWithEnvId), + ]), + [compareToAppId, compareToEnvId, compareWithAppId, compareWithEnvId, isManifestView], ) // Options for previous deployments and default versions @@ -189,31 +194,46 @@ export const DeploymentConfigCompare = ({ ]) } + const getComparisonData = async (): Promise => { + if (isManifestView) { + const manifestData = await fetchManifestData() + + return { + isManifestComparison: true, + manifestData, + } + } + + const appConfigData = await Promise.all([ + getConfigDiffData({ + type, + appName, + envName, + compareName: compareTo, + configType, + identifierId, + pipelineId, + }), + getConfigDiffData({ + type, + appName, + envName, + compareName: compareWith, + configType: compareWithConfigType, + identifierId: chartRefId || compareWithIdentifierId, + pipelineId: compareWithPipelineId, + }), + ]) + + return { + isManifestComparison: false, + appConfigData, + } + } + // Load data for comparing the two environments const [comparisonDataLoader, comparisonData, comparisonDataErr, reloadComparisonData] = useAsync( - () => - isManifestView - ? fetchManifestData() - : Promise.all([ - getConfigDiffData({ - type, - appName, - envName, - compareName: compareTo, - configType, - identifierId, - pipelineId, - }), - getConfigDiffData({ - type, - appName, - envName, - compareName: compareWith, - configType: compareWithConfigType, - identifierId: chartRefId || compareWithIdentifierId, - pipelineId: compareWithPipelineId, - }), - ]), + getComparisonData, [ isManifestView, compareTo, @@ -243,20 +263,34 @@ export const DeploymentConfigCompare = ({ // Generate the deployment configuration list for the environments using comparison data const appEnvDeploymentConfigList = useMemo(() => { if (!comparisonDataLoader && comparisonData) { - const [{ result: currentList }, { result: compareList }] = comparisonData + const { isManifestComparison, appConfigData, manifestData } = comparisonData + + if (isManifestComparison) { + const [{ result: currentList }, { result: compareList }] = manifestData + return getAppEnvDeploymentConfigList({ + currentList, + compareList, + getNavItemHref, + isManifestView: true, + }) + } - const configData = getAppEnvDeploymentConfigList({ - currentList, - compareList, - getNavItemHref, - isManifestView, - convertVariables, - }) - return configData + const [{ result: currentList }, { result: compareList }] = appConfigData + if (options) { + return getAppEnvDeploymentConfigList({ + currentList, + compareList, + getNavItemHref, + isManifestView, + convertVariables, + compareToTemplateOptions: options[0].result, + compareWithTemplateOptions: options[1].result, + }) + } } return null - }, [comparisonDataLoader, comparisonData, isManifestView, convertVariables]) + }, [comparisonDataLoader, comparisonData, isManifestView, convertVariables, options]) // SELECT PICKER OPTIONS /** Compare Environment Select Picker Options */ @@ -474,14 +508,15 @@ export const DeploymentConfigCompare = ({ onConvertVariablesClick: () => setConvertVariables(!convertVariables), } + // DATA CONSTANTS const isLoading = comparisonDataLoader || optionsLoader - const isError = comparisonDataErr || optionsErr + const isError = !!(comparisonDataErr || optionsErr) return ( = { code: 200, status: 'OK', result: { @@ -107,28 +107,31 @@ export const getManifestData = ({ configType === AppEnvDeploymentConfigType.PUBLISHED_WITH_DRAFT const isDefaultSelected = configType === AppEnvDeploymentConfigType.DEFAULT_VERSION - const deploymentManifestRequestData: Record = { + const isHistoricData = !values && envId > -1 + + const deploymentManifestRequestData: GetDeploymentManifestProps = { appId: +appId, - valuesAndManifestFlag: 2, chartRefId: manifestChartRefId, + ...(envId > -1 && { + envId, + }), + ...(!isDefaultSelected && { + ...(isHistoricData + ? { + type: + identifierId && pipelineId + ? TemplateListType.DeployedOnSelfEnvironment + : TemplateListType.PublishedOnEnvironments, + deploymentTemplateHistoryId: identifierId, + pipelineId, + } + : { + values: values || null, + }), + }), } - if (envId > -1) { - deploymentManifestRequestData.envId = envId - - if (!values && !isDefaultSelected) { - deploymentManifestRequestData.type = - identifierId && pipelineId - ? TemplateListType.DeployedOnSelfEnvironment - : TemplateListType.PublishedOnEnvironments - deploymentManifestRequestData.deploymentTemplateHistoryId = identifierId - deploymentManifestRequestData.pipelineId = pipelineId - } - } - - if (values && !isDefaultSelected) { - deploymentManifestRequestData.values = values - } - - return !isDraftSelected || values ? getDeploymentManisfest(deploymentManifestRequestData) : nullResponse + return !isDraftSelected || values + ? getDeploymentManifest(deploymentManifestRequestData) + : Promise.resolve(nullResponse) } diff --git a/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/DeploymentConfigCompare/types.ts b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/DeploymentConfigCompare/types.ts new file mode 100644 index 0000000000..fdf78e8794 --- /dev/null +++ b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/DeploymentConfigCompare/types.ts @@ -0,0 +1,15 @@ +import { getConfigDiffData, getManifestData } from './service.utils' + +type ManifestComparisonDataType = { + isManifestComparison: true + manifestData: [Awaited>, Awaited>] + appConfigData?: never +} + +type AppConfigComparisonDataType = { + isManifestComparison: false + appConfigData: [Awaited>, Awaited>] + manifestData?: never +} + +export type DeploymentConfigComparisonDataType = ManifestComparisonDataType | AppConfigComparisonDataType diff --git a/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/DeploymentConfigCompare/utils.ts b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/DeploymentConfigCompare/utils.ts similarity index 99% rename from src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/DeploymentConfigCompare/utils.ts rename to apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/DeploymentConfigCompare/utils.ts index d4863ba8df..32c491cd8e 100644 --- a/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/DeploymentConfigCompare/utils.ts +++ b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/DeploymentConfigCompare/utils.ts @@ -292,7 +292,7 @@ export const getManifestRequestValues = (config: AppEnvDeploymentConfigDTO): { d null return { - data: _data ? YAMLStringify(_data) ?? '' : '', + data: _data ? (YAMLStringify(_data) ?? '') : '', chartRefId: getDraftConfigChartRefId(config), } } diff --git a/src/components/deploymentConfig/ChartSelectorDropdown.tsx b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/DeploymentTemplate/DTChartSelector.tsx similarity index 56% rename from src/components/deploymentConfig/ChartSelectorDropdown.tsx rename to apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/DeploymentTemplate/DTChartSelector.tsx index 735eef3bf9..c9d6fbed1f 100644 --- a/src/components/deploymentConfig/ChartSelectorDropdown.tsx +++ b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/DeploymentTemplate/DTChartSelector.tsx @@ -14,38 +14,48 @@ * limitations under the License. */ -import React, { useState } from 'react' -import { sortObjectArrayAlphabetically } from '../common' +import { useState } from 'react' import { PopupMenu, stopPropagation, StyledRadioGroup as RadioGroup, + DeploymentChartVersionType, + SelectPicker, + SelectPickerOptionType, + SelectPickerVariantType, versionComparatorBySortOrder, + InvalidYAMLTippyWrapper, + ComponentSizeType, } from '@devtron-labs/devtron-fe-common-lib' -import { ReactComponent as Dropdown } from '../../assets/icons/ic-chevron-down.svg' -import { ChartSelectorModalType, DeploymentChartVersionType } from './types' -import { CHART_DOCUMENTATION_LINK, CHART_TYPE_TAB, CHART_TYPE_TAB_KEYS } from './constants' -import { DEPLOYMENT } from '../../config' +import { sortObjectArrayAlphabetically } from '@Components/common' +import { DEPLOYMENT } from '@Config/constants' +import { ReactComponent as Dropdown } from '@Icons/ic-chevron-down.svg' +import { ChartSelectorDropdownProps, DTChartSelectorProps } from './types' +import { CHART_TYPE_TAB_KEYS, CHART_TYPE_TAB, CHART_DOCUMENTATION_LINK } from './constants' -export default function ChartSelectorDropdown({ +const LoadingShimmer = () =>
+ +const ChartSelectorDropdown = ({ charts, chartsMetadata, selectedChartRefId, selectedChart, selectChart, isUnSet, -}: ChartSelectorModalType) { - const [popupOpen, togglePopup] = useState(false) + areChartsLoading, +}: ChartSelectorDropdownProps) => { + const [popupOpen, setPopupOpen] = useState(false) const [selectedChartTypeTab, setSelectedChartTypeTab] = useState( - selectedChart?.['userUploaded'] ? CHART_TYPE_TAB_KEYS.CUSTOM_CHARTS : CHART_TYPE_TAB_KEYS.DEVTRON_CHART, + selectedChart?.userUploaded ? CHART_TYPE_TAB_KEYS.CUSTOM_CHARTS : CHART_TYPE_TAB_KEYS.DEVTRON_CHART, ) const uniqueChartsByDevtron = new Map() const uniqueCustomCharts = new Map() let devtronCharts = [] let customCharts = [] - for (const chart of charts) { + + charts.forEach((chart) => { const chartName = chart.name - if (chart['userUploaded']) { + if (chart.userUploaded) { if (!uniqueCustomCharts.get(chartName)) { uniqueCustomCharts.set(chartName, true) customCharts.push(chart) @@ -54,15 +64,16 @@ export default function ChartSelectorDropdown({ uniqueChartsByDevtron.set(chartName, true) devtronCharts.push(chart) } - } + }) + devtronCharts = sortObjectArrayAlphabetically(devtronCharts, 'name') customCharts = sortObjectArrayAlphabetically(customCharts, 'name') const onSelectChartType = (selectedChartName: string): void => { - const filteredCharts = charts.filter((chart) => chart.name == selectedChartName) - const selectedChart = filteredCharts.find((chart) => chart.id == selectedChartRefId) - if (selectedChart) { - selectChart(selectedChart) + const filteredCharts = charts.filter((chart) => chart.name === selectedChartName) + const targetChart = filteredCharts.find((chart) => chart.id === selectedChartRefId) + if (targetChart) { + selectChart(targetChart) } else { const sortedFilteredCharts = filteredCharts.sort((a, b) => versionComparatorBySortOrder(a.version, b.version), @@ -76,28 +87,35 @@ export default function ChartSelectorDropdown({ } const setPopupState = (isOpen: boolean): void => { - togglePopup(isOpen) + setPopupOpen(isOpen) + } + + if (areChartsLoading) { + return } if (!isUnSet) { return ( - + {selectedChart?.name} ) } + return ( - - {selectedChart?.name || 'Select Chart'} +
+ + {selectedChart?.name || 'Select Chart'} + - +
- + <> {customCharts.length > 0 && (
(
onSelectChartType(chart.name)} >
{chart.name} - {DEPLOYMENT === chart.name && ( + {chart.name === DEPLOYMENT && ( Recommended )}
- {(chartsMetadata?.[chart.name]?.['chartDescription'] || chart.description) && ( -
- {chartsMetadata?.[chart.name]?.['chartDescription'] || + {(chartsMetadata?.[chart.name]?.chartDescription || chart.description) && ( +
+ {chartsMetadata?.[chart.name]?.chartDescription || chart.description.substring(0, 250)}   {CHART_DOCUMENTATION_LINK[chart.name] && ( @@ -178,3 +197,95 @@ export default function ChartSelectorDropdown({ ) } + +const DTChartSelector = ({ + isUnSet, + disableVersionSelect, + charts, + chartsMetadata, + selectedChart, + selectChart, + selectedChartRefId, + areChartsLoading, + parsingError, + restoreLastSavedTemplate, +}: DTChartSelectorProps) => { + const filteredCharts = selectedChart + ? charts + .filter((cv) => cv.name === selectedChart.name) + .sort((a, b) => versionComparatorBySortOrder(a.version, b.version)) + : [] + + const onSelectChartVersion = (selected: SelectPickerOptionType) => { + selectChart(charts.find((chart) => chart.id === selected.value) || selectedChart) + } + + const options: SelectPickerOptionType[] = filteredCharts.map((chart) => ({ + label: chart.version, + value: chart.id, + })) + + const selectedOption: SelectPickerOptionType = selectChart + ? { + label: selectedChart?.version, + value: selectedChart?.id, + } + : null + + const renderVersionSelector = () => { + if (areChartsLoading) { + return + } + + if (disableVersionSelect) { + return {selectedChart?.version} + } + + return ( +
+ +
+ ) + } + + return ( +
+ +
+ Chart + +
+
+ + +
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} + + {renderVersionSelector()} +
+
+
+ ) +} + +export default DTChartSelector diff --git a/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/DeploymentTemplate/DeleteOverrideDialog.tsx b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/DeploymentTemplate/DeleteOverrideDialog.tsx new file mode 100644 index 0000000000..42edd638a5 --- /dev/null +++ b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/DeploymentTemplate/DeleteOverrideDialog.tsx @@ -0,0 +1,58 @@ +import { useState } from 'react' +import { useParams } from 'react-router-dom' +import { + API_STATUS_CODES, + BaseURLParams, + DeleteDialog, + showError, + ToastManager, + ToastVariantType, +} from '@devtron-labs/devtron-fe-common-lib' +import { DeleteOverrideDialogProps } from './types' +import { deleteOverrideDeploymentTemplate } from './service' + +const DeleteOverrideDialog = ({ + environmentConfigId, + handleReload, + handleClose, + handleProtectionError, + reloadEnvironments, +}: DeleteOverrideDialogProps) => { + const { appId, envId } = useParams() + const [isDeletingOverride, setIsDeletingOverride] = useState(false) + + const handleDelete = async () => { + try { + setIsDeletingOverride(true) + await deleteOverrideDeploymentTemplate(environmentConfigId, Number(appId), Number(envId)) + ToastManager.showToast({ + variant: ToastVariantType.success, + description: 'Restored to global.', + }) + handleClose() + handleReload() + } catch (error) { + showError(error) + if (error.code === API_STATUS_CODES.LOCKED) { + handleProtectionError() + reloadEnvironments() + } + } finally { + setIsDeletingOverride(false) + } + } + + return ( + + ) +} + +export default DeleteOverrideDialog diff --git a/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/DeploymentTemplate/DeploymentTemplate.scss b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/DeploymentTemplate/DeploymentTemplate.scss new file mode 100644 index 0000000000..a3f70fb126 --- /dev/null +++ b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/DeploymentTemplate/DeploymentTemplate.scss @@ -0,0 +1,30 @@ +.deployment-template { + &__comments-view { + display: grid; + grid-template-columns: 1fr 0; + position: absolute; + left: 56px; + width: calc(100% - 56px); + height: calc(100% - 78px); + animation: comments-view-opening; + animation-duration: 200ms; + animation-timing-function: linear; + animation-fill-mode: forwards; + } + + &__approval-tippy { + .tippy-box { + top: 55px; + } + } + + @keyframes comments-view-opening { + 0% { + grid-template-columns: 1fr 0; + } + + 100% { + grid-template-columns: 1fr 300px; + } + } +} diff --git a/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/DeploymentTemplate/DeploymentTemplate.tsx b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/DeploymentTemplate/DeploymentTemplate.tsx new file mode 100644 index 0000000000..9303d6d287 --- /dev/null +++ b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/DeploymentTemplate/DeploymentTemplate.tsx @@ -0,0 +1,1661 @@ +import { useEffect, SyntheticEvent, useMemo, useReducer, Reducer, useRef } from 'react' +import ReactGA from 'react-ga4' +import { + BaseURLParams, + ConfigurationType, + DeploymentChartVersionType, + DraftState, + showError, + useMainContext, + YAMLStringify, + DeploymentTemplateConfigState, + Progressing, + ErrorScreenManager, + getResolvedDeploymentTemplate, + ModuleStatus, + useAsync, + ModuleNameMap, + ToastManager, + ToastVariantType, + SelectPickerOptionType, + ValuesAndManifestFlagDTO, + CompareFromApprovalOptionsValuesType, + noop, + logExceptionToSentry, + ConfigHeaderTabType, + ProtectConfigTabsType, + Button, + ComponentSizeType, + ButtonStyleType, + ButtonVariantType, + DryRunEditorMode, + usePrompt, + DEFAULT_LOCKED_KEYS_CONFIG, + GenericEmptyState, + GET_RESOLVED_DEPLOYMENT_TEMPLATE_EMPTY_RESPONSE, + ResponseType, + API_STATUS_CODES, +} from '@devtron-labs/devtron-fe-common-lib' +import { Prompt, useLocation, useParams } from 'react-router-dom' +import YAML from 'yaml' +import { FloatingVariablesSuggestions, importComponentFromFELibrary } from '@Components/common' +import { getModuleInfo } from '@Components/v2/devtronStackManager/DevtronStackManager.service' +import { URLS } from '@Config/routes' +import { DEFAULT_ROUTE_PROMPT_MESSAGE } from '@Config/constants' +import { ReactComponent as ICClose } from '@Icons/ic-close.svg' +import { ReactComponent as ICInfoOutlineGrey } from '@Icons/ic-info-outline-grey.svg' +import deleteOverrideEmptyStateImage from '@Images/no-artifact@2x.png' +import { + DeploymentTemplateProps, + DeploymentTemplateStateType, + GetLockConfigEligibleAndIneligibleChangesType, + GetPublishedAndBaseDeploymentTemplateReturnType, + HandleInitializeTemplatesWithoutDraftParamsType, + UpdateBaseDTPayloadType, + UpdateEnvironmentDTPayloadType, +} from './types' +import { BASE_DEPLOYMENT_TEMPLATE_ENV_ID, NO_SCOPED_VARIABLES_MESSAGE } from './constants' +import DeploymentTemplateOptionsHeader from './DeploymentTemplateOptionsHeader' +import DeploymentTemplateForm from './DeploymentTemplateForm' +import DeploymentTemplateCTA from './DeploymentTemplateCTA' +import { + getAreTemplateChangesPresent, + getCompareFromEditorConfig, + getCurrentEditorPayloadForScopedVariables, + getCurrentEditorState, + getDeleteProtectedOverridePayload, + getDeploymentTemplateResourceName, + getEditorTemplateAndLockedKeys, + getLockedDiffModalDocuments, + getUpdateBaseDeploymentTemplatePayload, + getUpdateEnvironmentDTPayload, + handleInitializeDraftData, +} from './utils' +import DeleteOverrideDialog from './DeleteOverrideDialog' +import { + updateBaseDeploymentTemplate, + createBaseDeploymentTemplate, + updateEnvDeploymentTemplate, + createEnvDeploymentTemplate, + getEnvOverrideDeploymentTemplate, + getBaseDeploymentTemplate, + getChartList, +} from './service' +import ConfigHeader from '../ConfigHeader' +import './DeploymentTemplate.scss' +import ConfigToolbar from '../ConfigToolbar' +import { DEFAULT_MERGE_STRATEGY } from '../constants' +import { ConfigToolbarProps, DeploymentTemplateComponentType } from '../types' +import { getConfigToolbarPopupConfig } from '../utils' +import ConfigDryRun from '../ConfigDryRun' +import NoOverrideEmptyState from '../NoOverrideEmptyState' +import CompareConfigView from '../CompareConfigView' +import NoPublishedVersionEmptyState from '../NoPublishedVersionEmptyState' +import BaseConfigurationNavigation from '../BaseConfigurationNavigation' +import { + DeploymentTemplateActionState, + DeploymentTemplateActionType, + deploymentTemplateReducer, + getDeploymentTemplateInitialState, +} from './deploymentTemplateReducer' + +const getDraftByResourceName = importComponentFromFELibrary('getDraftByResourceName', null, 'function') +const getJsonPath = importComponentFromFELibrary('getJsonPath', null, 'function') +const getLockConfigEligibleAndIneligibleChanges: GetLockConfigEligibleAndIneligibleChangesType = + importComponentFromFELibrary('getLockConfigEligibleAndIneligibleChanges', null, 'function') +const ProtectedDeploymentTemplateCTA = importComponentFromFELibrary('ProtectedDeploymentTemplateCTA', null, 'function') +const DeploymentTemplateLockedDiff = importComponentFromFELibrary('DeploymentTemplateLockedDiff') +const SaveChangesModal = importComponentFromFELibrary('SaveChangesModal') +const DraftComments = importComponentFromFELibrary('DraftComments') +const DeleteOverrideDraftModal = importComponentFromFELibrary('DeleteOverrideDraftModal') +const ProtectionViewToolbarPopupNode = importComponentFromFELibrary('ProtectionViewToolbarPopupNode', null, 'function') + +const DeploymentTemplate = ({ + respondOnSuccess = noop, + isUnSet = false, + isCiPipeline = false, + isProtected, + reloadEnvironments, + environmentName, + clusterId, + fetchEnvConfig, +}: DeploymentTemplateProps) => { + // If envId is there, then it is from envOverride + const { appId, envId } = useParams() + const location = useLocation() + const { isSuperAdmin } = useMainContext() + + const [state, dispatch] = useReducer>( + deploymentTemplateReducer, + getDeploymentTemplateInitialState({ isSuperAdmin }), + ) + + const { + chartDetails, + lockedConfigKeysWithLockType, + publishedTemplateData, + draftTemplateData, + baseDeploymentTemplateData, + resolveScopedVariables, + isResolvingVariables, + resolvedEditorTemplate, + resolvedOriginalTemplate, + showDraftComments, + hideLockedKeys, + editMode, + configHeaderTab, + shouldMergeTemplateWithPatches, + selectedProtectionViewTab, + dryRunEditorMode, + popupNodeType, + showReadMe, + compareFromSelectedOptionValue, + lockedDiffModalState: { showLockedDiffForApproval, showLockedTemplateDiffModal }, + currentEditorTemplateData, + showDeleteDraftOverrideDialog, + showDeleteOverrideDialog, + isSaving, + showSaveChangesModal, + isLoadingChangedChartDetails, + isLoadingInitialData, + initialLoadError, + resolvedPublishedTemplate, + areCommentsPresent, + wasGuiOrHideLockedKeysEdited, + } = state + + const manifestAbortController = useRef(new AbortController()) + const [, grafanaModuleStatus] = useAsync(() => getModuleInfo(ModuleNameMap.GRAFANA), []) + + const isDryRunView = configHeaderTab === ConfigHeaderTabType.DRY_RUN + const isInheritedView = configHeaderTab === ConfigHeaderTabType.INHERITED + const isDraftAvailable = isProtected && !!draftTemplateData?.latestDraft + + const isPublishedValuesView = !!( + isDraftAvailable && + ((configHeaderTab === ConfigHeaderTabType.VALUES && + selectedProtectionViewTab === ProtectConfigTabsType.PUBLISHED) || + (isDryRunView && dryRunEditorMode === DryRunEditorMode.PUBLISHED_VALUES)) + ) + const isCompareView = !!( + isProtected && + configHeaderTab === ConfigHeaderTabType.VALUES && + selectedProtectionViewTab === ProtectConfigTabsType.COMPARE + ) + + const isApprovalPending = isDraftAvailable && draftTemplateData.latestDraft.draftState === DraftState.AwaitApproval + const isApprovalView = + isApprovalPending && (isCompareView || (isDryRunView && dryRunEditorMode === DryRunEditorMode.APPROVAL_PENDING)) + + const showApprovalPendingEditorInCompareView = + isCompareView && + isApprovalView && + compareFromSelectedOptionValue === CompareFromApprovalOptionsValuesType.APPROVAL_PENDING + + const showNoOverrideTab = + !!envId && !isDraftAvailable && !publishedTemplateData?.isOverridden && !currentEditorTemplateData?.isOverridden + + const showNoOverrideEmptyState = showNoOverrideTab && configHeaderTab === ConfigHeaderTabType.VALUES + // TODO: After CM/CS Merge strategy is implemented, we can re-use enum + const isDeleteOverrideDraft = !!envId && draftTemplateData?.latestDraft?.action === 3 + const showDeleteOverrideDraftEmptyState = + isDeleteOverrideDraft && + configHeaderTab === ConfigHeaderTabType.VALUES && + selectedProtectionViewTab === ProtectConfigTabsType.EDIT_DRAFT + + const isLoadingSideEffects = isResolvingVariables || isSaving || isLoadingChangedChartDetails + + /** + * There are two cases: + * 1. In case of base config - Published config is the one that we are using currently and is always present (Even in zero state we are have a default saved config) + * 2. In case of override - Published config is the one that is overridden from base config (Inherited) and is not always present + */ + const isPublishedConfigPresent = !(envId && !publishedTemplateData?.isOverridden) + + const showNoPublishedVersionEmptyState = isPublishedValuesView && !isPublishedConfigPresent + + const isEditMode = + configHeaderTab === ConfigHeaderTabType.VALUES && + (!isProtected || selectedProtectionViewTab === ProtectConfigTabsType.EDIT_DRAFT) + + const isGuiSupported = isEditMode && !showDeleteOverrideDraftEmptyState + + const baseDeploymentTemplateURL = `${URLS.APP}/${appId}/${URLS.APP_CONFIG}/${URLS.APP_DEPLOYMENT_CONFIG}` + + const hasNoGlobalConfig = !envId && !chartDetails.latestAppChartRef + const shouldValidateLockChanges = + !!getLockConfigEligibleAndIneligibleChanges && + lockedConfigKeysWithLockType.config.length > 0 && + !isSuperAdmin && + !hasNoGlobalConfig + + const disableCodeEditor = resolveScopedVariables || !isEditMode + + const isUpdateView = envId + ? currentEditorTemplateData?.environmentConfig?.id > 0 + : !!currentEditorTemplateData?.chartConfig?.id + + const areChangesPresent: boolean = useMemo( + () => getAreTemplateChangesPresent(state), + [currentEditorTemplateData, wasGuiOrHideLockedKeysEdited], + ) + + usePrompt({ + shouldPrompt: areChangesPresent, + }) + + const handleUnResolveScopedVariables = () => { + ReactGA.event({ + category: 'devtronapp-configuration-dt', + action: 'clicked-unresolve-scoped-variable', + }) + + dispatch({ + type: DeploymentTemplateActionType.UN_RESOLVE_SCOPED_VARIABLES, + }) + } + + const handleToggleShowTemplateMergedWithPatch = () => { + dispatch({ + type: DeploymentTemplateActionType.TOGGLE_SHOW_COMPARISON_WITH_MERGED_PATCHES, + }) + } + + const handleUpdateProtectedTabSelection = (tab: ProtectConfigTabsType, triggerGA: boolean = true) => { + if (tab === selectedProtectionViewTab) { + return + } + + if (triggerGA && tab === ProtectConfigTabsType.COMPARE) { + ReactGA.event({ + category: 'devtronapp-configuration-dt', + action: 'clicked-compare', + }) + } + + dispatch({ + type: DeploymentTemplateActionType.UPDATE_PROTECTION_VIEW_TAB, + payload: { + selectedProtectionViewTab: tab, + }, + }) + } + + const handleCompareFromOptionSelection = (option: SelectPickerOptionType) => { + dispatch({ + type: DeploymentTemplateActionType.CHANGE_COMPARE_FROM_SELECTED_OPTION, + payload: { + compareFromSelectedOptionValue: option.value as CompareFromApprovalOptionsValuesType, + }, + }) + } + + const handleSetHideLockedKeys = (value: boolean) => { + ReactGA.event({ + category: 'devtronapp-configuration-dt', + action: value ? 'clicked-hide-locked-keys' : 'clicked-show-locked-keys', + }) + + dispatch({ + type: DeploymentTemplateActionType.UPDATE_HIDE_LOCKED_KEYS, + payload: { + hideLockedKeys: value, + }, + }) + } + + const handleLoadScopedVariables = async () => { + try { + /** + * This is used to fetch the unedited document to show gui view + */ + const shouldFetchOriginalTemplate: boolean = !!isGuiSupported + // Fetching LHS of compare view + const shouldFetchPublishedTemplate: boolean = isPublishedConfigPresent && isCompareView + + const [currentEditorTemplate, originalTemplate, publishedTemplate] = await Promise.all([ + getResolvedDeploymentTemplate({ + appId: +appId, + chartRefId: currentEditorTemplateData.selectedChartRefId, + values: getCurrentEditorPayloadForScopedVariables({ + state, + isPublishedConfigPresent, + isDryRunView, + isDeleteOverrideDraft, + isInheritedView, + isPublishedValuesView, + showApprovalPendingEditorInCompareView, + }), + valuesAndManifestFlag: ValuesAndManifestFlagDTO.DEPLOYMENT_TEMPLATE, + ...(envId && { envId: +envId }), + }), + shouldFetchOriginalTemplate + ? getResolvedDeploymentTemplate({ + appId: +appId, + chartRefId: currentEditorTemplateData.originalTemplateState.selectedChartRefId, + values: YAMLStringify(currentEditorTemplateData.originalTemplate, { simpleKeys: true }), + valuesAndManifestFlag: ValuesAndManifestFlagDTO.DEPLOYMENT_TEMPLATE, + ...(envId && { envId: +envId }), + }) + : structuredClone(GET_RESOLVED_DEPLOYMENT_TEMPLATE_EMPTY_RESPONSE), + shouldFetchPublishedTemplate + ? getResolvedDeploymentTemplate({ + appId: +appId, + chartRefId: publishedTemplateData.selectedChartRefId, + values: publishedTemplateData.editorTemplate, + valuesAndManifestFlag: ValuesAndManifestFlagDTO.DEPLOYMENT_TEMPLATE, + ...(envId && { envId: +envId }), + }) + : structuredClone(GET_RESOLVED_DEPLOYMENT_TEMPLATE_EMPTY_RESPONSE), + ]) + + const areNoVariablesPresent = + !currentEditorTemplate.areVariablesPresent && + (!shouldFetchPublishedTemplate || !publishedTemplate.areVariablesPresent) + + if (areNoVariablesPresent) { + ToastManager.showToast({ + variant: ToastVariantType.error, + description: NO_SCOPED_VARIABLES_MESSAGE, + }) + + dispatch({ + type: DeploymentTemplateActionType.UN_RESOLVE_SCOPED_VARIABLES, + }) + return + } + + dispatch({ + type: DeploymentTemplateActionType.RESOLVE_SCOPED_VARIABLES, + payload: { + resolvedEditorTemplate: { + originalTemplateString: currentEditorTemplate.resolvedData, + templateWithoutLockedKeys: getEditorTemplateAndLockedKeys( + currentEditorTemplate.resolvedData, + lockedConfigKeysWithLockType.config, + ).editorTemplate, + }, + resolvedOriginalTemplate: { + originalTemplateString: originalTemplate.resolvedData, + templateWithoutLockedKeys: getEditorTemplateAndLockedKeys( + originalTemplate.resolvedData, + lockedConfigKeysWithLockType.config, + ).editorTemplate, + }, + resolvedPublishedTemplate: { + originalTemplateString: publishedTemplate.resolvedData, + templateWithoutLockedKeys: getEditorTemplateAndLockedKeys( + publishedTemplate.resolvedData, + lockedConfigKeysWithLockType.config, + ).editorTemplate, + }, + }, + }) + } catch (error) { + showError(error) + dispatch({ + type: DeploymentTemplateActionType.UN_RESOLVE_SCOPED_VARIABLES, + }) + } + } + + const handleToggleDraftComments = () => { + dispatch({ + type: DeploymentTemplateActionType.TOGGLE_DRAFT_COMMENTS, + }) + } + + const handleUpdateAreCommentsPresent = (value: boolean) => { + dispatch({ + type: DeploymentTemplateActionType.UPDATE_ARE_COMMENTS_PRESENT, + payload: { + areCommentsPresent: value, + }, + }) + } + + const handleResolveScopedVariables = async () => { + ReactGA.event({ + category: 'devtronapp-configuration-dt', + action: 'clicked-resolve-scoped-variable', + }) + + dispatch({ + type: DeploymentTemplateActionType.INITIATE_RESOLVE_SCOPED_VARIABLES, + }) + await handleLoadScopedVariables() + } + + const handleEditorChange = (template: string) => { + if (disableCodeEditor) { + return + } + + dispatch({ + type: DeploymentTemplateActionType.CURRENT_EDITOR_VALUE_CHANGE, + payload: { template }, + }) + } + + const handleToggleResolveScopedVariables = async () => { + if (resolveScopedVariables) { + handleUnResolveScopedVariables() + return + } + + await handleResolveScopedVariables() + } + + const handleChangeToGUIMode = () => { + dispatch({ + type: DeploymentTemplateActionType.CHANGE_TO_GUI_MODE, + }) + } + + const handleChangeToYAMLMode = () => { + dispatch({ + type: DeploymentTemplateActionType.CHANGE_TO_YAML_MODE, + }) + } + + const handleChangeDryRunEditorMode = (mode: DryRunEditorMode) => { + dispatch({ + type: DeploymentTemplateActionType.UPDATE_DRY_RUN_EDITOR_MODE, + payload: { + dryRunEditorMode: mode, + }, + }) + } + + const handleUpdateReadmeMode = (value: boolean) => { + dispatch({ + type: DeploymentTemplateActionType.UPDATE_README_MODE, + payload: { + showReadMe: value, + }, + }) + } + + const handleEnableReadmeView = () => { + handleUpdateReadmeMode(true) + } + + const handleDisableReadmeView = () => { + handleUpdateReadmeMode(false) + } + + const handleConfigHeaderTabChange = (tab: ConfigHeaderTabType) => { + if (configHeaderTab === tab) { + return + } + + if (configHeaderTab === ConfigHeaderTabType.DRY_RUN) { + ReactGA.event({ + category: 'devtronapp-configuration-dt', + action: 'clicked-dry-run', + }) + } + + dispatch({ + type: DeploymentTemplateActionType.UPDATE_CONFIG_HEADER_TAB, + payload: { + configHeaderTab: tab, + }, + }) + } + + const handleViewInheritedConfig = () => { + handleConfigHeaderTabChange(ConfigHeaderTabType.INHERITED) + } + + const getCurrentTemplateSelectedChart = (): DeploymentChartVersionType => + getCurrentEditorState({ + state, + isPublishedConfigPresent, + isDryRunView, + isDeleteOverrideDraft, + isInheritedView, + isPublishedValuesView, + showApprovalPendingEditorInCompareView, + })?.selectedChart + + const getCurrentTemplateGUISchema = (): string => { + if (!isGuiSupported) { + return '{}' + } + + return currentEditorTemplateData?.guiSchema || '{}' + } + + const getCurrentEditorSchema = (): DeploymentTemplateConfigState['schema'] => + getCurrentEditorState({ + state, + isPublishedConfigPresent, + isDryRunView, + isDeleteOverrideDraft, + isInheritedView, + isPublishedValuesView, + showApprovalPendingEditorInCompareView, + })?.schema + + const handleFetchGlobalDeploymentTemplate = async ( + globalChartDetails: DeploymentChartVersionType, + lockedConfigKeys: string[] = lockedConfigKeysWithLockType.config, + ): Promise => { + const { + result: { + globalConfig: { + defaultAppOverride, + id, + refChartTemplate, + refChartTemplateVersion, + isAppMetricsEnabled, + chartRefId, + readme, + schema, + }, + guiSchema, + }, + } = await getBaseDeploymentTemplate(+appId, +globalChartDetails.id, null, globalChartDetails.name) + + const stringifiedTemplate = YAMLStringify(defaultAppOverride, { simpleKeys: true }) + + const { editorTemplate: editorTemplateWithoutLockedKeys } = getEditorTemplateAndLockedKeys( + stringifiedTemplate, + lockedConfigKeys, + ) + + return { + originalTemplate: defaultAppOverride, + schema, + readme, + guiSchema, + isAppMetricsEnabled, + chartConfig: { id, refChartTemplate, refChartTemplateVersion, chartRefId, readme }, + editorTemplate: stringifiedTemplate, + editorTemplateWithoutLockedKeys, + selectedChart: globalChartDetails, + selectedChartRefId: +globalChartDetails.id, + } + } + + /** + * Fetch deployment template on basis of chart id in case of base deployment template or env override + */ + const handleFetchDeploymentTemplate = async ( + chartInfo: DeploymentChartVersionType, + lockedConfigKeys: string[] = lockedConfigKeysWithLockType.config, + ): Promise => { + if (!envId) { + return handleFetchGlobalDeploymentTemplate(chartInfo, lockedConfigKeys) + } + + const { + result: { globalConfig, environmentConfig, guiSchema, IsOverride, schema, readme, appMetrics }, + } = await getEnvOverrideDeploymentTemplate(+appId, +envId, +chartInfo.id, chartInfo.name) + + const { id, status, manualReviewed, active, namespace, envOverrideValues } = environmentConfig || {} + + const originalTemplate = IsOverride ? envOverrideValues || globalConfig : globalConfig + const stringifiedTemplate = YAMLStringify(originalTemplate, { simpleKeys: true }) + + const { editorTemplate: editorTemplateWithoutLockedKeys } = getEditorTemplateAndLockedKeys( + stringifiedTemplate, + lockedConfigKeys, + ) + + return { + originalTemplate, + schema, + readme, + guiSchema, + isAppMetricsEnabled: appMetrics, + editorTemplate: stringifiedTemplate, + isOverridden: !!IsOverride, + environmentConfig: { + id, + status, + manualReviewed, + active, + namespace, + }, + mergeStrategy: DEFAULT_MERGE_STRATEGY, + editorTemplateWithoutLockedKeys, + selectedChart: chartInfo, + selectedChartRefId: +chartInfo.id, + } + } + + const getPublishedAndBaseDeploymentTemplate = async ( + chartRefsData: Awaited>, + lockedConfigKeys: string[], + ): Promise => { + const shouldFetchBaseDeploymentData = !!envId + const [templateData, baseDeploymentTemplateDataResponse] = await Promise.all([ + handleFetchDeploymentTemplate(chartRefsData.selectedChart, lockedConfigKeys), + shouldFetchBaseDeploymentData + ? handleFetchGlobalDeploymentTemplate(chartRefsData.globalChartDetails, lockedConfigKeys) + : null, + ]) + + return { + publishedTemplateState: templateData, + baseDeploymentTemplateState: shouldFetchBaseDeploymentData + ? baseDeploymentTemplateDataResponse + : templateData, + } + } + + const handleInitializeTemplatesWithoutDraft = ({ + baseDeploymentTemplateState, + publishedTemplateState, + chartDetailsState, + lockedConfigKeysWithLockTypeState, + }: HandleInitializeTemplatesWithoutDraftParamsType) => { + const clonedTemplateData = structuredClone(publishedTemplateState) + delete clonedTemplateData.editorTemplateWithoutLockedKeys + + const currentEditorState: typeof currentEditorTemplateData = { + ...clonedTemplateData, + parsingError: '', + removedPatches: [], + originalTemplateState: publishedTemplateState, + } + + dispatch({ + type: DeploymentTemplateActionType.INITIALIZE_TEMPLATES_WITHOUT_DRAFT, + payload: { + baseDeploymentTemplateData: baseDeploymentTemplateState, + publishedTemplateData: publishedTemplateState, + chartDetails: chartDetailsState, + lockedConfigKeysWithLockType: lockedConfigKeysWithLockTypeState, + currentEditorTemplateData: currentEditorState, + envId, + }, + }) + } + + const handleInitializePublishedAndCurrentEditorData = async ( + chartRefsData: Awaited>, + lockedKeysConfig: typeof lockedConfigKeysWithLockType, + ) => { + const { publishedTemplateState, baseDeploymentTemplateState } = await getPublishedAndBaseDeploymentTemplate( + chartRefsData, + lockedKeysConfig.config, + ) + handleInitializeTemplatesWithoutDraft({ + baseDeploymentTemplateState, + publishedTemplateState, + chartDetailsState: { + charts: chartRefsData.charts, + chartsMetadata: chartRefsData.chartsMetadata, + globalChartDetails: chartRefsData.globalChartDetails, + latestAppChartRef: chartRefsData.latestAppChartRef, + }, + lockedConfigKeysWithLockTypeState: lockedKeysConfig, + }) + } + + // Should remove edit draft mode in case of error and show normal edit values view with zero drafts where user can save as draft? + const handleLoadProtectedDeploymentTemplate = async ( + chartRefsData: Awaited>, + lockedKeysConfig: typeof lockedConfigKeysWithLockType, + ) => { + const [draftPromiseResponse, publishedAndBaseTemplateDataResponse] = await Promise.allSettled([ + getDraftByResourceName( + +appId, + +envId || BASE_DEPLOYMENT_TEMPLATE_ENV_ID, + 3, + getDeploymentTemplateResourceName(environmentName), + ), + getPublishedAndBaseDeploymentTemplate(chartRefsData, lockedKeysConfig.config), + ]) + + if (publishedAndBaseTemplateDataResponse.status === 'rejected') { + throw publishedAndBaseTemplateDataResponse.reason + } + + const { publishedTemplateState, baseDeploymentTemplateState } = publishedAndBaseTemplateDataResponse.value + + const shouldInitializeWithoutDraft = + draftPromiseResponse.status === 'rejected' || + !draftPromiseResponse.value?.result || + !( + draftPromiseResponse.value.result.draftState === DraftState.Init || + draftPromiseResponse.value.result.draftState === DraftState.AwaitApproval + ) + + if (shouldInitializeWithoutDraft) { + handleInitializeTemplatesWithoutDraft({ + baseDeploymentTemplateState, + publishedTemplateState, + chartDetailsState: { + charts: chartRefsData.charts, + chartsMetadata: chartRefsData.chartsMetadata, + globalChartDetails: chartRefsData.globalChartDetails, + latestAppChartRef: chartRefsData.latestAppChartRef, + }, + lockedConfigKeysWithLockTypeState: lockedKeysConfig, + }) + return + } + + const draftResponse = draftPromiseResponse.value + // NOTE: In case of support for version based guiSchema this won't work + // Since we do not have guiSchema for draft, we are using published guiSchema + const { guiSchema } = publishedTemplateState + + const latestDraft = draftResponse.result + const draftTemplateState = handleInitializeDraftData({ + latestDraft, + guiSchema, + chartRefsData, + lockedConfigKeys: lockedKeysConfig.config, + envId, + }) + + const clonedTemplateData = structuredClone(draftTemplateState) + delete clonedTemplateData.editorTemplateWithoutLockedKeys + + dispatch({ + type: DeploymentTemplateActionType.INITIALIZE_TEMPLATES_WITH_DRAFT, + payload: { + baseDeploymentTemplateData: baseDeploymentTemplateState, + publishedTemplateData: publishedTemplateState, + chartDetails: { + charts: chartRefsData.charts, + chartsMetadata: chartRefsData.chartsMetadata, + globalChartDetails: chartRefsData.globalChartDetails, + latestAppChartRef: chartRefsData.latestAppChartRef, + }, + lockedConfigKeysWithLockType: lockedKeysConfig, + draftTemplateData: draftTemplateState, + currentEditorTemplateData: { + ...clonedTemplateData, + parsingError: '', + removedPatches: [], + originalTemplateState: draftTemplateState, + }, + selectedProtectionViewTab: + draftTemplateState.latestDraft?.draftState === DraftState.AwaitApproval + ? ProtectConfigTabsType.COMPARE + : ProtectConfigTabsType.EDIT_DRAFT, + }, + }) + } + + const handleInitialDataLoad = async () => { + dispatch({ + type: DeploymentTemplateActionType.INITIATE_INITIAL_DATA_LOAD, + }) + + try { + reloadEnvironments() + const [chartRefsDataResponse, lockedKeysConfigResponse] = await Promise.allSettled([ + getChartList({ appId, envId }), + getJsonPath ? getJsonPath(appId, envId || BASE_DEPLOYMENT_TEMPLATE_ENV_ID) : Promise.resolve(null), + ]) + + if (chartRefsDataResponse.status === 'rejected') { + throw chartRefsDataResponse.reason + } + const chartRefsData = chartRefsDataResponse.value + + const isLockedConfigResponseValid = + lockedKeysConfigResponse.status === 'fulfilled' && !!lockedKeysConfigResponse.value?.result + + const lockedKeysConfig: typeof lockedConfigKeysWithLockType = isLockedConfigResponseValid + ? structuredClone(lockedKeysConfigResponse.value.result) + : structuredClone(DEFAULT_LOCKED_KEYS_CONFIG) + + const shouldFetchDraftDetails = isProtected && typeof getDraftByResourceName === 'function' + + if (shouldFetchDraftDetails) { + await handleLoadProtectedDeploymentTemplate(chartRefsData, lockedKeysConfig) + return + } + + await handleInitializePublishedAndCurrentEditorData(chartRefsData, lockedKeysConfig) + } catch (error) { + showError(error) + dispatch({ + type: DeploymentTemplateActionType.INITIAL_DATA_ERROR, + payload: { + error, + }, + }) + } + } + + useEffect(() => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + handleInitialDataLoad() + }, []) + + const handleReload = async () => { + dispatch({ + type: DeploymentTemplateActionType.RESET_ALL, + payload: { + isSuperAdmin, + }, + }) + + fetchEnvConfig(+envId || BASE_DEPLOYMENT_TEMPLATE_ENV_ID) + reloadEnvironments() + await handleInitialDataLoad() + } + + /** + * + * @param skipReadmeAndSchema - true only while doing handleSave + */ + const prepareDataToSave = ( + skipReadmeAndSchema: boolean = false, + fromDeleteOverride: boolean = false, + ): UpdateBaseDTPayloadType | UpdateEnvironmentDTPayloadType => { + if (!envId) { + return getUpdateBaseDeploymentTemplatePayload(state, +appId, skipReadmeAndSchema) + } + + // NOTE: We don't handle lock keys in case of deletion of override + if (fromDeleteOverride) { + return getDeleteProtectedOverridePayload(state, +envId, skipReadmeAndSchema) + } + + return getUpdateEnvironmentDTPayload(state, +envId, skipReadmeAndSchema) + } + + // NOTE: This is a hack ideally BE should not even take data for this, they should only need action + const handlePrepareDataToSaveForProtectedDeleteOverride = () => prepareDataToSave(false, true) + + /** + * @description - This function returns a method to save deployment template which is based on whether it is base or env override + * In case of base deployment template, it checks if it is an update or create based on chartConfig.id, if this is present then it is an update + * In case of env override, it checks if it is an update or create based on environmentConfig.id, if this is present then it is an update + */ + const getSaveAPIService = (): (( + payload: ReturnType, + abortSignal?: AbortSignal, + ) => Promise>) => { + if (!envId) { + return isUpdateView ? updateBaseDeploymentTemplate : createBaseDeploymentTemplate + } + + return isUpdateView + ? updateEnvDeploymentTemplate + : (payload, abortSignal) => + createEnvDeploymentTemplate(+appId, +envId, payload as UpdateEnvironmentDTPayloadType, abortSignal) + } + + const getSuccessToastMessage = (): string => { + if (!envId) { + return isUpdateView ? 'Updated' : 'Saved' + } + + return isUpdateView ? 'Updated override' : 'Overridden' + } + + const handleSaveTemplate = async () => { + dispatch({ + type: DeploymentTemplateActionType.INITIATE_SAVE, + }) + + try { + const apiService = getSaveAPIService() + const response = await apiService(prepareDataToSave(true), null) + + const isLockConfigError = !!response?.result?.isLockConfigError + + dispatch({ + type: DeploymentTemplateActionType.FINISH_SAVE, + payload: { + isLockConfigError, + }, + }) + + if (isLockConfigError) { + return + } + + await handleReload() + respondOnSuccess(!isCiPipeline) + ToastManager.showToast({ + variant: ToastVariantType.success, + title: getSuccessToastMessage(), + description: 'Changes will be reflected after next deployment.', + }) + } catch (error) { + const isProtectionError = error.code === API_STATUS_CODES.LOCKED + + showError(error) + dispatch({ + type: DeploymentTemplateActionType.SAVE_ERROR, + payload: { + isProtectionError, + }, + }) + + if (isProtectionError) { + reloadEnvironments() + } + } + } + + const handleTriggerSave = async (e: SyntheticEvent) => { + e.preventDefault() + + ReactGA.event({ + category: 'devtronapp-configuration-dt', + action: editMode === ConfigurationType.GUI ? 'clicked-saved-via-gui' : 'clicked-saved-via-yaml', + }) + + if (shouldValidateLockChanges) { + const { ineligibleChanges } = getLockConfigEligibleAndIneligibleChanges({ + documents: getLockedDiffModalDocuments(false, state), + lockedConfigKeysWithLockType, + }) + + if (Object.keys(ineligibleChanges || {}).length) { + dispatch({ + type: DeploymentTemplateActionType.LOCKED_CHANGES_DETECTED_ON_SAVE, + }) + + return + } + } + + if (isProtected) { + dispatch({ + type: DeploymentTemplateActionType.SHOW_PROTECTED_SAVE_MODAL, + }) + + return + } + + await handleSaveTemplate() + } + + const handleTriggerSaveFromLockedModal = async () => { + if (isProtected) { + dispatch({ + type: DeploymentTemplateActionType.SHOW_PROTECTED_SAVE_MODAL, + }) + + return + } + + await handleSaveTemplate() + } + + /** + * If true, it is valid, else would show locked diff modal + */ + const handleValidateApprovalState = (): boolean => { + if (shouldValidateLockChanges) { + // We are going to test the draftData not the current edited data and for this the computation has already been done + // TODO: Test concurrent behavior for api validation + const { ineligibleChanges } = getLockConfigEligibleAndIneligibleChanges({ + documents: getLockedDiffModalDocuments(true, state), + lockedConfigKeysWithLockType, + }) + + if (Object.keys(ineligibleChanges || {}).length) { + dispatch({ + type: DeploymentTemplateActionType.SHOW_LOCKED_DIFF_FOR_APPROVAL, + }) + + return false + } + } + + return true + } + + const restoreLastSavedTemplate = () => { + dispatch({ + type: DeploymentTemplateActionType.RESTORE_LAST_SAVED_TEMPLATE, + }) + } + + const handleChartChange = async (selectedChart: DeploymentChartVersionType) => { + dispatch({ + type: DeploymentTemplateActionType.INITIATE_CHART_CHANGE, + }) + + try { + const selectedChartTemplateDetails = await handleFetchDeploymentTemplate(selectedChart) + dispatch({ + type: DeploymentTemplateActionType.CHART_CHANGE_SUCCESS, + payload: { + selectedChart, + selectedChartTemplateDetails, + isEnvView: !!envId, + }, + }) + } catch (error) { + showError(error) + dispatch({ + type: DeploymentTemplateActionType.CHART_CHANGE_ERROR, + }) + } + } + + const handleCloseLockedDiffModal = () => { + dispatch({ + type: DeploymentTemplateActionType.CLOSE_LOCKED_DIFF_MODAL, + }) + } + + const handleCloseSaveChangesModal = () => { + dispatch({ + type: DeploymentTemplateActionType.CLOSE_SAVE_CHANGES_MODAL, + }) + } + + const handleAppMetricsToggle = () => { + dispatch({ + type: DeploymentTemplateActionType.TOGGLE_APP_METRICS, + }) + } + + const getCurrentEditorValue = (): string => { + if (resolveScopedVariables) { + return hideLockedKeys + ? resolvedEditorTemplate.templateWithoutLockedKeys + : resolvedEditorTemplate.originalTemplateString + } + + const currentTemplateState = getCurrentEditorState({ + state, + isPublishedConfigPresent, + isDryRunView, + isDeleteOverrideDraft, + isInheritedView, + isPublishedValuesView, + showApprovalPendingEditorInCompareView, + }) + + if (!currentTemplateState) { + return '' + } + + if ((currentTemplateState as typeof currentEditorTemplateData).originalTemplateState) { + return currentTemplateState.editorTemplate + } + + return hideLockedKeys + ? (currentTemplateState as DeploymentTemplateConfigState).editorTemplateWithoutLockedKeys + : currentTemplateState.editorTemplate + } + + /** + * We need to feed uneditedDocument to render GUIView + */ + const getUneditedDocument = (): string => { + if (!isGuiSupported) { + return '{}' + } + + if (resolveScopedVariables && resolvedOriginalTemplate) { + return resolvedOriginalTemplate.originalTemplateString + } + + // Question: No need to handle other modes as we are not going to show GUIView in those cases or should we? since we have a common method? + if (currentEditorTemplateData) { + return YAMLStringify(currentEditorTemplateData.originalTemplate, { simpleKeys: true }) + } + + return '' + } + + const handleCloseDeleteOverrideDialog = () => { + dispatch({ + type: DeploymentTemplateActionType.CLOSE_OVERRIDE_DIALOG, + }) + } + + const handleDeleteOverrideProtectionError = () => { + dispatch({ + type: DeploymentTemplateActionType.DELETE_OVERRIDE_CONCURRENT_PROTECTION_ERROR, + }) + } + + const handleToggleDeleteDraftOverrideDialog = () => { + dispatch({ + type: DeploymentTemplateActionType.CLOSE_DELETE_DRAFT_OVERRIDE_DIALOG, + }) + } + + const handleOverride = () => { + if (!envId) { + logExceptionToSentry(new Error('Trying to access override without envId in DeploymentTemplate')) + return + } + + if (currentEditorTemplateData.originalTemplateState.isOverridden) { + ReactGA.event({ + category: 'devtronapp-configuration-dt', + action: 'clicked-delete-override', + }) + + dispatch({ + type: DeploymentTemplateActionType.SHOW_DELETE_OVERRIDE_DIALOG, + payload: { + isProtected, + }, + }) + return + } + + if (currentEditorTemplateData.isOverridden) { + dispatch({ + type: DeploymentTemplateActionType.DELETE_LOCAL_OVERRIDE, + }) + return + } + + dispatch({ + type: DeploymentTemplateActionType.OVERRIDE_TEMPLATE, + }) + } + + const handleCreateOverrideFromNoOverrideEmptyState = () => { + ReactGA.event({ + category: 'devtronapp-configuration-dt', + action: 'clicked-create-override-button', + }) + handleOverride() + } + + const getCompareViewPublishedTemplate = (): string => { + if (!isPublishedConfigPresent) { + return '' + } + + if (resolveScopedVariables) { + return hideLockedKeys + ? resolvedPublishedTemplate.templateWithoutLockedKeys + : resolvedPublishedTemplate.originalTemplateString + } + + return hideLockedKeys + ? publishedTemplateData.editorTemplateWithoutLockedKeys + : publishedTemplateData.editorTemplate + } + + // NOTE: Need to implement when we have support for merge patches + const getShouldShowMergePatchesButton = (): boolean => false + + const handleMergeStrategyChange: ConfigToolbarProps['handleMergeStrategyChange'] = (mergeStrategy) => { + ReactGA.event({ + category: 'devtronapp-configuration-dt', + action: 'clicked-merge-strategy-dropdown', + }) + + dispatch({ + type: DeploymentTemplateActionType.UPDATE_MERGE_STRATEGY, + payload: { + mergeStrategy, + }, + }) + } + + const handleOpenDiscardDraftPopup = () => { + dispatch({ + type: DeploymentTemplateActionType.SHOW_DISCARD_DRAFT_POPUP, + }) + } + + const handleShowEditHistory = () => { + dispatch({ + type: DeploymentTemplateActionType.SHOW_EDIT_HISTORY, + }) + } + + const handleClearPopupNode = () => { + dispatch({ + type: DeploymentTemplateActionType.CLEAR_POPUP_NODE, + }) + } + + const getIsAppMetricsEnabledForCTA = (): boolean => + !!getCurrentEditorState({ + state, + isPublishedConfigPresent, + isDryRunView, + isDeleteOverrideDraft, + isInheritedView, + isPublishedValuesView, + showApprovalPendingEditorInCompareView, + })?.isAppMetricsEnabled + + const getPromptMessage = ({ pathname }) => location.pathname === pathname || DEFAULT_ROUTE_PROMPT_MESSAGE + + const toolbarPopupConfig: ConfigToolbarProps['popupConfig'] = { + menuConfig: getConfigToolbarPopupConfig({ + lockedConfigData: { + areLockedKeysPresent: lockedConfigKeysWithLockType.config.length > 0, + hideLockedKeys, + handleSetHideLockedKeys, + }, + configHeaderTab, + isOverridden: publishedTemplateData?.isOverridden, + isPublishedValuesView, + isPublishedConfigPresent, + handleDeleteOverride: handleOverride, + unableToParseData: !!currentEditorTemplateData?.parsingError, + isLoading: isLoadingSideEffects, + isDraftAvailable, + handleDiscardDraft: handleOpenDiscardDraftPopup, + handleShowEditHistory, + showDeleteOverrideDraftEmptyState, + isProtected, + isDeleteOverrideDraftPresent: isDeleteOverrideDraft, + }), + popupNodeType, + popupMenuNode: ProtectionViewToolbarPopupNode ? ( + + ) : null, + } + + const renderEditorComponent = () => { + if (isResolvingVariables || isLoadingChangedChartDetails) { + return ( +
+ +
+ ) + } + + if (showDeleteOverrideDraftEmptyState) { + return ( + + ) + } + + if (showNoOverrideEmptyState) { + return ( + + ) + } + + if (isCompareView) { + return ( + + ) + } + + if (configHeaderTab === ConfigHeaderTabType.DRY_RUN) { + return ( + + ) + } + + if (showNoPublishedVersionEmptyState) { + return + } + + return ( + + ) + } + + const renderCTA = () => { + const selectedChart = getCurrentTemplateSelectedChart() + const shouldRenderCTA = + isEditMode || + isApprovalView || + (isDryRunView && dryRunEditorMode === DryRunEditorMode.VALUES_FROM_DRAFT && !isDeleteOverrideDraft) + + if (!selectedChart || showNoOverrideTab || showDeleteOverrideDraftEmptyState || !shouldRenderCTA) { + return null + } + + const showApplicationMetrics = + !!chartDetails?.charts?.length && + window._env_.APPLICATION_METRICS_ENABLED && + grafanaModuleStatus?.result?.status === ModuleStatus.INSTALLED && + !isCompareView + + const isAppMetricsEnabled = getIsAppMetricsEnabledForCTA() + + const isDisabled = isLoadingSideEffects || resolveScopedVariables || !!currentEditorTemplateData.parsingError + + if (isProtected && ProtectedDeploymentTemplateCTA) { + return ( + + ) + } + + if (!currentEditorTemplateData) { + return null + } + + return ( + + ) + } + + const renderInheritedViewFooter = () => { + if (!window._env_.APPLICATION_METRICS_ENABLED) { + return null + } + + return ( +
+ +
+ + Application metrics is {!baseDeploymentTemplateData?.isAppMetricsEnabled ? 'not' : ''} enabled + in + +   + +
+
+ ) + } + + const renderValuesView = () => ( +
+ {window._env_.ENABLE_SCOPED_VARIABLES && ( +
+ +
+ )} + + {renderEditorComponent()} + {isInheritedView ? renderInheritedViewFooter() : renderCTA()} +
+ ) + + const renderHeader = () => { + if (showReadMe) { + return ( +
+
+ ) + } + + return ( + <> + + + {!showNoOverrideEmptyState && ( + + {!showNoPublishedVersionEmptyState && ( + + )} + + )} + + ) + } + + const renderBody = () => ( + <> + {renderHeader()} + {renderValuesView()} + + ) + + const renderDeploymentTemplate = () => { + if (isLoadingInitialData) { + return + } + + if (initialLoadError) { + return + } + + return ( +
+ {renderBody()} + + {showDeleteOverrideDialog && ( + + )} + + {DeleteOverrideDraftModal && showDeleteDraftOverrideDialog && ( + + )} + + {DeploymentTemplateLockedDiff && showLockedTemplateDiffModal && ( + + )} + + {SaveChangesModal && showSaveChangesModal && ( + + )} +
+ ) + } + + return ( + <> +
+ {renderDeploymentTemplate()} + + {DraftComments && showDraftComments && ( + + )} +
+ + + + ) +} + +export default DeploymentTemplate diff --git a/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/DeploymentTemplate/DeploymentTemplateCTA.tsx b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/DeploymentTemplate/DeploymentTemplateCTA.tsx new file mode 100644 index 0000000000..f651c83dad --- /dev/null +++ b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/DeploymentTemplate/DeploymentTemplateCTA.tsx @@ -0,0 +1,88 @@ +import { + BaseURLParams, + Button, + ComponentSizeType, + DTApplicationMetricsFormField, + InvalidYAMLTippyWrapper, +} from '@devtron-labs/devtron-fe-common-lib' +import { useParams } from 'react-router-dom' +import { ReactComponent as ICArrowRight } from '@Icons/ic-arrow-right.svg' +import { DeploymentTemplateCTAProps } from './types' + +// For protect we will have a separate component +const DeploymentTemplateCTA = ({ + isLoading, + isDisabled, + isAppMetricsEnabled, + showApplicationMetrics, + selectedChart, + isCiPipeline, + handleSave, + toggleAppMetrics, + parsingError, + restoreLastSavedYAML, + isDryRunView, +}: DeploymentTemplateCTAProps) => { + const { envId } = useParams() + + const renderAppMetrics = () => { + if (isDryRunView) { + return ( + + ) + } + + return ( + + ) + } + + return ( +
+
+ {renderAppMetrics()} + + +
+
+
+
+
+ ) +} + +export default DeploymentTemplateCTA diff --git a/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/DeploymentTemplate/DeploymentTemplateForm.tsx b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/DeploymentTemplate/DeploymentTemplateForm.tsx new file mode 100644 index 0000000000..4e1bd7f0b8 --- /dev/null +++ b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/DeploymentTemplate/DeploymentTemplateForm.tsx @@ -0,0 +1,117 @@ +import { CodeEditor, ConfigurationType, MarkDown, MODES, noop } from '@devtron-labs/devtron-fe-common-lib' +import { ReactComponent as ICBookOpen } from '@Icons/ic-book-open.svg' +import { ReactComponent as ICPencil } from '@Icons/ic-pencil.svg' +import { DeploymentTemplateFormProps } from './types' +import DeploymentTemplateGUIView from './DeploymentTemplateGUIView' +import { DEPLOYMENT_TEMPLATE_LABELS_KEYS } from './constants' + +const DeploymentTemplateForm = ({ + editMode, + hideLockedKeys, + lockedConfigKeysWithLockType, + readOnly, + selectedChart, + guiSchema, + schema, + isUnSet, + handleChangeToYAMLMode, + editorOnChange, + editedDocument, + uneditedDocument, + showReadMe, + readMe, + environmentName, + latestDraft, + isGuiSupported, +}: DeploymentTemplateFormProps) => { + if (editMode === ConfigurationType.GUI && isGuiSupported) { + return ( + + ) + } + + const getHeadingPrefix = (): string => { + if (latestDraft) { + return 'Last saved draft' + } + + if (environmentName) { + return environmentName + } + + return DEPLOYMENT_TEMPLATE_LABELS_KEYS.baseTemplate.label + } + + const renderEditorHeader = () => { + if (showReadMe) { + return ( + +
+
+
+ {!readOnly && } + + + {getHeadingPrefix()} + {selectedChart?.version && ` (v${selectedChart.version})`} + +
+
+
+
+ ) + } + + if (isUnSet) { + return + } + + return null + } + + return ( +
+ {showReadMe && ( +
+
+ + {`Readme ${selectedChart ? `(v${selectedChart.version})` : ''}`} +
+ + +
+ )} + +
+ + {renderEditorHeader()} + +
+
+ ) +} + +export default DeploymentTemplateForm diff --git a/src/components/deploymentConfig/DeploymentTemplateView/DeploymentTemplateGUIView.tsx b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/DeploymentTemplate/DeploymentTemplateGUIView.tsx similarity index 64% rename from src/components/deploymentConfig/DeploymentTemplateView/DeploymentTemplateGUIView.tsx rename to apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/DeploymentTemplate/DeploymentTemplateGUIView.tsx index 3395bcb7d0..d40069663e 100644 --- a/src/components/deploymentConfig/DeploymentTemplateView/DeploymentTemplateGUIView.tsx +++ b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/DeploymentTemplate/DeploymentTemplateGUIView.tsx @@ -14,30 +14,40 @@ * limitations under the License. */ -import { useContext, useEffect, useMemo, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import YAML from 'yaml' import { InfoColourBar, - Progressing, RJSFForm, FormProps, GenericEmptyState, joinObjects, flatMapOfJSONPaths, HIDE_SUBMIT_BUTTON_UI_SCHEMA, + Button, + ButtonVariantType, } from '@devtron-labs/devtron-fe-common-lib' import { JSONPath } from 'jsonpath-plus' -import { DEPLOYMENT_TEMPLATE_LABELS_KEYS, GUI_VIEW_TEXTS } from '../constants' -import { DeploymentConfigContextType, DeploymentConfigStateActionTypes, DeploymentTemplateGUIViewProps } from '../types' -import { ReactComponent as Help } from '../../../assets/icons/ic-help.svg' -import { ReactComponent as WarningIcon } from '../../../assets/icons/ic-warning.svg' -import { ReactComponent as ICArrow } from '../../../assets/icons/ic-arrow-forward.svg' -import EmptyFolderImage from '../../../assets/img/Empty-folder.png' -import { DeploymentConfigContext } from '../DeploymentConfig' -import { getRenderActionButton, makeObjectFromJsonPathArray } from '../utils' +import EmptyFolderImage from '@Images/Empty-folder.png' +import { ReactComponent as Help } from '@Icons/ic-help.svg' +import { ReactComponent as ICWarningY5 } from '@Icons/ic-warning-y5.svg' +import { ReactComponent as ICArrow } from '@Icons/ic-arrow-forward.svg' +import { DeploymentTemplateGUIViewProps } from './types' +import { GUI_VIEW_TEXTS, DEPLOYMENT_TEMPLATE_LABELS_KEYS } from './constants' +import { makeObjectFromJsonPathArray } from './utils' + +export const getRenderActionButton = + ({ handleChangeToYAMLMode }: Pick) => + () => ( +
+) + +export default NoOverrideEmptyState diff --git a/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/NoPublishedVersionEmptyState.tsx b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/NoPublishedVersionEmptyState.tsx new file mode 100644 index 0000000000..64bbb60385 --- /dev/null +++ b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/NoPublishedVersionEmptyState.tsx @@ -0,0 +1,14 @@ +import { GenericEmptyState } from '@devtron-labs/devtron-fe-common-lib' +import noArtifact from '@Images/no-artifact@2x.png' + +import { NoPublishedVersionEmptyStateProps } from './types' + +const NoPublishedVersionEmptyState = ({ isOverride = true }: NoPublishedVersionEmptyStateProps) => ( + +) + +export default NoPublishedVersionEmptyState diff --git a/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/SelectMergeStrategy.tsx b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/SelectMergeStrategy.tsx new file mode 100644 index 0000000000..a4e26005be --- /dev/null +++ b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/SelectMergeStrategy.tsx @@ -0,0 +1,46 @@ +import { + InfoIconTippy, + OverrideMergeStrategyType, + OverrideStrategyTippyContent, + SelectPicker, + SelectPickerOptionType, + SelectPickerVariantType, + DOCUMENTATION_HOME_PAGE, + ComponentSizeType, +} from '@devtron-labs/devtron-fe-common-lib' +import { MERGE_STRATEGY_OPTIONS } from './constants' +import { SelectMergeStrategyProps } from './types' + +const SelectMergeStrategy = ({ mergeStrategy, handleMergeStrategyChange, isDisabled }: SelectMergeStrategyProps) => { + const handleChange = (selectedOption: SelectPickerOptionType) => { + handleMergeStrategyChange(selectedOption.value as OverrideMergeStrategyType) + } + + return ( +
+ } + documentationLink={DOCUMENTATION_HOME_PAGE} + /> + + {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} + + + option.value === mergeStrategy)} + options={MERGE_STRATEGY_OPTIONS} + isDisabled={isDisabled} + variant={SelectPickerVariantType.BORDER_LESS} + isSearchable={false} + size={ComponentSizeType.small} + /> +
+ ) +} + +export default SelectMergeStrategy diff --git a/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/ToggleResolveScopedVariables.tsx b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/ToggleResolveScopedVariables.tsx new file mode 100644 index 0000000000..a15a58ac6b --- /dev/null +++ b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/ToggleResolveScopedVariables.tsx @@ -0,0 +1,29 @@ +import { Toggle, Tooltip } from '@devtron-labs/devtron-fe-common-lib' +import { ReactComponent as ICViewVariableToggle } from '@Icons/ic-view-variable-toggle.svg' +import { ToggleResolveScopedVariablesProps } from './types' + +const ToggleResolveScopedVariables = ({ + resolveScopedVariables, + handleToggleScopedVariablesView, + isDisabled = false, + showTooltip = true, +}: ToggleResolveScopedVariablesProps) => ( + +
+ +
+
+) + +export default ToggleResolveScopedVariables diff --git a/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/constants.ts b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/constants.ts new file mode 100644 index 0000000000..43de4eadcd --- /dev/null +++ b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/constants.ts @@ -0,0 +1,11 @@ +import { OverrideMergeStrategyType, SelectPickerOptionType } from '@devtron-labs/devtron-fe-common-lib' + +export const DEFAULT_MERGE_STRATEGY: OverrideMergeStrategyType = OverrideMergeStrategyType.REPLACE + +export const MERGE_STRATEGY_OPTIONS: SelectPickerOptionType[] = [ + { + label: 'Replace', + description: 'Override complete configuration', + value: OverrideMergeStrategyType.REPLACE, + }, +] diff --git a/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/index.ts b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/index.ts new file mode 100644 index 0000000000..cd1656256e --- /dev/null +++ b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/index.ts @@ -0,0 +1,10 @@ +export * from './DeploymentTemplate' +export { default as ConfigDryRun } from './ConfigDryRun' +export { default as ConfigToolbar } from './ConfigToolbar' +export { default as NoOverrideEmptyState } from './NoOverrideEmptyState' +export { default as ConfigHeader } from './ConfigHeader' +export { default as NoPublishedVersionEmptyState } from './NoPublishedVersionEmptyState' +export { default as CompareConfigView } from './CompareConfigView' +export * from './DeploymentConfigCompare' + +export type { ConfigToolbarProps, CompareConfigViewProps } from './types' diff --git a/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/types.ts b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/types.ts new file mode 100644 index 0000000000..672e58da83 --- /dev/null +++ b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/types.ts @@ -0,0 +1,298 @@ +import { + CompareFromApprovalOptionsValuesType, + ConfigHeaderTabType, + ConfigToolbarPopupMenuConfigType, + ConfigToolbarPopupNodeType, + DeploymentHistorySingleValue, + DeploymentTemplateConfigState, + OverrideMergeStrategyType, + ProtectConfigTabsType, + SelectPickerOptionType, +} from '@devtron-labs/devtron-fe-common-lib' +import { CMSecretComponentType } from '@Pages/Shared/ConfigMapSecret/types' +import { FunctionComponent, MutableRefObject, ReactNode } from 'react' + +export interface ConfigHeaderProps { + configHeaderTab: ConfigHeaderTabType + handleTabChange: (tab: ConfigHeaderTabType) => void + areChangesPresent: boolean + isDisabled: boolean + /** + * If false, can not override config and will show values tab as Configuration + */ + isOverridable: boolean + /** + * This means that we are showing no override empty state, i.e, currently we are inheriting base config + * and our editable state is also not overriding the base config. + */ + showNoOverride: boolean + parsingError: string + restoreLastSavedYAML: () => void + /** + * This prop hides the dry run tab in the header + * This prop is meant to be removed after patch merge strategy is introduced + * @default - false + */ + hideDryRunTab?: boolean +} + +export interface ConfigHeaderTabProps + extends Pick< + ConfigHeaderProps, + 'handleTabChange' | 'isDisabled' | 'areChangesPresent' | 'isOverridable' | 'showNoOverride' + > { + tab: ConfigHeaderTabType + activeTabIndex: number + currentTabIndex: number + hasError: boolean +} + +export interface ConfigHeaderTabConfigType { + text: string + icon?: FunctionComponent> | null +} + +interface ConfigToolbarPopupConfigType { + menuConfig: Record + /** + * If true, would show popupMenuNode in body of the popup menu + */ + popupNodeType?: ConfigToolbarPopupNodeType + /** + * If provided, will replace the popup menu view with the provided node given popupMenuConfig is not empty + */ + popupMenuNode?: ReactNode +} + +type ConfigToolbarReadMeProps = + | { + showEnableReadMeButton: boolean + handleEnableReadmeView: () => void + } + | { + showEnableReadMeButton?: never + handleEnableReadmeView?: never + } + +export type ConfigToolbarProps = { + configHeaderTab: ConfigHeaderTabType + handleToggleScopedVariablesView: () => void + resolveScopedVariables: boolean + /** + * Route for redirection to base configurations shown in case configHeaderTab is inherited + */ + baseConfigurationURL: string + /** + * Will feed the selected tab in the protection tab group + */ + selectedProtectionViewTab: ProtectConfigTabsType + handleProtectionViewTabChange: (tab: ProtectConfigTabsType) => void + + handleToggleCommentsView: () => void + /** + * Would show red dot on comments icon if comments are present + */ + areCommentsPresent: boolean + + showMergePatchesButton: boolean + shouldMergeTemplateWithPatches: boolean + handleToggleShowTemplateMergedWithPatch: () => void + + mergeStrategy: OverrideMergeStrategyType + handleMergeStrategyChange: (strategy: OverrideMergeStrategyType) => void + + /** + * Used to place toggle editor view and chart selectors in deployment template + */ + children?: ReactNode + /** + * If provided, will show popup menu on click three dots button + * If empty/null, will not show the button + */ + popupConfig?: ConfigToolbarPopupConfigType + /** + * @default false + */ + isProtected?: boolean + /** + * @default false + */ + isApprovalPending?: boolean + isDraftPresent?: boolean + approvalUsers: string[] + /** + * @default - false + * If given would disable all the actions + */ + disableAllActions?: boolean + parsingError: string + restoreLastSavedYAML: () => void + /** + * This key means if have saved a draft and have not proposed it yet, and we are creating a new entity like override. + * @default - true + * If false we will hide all the action in toolbar. + */ + isPublishedConfigPresent?: boolean + headerMessage?: string + showDeleteOverrideDraftEmptyState: boolean +} & ConfigToolbarReadMeProps + +interface ConfigToolbarPopupMenuLockedConfigDataType { + /** + * If false would not show hide/show locked keys button + */ + areLockedKeysPresent: boolean + hideLockedKeys: boolean + handleSetHideLockedKeys: (value: boolean) => void +} + +export interface GetConfigToolbarPopupConfigProps { + /** + * If not provided won't show locked config data + */ + lockedConfigData?: ConfigToolbarPopupMenuLockedConfigDataType | null + /** + * @default false + */ + showDeleteOverrideDraftEmptyState?: boolean + configHeaderTab: ConfigHeaderTabType + isOverridden: boolean + isPublishedValuesView: boolean + isPublishedConfigPresent: boolean + handleDeleteOverride: () => void + handleDelete?: () => void + handleDiscardDraft: () => void + unableToParseData: boolean + isLoading: boolean + isDraftAvailable: boolean + handleShowEditHistory: () => void + isProtected?: boolean + isDeletable?: boolean + isDeleteOverrideDraftPresent?: boolean +} + +type ConfigDryRunManifestProps = + | { + showManifest: true + manifestAbortController: MutableRefObject + } + | { + showManifest?: never + manifestAbortController?: never + } + +export type ConfigDryRunProps = { + isLoading: boolean + handleToggleResolveScopedVariables: () => void + resolveScopedVariables: boolean + showManifest: boolean + chartRefId?: number + editorTemplate: string + editorSchema?: DeploymentTemplateConfigState['schema'] + selectedChartVersion?: string + dryRunEditorMode: string + handleChangeDryRunEditorMode: (mode: string) => void + isDraftPresent: boolean + isApprovalPending: boolean + isPublishedConfigPresent: boolean +} & ConfigDryRunManifestProps + +export interface ToggleResolveScopedVariablesProps { + resolveScopedVariables: boolean + handleToggleScopedVariablesView: () => void + isDisabled?: boolean + /** + * @default true + */ + showTooltip?: boolean +} + +export enum DeploymentTemplateComponentType { + DEPLOYMENT_TEMPLATE = '3', +} + +type NoOverrideEmptyStateCMCSProps = { + componentType: CMSecretComponentType + configName: string +} + +type NoOverrideEmptyStateDeploymentTemplateProps = { + componentType: DeploymentTemplateComponentType + configName?: never +} + +export type NoOverrideEmptyStateProps = { + environmentName: string + handleCreateOverride: () => void + handleViewInheritedConfig: () => void + hideOverrideButton?: boolean +} & (NoOverrideEmptyStateCMCSProps | NoOverrideEmptyStateDeploymentTemplateProps) + +type CMSecretDiffViewConfigType = { + configuration?: DeploymentHistorySingleValue + dataType: DeploymentHistorySingleValue + mountDataAs: DeploymentHistorySingleValue + volumeMountPath: DeploymentHistorySingleValue + setSubPath: DeploymentHistorySingleValue + externalSubpathValues: DeploymentHistorySingleValue + filePermission: DeploymentHistorySingleValue + roleARN: DeploymentHistorySingleValue +} + +type DeploymentTemplateDiffViewConfigType = + | { + applicationMetrics?: DeploymentHistorySingleValue + chartName: DeploymentHistorySingleValue + chartVersion: DeploymentHistorySingleValue + mergeStrategy?: DeploymentHistorySingleValue + isOverride?: DeploymentHistorySingleValue + dataType?: never + mountDataAs?: never + volumeMountPath?: never + setSubPath?: never + externalSubpathValues?: never + filePermission?: never + roleARN?: never + } + | { + applicationMetrics?: never + chartName?: never + chartVersion?: never + mergeStrategy?: never + isOverride?: never + } + +export type CompareConfigViewEditorConfigType = DeploymentTemplateDiffViewConfigType | CMSecretDiffViewConfigType + +export interface CompareConfigViewProps { + compareFromSelectedOptionValue: CompareFromApprovalOptionsValuesType + handleCompareFromOptionSelection: (value: SelectPickerOptionType) => void + isApprovalView: boolean + isDeleteOverrideView: boolean + + currentEditorTemplate: Record + publishedEditorTemplate: Record + currentEditorConfig: CompareConfigViewEditorConfigType + publishedEditorConfig: CompareConfigViewEditorConfigType + draftChartVersion?: string + selectedChartVersion?: string + /** + * @default ${compareFromSelectedOptionValue}-"draft-editor-key" + */ + editorKey?: string + className?: string +} + +export interface BaseConfigurationNavigationProps { + baseConfigurationURL: string +} + +export interface NoPublishedVersionEmptyStateProps { + isOverride?: boolean +} + +export interface SelectMergeStrategyProps { + mergeStrategy: OverrideMergeStrategyType + handleMergeStrategyChange: (value: OverrideMergeStrategyType) => void + isDisabled: boolean +} diff --git a/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/utils.tsx b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/utils.tsx new file mode 100644 index 0000000000..a24144457a --- /dev/null +++ b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/utils.tsx @@ -0,0 +1,194 @@ +import { + ConfigHeaderTabType, + ConfigToolbarPopupMenuConfigType, + DeploymentTemplateHistoryType, + Tooltip, +} from '@devtron-labs/devtron-fe-common-lib' +import { ReactComponent as ICFilePlay } from '@Icons/ic-file-play.svg' +import { ReactComponent as ICFileCode } from '@Icons/ic-file-code.svg' +import { ReactComponent as ICArrowSquareIn } from '@Icons/ic-arrow-square-in.svg' +import { ReactComponent as ICDeleteInteractive } from '@Icons/ic-delete-interactive.svg' +import { importComponentFromFELibrary } from '@Components/common' +import { + CompareConfigViewEditorConfigType, + ConfigHeaderTabConfigType, + ConfigToolbarProps, + GetConfigToolbarPopupConfigProps, +} from './types' + +const getToggleViewLockedKeysPopupButtonConfig = importComponentFromFELibrary( + 'getToggleViewLockedKeysPopupButtonConfig', + null, + 'function', +) + +const getDeleteDraftPopupButtonConfig = importComponentFromFELibrary( + 'getDeleteDraftPopupButtonConfig', + null, + 'function', +) + +const getEditHistoryPopupButtonConfig = importComponentFromFELibrary( + 'getEditHistoryPopupButtonConfig', + null, + 'function', +) + +const getValuesViewTabText = ( + isOverridable: Parameters[1], + showNoOverride: Parameters[2], +) => { + if (!isOverridable) { + return 'Configuration' + } + if (showNoOverride) { + return 'No override' + } + return 'Override' +} + +export const getConfigHeaderTabConfig = ( + tab: ConfigHeaderTabType, + isOverridable: boolean, + showNoOverride: boolean, +): ConfigHeaderTabConfigType => { + switch (tab) { + case ConfigHeaderTabType.DRY_RUN: + return { + text: 'Dry run', + icon: ICFilePlay, + } + + case ConfigHeaderTabType.VALUES: + return { + text: getValuesViewTabText(isOverridable, showNoOverride), + icon: ICFileCode, + } + + case ConfigHeaderTabType.INHERITED: + return { + text: 'Inherited', + icon: ICArrowSquareIn, + } + default: + return { + text: tab, + } + } +} + +export const PopupMenuItem = ({ + text, + onClick, + dataTestId, + disabled, + icon, + variant, +}: ConfigToolbarPopupMenuConfigType) => ( + +) + +export const getConfigToolbarPopupConfig = ({ + lockedConfigData, + showDeleteOverrideDraftEmptyState = false, + configHeaderTab, + isOverridden, + isPublishedValuesView, + isPublishedConfigPresent, + handleDeleteOverride, + handleDelete, + handleDiscardDraft, + unableToParseData, + isLoading, + isDraftAvailable, + handleShowEditHistory, + isProtected = false, + isDeletable = false, + isDeleteOverrideDraftPresent = false, +}: GetConfigToolbarPopupConfigProps): ConfigToolbarProps['popupConfig']['menuConfig'] => { + if (isPublishedValuesView && !isPublishedConfigPresent) { + return null + } + + const firstConfigSegment: ConfigToolbarPopupMenuConfigType[] = [] + const secondConfigSegment: ConfigToolbarPopupMenuConfigType[] = [] + + if (getToggleViewLockedKeysPopupButtonConfig && lockedConfigData && !showDeleteOverrideDraftEmptyState) { + const lockedKeysConfig = getToggleViewLockedKeysPopupButtonConfig( + lockedConfigData.areLockedKeysPresent, + lockedConfigData.hideLockedKeys, + unableToParseData || isLoading, + lockedConfigData.handleSetHideLockedKeys, + ) + + if (lockedKeysConfig) { + firstConfigSegment.push(lockedKeysConfig) + } + } + + if (getEditHistoryPopupButtonConfig && isDraftAvailable && configHeaderTab === ConfigHeaderTabType.VALUES) { + const activityHistoryConfig = getEditHistoryPopupButtonConfig(handleShowEditHistory, isLoading) + if (activityHistoryConfig) { + firstConfigSegment.push(activityHistoryConfig) + } + } + + if (getDeleteDraftPopupButtonConfig && isDraftAvailable && configHeaderTab === ConfigHeaderTabType.VALUES) { + const deleteDraftConfig = getDeleteDraftPopupButtonConfig(handleDiscardDraft, isLoading) + if (deleteDraftConfig) { + secondConfigSegment.push(deleteDraftConfig) + } + } + + if (isOverridden && configHeaderTab === ConfigHeaderTabType.VALUES && !isDeleteOverrideDraftPresent) { + secondConfigSegment.push({ + text: 'Delete override', + onClick: handleDeleteOverride, + dataTestId: 'delete-override', + disabled: isLoading, + icon: , + variant: 'negative', + }) + } + + if (isDeletable && configHeaderTab === ConfigHeaderTabType.VALUES) { + secondConfigSegment.push({ + text: `Delete${isProtected ? '...' : ''}`, + onClick: handleDelete, + dataTestId: 'delete-config-map-secret', + disabled: isLoading, + icon: , + variant: 'negative', + }) + } + + return { + ...(firstConfigSegment.length && { firstConfigSegment }), + ...(secondConfigSegment.length && { secondConfigSegment }), + } +} + +export const getCompareViewHistoryDiffConfigProps = ( + showDisplayName: boolean, + editorTemplate: Record, + editorConfig: CompareConfigViewEditorConfigType, +): + | DeploymentTemplateHistoryType['baseTemplateConfiguration'] + | DeploymentTemplateHistoryType['currentConfiguration'] => ({ + codeEditorValue: { + displayName: showDisplayName ? 'Data' : '', + ...(editorTemplate && { value: JSON.stringify(editorTemplate) }), + }, + values: editorConfig, +}) diff --git a/src/Pages/Applications/DevtronApps/Details/AppConfigurations/Navigation/AppConfigurationCheckBox.tsx b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/Navigation/AppConfigurationCheckBox.tsx similarity index 100% rename from src/Pages/Applications/DevtronApps/Details/AppConfigurations/Navigation/AppConfigurationCheckBox.tsx rename to apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/Navigation/AppConfigurationCheckBox.tsx diff --git a/src/Pages/Applications/DevtronApps/Details/AppConfigurations/Navigation/AppNavigation.tsx b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/Navigation/AppNavigation.tsx similarity index 88% rename from src/Pages/Applications/DevtronApps/Details/AppConfigurations/Navigation/AppNavigation.tsx rename to apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/Navigation/AppNavigation.tsx index 84b3224cc6..97ca0a6609 100644 --- a/src/Pages/Applications/DevtronApps/Details/AppConfigurations/Navigation/AppNavigation.tsx +++ b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/Navigation/AppNavigation.tsx @@ -17,7 +17,16 @@ import { ReactNode } from 'react' import { Route, Switch, useLocation, useRouteMatch } from 'react-router-dom' -import { ConditionalWrap, EnvResourceType, TippyCustomized, TippyTheme } from '@devtron-labs/devtron-fe-common-lib' +import { + Button, + ButtonStyleType, + ButtonVariantType, + ComponentSizeType, + ConditionalWrap, + EnvResourceType, + TippyCustomized, + TippyTheme, +} from '@devtron-labs/devtron-fe-common-lib' import { DEVTRON_APPS_STEPS, STAGE_NAME } from '../AppConfig.types' import { URLS } from '../../../../../../config' import AppConfigurationCheckBox from './AppConfigurationCheckBox' @@ -118,7 +127,10 @@ export const AppNavigation = () => { return ( {({ match }) => ( { showBaseConfigurations showDeploymentTemplate={!isJobView} goBackURL={getValidBackURL()} + compareWithURL={`${path}/:envId(\\d+)?`} showComparison={!isJobView && isUnlocked.workflowEditor} isCMSecretLocked={!isUnlocked.workflowEditor} /> @@ -194,15 +207,16 @@ export const AppNavigation = () => { return })} {isJobView &&
} -
- + text={`Delete ${isJobView ? 'Job' : 'Application'}`} + fullWidth + />
diff --git a/src/Pages/Applications/DevtronApps/Details/AppConfigurations/Navigation/EnvConfigurationsNav.tsx b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/Navigation/EnvConfigurationsNav.tsx similarity index 53% rename from src/Pages/Applications/DevtronApps/Details/AppConfigurations/Navigation/EnvConfigurationsNav.tsx rename to apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/Navigation/EnvConfigurationsNav.tsx index 4e70f8debe..8692f0aa58 100644 --- a/src/Pages/Applications/DevtronApps/Details/AppConfigurations/Navigation/EnvConfigurationsNav.tsx +++ b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/Navigation/EnvConfigurationsNav.tsx @@ -1,21 +1,29 @@ -import { useEffect, useState } from 'react' -import { useRouteMatch, useLocation, NavLink, useHistory } from 'react-router-dom' +import { MouseEvent, useEffect, useState } from 'react' +import { useRouteMatch, useLocation, NavLink, useHistory, generatePath } from 'react-router-dom' import Tippy from '@tippyjs/react' +import { GroupBase, OptionsOrGroups } from 'react-select' import { + Button, + ButtonComponentType, + ButtonStyleType, + ButtonVariantType, CollapsibleList, CollapsibleListConfig, + ComponentSizeType, EnvResourceType, + getSelectPickerOptionByValue, SelectPicker, SelectPickerOptionType, SelectPickerVariantType, } from '@devtron-labs/devtron-fe-common-lib' import { ReactComponent as ICBack } from '@Icons/ic-caret-left-small.svg' +import { ReactComponent as ICArrowsLeftRight } from '@Icons/ic-arrows-left-right.svg' import { ReactComponent as ICAdd } from '@Icons/ic-add.svg' import { ReactComponent as ICLocked } from '@Icons/ic-locked.svg' +import { ReactComponent as ICFileCode } from '@Icons/ic-file-code.svg' import { URLS } from '@Config/routes' -import { importComponentFromFELibrary } from '@Components/common' import { ReactComponent as ProtectedIcon } from '@Icons/ic-shield-protect-fill.svg' import { ResourceConfigState } from '@Pages/Applications/DevtronApps/service.types' @@ -23,8 +31,6 @@ import { BASE_CONFIGURATIONS } from '../AppConfig.constants' import { EnvConfigRouteParams, EnvConfigurationsNavProps, EnvConfigObjectKey } from '../AppConfig.types' import { getEnvConfiguration, getNavigationPath, resourceTypeBasedOnPath } from './Navigation.helper' -const CompareWithButton = importComponentFromFELibrary('CompareWithButton', null, 'function') - // LOADING SHIMMER const ShimmerText = ({ width }: { width: string }) => (
@@ -43,16 +49,19 @@ export const EnvConfigurationsNav = ({ paramToCheck = 'envId', showComparison, isCMSecretLocked, + hideEnvSelector, + compareWithURL, }: EnvConfigurationsNavProps) => { // HOOKS const history = useHistory() const { pathname } = useLocation() const { path, params } = useRouteMatch() - const { envId, appId } = params + const { envId } = params // STATES const [expandedIds, setExpandedIds] = useState, boolean>>() + const [updatedEnvConfig, setUpdatedEnvConfig] = useState>({ deploymentTemplate: null, configmaps: [], @@ -62,26 +71,15 @@ export const EnvConfigurationsNav = ({ // CONSTANTS const { isLoading, config } = envConfig /** Current Environment Data. */ - const _environmentData = - environments && environments.find((environment) => environment.id === +params[paramToCheck]) - const selectedNavEnvironmentOptions = { - ..._environmentData, - label: _environmentData?.name || BASE_CONFIGURATIONS.name, - value: _environmentData?.id || BASE_CONFIGURATIONS.id, - isProtected: _environmentData?.isProtected, - } - - const selectedEnvironmentWithBaseConfiguration = showBaseConfigurations - ? { - label: BASE_CONFIGURATIONS.name, - value: BASE_CONFIGURATIONS.id, - endIcon: isBaseConfigProtected && , - isProtected: isBaseConfigProtected, - } - : null - - const environmentData = selectedNavEnvironmentOptions || selectedEnvironmentWithBaseConfiguration - + const environmentData = + environments.find((environment) => environment.id === +params[paramToCheck]) || + (showBaseConfigurations + ? { + name: BASE_CONFIGURATIONS.name, + id: BASE_CONFIGURATIONS.id, + isProtected: isBaseConfigProtected, + } + : null) const resourceType = resourceTypeBasedOnPath(pathname) const isCreate = pathname.includes('/create') @@ -89,13 +87,15 @@ export const EnvConfigurationsNav = ({ const envConfigKey = resourceType === EnvResourceType.ConfigMap ? EnvConfigObjectKey.ConfigMap : EnvConfigObjectKey.Secret + setExpandedIds({ ...expandedIds, [resourceType]: true }) + return { ..._updatedEnvConfig, [envConfigKey]: [ ..._updatedEnvConfig[envConfigKey], { title: 'Unnamed', - href: getNavigationPath(path, params, environmentData.value, resourceType, 'create', paramToCheck), + href: getNavigationPath(path, params, resourceType, 'create'), configState: ResourceConfigState.Unnamed, subtitle: '', }, @@ -103,6 +103,14 @@ export const EnvConfigurationsNav = ({ } } + useEffect(() => { + if (environmentData.id === BASE_CONFIGURATIONS.id && envId) { + // Removing `/env-override/:envId` from pathname, resulting path will be base configuration path. + const [basePath, resourcePath] = pathname.split(`/${URLS.APP_ENV_OVERRIDE_CONFIG}/${envId}`) + history.push(`${basePath}${resourcePath}`) + } + }, [environmentData, envId]) + useEffect(() => { // Fetch the env configuration fetchEnvConfig(+(envId || BASE_CONFIGURATIONS.id)) @@ -114,7 +122,7 @@ export const EnvConfigurationsNav = ({ useEffect(() => { if (!isLoading && config) { - const newEnvConfig = getEnvConfiguration(config, path, params, environmentData, paramToCheck) + const newEnvConfig = getEnvConfiguration(config, path, params, environmentData.isProtected) setUpdatedEnvConfig(isCreate ? addUnnamedNavLink(newEnvConfig) : newEnvConfig) } }, [isLoading, config, pathname]) @@ -170,11 +178,11 @@ export const EnvConfigurationsNav = ({ return } setExpandedIds({ ...expandedIds, [_resourceType]: true }) - history.push(getNavigationPath(path, params, environmentData.value, _resourceType, 'create', paramToCheck)) + history.push(getNavigationPath(path, params, _resourceType, 'create')) } /** Collapsible List Config. */ - const collapsibleListConfig: CollapsibleListConfig[] = [ + const collapsibleListConfig: CollapsibleListConfig<'navLink'>[] = [ { header: 'ConfigMaps', id: EnvResourceType.ConfigMap, @@ -221,40 +229,71 @@ export const EnvConfigurationsNav = ({ } : {}), }, - items: updatedEnvConfig.secrets, + items: updatedEnvConfig.secrets.map((secret) => { + const { title, subtitle, href, iconConfig } = secret + return { title, subtitle, href, iconConfig } + }), noItemsText: 'No secrets', isExpanded: expandedIds?.secrets, }, ] // REACT SELECT PROPS - const getSelectOptions = () => - [...environments].map((env) => ({ - label: env.name, - value: env.id, - endIcon: env.isProtected ? : null, - })) - - const envOptions: SelectPickerOptionType[] = [ - ...(showBaseConfigurations - ? [ - { - label: BASE_CONFIGURATIONS.name, - value: BASE_CONFIGURATIONS.id, - endIcon: isBaseConfigProtected && , - }, - ] - : []), - ...getSelectOptions(), + const baseEnvOption = showBaseConfigurations + ? [ + { + label: BASE_CONFIGURATIONS.name, + value: BASE_CONFIGURATIONS.id, + endIcon: isBaseConfigProtected ? : null, + }, + ] + : [] + + const envOptions: OptionsOrGroups, GroupBase>> = [ + ...baseEnvOption, + { + label: paramToCheck === 'envId' ? 'Environments' : 'Applications', + options: environments.map(({ name, id, isProtected }) => ({ + label: name, + value: id, + endIcon: isProtected ? : null, + })), + }, ] - const onEnvSelect = (_selectedEnv) => { - if (environmentData.value === _selectedEnv.value) { + const onEnvSelect = ({ value }: SelectPickerOptionType) => { + // Exit early if the selected environment is the current one + if (environmentData.id === value) { return } - const name = pathname.split(`${resourceType}/`)[1] - history.push(getNavigationPath(path, params, _selectedEnv.value, resourceType, name, paramToCheck)) + // Extract the resource name from the current pathname based on resourceType + const resourceName = pathname.split(`${resourceType}/`)[1] + + // Truncate the path to the base application configuration path + const truncatedPath = `${path.split(URLS.APP_CONFIG)[0]}${URLS.APP_CONFIG}` + + // Build the new app path, conditionally adding the environment override config when switching to environment + const appPath = `${truncatedPath}${ + value !== BASE_CONFIGURATIONS.id ? `/${URLS.APP_ENV_OVERRIDE_CONFIG}/:envId(\\d+)?` : '' + }/:resourceType(${Object.values(EnvResourceType).join('|')})` // Dynamically set valid resource types + + // Generate the final path + // if application/job (paramToCheck = envId), use `appPath` + // otherwise applicationGroups (paramToCheck = 'appId'), use `path` + const generatedPath = `${generatePath(paramToCheck === 'envId' ? appPath : path, { + ...params, + [paramToCheck]: value, + })}${resourceName ? `/${resourceName}` : ''}` + + // Navigate to the generated path + history.push(generatedPath) + } + + const handleDeploymentTemplateNavLinkOnClick = (e: MouseEvent) => { + if (pathname === updatedEnvConfig.deploymentTemplate.href) { + e.preventDefault() + } } const renderEnvSelector = () => ( @@ -264,52 +303,61 @@ export const EnvConfigurationsNav = ({
- `${option.label}`} - getOptionValue={(option) => `${option.value}`} - onChange={onEnvSelect} - placeholder="Select Environment" - variant={SelectPickerVariantType.BORDER_LESS} - /> +
+ + inputId="env-config-selector" + classNamePrefix="env-config-selector" + variant={SelectPickerVariantType.BORDER_LESS} + isClearable={false} + value={getSelectPickerOptionByValue(envOptions, +params[paramToCheck], baseEnvOption[0])} + options={envOptions} + onChange={onEnvSelect} + placeholder="Select Environment" + showSelectedOptionIcon={false} + /> +
+ {environmentData?.isProtected && }
) const renderCompareWithBtn = () => { - const { label: compareTo } = environmentData - - // Determine base path based on pathname - const isOverrideConfig = pathname.includes(URLS.APP_ENV_OVERRIDE_CONFIG) - const basePath = isOverrideConfig - ? pathname.split(URLS.APP_ENV_OVERRIDE_CONFIG)[0] - : `${pathname.split(URLS.APP_CONFIG)[0]}${URLS.APP_CONFIG}` - - // Determine comparePath based on paramToCheck - let comparePath = '' - if (paramToCheck === 'envId') { - comparePath = isOverrideConfig - ? `${basePath}${envId}/${URLS.APP_ENV_CONFIG_COMPARE}/${compareTo}${pathname.split(`${URLS.APP_ENV_OVERRIDE_CONFIG}/${envId}`)[1]}` - : `${basePath}/${URLS.APP_ENV_CONFIG_COMPARE}${pathname.split(URLS.APP_CONFIG)[1]}` - } else if (paramToCheck === 'appId') { - comparePath = `${basePath}/${appId}/${URLS.APP_ENV_CONFIG_COMPARE}/${compareTo}${pathname.split(`${URLS.APP_CONFIG}/${appId}`)[1]}` - } + const { name: compareTo } = environmentData + + // Extract the resource name from the current pathname based on resourceType + const resourceName = pathname.split(`/${resourceType}/`)[1] + + // Construct the compare view path with dynamic route parameters for comparison + const compareViewPath = `${compareWithURL}/${URLS.APP_ENV_CONFIG_COMPARE}/:compareTo?/:resourceType(${Object.values(EnvResourceType).join('|')})/:resourceName?` + + const compareWithHref = generatePath(compareViewPath, { + ...params, + // Only set compareTo if it's not the base configuration + compareTo: compareTo !== BASE_CONFIGURATIONS.name ? compareTo : null, + resourceType, + resourceName: resourceName ?? null, + }) return (
- +
) } return ( - <> - {renderEnvSelector()} - {showComparison && CompareWithButton && renderCompareWithBtn()} -
+ ) } diff --git a/src/Pages/Applications/DevtronApps/Details/AppConfigurations/Navigation/EnvironmentOverrideRouter.tsx b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/Navigation/EnvironmentOverrideRouter.tsx similarity index 100% rename from src/Pages/Applications/DevtronApps/Details/AppConfigurations/Navigation/EnvironmentOverrideRouter.tsx rename to apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/Navigation/EnvironmentOverrideRouter.tsx diff --git a/src/Pages/Applications/DevtronApps/Details/AppConfigurations/Navigation/Navigation.helper.tsx b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/Navigation/Navigation.helper.tsx similarity index 77% rename from src/Pages/Applications/DevtronApps/Details/AppConfigurations/Navigation/Navigation.helper.tsx rename to apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/Navigation/Navigation.helper.tsx index 10cf1d3bcc..9134abd00f 100644 --- a/src/Pages/Applications/DevtronApps/Details/AppConfigurations/Navigation/Navigation.helper.tsx +++ b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/Navigation/Navigation.helper.tsx @@ -6,7 +6,6 @@ import { ReactComponent as Lock } from '@Icons/ic-locked.svg' import { ReactComponent as ProtectedIcon } from '@Icons/ic-shield-protect-fill.svg' import { ReactComponent as ICStamp } from '@Icons/ic-stamp.svg' import { ReactComponent as ICEditFile } from '@Icons/ic-edit-file.svg' -import { URLS } from '@Config/routes' import { ResourceConfigStage, ResourceConfigState } from '@Pages/Applications/DevtronApps/service.types' import { @@ -16,7 +15,6 @@ import { ExtendedCollapsibleListItem, EnvConfigObjectKey, } from '../AppConfig.types' -import { BASE_CONFIGURATIONS } from '../AppConfig.constants' const renderNavItemIcon = (isLocked: boolean, isProtected: boolean, dataTestId: string) => { if (isLocked) { @@ -56,28 +54,14 @@ export const renderNavItem = (item: CustomNavItemsType, isBaseConfigProtected?: * @param params - URL parameters * @param resourceType - The type of resource. * @param href - An optional href to append to the path. - * @param paramToCheck - The parameter to check in the URL. * @returns The generated URL path. */ export const getNavigationPath = ( basePath: string, params: EnvConfigRouteParams, - id: number, resourceType: EnvResourceType, href?: string, - paramToCheck: 'appId' | 'envId' = 'envId', -) => { - const additionalPath = href ? `/${href}` : '' - const isEnvIdChanged = paramToCheck === 'envId' - const isBaseEnv = id === BASE_CONFIGURATIONS.id - const _resourceType = isEnvIdChanged && !isBaseEnv ? URLS.APP_ENV_OVERRIDE_CONFIG : resourceType - - return `${generatePath(basePath, { - ...params, - resourceType: _resourceType, - [paramToCheck]: !isBaseEnv ? id : null, - })}${isEnvIdChanged && !isBaseEnv ? `/${resourceType}${additionalPath}` : `${additionalPath}`}` -} +) => `${generatePath(basePath, { ...params, resourceType })}${href ? `/${href}` : ''}` /** * Returns an object containing the appropriate icon, icon properties and tooltip properties based on the resource configuration state. @@ -85,7 +69,10 @@ export const getNavigationPath = ( * @param configState - The state of the resource configuration. * @returns An object containing the icon, props and tooltipProps if conditions are met, otherwise null. */ -const getIcon = (configState: ResourceConfigState, isProtected: boolean): CollapsibleListItem['iconConfig'] => { +const getIcon = ( + configState: ResourceConfigState, + isProtected: boolean, +): CollapsibleListItem<'navLink'>['iconConfig'] => { if (isProtected && configState !== ResourceConfigState.Published && configState !== ResourceConfigState.Unnamed) { return { Icon: configState === ResourceConfigState.ApprovalPending ? ICStamp : ICEditFile, @@ -93,9 +80,10 @@ const getIcon = (configState: ResourceConfigState, isProtected: boolean): Collap content: configState === ResourceConfigState.ApprovalPending ? 'Approval pending' : 'Draft', placement: 'right', arrow: false, + className: 'default-tt', }, props: { - className: configState === ResourceConfigState.Draft ? 'scn-6' : '', + className: `p-2 ${configState === ResourceConfigState.Draft ? 'scn-6' : ''}`, }, } } @@ -114,8 +102,7 @@ export const getEnvConfiguration = ( envConfig: EnvConfigType, basePath: string, params: EnvConfigRouteParams, - { value: id, isProtected }, - paramToCheck: 'appId' | 'envId' = 'envId', + isProtected: boolean, ): { deploymentTemplate: ExtendedCollapsibleListItem configmaps: ExtendedCollapsibleListItem[] @@ -130,14 +117,7 @@ export const getEnvConfiguration = ( configState: envConfig[curr].configState, title: 'Deployment Template', subtitle: SUBTITLE[envConfig[curr].configStage], - href: getNavigationPath( - basePath, - params, - id, - EnvResourceType.DeploymentTemplate, - '', - paramToCheck, - ), + href: getNavigationPath(basePath, params, EnvResourceType.DeploymentTemplate), iconConfig: getIcon(envConfig[curr].configState, isProtected), } : envConfig[curr].map(({ configState, name, configStage }) => ({ @@ -146,12 +126,10 @@ export const getEnvConfiguration = ( href: getNavigationPath( basePath, params, - id, curr === EnvConfigObjectKey.ConfigMap ? EnvResourceType.ConfigMap : EnvResourceType.Secret, name, - paramToCheck, ), iconConfig: getIcon(configState, isProtected), subtitle: SUBTITLE[configStage], @@ -171,9 +149,5 @@ export const resourceTypeBasedOnPath = (pathname: string) => { if (pathname.includes(`/${EnvResourceType.Secret}`)) { return EnvResourceType.Secret } - if (pathname.includes(`/${EnvResourceType.DeploymentTemplate}`)) { - return EnvResourceType.DeploymentTemplate - } - - return null + return EnvResourceType.DeploymentTemplate } diff --git a/src/Pages/Applications/DevtronApps/Details/AppConfigurations/appConfig.scss b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/appConfig.scss similarity index 98% rename from src/Pages/Applications/DevtronApps/Details/AppConfigurations/appConfig.scss rename to apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/appConfig.scss index 28c0b59ee3..ace4b7516c 100644 --- a/src/Pages/Applications/DevtronApps/Details/AppConfigurations/appConfig.scss +++ b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/appConfig.scss @@ -30,7 +30,7 @@ } .app-compose__nav { - padding: 16px 12px 0; + padding: 16px 12px; border-right: solid 1px #d6dbdf; background: white; height: 100%; @@ -191,12 +191,6 @@ } } - .shebang { - padding: 0 60px; - color: #151515; - opacity: 0.6; - } - .yaml-container { border: 1px solid var(--N200); border-radius: 4px; diff --git a/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/index.ts b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/index.ts new file mode 100644 index 0000000000..7c0259c50d --- /dev/null +++ b/apps/web/src/Pages/Applications/DevtronApps/Details/AppConfigurations/index.ts @@ -0,0 +1 @@ +export * from './MainContent' diff --git a/apps/web/src/Pages/Applications/DevtronApps/Details/index.ts b/apps/web/src/Pages/Applications/DevtronApps/Details/index.ts new file mode 100644 index 0000000000..922ce5973d --- /dev/null +++ b/apps/web/src/Pages/Applications/DevtronApps/Details/index.ts @@ -0,0 +1 @@ +export * from './AppConfigurations' diff --git a/apps/web/src/Pages/Applications/DevtronApps/index.ts b/apps/web/src/Pages/Applications/DevtronApps/index.ts new file mode 100644 index 0000000000..fdbadef09a --- /dev/null +++ b/apps/web/src/Pages/Applications/DevtronApps/index.ts @@ -0,0 +1 @@ +export * from './Details' diff --git a/src/Pages/Applications/DevtronApps/service.ts b/apps/web/src/Pages/Applications/DevtronApps/service.ts similarity index 100% rename from src/Pages/Applications/DevtronApps/service.ts rename to apps/web/src/Pages/Applications/DevtronApps/service.ts diff --git a/src/Pages/Applications/DevtronApps/service.types.ts b/apps/web/src/Pages/Applications/DevtronApps/service.types.ts similarity index 100% rename from src/Pages/Applications/DevtronApps/service.types.ts rename to apps/web/src/Pages/Applications/DevtronApps/service.types.ts diff --git a/apps/web/src/Pages/Applications/index.ts b/apps/web/src/Pages/Applications/index.ts new file mode 100644 index 0000000000..7fd4cacc27 --- /dev/null +++ b/apps/web/src/Pages/Applications/index.ts @@ -0,0 +1 @@ +export * from './DevtronApps' diff --git a/src/Pages/GlobalConfigurations/Authorization/APITokens/APITokenList.tsx b/apps/web/src/Pages/GlobalConfigurations/Authorization/APITokens/APITokenList.tsx similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/APITokens/APITokenList.tsx rename to apps/web/src/Pages/GlobalConfigurations/Authorization/APITokens/APITokenList.tsx diff --git a/src/Pages/GlobalConfigurations/Authorization/APITokens/ApiTokens.component.tsx b/apps/web/src/Pages/GlobalConfigurations/Authorization/APITokens/ApiTokens.component.tsx similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/APITokens/ApiTokens.component.tsx rename to apps/web/src/Pages/GlobalConfigurations/Authorization/APITokens/ApiTokens.component.tsx diff --git a/src/Pages/GlobalConfigurations/Authorization/APITokens/CreateAPIToken.tsx b/apps/web/src/Pages/GlobalConfigurations/Authorization/APITokens/CreateAPIToken.tsx similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/APITokens/CreateAPIToken.tsx rename to apps/web/src/Pages/GlobalConfigurations/Authorization/APITokens/CreateAPIToken.tsx diff --git a/src/Pages/GlobalConfigurations/Authorization/APITokens/DeleteAPITokenModal.tsx b/apps/web/src/Pages/GlobalConfigurations/Authorization/APITokens/DeleteAPITokenModal.tsx similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/APITokens/DeleteAPITokenModal.tsx rename to apps/web/src/Pages/GlobalConfigurations/Authorization/APITokens/DeleteAPITokenModal.tsx diff --git a/src/Pages/GlobalConfigurations/Authorization/APITokens/EditAPIToken.tsx b/apps/web/src/Pages/GlobalConfigurations/Authorization/APITokens/EditAPIToken.tsx similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/APITokens/EditAPIToken.tsx rename to apps/web/src/Pages/GlobalConfigurations/Authorization/APITokens/EditAPIToken.tsx diff --git a/src/Pages/GlobalConfigurations/Authorization/APITokens/ExpirationDate.tsx b/apps/web/src/Pages/GlobalConfigurations/Authorization/APITokens/ExpirationDate.tsx similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/APITokens/ExpirationDate.tsx rename to apps/web/src/Pages/GlobalConfigurations/Authorization/APITokens/ExpirationDate.tsx diff --git a/src/Pages/GlobalConfigurations/Authorization/APITokens/GenerateActionButton.tsx b/apps/web/src/Pages/GlobalConfigurations/Authorization/APITokens/GenerateActionButton.tsx similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/APITokens/GenerateActionButton.tsx rename to apps/web/src/Pages/GlobalConfigurations/Authorization/APITokens/GenerateActionButton.tsx diff --git a/src/Pages/GlobalConfigurations/Authorization/APITokens/GenerateModal.tsx b/apps/web/src/Pages/GlobalConfigurations/Authorization/APITokens/GenerateModal.tsx similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/APITokens/GenerateModal.tsx rename to apps/web/src/Pages/GlobalConfigurations/Authorization/APITokens/GenerateModal.tsx diff --git a/src/Pages/GlobalConfigurations/Authorization/APITokens/RegenerateModal.tsx b/apps/web/src/Pages/GlobalConfigurations/Authorization/APITokens/RegenerateModal.tsx similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/APITokens/RegenerateModal.tsx rename to apps/web/src/Pages/GlobalConfigurations/Authorization/APITokens/RegenerateModal.tsx diff --git a/src/Pages/GlobalConfigurations/Authorization/APITokens/__mocks__/ApiTokens.mock.ts b/apps/web/src/Pages/GlobalConfigurations/Authorization/APITokens/__mocks__/ApiTokens.mock.ts similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/APITokens/__mocks__/ApiTokens.mock.ts rename to apps/web/src/Pages/GlobalConfigurations/Authorization/APITokens/__mocks__/ApiTokens.mock.ts diff --git a/src/Pages/GlobalConfigurations/Authorization/APITokens/__tests__/ApiTokens.test.tsx b/apps/web/src/Pages/GlobalConfigurations/Authorization/APITokens/__tests__/ApiTokens.test.tsx similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/APITokens/__tests__/ApiTokens.test.tsx rename to apps/web/src/Pages/GlobalConfigurations/Authorization/APITokens/__tests__/ApiTokens.test.tsx diff --git a/src/Pages/GlobalConfigurations/Authorization/APITokens/apiToken.scss b/apps/web/src/Pages/GlobalConfigurations/Authorization/APITokens/apiToken.scss similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/APITokens/apiToken.scss rename to apps/web/src/Pages/GlobalConfigurations/Authorization/APITokens/apiToken.scss diff --git a/src/Pages/GlobalConfigurations/Authorization/APITokens/apiToken.type.ts b/apps/web/src/Pages/GlobalConfigurations/Authorization/APITokens/apiToken.type.ts similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/APITokens/apiToken.type.ts rename to apps/web/src/Pages/GlobalConfigurations/Authorization/APITokens/apiToken.type.ts diff --git a/src/Pages/GlobalConfigurations/Authorization/APITokens/apiToken.utils.ts b/apps/web/src/Pages/GlobalConfigurations/Authorization/APITokens/apiToken.utils.ts similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/APITokens/apiToken.utils.ts rename to apps/web/src/Pages/GlobalConfigurations/Authorization/APITokens/apiToken.utils.ts diff --git a/src/Pages/GlobalConfigurations/Authorization/APITokens/index.ts b/apps/web/src/Pages/GlobalConfigurations/Authorization/APITokens/index.ts similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/APITokens/index.ts rename to apps/web/src/Pages/GlobalConfigurations/Authorization/APITokens/index.ts diff --git a/src/Pages/GlobalConfigurations/Authorization/APITokens/service.ts b/apps/web/src/Pages/GlobalConfigurations/Authorization/APITokens/service.ts similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/APITokens/service.ts rename to apps/web/src/Pages/GlobalConfigurations/Authorization/APITokens/service.ts diff --git a/src/Pages/GlobalConfigurations/Authorization/APITokens/validationRules.ts b/apps/web/src/Pages/GlobalConfigurations/Authorization/APITokens/validationRules.ts similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/APITokens/validationRules.ts rename to apps/web/src/Pages/GlobalConfigurations/Authorization/APITokens/validationRules.ts diff --git a/src/Pages/GlobalConfigurations/Authorization/Authorization.component.tsx b/apps/web/src/Pages/GlobalConfigurations/Authorization/Authorization.component.tsx similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/Authorization.component.tsx rename to apps/web/src/Pages/GlobalConfigurations/Authorization/Authorization.component.tsx diff --git a/src/Pages/GlobalConfigurations/Authorization/AuthorizationProvider.tsx b/apps/web/src/Pages/GlobalConfigurations/Authorization/AuthorizationProvider.tsx similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/AuthorizationProvider.tsx rename to apps/web/src/Pages/GlobalConfigurations/Authorization/AuthorizationProvider.tsx diff --git a/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/AddEdit/PermissionGroupAddEdit.component.tsx b/apps/web/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/AddEdit/PermissionGroupAddEdit.component.tsx similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/PermissionGroups/AddEdit/PermissionGroupAddEdit.component.tsx rename to apps/web/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/AddEdit/PermissionGroupAddEdit.component.tsx diff --git a/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/AddEdit/PermissionGroupForm.tsx b/apps/web/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/AddEdit/PermissionGroupForm.tsx similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/PermissionGroups/AddEdit/PermissionGroupForm.tsx rename to apps/web/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/AddEdit/PermissionGroupForm.tsx diff --git a/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/AddEdit/index.ts b/apps/web/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/AddEdit/index.ts similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/PermissionGroups/AddEdit/index.ts rename to apps/web/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/AddEdit/index.ts diff --git a/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/List/ExportPermissionGroupsToCsv.tsx b/apps/web/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/List/ExportPermissionGroupsToCsv.tsx similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/PermissionGroups/List/ExportPermissionGroupsToCsv.tsx rename to apps/web/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/List/ExportPermissionGroupsToCsv.tsx diff --git a/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/List/NoPermissionGroups.tsx b/apps/web/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/List/NoPermissionGroups.tsx similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/PermissionGroups/List/NoPermissionGroups.tsx rename to apps/web/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/List/NoPermissionGroups.tsx diff --git a/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/List/PermissionGroupContainer.tsx b/apps/web/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/List/PermissionGroupContainer.tsx similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/PermissionGroups/List/PermissionGroupContainer.tsx rename to apps/web/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/List/PermissionGroupContainer.tsx diff --git a/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/List/PermissionGroupList.component.tsx b/apps/web/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/List/PermissionGroupList.component.tsx similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/PermissionGroups/List/PermissionGroupList.component.tsx rename to apps/web/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/List/PermissionGroupList.component.tsx diff --git a/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/List/PermissionGroupListHeader.tsx b/apps/web/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/List/PermissionGroupListHeader.tsx similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/PermissionGroups/List/PermissionGroupListHeader.tsx rename to apps/web/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/List/PermissionGroupListHeader.tsx diff --git a/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/List/PermissionGroupRow.tsx b/apps/web/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/List/PermissionGroupRow.tsx similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/PermissionGroups/List/PermissionGroupRow.tsx rename to apps/web/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/List/PermissionGroupRow.tsx diff --git a/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/List/PermissionGroupTable.tsx b/apps/web/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/List/PermissionGroupTable.tsx similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/PermissionGroups/List/PermissionGroupTable.tsx rename to apps/web/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/List/PermissionGroupTable.tsx diff --git a/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/List/constants.ts b/apps/web/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/List/constants.ts similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/PermissionGroups/List/constants.ts rename to apps/web/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/List/constants.ts diff --git a/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/List/index.ts b/apps/web/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/List/index.ts similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/PermissionGroups/List/index.ts rename to apps/web/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/List/index.ts diff --git a/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/List/types.ts b/apps/web/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/List/types.ts similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/PermissionGroups/List/types.ts rename to apps/web/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/List/types.ts diff --git a/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/PermissionGroups.component.tsx b/apps/web/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/PermissionGroups.component.tsx similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/PermissionGroups/PermissionGroups.component.tsx rename to apps/web/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/PermissionGroups.component.tsx diff --git a/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/index.ts b/apps/web/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/index.ts similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/PermissionGroups/index.ts rename to apps/web/src/Pages/GlobalConfigurations/Authorization/PermissionGroups/index.ts diff --git a/src/Pages/GlobalConfigurations/Authorization/README.md b/apps/web/src/Pages/GlobalConfigurations/Authorization/README.md similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/README.md rename to apps/web/src/Pages/GlobalConfigurations/Authorization/README.md diff --git a/src/Pages/GlobalConfigurations/Authorization/SSOLoginServices/SSOLogin.component.tsx b/apps/web/src/Pages/GlobalConfigurations/Authorization/SSOLoginServices/SSOLogin.component.tsx similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/SSOLoginServices/SSOLogin.component.tsx rename to apps/web/src/Pages/GlobalConfigurations/Authorization/SSOLoginServices/SSOLogin.component.tsx diff --git a/src/Pages/GlobalConfigurations/Authorization/SSOLoginServices/constants.ts b/apps/web/src/Pages/GlobalConfigurations/Authorization/SSOLoginServices/constants.ts similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/SSOLoginServices/constants.ts rename to apps/web/src/Pages/GlobalConfigurations/Authorization/SSOLoginServices/constants.ts diff --git a/src/Pages/GlobalConfigurations/Authorization/SSOLoginServices/index.ts b/apps/web/src/Pages/GlobalConfigurations/Authorization/SSOLoginServices/index.ts similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/SSOLoginServices/index.ts rename to apps/web/src/Pages/GlobalConfigurations/Authorization/SSOLoginServices/index.ts diff --git a/src/Pages/GlobalConfigurations/Authorization/SSOLoginServices/sampleSSOConfig.json b/apps/web/src/Pages/GlobalConfigurations/Authorization/SSOLoginServices/sampleSSOConfig.json similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/SSOLoginServices/sampleSSOConfig.json rename to apps/web/src/Pages/GlobalConfigurations/Authorization/SSOLoginServices/sampleSSOConfig.json diff --git a/src/Pages/GlobalConfigurations/Authorization/SSOLoginServices/service.ts b/apps/web/src/Pages/GlobalConfigurations/Authorization/SSOLoginServices/service.ts similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/SSOLoginServices/service.ts rename to apps/web/src/Pages/GlobalConfigurations/Authorization/SSOLoginServices/service.ts diff --git a/src/Pages/GlobalConfigurations/Authorization/SSOLoginServices/ssoConfig.types.tsx b/apps/web/src/Pages/GlobalConfigurations/Authorization/SSOLoginServices/ssoConfig.types.tsx similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/SSOLoginServices/ssoConfig.types.tsx rename to apps/web/src/Pages/GlobalConfigurations/Authorization/SSOLoginServices/ssoConfig.types.tsx diff --git a/src/Pages/GlobalConfigurations/Authorization/SSOLoginServices/ssoLogin.scss b/apps/web/src/Pages/GlobalConfigurations/Authorization/SSOLoginServices/ssoLogin.scss similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/SSOLoginServices/ssoLogin.scss rename to apps/web/src/Pages/GlobalConfigurations/Authorization/SSOLoginServices/ssoLogin.scss diff --git a/src/Pages/GlobalConfigurations/Authorization/SSOLoginServices/utils.tsx b/apps/web/src/Pages/GlobalConfigurations/Authorization/SSOLoginServices/utils.tsx similarity index 100% rename from src/Pages/GlobalConfigurations/Authorization/SSOLoginServices/utils.tsx rename to apps/web/src/Pages/GlobalConfigurations/Authorization/SSOLoginServices/utils.tsx diff --git a/src/Pages/GlobalConfigurations/Authorization/Shared/components/AppPermissions/AppPermissionDetail.tsx b/apps/web/src/Pages/GlobalConfigurations/Authorization/Shared/components/AppPermissions/AppPermissionDetail.tsx similarity index 99% rename from src/Pages/GlobalConfigurations/Authorization/Shared/components/AppPermissions/AppPermissionDetail.tsx rename to apps/web/src/Pages/GlobalConfigurations/Authorization/Shared/components/AppPermissions/AppPermissionDetail.tsx index b9d3ab54a9..35a3ac1182 100644 --- a/src/Pages/GlobalConfigurations/Authorization/Shared/components/AppPermissions/AppPermissionDetail.tsx +++ b/apps/web/src/Pages/GlobalConfigurations/Authorization/Shared/components/AppPermissions/AppPermissionDetail.tsx @@ -41,7 +41,7 @@ const AppPermissionDetail = ({