diff --git a/package-lock.json b/package-lock.json index d2db8046b..3e097b652 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.14.2", + "version": "1.15.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.14.2", + "version": "1.15.0", "hasInstallScript": true, "license": "ISC", "dependencies": { @@ -36,7 +36,6 @@ "qrcode.react": "^4.2.0", "react-canvas-confetti": "^2.0.7", "react-dates": "^21.8.0", - "react-diff-viewer-continued": "^3.4.0", "react-draggable": "^4.4.5", "react-international-phone": "^4.5.0", "react-virtualized-sticky-tree": "^3.0.0-beta18", @@ -777,6 +776,7 @@ "version": "11.12.0", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.12.0.tgz", "integrity": "sha512-y2WQb+oP8Jqvvclh8Q55gLUyb7UFvgv7eJfsj7td5TToBrIUtPay2kMrZi4xjq9qw2vD0ZR5fSho0yqoFgX7Rw==", + "peer": true, "dependencies": { "@babel/helper-module-imports": "^7.16.7", "@babel/runtime": "^7.18.3", @@ -794,12 +794,14 @@ "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "peer": true }, "node_modules/@emotion/cache": { "version": "11.13.1", "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.1.tgz", "integrity": "sha512-iqouYkuEblRcXmylXIwwOodiEK5Ifl7JcX7o6V4jI3iW4mLXX3dmt5xwBtIkJiQEXFAI+pC8X0i67yiPkH9Ucw==", + "peer": true, "dependencies": { "@emotion/memoize": "^0.9.0", "@emotion/sheet": "^1.4.0", @@ -808,23 +810,11 @@ "stylis": "4.2.0" } }, - "node_modules/@emotion/css": { - "version": "11.13.4", - "resolved": "https://registry.npmjs.org/@emotion/css/-/css-11.13.4.tgz", - "integrity": "sha512-CthbOD5EBw+iN0rfM96Tuv5kaZN4nxPyYDvGUs0bc7wZBBiU/0mse+l+0O9RshW2d+v5HH1cme+BAbLJ/3Folw==", - "license": "MIT", - "dependencies": { - "@emotion/babel-plugin": "^11.12.0", - "@emotion/cache": "^11.13.0", - "@emotion/serialize": "^1.3.0", - "@emotion/sheet": "^1.4.0", - "@emotion/utils": "^1.4.0" - } - }, "node_modules/@emotion/hash": { "version": "0.9.2", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", - "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==" + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "peer": true }, "node_modules/@emotion/is-prop-valid": { "version": "0.8.8", @@ -846,7 +836,8 @@ "node_modules/@emotion/memoize": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", - "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==" + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "peer": true }, "node_modules/@emotion/react": { "version": "11.13.3", @@ -876,6 +867,7 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.2.tgz", "integrity": "sha512-grVnMvVPK9yUVE6rkKfAJlYZgo0cu3l9iMC77V7DW6E1DUIrU68pSEXRmFZFOFB1QFo57TncmOcvcbMDWsL4yA==", + "peer": true, "dependencies": { "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", @@ -887,12 +879,14 @@ "node_modules/@emotion/sheet": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", - "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==" + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "peer": true }, "node_modules/@emotion/unitless": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", - "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==" + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "peer": true }, "node_modules/@emotion/use-insertion-effect-with-fallbacks": { "version": "1.1.0", @@ -906,12 +900,14 @@ "node_modules/@emotion/utils": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.1.tgz", - "integrity": "sha512-BymCXzCG3r72VKJxaYVwOXATqXIZ85cuvg0YOUDxMGNrKc1DJRZk8MgV5wyXRyEayIMd4FuXJIUgTBXvDNW5cA==" + "integrity": "sha512-BymCXzCG3r72VKJxaYVwOXATqXIZ85cuvg0YOUDxMGNrKc1DJRZk8MgV5wyXRyEayIMd4FuXJIUgTBXvDNW5cA==", + "peer": true }, "node_modules/@emotion/weak-memoize": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", - "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==" + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "peer": true }, "node_modules/@esbuild-plugins/node-globals-polyfill": { "version": "0.2.3", @@ -4907,6 +4903,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "peer": true, "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", @@ -5160,12 +5157,6 @@ "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", "license": "MIT" }, - "node_modules/classnames": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", - "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", - "license": "MIT" - }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -5826,15 +5817,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -7090,7 +7072,8 @@ "node_modules/find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "peer": true }, "node_modules/find-up": { "version": "5.0.0", @@ -9112,7 +9095,8 @@ "node_modules/memoize-one": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", - "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "peer": true }, "node_modules/merge-stream": { "version": "2.0.0", @@ -10249,26 +10233,6 @@ "react-with-direction": "^1.3.1" } }, - "node_modules/react-diff-viewer-continued": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/react-diff-viewer-continued/-/react-diff-viewer-continued-3.4.0.tgz", - "integrity": "sha512-kMZmUyb3Pv5L9vUtCfIGYsdOHs8mUojblGy1U1Sm0D7FhAOEsH9QhnngEIRo5hXWIPNGupNRJls1TJ6Eqx84eg==", - "license": "MIT", - "dependencies": { - "@emotion/css": "^11.11.2", - "classnames": "^2.3.2", - "diff": "^5.1.0", - "memoize-one": "^6.0.0", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">= 8" - }, - "peerDependencies": { - "react": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/react-dom": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", @@ -11151,6 +11115,7 @@ "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -11488,7 +11453,8 @@ "node_modules/stylis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", - "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "peer": true }, "node_modules/supports-color": { "version": "7.2.0", diff --git a/package.json b/package.json index 130def67b..50b1a46a8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.14.2", + "version": "1.15.0", "description": "Supporting common component library", "type": "module", "main": "dist/index.js", @@ -122,7 +122,6 @@ "qrcode.react": "^4.2.0", "react-canvas-confetti": "^2.0.7", "react-dates": "^21.8.0", - "react-diff-viewer-continued": "^3.4.0", "react-draggable": "^4.4.5", "react-international-phone": "^4.5.0", "react-virtualized-sticky-tree": "^3.0.0-beta18", diff --git a/src/Assets/IconV2/ic-app-template.svg b/src/Assets/IconV2/ic-app-template.svg new file mode 100644 index 000000000..70a64cf7a --- /dev/null +++ b/src/Assets/IconV2/ic-app-template.svg @@ -0,0 +1,23 @@ + + + + + + + + + diff --git a/src/Assets/IconV2/ic-devtron-app.svg b/src/Assets/IconV2/ic-devtron-app.svg new file mode 100644 index 000000000..adc694a17 --- /dev/null +++ b/src/Assets/IconV2/ic-devtron-app.svg @@ -0,0 +1,23 @@ + + + + + + + + + diff --git a/src/Assets/IconV2/ic-devtron-job.svg b/src/Assets/IconV2/ic-devtron-job.svg new file mode 100644 index 000000000..d5a2bc1bd --- /dev/null +++ b/src/Assets/IconV2/ic-devtron-job.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/Assets/IconV2/ic-email.svg b/src/Assets/IconV2/ic-email.svg new file mode 100644 index 000000000..27c3e1fd8 --- /dev/null +++ b/src/Assets/IconV2/ic-email.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Assets/IconV2/ic-key-enter.svg b/src/Assets/IconV2/ic-key-enter.svg new file mode 100644 index 000000000..d6b668125 --- /dev/null +++ b/src/Assets/IconV2/ic-key-enter.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Assets/IconV2/ic-spray-can.svg b/src/Assets/IconV2/ic-spray-can.svg new file mode 100644 index 000000000..d7ac168eb --- /dev/null +++ b/src/Assets/IconV2/ic-spray-can.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Assets/Img/empty-create.png b/src/Assets/Img/empty-create.png new file mode 100755 index 000000000..705f13dd7 Binary files /dev/null and b/src/Assets/Img/empty-create.png differ diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index bd6cc21a5..e0de61a42 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -19,19 +19,10 @@ export const Host = window?.__ORCHESTRATOR_ROOT__ ?? '/orchestrator' export const DOCUMENTATION_HOME_PAGE = 'https://docs.devtron.ai' export const DEVTRON_HOME_PAGE = 'https://devtron.ai/' -export const DOCUMENTATION_VERSION = '/v/v0.7' +export const DOCUMENTATION_VERSION = '/devtron/v0.7' export const DISCORD_LINK = 'https://discord.devtron.ai/' export const DEFAULT_JSON_SCHEMA_URI = 'https://json-schema.org/draft/2020-12/schema' -export const DOCUMENTATION = { - APP_METRICS: `${DOCUMENTATION_HOME_PAGE}${DOCUMENTATION_VERSION}/usage/applications/app-details/app-metrics`, - APP_TAGS: `${DOCUMENTATION_HOME_PAGE}${DOCUMENTATION_VERSION}/usage/applications/create-application#tags`, - APP_OVERVIEW_TAGS: `${DOCUMENTATION_HOME_PAGE}${DOCUMENTATION_VERSION}/usage/applications/overview#manage-tags`, - BLOB_STORAGE: `${DOCUMENTATION_HOME_PAGE}${DOCUMENTATION_VERSION}/getting-started/install/installation-configuration#configuration-of-blob-storage`, - GLOBAL_CONFIG_BUILD_INFRA: `${DOCUMENTATION_HOME_PAGE}${DOCUMENTATION_VERSION}/global-configurations/build-infra`, - ENTERPRISE_LICENSE: `${DOCUMENTATION_HOME_PAGE}/enterprise-license`, - KUBE_CONFIG: `${DOCUMENTATION_HOME_PAGE}${DOCUMENTATION_VERSION}/usage/resource-browser#running-kubectl-commands-locally`, - TENANT_INSTALLATION: `${DOCUMENTATION_HOME_PAGE}${DOCUMENTATION_VERSION}/usage/software-distribution-hub/tenants`, -} +export const LICENSE_DASHBOARD_HOME_PAGE = 'https://license.devtron.ai/dashboard' export const PATTERNS = { STRING: /^[a-zA-Z0-9_]+$/, @@ -51,11 +42,13 @@ export const PATTERNS = { CONFIG_MAP_AND_SECRET_KEY: /^[-._a-zA-Z0-9]+$/, CONFIGMAP_AND_SECRET_NAME: /^[a-z0-9][a-z0-9-.]*[a-z0-9]$/, ALPHANUMERIC_WITH_SPECIAL_CHAR_AND_SLASH: /^[A-Za-z0-9._/-]+$/, // allow alphanumeric,(.) ,(-),(_),(/) + EMAIL: /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, } const GLOBAL_CONFIG_TEMPLATES_DEVTRON_APP = '/global-config/templates/devtron-apps' export const URLS = { + LOGIN: '/login', LOGIN_SSO: '/login/sso', PERMISSION_GROUPS: '/global-config/auth/groups', APP: '/app', diff --git a/src/Common/CustomTagSelector/PropagateTagInfo.tsx b/src/Common/CustomTagSelector/PropagateTagInfo.tsx index e15f2d119..3bc57a9cf 100644 --- a/src/Common/CustomTagSelector/PropagateTagInfo.tsx +++ b/src/Common/CustomTagSelector/PropagateTagInfo.tsx @@ -14,12 +14,10 @@ * limitations under the License. */ -import React from 'react' import { ReactComponent as InjectTag } from '../../Assets/Icon/inject-tag.svg' import { ReactComponent as ICHelpOutline } from '../../Assets/Icon/ic-help-outline.svg' import { TippyCustomized } from '../TippyCustomized' import { TippyTheme } from '../Types' -import { DOCUMENTATION } from '../Constants' export default function PropagateTagInfo({ isCreateApp }: { isCreateApp: boolean }) { const additionalInfo = () => ( @@ -47,8 +45,9 @@ export default function PropagateTagInfo({ isCreateApp }: { isCreateApp: boolean showCloseButton trigger="click" interactive - documentationLink={isCreateApp ? DOCUMENTATION.APP_TAGS : DOCUMENTATION.APP_OVERVIEW_TAGS} + documentationLink={isCreateApp ? "APP_TAGS" : "APP_OVERVIEW_TAGS"} documentationLinkText="View Documentation" + openInNewTab >
diff --git a/src/Common/CustomTagSelector/ResizableTagTextArea.tsx b/src/Common/CustomTagSelector/ResizableTagTextArea.tsx index 2a15f9b33..432d2dc17 100644 --- a/src/Common/CustomTagSelector/ResizableTagTextArea.tsx +++ b/src/Common/CustomTagSelector/ResizableTagTextArea.tsx @@ -89,7 +89,9 @@ export const ResizableTagTextArea = ({ } useEffect(() => { - reInitHeight() + setTimeout(() => { + reInitHeight() + }, 100) }, []) useThrottledEffect(reInitHeight, 500, [value]) diff --git a/src/Common/SegmentedControl/Segment.tsx b/src/Common/SegmentedControl/Segment.tsx index 437fc9020..c4b23fdb3 100644 --- a/src/Common/SegmentedControl/Segment.tsx +++ b/src/Common/SegmentedControl/Segment.tsx @@ -1,4 +1,5 @@ import { ReactElement, useMemo } from 'react' +import { motion } from 'framer-motion' import { Tooltip } from '@Common/Tooltip' import { Icon } from '@Shared/Components' @@ -15,16 +16,7 @@ const wrapWithTooltip = (tooltipProps: SegmentType['tooltipProps']) => (children ) -const Segment = ({ - segment, - isSelected, - name, - selectedSegmentRef, - onChange, - fullWidth, - size, - disabled, -}: SegmentProps) => { +const Segment = ({ segment, isSelected, name, onChange, fullWidth, size, disabled }: SegmentProps) => { const inputId = useMemo(getUniqueId, []) const { value, icon, isError, label, tooltipProps, ariaLabel } = segment @@ -34,10 +26,7 @@ const Segment = ({ return ( -
+
- +
) diff --git a/src/Common/SegmentedControl/SegmentedControl.component.tsx b/src/Common/SegmentedControl/SegmentedControl.component.tsx index 92af6cf7f..9b7e9a9c5 100644 --- a/src/Common/SegmentedControl/SegmentedControl.component.tsx +++ b/src/Common/SegmentedControl/SegmentedControl.component.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react' +import { useState } from 'react' import { ComponentSizeType } from '@Shared/constants' @@ -17,25 +17,9 @@ const SegmentedControl = ({ disabled, }: SegmentedControlProps) => { const isUnControlledComponent = controlledValue === undefined - - const segmentedControlRefContainer = useRef(null) - /** - * Using this ref to show the selected segment highlight with transition - */ - const selectedSegmentRef = useRef(null) const [selectedSegmentValue, setSelectedSegmentValue] = useState(segments[0].value) const segmentValue = isUnControlledComponent ? selectedSegmentValue : controlledValue - useEffect(() => { - if (segmentValue) { - const { offsetWidth, offsetLeft } = selectedSegmentRef.current - const { style } = segmentedControlRefContainer.current - - style.setProperty('--segmented-control-highlight-width', `${offsetWidth}px`) - style.setProperty('--segmented-control-highlight-x-position', `${offsetLeft}px`) - } - }, [segmentValue, size, fullWidth]) - const handleSegmentChange = (updatedSegment: SegmentType) => { if (isUnControlledComponent) { setSelectedSegmentValue(updatedSegment.value) @@ -47,16 +31,12 @@ const SegmentedControl = ({
-
+
{segments.map((segment) => { const isSelected = segment.value === segmentValue return ( *:not(.active-mask) { + z-index: 1; + } + } + + &:hover:not(#{$parent-selector}--selected):not(.cursor-not-allowed) { + background-color: var(--bg-hover); } } } diff --git a/src/Common/SegmentedControl/types.ts b/src/Common/SegmentedControl/types.ts index 32ce5c8b4..649fa0801 100644 --- a/src/Common/SegmentedControl/types.ts +++ b/src/Common/SegmentedControl/types.ts @@ -1,5 +1,3 @@ -import { RefObject } from 'react' - import { TooltipProps } from '@Common/Tooltip' import { IconsProps, SelectPickerOptionType } from '@Shared/Components' import { ComponentSizeType } from '@Shared/constants' @@ -56,6 +54,9 @@ export type SegmentedControlProps = { * List of segments to be displayed */ segments: SegmentType[] + /** + * Please make sure this is unique + */ name: string size?: Extract fullWidth?: boolean @@ -81,5 +82,4 @@ export interface SegmentProps extends Required> { isSelected: boolean segment: SegmentType - selectedSegmentRef: RefObject | undefined } diff --git a/src/Common/TippyCustomized.tsx b/src/Common/TippyCustomized.tsx index fbfaa4ec9..c8d64c1e7 100644 --- a/src/Common/TippyCustomized.tsx +++ b/src/Common/TippyCustomized.tsx @@ -19,15 +19,15 @@ import Tippy from '@tippyjs/react' import { ReactComponent as CloseIcon } from '../Assets/Icon/ic-cross.svg' import { ReactComponent as Help } from '../Assets/Icon/ic-help.svg' import { ReactComponent as ICHelpOutline } from '../Assets/Icon/ic-help-outline.svg' -import { ReactComponent as ICOpenInNew } from '../Assets/Icon/ic-open-in-new.svg' import 'tippy.js/animations/shift-toward-subtle.css' import 'tippy.js/animations/shift-toward.css' import { TippyCustomizedProps, TippyTheme } from './Types' import { not, stopPropagation } from './Helper' +import { DocLink } from '@Shared/Components' // This component will handle some of the new tippy designs and interactions // So this can be updated to support further for new features or interactions -export const TippyCustomized = (props: TippyCustomizedProps) => { +export const TippyCustomized = (props: TippyCustomizedProps) => { const tippyRef = useRef(null) const [showHeadingInfo, setShowHeadingInfo] = useState(false) const isWhiteTheme = props.theme === TippyTheme.white @@ -79,6 +79,8 @@ export const TippyCustomized = (props: TippyCustomizedProps) => { additionalContent, documentationLink, documentationLinkText, + isExternalLink, + openInNewTab, } = props return ( <> @@ -156,17 +158,16 @@ export const TippyCustomized = (props: TippyCustomizedProps) => { )} {additionalContent} {documentationLink && ( -
- + - {documentationLinkText || 'Learn more'} - - + isExternalLink={isExternalLink} + docLinkKey={documentationLink} + openInNewTab={openInNewTab} + />
)} diff --git a/src/Common/Types.ts b/src/Common/Types.ts index 9f5b940f2..94c5a2ba2 100644 --- a/src/Common/Types.ts +++ b/src/Common/Types.ts @@ -28,6 +28,7 @@ import { ButtonProps, ComponentLayoutType, StatusType, + DocLinkProps, DeploymentStrategyType, } from '../Shared' import { @@ -121,52 +122,59 @@ export interface CheckboxProps { children?: ReactNode } -export interface TippyCustomizedProps extends Pick { - theme: TippyTheme - visible?: boolean - heading?: ReactNode | string - headingInfo?: ReactNode | string - noHeadingBorder?: boolean - infoTextHeading?: string - hideHeading?: boolean - placement?: TippyProps['placement'] - className?: string - Icon?: React.FunctionComponent> - iconPath?: string - iconClass?: string - iconSize?: number // E.g. 16, 20, etc.. Currently, there are around 12 sizes supported. Check `icons.css` or `base.scss` for supported sizes or add new size (class names starts with `icon-dim-`). - onImageLoadError?: (e) => void - onClose?: () => void - infoText?: React.ReactNode - showCloseButton?: boolean - arrow?: boolean - interactive?: boolean - showOnCreate?: boolean - trigger?: string - animation?: string - duration?: number - additionalContent?: ReactNode - documentationLink?: string - documentationLinkText?: string - children: React.ReactElement - disableClose?: boolean -} - -export interface InfoIconTippyProps +export interface TippyWithBaseDocLinkTypes + extends Pick, 'isExternalLink' | 'openInNewTab'> { + documentationLink?: DocLinkProps['docLinkKey'] +} + +export type TippyCustomizedProps = Pick & + TippyWithBaseDocLinkTypes & { + theme: TippyTheme + visible?: boolean + heading?: ReactNode | string + headingInfo?: ReactNode | string + noHeadingBorder?: boolean + infoTextHeading?: string + hideHeading?: boolean + placement?: TippyProps['placement'] + className?: string + Icon?: React.FunctionComponent> + iconPath?: string + iconClass?: string + iconSize?: number // E.g. 16, 20, etc.. Currently, there are around 12 sizes supported. Check `icons.css` or `base.scss` for supported sizes or add new size (class names starts with `icon-dim-`). + onImageLoadError?: (e) => void + onClose?: () => void + infoText?: React.ReactNode + showCloseButton?: boolean + arrow?: boolean + interactive?: boolean + showOnCreate?: boolean + trigger?: string + animation?: string + duration?: number + additionalContent?: ReactNode + documentationLinkText?: string + children: React.ReactElement + disableClose?: boolean + } + +export interface InfoIconTippyProps extends Pick< - TippyCustomizedProps, + TippyCustomizedProps, | 'heading' | 'infoText' | 'iconClass' - | 'documentationLink' | 'documentationLinkText' | 'additionalContent' | 'placement' | 'Icon' | 'headingInfo' + | 'documentationLink' + | 'isExternalLink' + | 'openInNewTab' > { dataTestid?: string - children?: TippyCustomizedProps['children'] + children?: TippyCustomizedProps['children'] iconClassName?: string buttonPadding?: string } diff --git a/src/Pages/Applications/DevtronApps/Details/AppConfigurations/DeploymentTemplate/DTApplicationMetricsFormField.tsx b/src/Pages/Applications/DevtronApps/Details/AppConfigurations/DeploymentTemplate/DTApplicationMetricsFormField.tsx index 4ca82f852..5ffea8c0e 100644 --- a/src/Pages/Applications/DevtronApps/Details/AppConfigurations/DeploymentTemplate/DTApplicationMetricsFormField.tsx +++ b/src/Pages/Applications/DevtronApps/Details/AppConfigurations/DeploymentTemplate/DTApplicationMetricsFormField.tsx @@ -16,7 +16,6 @@ import { ReactComponent as ICInfoFilledOverride } from '@Icons/ic-info-filled-override.svg' import { Checkbox } from '@Common/Checkbox' -import { DOCUMENTATION } from '@Common/Constants' import { Progressing } from '@Common/Progressing' import { Tooltip } from '@Common/Tooltip' import { CHECKBOX_VALUE } from '@Common/Types' @@ -96,7 +95,7 @@ const DTApplicationMetricsFormField = ({ diff --git a/src/Shared/Components/AboutDevtron/AboutDevtronBody.tsx b/src/Shared/Components/AboutDevtron/AboutDevtronBody.tsx index f5bd435de..a4fb3d166 100644 --- a/src/Shared/Components/AboutDevtron/AboutDevtronBody.tsx +++ b/src/Shared/Components/AboutDevtron/AboutDevtronBody.tsx @@ -5,14 +5,12 @@ import { EULA_LINK, PRIVACY_POLICY_LINK, TERMS_OF_USE_LINK } from '@Shared/const 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 { currentServerInfo, isEnterprise } = useMainContext() const currentVersion = currentServerInfo?.serverInfo?.currentVersion - const isEnterprise = currentServerInfo?.serverInfo?.installationType === InstallationType.ENTERPRISE const isVersionCompatible = isFELibAvailable === isEnterprise diff --git a/src/Shared/Components/ActionMenu/ActionMenu.component.tsx b/src/Shared/Components/ActionMenu/ActionMenu.component.tsx index 938301e16..8dd8d6f92 100644 --- a/src/Shared/Components/ActionMenu/ActionMenu.component.tsx +++ b/src/Shared/Components/ActionMenu/ActionMenu.component.tsx @@ -1,10 +1,8 @@ -import { MutableRefObject } from 'react' - import { CustomInput } from '../CustomInput' import { Popover } from '../Popover' import { SelectPickerMenuListFooter } from '../SelectPicker/common' import { ActionMenuItem } from './ActionMenuItem' -import { ActionMenuItemType, ActionMenuProps } from './types' +import { ActionMenuItemProps, ActionMenuProps } from './types' import { useActionMenu } from './useActionMenu.hook' import './actionMenu.scss' @@ -51,8 +49,8 @@ export const ActionMenu = ({ // HANDLERS const handleOptionMouseEnter = (index: number) => () => setFocusedIndex(index) - const handleOptionOnClick = (item: ActionMenuItemType) => () => { - onClick(item) + const handleOptionOnClick: ActionMenuItemProps['onClick'] = (item, e) => { + onClick(item, e) closePopover() } @@ -82,7 +80,7 @@ export const ActionMenu = ({
)}
    } + ref={scrollableRef} role="menu" className="action-menu m-0 p-0 flex-grow-1 dc__overflow-auto dc__overscroll-none" > @@ -114,7 +112,7 @@ export const ActionMenu = ({ itemRef={itemsRef.current[index]} isFocused={index === focusedIndex} onMouseEnter={handleOptionMouseEnter(index)} - onClick={handleOptionOnClick(item)} + onClick={handleOptionOnClick} disableDescriptionEllipsis={disableDescriptionEllipsis} /> ) @@ -132,7 +130,7 @@ export const ActionMenu = ({ )}
{footerConfig && ( -
+
)} diff --git a/src/Shared/Components/ActionMenu/ActionMenuItem.tsx b/src/Shared/Components/ActionMenu/ActionMenuItem.tsx index 96300da47..b140129d1 100644 --- a/src/Shared/Components/ActionMenu/ActionMenuItem.tsx +++ b/src/Shared/Components/ActionMenu/ActionMenuItem.tsx @@ -1,13 +1,17 @@ -import { LegacyRef, Ref } from 'react' +import { LegacyRef, MouseEvent, Ref } from 'react' import { Link } from 'react-router-dom' import { Tooltip } from '@Common/Tooltip' +import { ComponentSizeType } from '@Shared/constants' +import { Button, ButtonProps, ButtonVariantType } from '../Button' import { Icon } from '../Icon' +import { NumbersCount } from '../NumbersCount' import { getTooltipProps } from '../SelectPicker/common' -import { ActionMenuItemProps } from './types' +import { DTSwitch, DTSwitchProps } from '../Switch' +import { ActionMenuItemProps, ActionMenuItemType } from './types' -const COMMON_ACTION_MENU_ITEM_CLASS = 'flex-grow-1 flex left top dc__gap-8 py-6 px-8' +const COMMON_ACTION_MENU_ITEM_CLASS = 'w-100 flex left top dc__gap-8 py-6 px-8' export const ActionMenuItem = ({ item, @@ -22,7 +26,7 @@ export const ActionMenuItem = ({ description, label, startIcon, - endIcon, + trailingItem, tooltipProps, type = 'neutral', isDisabled, @@ -40,22 +44,83 @@ export const ActionMenuItem = ({ const isNegativeType = type === 'negative' // HANDLERS - const handleClick = () => { - onClick(item) + const handleClick = (e: MouseEvent | MouseEvent) => { + onClick(item, e) } + const handleTrailingSwitchChange = + ({ type: trailingItemType, config }: ActionMenuItemType['trailingItem']): DTSwitchProps['onChange'] => + (e) => { + if (trailingItemType === 'switch') { + e.stopPropagation() + config.onChange(e) + } + } + + const handleTrailingButtonClick = + ({ type: trailingItemType, config }: ActionMenuItemType['trailingItem']): ButtonProps['onClick'] => + (e) => { + e.stopPropagation() + if (trailingItemType === 'button' && config.onClick) { + config.onClick(e) + } + } + // RENDERERS const renderIcon = (iconProps: typeof startIcon) => iconProps && ( -
+ -
+ ) + const renderTrailingItem = () => { + if (!trailingItem) { + return null + } + + const { type: trailingItemType, config } = trailingItem + + switch (trailingItemType) { + case 'icon': + return renderIcon(config) + case 'text': { + const { value, icon } = config + return ( + + {value} + {icon && } + + ) + } + case 'counter': + return + case 'switch': + return ( + + ) + case 'button': + return ( + @@ -124,7 +192,6 @@ export const ActionMenuItem = ({ tabIndex={-1} // Intentionally added margin to the left and right to have the gap on the edges of the options className={`action-menu__option br-4 mr-4 ml-4 ${isDisabled ? 'dc__disabled' : 'cursor'} ${isNegativeType ? 'dc__hover-r50' : 'dc__hover-n50'} ${isFocused ? `action-menu__option--focused${isNegativeType ? '-negative' : ''}` : ''}`} - onClick={!isDisabled ? handleClick : undefined} aria-disabled={isDisabled} > {renderComponent()} diff --git a/src/Shared/Components/ActionMenu/types.ts b/src/Shared/Components/ActionMenu/types.ts index 599212c4f..838a93f03 100644 --- a/src/Shared/Components/ActionMenu/types.ts +++ b/src/Shared/Components/ActionMenu/types.ts @@ -1,9 +1,14 @@ -import { LegacyRef, Ref } from 'react' +import { LegacyRef, MouseEvent, ReactElement, Ref } from 'react' import { LinkProps } from 'react-router-dom' +import { OmitNever } from '@Shared/types' + +import { ButtonProps } from '../Button' import { IconsProps } from '../Icon' +import { NumbersCountProps } from '../NumbersCount' import { PopoverProps, UsePopoverProps } from '../Popover' import { SelectPickerOptionType, SelectPickerProps } from '../SelectPicker' +import { DTSwitchProps } from '../Switch' type ConditionalActionMenuComponentType = | { @@ -32,6 +37,43 @@ type ActionMenuItemIconType = Pick & { color?: IconsProps['color'] } +type TrailingItemType = + | { + type: 'icon' + config: ActionMenuItemIconType + } + | { + type: 'text' + config: { + value: string + icon?: ActionMenuItemIconType + } + } + | { + type: 'counter' + config: { + value: NumbersCountProps['count'] + } + } + | { + type: 'switch' + config: Pick< + DTSwitchProps, + | 'ariaLabel' + | 'isChecked' + | 'indeterminate' + | 'isDisabled' + | 'isLoading' + | 'name' + | 'onChange' + | 'tooltipContent' + > + } + | { + type: 'button' + config: OmitNever, 'size' | 'variant'>> + } + export type ActionMenuItemType = Omit< SelectPickerOptionType, 'label' | 'value' | 'endIcon' | 'startIcon' @@ -49,8 +91,8 @@ export type ActionMenuItemType = Om type?: 'neutral' | 'negative' /** Defines the icon to be displayed at the start of the menu item. */ startIcon?: ActionMenuItemIconType - /** Defines the icon to be displayed at the end of the menu item. */ - endIcon?: ActionMenuItemIconType + /** Defines the item to be displayed at the end of the menu item. */ + trailingItem?: TrailingItemType } & ConditionalActionMenuComponentType export type ActionMenuOptionType = { @@ -85,7 +127,7 @@ export type ActionMenuProps = UseAc * Callback function triggered when an item is clicked. * @param item - The selected item. */ - onClick: (item: ActionMenuItemType) => void + onClick: (item: ActionMenuItemType, e: MouseEvent | MouseEvent) => void /** * Config for the footer at the bottom of action menu list. It is sticky by default */ diff --git a/src/Shared/Components/ActionMenu/useActionMenu.hook.ts b/src/Shared/Components/ActionMenu/useActionMenu.hook.ts index 4898d836d..6d78b2f5a 100644 --- a/src/Shared/Components/ActionMenu/useActionMenu.hook.ts +++ b/src/Shared/Components/ActionMenu/useActionMenu.hook.ts @@ -1,4 +1,4 @@ -import { ChangeEvent, createRef, RefObject, useEffect, useMemo, useRef, useState } from 'react' +import { ChangeEvent, createRef, RefObject, useEffect, useRef, useState } from 'react' import { usePopover, UsePopoverProps } from '../Popover' import { UseActionMenuProps } from './types' @@ -17,13 +17,9 @@ export const useActionMenu = ({ const [focusedIndex, setFocusedIndex] = useState(-1) const [searchTerm, setSearchTerm] = useState('') - // MEMOIZED CONSTANTS - const filteredOptions = useMemo( - () => (isSearchable ? filterActionMenuOptions(options, searchTerm) : options), - [isSearchable, JSON.stringify(options), searchTerm], - ) - - const flatOptions = useMemo(() => getActionMenuFlatOptions(filteredOptions), [filteredOptions]) + // CONSTANTS + const filteredOptions = isSearchable ? filterActionMenuOptions(options, searchTerm) : options + const flatOptions = getActionMenuFlatOptions(filteredOptions) // REFS const itemsRef = useRef[]>( diff --git a/src/Shared/Components/Button/Button.component.tsx b/src/Shared/Components/Button/Button.component.tsx index 525f96440..d8eb3a367 100644 --- a/src/Shared/Components/Button/Button.component.tsx +++ b/src/Shared/Components/Button/Button.component.tsx @@ -18,8 +18,7 @@ import { MutableRefObject, PropsWithChildren, useEffect, useRef, useState } from import { Link } from 'react-router-dom' import { Progressing } from '@Common/Progressing' -import { Tooltip } from '@Common/Tooltip' -import { TooltipProps } from '@Common/Tooltip/types' +import { Tooltip, TooltipProps } from '@Common/Tooltip' import { ComponentSizeType } from '@Shared/constants' import { ButtonComponentType, ButtonProps, ButtonStyleType, ButtonVariantType } from './types' @@ -170,6 +169,7 @@ const Button = ({ variant = ButtonVariantType.primary, size = ComponentSizeType.large, style = ButtonStyleType.default, + fontWeight = 'bold', startIcon = null, endIcon = null, disabled = false, @@ -231,23 +231,22 @@ const Button = ({ } if (Object.hasOwn(tooltipProps, 'shortcutKeyCombo') && 'shortcutKeyCombo' in tooltipProps) { - return tooltipProps + return tooltipProps as TooltipProps } return { - // TODO: using some typing somersaults here, clean it up later - alwaysShowTippyOnHover: showTooltip && !!(tooltipProps as Required>)?.content, - ...(tooltipProps as Required>), - } + alwaysShowTippyOnHover: showTooltip && !!tooltipProps?.content, + ...tooltipProps, + } as TooltipProps } return ( -
+
= { - [ComponentSizeType.xxs_small_icon]: `${COMPONENT_SIZE_TYPE_TO_FONT_AND_BLOCK_PADDING_MAP[ComponentSizeType.xxs]} px-9 fw-6 dc__gap-6 mw-48`, - [ComponentSizeType.xxs]: `${COMPONENT_SIZE_TYPE_TO_FONT_AND_BLOCK_PADDING_MAP[ComponentSizeType.xxs]} px-9 fw-6 dc__gap-6 mw-48`, - [ComponentSizeType.xs]: `${COMPONENT_SIZE_TYPE_TO_FONT_AND_BLOCK_PADDING_MAP[ComponentSizeType.xs]} px-9 fw-6 dc__gap-6 mw-48`, - [ComponentSizeType.small]: `${COMPONENT_SIZE_TYPE_TO_FONT_AND_BLOCK_PADDING_MAP[ComponentSizeType.small]} px-9 fw-6 dc__gap-8 mw-48`, - [ComponentSizeType.medium]: `${COMPONENT_SIZE_TYPE_TO_FONT_AND_BLOCK_PADDING_MAP[ComponentSizeType.medium]} px-11 fw-6 dc__gap-8 mw-48`, - [ComponentSizeType.large]: `${COMPONENT_SIZE_TYPE_TO_FONT_AND_BLOCK_PADDING_MAP[ComponentSizeType.large]} px-13 fw-6 dc__gap-8 mw-64`, - [ComponentSizeType.xl]: `${COMPONENT_SIZE_TYPE_TO_FONT_AND_BLOCK_PADDING_MAP[ComponentSizeType.xl]} px-15 fw-6 dc__gap-12 mw-64`, + [ComponentSizeType.xxs_small_icon]: `${COMPONENT_SIZE_TYPE_TO_FONT_AND_BLOCK_PADDING_MAP[ComponentSizeType.xxs]} px-9 dc__gap-6 mw-48`, + [ComponentSizeType.xxs]: `${COMPONENT_SIZE_TYPE_TO_FONT_AND_BLOCK_PADDING_MAP[ComponentSizeType.xxs]} px-9 dc__gap-6 mw-48`, + [ComponentSizeType.xs]: `${COMPONENT_SIZE_TYPE_TO_FONT_AND_BLOCK_PADDING_MAP[ComponentSizeType.xs]} px-9 dc__gap-6 mw-48`, + [ComponentSizeType.small]: `${COMPONENT_SIZE_TYPE_TO_FONT_AND_BLOCK_PADDING_MAP[ComponentSizeType.small]} px-9 dc__gap-8 mw-48`, + [ComponentSizeType.medium]: `${COMPONENT_SIZE_TYPE_TO_FONT_AND_BLOCK_PADDING_MAP[ComponentSizeType.medium]} px-11 dc__gap-8 mw-48`, + [ComponentSizeType.large]: `${COMPONENT_SIZE_TYPE_TO_FONT_AND_BLOCK_PADDING_MAP[ComponentSizeType.large]} px-13 dc__gap-8 mw-64`, + [ComponentSizeType.xl]: `${COMPONENT_SIZE_TYPE_TO_FONT_AND_BLOCK_PADDING_MAP[ComponentSizeType.xl]} px-15 dc__gap-12 mw-64`, +} as const + +export const BUTTON_FONT_WEIGHT_TO_CLASS_NAME_MAP: Record = { + bold: 'fw-6', + normal: 'fw-4', } as const export const ICON_BUTTON_SIZE_TO_CLASS_NAME_MAP: Record = { diff --git a/src/Shared/Components/Button/types.ts b/src/Shared/Components/Button/types.ts index 5ed004be5..240bf10ff 100644 --- a/src/Shared/Components/Button/types.ts +++ b/src/Shared/Components/Button/types.ts @@ -150,11 +150,10 @@ export type ButtonProps - | (Omit & - Required>) + | Omit, 'alwaysShowTippyOnHover'> + | Omit, 'alwaysShowTippyOnHover'> + | Omit, 'alwaysShowTippyOnHover'> } | { showTooltip?: never @@ -178,6 +177,12 @@ export type ButtonProps & { + Pick & { isAutoTriggerActive: boolean } >) => - `button button__${variant}--${style} ${icon ? ICON_BUTTON_SIZE_TO_CLASS_NAME_MAP[size] : BUTTON_SIZE_TO_CLASS_NAME_MAP[size]} ${isAutoTriggerActive ? 'button--auto-click' : ''} ${isLoading ? 'button--loading' : ''}` + `button button__${variant}--${style} ${icon ? ICON_BUTTON_SIZE_TO_CLASS_NAME_MAP[size] : BUTTON_SIZE_TO_CLASS_NAME_MAP[size]} ${BUTTON_FONT_WEIGHT_TO_CLASS_NAME_MAP[fontWeight]} ${isAutoTriggerActive ? 'button--auto-click' : ''} ${isLoading ? 'button--loading' : ''}` diff --git a/src/Shared/Components/CICDHistory/Artifacts.tsx b/src/Shared/Components/CICDHistory/Artifacts.tsx index 9e4f2beda..0c4436ba9 100644 --- a/src/Shared/Components/CICDHistory/Artifacts.tsx +++ b/src/Shared/Components/CICDHistory/Artifacts.tsx @@ -27,15 +27,9 @@ import noartifact from '@Images/no-artifact.webp' import { getIsApprovalPolicyConfigured } from '@Shared/Helpers' import { useDownload } from '@Shared/Hooks' -import { - ClipboardButton, - DOCUMENTATION, - extractImage, - GenericEmptyState, - ImageTagsContainer, - useGetUserRoles, -} from '../../../Common' +import { ClipboardButton, extractImage, GenericEmptyState, ImageTagsContainer, useGetUserRoles } from '../../../Common' import { EMPTY_STATE_STATUS } from '../../constants' +import { DocLink } from '../DocLink' import { TargetPlatformBadgeList } from '../TargetPlatforms' import { TERMINAL_STATUS_MAP } from './constants' import { ArtifactType, CIListItemType } from './types' @@ -274,14 +268,11 @@ const Artifacts = ({ {EMPTY_STATE_STATUS.ARTIFACTS_EMPTY_STATE_TEXTS.StoreFiles} - - {EMPTY_STATE_STATUS.ARTIFACTS_EMPTY_STATE_TEXTS.ConfigureBlobStorage} - +
diff --git a/src/Shared/Components/CICDHistory/CiPipelineSourceConfig.tsx b/src/Shared/Components/CICDHistory/CiPipelineSourceConfig.tsx index ccc7ee723..36e89ded4 100644 --- a/src/Shared/Components/CICDHistory/CiPipelineSourceConfig.tsx +++ b/src/Shared/Components/CICDHistory/CiPipelineSourceConfig.tsx @@ -162,7 +162,7 @@ export const CiPipelineSourceConfig = ({ {!baseText && ( <>
@@ -181,7 +181,7 @@ export const CiPipelineSourceConfig = ({
) : ( - {sourceValueAdv} + {sourceValueAdv} )}
)} diff --git a/src/Shared/Components/CICDHistory/DeploymentHistoryConfigDiff/DeploymentHistoryDiffView.tsx b/src/Shared/Components/CICDHistory/DeploymentHistoryConfigDiff/DeploymentHistoryDiffView.tsx index cc9448003..edfc4bbe8 100644 --- a/src/Shared/Components/CICDHistory/DeploymentHistoryConfigDiff/DeploymentHistoryDiffView.tsx +++ b/src/Shared/Components/CICDHistory/DeploymentHistoryConfigDiff/DeploymentHistoryDiffView.tsx @@ -19,8 +19,6 @@ import { useParams } from 'react-router-dom' import Tippy from '@tippyjs/react' import { CodeEditor } from '@Shared/Components/CodeEditor' -import { renderDiffViewNoDifferenceState } from '@Shared/Components/DeploymentConfigDiff' -import { DiffViewer } from '@Shared/Components/DiffViewer' import { ReactComponent as Info } from '../../../../Assets/Icon/ic-info-filled.svg' import { ReactComponent as ViewVariablesIcon } from '../../../../Assets/Icon/ic-view-variable-toggle.svg' @@ -34,7 +32,6 @@ const DeploymentHistoryDiffView = ({ baseTemplateConfiguration, previousConfigAvailable, rootClassName, - codeEditorKey, }: DeploymentTemplateHistoryType) => { const { historyComponent, historyComponentName } = useParams() @@ -71,21 +68,17 @@ const DeploymentHistoryDiffView = ({ const renderDeploymentDiffViaCodeEditor = () => previousConfigAvailable ? ( - - ) : ( + ) : ( + ) const handleShowVariablesClick = () => { diff --git a/src/Shared/Components/CICDHistory/LogsRenderer.tsx b/src/Shared/Components/CICDHistory/LogsRenderer.tsx index 5f82948ff..19057c01e 100644 --- a/src/Shared/Components/CICDHistory/LogsRenderer.tsx +++ b/src/Shared/Components/CICDHistory/LogsRenderer.tsx @@ -30,7 +30,6 @@ import { ReactComponent as OpenInNew } from '../../../Assets/Icon/ic-arrow-out.s import { ReactComponent as HelpIcon } from '../../../Assets/Icon/ic-help.svg' import { ReactComponent as Info } from '../../../Assets/Icon/ic-info-filled.svg' import { - DOCUMENTATION, Host, Progressing, ROUTES, @@ -40,6 +39,7 @@ import { useRegisterShortcut, useUrlFilters, } from '../../../Common' +import { DocLink } from '../DocLink' import { EVENT_STREAM_EVENTS_MAP, LOGS_RETRY_COUNT, @@ -82,14 +82,8 @@ const renderBlobNotConfigured = (): JSX.Element => (
Want to store logs to view later? - - Configure blob storage - + +
diff --git a/src/Shared/Components/CICDHistory/StatusFilterButtonComponent.tsx b/src/Shared/Components/CICDHistory/StatusFilterButtonComponent.tsx index 6b0890b48..06c5afc6b 100644 --- a/src/Shared/Components/CICDHistory/StatusFilterButtonComponent.tsx +++ b/src/Shared/Components/CICDHistory/StatusFilterButtonComponent.tsx @@ -14,11 +14,12 @@ * limitations under the License. */ -import { useEffect, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { ReactComponent as ICCaretDown } from '@Icons/ic-caret-down.svg' import { SegmentType } from '@Common/SegmentedControl/types' import { ComponentSizeType } from '@Shared/constants' +import { getUniqueId } from '@Shared/Helpers' import { PopupMenu, SegmentedControl } from '../../../Common' import { StatusFilterButtonType } from './types' @@ -32,6 +33,8 @@ export const StatusFilterButtonComponent = ({ handleFilterClick, maxInlineFiltersCount = 0, }: StatusFilterButtonType) => { + const segmentControlName = useRef(getUniqueId()) + // STATES const [overflowFilterIndex, setOverflowFilterIndex] = useState(0) @@ -104,19 +107,13 @@ export const StatusFilterButtonComponent = ({ const segmentValue = segments.find(({ value }) => value === selectedTab)?.value || null - const segmentControlKey = inlineFilters.reduce( - (acc, inlineFilter) => `${acc}-${inlineFilter.status}`, - `${allResourceKindFilter.status}`, - ) - return (
diff --git a/src/Shared/Components/CICDHistory/types.tsx b/src/Shared/Components/CICDHistory/types.tsx index 280b5124e..8fe87ce26 100644 --- a/src/Shared/Components/CICDHistory/types.tsx +++ b/src/Shared/Components/CICDHistory/types.tsx @@ -550,8 +550,8 @@ export interface DeploymentTemplateHistoryType { baseTemplateConfiguration: DeploymentHistoryDetail previousConfigAvailable: boolean rootClassName?: string - codeEditorKey?: string } + export interface DeploymentHistoryDetailRes extends ResponseType { result?: DeploymentHistoryDetail } diff --git a/src/Shared/Components/CodeEditor/CodeEditor.tsx b/src/Shared/Components/CodeEditor/CodeEditor.tsx index 22838f114..b1cdcbcaa 100644 --- a/src/Shared/Components/CodeEditor/CodeEditor.tsx +++ b/src/Shared/Components/CodeEditor/CodeEditor.tsx @@ -33,7 +33,7 @@ import { } from '@uiw/react-codemirror' import { DEFAULT_JSON_SCHEMA_URI, MODES } from '@Common/Constants' -import { cleanKubeManifest } from '@Common/Helper' +import { cleanKubeManifest, noop } from '@Common/Helper' import { getUniqueId } from '@Shared/Helpers' import { AppThemeType, useTheme } from '@Shared/Providers' @@ -48,7 +48,7 @@ import { replaceAll, showReplaceFieldState, } from './Commands' -import { codeEditorFindReplace, readOnlyTooltip, yamlHighlight } from './Extensions' +import { getCodeEditorFindReplace, readOnlyTooltip, yamlHighlight } from './Extensions' import { CodeEditorContextProps, CodeEditorProps } from './types' import { getFoldGutterElement, getLanguageExtension, getValidationSchema, parseValueToCode } from './utils' @@ -77,7 +77,6 @@ const CodeEditor = ({ onChange, onOriginalValueChange, onModifiedValueChange, - readOnly, placeholder, diffView, loading, @@ -88,8 +87,15 @@ const CodeEditor = ({ onBlur, onFocus, autoFocus, - disableSearch = false, + onSearchPanelOpen = noop, + onSearchBarAction = noop, + collapseUnchangedDiffView = false, + ...resProps }: CodeEditorProps) => { + // DERIVED PROPS + const disableSearch = (collapseUnchangedDiffView || resProps.disableSearch) ?? false + const readOnly = (collapseUnchangedDiffView || resProps.readOnly) ?? false + // HOOKS const { appTheme } = useTheme() @@ -184,7 +190,7 @@ const CodeEditor = ({ defaultKeymap: false, searchKeymap: false, foldGutter: false, - drawSelection: false, + drawSelection: true, highlightActiveLineGutter: true, tabSize, } @@ -197,23 +203,33 @@ const CodeEditor = ({ setLhsCode(newLhsValue) } + const openSearchPanelWrapper: typeof openSearchPanel = (view) => { + onSearchPanelOpen() + return openSearchPanel(view) + } + + const openSearchPanelWithReplaceWrapper: typeof openSearchPanelWithReplace = (view) => { + onSearchPanelOpen() + return openSearchPanelWithReplace(view) + } + // EXTENSIONS const getBaseExtensions = (): Extension[] => [ basicSetup(basicSetupOptions), themeExtension, keymap.of([ ...vscodeKeymap.filter(({ key }) => key !== 'Mod-Alt-Enter' && key !== 'Mod-Enter' && key !== 'Mod-f'), - ...(!disableSearch ? [{ key: 'Mod-f', run: openSearchPanel, scope: 'editor search-panel' }] : []), + ...(!disableSearch ? [{ key: 'Mod-f', run: openSearchPanelWrapper, scope: 'editor search-panel' }] : []), { key: 'Mod-Enter', run: replaceAll, scope: 'editor search-panel' }, - { key: 'Mod-Alt-f', run: openSearchPanelWithReplace, scope: 'editor search-panel' }, + { key: 'Mod-Alt-f', run: openSearchPanelWithReplaceWrapper, scope: 'editor search-panel' }, { key: 'Escape', run: blurOnEscape, stopPropagation: true }, ]), indentationMarkers(), - getLanguageExtension(mode), + getLanguageExtension(mode, collapseUnchangedDiffView), foldingCompartment.of(foldConfig), lintGutter(), search({ - createPanel: codeEditorFindReplace, + createPanel: getCodeEditorFindReplace(onSearchBarAction), }), showReplaceFieldState, ...(mode === MODES.YAML ? [yamlHighlight] : []), @@ -244,6 +260,7 @@ const CodeEditor = ({ codeEditorTheme, basicSetup({ ...basicSetupOptions, + drawSelection: false, lineNumbers: false, highlightActiveLine: false, highlightActiveLineGutter: false, @@ -277,6 +294,8 @@ const CodeEditor = ({ modifiedViewExtensions={modifiedViewExtensions} extensions={extensions} diffMinimapExtensions={diffMinimapExtensions} + collapseUnchanged={collapseUnchangedDiffView} + disableMinimap={collapseUnchangedDiffView} /> ) diff --git a/src/Shared/Components/CodeEditor/CodeEditorRenderer.tsx b/src/Shared/Components/CodeEditor/CodeEditorRenderer.tsx index 7ec4f6408..29010db87 100644 --- a/src/Shared/Components/CodeEditor/CodeEditorRenderer.tsx +++ b/src/Shared/Components/CodeEditor/CodeEditorRenderer.tsx @@ -46,6 +46,8 @@ export const CodeEditorRenderer = ({ extensions, autoFocus, diffMinimapExtensions, + collapseUnchanged = false, + disableMinimap = false, }: CodeEditorRendererProps) => { // CONTEXTS const { value, lhsValue, diffMode } = useCodeEditorContext() @@ -59,6 +61,7 @@ export const CodeEditorRenderer = ({ // REFS const codeMirrorRef = useRef() const codeMirrorMergeParentRef = useRef() + const codeMirrorMergeRef = useRef() const diffMinimapRef = useRef() const diffMinimapParentRef = useRef() @@ -160,9 +163,11 @@ export const CodeEditorRenderer = ({ handleLhsOnChange(val, vu) } - // Using `diffMinimapRef` instead of `diffMinimapInstance` since this extension captures the initial reference in a closure. - // Changes to `diffMinimapInstance` won't be reflected after initialization, so we rely on `diffMinimapRef.current` for updates. - updateDiffMinimapValues(diffMinimapRef.current, vu.transactions, 'a') + if (!disableMinimap) { + // Using `diffMinimapRef` instead of `diffMinimapInstance` since this extension captures the initial reference in a closure. + // Changes to `diffMinimapInstance` won't be reflected after initialization, so we rely on `diffMinimapRef.current` for updates. + updateDiffMinimapValues(diffMinimapRef.current, vu.transactions, 'a') + } }) const modifiedUpdateListener = EditorView.updateListener.of((vu: ViewUpdate) => { @@ -172,93 +177,125 @@ export const CodeEditorRenderer = ({ handleOnChange(val, vu) } - // Using `diffMinimapRef` instead of `diffMinimapInstance` since this extension captures the initial reference in a closure. - // Changes to `diffMinimapInstance` won't be reflected after initialization, so we rely on `diffMinimapRef.current` for updates. - updateDiffMinimapValues(diffMinimapRef.current, vu.transactions, 'b') + if (!disableMinimap) { + // Using `diffMinimapRef` instead of `diffMinimapInstance` since this extension captures the initial reference in a closure. + // Changes to `diffMinimapInstance` won't be reflected after initialization, so we rely on `diffMinimapRef.current` for updates. + updateDiffMinimapValues(diffMinimapRef.current, vu.transactions, 'b') + } }) - useEffect(() => { - // DIFF VIEW INITIALIZATION - if (!loading && codeMirrorMergeParentRef.current) { - const scanLimit = getScanLimit(lhsValue, value) + /** + * Initializes or reinitializes the CodeMirror merge view for diff comparison. + * + * This function: + * 1. Destroys any existing merge view instances to prevent memory leaks + * 2. Creates a new MergeView instance with the current values and configurations + * 3. Initializes the diff minimap if enabled + * 4. Updates the component state with the new instances + * + */ + const initializeCodeMirrorMergeView = () => { + // Destroy existing merge view instance if it exists + codeMirrorMergeInstance?.destroy() + codeMirrorMergeRef.current?.destroy() + + const codeMirrorMergeView = new MergeView({ + a: { + doc: lhsValue, + extensions: [...originalViewExtensions, originalUpdateListener], + }, + b: { + doc: value, + extensions: [...modifiedViewExtensions, modifiedUpdateListener], + }, + ...(!readOnly ? { revertControls: 'a-to-b', renderRevertControl: getRevertControlButton } : {}), + diffConfig: { scanLimit: getScanLimit(lhsValue, value), timeout: 5000 }, + parent: codeMirrorMergeParentRef.current, + collapseUnchanged: collapseUnchanged ? {} : undefined, + }) + + codeMirrorMergeRef.current = codeMirrorMergeView + setCodeMirrorMergeInstance(codeMirrorMergeView) - codeMirrorMergeInstance?.destroy() + // MINIMAP INITIALIZATION + if (!disableMinimap && codeMirrorMergeView && diffMinimapParentRef.current) { + diffMinimapInstance?.destroy() + diffMinimapRef.current?.destroy() - const codeMirrorMergeView = new MergeView({ + const diffMinimapMergeView = new MergeView({ a: { doc: lhsValue, - extensions: [...originalViewExtensions, originalUpdateListener], + extensions: diffMinimapExtensions, }, b: { doc: value, - extensions: [...modifiedViewExtensions, modifiedUpdateListener], + extensions: diffMinimapExtensions, }, - ...(!readOnly ? { revertControls: 'a-to-b', renderRevertControl: getRevertControlButton } : {}), - diffConfig: { scanLimit, timeout: 5000 }, - parent: codeMirrorMergeParentRef.current, + gutter: false, + diffConfig: { scanLimit: getScanLimit(lhsValue, value), timeout: 5000 }, + parent: diffMinimapParentRef.current, }) - setCodeMirrorMergeInstance(codeMirrorMergeView) - - // MINIMAP INITIALIZATION - if (codeMirrorMergeView && diffMinimapParentRef.current) { - diffMinimapInstance?.destroy() - diffMinimapRef.current?.destroy() - - const diffMinimapMergeView = new MergeView({ - a: { - doc: lhsValue, - extensions: diffMinimapExtensions, - }, - b: { - doc: value, - extensions: diffMinimapExtensions, - }, - gutter: false, - diffConfig: { scanLimit, timeout: 5000 }, - parent: diffMinimapParentRef.current, - }) - - diffMinimapRef.current = diffMinimapMergeView - setDiffMinimapInstance(diffMinimapMergeView) - } + + diffMinimapRef.current = diffMinimapMergeView + setDiffMinimapInstance(diffMinimapMergeView) + } + } + + useEffect(() => { + // DIFF VIEW INITIALIZATION + if (!loading && codeMirrorMergeParentRef.current) { + initializeCodeMirrorMergeView() } return () => { setCodeMirrorMergeInstance(null) setDiffMinimapInstance(null) + codeMirrorMergeRef.current = null diffMinimapRef.current = null } - }, [loading, codemirrorMergeKey, diffMode]) - - // Sync external changes of `lhsValue` and `value` state to the diff-editor state. + }, [loading, codemirrorMergeKey, diffMode, collapseUnchanged, disableMinimap]) + + /** + * Synchronizes external changes of `lhsValue` and `value` with the diff-editor state. + * + * When the external state (lhsValue for left-hand side or value for right-hand side) changes, + * we need to update the CodeMirror merge view to reflect these changes. This effect detects + * discrepancies between the current editor content and the external state. + * + * Instead of trying to update the existing editors directly (which can be complex and error-prone), + * we reinitialize the entire merge view when the external state differs from the editor content. + * This ensures a clean, consistent state that properly reflects the external data. + * + */ useEffect(() => { - if (codeMirrorMergeInstance) { - const originalDoc = codeMirrorMergeInstance.a.state.doc.toString() - if (originalDoc !== lhsValue) { - codeMirrorMergeInstance.a.dispatch({ - changes: { from: 0, to: originalDoc.length, insert: lhsValue || '' }, - }) - } - - const modifiedDoc = codeMirrorMergeInstance.b.state.doc.toString() - if (modifiedDoc !== value) { - codeMirrorMergeInstance.b.dispatch({ - changes: { from: 0, to: modifiedDoc.length, insert: value || '' }, - }) + if (codeMirrorMergeRef.current) { + // Get the current content from both editors + const originalDoc = codeMirrorMergeRef.current.a.state.doc.toString() + const modifiedDoc = codeMirrorMergeRef.current.b.state.doc.toString() + + // If either editor's content doesn't match the external state, + // reinitialize the entire merge view with the current values + if (originalDoc !== lhsValue || modifiedDoc !== value) { + initializeCodeMirrorMergeView() } } - }, [lhsValue, value, codeMirrorMergeInstance]) + }, [lhsValue, value]) // SCALING FACTOR UPDATER useEffect(() => { - setTimeout(() => { - setScalingFactor( - codeMirrorMergeInstance - ? Math.min(codeMirrorMergeInstance.dom.clientHeight / codeMirrorMergeInstance.dom.scrollHeight, 1) - : 1, - ) - }, 100) - }, [lhsValue, value, codeMirrorMergeInstance]) + if (!disableMinimap) { + setTimeout(() => { + setScalingFactor( + codeMirrorMergeInstance + ? Math.min( + codeMirrorMergeInstance.dom.clientHeight / codeMirrorMergeInstance.dom.scrollHeight, + 1, + ) + : 1, + ) + }, 100) + } + }, [lhsValue, value, codeMirrorMergeInstance, disableMinimap]) const { codeEditorClassName, codeEditorHeight, codeEditorParentClassName } = getCodeEditorHeight(height) @@ -286,12 +323,14 @@ export const CodeEditorRenderer = ({ ref={codeMirrorMergeParentRef} className={`cm-merge-theme flex-grow-1 h-100 dc__overflow-hidden ${readOnly ? 'code-editor__read-only' : ''}`} /> - + {!disableMinimap && ( + + )}
) : (
{ +const FindReplace = ({ view, defaultQuery, defaultShowReplace, onSearchBarAction }: FindReplaceProps) => { // STATES const [query, setQuery] = useState(new SearchQuery({ search: '' })) const [matchesCount, setMatchesCount] = useState({ count: 0, current: 1 }) @@ -103,6 +103,9 @@ const FindReplace = ({ view, defaultQuery, defaultShowReplace }: FindReplaceProp search = query.search, wholeWord = query.wholeWord, }: FindReplaceQuery) => { + // Calling this irrespective of whether the query has changed or not + onSearchBarAction() + const newQuery = new SearchQuery({ caseSensitive, regexp, @@ -120,6 +123,7 @@ const FindReplace = ({ view, defaultQuery, defaultShowReplace }: FindReplaceProp useEffect(() => { if (!defaultQuery.eq(query)) { + onSearchBarAction() setMatchesCount(getUpdatedSearchMatchesCount(defaultQuery, view)) setQuery(defaultQuery) } @@ -144,6 +148,7 @@ const FindReplace = ({ view, defaultQuery, defaultShowReplace }: FindReplaceProp const onNext = (e?: MouseEvent) => { e?.preventDefault() e?.stopPropagation() + onSearchBarAction() findNext(view) setMatchesCount(getUpdatedSearchMatchesCount(query, view)) } @@ -151,6 +156,7 @@ const FindReplace = ({ view, defaultQuery, defaultShowReplace }: FindReplaceProp const onPrevious = (e?: MouseEvent) => { e?.preventDefault() e?.stopPropagation() + onSearchBarAction() findPrevious(view) setMatchesCount(getUpdatedSearchMatchesCount(query, view)) } @@ -179,6 +185,7 @@ const FindReplace = ({ view, defaultQuery, defaultShowReplace }: FindReplaceProp e.stopPropagation() if (e.key === 'Enter') { e.preventDefault() + onSearchBarAction() replaceNext(view) } } @@ -188,10 +195,12 @@ const FindReplace = ({ view, defaultQuery, defaultShowReplace }: FindReplaceProp } const onReplaceTextClick = () => { + onSearchBarAction() replaceNext(view) } const onReplaceTextAllClick = () => { + onSearchBarAction() replaceAll(view) } @@ -431,62 +440,65 @@ const FindReplace = ({ view, defaultQuery, defaultShowReplace }: FindReplaceProp ) } -export const codeEditorFindReplace = (view: EditorView): Panel => { - const dom = document.createElement('div') +export const getCodeEditorFindReplace = + (onSearchBarAction: CodeEditorProps['onSearchBarAction']) => + (view: EditorView): Panel => { + const dom = document.createElement('div') - const keydown = (e: KeyboardEvent) => { - if (runScopeHandlers(view, e, 'search-panel')) { - e.preventDefault() - e.stopPropagation() + const keydown = (e: KeyboardEvent) => { + if (runScopeHandlers(view, e, 'search-panel')) { + e.preventDefault() + e.stopPropagation() + } } - } - dom.className = - 'code-editor__search mt-8 mb-4 mr-8 ml-auto p-5 bg__secondary dc__border br-4 dc__w-fit-content fs-14' - dom.onkeydown = keydown - - const renderFindReplace = () => { - render( - , - dom, - ) - } + dom.className = + 'code-editor__search mt-8 mb-4 mr-8 ml-auto p-5 bg__secondary dc__border br-4 dc__w-fit-content fs-14' + dom.onkeydown = keydown + + const renderFindReplace = () => { + render( + , + dom, + ) + } - const mount = () => { - requestAnimationFrame(() => { - const findField = document.querySelector('[data-code-editor-find]') as HTMLInputElement - findField?.focus() - findField?.select() - }) - } + const mount = () => { + requestAnimationFrame(() => { + const findField = document.querySelector('[data-code-editor-find]') as HTMLInputElement + findField?.focus() + findField?.select() + }) + } - const update = ({ transactions, docChanged, state, startState }: ViewUpdate) => { - transactions.forEach((tr) => { - tr.effects.forEach((effect) => { - if (effect.is(setSearchQuery)) { - renderFindReplace() - } - if (effect.is(setShowReplaceField)) { - renderFindReplace() - } + const update = ({ transactions, docChanged, state, startState }: ViewUpdate) => { + transactions.forEach((tr) => { + tr.effects.forEach((effect) => { + if (effect.is(setSearchQuery)) { + renderFindReplace() + } + if (effect.is(setShowReplaceField)) { + renderFindReplace() + } + }) }) - }) - if (docChanged || state.readOnly !== startState.readOnly) { - renderFindReplace() + if (docChanged || state.readOnly !== startState.readOnly) { + renderFindReplace() + } } - } - renderFindReplace() + renderFindReplace() - return { - top: true, - dom, - mount, - update, + return { + top: true, + dom, + mount, + update, + } } -} diff --git a/src/Shared/Components/CodeEditor/codeEditor.scss b/src/Shared/Components/CodeEditor/codeEditor.scss index cdaac94b2..bd1894d7e 100644 --- a/src/Shared/Components/CodeEditor/codeEditor.scss +++ b/src/Shared/Components/CodeEditor/codeEditor.scss @@ -133,7 +133,8 @@ border-bottom: none; &:has(.code-editor__search) { - z-index: 0; + width: fit-content; + margin-left: auto; } } @@ -244,6 +245,20 @@ content: url("data:image/svg+xml,%3Csvg width='14' height='14' viewBox='0 0 14 14' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.24239 2.18666L1.43119 10.4987C1.3542 10.6317 1.3136 10.7827 1.31348 10.9364C1.31335 11.09 1.3537 11.241 1.43046 11.3742C1.50723 11.5073 1.6177 11.6179 1.75077 11.6947C1.88384 11.7716 2.0348 11.8121 2.18848 11.8121H11.8109C11.9646 11.8121 12.1155 11.7716 12.2486 11.6947C12.3817 11.6179 12.4921 11.5073 12.5689 11.3742C12.6457 11.241 12.686 11.09 12.6859 10.9364C12.6858 10.7827 12.6452 10.6317 12.5682 10.4987L7.75697 2.18666C7.68011 2.05387 7.56968 1.94363 7.43676 1.86699C7.30384 1.79034 7.15311 1.75 6.99968 1.75C6.84625 1.75 6.69551 1.79034 6.5626 1.86699C6.42968 1.94363 6.31925 2.05387 6.24239 2.18666Z' fill='%23F4BA63'/%3E%3Cpath d='M7.58333 5.68758C7.58333 5.36542 7.32217 5.10425 7 5.10425C6.67783 5.10425 6.41667 5.36542 6.41667 5.68758V7.87508C6.41667 8.19725 6.67783 8.45841 7 8.45841C7.32217 8.45841 7.58333 8.19725 7.58333 7.87508V5.68758Z' fill='%23000A14'/%3E%3Cpath d='M7.65625 9.84383C7.65625 10.2063 7.36244 10.5001 7 10.5001C6.63756 10.5001 6.34375 10.2063 6.34375 9.84383C6.34375 9.48139 6.63756 9.18758 7 9.18758C7.36244 9.18758 7.65625 9.48139 7.65625 9.84383Z' fill='%23000A14'/%3E%3C/svg%3E%0A"); } + // COLLAPSED + .cm-collapsedLines { + padding: 6px 12px; + background: var(--B50); + font-size: 14px; + line-height: 20px; + color: var(--B500); + + &::before, + &::after { + content: none; + } + } + // SEARCH .cm-searchMatch { background-color: var(--bg-matching-keyword); diff --git a/src/Shared/Components/CodeEditor/types.ts b/src/Shared/Components/CodeEditor/types.ts index 1ffb86fd1..edb7a5b92 100644 --- a/src/Shared/Components/CodeEditor/types.ts +++ b/src/Shared/Components/CodeEditor/types.ts @@ -51,6 +51,12 @@ type CodeEditorDiffBaseProps = { originalValue?: ReactCodeMirrorProps['value'] modifiedValue?: ReactCodeMirrorProps['value'] isOriginalModifiable?: boolean + /** + * When true, renders a diff view in readOnly mode with collapsed unchanged diffs. + * This disables the minimap, code-editor search functionality, and language linting. + * @default false + */ + collapseUnchangedDiffView?: boolean } type CodeEditorPropsBasedOnDiffView = DiffView extends true @@ -78,6 +84,11 @@ export type CodeEditorProps = { disableSearch?: boolean diffView?: DiffView theme?: AppThemeType + onSearchPanelOpen?: () => void + /** + * This method is triggered when user types something in the search/replace bar or applies a search or replace action. + */ + onSearchBarAction?: () => void } & CodeEditorPropsBasedOnDiffView export interface GetCodeEditorHeightReturnType { @@ -96,7 +107,7 @@ export type FindReplaceQuery = Partial< Pick > -export interface FindReplaceProps { +export interface FindReplaceProps extends Pick { view: EditorView /** Default value for Search Query state. */ defaultQuery: SearchQuery @@ -146,6 +157,8 @@ export type CodeEditorRendererProps = Required< modifiedViewExtensions: ReactCodeMirrorProps['extensions'] extensions: ReactCodeMirrorProps['extensions'] diffMinimapExtensions: ReactCodeMirrorProps['extensions'] + collapseUnchanged?: boolean + disableMinimap?: boolean } export interface DiffMinimapProps extends Pick { diff --git a/src/Shared/Components/ConfirmationModal/ConfirmationModal.tsx b/src/Shared/Components/ConfirmationModal/ConfirmationModal.tsx index 9a9d6f550..583de9898 100644 --- a/src/Shared/Components/ConfirmationModal/ConfirmationModal.tsx +++ b/src/Shared/Components/ConfirmationModal/ConfirmationModal.tsx @@ -25,6 +25,7 @@ import { Backdrop } from '../Backdrop' import { Button, ButtonStyleType, ButtonVariantType } from '../Button' import { Confetti } from '../Confetti' import { CustomInput } from '../CustomInput' +import { Icon } from '../Icon' import { useConfirmationModalContext } from './ConfirmationModalContext' import { ConfirmationModalBodyProps, ConfirmationModalProps, ConfirmationModalVariantType } from './types' import { getConfirmationLabel, getIconFromVariant, getPrimaryButtonStyleFromVariant } from './utils' @@ -34,7 +35,7 @@ import './confirmationModal.scss' const ConfirmationModalBody = ({ title, subtitle, - Icon, + Icon: ButtonIcon, variant, buttonConfig, confirmationConfig, @@ -53,8 +54,8 @@ const ConfirmationModalBody = ({ const { primaryButtonConfig, secondaryButtonConfig } = buttonConfig - const RenderIcon = Icon ?? getIconFromVariant(variant) - const hideIcon = variant === ConfirmationModalVariantType.custom && !Icon + const RenderIcon = ButtonIcon ?? getIconFromVariant(variant) + const hideIcon = variant === ConfirmationModalVariantType.custom && !ButtonIcon const disablePrimaryButton: boolean = ('disabled' in primaryButtonConfig && primaryButtonConfig.disabled) || @@ -159,7 +160,7 @@ const ConfirmationModalBody = ({ text={primaryButtonConfig.text} onClick={primaryButtonConfig.onClick as ButtonHTMLAttributes['onClick']} startIcon={primaryButtonConfig.startIcon} - endIcon={primaryButtonConfig.endIcon} + endIcon={primaryButtonConfig.endIcon || } /> )}
diff --git a/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiff.utils.tsx b/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiff.utils.tsx index b57c4cbf4..46c27b858 100644 --- a/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiff.utils.tsx +++ b/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiff.utils.tsx @@ -20,7 +20,7 @@ import { ReactComponent as ICCheck } from '@Icons/ic-check.svg' import { ReactComponent as ICCheckCircleDots } from '@Icons/ic-check-circle-dots.svg' import { ReactComponent as ICEditFile } from '@Icons/ic-edit-file.svg' import { ReactComponent as ICFileCode } from '@Icons/ic-file-code.svg' -import { deepEqual, noop, YAMLStringify } from '@Common/Helper' +import { deepEqual, YAMLStringify } from '@Common/Helper' import { AppEnvDeploymentConfigListParams, DeploymentConfigDiffProps, @@ -28,7 +28,6 @@ import { DeploymentHistoryDetail, DeploymentHistorySingleValue, DiffHeadingDataType, - GenericSectionErrorState, prepareHistoryData, } from '@Shared/Components' import { DEPLOYMENT_HISTORY_CONFIGURATION_LIST_MAP } from '@Shared/constants' @@ -47,7 +46,6 @@ import { TemplateListDTO, TemplateListType, } from '../../Services/app.types' -import { DiffViewerProps } from '../DiffViewer/types' export const getDeploymentTemplateData = (data: DeploymentTemplateDTO) => { const parsedDraftData = JSON.parse(data?.deploymentDraftData?.configData[0].draftMetadata.data || null) @@ -911,21 +909,3 @@ export const getDefaultVersionAndPreviousDeploymentOptions = (data: TemplateList previousDeployments: [], }, ) - -export const renderDiffViewNoDifferenceState = ( - lhsValue: string, - rhsValue: string, -): DiffViewerProps['codeFoldMessageRenderer'] => - lhsValue === rhsValue - ? () => ( - - ) - : null diff --git a/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiffMain.tsx b/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiffMain.tsx index 595f437a5..db751dd79 100644 --- a/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiffMain.tsx +++ b/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiffMain.tsx @@ -18,14 +18,14 @@ import { Fragment, useEffect, useState } from 'react' import { ReactComponent as ICSort } from '@Icons/ic-arrow-up-down.svg' import { ReactComponent as ICSortArrowDown } from '@Icons/ic-sort-arrow-down.svg' -import { SortingOrder } from '@Common/Constants' +import { MODES, SortingOrder } from '@Common/Constants' import ErrorScreenManager from '@Common/ErrorScreenManager' import { Progressing } from '@Common/Progressing' -import { DiffViewer } from '@Shared/Components/DiffViewer' import { ComponentSizeType } from '@Shared/constants' import { Button, ButtonStyleType, ButtonVariantType } from '../Button' import { DeploymentHistoryDiffView } from '../CICDHistory' +import { CodeEditor } from '../CodeEditor' import { SelectPicker } from '../SelectPicker' import { ToggleResolveScopedVariables } from '../ToggleResolveScopedVariables' import { @@ -34,7 +34,6 @@ import { DeploymentConfigDiffSelectPickerProps, DeploymentConfigDiffState, } from './DeploymentConfigDiff.types' -import { renderDiffViewNoDifferenceState } from './DeploymentConfigDiff.utils' import { DeploymentConfigDiffAccordion } from './DeploymentConfigDiffAccordion' export const DeploymentConfigDiffMain = ({ @@ -191,19 +190,25 @@ export const DeploymentConfigDiffMain = ({ hideDiffState={hideDiffState} > {singleView ? ( - + <> +
+
+

{primaryHeading}

+
+
+

{secondaryHeading}

+
+
+ + ) : (
{primaryHeading && secondaryHeading && ( @@ -213,7 +218,6 @@ export const DeploymentConfigDiffMain = ({
)} +
{!!headerText &&

{headerText}

} diff --git a/src/Shared/Components/DiffViewer/DiffViewer.component.tsx b/src/Shared/Components/DiffViewer/DiffViewer.component.tsx deleted file mode 100644 index 863d20cca..000000000 --- a/src/Shared/Components/DiffViewer/DiffViewer.component.tsx +++ /dev/null @@ -1,76 +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 ReactDiffViewer, { DiffMethod } from 'react-diff-viewer-continued' - -import { diffViewerStyles } from './constants' -import { DiffViewerProps, DiffViewTitleWrapperProps } from './types' - -const DiffViewTitleWrapper = ({ title }: DiffViewTitleWrapperProps) =>
{title}
- -/** - * Component for showing diff between two string or object. - * - * Note: Pass down the object as stringified for optimized performance. - * - * @example Usage - * - * ```tsx - * - * ``` - * - * @example With left/right title for lhs/rhs - * - * ```tsx - * Title for RHS - * } - * /> - * ``` - * - * @example With custom message for folded code - * Note: the entire section would be clickable - * - * ```tsx - * Custom text} - * /> - * ``` - */ -const DiffViewer = ({ oldValue, newValue, leftTitle, rightTitle, ...props }: DiffViewerProps) => ( - : null} - rightTitle={rightTitle ? : null} - compareMethod={DiffMethod.WORDS} - styles={diffViewerStyles} - /> -) - -export default DiffViewer diff --git a/src/Shared/Components/DiffViewer/constants.ts b/src/Shared/Components/DiffViewer/constants.ts deleted file mode 100644 index 1c7078dbf..000000000 --- a/src/Shared/Components/DiffViewer/constants.ts +++ /dev/null @@ -1,120 +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. - */ - -// Default variables and style keys - -import { ReactDiffViewerProps } from 'react-diff-viewer-continued' - -export const diffViewerStyles: ReactDiffViewerProps['styles'] = { - variables: { - light: { - diffViewerBackground: 'var(--bg-primary)', - diffViewerColor: 'var(--N900)', - addedBackground: 'var(--G50)', - addedColor: 'var(--N900)', - removedBackground: 'var(--R50)', - removedColor: 'var(--N900)', - wordAddedBackground: 'var(--G200)', - wordRemovedBackground: 'var(--R200)', - addedGutterBackground: 'var(--G100)', - removedGutterBackground: 'var(--R100)', - gutterBackground: 'var(--bg-secondary)', - gutterBackgroundDark: 'var(--bg-secondary)', - highlightBackground: 'var(--N100)', - highlightGutterBackground: 'var(--N100)', - codeFoldGutterBackground: 'var(--B100)', - codeFoldBackground: 'var(--B50)', - emptyLineBackground: 'var(--bg-primary)', - gutterColor: 'var(--N500)', - addedGutterColor: 'var(--N700)', - removedGutterColor: 'var(--N700)', - codeFoldContentColor: 'var(--B600)', - diffViewerTitleBackground: 'var(--N100)', - diffViewerTitleColor: 'var(--N700)', - diffViewerTitleBorderColor: 'var(--N200)', - }, - }, - diffContainer: { - fontSize: '14px', - fontWeight: 400, - lineHeight: '20px', - - pre: { - fontSize: '14px', - lineHeight: '20px', - fontFamily: 'Inconsolata, monospace', - wordBreak: 'break-word', - // Reset for styling from patternfly - padding: 0, - margin: 0, - backgroundColor: 'transparent', - border: 'none', - borderRadius: 0, - }, - }, - marker: { - pre: { - display: 'none', - }, - }, - gutter: { - padding: `0 6px`, - minWidth: '36px', - // Cursor would be default for all cases in gutter till we don't support highlighting - cursor: 'default', - - pre: { - opacity: 1, - }, - }, - wordDiff: { - padding: 0, - }, - wordAdded: { - paddingInline: '2px', - lineHeight: '16px', - }, - wordRemoved: { - paddingInline: '2px', - lineHeight: '16px', - }, - codeFold: { - fontSize: '14px', - fontWeight: 400, - lineHeight: '20px', - height: '32px', - - a: { - textDecoration: 'none !important', - }, - }, - codeFoldGutter: { - '+ td': { - width: '12px', - }, - }, - titleBlock: { - padding: '8px 12px', - fontSize: '12px', - lineHeight: '20px', - fontWeight: 600, - borderBottom: 'none', - - pre: { - fontFamily: 'Open Sans', - }, - }, -} diff --git a/src/Shared/Components/DiffViewer/index.ts b/src/Shared/Components/DiffViewer/index.ts deleted file mode 100644 index 0abf9bc28..000000000 --- a/src/Shared/Components/DiffViewer/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 { default as DiffViewer } from './DiffViewer.component' diff --git a/src/Shared/Components/DiffViewer/types.ts b/src/Shared/Components/DiffViewer/types.ts deleted file mode 100644 index 8b4958c6e..000000000 --- a/src/Shared/Components/DiffViewer/types.ts +++ /dev/null @@ -1,28 +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 { ReactNode } from 'react' -import { ReactDiffViewerProps } from 'react-diff-viewer-continued' - -export interface DiffViewerProps - extends Pick { - leftTitle?: ReactDiffViewerProps['leftTitle'] | ReactNode - rightTitle?: ReactDiffViewerProps['rightTitle'] | ReactNode -} - -export interface DiffViewTitleWrapperProps { - title: DiffViewerProps['leftTitle'] -} diff --git a/src/Shared/Components/DocLink/DocLink.tsx b/src/Shared/Components/DocLink/DocLink.tsx new file mode 100644 index 000000000..bbb698abd --- /dev/null +++ b/src/Shared/Components/DocLink/DocLink.tsx @@ -0,0 +1,61 @@ +import { MouseEvent } from 'react' + +import { DOCUMENTATION_HOME_PAGE } from '@Common/Constants' +import { Button, ButtonComponentType, ButtonVariantType, Icon } from '@Shared/Components' +import { ComponentSizeType } from '@Shared/constants' +import { useMainContext } from '@Shared/Providers' + +import { DocLinkProps } from './types' +import { getDocumentationUrl } from './utils' + +export const DocLink = ({ + docLinkKey, + text = 'Learn more', + dataTestId, + startIcon, + showExternalIcon, + onClick, + fontWeight, + size = ComponentSizeType.medium, + variant = ButtonVariantType.text, + isExternalLink, + openInNewTab = false, + fullWidth = false, +}: DocLinkProps) => { + // HOOKS + const { isEnterprise, setSidePanelConfig } = useMainContext() + + // CONSTANTS + const documentationLink = getDocumentationUrl({ + docLinkKey, + isEnterprise, + isExternalLink, + }) + + // HANDLERS + const handleClick = (e: MouseEvent) => { + if (!isExternalLink && !openInNewTab && !e.metaKey && documentationLink.startsWith(DOCUMENTATION_HOME_PAGE)) { + e.preventDefault() + setSidePanelConfig((prev) => ({ ...prev, open: true, docLink: documentationLink, reinitialize: true })) + } + onClick?.(e) + } + + return ( + +
+ +
)} @@ -181,8 +166,6 @@ const PageHeader = ({ Beta ) - const showingLicenseBar = getIsShowingLicenseData(licenseData) - const renderIframeButton = () => return ( @@ -215,7 +198,7 @@ const PageHeader = ({ heading={headerName} iconClassName="icon-dim-20 ml-8 fcn-5" documentationLink={tippyRedirectLink} - documentationLinkText="Learn More" + documentationLinkText="View Documentation" additionalContent={additionalContent} > {TippyIcon && ( @@ -249,7 +232,7 @@ const PageHeader = ({ {markAsBeta && renderBetaTag()}
{showTabs && ( -
+
{renderIframeButton()} {typeof renderActionButtons === 'function' && renderActionButtons()} {renderLogoutHelpSection()} @@ -269,16 +252,8 @@ const PageHeader = ({ loginCount={loginCount} /> )} - {showLogOutCard && ( - - )} {!showTabs && ( -
+
{typeof renderActionButtons === 'function' && renderActionButtons()} {renderIframeButton()} {renderLogoutHelpSection()} diff --git a/src/Shared/Components/Header/ProfileMenu.tsx b/src/Shared/Components/Header/ProfileMenu.tsx new file mode 100644 index 000000000..1b84c759e --- /dev/null +++ b/src/Shared/Components/Header/ProfileMenu.tsx @@ -0,0 +1,78 @@ +import { useMemo } from 'react' +import { Link } from 'react-router-dom' + +import { URLS } from '@Common/Constants' +import { getAlphabetIcon } from '@Common/Helper' +import { clearCookieOnLogout } from '@Shared/Helpers' +import { useMainContext } from '@Shared/Providers' + +import { Icon } from '../Icon' +import { Popover, usePopover } from '../Popover' +import { ThemeSwitcher } from '../ThemeSwitcher' +import { ProfileMenuProps } from './types' + +export const ProfileMenu = ({ user, onClick }: ProfileMenuProps) => { + // HOOKS + const { viewIsPipelineRBACConfiguredNode } = useMainContext() + + const { open, overlayProps, popoverProps, triggerProps, scrollableRef, closePopover } = usePopover({ + id: 'profile-menu', + alignment: 'end', + width: 250, + }) + + // ELEMENTS + const triggerElement = useMemo( + () => ( + + ), + [open], + ) + + // HANDLERS + const onLogout = () => { + closePopover() + clearCookieOnLogout() + } + + return ( + +
+
+
+
+

{user}

+

{user}

+
+ {getAlphabetIcon(user, 'dc__no-shrink m-0-imp fs-16 icon-dim-36')} +
+
+
+ + {viewIsPipelineRBACConfiguredNode} +
+
+ + Logout + + +
+
+
+ ) +} diff --git a/src/Shared/Components/Header/types.ts b/src/Shared/Components/Header/types.ts index 0dc15f387..925965001 100644 --- a/src/Shared/Components/Header/types.ts +++ b/src/Shared/Components/Header/types.ts @@ -14,16 +14,11 @@ * limitations under the License. */ -import { ModuleStatus } from '@Shared/types' +import { InstallationType, ModuleStatus } from '@Shared/types' import { ResponseType, TippyCustomizedProps } from '../../../Common' import { ActionMenuProps } from '../ActionMenu' - -export enum InstallationType { - OSS_KUBECTL = 'oss_kubectl', - OSS_HELM = 'oss_helm', - ENTERPRISE = 'enterprise', -} +import { DOCUMENTATION } from '../DocLink' export interface PageHeaderType { headerName?: string @@ -36,9 +31,9 @@ export interface PageHeaderType { showCloseButton?: boolean onClose?: () => void markAsBeta?: boolean - tippyProps?: Pick & { + tippyProps?: Pick, 'additionalContent'> & { isTippyCustomized?: boolean - tippyRedirectLink?: string + tippyRedirectLink?: keyof typeof DOCUMENTATION TippyIcon?: React.FunctionComponent tippyMessage?: string onClickTippyButton?: () => void @@ -76,3 +71,8 @@ export enum HelpMenuItems { } export type HelpButtonActionMenuProps = ActionMenuProps + +export interface ProfileMenuProps { + user: string + onClick?: () => void +} diff --git a/src/Shared/Components/Header/utils.ts b/src/Shared/Components/Header/utils.ts index 769041b14..1896ea901 100644 --- a/src/Shared/Components/Header/utils.ts +++ b/src/Shared/Components/Header/utils.ts @@ -16,7 +16,6 @@ import { LOGIN_COUNT } from '@Common/Constants' -import { DevtronLicenseInfo, LicenseStatus } from '../License' import { COMMON_HELP_ACTION_MENU_ITEMS, ENTERPRISE_HELP_ACTION_MENU_ITEMS, @@ -43,9 +42,6 @@ export const setActionWithExpiry = (key: string, days: number): void => { localStorage.setItem(key, `${getDateInMilliseconds(days)}`) } -export const getIsShowingLicenseData = (licenseData: DevtronLicenseInfo) => - licenseData && (licenseData.licenseStatus !== LicenseStatus.ACTIVE || licenseData.isTrial) - export const getHelpActionMenuOptions = ({ isEnterprise, isTrial, diff --git a/src/Shared/Components/Icon/Icon.tsx b/src/Shared/Components/Icon/Icon.tsx index c1b87b4d1..302c24049 100644 --- a/src/Shared/Components/Icon/Icon.tsx +++ b/src/Shared/Components/Icon/Icon.tsx @@ -6,6 +6,7 @@ import { ReactComponent as ICAdd } from '@IconsV2/ic-add.svg' import { ReactComponent as ICAmazonEks } from '@IconsV2/ic-amazon-eks.svg' import { ReactComponent as ICApica } from '@IconsV2/ic-apica.svg' import { ReactComponent as ICAppGroup } from '@IconsV2/ic-app-group.svg' +import { ReactComponent as ICAppTemplate } from '@IconsV2/ic-app-template.svg' import { ReactComponent as ICArrowClockwise } from '@IconsV2/ic-arrow-clockwise.svg' import { ReactComponent as ICArrowRight } from '@IconsV2/ic-arrow-right.svg' import { ReactComponent as ICArrowSquareOut } from '@IconsV2/ic-arrow-square-out.svg' @@ -51,12 +52,15 @@ import { ReactComponent as ICDeleteDots } from '@IconsV2/ic-delete-dots.svg' import { ReactComponent as ICDeleteLightning } from '@IconsV2/ic-delete-lightning.svg' import { ReactComponent as ICDelhivery } from '@IconsV2/ic-delhivery.svg' import { ReactComponent as ICDevtron } from '@IconsV2/ic-devtron.svg' +import { ReactComponent as ICDevtronApp } from '@IconsV2/ic-devtron-app.svg' import { ReactComponent as ICDevtronHeaderLogo } from '@IconsV2/ic-devtron-header-logo.svg' +import { ReactComponent as ICDevtronJob } from '@IconsV2/ic-devtron-job.svg' import { ReactComponent as ICDisconnect } from '@IconsV2/ic-disconnect.svg' import { ReactComponent as ICDiscordFill } from '@IconsV2/ic-discord-fill.svg' import { ReactComponent as ICDockerhub } from '@IconsV2/ic-dockerhub.svg' import { ReactComponent as ICEcr } from '@IconsV2/ic-ecr.svg' import { ReactComponent as ICEdit } from '@IconsV2/ic-edit.svg' +import { ReactComponent as ICEmail } from '@IconsV2/ic-email.svg' import { ReactComponent as ICEnterpriseFeat } from '@IconsV2/ic-enterprise-feat.svg' import { ReactComponent as ICEnterpriseTag } from '@IconsV2/ic-enterprise-tag.svg' import { ReactComponent as ICEnv } from '@IconsV2/ic-env.svg' @@ -101,6 +105,7 @@ import { ReactComponent as ICJobColor } from '@IconsV2/ic-job-color.svg' import { ReactComponent as ICK3s } from '@IconsV2/ic-k3s.svg' import { ReactComponent as ICK8sJob } from '@IconsV2/ic-k8s-job.svg' import { ReactComponent as ICKey } from '@IconsV2/ic-key.svg' +import { ReactComponent as ICKeyEnter } from '@IconsV2/ic-key-enter.svg' import { ReactComponent as ICKind } from '@IconsV2/ic-kind.svg' import { ReactComponent as ICLaptop } from '@IconsV2/ic-laptop.svg' import { ReactComponent as ICLdap } from '@IconsV2/ic-ldap.svg' @@ -144,6 +149,7 @@ import { ReactComponent as ICSortDescending } from '@IconsV2/ic-sort-descending. import { ReactComponent as ICSortable } from '@IconsV2/ic-sortable.svg' import { ReactComponent as ICSparkleColor } from '@IconsV2/ic-sparkle-color.svg' import { ReactComponent as ICSpinny } from '@IconsV2/ic-spinny.svg' +import { ReactComponent as ICSprayCan } from '@IconsV2/ic-spray-can.svg' import { ReactComponent as ICStack } from '@IconsV2/ic-stack.svg' import { ReactComponent as ICStamp } from '@IconsV2/ic-stamp.svg' import { ReactComponent as ICStrategyBlueGreen } from '@IconsV2/ic-strategy-blue-green.svg' @@ -186,6 +192,7 @@ export const iconMap = { 'ic-amazon-eks': ICAmazonEks, 'ic-apica': ICApica, 'ic-app-group': ICAppGroup, + 'ic-app-template': ICAppTemplate, 'ic-arrow-clockwise': ICArrowClockwise, 'ic-arrow-right': ICArrowRight, 'ic-arrow-square-out': ICArrowSquareOut, @@ -230,13 +237,16 @@ export const iconMap = { 'ic-delete-lightning': ICDeleteLightning, 'ic-delete': ICDelete, 'ic-delhivery': ICDelhivery, + 'ic-devtron-app': ICDevtronApp, 'ic-devtron-header-logo': ICDevtronHeaderLogo, + 'ic-devtron-job': ICDevtronJob, 'ic-devtron': ICDevtron, 'ic-disconnect': ICDisconnect, 'ic-discord-fill': ICDiscordFill, 'ic-dockerhub': ICDockerhub, 'ic-ecr': ICEcr, 'ic-edit': ICEdit, + 'ic-email': ICEmail, 'ic-enterprise-feat': ICEnterpriseFeat, 'ic-enterprise-tag': ICEnterpriseTag, 'ic-env': ICEnv, @@ -280,6 +290,7 @@ export const iconMap = { 'ic-job-color': ICJobColor, 'ic-k3s': ICK3s, 'ic-k8s-job': ICK8sJob, + 'ic-key-enter': ICKeyEnter, 'ic-key': ICKey, 'ic-kind': ICKind, 'ic-laptop': ICLaptop, @@ -324,6 +335,7 @@ export const iconMap = { 'ic-sortable': ICSortable, 'ic-sparkle-color': ICSparkleColor, 'ic-spinny': ICSpinny, + 'ic-spray-can': ICSprayCan, 'ic-stack': ICStack, 'ic-stamp': ICStamp, 'ic-strategy-blue-green-color': ICStrategyBlueGreenColor, diff --git a/src/Shared/Components/InfoIconTippy/InfoIconTippy.tsx b/src/Shared/Components/InfoIconTippy/InfoIconTippy.tsx index eb79c7339..563df99a4 100644 --- a/src/Shared/Components/InfoIconTippy/InfoIconTippy.tsx +++ b/src/Shared/Components/InfoIconTippy/InfoIconTippy.tsx @@ -19,7 +19,7 @@ import { ReactComponent as ICHelpOutline } from '../../../Assets/Icon/ic-help-ou import { TippyCustomized } from '../../../Common/TippyCustomized' import { InfoIconTippyProps, TippyTheme } from '../../../Common/Types' -const InfoIconTippy = ({ +const InfoIconTippy = ({ heading, infoText, iconClass = 'fcv-5', @@ -32,7 +32,9 @@ const InfoIconTippy = ({ children, headingInfo, buttonPadding = 'p-0', -}: InfoIconTippyProps) => ( + isExternalLink, + openInNewTab, +}: InfoIconTippyProps) => ( {children || (
)} diff --git a/src/Shared/Components/License/License.components.tsx b/src/Shared/Components/License/License.components.tsx index df879988a..585e43cdf 100644 --- a/src/Shared/Components/License/License.components.tsx +++ b/src/Shared/Components/License/License.components.tsx @@ -2,7 +2,6 @@ import { useEffect, useRef, useState } from 'react' import { ReactComponent as ICCheck } from '@Icons/ic-check.svg' import { ReactComponent as ICClipboard } from '@Icons/ic-copy.svg' -import { DOCUMENTATION } from '@Common/Constants' import { ClipboardButton, copyToClipboard, showError } from '@Common/index' import { Backdrop, Button, ButtonStyleType, ButtonVariantType, Icon, InfoIconTippy, QRCode } from '..' @@ -90,7 +89,8 @@ const InstallationFingerprintInfo = ({ fingerprint, showHelpTooltip = false }: I documentationLinkText="Documentation" iconClassName="icon-dim-20 fcn-6" placement="right" - documentationLink={DOCUMENTATION.ENTERPRISE_LICENSE} + documentationLink="ENTERPRISE_LICENSE" + openInNewTab /> )}
diff --git a/src/Shared/Components/LogoutCard.tsx b/src/Shared/Components/LogoutCard.tsx deleted file mode 100644 index 6f2e4cece..000000000 --- a/src/Shared/Components/LogoutCard.tsx +++ /dev/null @@ -1,85 +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 from 'react' -import { useHistory } from 'react-router-dom' - -import { clearCookieOnLogout } from '@Shared/Helpers' -import { useMainContext } from '@Shared/Providers' - -import { getRandomColor, stopPropagation } from '../../Common' -import { Icon } from './Icon' -import { ThemeSwitcher } from './ThemeSwitcher' - -interface LogoutCardType { - className: string - userFirstLetter: string - setShowLogOutCard: React.Dispatch> - showLogOutCard: boolean -} - -export const LOGOUT_CARD_BASE_BUTTON_CLASS = - 'dc__unset-button-styles dc__content-space px-12 py-8 fs-13 fw-4 lh-20 cursor w-100 flex left br-4' - -const LogoutCard = ({ className, userFirstLetter, setShowLogOutCard, showLogOutCard }: LogoutCardType) => { - const history = useHistory() - const { viewIsPipelineRBACConfiguredNode } = useMainContext() - - const onLogout = () => { - clearCookieOnLogout() - history.push('/login') - } - - const toggleLogoutCard = () => { - setShowLogOutCard(!showLogOutCard) - } - - return ( -
-
-
-
-

{userFirstLetter}

-

{userFirstLetter}

-
-

- {userFirstLetter[0]} -

-
-
- - - {viewIsPipelineRBACConfiguredNode} - - -
-
-
- ) -} - -export default LogoutCard diff --git a/src/Shared/Components/ModalSidebarPanel/ModalSidebarPanel.component.tsx b/src/Shared/Components/ModalSidebarPanel/ModalSidebarPanel.component.tsx index 13d863b8e..eb8315a25 100644 --- a/src/Shared/Components/ModalSidebarPanel/ModalSidebarPanel.component.tsx +++ b/src/Shared/Components/ModalSidebarPanel/ModalSidebarPanel.component.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ReactComponent as ArrowOut } from '../../../Assets/Icon/ic-arrow-square-out.svg' +import { DocLink } from '../DocLink' import { ModalSidebarPanelProps } from './types' const ModalSidebarPanel = ({ @@ -34,17 +34,15 @@ const ModalSidebarPanel = ({ )} {children &&
{children}
}
-
+
📙 Need help? - - View documentation - - +
) diff --git a/src/Shared/Components/ModalSidebarPanel/types.ts b/src/Shared/Components/ModalSidebarPanel/types.ts index 26787ea38..dbee95e50 100644 --- a/src/Shared/Components/ModalSidebarPanel/types.ts +++ b/src/Shared/Components/ModalSidebarPanel/types.ts @@ -16,10 +16,12 @@ import { ReactNode } from 'react' +import { DocLinkProps } from '../DocLink' + export interface ModalSidebarPanelProps { rootClassName?: string heading: string | null icon?: JSX.Element children?: ReactNode - documentationLink: string + documentationLink: DocLinkProps['docLinkKey'] } diff --git a/src/Shared/Components/NumbersCount/index.ts b/src/Shared/Components/NumbersCount/index.ts index b3b6ec1db..cdf8ca7ba 100644 --- a/src/Shared/Components/NumbersCount/index.ts +++ b/src/Shared/Components/NumbersCount/index.ts @@ -15,3 +15,4 @@ */ export { default as NumbersCount } from './NumbersCount.component' +export * from './types' diff --git a/src/Shared/Components/Popover/Popover.component.tsx b/src/Shared/Components/Popover/Popover.component.tsx index 68640c840..d332af886 100644 --- a/src/Shared/Components/Popover/Popover.component.tsx +++ b/src/Shared/Components/Popover/Popover.component.tsx @@ -15,24 +15,25 @@ export const Popover = ({ open, popoverProps, overlayProps, - triggerProps, + triggerProps: { bounds, ...triggerProps }, buttonProps, triggerElement, children, }: PopoverProps) => ( -
+ <>
{triggerElement ||
{open && ( - <> - {/* Overlay to block interactions with the background */} -
- - {children} - - +
+
+
+ + {children} + +
+
)} -
+ ) diff --git a/src/Shared/Components/Popover/popover.scss b/src/Shared/Components/Popover/popover.scss index f241ad180..26caeb6e0 100644 --- a/src/Shared/Components/Popover/popover.scss +++ b/src/Shared/Components/Popover/popover.scss @@ -6,7 +6,3 @@ left:0; z-index: var(--modal-index); } - -.popover-content { - z-index: var(--modal-index); -} diff --git a/src/Shared/Components/Popover/types.ts b/src/Shared/Components/Popover/types.ts index 5b1fdaac8..40ea7c343 100644 --- a/src/Shared/Components/Popover/types.ts +++ b/src/Shared/Components/Popover/types.ts @@ -1,4 +1,4 @@ -import { DetailedHTMLProps, KeyboardEvent, MutableRefObject, ReactElement } from 'react' +import { DetailedHTMLProps, KeyboardEvent, LegacyRef, MutableRefObject, ReactElement } from 'react' import { HTMLMotionProps } from 'framer-motion' import { ButtonProps } from '../Button' @@ -67,7 +67,9 @@ export interface UsePopoverReturnType { * Props to be spread onto the trigger element that opens the popover. * These props include standard HTML attributes for a `div` element. */ - triggerProps: DetailedHTMLProps, HTMLDivElement> + triggerProps: DetailedHTMLProps, HTMLDivElement> & { + bounds: Pick + } /** * Props to be spread onto the overlay element of the popover. * These props include standard HTML attributes for a `div` element. @@ -82,7 +84,7 @@ export interface UsePopoverReturnType { * A mutable reference to the scrollable element inside the popover. \ * This reference should be assigned to the element that is scrollable. */ - scrollableRef: MutableRefObject + scrollableRef: MutableRefObject | LegacyRef /** * A function to close the popover. */ diff --git a/src/Shared/Components/Popover/usePopover.hook.ts b/src/Shared/Components/Popover/usePopover.hook.ts index d95ff93b2..26a0b7a0e 100644 --- a/src/Shared/Components/Popover/usePopover.hook.ts +++ b/src/Shared/Components/Popover/usePopover.hook.ts @@ -1,4 +1,4 @@ -import { useLayoutEffect, useRef, useState } from 'react' +import { MouseEvent, useLayoutEffect, useRef, useState } from 'react' import { UsePopoverProps, UsePopoverReturnType } from './types' import { @@ -21,6 +21,7 @@ export const usePopover = ({ const [open, setOpen] = useState(false) const [actualPosition, setActualPosition] = useState(position) const [actualAlignment, setActualAlignment] = useState(alignment) + const [triggerBounds, setTriggerBounds] = useState(null) // CONSTANTS const isAutoWidth = width === 'auto' @@ -49,25 +50,42 @@ export const usePopover = ({ onTriggerKeyDown?.(e, open, closePopover) } - const handlePopoverKeyDown = (e: React.KeyboardEvent) => onPopoverKeyDown(e, open, closePopover) + const handlePopoverKeyDown = (e: React.KeyboardEvent) => onPopoverKeyDown?.(e, open, closePopover) + + const handleOverlayClick = (e: MouseEvent) => { + if (!popover.current?.contains(e.target as Node)) { + closePopover() + } + } useLayoutEffect(() => { if (!open || !triggerRef.current || !popover.current || !scrollableRef.current) { return } - const triggerRect = triggerRef.current.getBoundingClientRect() - const popoverRect = popover.current.getBoundingClientRect() - - const { fallbackPosition, fallbackAlignment } = getPopoverActualPositionAlignment({ - position, - alignment, - triggerRect, - popoverRect, - }) + const updatePopoverPosition = () => { + const triggerRect = triggerRef.current.getBoundingClientRect() + const popoverRect = popover.current.getBoundingClientRect() + + const { fallbackPosition, fallbackAlignment } = getPopoverActualPositionAlignment({ + position, + alignment, + triggerRect, + popoverRect, + }) + + setActualPosition(fallbackPosition) + setActualAlignment(fallbackAlignment) + setTriggerBounds({ + left: triggerRect.left, + top: triggerRect.top, + height: triggerRect.height, + width: triggerRect.width, + }) + } - setActualPosition(fallbackPosition) - setActualAlignment(fallbackAlignment) + // update position on open + updatePopoverPosition() // prevent scroll propagation unless scrollable const handleWheel = (e: WheelEvent) => { @@ -84,9 +102,12 @@ export const usePopover = ({ } scrollableRef.current.addEventListener('wheel', handleWheel, { passive: false }) + window.addEventListener('resize', updatePopoverPosition) + // eslint-disable-next-line consistent-return return () => { scrollableRef.current.removeEventListener('wheel', handleWheel) + window.removeEventListener('resize', updatePopoverPosition) } }, [open, position, alignment]) @@ -100,17 +121,18 @@ export const usePopover = ({ 'aria-haspopup': 'listbox', 'aria-expanded': open, tabIndex: 0, + bounds: triggerBounds ?? { left: 0, top: 0, height: 0, width: 0 }, }, overlayProps: { role: 'dialog', - onClick: closePopover, + onClick: handleOverlayClick, className: 'popover-overlay', }, popoverProps: { id, ref: popover, role: 'listbox', - className: `popover-content dc__position-abs bg__menu--primary shadow__menu border__primary br-6 dc__overflow-hidden ${isAutoWidth ? 'dc_width-max-content dc__mxw-250' : ''}`, + className: `dc__position-abs bg__menu--primary shadow__menu border__primary br-6 dc__overflow-hidden ${isAutoWidth ? 'dc_width-max-content dc__mxw-250' : ''}`, onKeyDown: handlePopoverKeyDown, style: { width: !isAutoWidth ? `${width}px` : undefined, diff --git a/src/Shared/Components/Switch/Switch.component.tsx b/src/Shared/Components/Switch/Switch.component.tsx new file mode 100644 index 000000000..9e32a0feb --- /dev/null +++ b/src/Shared/Components/Switch/Switch.component.tsx @@ -0,0 +1,150 @@ +import { AriaAttributes, useRef } from 'react' +import { AnimatePresence, motion } from 'framer-motion' + +import { Tooltip } from '@Common/Tooltip' +import { ComponentSizeType } from '@Shared/constants' +import { getUniqueId } from '@Shared/Helpers' + +import { Icon } from '../Icon' +import { INDETERMINATE_ICON_WIDTH_MAP, LOADING_COLOR_MAP } from './constants' +import { DTSwitchProps } from './types' +import { + getSwitchContainerClass, + getSwitchIconColor, + getSwitchThumbClass, + getSwitchTrackColor, + getSwitchTrackHoverColor, + getThumbPadding, + getThumbPosition, +} from './utils' + +import './switch.scss' + +const Switch = ({ + ariaLabel, + isDisabled, + isLoading, + isChecked, + tooltipContent, + shape = 'rounded', + variant = 'positive', + iconColor, + iconName, + indeterminate = false, + size = ComponentSizeType.medium, + name, + onChange, +}: DTSwitchProps) => { + const inputId = useRef(getUniqueId()) + + const getAriaCheckedValue = (): AriaAttributes['aria-checked'] => { + if (!isChecked) { + return false + } + + return indeterminate ? 'mixed' : true + } + + const ariaCheckedValue = getAriaCheckedValue() + + const showIndeterminateIcon = ariaCheckedValue === 'mixed' + + const renderContent = () => ( + + {isLoading ? ( + + + + ) : ( + + + {showIndeterminateIcon ? ( + + ) : ( + iconName && ( + + + + ) + )} + + + )} + + ) + + return ( + + + + ) +} + +export default Switch diff --git a/src/Shared/Components/Switch/constants.ts b/src/Shared/Components/Switch/constants.ts new file mode 100644 index 000000000..6d1c97cbc --- /dev/null +++ b/src/Shared/Components/Switch/constants.ts @@ -0,0 +1,64 @@ +import { ComponentSizeType } from '@Shared/constants' +import { IconBaseColorType } from '@Shared/types' + +import { DTSwitchProps } from './types' + +export const ROUNDED_SWITCH_SIZE_MAP: Readonly> = { + [ComponentSizeType.medium]: 'w-32', + [ComponentSizeType.small]: 'w-24', +} + +export const SQUARE_SWITCH_SIZE_MAP: typeof ROUNDED_SWITCH_SIZE_MAP = { + [ComponentSizeType.medium]: 'w-28', + [ComponentSizeType.small]: 'w-24', +} + +export const SWITCH_HEIGHT_MAP: Readonly> = { + [ComponentSizeType.medium]: 'h-24', + [ComponentSizeType.small]: 'h-20', +} + +export const LOADING_COLOR_MAP: Record = { + theme: 'B500', + positive: 'G500', +} + +export const ROUNDED_SWITCH_TRACK_COLOR_MAP: Record = { + theme: 'bcb-5', + positive: 'bcg-5', +} + +export const ROUNDED_SWITCH_TRACK_HOVER_COLOR_MAP: Record = { + theme: 'var(--B600)', + positive: 'var(--G600)', +} + +export const SQUARE_SWITCH_TRACK_COLOR_MAP: typeof ROUNDED_SWITCH_TRACK_COLOR_MAP = { + theme: 'bcb-3', + positive: 'bcg-3', +} + +export const SQUARE_SWITCH_TRACK_HOVER_COLOR_MAP: typeof ROUNDED_SWITCH_TRACK_HOVER_COLOR_MAP = { + theme: 'var(--B400)', + positive: 'var(--G400)', +} + +export const ROUNDED_SWITCH_THUMB_SIZE_MAP: Record = { + [ComponentSizeType.medium]: 'icon-dim-16', + [ComponentSizeType.small]: 'icon-dim-12', +} + +export const INDETERMINATE_ICON_WIDTH_MAP: Record = { + [ComponentSizeType.medium]: 'w-12', + [ComponentSizeType.small]: 'w-10', +} + +export const SWITCH_THUMB_PADDING_MAP: Record = { + [ComponentSizeType.medium]: 'p-3', + [ComponentSizeType.small]: 'p-1', +} + +export const THUMB_OUTER_PADDING_MAP: Record = { + rounded: 'p-2', + square: 'p-1', +} diff --git a/src/Shared/Components/Switch/index.ts b/src/Shared/Components/Switch/index.ts new file mode 100644 index 000000000..a5ff57e39 --- /dev/null +++ b/src/Shared/Components/Switch/index.ts @@ -0,0 +1,2 @@ +export { default as DTSwitch } from './Switch.component' +export type { DTSwitchProps } from './types' diff --git a/src/Shared/Components/Switch/switch.scss b/src/Shared/Components/Switch/switch.scss new file mode 100644 index 000000000..522b011c0 --- /dev/null +++ b/src/Shared/Components/Switch/switch.scss @@ -0,0 +1,9 @@ +.dt-switch { + &__track { + --switch-track-hover-color: 'transparent'; + + &:hover { + background-color: var(--switch-track-hover-color); + } + } +} \ No newline at end of file diff --git a/src/Shared/Components/Switch/types.ts b/src/Shared/Components/Switch/types.ts new file mode 100644 index 000000000..fa4c6e42c --- /dev/null +++ b/src/Shared/Components/Switch/types.ts @@ -0,0 +1,119 @@ +import { ButtonHTMLAttributes } from 'react' + +import { ComponentSizeType } from '@Shared/constants' +import { IconBaseColorType } from '@Shared/types' + +import { IconName } from '../Icon' + +/** + * Represents the properties for configuring the shape and behavior of a switch component. + * + * - When `shape` is `rounded`: + * - The switch will have a rounded appearance. + * - `iconName`, `iconColor`, and `indeterminate` are not applicable. + * + * - When `shape` is `square`: + * - The switch will have a square appearance. + * - `iconName` specifies the name of the icon to display. + * - `iconColor` allows customization of the icon's color in the active state. + * - `indeterminate` indicates whether the switch is in an indeterminate state, typically used for checkboxes to represent a mixed state. + * If `indeterminate` is true, the switch will not be fully checked or unchecked. + */ +type SwitchShapeProps = + | { + /** + * The shape of the switch. Defaults to `rounded` if not specified. + */ + shape?: 'rounded' + + /** + * Icon name is not applicable for the `rounded` shape. + */ + iconName?: never + + /** + * Icon color is not applicable for the `rounded` shape. + */ + iconColor?: never + /** + * Indicates whether the switch is in an indeterminate state. + * This state is typically used for checkboxes to indicate a mixed state. + * If true, the switch will not be fully checked or unchecked. Due this state alone we are keeping role as `checkbox` instead of `switch`. + * This property is not applicable for the `square` shape. + * @default false + */ + indeterminate?: boolean + } + | { + /** + * The shape of the switch. Must be `square` to enable icon-related properties. + */ + shape: 'square' + + /** + * The name of the icon to display when the shape is `square`. + */ + iconName: IconName + + /** + * The color of the icon. If provided, this will override the default color in the active state. + */ + iconColor?: IconBaseColorType + indeterminate?: never + } + +/** + * Represents the properties for the `Switch` component. + */ +export type DTSwitchProps = { + /** + * The ARIA label for the switch, used for accessibility purposes. + */ + ariaLabel: string + + /** + * Used in forms to identify the switch. + */ + name: string + + /** + * The visual variant of the switch. + * + * @default `positive` + */ + variant?: 'theme' | 'positive' + + /** + * The size of the switch. + * @default `ComponentSizeType.medium` + */ + size?: Extract + + /** + * Callback function that is called when the switch state changes. + * This function should handle the logic for toggling the switch. + */ + onChange: ButtonHTMLAttributes['onClick'] + + /** + * Indicates whether the switch is disabled. + */ + isDisabled?: boolean + + /** + * Indicates whether the switch is in a loading state. + */ + isLoading?: boolean + + /** + * Indicates whether the switch is currently checked (on). + */ + isChecked: boolean + + /** + * Optional tooltip content to display when hovering over the switch. + * + * @default undefined + */ + tooltipContent?: string +} & SwitchShapeProps diff --git a/src/Shared/Components/Switch/utils.ts b/src/Shared/Components/Switch/utils.ts new file mode 100644 index 000000000..514697ff6 --- /dev/null +++ b/src/Shared/Components/Switch/utils.ts @@ -0,0 +1,94 @@ +import { IconBaseColorType } from '@Shared/types' + +import { + ROUNDED_SWITCH_SIZE_MAP, + ROUNDED_SWITCH_THUMB_SIZE_MAP, + ROUNDED_SWITCH_TRACK_COLOR_MAP, + ROUNDED_SWITCH_TRACK_HOVER_COLOR_MAP, + SQUARE_SWITCH_SIZE_MAP, + SQUARE_SWITCH_TRACK_COLOR_MAP, + SQUARE_SWITCH_TRACK_HOVER_COLOR_MAP, + SWITCH_HEIGHT_MAP, + SWITCH_THUMB_PADDING_MAP, + THUMB_OUTER_PADDING_MAP, +} from './constants' +import { DTSwitchProps } from './types' + +export const getSwitchContainerClass = ({ shape, size }: Required>): string => + `${SWITCH_HEIGHT_MAP[size]} ${shape === 'rounded' ? ROUNDED_SWITCH_SIZE_MAP[size] : SQUARE_SWITCH_SIZE_MAP[size]}` + +export const getSwitchTrackColor = ({ + shape, + variant, + isChecked, + isLoading, +}: Required>): string => { + if (isLoading) { + return 'dc__transparent--unstyled' + } + + if (!isChecked) { + return 'bcn-2' + } + + return shape === 'rounded' ? ROUNDED_SWITCH_TRACK_COLOR_MAP[variant] : SQUARE_SWITCH_TRACK_COLOR_MAP[variant] +} + +export const getSwitchTrackHoverColor = ({ + shape, + variant, + isChecked, +}: Required< + Pick +>): (typeof ROUNDED_SWITCH_TRACK_HOVER_COLOR_MAP)[DTSwitchProps['variant']] => { + if (!isChecked) { + return 'var(--N300)' + } + + return shape === 'rounded' + ? ROUNDED_SWITCH_TRACK_HOVER_COLOR_MAP[variant] + : SQUARE_SWITCH_TRACK_HOVER_COLOR_MAP[variant] +} + +export const getSwitchThumbClass = ({ + shape, + size, + showIndeterminateIcon, +}: Pick & { showIndeterminateIcon: boolean }) => { + if (showIndeterminateIcon) { + return 'w-100 h-100 flex' + } + + return `flex ${SWITCH_THUMB_PADDING_MAP[size]} ${shape === 'rounded' ? `dc__border-radius-50-per ${ROUNDED_SWITCH_THUMB_SIZE_MAP[size]}` : 'br-3'} bg__white` +} + +export const getSwitchIconColor = ({ + iconColor, + isChecked, + variant, +}: Pick): IconBaseColorType => { + if (!isChecked) { + return 'N500' + } + + return iconColor || (variant === 'theme' ? 'B500' : 'G500') +} + +export const getThumbPosition = ({ + isLoading, + isChecked, +}: Pick): 'left' | 'right' | 'center' => { + if (isLoading) { + return 'center' + } + + return isChecked ? 'right' : 'left' +} + +export const getThumbPadding = ({ shape, isLoading }: Pick): string => { + if (isLoading) { + return '' + } + + return THUMB_OUTER_PADDING_MAP[shape] +} diff --git a/src/Shared/Components/ThemeSwitcher/ThemeSwitcher.component.tsx b/src/Shared/Components/ThemeSwitcher/ThemeSwitcher.component.tsx index 93f23dcf1..587de3b63 100644 --- a/src/Shared/Components/ThemeSwitcher/ThemeSwitcher.component.tsx +++ b/src/Shared/Components/ThemeSwitcher/ThemeSwitcher.component.tsx @@ -14,32 +14,31 @@ * limitations under the License. */ -import { ReactComponent as ICCaretLeftSmall } from '@Icons/ic-caret-left-small.svg' import { getThemePreferenceText, useTheme } from '@Shared/Providers' -import { LOGOUT_CARD_BASE_BUTTON_CLASS } from '../LogoutCard' +import { Icon } from '../Icon' import { ThemeSwitcherProps } from './types' -const ThemeSwitcher = ({ onChange }: ThemeSwitcherProps) => { +const ThemeSwitcher = ({ onClick }: ThemeSwitcherProps) => { const { handleThemeSwitcherDialogVisibilityChange, themePreference } = useTheme() const handleShowThemeSwitcherDialog = () => { handleThemeSwitcherDialogVisibilityChange(true) - onChange() + onClick?.() } return ( ) } diff --git a/src/Shared/Components/ThemeSwitcher/index.ts b/src/Shared/Components/ThemeSwitcher/index.ts index 5913dfa3f..9dba93c23 100644 --- a/src/Shared/Components/ThemeSwitcher/index.ts +++ b/src/Shared/Components/ThemeSwitcher/index.ts @@ -15,4 +15,3 @@ */ export { default as ThemeSwitcher } from './ThemeSwitcher.component' -export type { ThemeSwitcherProps } from './types' diff --git a/src/Shared/Components/ThemeSwitcher/types.ts b/src/Shared/Components/ThemeSwitcher/types.ts index a75045c9b..f86536088 100644 --- a/src/Shared/Components/ThemeSwitcher/types.ts +++ b/src/Shared/Components/ThemeSwitcher/types.ts @@ -15,5 +15,5 @@ */ export interface ThemeSwitcherProps { - onChange: () => void + onClick?: () => void } diff --git a/src/Shared/Components/index.ts b/src/Shared/Components/index.ts index 89dcbeb34..a6252dbd6 100644 --- a/src/Shared/Components/index.ts +++ b/src/Shared/Components/index.ts @@ -42,7 +42,7 @@ export * from './DatePicker' export * from './DeploymentConfigDiff' export * from './DeploymentStatusBreakdown' export * from './DetectBottom' -export * from './DiffViewer' +export * from './DocLink' export * from './DynamicDataTable' export * from './EditableTextArea' export * from './EditImageFormField' @@ -88,6 +88,7 @@ export * from './SelectPicker' export * from './ShowMoreText' export * from './SSOProviderIcon' export * from './StatusComponent' +export * from './Switch' export * from './TabGroup' export * from './Table' export * from './TagsKeyValueTable' diff --git a/src/Shared/Helpers.tsx b/src/Shared/Helpers.tsx index 2120400ef..b11bccb57 100644 --- a/src/Shared/Helpers.tsx +++ b/src/Shared/Helpers.tsx @@ -19,6 +19,7 @@ import { ReactElement, useEffect, useRef, useState } from 'react' import { PromptProps } from 'react-router-dom' import { StrictRJSFSchema } from '@rjsf/utils' import Tippy from '@tippyjs/react' +import { animate } from 'framer-motion' import moment from 'moment' import { nanoid } from 'nanoid' import { Pair } from 'yaml' @@ -52,7 +53,7 @@ import { } from '../Common' import { getAggregator } from '../Pages' import { AggregatedNodes, PodMetadatum } from './Components' -import { UNSAVED_CHANGES_PROMPT_MESSAGE } from './constants' +import { CUBIC_BEZIER_CURVE, UNSAVED_CHANGES_PROMPT_MESSAGE } from './constants' import { AggregationKeys, BorderConfigType, @@ -700,3 +701,16 @@ export const getAppDetailsURL = (appId: number | string, envId?: number | string } return baseURL } + +export const smoothScrollToTop = (scrollContainer: HTMLElement, targetPosition: number) => { + const start = scrollContainer.scrollTop + + const controls = animate(start, targetPosition, { + ease: CUBIC_BEZIER_CURVE, + onUpdate: (value) => { + scrollContainer.scrollTop = value + }, + }) + + return controls +} diff --git a/src/Shared/Providers/index.ts b/src/Shared/Providers/index.ts index 1ba004223..a1dcc842c 100644 --- a/src/Shared/Providers/index.ts +++ b/src/Shared/Providers/index.ts @@ -17,5 +17,5 @@ export * from './ImageSelectionUtility' export * from './MainContextProvider' export * from './ThemeProvider' -export type { MainContext, ReloadVersionConfigTypes } from './types' +export type { MainContext, ReloadVersionConfigTypes, SidePanelConfig } from './types' export * from './UserEmailProvider' diff --git a/src/Shared/Providers/types.ts b/src/Shared/Providers/types.ts index 82f1630cb..4911a42b9 100644 --- a/src/Shared/Providers/types.ts +++ b/src/Shared/Providers/types.ts @@ -29,6 +29,16 @@ export interface ReloadVersionConfigTypes { updateToastRef: MutableRefObject> | null isRefreshing: boolean } + +export interface SidePanelConfig { + /** Determines whether the side panel is visible */ + open: boolean + /** Optional flag to reset/reinitialize the side panel state */ + reinitialize?: boolean + /** URL to documentation that should be displayed in the panel */ + docLink: string | null +} + export interface MainContext { serverMode: SERVER_MODE setServerMode: (serverMode: SERVER_MODE) => void @@ -78,6 +88,20 @@ export interface MainContext { reloadVersionConfig: ReloadVersionConfigTypes intelligenceConfig: IntelligenceConfig setIntelligenceConfig: Dispatch> + + sidePanelConfig: SidePanelConfig + setSidePanelConfig: Dispatch> + + /** + * Indicates whether the current Devtron instance is running as an Enterprise edition. \ + * This flag is determined based on server-side configuration. + */ + isEnterprise: boolean + /** + * Indicates whether the fe-lib modules are available in the current instance. \ + * Used to conditionally render or enable features that depend on fe-lib + */ + isFELibAvailable: boolean } export interface MainContextProviderProps { diff --git a/src/Shared/constants.tsx b/src/Shared/constants.tsx index 501c21c70..948a007d9 100644 --- a/src/Shared/constants.tsx +++ b/src/Shared/constants.tsx @@ -580,3 +580,4 @@ export const DEPLOYMENT_STAGE_TO_NODE_MAP: Readonly = { [K in keyof T]?: never } +/** + * A utility type that filters out properties from type `T` that are of type `never`. \ + * This is useful when you want to remove properties that have been marked as `never` from a type, + * effectively creating a new type without those properties. + * + * @template T - The input type from which to filter out `never` properties. + * @example + * ```typescript + * type User = { + * id: number; + * name: string; + * deleted: never; + * } + * + * type ActiveUser = OmitNever; // { id: number; name: string; } + * ``` + */ +export type OmitNever = { + [K in keyof T as T[K] extends never ? never : K]: T[K] +} + export interface TargetPlatformItemDTO { name: string } diff --git a/src/Shared/validations.tsx b/src/Shared/validations.tsx index 7f2c3cf06..c55244015 100644 --- a/src/Shared/validations.tsx +++ b/src/Shared/validations.tsx @@ -497,3 +497,25 @@ export const validateYAML = (yamlString: string, isRequired?: boolean): Validati } } } + +export const validateEmail = (email: string): ValidationResponseType => { + if (!email) { + return { + isValid: false, + message: 'Email is required', + } + } + + const result = PATTERNS.EMAIL.test(String(email).toLowerCase()) + + if (result) { + return { + isValid: true, + } + } + + return { + isValid: false, + message: 'Please provide a valid email address', + } +}