diff --git a/package-lock.json b/package-lock.json index 6ec603ebe..01938014c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.12.0", + "version": "1.13.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.12.0", + "version": "1.13.0", "hasInstallScript": true, "license": "ISC", "dependencies": { @@ -39,7 +39,6 @@ "react-diff-viewer-continued": "^3.4.0", "react-draggable": "^4.4.5", "react-international-phone": "^4.5.0", - "react-monaco-editor": "^0.54.0", "react-virtualized-sticky-tree": "^3.0.0-beta18", "sass": "^1.69.7", "tslib": "2.7.0" @@ -75,8 +74,6 @@ "json-schema": "^0.4.0", "lint-staged": "^12.5.0", "moment": "^2.29.4", - "monaco-editor": "0.44.0", - "monaco-yaml": "5.1.1", "prettier": "^3.1.1", "react-ga4": "^1.4.1", "react-toastify": "9.1.3", @@ -8527,12 +8524,6 @@ "node": ">=6" } }, - "node_modules/jsonc-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", - "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", - "dev": true - }, "node_modules/jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -9304,96 +9295,6 @@ "node": "*" } }, - "node_modules/monaco-editor": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.44.0.tgz", - "integrity": "sha512-5SmjNStN6bSuSE5WPT2ZV+iYn1/yI9sd4Igtk23ChvqB7kDk9lZbB9F5frsuvpB+2njdIeGGFf2G4gbE6rCC9Q==" - }, - "node_modules/monaco-languageserver-types": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/monaco-languageserver-types/-/monaco-languageserver-types-0.3.4.tgz", - "integrity": "sha512-d58sP5yNhjs8uG1ESXs0hFnuX2YfdMhiGeWhdgTUZyG9aaWgyI4dDwrK1khf1mPF2u9Sljv42sfYqPFZnqYMYg==", - "dev": true, - "dependencies": { - "monaco-types": "^0.1.0", - "vscode-languageserver-protocol": "^3.0.0", - "vscode-uri": "^3.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/remcohaszing" - } - }, - "node_modules/monaco-marker-data-provider": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/monaco-marker-data-provider/-/monaco-marker-data-provider-1.2.4.tgz", - "integrity": "sha512-4DsPgsAqpTyUDs3humXRBPUJoihTv+L6v9aupQWD80X2YXaCXUd11mWYeSCYHuPgdUmjFaNWCEOjQ6ewf/QA1Q==", - "dev": true, - "dependencies": { - "monaco-types": "^0.1.0" - }, - "funding": { - "url": "https://github.com/sponsors/remcohaszing" - } - }, - "node_modules/monaco-types": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/monaco-types/-/monaco-types-0.1.0.tgz", - "integrity": "sha512-aWK7SN9hAqNYi0WosPoMjenMeXJjwCxDibOqWffyQ/qXdzB/86xshGQobRferfmNz7BSNQ8GB0MD0oby9/5fTQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/remcohaszing" - } - }, - "node_modules/monaco-worker-manager": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/monaco-worker-manager/-/monaco-worker-manager-2.0.1.tgz", - "integrity": "sha512-kdPL0yvg5qjhKPNVjJoym331PY/5JC11aPJXtCZNwWRvBr6jhkIamvYAyiY5P1AWFmNOy0aRDRoMdZfa71h8kg==", - "dev": true, - "peerDependencies": { - "monaco-editor": ">=0.30.0" - } - }, - "node_modules/monaco-yaml": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/monaco-yaml/-/monaco-yaml-5.1.1.tgz", - "integrity": "sha512-BuZ0/ZCGjrPNRzYMZ/MoxH8F/SdM+mATENXnpOhDYABi1Eh+QvxSszEct+ACSCarZiwLvy7m6yEF/pvW8XJkyQ==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.0", - "jsonc-parser": "^3.0.0", - "monaco-languageserver-types": "^0.3.0", - "monaco-marker-data-provider": "^1.0.0", - "monaco-types": "^0.1.0", - "monaco-worker-manager": "^2.0.0", - "path-browserify": "^1.0.0", - "prettier": "^2.0.0", - "vscode-languageserver-textdocument": "^1.0.0", - "vscode-languageserver-types": "^3.0.0", - "vscode-uri": "^3.0.0", - "yaml": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/remcohaszing" - }, - "peerDependencies": { - "monaco-editor": ">=0.36" - } - }, - "node_modules/monaco-yaml/node_modules/prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", - "dev": true, - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, "node_modules/moo": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", @@ -10450,19 +10351,6 @@ "moment": ">=1.6.0" } }, - "node_modules/react-monaco-editor": { - "version": "0.54.0", - "resolved": "https://registry.npmjs.org/react-monaco-editor/-/react-monaco-editor-0.54.0.tgz", - "integrity": "sha512-9JwO69851mfpuhYLHlKbae7omQWJ/2ICE2lbL0VHyNyZR8rCOH7440u+zAtDgiOMpLwmYdY1sEZCdRefywX6GQ==", - "dependencies": { - "prop-types": "^15.8.1" - }, - "peerDependencies": { - "@types/react": ">=16 <= 18", - "monaco-editor": "^0.39.0", - "react": ">=16 <= 18" - } - }, "node_modules/react-outside-click-handler": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/react-outside-click-handler/-/react-outside-click-handler-1.3.0.tgz", @@ -12728,37 +12616,6 @@ "@esbuild/win32-x64": "0.21.5" } }, - "node_modules/vscode-jsonrpc": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", - "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", - "dev": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/vscode-languageserver-protocol": { - "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", - "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", - "dev": true, - "dependencies": { - "vscode-jsonrpc": "8.2.0", - "vscode-languageserver-types": "3.17.5" - } - }, - "node_modules/vscode-languageserver-textdocument": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", - "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", - "dev": true - }, - "node_modules/vscode-languageserver-types": { - "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", - "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", - "dev": true - }, "node_modules/vscode-uri": { "version": "3.0.8", "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", diff --git a/package.json b/package.json index 7a0801063..a10088e67 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.12.0", + "version": "1.13.0", "description": "Supporting common component library", "type": "module", "main": "dist/index.js", @@ -66,8 +66,6 @@ "json-schema": "^0.4.0", "lint-staged": "^12.5.0", "moment": "^2.29.4", - "monaco-editor": "0.44.0", - "monaco-yaml": "5.1.1", "prettier": "^3.1.1", "react-ga4": "^1.4.1", "react-toastify": "9.1.3", @@ -127,7 +125,6 @@ "react-diff-viewer-continued": "^3.4.0", "react-draggable": "^4.4.5", "react-international-phone": "^4.5.0", - "react-monaco-editor": "^0.54.0", "react-virtualized-sticky-tree": "^3.0.0-beta18", "sass": "^1.69.7", "tslib": "2.7.0" @@ -138,9 +135,6 @@ "react": "^17.0.2", "react-dom": "^17.0.2" }, - "react-monaco-editor": { - "monaco-editor": "0.44.0" - }, "vite-plugin-svgr": { "vite": "5.4.17" }, diff --git a/src/Assets/Icon/ic-cpu.svg b/src/Assets/Icon/ic-cpu.svg deleted file mode 100644 index d1554b282..000000000 --- a/src/Assets/Icon/ic-cpu.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/Assets/Icon/ic-nav-rocket.svg b/src/Assets/Icon/ic-nav-rocket.svg deleted file mode 100644 index 5ee6b84c4..000000000 --- a/src/Assets/Icon/ic-nav-rocket.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - diff --git a/src/Assets/IconV2/ic-checks.svg b/src/Assets/IconV2/ic-checks.svg new file mode 100644 index 000000000..1a48f93b4 --- /dev/null +++ b/src/Assets/IconV2/ic-checks.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Assets/IconV2/ic-cpu.svg b/src/Assets/IconV2/ic-cpu.svg new file mode 100644 index 000000000..7aafefb1d --- /dev/null +++ b/src/Assets/IconV2/ic-cpu.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/Assets/IconV2/ic-hand-pointing.svg b/src/Assets/IconV2/ic-hand-pointing.svg new file mode 100644 index 000000000..34385cc82 --- /dev/null +++ b/src/Assets/IconV2/ic-hand-pointing.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/Assets/IconV2/ic-help-filled.svg b/src/Assets/IconV2/ic-help-filled.svg new file mode 100644 index 000000000..c375921bd --- /dev/null +++ b/src/Assets/IconV2/ic-help-filled.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/Assets/IconV2/ic-info-filled.svg b/src/Assets/IconV2/ic-info-filled.svg index 8f79e556b..f9f02f2b0 100644 --- a/src/Assets/IconV2/ic-info-filled.svg +++ b/src/Assets/IconV2/ic-info-filled.svg @@ -16,5 +16,5 @@ - + diff --git a/src/Assets/IconV2/ic-key.svg b/src/Assets/IconV2/ic-key.svg new file mode 100644 index 000000000..fc4da3ebc --- /dev/null +++ b/src/Assets/IconV2/ic-key.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Assets/IconV2/ic-memory.svg b/src/Assets/IconV2/ic-memory.svg new file mode 100644 index 000000000..d7e872ce2 --- /dev/null +++ b/src/Assets/IconV2/ic-memory.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/Assets/IconV2/ic-rocket-launch.svg b/src/Assets/IconV2/ic-rocket-launch.svg new file mode 100644 index 000000000..652cd1197 --- /dev/null +++ b/src/Assets/IconV2/ic-rocket-launch.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Assets/IconV2/ic-timeout-two-dash.svg b/src/Assets/IconV2/ic-selected.svg similarity index 62% rename from src/Assets/IconV2/ic-timeout-two-dash.svg rename to src/Assets/IconV2/ic-selected.svg index 85b6d9893..e42627f7e 100644 --- a/src/Assets/IconV2/ic-timeout-two-dash.svg +++ b/src/Assets/IconV2/ic-selected.svg @@ -15,7 +15,6 @@ --> - - - + + diff --git a/src/Assets/IconV2/ic-thumb-down.svg b/src/Assets/IconV2/ic-thumb-down.svg new file mode 100644 index 000000000..54ef52337 --- /dev/null +++ b/src/Assets/IconV2/ic-thumb-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Assets/IconV2/ic-thumb-up.svg b/src/Assets/IconV2/ic-thumb-up.svg new file mode 100644 index 000000000..c98369a89 --- /dev/null +++ b/src/Assets/IconV2/ic-thumb-up.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Assets/Icon/ic-memory.svg b/src/Assets/IconV2/ic-timeout-dash.svg similarity index 51% rename from src/Assets/Icon/ic-memory.svg rename to src/Assets/IconV2/ic-timeout-dash.svg index e7a41596b..08932efac 100644 --- a/src/Assets/Icon/ic-memory.svg +++ b/src/Assets/IconV2/ic-timeout-dash.svg @@ -14,6 +14,8 @@ - limitations under the License. --> - - - \ No newline at end of file + + + + + diff --git a/src/Common/CIPipeline.Types.ts b/src/Common/CIPipeline.Types.ts index 45ab04918..c0bcb8aa0 100644 --- a/src/Common/CIPipeline.Types.ts +++ b/src/Common/CIPipeline.Types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { DynamicDataTableCellValidationState } from '@Shared/Components' +import { DynamicDataTableCellValidationState, KeyValueTableData } from '@Shared/Components' export interface MaterialType { name: string @@ -158,7 +158,7 @@ export interface PortMapType { portOnLocal: number portOnContainer: number } -interface ConditionDetails { +export interface ConditionDetails { id: number conditionOnVariable: string conditionOperator: string @@ -266,7 +266,7 @@ export enum WORKFLOW_CACHE_CONFIG_ENUM { export interface FormType { name: string - args: { key: string; value: string }[] + args: KeyValueTableData[] materials: MaterialType[] gitHost: Githost webhookEvents: WebhookEvent[] @@ -314,23 +314,29 @@ export enum InputOutputVariablesHeaderKeys { VALUE = 'val', } -export type InputOutputVariablesErrorObj = Record +export enum ConditionDataTableHeaderKeys { + VARIABLE = 'variable', + OPERATOR = 'operator', + VALUE = 'val', +} + +type InputOutputVariablesErrorObj = Record +type ConditionDetailsErrorObj = Record + +interface StepDetailTaskErrorObj { + inputVariables?: Record + outputVariables?: Record + isInputVariablesValid?: boolean + isOutputVariablesValid?: boolean + conditionDetails?: Record + isConditionDetailsValid?: boolean +} export interface TaskErrorObj { isValid: boolean name: ErrorObj - inlineStepDetail?: { - inputVariables?: Record - outputVariables?: Record - isInputVariablesValid?: boolean - isOutputVariablesValid?: boolean - } - pluginRefStepDetail?: { - inputVariables?: Record - outputVariables?: Record - isInputVariablesValid?: boolean - isOutputVariablesValid?: boolean - } + inlineStepDetail?: StepDetailTaskErrorObj + pluginRefStepDetail?: StepDetailTaskErrorObj } export interface FormErrorObjectType { name: ErrorObj diff --git a/src/Common/CodeEditor/CodeEditor.reducer.ts b/src/Common/CodeEditor/CodeEditor.reducer.ts deleted file mode 100644 index 1def619f1..000000000 --- a/src/Common/CodeEditor/CodeEditor.reducer.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright (c) 2024. Devtron Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import YAML from 'yaml' - -import { noop, YAMLStringify } from '@Common/Helper' - -import { MODES } from '../Constants' -import { Action, CodeEditorInitialValueType, CodeEditorState } from './types' -import { getCodeEditorThemeFromAppTheme } from './utils' - -export const CodeEditorReducer = (state: CodeEditorState, action: Action) => { - switch (action.type) { - case 'changeLanguage': - return { ...state, mode: action.value } - case 'setDiff': - return { ...state, diffMode: action.value } - case 'setTheme': - return { ...state, theme: action.value } - case 'setCode': - return { ...state, code: action.value } - case 'setDefaultCode': - return { ...state, defaultCode: action.value } - case 'setHeight': - return { ...state, height: action.value.toString() } - default: - return state - } -} - -export const parseValueToCode = (value: string, mode: string, tabSize: number) => { - let obj = null - - try { - obj = JSON.parse(value) - } catch { - try { - obj = YAML.parse(value) - } catch { - noop() - } - } - - let final = value - - if (obj) { - switch (mode) { - case MODES.JSON: - final = JSON.stringify(obj, null, tabSize) - break - case MODES.YAML: - final = YAMLStringify(obj) - break - default: - break - } - } - - return final -} - -export const initialState = ({ - mode, - theme, - value, - defaultValue, - diffView, - noParsing, - tabSize, - appTheme, -}: CodeEditorInitialValueType): CodeEditorState => ({ - mode: mode as MODES, - theme: getCodeEditorThemeFromAppTheme(theme, appTheme), - code: noParsing ? value : parseValueToCode(value, mode, tabSize), - defaultCode: noParsing ? defaultValue : parseValueToCode(defaultValue, mode, tabSize), - diffMode: diffView, - noParsing: [MODES.JSON, MODES.YAML].includes(mode as MODES) ? noParsing : true, -}) diff --git a/src/Common/CodeEditor/CodeEditor.tsx b/src/Common/CodeEditor/CodeEditor.tsx deleted file mode 100644 index d3e755da8..000000000 --- a/src/Common/CodeEditor/CodeEditor.tsx +++ /dev/null @@ -1,556 +0,0 @@ -/* - * Copyright (c) 2024. Devtron Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { useEffect, useMemo, useReducer, useRef, useState } from 'react' -import MonacoEditor, { ChangeHandler, DiffEditorDidMount, EditorDidMount, MonacoDiffEditor } from 'react-monaco-editor' -import ReactGA from 'react-ga4' -import * as monaco from 'monaco-editor/esm/vs/editor/editor.api' -import { configureMonacoYaml } from 'monaco-yaml' - -import { ReactComponent as ICWarningY5 } from '@Icons/ic-warning-y5.svg' -import { ReactComponent as ICCompare } from '@Icons/ic-compare.svg' -import { useTheme } from '@Shared/Providers' -import { ReactComponent as Info } from '../../Assets/Icon/ic-info-filled.svg' -import { ReactComponent as ErrorIcon } from '../../Assets/Icon/ic-error-exclamation.svg' -import './codeEditor.scss' -import 'monaco-editor' - -import { cleanKubeManifest, useEffectAfterMount, useJsonYaml } from '../Helper' -import { useWindowSize } from '../Hooks' -import Select from '../Select/Select' -import RadioGroup from '../RadioGroup/RadioGroup' -import { ClipboardButton } from '../ClipboardButton/ClipboardButton' -import { Progressing } from '../Progressing' -import { - CodeEditorComposition, - CodeEditorHeaderComposition, - CodeEditorHeaderInterface, - CodeEditorInterface, - CodeEditorThemesKeys, - InformationBarProps, -} from './types' -import { CodeEditorReducer, initialState, parseValueToCode } from './CodeEditor.reducer' -import { DEFAULT_JSON_SCHEMA_URI, MODES } from '../Constants' -import { getCodeEditorThemeFromAppTheme } from './utils' - -const CodeEditorContext = React.createContext(null) - -function useCodeEditorContext() { - const context = React.useContext(CodeEditorContext) - if (!context) { - throw new Error(`cannot be rendered outside the component`) - } - return context -} - -const INITIAL_HEIGHT_WHEN_DYNAMIC_HEIGHT = 100 - -const CodeEditor: React.FC & CodeEditorComposition = React.memo( - ({ - value, - mode = MODES.JSON, - noParsing = false, - defaultValue = '', - children, - tabSize = 2, - lineDecorationsWidth = 0, - height = 450, - inline = false, - shebang = '', - onChange, - readOnly, - diffView, - theme, - loading, - customLoader, - focus, - validatorSchema, - schemaURI = DEFAULT_JSON_SCHEMA_URI, - isKubernetes = true, - cleanData = false, - onBlur, - onFocus, - adjustEditorHeightToContent = false, - disableSearch = false, - originalEditable = false, - }) => { - const { appTheme } = useTheme() - - if (cleanData) { - value = cleanKubeManifest(value) - defaultValue = cleanKubeManifest(defaultValue) - } - - const editorRef = useRef(null) - const monacoRef = useRef(null) - const { width, height: windowHeight } = useWindowSize() - const memoisedReducer = React.useCallback(CodeEditorReducer, []) - const [state, dispatch] = useReducer( - memoisedReducer, - initialState({ - mode, - theme, - value, - defaultValue, - diffView, - noParsing, - tabSize, - appTheme, - }), - ) - const [, json, yamlCode, error] = useJsonYaml(state.code, tabSize, state.mode, !state.noParsing) - const [, originalJson, originalYaml] = useJsonYaml(state.defaultCode, tabSize, state.mode, !state.noParsing) - const [contentHeight, setContentHeight] = useState( - adjustEditorHeightToContent ? INITIAL_HEIGHT_WHEN_DYNAMIC_HEIGHT : height, - ) - // TODO: upgrade to 0.56.2 to remove this - const onChangeRef = useRef(onChange) - onChangeRef.current = onChange - monaco.editor.defineTheme(CodeEditorThemesKeys.vsDarkDT, { - base: 'vs-dark', - inherit: true, - rules: [ - // @ts-ignore - { background: '#181920' }, - ], - colors: { - 'editor.background': '#181920', - }, - }) - - monaco.editor.defineTheme(CodeEditorThemesKeys.networkStatusInterface, { - base: 'vs-dark', - inherit: true, - rules: [ - // @ts-ignore - { background: '#1A1A1A' }, - ], - colors: { - 'editor.background': '#1A1A1A', - }, - }) - - useEffect(() => { - dispatch({ type: 'setTheme', value: getCodeEditorThemeFromAppTheme(theme, appTheme) }) - }, [appTheme]) - - useEffect(() => { - const rule = !disableSearch - ? null - : monaco.editor.addKeybindingRule({ - command: null, - keybinding: monaco.KeyCode.KeyF | monaco.KeyMod.CtrlCmd, - }) - return () => { - rule?.dispose() - } - }, [disableSearch]) - - const editorDidMount: EditorDidMount = (editor) => { - if ( - mode === MODES.YAML && - editor && - typeof editor.getModel === 'function' && - typeof editor.getModel().updateOptions === 'function' - ) { - editor.getModel().updateOptions({ tabSize: 2 }) - } - - if (editor) { - if (typeof editor.onDidFocusEditorWidget === 'function' && typeof onFocus === 'function') { - editor.onDidFocusEditorWidget(onFocus) - } - - if (typeof editor.onDidBlurEditorWidget === 'function' && typeof onBlur === 'function') { - editor.onDidBlurEditorWidget(onBlur) - } - } - - if (adjustEditorHeightToContent && editor) { - editor.onDidContentSizeChange(() => { - setContentHeight(editor.getContentHeight()) - }) - setContentHeight(editor.getContentHeight()) - } - - editorRef.current = editor - monacoRef.current = monaco - } - - const diffEditorDidMount: DiffEditorDidMount = (editor, monaco) => { - const originalEditor = editor.getOriginalEditor() - const modifiedEditor = editor.getModifiedEditor() - - if (adjustEditorHeightToContent) { - originalEditor.onDidContentSizeChange(() => { - setContentHeight( - Math.max( - typeof contentHeight === 'number' ? contentHeight : Number.MIN_SAFE_INTEGER, - originalEditor.getContentHeight(), - ), - ) - }) - - modifiedEditor.onDidContentSizeChange(() => { - setContentHeight( - Math.max( - typeof contentHeight === 'number' ? contentHeight : Number.MIN_SAFE_INTEGER, - modifiedEditor.getContentHeight(), - ), - ) - }) - - setContentHeight(Math.max(originalEditor.getContentHeight(), modifiedEditor.getContentHeight())) - } - if (originalEditable) { - originalEditor.onDidChangeModelContent(() => { - codeEditorOnChange(modifiedEditor.getValue(), originalEditor.getValue()) - }) - } - - editorRef.current = editor - monacoRef.current = monaco - } - - const editorHeight = useMemo(() => { - if (!adjustEditorHeightToContent) { - return height - } - return contentHeight - }, [height, contentHeight, adjustEditorHeightToContent]) - - useEffect(() => { - if (!validatorSchema) { - return - } - const config = configureMonacoYaml(monaco, { - enableSchemaRequest: true, - isKubernetes, - schemas: [ - { - uri: schemaURI, - fileMatch: ['*'], // associate with our model - schema: validatorSchema, - }, - ], - }) - return () => { - config.dispose() - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [validatorSchema, schemaURI]) - useEffect(() => { - if (!editorRef.current) { - return - } - editorRef.current.updateOptions({ readOnly }) - }, [readOnly]) - - useEffect(() => { - if (!editorRef.current) { - return - } - editorRef.current.layout() - }, [width, windowHeight]) - - const setCode = (value: string, originalValue: string) => { - dispatch({ type: 'setCode', value }) - dispatch({ type: 'setDefaultCode', value: originalValue }) - onChangeRef.current?.(value, originalValue) - } - - useEffectAfterMount(() => { - if (noParsing) { - setCode(value, defaultValue) - - return - } - - if (value === state.code) { - return - } - - setCode(parseValueToCode(value, state.mode, tabSize), parseValueToCode(defaultValue, state.mode, tabSize)) - }, [value, defaultValue, noParsing]) - - useEffect(() => { - dispatch({ type: 'setDiff', value: diffView }) - }, [diffView]) - - useEffect(() => { - if (focus) { - editorRef.current.focus() - } - }, [focus]) - - const codeEditorOnChange = (newValue: string, newOriginalValue: string) => { - setCode(newValue, newOriginalValue) - } - - const handleOnChange: ChangeHandler = (newValue) => { - codeEditorOnChange(newValue, editorRef.current?.getOriginalEditor?.().getValue?.() ?? '') - } - - function handleLanguageChange(mode: 'json' | 'yaml') { - dispatch({ type: 'changeLanguage', value: mode }) - setCode(mode === 'json' ? json : yamlCode, mode === 'json' ? originalJson : originalYaml) - } - - const options: monaco.editor.IEditorConstructionOptions = { - selectOnLineNumbers: true, - roundedSelection: false, - readOnly, - lineDecorationsWidth, - automaticLayout: true, - scrollBeyondLastLine: false, - ...(adjustEditorHeightToContent - ? { - overviewRulerLanes: adjustEditorHeightToContent ? 0 : 1, - } - : {}), - minimap: { - enabled: false, - }, - scrollbar: { - alwaysConsumeMouseWheel: false, - vertical: inline ? 'hidden' : 'auto', - ...(adjustEditorHeightToContent - ? { - vertical: 'hidden', - verticalScrollbarSize: 0, - verticalSliderSize: 0, - } - : {}), - }, - lineNumbers(lineNumber) { - return `${lineNumber}` - }, - } - - const diffViewOptions: monaco.editor.IDiffEditorConstructionOptions = { - ...options, - originalEditable: originalEditable && !readOnly, - useInlineViewWhenSpaceIsLimited: false, - } - - return ( - - {children} - {loading ? ( - - ) : ( - <> - {shebang &&
{shebang}
} - {state.diffMode ? ( - - ) : ( - - )} - - )} -
- ) - }, -) - -const Header: React.FC & CodeEditorHeaderComposition = ({ - children, - className, - hideDefaultSplitHeader, -}) => { - const { defaultValue, state } = useCodeEditorContext() - return ( -
-
- {children} - {!hideDefaultSplitHeader && defaultValue && } -
- {state.diffMode ?
: null} -
- ) -} - -const ThemeChanger = ({}) => { - const { readOnly, state, dispatch } = useCodeEditorContext() - function handleChangeTheme(e) { - dispatch({ type: 'setTheme', value: e.target.value }) - } - - const themes = ['vs', 'vs-dark'] - return ( - - ) -} - -const LanguageChanger = ({}) => { - const { readOnly, handleLanguageChange, state } = useCodeEditorContext() - if (state.noParsing) { - return null - } - return ( -
- { - ReactGA.event({ - category: 'JSON-YAML Switch', - action: `${event.target.value} view`, - }) - handleLanguageChange(event.target.value) - }} - > - JSON - YAML - -
- ) -} - -const ValidationError = () => { - const { error } = useCodeEditorContext() - return error ?
{error}
: null -} - -const Warning: React.FC = (props) => ( -
- - {props.text} - {props.children} -
-) - -const ErrorBar: React.FC = (props) => ( -
- - {props.text} - {props.children} -
-) - -const Information: React.FC = (props) => ( -
- - {props.text} - {props.children} -
-) - -const Clipboard = () => { - const { state } = useCodeEditorContext() - return -} - -const SplitPane = ({}) => { - const { state, dispatch, readOnly } = useCodeEditorContext() - function handleToggle(e) { - if (readOnly) { - return - } - dispatch({ type: 'setDiff', value: !state.diffMode }) - } - return ( -
- - {state.diffMode ? 'Hide comparison' : 'Compare with default'} -
- ) -} -// TODO: CodeEditor should be composed of CodeEditorPlaceholder -const CodeEditorPlaceholder = ({ className = '', style = {}, customLoader }): JSX.Element => { - const { height } = useCodeEditorContext() - - if (customLoader) { - return customLoader - } - - return ( -
-
-
- -
-
-
- ) -} - -const Container = ({ - children, - flexExpand, - overflowHidden, -}: { - children: React.ReactNode - flexExpand?: boolean - overflowHidden?: boolean -}) => ( -
- {children} -
-) - -CodeEditor.LanguageChanger = LanguageChanger -CodeEditor.ThemeChanger = ThemeChanger -CodeEditor.ValidationError = ValidationError -CodeEditor.Clipboard = Clipboard -CodeEditor.Header = Header -CodeEditor.Warning = Warning -CodeEditor.ErrorBar = ErrorBar -CodeEditor.Information = Information -CodeEditor.Container = Container - -export default CodeEditor diff --git a/src/Common/CodeEditor/codeEditor.scss b/src/Common/CodeEditor/codeEditor.scss deleted file mode 100644 index 9415f8385..000000000 --- a/src/Common/CodeEditor/codeEditor.scss +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright (c) 2024. Devtron Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - - .code-editor__header { - .radio-group { - height: 24px; - overflow: hidden; - } - - .radio-group input[type='checkbox']:checked + .radio__item-label { - background: var(--N700); - border-radius: 0; - color: var(--N0); - } - - .radio__item-label { - padding: 0 8px; - display: inline-block; - width: 100%; - height: 100%; - line-height: 24px; - cursor: pointer; - } - - .radio-group { - padding: 0; - } - - .code-editor__split-pane { - color: var(--N700); - margin-left: auto; - fill: var(--N700); - } - - label.form__error { - margin-left: 8px; - } - - button.clipboard { - margin-left: auto; - height: 20px; - width: 20px; - padding: 0; - background: transparent; - border: unset; - outline: unset; - - svg { - height: 100%; - width: 100%; - } - - &:active { - svg { - height: 90%; - width: 90%; - - path { - fill: var(--G500); - } - } - } - } -} - -.code-editor__header-error { - padding: 8px 16px; - font-size: 12px; - border: solid 1px var(--N200); - background-color: var(--R100); - font-weight: normal; - font-stretch: normal; - font-style: normal; - line-height: 1.33; - letter-spacing: normal; - color: var(--R500); -} - -.code-editor-container { - width: 100%; - border: 1px solid var(--N200); - border-radius: 4px; - overflow: hidden; -} - -.react-monaco-editor-container { - min-height: 300px; -} - -.code-editor__information-info-icon { - width: 16px; - height: 16px; - margin: 0 8px 0 0; - vertical-align: bottom; -} - -.code-editor__toggle { - .toggle__switch { - width: 32px; - margin: 0 12px; - } - - color: var(--N0); -} - -.monaco-editor-hover { - margin-left: 40px; -} - -.monaco-scrollable-element { - & > .visible { - z-index: 9; - } -} - -.code-editor-shebang { - padding: 0 52px; - color: var(--N900); - opacity: 0.6; -} diff --git a/src/Common/CodeEditor/index.ts b/src/Common/CodeEditor/index.ts deleted file mode 100644 index b3fc4c73c..000000000 --- a/src/Common/CodeEditor/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright (c) 2024. Devtron Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { default as CodeEditor } from './CodeEditor' -export * from './types' diff --git a/src/Common/CodeEditor/types.ts b/src/Common/CodeEditor/types.ts deleted file mode 100644 index 85f9a7813..000000000 --- a/src/Common/CodeEditor/types.ts +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright (c) 2024. Devtron Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { AppThemeType } from '@Shared/Providers' - -import { MODES } from '../Constants' - -export interface InformationBarProps { - text: string - className?: string - children?: React.ReactNode -} - -export enum CodeEditorThemesKeys { - vsDarkDT = 'vs-dark--dt', - vs = 'vs', - networkStatusInterface = 'network-status-interface', -} - -interface CodeEditorBaseInterface { - value?: string - lineDecorationsWidth?: number - responseType?: string - onChange?: (value: string, defaultValue: string) => void - onBlur?: () => void - onFocus?: () => void - children?: any - defaultValue?: string - mode?: MODES | string - tabSize?: number - readOnly?: boolean - noParsing?: boolean - inline?: boolean - shebang?: string | JSX.Element - diffView?: boolean - loading?: boolean - customLoader?: JSX.Element - theme?: CodeEditorThemesKeys - original?: string - focus?: boolean - validatorSchema?: any - isKubernetes?: boolean - cleanData?: boolean - schemaURI?: string - /** - * If true, disable the in-built search of monaco editor - * @default false - */ - disableSearch?: boolean - /** - * If true, Enable original value editing of monaco editor - * @default false - */ - originalEditable?: boolean -} - -export type CodeEditorInterface = CodeEditorBaseInterface & - ( - | { - adjustEditorHeightToContent?: boolean - height?: never - } - | { - adjustEditorHeightToContent?: never - height?: number | string - } - ) - -export interface CodeEditorHeaderInterface { - children?: any - className?: string - hideDefaultSplitHeader?: boolean -} -export interface CodeEditorComposition { - Header?: React.FC - LanguageChanger?: React.FC - ThemeChanger?: React.FC - ValidationError?: React.FC - Clipboard?: React.FC - Warning?: React.FC - ErrorBar?: React.FC - Information?: React.FC - Container?: React.FC<{ children: React.ReactNode; flexExpand?: boolean; overflowHidden?: boolean }> -} -export interface CodeEditorHeaderComposition { - LanguageChanger?: React.FC - ThemeChanger?: React.FC - ValidationError?: React.FC - Clipboard?: React.FC -} - -type ActionTypes = 'changeLanguage' | 'setDiff' | 'setTheme' | 'setCode' | 'setDefaultCode' | 'setHeight' - -export interface Action { - type: ActionTypes - value: any -} - -export interface CodeEditorInitialValueType extends Pick { - mode: string - diffView: boolean - value: string - defaultValue: string - noParsing?: boolean - tabSize: number - appTheme: AppThemeType -} - -export interface CodeEditorState { - mode: MODES - diffMode: boolean - theme: CodeEditorThemesKeys - code: string - defaultCode: string - noParsing: boolean -} - -export enum CodeEditorActionTypes { - reInit = 'reInit', - submitLoading = 'submitLoading', - overrideLoading = 'overrideLoading', - success = 'success', -} diff --git a/src/Common/CodeEditor/utils.ts b/src/Common/CodeEditor/utils.ts deleted file mode 100644 index 675519195..000000000 --- a/src/Common/CodeEditor/utils.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (c) 2024. Devtron Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { AppThemeType } from '@Shared/Providers' - -import { CodeEditorInterface, CodeEditorThemesKeys } from './types' - -export const getCodeEditorThemeFromAppTheme = ( - editorTheme: CodeEditorInterface['theme'], - appTheme: AppThemeType, -): CodeEditorInterface['theme'] => { - if (!editorTheme) { - const editorThemeBasedOnAppTheme = - appTheme === AppThemeType.dark ? CodeEditorThemesKeys.vsDarkDT : CodeEditorThemesKeys.vs - return editorThemeBasedOnAppTheme - } - - return editorTheme -} diff --git a/src/Common/Common.service.ts b/src/Common/Common.service.ts index d94cd279c..c7d1a3f13 100644 --- a/src/Common/Common.service.ts +++ b/src/Common/Common.service.ts @@ -22,11 +22,7 @@ import { sanitizeUserApprovalList, stringComparatorBySortOrder, } from '@Shared/Helpers' -import { - PolicyBlockInfo, - RuntimeParamsAPIResponseType, - RuntimePluginVariables, -} from '@Shared/types' +import { PolicyBlockInfo, RuntimeParamsAPIResponseType, RuntimePluginVariables } from '@Shared/types' import { GitProviderType, ROUTES } from './Constants' import { getUrlWithSearchParams, sortCallback } from './Helper' import { @@ -49,10 +45,14 @@ import { GlobalVariableDTO, GlobalVariableOptionType, UserRole, + EnvAppsMetaDTO, + GetAppsInfoForEnvProps, + AppMeta, } from './Types' import { ApiResourceType, STAGE_MAP } from '../Pages' import { RefVariableType, VariableTypeFormat } from './CIPipeline.Types' import { get, post } from './API' +import { StatusType } from '@Shared/Components' export const getTeamListMin = (): Promise => { // ignore active field @@ -264,7 +264,7 @@ export const parseRuntimeParams = (response: RuntimeParamsAPIResponseType): Runt const runtimeParams = (response?.runtimePluginVariables ?? []).map((variable) => ({ ...variable, defaultValue: variable.value, - stepVariableId: variable.stepVariableId || Math.floor(new Date().valueOf() * Math.random()) + stepVariableId: variable.stepVariableId || Math.floor(new Date().valueOf() * Math.random()), })) runtimeParams.push(...envVariables) @@ -515,3 +515,25 @@ export const getGlobalVariables = async ({ throw err } } + +export const getAppsInfoForEnv = async ({ envId, appIds }: GetAppsInfoForEnvProps): Promise => { + const url = getUrlWithSearchParams(`${ROUTES.ENV}/${envId}/${ROUTES.APP_METADATA}`, { + appIds: appIds?.join(), + }) + const response = await get(url) + + return { + appCount: response.result?.appCount ?? 0, + apps: (response.result?.apps ?? []).reduce((agg, { appId, appName, appStatus }) => { + if (!appId) { + return agg + } + agg.push({ + appId, + appName: appName || '', + appStatus: appStatus || StatusType.UNKNOWN, + }) + return agg + }, []), + } +} diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index db6438898..ad1b89398 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -157,6 +157,8 @@ export const ROUTES = { ENVIRONMENT_DATA: 'global/environment-variables', DASHBOARD_EVENT: 'dashboard-event', LICENSE_DATA: 'license/data', + ENV: 'env', + APP_METADATA: 'app-metadata', } as const export enum KEY_VALUE { diff --git a/src/Common/EmptyState/emptyState.scss b/src/Common/EmptyState/emptyState.scss index 3e94a442a..37b6e52a2 100644 --- a/src/Common/EmptyState/emptyState.scss +++ b/src/Common/EmptyState/emptyState.scss @@ -16,22 +16,6 @@ .empty-state { height: 100%; - img { - height: 40%; - max-height: 200px; - width: auto; - max-width: 250px; - } - svg { - height: 40%; - max-height: 200px; - width: auto; - } - .button svg { - height: 100%; - max-height: none; - width: 100%; - } h1, h2, h3, diff --git a/src/Common/Types.ts b/src/Common/Types.ts index fd9d9700f..9f67632bd 100644 --- a/src/Common/Types.ts +++ b/src/Common/Types.ts @@ -27,6 +27,7 @@ import { TargetPlatformItemDTO, ButtonProps, ComponentLayoutType, + StatusType, } from '../Shared' import { ACTION_STATE, @@ -230,25 +231,27 @@ interface InfoColourBarTextConfigType { actionButtonConfig?: ButtonProps } -type InfoColourBarMessageProp = { - message: ReactNode - linkText?: ReactNode - redirectLink?: string - linkOnClick?: () => void - linkClass?: string - internalLink?: boolean - - textConfig?: never -} | { - textConfig: InfoColourBarTextConfigType - - message?: never - linkText?: never - redirectLink?: never - linkOnClick?: () => never - linkClass?: never - internalLink?: never -} +type InfoColourBarMessageProp = + | { + message: ReactNode + linkText?: ReactNode + redirectLink?: string + linkOnClick?: () => void + linkClass?: string + internalLink?: boolean + + textConfig?: never + } + | { + textConfig: InfoColourBarTextConfigType + + message?: never + linkText?: never + redirectLink?: never + linkOnClick?: () => never + linkClass?: never + internalLink?: never + } export type InfoColourBarType = InfoColourBarMessageProp & { classname: string @@ -847,7 +850,7 @@ export interface Strategy { default?: boolean } -export interface CDStage extends Partial> { +export interface CDStage extends Partial> { status: string name: string triggerType: 'AUTOMATIC' | 'MANUAL' @@ -859,7 +862,9 @@ export interface CDStageConfigMapSecretNames { secrets: any[] } -export interface PrePostDeployStageType extends MandatoryPluginBaseStateType, Partial> { +export interface PrePostDeployStageType + extends MandatoryPluginBaseStateType, + Partial> { isValid: boolean steps: TaskErrorObj[] triggerType: string @@ -1066,16 +1071,6 @@ export interface EnvironmentHelmResult { export type EnvironmentListHelmResponse = ResponseType -export interface WidgetEventDetails { - message: string - namespace: string - object: string - source: string - count: number - age: string - lastSeen: string -} - export interface GlobalVariableDTO { name: string format: VariableTypeFormat @@ -1109,3 +1104,19 @@ export enum ActionTypes { EDIT = 'edit', APPROVER = 'approver', } + +export interface GetAppsInfoForEnvProps { + envId: number + appIds?: number[] +} + +export interface AppMeta { + appId: number + appStatus: StatusType + appName: string +} + +export interface EnvAppsMetaDTO { + appCount: number + apps: AppMeta[] +} diff --git a/src/Common/index.ts b/src/Common/index.ts index a1aadbe5a..1a91abe1f 100644 --- a/src/Common/index.ts +++ b/src/Common/index.ts @@ -22,7 +22,6 @@ export { default as ChartVersionAndTypeSelector } from './ChartVersionAndTypeSel export * from './Checkbox' export * from './CIPipeline.Types' export { ClipboardButton } from './ClipboardButton/ClipboardButton' -export * from './CodeEditor/types' export * from './Common.service' export * from './Constants' export * from './CustomTagSelector' diff --git a/src/Pages/GlobalConfigurations/BuildInfra/constants.tsx b/src/Pages/GlobalConfigurations/BuildInfra/constants.tsx index f2c82d192..69316efa4 100644 --- a/src/Pages/GlobalConfigurations/BuildInfra/constants.tsx +++ b/src/Pages/GlobalConfigurations/BuildInfra/constants.tsx @@ -14,11 +14,11 @@ * limitations under the License. */ -import { ReactComponent as ICCpu } from '@Icons/ic-cpu.svg' -import { ReactComponent as ICMemory } from '@Icons/ic-memory.svg' import { ReactComponent as ICSprayCan } from '@Icons/ic-spray-can.svg' import { ReactComponent as ICTag } from '@Icons/ic-tag.svg' import { ReactComponent as ICTimer } from '@Icons/ic-timer.svg' +import { ReactComponent as ICCpu } from '@IconsV2/ic-cpu.svg' +import { ReactComponent as ICMemory } from '@IconsV2/ic-memory.svg' import { UseBreadcrumbProps } from '@Common/BreadCrumb/Types' import { CMSecretComponentType } from '@Shared/Services' diff --git a/src/Shared/Components/AboutDevtron/AboutDevtronBody.tsx b/src/Shared/Components/AboutDevtron/AboutDevtronBody.tsx new file mode 100644 index 000000000..f5bd435de --- /dev/null +++ b/src/Shared/Components/AboutDevtron/AboutDevtronBody.tsx @@ -0,0 +1,79 @@ +import ReactGA from 'react-ga4' + +import DevtronCopyright from '@Common/DevtronCopyright' +import { EULA_LINK, PRIVACY_POLICY_LINK, TERMS_OF_USE_LINK } from '@Shared/constants' +import { useMainContext } from '@Shared/Providers' + +import { Button, ButtonComponentType, ButtonStyleType, ButtonVariantType } from '../Button' +import { InstallationType } from '../Header/types' +import { Icon } from '../Icon' + +const AboutDevtronBody = ({ isFELibAvailable }: { isFELibAvailable: boolean }) => { + const { currentServerInfo } = useMainContext() + + const currentVersion = currentServerInfo?.serverInfo?.currentVersion + const isEnterprise = currentServerInfo?.serverInfo?.installationType === InstallationType.ENTERPRISE + + const isVersionCompatible = isFELibAvailable === isEnterprise + + const handleEULAClick = () => { + ReactGA.event({ + category: 'about-devtron', + action: 'ABOUT_DEVTRON_LICENSE_AGREEMENT_CLICKED', + }) + } + + return ( +
+
+
+ +
+
+

Devtron

+ {isVersionCompatible && ( +

{`${isEnterprise ? 'Enterprise' : 'OSS'} Version${currentVersion ? `(${currentVersion})` : ''}`}

+ )} +
+ +
+
+
+
+ ) +} + +export default AboutDevtronBody diff --git a/src/Shared/Components/AboutDevtron/AboutDevtronDialog.tsx b/src/Shared/Components/AboutDevtron/AboutDevtronDialog.tsx new file mode 100644 index 000000000..1b48138e6 --- /dev/null +++ b/src/Shared/Components/AboutDevtron/AboutDevtronDialog.tsx @@ -0,0 +1,31 @@ +import { ComponentSizeType } from '@Shared/constants' + +import { Backdrop } from '../Backdrop' +import { Button } from '../Button' +import AboutDevtronBody from './AboutDevtronBody' + +const AboutDevtronDialog = ({ + handleCloseLicenseInfoDialog, + isFELibAvailable, +}: { + handleCloseLicenseInfoDialog: () => void + isFELibAvailable: boolean +}) => ( + +
+
+ +
+
+
+
+
+) + +export default AboutDevtronDialog diff --git a/src/Shared/Components/AboutDevtron/index.tsx b/src/Shared/Components/AboutDevtron/index.tsx new file mode 100644 index 000000000..70550e75a --- /dev/null +++ b/src/Shared/Components/AboutDevtron/index.tsx @@ -0,0 +1,2 @@ +export { default as AboutDevtronBody } from './AboutDevtronBody' +export { default as AboutDevtronDialog } from './AboutDevtronDialog' diff --git a/src/Shared/Components/AnimatedDeployButton/AnimatedDeployButton.tsx b/src/Shared/Components/AnimatedDeployButton/AnimatedDeployButton.tsx index f8dea6c50..257f5d907 100644 --- a/src/Shared/Components/AnimatedDeployButton/AnimatedDeployButton.tsx +++ b/src/Shared/Components/AnimatedDeployButton/AnimatedDeployButton.tsx @@ -17,16 +17,22 @@ import { SyntheticEvent, useEffect, useRef, useState } from 'react' import { motion } from 'framer-motion' -import { ReactComponent as ICDeploy } from '@Icons/ic-nav-rocket.svg' import DeployAudio from '@Sounds/DeployAudio.mp3' import { ComponentSizeType } from '@Shared/constants' -import { Button } from '../Button' +import { Button, ButtonStyleType } from '../Button' +import { Icon } from '../Icon' import { AnimatedDeployButtonProps } from './types' import './animatedDeployButton.scss' -const AnimatedDeployButton = ({ isVirtualEnvironment, onButtonClick }: AnimatedDeployButtonProps) => { +const AnimatedDeployButton = ({ + isLoading, + isVirtualEnvironment, + onButtonClick, + exceptionUserConfig, + isBulkCDTrigger, +}: AnimatedDeployButtonProps) => { const audioRef = useRef(null) const timeoutRef = useRef>(null) const isAudioEnabled: boolean = window._env_.FEATURE_ACTION_AUDIOS_ENABLE @@ -36,6 +42,7 @@ const AnimatedDeployButton = ({ isVirtualEnvironment, onButtonClick }: AnimatedD rotate: 45, }, } + const isExceptionUser = exceptionUserConfig?.canDeploy || exceptionUserConfig?.isImageApprover const handleButtonClick = async (e: SyntheticEvent) => { if (clicked) { @@ -66,7 +73,12 @@ const AnimatedDeployButton = ({ isVirtualEnvironment, onButtonClick }: AnimatedD
)} - {headerMetaDataPresent && - renderCIListHeader && + {showCIListHeader && renderCIListHeader({ userApprovalMetadata, triggeredBy, @@ -92,11 +94,12 @@ export const CIListItem = ({ appliedFiltersTimestamp, promotionApprovalMetadata, selectedEnvironmentName, + isDeploymentWithoutApproval, })}
diff --git a/src/Shared/Components/CICDHistory/DeploymentDetailSteps.tsx b/src/Shared/Components/CICDHistory/DeploymentDetailSteps.tsx index 08f56b220..115cab8cf 100644 --- a/src/Shared/Components/CICDHistory/DeploymentDetailSteps.tsx +++ b/src/Shared/Components/CICDHistory/DeploymentDetailSteps.tsx @@ -43,6 +43,7 @@ const DeploymentDetailSteps = ({ isVirtualEnvironment, processVirtualEnvironmentDeploymentData, renderDeploymentApprovalInfo, + isDeploymentWithoutApproval, }: DeploymentDetailStepsType) => { const history = useHistory() const { url } = useRouteMatch() @@ -51,6 +52,8 @@ const DeploymentDetailSteps = ({ deploymentStatus?.toUpperCase() !== TIMELINE_STATUS.ABORTED, ) const isVirtualEnv = useRef(isVirtualEnvironment) + const isDeploymentWithoutApprovalRef = useRef(isDeploymentWithoutApproval) + const processedData = isVirtualEnv.current && processVirtualEnvironmentDeploymentData ? processVirtualEnvironmentDeploymentData() @@ -69,7 +72,10 @@ const DeploymentDetailSteps = ({ .then((deploymentStatusDetailRes) => { if (deploymentStatus !== 'Aborted') { // eslint-disable-next-line no-use-before-define - processDeploymentStatusData(deploymentStatusDetailRes.result) + processDeploymentStatusData({ + ...deploymentStatusDetailRes.result, + isDeploymentWithoutApproval: isDeploymentWithoutApprovalRef.current, + }) } }) .catch(() => { @@ -97,6 +103,10 @@ const DeploymentDetailSteps = ({ isVirtualEnv.current = isVirtualEnvironment }, [isVirtualEnvironment]) + useEffect(() => { + isDeploymentWithoutApprovalRef.current = isDeploymentWithoutApproval + }, [isDeploymentWithoutApproval]) + const processDeploymentStatusData = (deploymentStatusDetailRes: DeploymentStatusDetailsType): void => { const processedDeploymentStatusDetailsData = isVirtualEnv.current && processVirtualEnvironmentDeploymentData diff --git a/src/Shared/Components/CICDHistory/DeploymentHistoryConfigDiff/DeploymentHistoryDiffView.tsx b/src/Shared/Components/CICDHistory/DeploymentHistoryConfigDiff/DeploymentHistoryDiffView.tsx index d121b3519..cc9448003 100644 --- a/src/Shared/Components/CICDHistory/DeploymentHistoryConfigDiff/DeploymentHistoryDiffView.tsx +++ b/src/Shared/Components/CICDHistory/DeploymentHistoryConfigDiff/DeploymentHistoryDiffView.tsx @@ -18,7 +18,7 @@ import { useMemo, useState } from 'react' import { useParams } from 'react-router-dom' import Tippy from '@tippyjs/react' -import { CodeEditor } from '@Shared/Components/CodeEditorWrapper' +import { CodeEditor } from '@Shared/Components/CodeEditor' import { renderDiffViewNoDifferenceState } from '@Shared/Components/DeploymentConfigDiff' import { DiffViewer } from '@Shared/Components/DiffViewer' @@ -79,19 +79,12 @@ const DeploymentHistoryDiffView = ({ ) : ( ) diff --git a/src/Shared/Components/CICDHistory/DeploymentStatusDetailRow.tsx b/src/Shared/Components/CICDHistory/DeploymentStatusDetailRow.tsx index d55511dbd..56417d711 100644 --- a/src/Shared/Components/CICDHistory/DeploymentStatusDetailRow.tsx +++ b/src/Shared/Components/CICDHistory/DeploymentStatusDetailRow.tsx @@ -176,7 +176,9 @@ export const DeploymentStatusDetailRow = ({ > {renderIcon(statusBreakDownType.icon)} - {statusBreakDownType.displayText} + + {statusBreakDownType.displayText} + {statusBreakDownType.displaySubText && ( {statusBreakDownType.displaySubText} diff --git a/src/Shared/Components/CICDHistory/History.components.tsx b/src/Shared/Components/CICDHistory/History.components.tsx index 779bf2872..712a6d8d2 100644 --- a/src/Shared/Components/CICDHistory/History.components.tsx +++ b/src/Shared/Components/CICDHistory/History.components.tsx @@ -138,6 +138,7 @@ export const GitChanges = ({ selectedEnvironmentName, renderCIListHeader, targetPlatforms, + isDeploymentWithoutApproval, }: GitChangesType) => { const { isSuperAdmin } = useGetUserRoles() @@ -202,6 +203,7 @@ export const GitChanges = ({ selectedEnvironmentName={selectedEnvironmentName} renderCIListHeader={renderCIListHeader} targetPlatforms={targetPlatforms} + isDeploymentWithoutApproval={isDeploymentWithoutApproval} >
diff --git a/src/Shared/Components/CICDHistory/TriggerOutput.tsx b/src/Shared/Components/CICDHistory/TriggerOutput.tsx index 404129be8..79690560d 100644 --- a/src/Shared/Components/CICDHistory/TriggerOutput.tsx +++ b/src/Shared/Components/CICDHistory/TriggerOutput.tsx @@ -133,6 +133,7 @@ const HistoryLogs: React.FC = ({ isVirtualEnvironment={triggerDetails.IsVirtualEnvironment} processVirtualEnvironmentDeploymentData={processVirtualEnvironmentDeploymentData} renderDeploymentApprovalInfo={renderDeploymentApprovalInfo} + isDeploymentWithoutApproval={triggerDetails.isDeploymentWithoutApproval ?? false} /> )} @@ -156,6 +157,7 @@ const HistoryLogs: React.FC = ({ promotionApprovalMetadata={triggerDetails?.promotionApprovalMetadata} renderCIListHeader={renderCIListHeader} targetPlatforms={targetPlatforms} + isDeploymentWithoutApproval={triggerDetails.isDeploymentWithoutApproval ?? false} /> {triggerDetails.stage === 'DEPLOY' && ( diff --git a/src/Shared/Components/CICDHistory/types.tsx b/src/Shared/Components/CICDHistory/types.tsx index f34a00765..417f5ed8b 100644 --- a/src/Shared/Components/CICDHistory/types.tsx +++ b/src/Shared/Components/CICDHistory/types.tsx @@ -187,6 +187,7 @@ export interface History extends Pick, Wo triggerMetadata?: string runSource?: RunSourceType targetConfig?: TargetConfigType + isDeploymentWithoutApproval?: boolean } export interface ExecutionInfoType { @@ -389,7 +390,7 @@ export interface DeploymentStatusDetailsTimelineType { statusTime: string resourceDetails?: SyncStageResourceDetail[] } -export interface DeploymentStatusDetailsType { +export interface DeploymentStatusDetailsType extends Pick { deploymentFinishedOn: string deploymentStartedOn: string triggeredBy: string @@ -405,7 +406,7 @@ export interface DeploymentStatusDetailsResponse extends ResponseType { interface DeploymentStatusDetailRow { icon: string - displayText: string + displayText: ReactNode displaySubText: string time: string resourceDetails?: any @@ -431,7 +432,7 @@ export interface DeploymentStatusDetailsBreakdownDataType { } } -export interface DeploymentDetailStepsType { +export interface DeploymentDetailStepsType extends Pick { deploymentStatus?: string deploymentAppType?: DeploymentAppTypes isHelmApps?: boolean @@ -445,7 +446,7 @@ export interface DeploymentDetailStepsType { renderDeploymentApprovalInfo: (userApprovalMetadata: UserApprovalMetadataType) => JSX.Element } -export interface RenderCIListHeaderProps { +export interface RenderCIListHeaderProps extends Required> { userApprovalMetadata: UserApprovalMetadataType triggeredBy: string appliedFilters: FilterConditionsListType[] @@ -465,7 +466,7 @@ export interface VirtualHistoryArtifactProps { } } -export type CIListItemType = Pick & { +export type CIListItemType = Pick & { userApprovalMetadata?: UserApprovalMetadataType triggeredBy?: string children: ReactNode @@ -737,6 +738,7 @@ export type GitChangesType = { appliedFilters?: never appliedFiltersTimestamp?: never renderCIListHeader?: never + isDeploymentWithoutApproval?: never } | { artifact: string @@ -755,6 +757,7 @@ export type GitChangesType = { appliedFilters?: FilterConditionsListType[] appliedFiltersTimestamp?: string renderCIListHeader: (renderCIListHeaderProps: RenderCIListHeaderProps) => JSX.Element + isDeploymentWithoutApproval?: History['isDeploymentWithoutApproval'] } ) diff --git a/src/Shared/Components/CMCS/ConfigMapSecretReadyOnly.tsx b/src/Shared/Components/CMCS/ConfigMapSecretReadyOnly.tsx index 2d37a3c96..2fd444652 100644 --- a/src/Shared/Components/CMCS/ConfigMapSecretReadyOnly.tsx +++ b/src/Shared/Components/CMCS/ConfigMapSecretReadyOnly.tsx @@ -18,7 +18,7 @@ import { MODES } from '@Common/Constants' import { Progressing } from '@Common/Progressing' import { hasHashiOrAWS } from '@Pages/index' -import { CodeEditor } from '../CodeEditorWrapper' +import { CodeEditor } from '../CodeEditor' import { renderHashiOrAwsDeprecatedInfo } from './helpers' import { ConfigMapSecretReadyOnlyProps } from './types' import { getConfigMapSecretReadOnlyValues } from './utils' @@ -68,19 +68,7 @@ const ConfigMapSecretReadyOnly = ({
{!hideCodeEditor && displayValues.data && ( - +

Data

diff --git a/src/Shared/Components/CMCS/constants.ts b/src/Shared/Components/CMCS/constants.ts index d24b0012f..616ccb1e0 100644 --- a/src/Shared/Components/CMCS/constants.ts +++ b/src/Shared/Components/CMCS/constants.ts @@ -14,8 +14,9 @@ * limitations under the License. */ -import { CMSecretExternalType, CMSecretYamlData } from '@Shared/Services' +import { CMSecretExternalType } from '@Shared/Services' +import { KeyValueTableData } from '../KeyValueTable' import { ConfigMapSecretDataTypeOptionType } from './types' export const CONFIG_MAP_SECRET_YAML_PARSE_ERROR = 'Please provide valid YAML' @@ -33,7 +34,7 @@ export const configMapDataTypeOptions: ConfigMapSecretDataTypeOptionType[] = [ { value: CMSecretExternalType.KubernetesConfigMap, label: 'Kubernetes External ConfigMap' }, ] -export const CONFIG_MAP_SECRET_DEFAULT_CURRENT_DATA: CMSecretYamlData[] = [{ k: '', v: '', id: 0 }] +export const CONFIG_MAP_SECRET_DEFAULT_CURRENT_DATA: KeyValueTableData[] = [{ key: '', value: '', id: 0 }] export const configMapSecretMountDataMap = { environment: { title: 'Environment Variable', value: 'environment' }, diff --git a/src/Shared/Components/CMCS/utils.ts b/src/Shared/Components/CMCS/utils.ts index 2d5347d49..6222d34c2 100644 --- a/src/Shared/Components/CMCS/utils.ts +++ b/src/Shared/Components/CMCS/utils.ts @@ -26,7 +26,6 @@ import { CMSecretConfigData, CMSecretExternalType, CMSecretPayloadType, - CMSecretYamlData, CODE_EDITOR_RADIO_STATE, ConfigDatum, ConfigMapSecretUseFormProps, @@ -36,6 +35,7 @@ import { } from '@Shared/Services' import { hasESO, OverrideMergeStrategyType } from '@Pages/index' +import { KeyValueTableData } from '../KeyValueTable' import { getSelectPickerOptionByValue } from '../SelectPicker' import { CONFIG_MAP_SECRET_DEFAULT_CURRENT_DATA, @@ -93,7 +93,7 @@ export const getSecretDataTypeOptions = ( return isJob ? kubernetesOptions : [...kubernetesOptions, ...esoOptions, ...(isHashiOrAWS ? kesOptions : [])] } -const secureValues = (data: Record, decodeData: boolean): CMSecretYamlData[] => { +const secureValues = (data: Record, decodeData: boolean): KeyValueTableData[] => { let decodedData = data || DEFAULT_SECRET_PLACEHOLDER if (decodeData) { @@ -104,9 +104,9 @@ const secureValues = (data: Record, decodeData: boolean): CMSecr } } - return Object.keys(decodedData).map((k, id) => ({ - k, - v: typeof decodedData[k] === 'object' ? YAMLStringify(decodedData[k]) : decodedData[k], + return Object.keys(decodedData).map((key, id) => ({ + key, + value: typeof decodedData[key] === 'object' ? YAMLStringify(decodedData[key]) : decodedData[key], id, })) } @@ -150,8 +150,8 @@ const processExternalSubPathValues = ({ return '' } -export const convertKeyValuePairToYAML = (currentData: CMSecretYamlData[]) => - currentData.length ? YAMLStringify(currentData.reduce((agg, { k, v }) => ({ ...agg, [k]: v }), {})) : '' +export const convertKeyValuePairToYAML = (currentData: KeyValueTableData[]) => + currentData.length ? YAMLStringify(currentData.reduce((agg, { key, value }) => ({ ...agg, [key]: value }), {})) : '' const getSecretDataFromConfigData = ({ secretData, @@ -368,29 +368,30 @@ export const getConfigMapSecretReadOnlyValues = ({ ? [ { displayName: 'Keys', - value: currentData?.length > 0 ? currentData.map((d) => d.k).join(', ') : 'No keys available', + value: + currentData?.length > 0 ? currentData.map((d) => d.key).join(', ') : 'No keys available', key: 'keys', }, ] : []), ], - data: !mountExistingExternal ? (currentData?.[0]?.k && yaml) || esoSecretYaml || secretDataYaml : null, + data: !mountExistingExternal ? (currentData?.[0]?.key && yaml) || esoSecretYaml || secretDataYaml : null, } } -export const convertYAMLToKeyValuePair = (yaml: string): CMSecretYamlData[] => { +export const convertYAMLToKeyValuePair = (yaml: string): KeyValueTableData[] => { try { const obj = yaml && YAML.parse(yaml) if (typeof obj !== 'object') { throw new Error() } - const keyValueArray: CMSecretYamlData[] = Object.keys(obj).reduce((agg, k, id) => { - if (!k && !obj[k]) { + const keyValueArray = Object.keys(obj).reduce((agg, key, id) => { + if (!key && !obj[key]) { return CONFIG_MAP_SECRET_DEFAULT_CURRENT_DATA } - const v = obj[k] && typeof obj[k] === 'object' ? YAMLStringify(obj[k]) : obj[k].toString() + const value = obj[key] && typeof obj[key] === 'object' ? YAMLStringify(obj[key]) : obj[key].toString() - return [...agg, { k, v: v ?? '', id }] + return [...agg, { key, value: value ?? '', id }] }, []) return keyValueArray } catch { @@ -449,14 +450,14 @@ export const getConfigMapSecretPayload = ({ const isESO = isSecret && hasESO(externalType) const _currentData = yamlMode ? convertYAMLToKeyValuePair(yaml) : currentData const data = _currentData.reduce((acc, curr) => { - if (!curr.k) { + if (!curr.key) { return acc } - const value = curr.v ?? '' + const value = curr.value ?? '' return { ...acc, - [curr.k]: isSecret && externalType === '' ? btoa(value) : value, + [curr.key]: isSecret && externalType === '' ? btoa(value) : value, } }, {}) diff --git a/src/Shared/Components/CMCS/validations.ts b/src/Shared/Components/CMCS/validations.ts index f1858649a..fa76dfe12 100644 --- a/src/Shared/Components/CMCS/validations.ts +++ b/src/Shared/Components/CMCS/validations.ts @@ -19,10 +19,11 @@ import YAML from 'yaml' import { PATTERNS } from '@Common/Constants' import { YAMLStringify } from '@Common/Helper' import { UseFormValidation, UseFormValidations } from '@Shared/Hooks' -import { CMSecretExternalType, CMSecretYamlData, ConfigMapSecretUseFormProps } from '@Shared/Services' +import { CMSecretExternalType, ConfigMapSecretUseFormProps } from '@Shared/Services' import { validateCMVolumeMountPath } from '@Shared/validations' import { hasESO } from '@Pages/index' +import { KeyValueTableData } from '../KeyValueTable' import { CONFIG_MAP_SECRET_YAML_PARSE_ERROR, SECRET_TOAST_INFO } from './constants' import { getESOSecretDataFromYAML } from './utils' @@ -310,7 +311,7 @@ export const getConfigMapSecretFormValidations: UseFormValidations !!value.filter(({ k }) => !!k).length, + isValid: (value: KeyValueTableData[]) => !!value.filter(({ key }) => !!key).length, message: 'This is a required field', }, }, diff --git a/src/Common/CodeMirror/CodeEditor.components.tsx b/src/Shared/Components/CodeEditor/CodeEditor.components.tsx similarity index 100% rename from src/Common/CodeMirror/CodeEditor.components.tsx rename to src/Shared/Components/CodeEditor/CodeEditor.components.tsx diff --git a/src/Common/CodeMirror/CodeEditor.constants.ts b/src/Shared/Components/CodeEditor/CodeEditor.constants.ts similarity index 100% rename from src/Common/CodeMirror/CodeEditor.constants.ts rename to src/Shared/Components/CodeEditor/CodeEditor.constants.ts diff --git a/src/Common/CodeMirror/CodeEditor.context.ts b/src/Shared/Components/CodeEditor/CodeEditor.context.ts similarity index 100% rename from src/Common/CodeMirror/CodeEditor.context.ts rename to src/Shared/Components/CodeEditor/CodeEditor.context.ts diff --git a/src/Common/CodeMirror/CodeEditor.theme.ts b/src/Shared/Components/CodeEditor/CodeEditor.theme.ts similarity index 100% rename from src/Common/CodeMirror/CodeEditor.theme.ts rename to src/Shared/Components/CodeEditor/CodeEditor.theme.ts diff --git a/src/Common/CodeMirror/CodeEditor.tsx b/src/Shared/Components/CodeEditor/CodeEditor.tsx similarity index 100% rename from src/Common/CodeMirror/CodeEditor.tsx rename to src/Shared/Components/CodeEditor/CodeEditor.tsx diff --git a/src/Common/CodeMirror/CodeEditorRenderer.tsx b/src/Shared/Components/CodeEditor/CodeEditorRenderer.tsx similarity index 100% rename from src/Common/CodeMirror/CodeEditorRenderer.tsx rename to src/Shared/Components/CodeEditor/CodeEditorRenderer.tsx diff --git a/src/Common/CodeMirror/Commands/findAndReplace.ts b/src/Shared/Components/CodeEditor/Commands/findAndReplace.ts similarity index 100% rename from src/Common/CodeMirror/Commands/findAndReplace.ts rename to src/Shared/Components/CodeEditor/Commands/findAndReplace.ts diff --git a/src/Common/CodeMirror/Commands/index.ts b/src/Shared/Components/CodeEditor/Commands/index.ts similarity index 100% rename from src/Common/CodeMirror/Commands/index.ts rename to src/Shared/Components/CodeEditor/Commands/index.ts diff --git a/src/Common/CodeMirror/Commands/keyMaps.ts b/src/Shared/Components/CodeEditor/Commands/keyMaps.ts similarity index 100% rename from src/Common/CodeMirror/Commands/keyMaps.ts rename to src/Shared/Components/CodeEditor/Commands/keyMaps.ts diff --git a/src/Common/CodeMirror/Extensions/DiffMinimap.tsx b/src/Shared/Components/CodeEditor/Extensions/DiffMinimap.tsx similarity index 100% rename from src/Common/CodeMirror/Extensions/DiffMinimap.tsx rename to src/Shared/Components/CodeEditor/Extensions/DiffMinimap.tsx diff --git a/src/Common/CodeMirror/Extensions/findAndReplace.tsx b/src/Shared/Components/CodeEditor/Extensions/findAndReplace.tsx similarity index 100% rename from src/Common/CodeMirror/Extensions/findAndReplace.tsx rename to src/Shared/Components/CodeEditor/Extensions/findAndReplace.tsx diff --git a/src/Common/CodeMirror/Extensions/index.ts b/src/Shared/Components/CodeEditor/Extensions/index.ts similarity index 100% rename from src/Common/CodeMirror/Extensions/index.ts rename to src/Shared/Components/CodeEditor/Extensions/index.ts diff --git a/src/Common/CodeMirror/Extensions/readOnlyTooltip.ts b/src/Shared/Components/CodeEditor/Extensions/readOnlyTooltip.ts similarity index 100% rename from src/Common/CodeMirror/Extensions/readOnlyTooltip.ts rename to src/Shared/Components/CodeEditor/Extensions/readOnlyTooltip.ts diff --git a/src/Common/CodeMirror/Extensions/yamlHighlight.ts b/src/Shared/Components/CodeEditor/Extensions/yamlHighlight.ts similarity index 100% rename from src/Common/CodeMirror/Extensions/yamlHighlight.ts rename to src/Shared/Components/CodeEditor/Extensions/yamlHighlight.ts diff --git a/src/Common/CodeMirror/Extensions/yamlParseLinter.ts b/src/Shared/Components/CodeEditor/Extensions/yamlParseLinter.ts similarity index 100% rename from src/Common/CodeMirror/Extensions/yamlParseLinter.ts rename to src/Shared/Components/CodeEditor/Extensions/yamlParseLinter.ts diff --git a/src/Common/CodeMirror/codeEditor.scss b/src/Shared/Components/CodeEditor/codeEditor.scss similarity index 99% rename from src/Common/CodeMirror/codeEditor.scss rename to src/Shared/Components/CodeEditor/codeEditor.scss index 202fa4ee0..b25e43aef 100644 --- a/src/Common/CodeMirror/codeEditor.scss +++ b/src/Shared/Components/CodeEditor/codeEditor.scss @@ -175,6 +175,10 @@ max-width: 300px; } + .cm-tooltip-autocomplete { + background-color: var(--bg-primary); + } + .cm-diagnostic-error { border-top-left-radius: 2px; border-bottom-left-radius: 2px; diff --git a/src/Common/CodeMirror/index.ts b/src/Shared/Components/CodeEditor/index.ts similarity index 100% rename from src/Common/CodeMirror/index.ts rename to src/Shared/Components/CodeEditor/index.ts diff --git a/src/Common/CodeMirror/types.ts b/src/Shared/Components/CodeEditor/types.ts similarity index 100% rename from src/Common/CodeMirror/types.ts rename to src/Shared/Components/CodeEditor/types.ts diff --git a/src/Common/CodeMirror/utils.tsx b/src/Shared/Components/CodeEditor/utils.tsx similarity index 100% rename from src/Common/CodeMirror/utils.tsx rename to src/Shared/Components/CodeEditor/utils.tsx diff --git a/src/Shared/Components/CodeEditorWrapper/CodeEditorWrapper.tsx b/src/Shared/Components/CodeEditorWrapper/CodeEditorWrapper.tsx deleted file mode 100644 index c28e1bd06..000000000 --- a/src/Shared/Components/CodeEditorWrapper/CodeEditorWrapper.tsx +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (c) 2024. Devtron Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { CodeEditor } from '@Common/CodeEditor' -import { CodeEditor as CodeMirror, CodeEditorHeaderProps, CodeEditorStatusBarProps } from '@Common/CodeMirror' - -import { CodeEditorWrapperProps } from './types' - -export const isCodeMirrorEnabled = () => window._env_.FEATURE_CODE_MIRROR_ENABLE - -export const CodeEditorWrapper = ({ - codeEditorProps, - codeMirrorProps, - children, - ...restProps -}: CodeEditorWrapperProps) => - isCodeMirrorEnabled() ? ( - {...(codeMirrorProps as any)} {...restProps}> - {children} - - ) : ( - - {children} - - ) - -const CodeEditorLanguageChangerWrapper = () => (isCodeMirrorEnabled() ? null : ) - -const CodeEditorThemeChangerWrapper = () => (isCodeMirrorEnabled() ? null : ) - -const CodeEditorValidationErrorWrapper = () => (isCodeMirrorEnabled() ? null : ) - -const CodeEditorClipboardWrapper = () => (isCodeMirrorEnabled() ? : ) - -const CodeEditorHeaderWrapper = (props: CodeEditorHeaderProps) => - isCodeMirrorEnabled() ? : - -const CodeEditorWarningWrapper = (props: CodeEditorStatusBarProps) => - isCodeMirrorEnabled() ? : - -const CodeEditorErrorBarWrapper = (props: CodeEditorStatusBarProps) => - isCodeMirrorEnabled() ? : - -const CodeEditorInformationWrapper = (props: CodeEditorStatusBarProps) => - isCodeMirrorEnabled() ? : - -const CodeEditorContainerWrapper = ({ - overflowHidden, - ...props -}: { - children: React.ReactNode - flexExpand?: boolean - overflowHidden?: boolean -}) => - isCodeMirrorEnabled() ? ( - - ) : ( - - ) - -CodeEditorWrapper.LanguageChanger = CodeEditorLanguageChangerWrapper -CodeEditorWrapper.ThemeChanger = CodeEditorThemeChangerWrapper -CodeEditorWrapper.ValidationError = CodeEditorValidationErrorWrapper -CodeEditorWrapper.Clipboard = CodeEditorClipboardWrapper -CodeEditorWrapper.Header = CodeEditorHeaderWrapper -CodeEditorWrapper.Warning = CodeEditorWarningWrapper -CodeEditorWrapper.ErrorBar = CodeEditorErrorBarWrapper -CodeEditorWrapper.Information = CodeEditorInformationWrapper -CodeEditorWrapper.Container = CodeEditorContainerWrapper diff --git a/src/Shared/Components/CodeEditorWrapper/index.ts b/src/Shared/Components/CodeEditorWrapper/index.ts deleted file mode 100644 index 0a036a17c..000000000 --- a/src/Shared/Components/CodeEditorWrapper/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright (c) 2024. Devtron Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { CodeEditorWrapper as CodeEditor, isCodeMirrorEnabled } from './CodeEditorWrapper' diff --git a/src/Shared/Components/CodeEditorWrapper/types.ts b/src/Shared/Components/CodeEditorWrapper/types.ts deleted file mode 100644 index b69233712..000000000 --- a/src/Shared/Components/CodeEditorWrapper/types.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (c) 2024. Devtron Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { CodeEditorInterface } from '@Common/CodeEditor' -import { CodeEditorProps } from '@Common/CodeMirror' - -export type CodeEditorWrapperProps = Pick< - CodeEditorProps, - | 'mode' - | 'tabSize' - | 'readOnly' - | 'placeholder' - | 'noParsing' - | 'loading' - | 'customLoader' - | 'cleanData' - | 'disableSearch' - | 'children' -> & { - diffView?: DiffView - codeEditorProps: Omit< - CodeEditorInterface, - | 'mode' - | 'tabSize' - | 'readOnly' - | 'placeholder' - | 'noParsing' - | 'loading' - | 'customLoader' - | 'cleanData' - | 'disableSearch' - | 'children' - > - codeMirrorProps: Omit< - CodeEditorProps, - | 'mode' - | 'tabSize' - | 'readOnly' - | 'placeholder' - | 'noParsing' - | 'loading' - | 'customLoader' - | 'cleanData' - | 'disableSearch' - | 'children' - > -} diff --git a/src/Shared/Components/DynamicDataTable/DynamicDataTable.tsx b/src/Shared/Components/DynamicDataTable/DynamicDataTable.tsx index b98cf41bd..95349fc75 100644 --- a/src/Shared/Components/DynamicDataTable/DynamicDataTable.tsx +++ b/src/Shared/Components/DynamicDataTable/DynamicDataTable.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { useMemo } from 'react' +import { useMemo, useState } from 'react' import { DynamicDataTableHeader } from './DynamicDataTableHeader' import { DynamicDataTableRow } from './DynamicDataTableRow' @@ -24,14 +24,30 @@ import './styles.scss' export const DynamicDataTable = >({ headers, + onRowAdd, ...props }: DynamicDataTableProps) => { + // STATES + const [isAddRowButtonClicked, setIsAddRowButtonClicked] = useState(false) + + // CONSTANTS const filteredHeaders = useMemo(() => headers.filter(({ isHidden }) => !isHidden), [headers]) + // HANDLERS + const handleRowAdd = () => { + setIsAddRowButtonClicked(true) + onRowAdd() + } + return (
- - + +
) } diff --git a/src/Shared/Components/DynamicDataTable/DynamicDataTableHeader.tsx b/src/Shared/Components/DynamicDataTable/DynamicDataTableHeader.tsx index a6d59cb91..e0f6eb83a 100644 --- a/src/Shared/Components/DynamicDataTable/DynamicDataTableHeader.tsx +++ b/src/Shared/Components/DynamicDataTable/DynamicDataTableHeader.tsx @@ -27,6 +27,7 @@ export const DynamicDataTableHeader = } variant={ButtonVariantType.borderLess} size={ComponentSizeType.xs} - showAriaLabelInTippy={false} /> )} {key === lastHeaderKey && headerComponent} diff --git a/src/Shared/Components/DynamicDataTable/DynamicDataTableRow.tsx b/src/Shared/Components/DynamicDataTable/DynamicDataTableRow.tsx index 3904c5147..45b141d80 100644 --- a/src/Shared/Components/DynamicDataTable/DynamicDataTableRow.tsx +++ b/src/Shared/Components/DynamicDataTable/DynamicDataTableRow.tsx @@ -73,6 +73,8 @@ export const DynamicDataTableRow = ) => { // CONSTANTS const isFirstRowEmpty = headers.every(({ key }) => !rows[0]?.data[key].value) @@ -105,6 +107,8 @@ export const DynamicDataTableRow = { setIsRowAdded(rows.length > 0 && Object.keys(cellRef.current).length < rows.length) + // When a new row is added, we create references for its cells. + // This logic ensures that references are created only for the newly added row, while retaining the existing references. const updatedCellRef = rowIds.reduce((acc, curr) => { if (cellRef.current[curr]) { acc[curr] = cellRef.current[curr] @@ -118,15 +122,16 @@ export const DynamicDataTableRow = { - if (isRowAdded) { + if (isAddRowButtonClicked && isRowAdded) { // Using the below logic to ensure the cell is focused after row addition. const cell = cellRef.current[rows[0].id][focusableFieldKey || headers[0].key].current if (cell) { cell.focus() setIsRowAdded(false) + setIsAddRowButtonClicked(false) } } - }, [isRowAdded]) + }, [isRowAdded, isAddRowButtonClicked]) // METHODS const onChange = @@ -290,16 +295,18 @@ export const DynamicDataTableRow = {errorMessages.map((error) => renderErrorMessage(error))}
diff --git a/src/Shared/Components/DynamicDataTable/types.ts b/src/Shared/Components/DynamicDataTable/types.ts index 8d3785d17..3e9574cb1 100644 --- a/src/Shared/Components/DynamicDataTable/types.ts +++ b/src/Shared/Components/DynamicDataTable/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { DetailedHTMLProps, ReactElement, ReactNode } from 'react' +import { DetailedHTMLProps, Dispatch, ReactElement, ReactNode, SetStateAction } from 'react' import { ResizableTagTextAreaProps } from '@Common/CustomTagSelector' import { UseStateFiltersReturnType } from '@Common/Hooks' @@ -169,6 +169,10 @@ export type DynamicDataTableProps void /** @@ -233,6 +237,7 @@ export interface DynamicDataTableHeaderProps {} + > { + isAddRowButtonClicked: boolean + setIsAddRowButtonClicked: Dispatch> +} diff --git a/src/Shared/Components/Header/HelpNav.tsx b/src/Shared/Components/Header/HelpNav.tsx index 1c6cc2251..0916f53f9 100644 --- a/src/Shared/Components/Header/HelpNav.tsx +++ b/src/Shared/Components/Header/HelpNav.tsx @@ -76,7 +76,7 @@ const HelpNav = ({ onClickHelpOptions(option) } - const handleOpenLicenseDialog = () => { + const handleOpenAboutDevtron = () => { ReactGA.event({ category: 'help-nav__about-devtron', action: 'ABOUT_DEVTRON_CLICKED', @@ -100,23 +100,22 @@ const HelpNav = ({
{option.name}
- {/* licenseData is only set when showLicenseData is received true */} - {isEnterprise && index === 1 && ( + {index === 1 && ( <> - {licenseData && ( - + + {isEnterprise && ( +
+ Enterprise Support +
)} -
- Enterprise Support -
)} diff --git a/src/Shared/Components/Icon/Icon.tsx b/src/Shared/Components/Icon/Icon.tsx index 1b68f10d4..5db478d3f 100644 --- a/src/Shared/Components/Icon/Icon.tsx +++ b/src/Shared/Components/Icon/Icon.tsx @@ -23,6 +23,7 @@ import { ReactComponent as ICCaretLeft } from '@IconsV2/ic-caret-left.svg' import { ReactComponent as ICCd } from '@IconsV2/ic-cd.svg' import { ReactComponent as ICChatCircleDots } from '@IconsV2/ic-chat-circle-dots.svg' import { ReactComponent as ICCheck } from '@IconsV2/ic-check.svg' +import { ReactComponent as ICChecks } from '@IconsV2/ic-checks.svg' import { ReactComponent as ICCiLinked } from '@IconsV2/ic-ci-linked.svg' import { ReactComponent as ICCiWebhook } from '@IconsV2/ic-ci-webhook.svg' import { ReactComponent as ICCircleLoader } from '@IconsV2/ic-circle-loader.svg' @@ -33,6 +34,7 @@ import { ReactComponent as ICCode } from '@IconsV2/ic-code.svg' import { ReactComponent as ICContainer } from '@IconsV2/ic-container.svg' import { ReactComponent as ICCookr } from '@IconsV2/ic-cookr.svg' import { ReactComponent as ICCopy } from '@IconsV2/ic-copy.svg' +import { ReactComponent as ICCpu } from '@IconsV2/ic-cpu.svg' import { ReactComponent as ICCrown } from '@IconsV2/ic-crown.svg' import { ReactComponent as ICCube } from '@IconsV2/ic-cube.svg' import { ReactComponent as ICDelete } from '@IconsV2/ic-delete.svg' @@ -55,10 +57,12 @@ import { ReactComponent as ICGoogle } from '@IconsV2/ic-google.svg' import { ReactComponent as ICGoogleArtifactRegistry } from '@IconsV2/ic-google-artifact-registry.svg' import { ReactComponent as ICGoogleContainerRegistry } from '@IconsV2/ic-google-container-registry.svg' import { ReactComponent as ICGridView } from '@IconsV2/ic-grid-view.svg' +import { ReactComponent as ICHandPointing } from '@IconsV2/ic-hand-pointing.svg' import { ReactComponent as ICHeartGreen } from '@IconsV2/ic-heart-green.svg' import { ReactComponent as ICHeartRed } from '@IconsV2/ic-heart-red.svg' import { ReactComponent as ICHeartRedAnimated } from '@IconsV2/ic-heart-red-animated.svg' import { ReactComponent as ICHelm } from '@IconsV2/ic-helm.svg' +import { ReactComponent as ICHelpFilled } from '@IconsV2/ic-help-filled.svg' import { ReactComponent as ICHelpOutline } from '@IconsV2/ic-help-outline.svg' import { ReactComponent as ICHibernate } from '@IconsV2/ic-hibernate.svg' import { ReactComponent as ICInProgress } from '@IconsV2/ic-in-progress.svg' @@ -66,6 +70,7 @@ import { ReactComponent as ICInfoFilled } from '@IconsV2/ic-info-filled.svg' import { ReactComponent as ICInfoOutline } from '@IconsV2/ic-info-outline.svg' import { ReactComponent as ICJobColor } from '@IconsV2/ic-job-color.svg' import { ReactComponent as ICK8sJob } from '@IconsV2/ic-k8s-job.svg' +import { ReactComponent as ICKey } from '@IconsV2/ic-key.svg' import { ReactComponent as ICLdap } from '@IconsV2/ic-ldap.svg' import { ReactComponent as ICLightning } from '@IconsV2/ic-lightning.svg' import { ReactComponent as ICLightningFill } from '@IconsV2/ic-lightning-fill.svg' @@ -74,6 +79,7 @@ import { ReactComponent as ICLoginDevtronLogo } from '@IconsV2/ic-login-devtron- import { ReactComponent as ICLogout } from '@IconsV2/ic-logout.svg' import { ReactComponent as ICMediumDelete } from '@IconsV2/ic-medium-delete.svg' import { ReactComponent as ICMediumPaintbucket } from '@IconsV2/ic-medium-paintbucket.svg' +import { ReactComponent as ICMemory } from '@IconsV2/ic-memory.svg' import { ReactComponent as ICMicrosoft } from '@IconsV2/ic-microsoft.svg' import { ReactComponent as ICMissing } from '@IconsV2/ic-missing.svg' import { ReactComponent as ICMonitoring } from '@IconsV2/ic-monitoring.svg' @@ -88,6 +94,8 @@ import { ReactComponent as ICPaperPlaneColor } from '@IconsV2/ic-paper-plane-col import { ReactComponent as ICPencil } from '@IconsV2/ic-pencil.svg' import { ReactComponent as ICQuay } from '@IconsV2/ic-quay.svg' import { ReactComponent as ICQuote } from '@IconsV2/ic-quote.svg' +import { ReactComponent as ICRocketLaunch } from '@IconsV2/ic-rocket-launch.svg' +import { ReactComponent as ICSelected } from '@IconsV2/ic-selected.svg' import { ReactComponent as ICShieldCheck } from '@IconsV2/ic-shield-check.svg' import { ReactComponent as ICSlidersVertical } from '@IconsV2/ic-sliders-vertical.svg' import { ReactComponent as ICSortAscending } from '@IconsV2/ic-sort-ascending.svg' @@ -100,7 +108,9 @@ import { ReactComponent as ICSuccess } from '@IconsV2/ic-success.svg' import { ReactComponent as ICSuspended } from '@IconsV2/ic-suspended.svg' import { ReactComponent as ICTata1mg } from '@IconsV2/ic-tata1mg.svg' import { ReactComponent as ICTerminalFill } from '@IconsV2/ic-terminal-fill.svg' -import { ReactComponent as ICTimeoutTwoDash } from '@IconsV2/ic-timeout-two-dash.svg' +import { ReactComponent as ICThumbDown } from '@IconsV2/ic-thumb-down.svg' +import { ReactComponent as ICThumbUp } from '@IconsV2/ic-thumb-up.svg' +import { ReactComponent as ICTimeoutDash } from '@IconsV2/ic-timeout-dash.svg' import { ReactComponent as ICTimer } from '@IconsV2/ic-timer.svg' import { ReactComponent as ICTravclan } from '@IconsV2/ic-travclan.svg' import { ReactComponent as ICUnknown } from '@IconsV2/ic-unknown.svg' @@ -136,6 +146,7 @@ export const iconMap = { 'ic-cd': ICCd, 'ic-chat-circle-dots': ICChatCircleDots, 'ic-check': ICCheck, + 'ic-checks': ICChecks, 'ic-ci-linked': ICCiLinked, 'ic-ci-webhook': ICCiWebhook, 'ic-circle-loader': ICCircleLoader, @@ -146,6 +157,7 @@ export const iconMap = { 'ic-container': ICContainer, 'ic-cookr': ICCookr, 'ic-copy': ICCopy, + 'ic-cpu': ICCpu, 'ic-crown': ICCrown, 'ic-cube': ICCube, 'ic-delete-lightning': ICDeleteLightning, @@ -168,10 +180,12 @@ export const iconMap = { 'ic-google-container-registry': ICGoogleContainerRegistry, 'ic-google': ICGoogle, 'ic-grid-view': ICGridView, + 'ic-hand-pointing': ICHandPointing, 'ic-heart-green': ICHeartGreen, 'ic-heart-red-animated': ICHeartRedAnimated, 'ic-heart-red': ICHeartRed, 'ic-helm': ICHelm, + 'ic-help-filled': ICHelpFilled, 'ic-help-outline': ICHelpOutline, 'ic-hibernate': ICHibernate, 'ic-in-progress': ICInProgress, @@ -179,6 +193,7 @@ export const iconMap = { 'ic-info-outline': ICInfoOutline, 'ic-job-color': ICJobColor, 'ic-k8s-job': ICK8sJob, + 'ic-key': ICKey, 'ic-ldap': ICLdap, 'ic-lightning-fill': ICLightningFill, 'ic-lightning': ICLightning, @@ -187,6 +202,7 @@ export const iconMap = { 'ic-logout': ICLogout, 'ic-medium-delete': ICMediumDelete, 'ic-medium-paintbucket': ICMediumPaintbucket, + 'ic-memory': ICMemory, 'ic-microsoft': ICMicrosoft, 'ic-missing': ICMissing, 'ic-monitoring': ICMonitoring, @@ -201,6 +217,8 @@ export const iconMap = { 'ic-pencil': ICPencil, 'ic-quay': ICQuay, 'ic-quote': ICQuote, + 'ic-rocket-launch': ICRocketLaunch, + 'ic-selected': ICSelected, 'ic-shield-check': ICShieldCheck, 'ic-sliders-vertical': ICSlidersVertical, 'ic-sort-ascending': ICSortAscending, @@ -213,7 +231,9 @@ export const iconMap = { 'ic-suspended': ICSuspended, 'ic-tata1mg': ICTata1mg, 'ic-terminal-fill': ICTerminalFill, - 'ic-timeout-two-dash': ICTimeoutTwoDash, + 'ic-thumb-down': ICThumbDown, + 'ic-thumb-up': ICThumbUp, + 'ic-timeout-dash': ICTimeoutDash, 'ic-timer': ICTimer, 'ic-travclan': ICTravclan, 'ic-unknown': ICUnknown, diff --git a/src/Shared/Components/ImageCard/ArtifactInfo/ArtifactInfo.tsx b/src/Shared/Components/ImageCard/ArtifactInfo/ArtifactInfo.tsx index 35129d8c2..b51de46c8 100644 --- a/src/Shared/Components/ImageCard/ArtifactInfo/ArtifactInfo.tsx +++ b/src/Shared/Components/ImageCard/ArtifactInfo/ArtifactInfo.tsx @@ -16,11 +16,11 @@ import Tippy from '@tippyjs/react' +import { ReactComponent as ICBot } from '@Icons/ic-bot.svg' import { Tooltip } from '@Common/Tooltip' +import { Icon } from '@Shared/Components/Icon' import { RegistryIcon } from '@Shared/Components/RegistryIcon' -import { ReactComponent as ICBot } from '../../../../Assets/Icon/ic-bot.svg' -import { ReactComponent as DeployIcon } from '../../../../Assets/Icon/ic-nav-rocket.svg' import { ConditionalWrap, getRandomColor } from '../../../../Common/Helper' import { DefaultUserKey } from '../../../types' import { ArtifactInfoProps } from '../types' @@ -62,7 +62,7 @@ const ArtifactInfo = ({ return (
- + {deployedTime}
) diff --git a/src/Shared/Components/InfoBlock/constants.tsx b/src/Shared/Components/InfoBlock/constants.tsx index e372ad68b..7d6867861 100644 --- a/src/Shared/Components/InfoBlock/constants.tsx +++ b/src/Shared/Components/InfoBlock/constants.tsx @@ -14,10 +14,6 @@ * limitations under the License. */ -import { ReactComponent as ICError } from '@Icons/ic-error.svg' -import { ReactComponent as ICHelp } from '@Icons/ic-help.svg' -import { ReactComponent as ICSuccess } from '@Icons/ic-success.svg' -import { ReactComponent as ICWarningY5 } from '@Icons/ic-warning-y5.svg' import { ComponentSizeType } from '@Shared/constants' import { ButtonProps } from '../Button' @@ -34,11 +30,11 @@ export const VARIANT_TO_BG_MAP: Record = { } export const VARIANT_TO_ICON_MAP: Record = { - error: , - help: , + error: , + help: , information: , - success: , - warning: , + success: , + warning: , neutral: , } diff --git a/src/Shared/Components/KeyValueTable/KeyValueTable.component.tsx b/src/Shared/Components/KeyValueTable/KeyValueTable.component.tsx index a292140df..e43a693a9 100644 --- a/src/Shared/Components/KeyValueTable/KeyValueTable.component.tsx +++ b/src/Shared/Components/KeyValueTable/KeyValueTable.component.tsx @@ -14,467 +14,177 @@ * limitations under the License. */ -import { createRef, Fragment, ReactElement, useEffect, useMemo, useRef, useState } from 'react' -import Tippy from '@tippyjs/react' -// eslint-disable-next-line import/no-extraneous-dependencies -import { followCursor } from 'tippy.js' - -import { ReactComponent as ICAdd } from '@Icons/ic-add.svg' -import { ReactComponent as ICClose } from '@Icons/ic-close.svg' -import { ReactComponent as ICCross } from '@Icons/ic-cross.svg' -import { ReactComponent as ICArrowDown } from '@Icons/ic-sort-arrow-down.svg' -import { ConditionalWrap, ResizableTagTextArea, SortingOrder, useStateFilters } from '@Common/index' -import { DEFAULT_SECRET_PLACEHOLDER } from '@Shared/constants' -import { stringComparatorBySortOrder } from '@Shared/Helpers' - -import { DUPLICATE_KEYS_VALIDATION_MESSAGE, EMPTY_KEY_VALIDATION_MESSAGE } from './constants' -import { KeyValueRow, KeyValueTableProps } from './KeyValueTable.types' +import { useEffect, useMemo, useState } from 'react' + +import { useEffectAfterMount } from '@Common/Helper' +import { useStateFilters } from '@Common/Hooks' + +import { DynamicDataTable } from '../DynamicDataTable' +import { KeyValueTableDataType, KeyValueTableInternalProps, KeyValueTableProps } from './KeyValueTable.types' +import { + getEmptyRow, + getKeyValueHeaders, + getKeyValueTableCellError, + getKeyValueTableRows, + getKeyValueTableSortedRows, + getModifiedDataForOnChange, +} from './utils' import './KeyValueTable.scss' -const renderWithReadOnlyTippy = (children: ReactElement) => ( - - {children} - -) - -export const KeyValueTable = ({ - config, +export const KeyValueTable = ({ + headerLabel, + rows: initialRows, + placeholder, maskValue, isSortable, headerComponent, onChange, - onDelete, - placeholder, isAdditionNotAllowed, readOnly, showError, validationSchema: parentValidationSchema, - errorMessages: parentErrorMessages = [], onError, validateDuplicateKeys = false, validateEmptyKeys = false, -}: KeyValueTableProps) => { - // CONSTANTS - const { headers, rows } = config - const firstHeaderKey = headers[0].key - const secondHeaderKey = headers[1].key - +}: KeyValueTableProps) => { // STATES - const [updatedRows, setUpdatedRows] = useState[]>(rows) - /** State to trigger useEffect to trigger autoFocus */ - const [newRowAdded, setNewRowAdded] = useState(false) - - const isActionDisabled = readOnly || isAdditionNotAllowed - - /** Boolean determining if table has rows. */ - const hasRows = (!readOnly && !isAdditionNotAllowed) || !!updatedRows.length - const isFirstRowEmpty = !updatedRows[0]?.data[firstHeaderKey].value && !updatedRows[0]?.data[secondHeaderKey].value - const disableDeleteRow = updatedRows.length === 1 && isFirstRowEmpty + const [cellError, setCellError] = useState({}) + const [sortedRows, setSortedRows] = useState([]) // HOOKS - const { sortBy, sortOrder, handleSorting } = useStateFilters({ - initialSortKey: isSortable ? firstHeaderKey : null, + const { sortBy, sortOrder, handleSorting } = useStateFilters({ + initialSortKey: isSortable ? 'key' : null, }) - const keyTextAreaRef = useRef>>() - const valueTextAreaRef = useRef>>() - if (!keyTextAreaRef.current) { - keyTextAreaRef.current = updatedRows.reduce((acc, curr) => ({ ...acc, [curr.id]: createRef() }), {}) - } - - if (!valueTextAreaRef.current) { - valueTextAreaRef.current = updatedRows.reduce((acc, curr) => ({ ...acc, [curr.id]: createRef() }), {}) - } - - const updatedRowsKeysFrequency: Record = useMemo( - () => - updatedRows.reduce( - (acc, curr) => { - const currentKey = curr.data[firstHeaderKey].value - if (currentKey) { - acc[currentKey] = (acc[currentKey] || 0) + 1 - } - return acc - }, - {} as Record, - ), - [updatedRows], + // COMPUTED ROWS FOR DYNAMIC DATA TABLE + const rows = useMemo( + () => getKeyValueTableRows({ rows: initialRows, placeholder, maskValue }), + [initialRows, placeholder, maskValue, isSortable, sortOrder, sortBy], ) - const validationSchema = ( - value: Parameters[0], - key: Parameters[1], - rowId: Parameters[2], - shouldTriggerCustomValidation: boolean = true, - ) => { - if (shouldTriggerCustomValidation) { - const trimmedValue = value.trim() - - if (validateDuplicateKeys && key === firstHeaderKey && updatedRowsKeysFrequency[trimmedValue] > 1) { - return false - } - - if (validateEmptyKeys && key === firstHeaderKey && !trimmedValue) { - const isValuePresentAtRow = updatedRows.some( - ({ id, data }) => id === rowId && data[secondHeaderKey].value.trim(), - ) - if (isValuePresentAtRow) { - return false - } - } - } - - if (parentValidationSchema) { - return parentValidationSchema(value, key, rowId) - } - - return true - } - - const checkAllRowsAreValid = (editedRows: KeyValueRow[]) => { - if (validateDuplicateKeys) { - const { isAnyKeyDuplicated } = editedRows.reduce( - (acc, curr) => { - const { keysFrequency } = acc - const currentKey = curr.data[firstHeaderKey].value.trim() - - if (currentKey) { - keysFrequency[currentKey] = (keysFrequency[currentKey] || 0) + 1 - } - - return { - isAnyKeyDuplicated: acc.isAnyKeyDuplicated || keysFrequency[currentKey] > 1, - keysFrequency, - } - }, - { isAnyKeyDuplicated: false, keysFrequency: {} as Record }, + /** Function to update the sorted rows based on the current sorting configuration */ + const updateSortedRows = () => { + if (isSortable) { + setSortedRows( + getKeyValueTableSortedRows({ + rows, + sortBy, + sortOrder, + }), ) - - if (isAnyKeyDuplicated) { - return false - } } + } - if (validateEmptyKeys) { - const isEmptyKeyPresent = editedRows.some( - (row) => !row.data[firstHeaderKey].value.trim() && row.data[secondHeaderKey].value.trim(), - ) + useEffect(() => { + // Set cell error on mount + const { isValid, updatedCellError } = getKeyValueTableCellError({ + rows, + validateDuplicateKeys, + validateEmptyKeys, + validationSchema: parentValidationSchema, + }) + + setCellError(updatedCellError) + onError?.(!isValid) + + // Set sorted rows on mount + updateSortedRows() + }, []) - if (isEmptyKeyPresent) { - return false - } + // Sort rows for display purposes only. \ + // The `sortedRows` state is used internally to render the data, while the original `rows` prop remains unaltered during sorting. + useEffectAfterMount(() => { + if (!isSortable) { + // If sorting is disabled, directly set rows without any processing + setSortedRows(rows) + return } - // Sending custom validation as false since already checked above - const isValid = editedRows.every( - ({ data: _data, id }) => - validationSchema(_data[firstHeaderKey].value, firstHeaderKey, id, false) && - validationSchema(_data[secondHeaderKey].value, secondHeaderKey, id, false), - ) + // Create a mapping of row IDs to row objects for quick lookup + const rowMap = new Map(rows.map((row) => [row.id, row])) - return isValid - } - - const getEmptyRow = (): KeyValueRow => { - const id = (Date.now() * Math.random()).toString(16) - const data = { - data: { - [firstHeaderKey]: { - value: '', - }, - [secondHeaderKey]: { - value: '', - }, - }, - id, - } as KeyValueRow + // Create a set of IDs from the current sorted rows for efficient membership checking + const sortedRowIds = new Set(sortedRows.map((row) => row.id)) - return data - } + // Update the sorted rows by mapping them to the latest version from `rows` and filtering out any rows that no longer exist + const updatedSortedRows = sortedRows.map((row) => rowMap.get(row.id)).filter(Boolean) - const handleAddNewRow = () => { - const data = getEmptyRow() - const editedRows = [data, ...updatedRows] + // Find any new rows that are not already in the sorted list + const newUnsortedRows = rows.filter((row) => !sortedRowIds.has(row.id)) - const { id } = data + // Combine new unsorted rows (at the top) with the updated sorted rows (preserving original order) + setSortedRows([...newUnsortedRows, ...updatedSortedRows]) + }, [rows]) - onError?.(!checkAllRowsAreValid(editedRows)) - setNewRowAdded(true) - setUpdatedRows(editedRows) + // Update the sorted rows whenever the sorting configuration changes + useEffectAfterMount(() => { + updateSortedRows() + }, [sortBy, sortOrder]) - keyTextAreaRef.current = { - ...(keyTextAreaRef.current || {}), - [id as string]: createRef(), - } - valueTextAreaRef.current = { - ...(valueTextAreaRef.current || {}), - [id as string]: createRef(), - } + // METHODS + const setUpdatedRows = (updatedRows: typeof rows) => { + const { isValid, updatedCellError } = getKeyValueTableCellError({ + rows: updatedRows, + validateDuplicateKeys, + validateEmptyKeys, + validationSchema: parentValidationSchema, + }) + + setCellError(updatedCellError) + onError?.(!isValid) + + onChange(getModifiedDataForOnChange(updatedRows)) } - useEffect(() => { - if (!isActionDisabled && !updatedRows.length) { - handleAddNewRow() - } - }, []) - - useEffect(() => { - if (isSortable) { - setUpdatedRows((prevRows) => { - const sortedRows = structuredClone(prevRows) - sortedRows.sort((a, b) => - stringComparatorBySortOrder(a.data[sortBy].value, b.data[sortBy].value, sortOrder), - ) - return sortedRows - }) - } - }, [sortOrder]) - - useEffect(() => { - const firstRow = updatedRows[0] - - if (firstRow && newRowAdded) { - setNewRowAdded(false) - const areKeyAndValueTextAreaRefsPresent = - keyTextAreaRef.current[firstRow.id].current && valueTextAreaRef.current[firstRow.id].current + const onRowAdd = () => { + const newRow = getEmptyRow(placeholder) + const updatedRows = [newRow, ...rows] - if (!firstRow.data[firstHeaderKey].value && areKeyAndValueTextAreaRefsPresent) { - valueTextAreaRef.current[firstRow.id].current.focus() - } - if (!firstRow.data[secondHeaderKey].value && areKeyAndValueTextAreaRefsPresent) { - keyTextAreaRef.current[firstRow.id].current.focus() - } - } - }, [newRowAdded]) - - // METHODS - const onSortBtnClick = () => handleSorting(sortBy) + setUpdatedRows(updatedRows) + } - const onRowDelete = (row: KeyValueRow) => () => { - const remainingRows = updatedRows.filter(({ id }) => id !== row.id) + const onRowDelete: KeyValueTableInternalProps['onRowDelete'] = (row) => { + const remainingRows = rows.filter(({ id }) => id !== row.id) if (remainingRows.length === 0 && !isAdditionNotAllowed) { - const emptyRowData = getEmptyRow() - const { id } = emptyRowData + const emptyRowData = getEmptyRow(placeholder) - setNewRowAdded(true) - onError?.(!checkAllRowsAreValid([emptyRowData])) setUpdatedRows([emptyRowData]) - - keyTextAreaRef.current = { - [id as string]: createRef(), - } - valueTextAreaRef.current = { - [id as string]: createRef(), - } - - onDelete?.(row.id) return } - onError?.(!checkAllRowsAreValid(remainingRows)) setUpdatedRows(remainingRows) - - delete keyTextAreaRef.current[row.id] - delete valueTextAreaRef.current[row.id] - - onDelete?.(row.id) - } - - const onRowDataEdit = (row: KeyValueRow, key: K) => (e: React.ChangeEvent) => { - const { value } = e.target - const rowData = { - ...row, - data: { - ...row.data, - [key]: { - ...row.data[key], - value, - }, - }, - } - const editedRows = updatedRows.map((_row) => (_row.id === row.id ? rowData : _row)) - onError?.(!checkAllRowsAreValid(editedRows)) - setUpdatedRows(editedRows) - } - - const onRowDataBlur = (row: KeyValueRow, key: K) => (e: React.FocusEvent) => { - const { value } = e.target - - onChange?.(row.id, key, value) - onError?.(!checkAllRowsAreValid(updatedRows)) } - const renderFirstHeader = (key: K, label: string, className: string) => ( -
- {isSortable ? ( - - ) : ( -
- {label} - {!!headerComponent && headerComponent} -
- )} - - -
- ) - - const renderErrorMessage = (errorMessage: string) => ( -
- -

{errorMessage}

-
- ) - - const renderErrorMessages = ( - value: Parameters[0], - key: Parameters[1], - rowId: KeyValueRow['id'], - ) => { - const showErrorMessages = showError && !validationSchema(value, key, rowId) - if (!showErrorMessages) { - return null + const onRowEdit: KeyValueTableInternalProps['onRowEdit'] = (row, headerKey, value) => { + const updatedRows = rows + const rowIndex = rows.findIndex(({ id }) => row.id === id) + const selectedRow = rows[rowIndex] + if (selectedRow) { + selectedRow.data[headerKey].value = value + updatedRows[rowIndex] = selectedRow } - return ( -
- {validateDuplicateKeys && renderErrorMessage(DUPLICATE_KEYS_VALIDATION_MESSAGE)} - {validateEmptyKeys && renderErrorMessage(EMPTY_KEY_VALIDATION_MESSAGE)} - {parentErrorMessages.map((error) => renderErrorMessage(error))} -
- ) + setUpdatedRows(updatedRows) } return ( - <> -
-
- {/* HEADER */} -
- {headers.map(({ key, label, className }) => - key === firstHeaderKey ? ( - renderFirstHeader(key, label, className) - ) : ( -
- {label} - {!!headerComponent && headerComponent} -
- ), - )} -
-
-
- - {hasRows && ( -
- {!!updatedRows.length && ( -
- {updatedRows.map((row) => ( -
- {headers.map(({ key }) => ( - - -
- {maskValue?.[key] && row.data[key].value ? ( -
- {DEFAULT_SECRET_PLACEHOLDER} -
- ) : ( - <> - - {row.data[key].required && ( - - * - - )} - {renderErrorMessages(row.data[key].value, key, row.id)} - - )} -
-
-
- ))} - {!readOnly && ( - - )} -
- ))} -
- )} -
- )} - + ) } diff --git a/src/Shared/Components/KeyValueTable/KeyValueTable.types.ts b/src/Shared/Components/KeyValueTable/KeyValueTable.types.ts index d437b514a..fff70a1b0 100644 --- a/src/Shared/Components/KeyValueTable/KeyValueTable.types.ts +++ b/src/Shared/Components/KeyValueTable/KeyValueTable.types.ts @@ -14,47 +14,13 @@ * limitations under the License. */ -import { ReactNode } from 'react' +import { TooltipProps } from '@Common/Tooltip' -import { ResizableTagTextAreaProps } from '../../../Common' +import { DynamicDataTableProps } from '../DynamicDataTable' -/** - * Interface representing a key-value header. - * @template K - A string representing the key type. - */ -export interface KeyValueHeader { - /** The label of the header. */ - label: string - /** The key associated with the header. */ - key: K - /** An optional class name for the header. */ - className?: string -} +export type KeyValueTableDataType = 'key' | 'value' -/** - * Type representing a key-value row. - * @template K - A string representing the key type. - */ -export type KeyValueRow = { - data: { - [key in K]: Pick & { - /** An optional boolean indicating if an asterisk should be shown. */ - required?: boolean - } - } - id: string | number -} - -/** - * Interface representing the configuration for a key-value table. - * @template K - A string representing the key type. - */ -export interface KeyValueConfig { - /** An array containing two key-value headers. */ - headers: [KeyValueHeader, KeyValueHeader] - /** An array of key-value rows. */ - rows: KeyValueRow[] -} +export type KeyValueTableInternalProps = DynamicDataTableProps type ErrorUIProps = | { @@ -82,64 +48,106 @@ type ErrorUIProps = validateEmptyKeys?: never } -/** - * Type representing a mask for key-value pairs. - * @template K - A string representing the key type. - */ -export type KeyValueMask = { +export type KeyValueHeaderLabel = { + [key in K]: string +} + +export type KeyValueMask = { [key in K]?: boolean } -export type KeyValuePlaceholder = { +export type KeyValuePlaceholder = { [key in K]?: string } +export interface KeyValueTableRowType { + id: string | number + data: { + [key in K]: { + value: string + /** An optional boolean indicating if the cell should be marked as disabled. */ + disabled?: boolean + /** An optional boolean indicating if an asterisk should be shown. */ + required?: boolean + /** An optional tooltip to show when hovering over cell. */ + tooltip?: Partial> + } + } +} + +export interface KeyValueTableData extends Pick { + key: string + value: string +} + /** - * Interface representing the properties for a key-value table component. - * @template K - A string representing the key type. + * Props for the KeyValueTable component. */ -export type KeyValueTableProps = { - /** The configuration for the key-value table. */ - config: KeyValueConfig - /** An optional mask for the key-value pairs. */ - maskValue?: KeyValueMask - placeholder?: KeyValuePlaceholder - /** An optional boolean indicating if the table is sortable. */ - isSortable?: boolean - /** An optional React node for a custom header component. */ - headerComponent?: ReactNode - /** When true, data addition field will not be shown. */ - isAdditionNotAllowed?: boolean - /** When true, data add or update is disabled. */ - readOnly?: boolean +export type KeyValueTableProps = Pick< + DynamicDataTableProps, + 'isAdditionNotAllowed' | 'readOnly' | 'headerComponent' +> & { /** - * An optional function to handle changes in the table rows. - * @param rowId - The id of the row that changed. - * @param headerKey - The key of the header that changed. - * @param value - The value of the cell. + * The label for the table header. */ - onChange?: (rowId: string | number, headerKey: K, value: string) => void + headerLabel: KeyValueHeaderLabel /** - * An optional function to handle row deletions. - * @param deletedRowIndex - The index of the row that was deleted. + * The rows of the key-value table. */ - onDelete?: (deletedRowId: string | number) => void + rows: KeyValueTableRowType[] /** - * The function to use to validate the value of the cell. - * @param value - The value to validate. - * @param key - The row key of the value. - * @param rowId - The id of the row. - * @returns Return true if the value is valid, otherwise false - * and set `showError` to `true` and provide errorMessages array to show error message. + * An optional configuration to mask values in the table. */ - validationSchema?: (value: string, key: K, rowId: string | number) => boolean + maskValue?: KeyValueMask /** - * An array of error messages to be displayed in the cell error tooltip. + * An optional placeholder configuration for the table columns. */ - errorMessages?: string[] + placeholder?: KeyValuePlaceholder /** - * A callback function called when an error occurs. - * @param errorState - The error state, true when any cell has error, otherwise false. + * An optional boolean indicating if the `key` column is sortable. + */ + isSortable?: boolean + /** + * A callback function triggered when the table rows change. + * + * @param data - The updated table data. + */ + onChange: (data: KeyValueTableData[]) => void + /** + * A function to validate the value of a cell. + * + * @param value - The value to validate. + * @param key - The key of the header associated with the value. + * @param row - The row containing the value. + * @returns A boolean indicating whether the value is valid. If false, + * `showError` should be set to `true` and `errorMessages` should + * provide an array of error messages to display. + */ + validationSchema?: ( + value: string, + key: KeyValueTableDataType, + row: KeyValueTableRowType, + ) => { + /** Boolean indicating if the cell data is valid or not. */ + isValid: boolean + /** + * An array of error messages to display in the cell error tooltip. + */ + errorMessages?: string[] + } + /** + * A callback function triggered when an error occurs in the table. + * + * @param errorState - A boolean indicating the error state. True if any + * cell has an error, otherwise false. */ onError?: (errorState: boolean) => void } & ErrorUIProps + +export type KeyValueValidationSchemaProps = { + value: Parameters[0] + key: Parameters[1] + row: Parameters[2] + keysFrequency?: Record +} & Pick & + Partial> diff --git a/src/Shared/Components/KeyValueTable/index.ts b/src/Shared/Components/KeyValueTable/index.ts index e81474570..058030a56 100644 --- a/src/Shared/Components/KeyValueTable/index.ts +++ b/src/Shared/Components/KeyValueTable/index.ts @@ -15,4 +15,4 @@ */ export * from './KeyValueTable.component' -export * from './KeyValueTable.types' +export type { KeyValueTableData, KeyValueTableProps } from './KeyValueTable.types' diff --git a/src/Shared/Components/KeyValueTable/utils.ts b/src/Shared/Components/KeyValueTable/utils.ts new file mode 100644 index 000000000..ff6308872 --- /dev/null +++ b/src/Shared/Components/KeyValueTable/utils.ts @@ -0,0 +1,181 @@ +import { UseStateFiltersReturnType } from '@Common/Hooks' +import { DEFAULT_SECRET_PLACEHOLDER } from '@Shared/constants' +import { getUniqueId, stringComparatorBySortOrder } from '@Shared/Helpers' + +import { DynamicDataTableCellValidationState, DynamicDataTableRowDataType } from '../DynamicDataTable' +import { DUPLICATE_KEYS_VALIDATION_MESSAGE, EMPTY_KEY_VALIDATION_MESSAGE } from './constants' +import { + KeyValueTableData, + KeyValueTableDataType, + KeyValueTableInternalProps, + KeyValueTableProps, + KeyValueValidationSchemaProps, +} from './KeyValueTable.types' + +export const getModifiedDataForOnChange = (rows: KeyValueTableInternalProps['rows']): KeyValueTableData[] => + rows.map(({ data, id }) => ({ id, key: data.key.value, value: data.value.value })) + +export const getEmptyRow = ( + placeholder: KeyValueTableProps['placeholder'], +): KeyValueTableInternalProps['rows'][number] => ({ + id: getUniqueId(), + data: { + key: { + type: DynamicDataTableRowDataType.TEXT, + props: { placeholder: placeholder.key }, + value: '', + }, + value: { + type: DynamicDataTableRowDataType.TEXT, + props: { placeholder: placeholder.value }, + value: '', + }, + }, +}) + +export const getKeyValueTableRows = ({ + rows: initialRows, + placeholder, + maskValue, +}: Required>): KeyValueTableInternalProps['rows'] => { + const isMaskValue = maskValue && Object.keys(maskValue).length + + const rows: KeyValueTableInternalProps['rows'] = initialRows?.length + ? initialRows.map(({ data: { key, value }, id }) => ({ + data: { + key: { + ...key, + type: DynamicDataTableRowDataType.TEXT, + value: isMaskValue && maskValue.key ? DEFAULT_SECRET_PLACEHOLDER : key.value, + props: { placeholder: placeholder.key }, + }, + value: { + ...value, + type: DynamicDataTableRowDataType.TEXT, + value: isMaskValue && maskValue.value ? DEFAULT_SECRET_PLACEHOLDER : value.value, + props: { placeholder: placeholder.value }, + }, + }, + id, + })) + : [getEmptyRow(placeholder)] + + return rows +} + +export const getKeyValueTableSortedRows = ({ + rows, + sortBy, + sortOrder, +}: Required, 'sortBy' | 'sortOrder'>> & + Pick) => + rows + .map((item) => item) + .sort((a, b) => stringComparatorBySortOrder(a.data[sortBy].value, b.data[sortBy].value, sortOrder)) + +export const getKeyValueHeaders = ({ + headerLabel, + isSortable, +}: Pick): KeyValueTableInternalProps['headers'] => [ + { key: 'key', label: headerLabel.key, width: '30%', isSortable }, + { key: 'value', label: headerLabel.value, width: '1fr' }, +] + +const getKeyValueTableKeysFrequency = (rows: KeyValueTableInternalProps['rows']) => + rows.reduce( + (acc, curr) => { + const currentKey = curr.data.key.value + if (currentKey) { + acc[currentKey] = (acc[currentKey] || 0) + 1 + } + return acc + }, + {} as Record, + ) + +const validationSchema = ({ + value, + key, + row, + validateDuplicateKeys, + validateEmptyKeys, + validationSchema: parentValidationSchema, + rows = [], + keysFrequency = {}, +}: KeyValueValidationSchemaProps): DynamicDataTableCellValidationState => { + const trimmedValue = value.trim() + + if (validateDuplicateKeys && key === 'key' && (keysFrequency[trimmedValue] ?? 0) > 1) { + return { + isValid: false, + errorMessages: [DUPLICATE_KEYS_VALIDATION_MESSAGE], + } + } + + if (validateEmptyKeys && key === 'key' && !trimmedValue) { + const isValuePresentAtRow = rows.some(({ id, data }) => id === row.id && data.value.value.trim()) + if (isValuePresentAtRow) { + return { + isValid: false, + errorMessages: [EMPTY_KEY_VALIDATION_MESSAGE], + } + } + } + + if (parentValidationSchema) { + const { isValid, errorMessages } = parentValidationSchema(value, key, row) + return { + isValid, + errorMessages: errorMessages || [], + } + } + + return { + isValid: true, + errorMessages: [], + } +} + +export const getKeyValueTableCellError = ({ + validateDuplicateKeys, + validateEmptyKeys, + validationSchema: parentValidationSchema, + rows, +}: Pick & { + skipValidationIfValueIsEmpty?: boolean +}) => { + let isValid = true + + const updatedCellError = rows.reduce((acc, row) => { + const keyError = validationSchema({ + rows, + value: row.data.key.value, + key: 'key', + row, + validateDuplicateKeys, + validateEmptyKeys, + validationSchema: parentValidationSchema, + keysFrequency: validateDuplicateKeys ? getKeyValueTableKeysFrequency(rows) : {}, + }) + + const valueError = validationSchema({ + value: row.data.value.value, + key: 'value', + row, + validationSchema: parentValidationSchema, + }) + + if (isValid && !(keyError.isValid && valueError.isValid)) { + isValid = false + } + + acc[row.id] = { + key: keyError, + value: valueError, + } + + return acc + }, {}) + + return { isValid, updatedCellError } +} diff --git a/src/Shared/Components/StatusComponent/utils.ts b/src/Shared/Components/StatusComponent/utils.ts index eddd95a1e..b6303d7c8 100644 --- a/src/Shared/Components/StatusComponent/utils.ts +++ b/src/Shared/Components/StatusComponent/utils.ts @@ -70,7 +70,7 @@ export const getIconName = (status: string, showAnimatedIcon: boolean): IconName return 'ic-hibernate' case 'timedout': case 'timed_out': - return 'ic-timeout-two-dash' + return 'ic-timeout-dash' default: return null } diff --git a/src/Shared/Components/TabGroup/TabGroup.component.tsx b/src/Shared/Components/TabGroup/TabGroup.component.tsx index 801a4d806..63b3a54b1 100644 --- a/src/Shared/Components/TabGroup/TabGroup.component.tsx +++ b/src/Shared/Components/TabGroup/TabGroup.component.tsx @@ -14,17 +14,31 @@ * limitations under the License. */ -import { Link, NavLink } from 'react-router-dom' +import { useEffect, useMemo, useRef, useState } from 'react' +import { Link, NavLink, useLocation } from 'react-router-dom' +import { motion } from 'framer-motion' import { Tooltip } from '@Common/Tooltip' import { ComponentSizeType } from '@Shared/constants' import { getTabBadge, getTabDescription, getTabIcon, getTabIndicator } from './TabGroup.helpers' -import { TabGroupProps, TabProps } from './TabGroup.types' +import { AdditionalTabProps, TabGroupProps, TabProps } from './TabGroup.types' import { getClassNameBySizeMap, tabGroupClassMap } from './TabGroup.utils' import './TabGroup.scss' +const MotionLayoutUnderline = ({ layoutId }: { layoutId: string }) => ( + + // Replace the y value in translate3d(x, y, z) with 0px to omit y axis transitions + generatedTransform.replace(/translate3d\(([^,]+),\s*[^,]+,\s*([^)]+)\)/, 'translate3d($1, 0px, $2)') + } + layoutId={layoutId} + className="underline bcb-5 w-100 dc__position-abs" + /> +) + const Tab = ({ label, props, @@ -33,7 +47,6 @@ const Tab = ({ icon, size, badge = null, - alignActiveBorderWithContainer, hideTopPadding, showIndicator, showError, @@ -42,10 +55,22 @@ const Tab = ({ description, shouldWrapTooltip, tooltipProps, -}: TabProps & Pick) => { + uniqueGroupId, +}: TabProps & Pick & AdditionalTabProps) => { + const { pathname, search } = useLocation() + const ref = useRef(null) + const [isTabActive, setIsTabActive] = useState(tabType === 'button' && active) + + useEffect(() => { + if (tabType === 'navLink') { + setIsTabActive(ref.current?.classList.contains('active') || false) + return + } + setIsTabActive(active) + }, [active, tabType, pathname, search]) + const { tabClassName, iconClassName, badgeClassName } = getClassNameBySizeMap({ hideTopPadding, - alignActiveBorderWithContainer, })[size] const onClickHandler = ( @@ -62,12 +87,14 @@ const Tab = ({ const getTabComponent = () => { const content = ( <> -

+ {getTabIcon({ className: iconClassName, icon, showError, showWarning, size, active })} - {label} + + {label} + {getTabBadge(badge, badgeClassName)} {getTabIndicator(showIndicator)} -

+ {getTabDescription(description)} ) @@ -87,6 +114,7 @@ const Tab = ({ case 'navLink': return ( (
  • {getTabComponent()} + {isTabActive && }
  • ) @@ -138,22 +167,27 @@ export const TabGroup = ({ tabs = [], size = ComponentSizeType.large, rightComponent, - alignActiveBorderWithContainer, hideTopPadding, -}: TabGroupProps) => ( -
    -
      - {tabs.map(({ id, ...resProps }) => ( - - ))} -
    - {rightComponent || null} -
    -) +}: TabGroupProps) => { + // Unique layoutId for motion.div to handle multiple tab groups on same page + // Using tab labels so that id remains same on re mount as well + const uniqueGroupId = useMemo(() => tabs.map((tab) => tab.label).join('-'), []) + + return ( +
    +
      + {tabs.map(({ id, ...resProps }) => ( + + ))} +
    + {rightComponent || null} +
    + ) +} diff --git a/src/Shared/Components/TabGroup/TabGroup.helpers.tsx b/src/Shared/Components/TabGroup/TabGroup.helpers.tsx index 3d515b910..861e7550b 100644 --- a/src/Shared/Components/TabGroup/TabGroup.helpers.tsx +++ b/src/Shared/Components/TabGroup/TabGroup.helpers.tsx @@ -47,7 +47,7 @@ export const getTabIcon = ({ } export const getTabBadge = (badge: TabProps['badge'], className: string) => - badge !== null &&
    {badge}
    + badge !== null && {badge} export const getTabIndicator = (showIndicator: TabProps['showIndicator']) => showIndicator && diff --git a/src/Shared/Components/TabGroup/TabGroup.scss b/src/Shared/Components/TabGroup/TabGroup.scss index c5a352898..02f5e7967 100644 --- a/src/Shared/Components/TabGroup/TabGroup.scss +++ b/src/Shared/Components/TabGroup/TabGroup.scss @@ -33,20 +33,15 @@ @include svg-styles(var(--N700)); - &::after { - content: ''; - position: absolute; - bottom: 0; - left: 0; - width: 100%; - height: 2px; - background-color: transparent; + .underline { + height: 2px !important; border-top-left-radius: 2px; border-top-right-radius: 2px; + bottom: -1px; } - &--align-active-border::after { - bottom: -1px; + &--active { + @include svg-styles(var(--B500)); } &:hover:not(.tab-group__tab--block):not(.dc__disabled) { @@ -58,14 +53,6 @@ } } - &--active { - @include svg-styles(var(--B500)); - - &::after { - background-color: var(--B500); - } - } - &__badge { border-radius: 10px; min-width: 20px; @@ -104,11 +91,5 @@ color: var(--B500); } } - - &:has(.active) { - &::after { - background-color: var(--B500); - } - } } } diff --git a/src/Shared/Components/TabGroup/TabGroup.types.ts b/src/Shared/Components/TabGroup/TabGroup.types.ts index 0ddb160f9..022b83254 100644 --- a/src/Shared/Components/TabGroup/TabGroup.types.ts +++ b/src/Shared/Components/TabGroup/TabGroup.types.ts @@ -152,14 +152,13 @@ export interface TabGroupProps { * Optional component to be rendered on the right side of the tab list. */ rightComponent?: React.ReactElement - /** - * Set to `true` to align the active tab's border with the bottom border of the parent container. - * @default false - */ - alignActiveBorderWithContainer?: boolean /** * Determines if the top padding of the tab group should be hidden. * @default false */ hideTopPadding?: boolean } + +export type AdditionalTabProps = { + uniqueGroupId: string +} diff --git a/src/Shared/Components/TabGroup/TabGroup.utils.ts b/src/Shared/Components/TabGroup/TabGroup.utils.ts index e2f73bb93..5682c1651 100644 --- a/src/Shared/Components/TabGroup/TabGroup.utils.ts +++ b/src/Shared/Components/TabGroup/TabGroup.utils.ts @@ -21,8 +21,7 @@ import { TabGroupProps } from './TabGroup.types' export const getClassNameBySizeMap = ({ hideTopPadding, - alignActiveBorderWithContainer, -}: Pick): Record< +}: Pick): Record< TabGroupProps['size'], { tabClassName: string @@ -31,17 +30,17 @@ export const getClassNameBySizeMap = ({ } > => ({ [ComponentSizeType.medium]: { - tabClassName: `fs-12 ${!hideTopPadding ? 'pt-6' : ''} ${alignActiveBorderWithContainer ? 'pb-5' : 'pb-6'}`, + tabClassName: `fs-12 ${!hideTopPadding ? 'pt-6' : ''} pb-5`, iconClassName: 'icon-dim-14', badgeClassName: 'fs-11 lh-18 tab-group__tab__badge--medium', }, [ComponentSizeType.large]: { - tabClassName: `fs-13 ${!hideTopPadding ? 'pt-8' : ''} ${alignActiveBorderWithContainer ? 'pb-7' : 'pb-8'}`, + tabClassName: `fs-13 ${!hideTopPadding ? 'pt-8' : ''} pb-7`, iconClassName: 'icon-dim-16', badgeClassName: 'fs-12 lh-20', }, [ComponentSizeType.xl]: { - tabClassName: `min-w-200 fs-13 ${!hideTopPadding ? 'pt-10' : ''} ${alignActiveBorderWithContainer ? 'pb-9' : 'pb-10'}`, + tabClassName: `min-w-200 fs-13 ${!hideTopPadding ? 'pt-10' : ''} pb-9`, iconClassName: 'icon-dim-16', badgeClassName: 'fs-12 lh-20', }, diff --git a/src/Shared/Components/UserIdentifier/UserIdentifier.tsx b/src/Shared/Components/UserIdentifier/UserIdentifier.tsx new file mode 100644 index 000000000..8e9318cc2 --- /dev/null +++ b/src/Shared/Components/UserIdentifier/UserIdentifier.tsx @@ -0,0 +1,77 @@ +import { getAlphabetIcon } from '@Common/Helper' +import { Tooltip } from '@Common/Tooltip' +import { API_TOKEN_PREFIX } from '@Shared/constants' +import { useUserEmail } from '@Shared/Providers' + +import { Icon } from '../Icon' +import { UserIdentifierProps } from './types' + +const UserIdentifierTooltip = ({ + children, + tooltipContent, +}: Pick) => ( + +
    {children}
    +
    +) + +export const UserIdentifier = ({ + identifier, + children, + rootClassName, + tooltipContent, + isUserGroup = false, +}: UserIdentifierProps) => { + // HOOKS + const { email: currentUserEmail } = useUserEmail() + + if (!identifier) { + return null + } + + // CONSTANTS + const isCurrentUser = identifier === currentUserEmail + const isApiToken = identifier.startsWith(API_TOKEN_PREFIX) + + const renderIcon = () => { + if (isApiToken) { + return + } + + return isUserGroup ? ( + + ) : ( + getAlphabetIcon(identifier, 'dc__no-shrink m-0-imp') + ) + } + + const renderText = () => { + if (isCurrentUser) { + return 'You' + } + + if (isApiToken) { + return identifier.split(':')?.[1] || '-' + } + + return identifier + } + + return ( +
    + + {renderIcon()} +
    + + {renderText()} + + {children} +
    +
    +
    + ) +} diff --git a/src/Shared/Components/UserIdentifier/index.ts b/src/Shared/Components/UserIdentifier/index.ts new file mode 100644 index 000000000..0b3e4d0e6 --- /dev/null +++ b/src/Shared/Components/UserIdentifier/index.ts @@ -0,0 +1,2 @@ +export * from './types' +export * from './UserIdentifier' diff --git a/src/Shared/Components/UserIdentifier/types.ts b/src/Shared/Components/UserIdentifier/types.ts new file mode 100644 index 000000000..7a2e3b8dc --- /dev/null +++ b/src/Shared/Components/UserIdentifier/types.ts @@ -0,0 +1,12 @@ +import { ReactNode } from 'react' + +export interface UserIdentifierProps { + identifier: string + children?: ReactNode + isUserGroup?: boolean + rootClassName?: string + /** + * @description - If given, would show tooltip on div containing avatar, email and children + */ + tooltipContent?: string +} diff --git a/src/Shared/Components/index.ts b/src/Shared/Components/index.ts index 14503779c..abcb23727 100644 --- a/src/Shared/Components/index.ts +++ b/src/Shared/Components/index.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +export * from './AboutDevtron' export * from './ActionMenu' export * from './ActivityIndicator' export * from './AnimatedDeployButton' @@ -29,7 +30,7 @@ export * from './ButtonWithLoader' export * from './ButtonWithSelector' export * from './CICDHistory' export * from './CMCS' -export * from './CodeEditorWrapper' +export * from './CodeEditor' export * from './Collapse' export * from './CollapsibleList' export * from './CommitChipCell' @@ -95,5 +96,6 @@ export * from './ThemeSwitcher' export * from './ToggleResolveScopedVariables' export * from './UnsavedChanges' export * from './UnsavedChangesDialog' +export * from './UserIdentifier' export * from './VirtualizedList' export * from './WorkflowOptionsModal' diff --git a/src/Shared/Helpers.tsx b/src/Shared/Helpers.tsx index eda31ea24..b73a459ad 100644 --- a/src/Shared/Helpers.tsx +++ b/src/Shared/Helpers.tsx @@ -790,19 +790,31 @@ export const getTimeDifference = ({ return fallbackString } - const seconds = moment(endTime).diff(moment(startTime), 'seconds') - const minutes = moment(endTime).diff(moment(startTime), 'minutes') - const hours = moment(endTime).diff(moment(startTime), 'hours') - - if (seconds < 60) { - return `${seconds}s` + const start = moment(startTime) + const end = moment(endTime) + if (!start.isValid() || !end.isValid()) { + return fallbackString } - if (minutes < 60) { - return `${minutes}m ${seconds % 60}s` + + const diff = Math.abs(end.diff(start)) + const duration = moment.duration(diff) + + const units = [ + { label: 'd', value: duration.days() }, + { label: 'h', value: duration.hours() }, + { label: 'm', value: duration.minutes() }, + { label: 's', value: duration.seconds() }, + ] + + // Filter out zero values and take the first two non-zero units + const nonZeroUnits = units.filter((unit) => unit.value > 0).slice(0, 2) + + // If all units are zero, show "0s" + if (nonZeroUnits.length === 0) { + return '0s' } - const leftOverMinutes = minutes - hours * 60 - const leftOverSeconds = seconds - minutes * 60 - return `${hours}h ${leftOverMinutes}m ${leftOverSeconds}s` + + return nonZeroUnits.map((unit) => `${unit.value}${unit.label}`).join(' ') } export const getFileNameFromHeaders = (headers: Headers) => diff --git a/src/Shared/Providers/types.ts b/src/Shared/Providers/types.ts index 68af58238..d3d456ad2 100644 --- a/src/Shared/Providers/types.ts +++ b/src/Shared/Providers/types.ts @@ -18,7 +18,7 @@ import { Dispatch, MutableRefObject, ReactNode, SetStateAction } from 'react' import { SERVER_MODE } from '../../Common' import { ServerInfo } from '../Components/Header/types' -import { DevtronLicenseInfo, LicenseInfoDialogType } from '..' +import { DevtronLicenseInfo, IntelligenceConfig, LicenseInfoDialogType } from '..' export interface MainContext { serverMode: SERVER_MODE @@ -66,6 +66,8 @@ export interface MainContext { licenseData: DevtronLicenseInfo setLicenseData: Dispatch> canFetchHelmAppStatus: boolean + intelligenceConfig: IntelligenceConfig + setIntelligenceConfig: Dispatch> } export interface MainContextProviderProps { diff --git a/src/Shared/Services/app.types.ts b/src/Shared/Services/app.types.ts index 399b195af..e04c22c39 100644 --- a/src/Shared/Services/app.types.ts +++ b/src/Shared/Services/app.types.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { KeyValueTableData } from '@Shared/Components' import { TargetPlatformsDTO } from '@Shared/types' import { OverrideMergeStrategyType } from '@Pages/Applications' @@ -241,12 +242,6 @@ export interface ConfigMapSecretDataType { isDeletable: boolean } -export interface CMSecretYamlData { - k: string - v: string - id: string | number -} - export interface ConfigMapSecretUseFormProps { name: string isSecret: boolean @@ -261,7 +256,7 @@ export interface ConfigMapSecretUseFormProps { roleARN: string yamlMode: boolean yaml: string - currentData: CMSecretYamlData[] + currentData: KeyValueTableData[] secretDataYaml: string esoSecretYaml: string hasCurrentDataErr: boolean diff --git a/src/Shared/constants.tsx b/src/Shared/constants.tsx index ae2c2d9be..d53848f04 100644 --- a/src/Shared/constants.tsx +++ b/src/Shared/constants.tsx @@ -538,6 +538,8 @@ export const DC_DELETE_SUBTITLES = { export const EULA_LINK = 'https://devtron.ai/end-user-license-agreement-eula' export const CONTACT_SUPPORT_LINK = 'https://devtron.ai/enterprise-support' +export const PRIVACY_POLICY_LINK = 'https://devtron.ai/privacy-policy' +export const TERMS_OF_USE_LINK = 'https://devtron.ai/terms-of-use' export const enum DeleteComponentsName { Cluster = 'cluster', diff --git a/src/Shared/types.ts b/src/Shared/types.ts index 4b837338d..869bd4357 100644 --- a/src/Shared/types.ts +++ b/src/Shared/types.ts @@ -298,6 +298,8 @@ export interface WorkflowType { showTippy?: boolean appId?: number isSelected?: boolean + isExceptionUser?: boolean + canApproverDeploy?: boolean approvalConfiguredIdsMap?: Record imageReleaseTags: string[] appReleaseTags?: string[] @@ -1150,3 +1152,10 @@ export enum RegistryCredentialsType { USERNAME_PASSWORD = 'username_password', ANONYMOUS = 'anonymous', } + +export interface IntelligenceConfig { + clusterId: number + metadata: Record + prompt: string + analyticsCategory: string +} diff --git a/src/index.ts b/src/index.ts index f582c59a3..4d5f80fe0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -87,11 +87,6 @@ export interface customEnv { FEATURE_DEFAULT_MERGE_STRATEGY?: OverrideMergeStrategyType FEATURE_DEFAULT_LANDING_RB_ENABLE?: boolean FEATURE_ACTION_AUDIOS_ENABLE?: boolean - /** - * If true, the code-editor will use codemirror engine - * @default false - */ - FEATURE_CODE_MIRROR_ENABLE?: boolean // ================== Feature flags for the enterprise release ================== /** * If true, only pipelines to which the user has access will be shown across the application @@ -155,6 +150,8 @@ export interface customEnv { */ FEATURE_APPLICATION_TEMPLATES_ENABLE?: boolean GATEKEEPER_URL?: string + FEATURE_AI_INTEGRATION_ENABLE?: boolean + LOGIN_PAGE_IMAGE?: string } declare global { interface Window { diff --git a/vite.config.ts b/vite.config.ts index 232f92d82..e62fa6155 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -64,13 +64,6 @@ export default defineConfig({ assetFileNames: 'assets/[name][extname]', entryFileNames: '[name].js', manualChunks(id: string) { - if ( - id.includes('/node_modules/monaco-editor') || - id.includes('/node_modules/react-monaco-editor') - ) { - return '@monaco-editor' - } - if (id.includes('/node_modules/react-dates')) { return '@react-dates' } @@ -95,7 +88,7 @@ export default defineConfig({ return '@vendor' } - if (id.includes('src/Common/CodeEditor')) { + if (id.includes('codemirror') || id.includes('src/Common/CodeMirror')) { return '@code-editor' }