diff --git a/.eslintignore b/.eslintignore index e9659cf41..551aa469f 100755 --- a/.eslintignore +++ b/.eslintignore @@ -10,7 +10,6 @@ src/Common/BreadCrumb/BreadcrumbStore.tsx src/Common/CIPipeline.Types.ts src/Common/ChartVersionAndTypeSelector.tsx src/Common/Checkbox.tsx -src/Common/ClipboardButton/ClipboardButton.tsx src/Common/ClipboardButton/__tests__/ClipboardButton.test.tsx src/Common/CodeEditor/CodeEditor.tsx src/Common/Common.service.ts diff --git a/package-lock.json b/package-lock.json index 9b1ecfedd..adf45b8ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.0.4-beta-6", + "version": "1.1.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.0.4-beta-6", + "version": "1.1.3", "license": "ISC", "dependencies": { "@types/react-dates": "^21.8.6", @@ -15,6 +15,7 @@ "fast-json-patch": "^3.1.1", "jsonpath-plus": "^10.0.0", "react-dates": "^21.8.0", + "react-diff-viewer-continued": "^3.4.0", "react-monaco-editor": "^0.54.0", "sass": "^1.69.7", "tslib": "2.7.0" @@ -612,7 +613,6 @@ "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", @@ -630,14 +630,12 @@ "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==", - "peer": true + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" }, "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", @@ -646,17 +644,28 @@ "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==", - "peer": true + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==" }, "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==", - "peer": true + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==" }, "node_modules/@emotion/react": { "version": "11.13.3", @@ -686,7 +695,6 @@ "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", @@ -698,14 +706,12 @@ "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==", - "peer": true + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==" }, "node_modules/@emotion/unitless": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", - "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", - "peer": true + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==" }, "node_modules/@emotion/use-insertion-effect-with-fallbacks": { "version": "1.1.0", @@ -719,14 +725,12 @@ "node_modules/@emotion/utils": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.1.tgz", - "integrity": "sha512-BymCXzCG3r72VKJxaYVwOXATqXIZ85cuvg0YOUDxMGNrKc1DJRZk8MgV5wyXRyEayIMd4FuXJIUgTBXvDNW5cA==", - "peer": true + "integrity": "sha512-BymCXzCG3r72VKJxaYVwOXATqXIZ85cuvg0YOUDxMGNrKc1DJRZk8MgV5wyXRyEayIMd4FuXJIUgTBXvDNW5cA==" }, "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==", - "peer": true + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==" }, "node_modules/@esbuild-plugins/node-globals-polyfill": { "version": "0.2.3", @@ -3872,7 +3876,6 @@ "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", @@ -4046,6 +4049,12 @@ "node": ">=8" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -4406,6 +4415,15 @@ "node": ">=0.10" } }, + "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", @@ -5557,8 +5575,7 @@ "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==", - "peer": true + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" }, "node_modules/find-up": { "version": "5.0.0", @@ -7306,8 +7323,7 @@ "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==", - "peer": true + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" }, "node_modules/merge-stream": { "version": "2.0.0", @@ -8153,6 +8169,26 @@ "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", @@ -8889,7 +8925,6 @@ "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" } @@ -9190,8 +9225,7 @@ "node_modules/stylis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", - "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", - "peer": true + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" }, "node_modules/supports-color": { "version": "7.2.0", diff --git a/package.json b/package.json index 11abb5c40..9704480bd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.0.4-beta-6", + "version": "1.1.3", "description": "Supporting common component library", "type": "module", "main": "dist/index.js", @@ -97,6 +97,7 @@ "fast-json-patch": "^3.1.1", "jsonpath-plus": "^10.0.0", "react-dates": "^21.8.0", + "react-diff-viewer-continued": "^3.4.0", "react-monaco-editor": "^0.54.0", "sass": "^1.69.7", "tslib": "2.7.0" diff --git a/src/Assets/Icon/ic-hash.svg b/src/Assets/Icon/ic-hash.svg new file mode 100644 index 000000000..8220a82ae --- /dev/null +++ b/src/Assets/Icon/ic-hash.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/Assets/Icon/ic-info-outline.svg b/src/Assets/Icon/ic-info-outline.svg new file mode 100644 index 000000000..b3912b7dc --- /dev/null +++ b/src/Assets/Icon/ic-info-outline.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/Assets/Icon/ic-pull-request.svg b/src/Assets/Icon/ic-pull-request.svg index 9ba5cc629..695951ef5 100644 --- a/src/Assets/Icon/ic-pull-request.svg +++ b/src/Assets/Icon/ic-pull-request.svg @@ -14,6 +14,6 @@ - limitations under the License. --> - - + + \ No newline at end of file diff --git a/src/Assets/Icon/ic-tag.svg b/src/Assets/Icon/ic-tag.svg index f71d3f1f8..65b11a10d 100644 --- a/src/Assets/Icon/ic-tag.svg +++ b/src/Assets/Icon/ic-tag.svg @@ -14,7 +14,6 @@ - limitations under the License. --> - - - + + \ No newline at end of file diff --git a/src/Common/Api.ts b/src/Common/Api.ts index 485d07f9c..1e1985c8b 100644 --- a/src/Common/Api.ts +++ b/src/Common/Api.ts @@ -192,14 +192,18 @@ function fetchInTime( options?: APIOptions, isMultipartRequest?: boolean, ): Promise { - const controller = new AbortController() - const { signal } = controller + const controller = options?.abortControllerRef?.current ?? new AbortController() + const signal = options?.abortControllerRef?.current?.signal || options?.signal || controller.signal const timeoutPromise: Promise = new Promise((resolve, reject) => { const requestTimeout = (window as any)?._env_?.GLOBAL_API_TIMEOUT || FALLBACK_REQUEST_TIMEOUT const timeout = options?.timeout ? options.timeout : requestTimeout setTimeout(() => { controller.abort() + if (options?.abortControllerRef?.current) { + options.abortControllerRef.current = new AbortController() + } + reject({ code: 408, errors: [{ code: 408, internalMessage: 'Request cancelled', userMessage: 'Request Cancelled' }], @@ -207,7 +211,14 @@ function fetchInTime( }, timeout) }) return Promise.race([ - fetchAPI(url, type, data, options?.signal || signal, options?.preventAutoLogout || false, isMultipartRequest), + fetchAPI( + url, + type, + data, + signal, + options?.preventAutoLogout || false, + isMultipartRequest, + ), timeoutPromise, ]).catch((err) => { if (err instanceof ServerErrors) { diff --git a/src/Common/ClipboardButton/ClipboardButton.tsx b/src/Common/ClipboardButton/ClipboardButton.tsx index 878b55a77..80122bd5a 100644 --- a/src/Common/ClipboardButton/ClipboardButton.tsx +++ b/src/Common/ClipboardButton/ClipboardButton.tsx @@ -14,8 +14,8 @@ * limitations under the License. */ -import { useState, useEffect, useCallback } from 'react' -import Tippy from '@tippyjs/react' +import { useState, useEffect, useRef } from 'react' +import Tooltip from '@Common/Tooltip/Tooltip' import { copyToClipboard, noop, stopPropagation } from '../Helper' import ClipboardProps from './types' import { ReactComponent as ICCopy } from '../../Assets/Icon/ic-copy.svg' @@ -25,73 +25,86 @@ import { ReactComponent as Check } from '../../Assets/Icon/ic-check.svg' * @param content - Content to be copied * @param copiedTippyText - Text to be shown in the tippy when the content is copied, default 'Copied!' * @param duration - Duration for which the tippy should be shown, default 1000 - * @param trigger - To trigger the copy action outside the button, if set to true the content will be copied, use case being triggering the copy action from outside the component - * @param setTrigger - Callback function to set the trigger outside the button + * @param copyToClipboardPromise - the promise returned by copyToClipboard util function * @param rootClassName - additional classes to add to button * @param iconSize - size of svg icon to be shown, default 16 (icon-dim-16) */ -export default function ClipboardButton({ +export const ClipboardButton = ({ content, copiedTippyText = 'Copied!', duration = 1000, - trigger, - setTrigger = noop, + copyToClipboardPromise, rootClassName = '', iconSize = 16, -}: ClipboardProps) { +}: ClipboardProps) => { const [copied, setCopied] = useState(false) - const [enableTippy, setEnableTippy] = useState(false) + const setCopiedFalseTimeoutRef = useRef>(-1) - const handleTextCopied = () => { + const handleTriggerCopy = () => { setCopied(true) + + setCopiedFalseTimeoutRef.current = setTimeout(() => { + setCopied(false) + + setCopiedFalseTimeoutRef.current = -1 + }, duration) } - const isTriggerUndefined = typeof trigger === 'undefined' - const handleEnableTippy = () => setEnableTippy(true) - const handleDisableTippy = () => setEnableTippy(false) - const handleCopyContent = useCallback( - (e?) => { - if (e) stopPropagation(e) - copyToClipboard(content, handleTextCopied) - }, - [content], - ) - const iconClassName = `icon-dim-${iconSize} dc__no-shrink` + const handleAwaitCopyToClipboardPromise = async (shouldRunCopy?: boolean) => { + try { + if (shouldRunCopy) { + await copyToClipboard(content) + } else { + await copyToClipboardPromise + } - useEffect(() => { - if (!copied) return + handleTriggerCopy() + } catch { + noop() + } + } - const timeout = setTimeout(() => { - setCopied(false) - setTrigger(false) - }, duration) + const handleCopyContent = async (e?: React.MouseEvent) => { + if (e) { + stopPropagation(e) + } - return () => clearTimeout(timeout) - }, [copied, duration, setTrigger]) + await handleAwaitCopyToClipboardPromise(true) + } useEffect(() => { - if (!isTriggerUndefined && trigger) { - setCopied(true) - handleCopyContent() + if (!copyToClipboardPromise) { + return } - }, [trigger, handleCopyContent]) + + handleAwaitCopyToClipboardPromise().catch(noop) + }, [copyToClipboardPromise]) + + useEffect( + () => () => { + if (setCopiedFalseTimeoutRef.current > -1) { + clearTimeout(setCopiedFalseTimeoutRef.current) + } + }, + [], + ) + + const iconClassName = `icon-dim-${iconSize} dc__no-shrink` + return ( - + + {/* TODO: semantically buttons should not be nested; fix later */}
+ {copied ? : } +
+ - + ) } diff --git a/src/Common/ClipboardButton/types.ts b/src/Common/ClipboardButton/types.ts index 94eb73554..c8f02c73c 100644 --- a/src/Common/ClipboardButton/types.ts +++ b/src/Common/ClipboardButton/types.ts @@ -18,8 +18,7 @@ export default interface ClipboardProps { content: string copiedTippyText?: string duration?: number - trigger?: boolean - setTrigger?: React.Dispatch> + copyToClipboardPromise?: Promise rootClassName?: string iconSize?: number } diff --git a/src/Common/CodeEditor/CodeEditor.tsx b/src/Common/CodeEditor/CodeEditor.tsx index 7cd1e46ff..05e23ca53 100644 --- a/src/Common/CodeEditor/CodeEditor.tsx +++ b/src/Common/CodeEditor/CodeEditor.tsx @@ -30,7 +30,7 @@ import { cleanKubeManifest, useEffectAfterMount, useJsonYaml } from '../Helper' import { useWindowSize } from '../Hooks' import Select from '../Select/Select' import RadioGroup from '../RadioGroup/RadioGroup' -import ClipboardButton from '../ClipboardButton/ClipboardButton' +import { ClipboardButton } from '../ClipboardButton/ClipboardButton' import { Progressing } from '../Progressing' import { CodeEditorComposition, diff --git a/src/Common/Common.service.ts b/src/Common/Common.service.ts index 99e28ff6c..a9a5559d9 100644 --- a/src/Common/Common.service.ts +++ b/src/Common/Common.service.ts @@ -18,7 +18,7 @@ import moment from 'moment' import { RuntimeParamsAPIResponseType, RuntimeParamsListItemType } from '@Shared/types' import { getIsManualApprovalSpecific, sanitizeUserApprovalConfig, stringComparatorBySortOrder } from '@Shared/Helpers' import { get, post } from './Api' -import { ROUTES } from './Constants' +import { GitProviderType, ROUTES } from './Constants' import { getUrlWithSearchParams, sortCallback } from './Helper' import { TeamList, @@ -250,7 +250,7 @@ const getImageApprovalPolicyDetailsFromMaterialResult = (cdMaterialsResult): Ima const validGroups = userApprovalConfig.userGroups.map((group) => group.identifier) // Have moved from Object.keys(imageApprovalUsersInfo) to approvalUsers since backend is not filtering out the users without approval - // TODO: This check should be on BE. Need to remove this once BE is updated + // TODO: This check should be on BE. Need to remove this once BE is updated const usersList = approvalUsers.filter((user) => user !== DefaultUserKey.system) const groupIdentifierToUsersMap = usersList.reduce( (acc, user) => { @@ -511,3 +511,19 @@ export function getWebhookEventsForEventId(eventId: string | number) { const URL = `${ROUTES.GIT_HOST_EVENT}/${eventId}` return get(URL) } + +/** + * + * @param gitUrl Git URL of the repository + * @param branchName Branch name + * @returns URL to the branch in the Git repository + */ +export const getGitBranchUrl = (gitUrl: string, branchName: string): string | null => { + if (!gitUrl) return null + const trimmedGitUrl = gitUrl.trim().replace(/\.git$/, '').replace(/\/$/, '') // Remove any trailing slash + if (trimmedGitUrl.includes(GitProviderType.GITLAB)) return `${trimmedGitUrl}/-/tree/${branchName}` + else if (trimmedGitUrl.includes(GitProviderType.GITHUB)) return `${trimmedGitUrl}/tree/${branchName}` + else if (trimmedGitUrl.includes(GitProviderType.BITBUCKET)) return `${trimmedGitUrl}/branch/${branchName}` + else if (trimmedGitUrl.includes(GitProviderType.AZURE)) return `${trimmedGitUrl}/src/branch/${branchName}` + return null +} diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index c45423a77..4fc0796fc 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -560,3 +560,20 @@ export const VULNERABILITIES_SORT_PRIORITY = { // TODO: might not work need to verify export const IS_PLATFORM_MAC_OS = window.navigator.userAgent.toUpperCase().includes('MAC') + +/** + * Git provider types + */ + +export enum GitProviderType { + GITHUB = 'github', + GITLAB = 'gitlab', + BITBUCKET = 'bitbucket', + AZURE = 'azure', + GITEA = 'gitea', +} + +/** + * Formats the schema removing any irregularity in the existing schema + */ +export const getFormattedSchema = (schema?: string) => JSON.stringify(JSON.parse(schema ?? '{}'), null, 2) diff --git a/src/Common/CustomInput/CustomInput.tsx b/src/Common/CustomInput/CustomInput.tsx index 00c71316f..fdc0cbbad 100644 --- a/src/Common/CustomInput/CustomInput.tsx +++ b/src/Common/CustomInput/CustomInput.tsx @@ -69,9 +69,13 @@ export const CustomInput = ({ const handleOnBlur = (event) => { // NOTE: This is to prevent the input from being trimmed when the user do not want to trim the input if (!noTrim) { - event.stopPropagation() - event.target.value = event.target.value?.trim() - onChange(event) + const trimmedValue = event.target.value?.trim() + + if (event.target.value !== trimmedValue) { + event.stopPropagation() + event.target.value = trimmedValue + onChange(event) + } } if (typeof onBlur === 'function') { onBlur(event) diff --git a/src/Common/Helper.tsx b/src/Common/Helper.tsx index a2667d0cd..6c7f4869b 100644 --- a/src/Common/Helper.tsx +++ b/src/Common/Helper.tsx @@ -40,8 +40,15 @@ import { ToastManager, ToastVariantType, versionComparatorBySortOrder, + WebhookEventNameType, } from '../Shared' -import { ReactComponent as ArrowDown } from '../Assets/Icon/ic-chevron-down.svg' +import { ReactComponent as ArrowDown } from '@Icons/ic-chevron-down.svg' +import webhookIcon from '@Icons/ic-webhook.svg' +import branchIcon from '@Icons/ic-branch.svg' +import regexIcon from '@Icons/ic-regex.svg' +import pullRequest from '@Icons/ic-pull-request.svg' +import tagIcon from '@Icons/ic-tag.svg' +import { SourceTypeMap } from '@Common/Common.service' export function showError(serverError, showToastOnUnknownError = true, hideAccessError = false) { if (serverError instanceof ServerErrors && Array.isArray(serverError.errors)) { @@ -346,7 +353,7 @@ export function cleanKubeManifest(manifestJsonString: string): string { return manifestJsonString } } -const unsecureCopyToClipboard = (str, callback = noop) => { +const unsecureCopyToClipboard = (str: string) => { const listener = function (ev) { ev.preventDefault() ev.clipboardData.setData('text/plain', str) @@ -354,35 +361,41 @@ const unsecureCopyToClipboard = (str, callback = noop) => { document.addEventListener('copy', listener) document.execCommand('copy') document.removeEventListener('copy', listener) - callback() } /** - * It will copy the passed content to clipboard and invoke the callback function, in case of error it will show the toast message. - * On HTTP system clipboard is not supported, so it will use the unsecureCopyToClipboard function + * This is a promise that will resolve if str is successfully copied + * On HTTP (other than localhost) system clipboard is not supported, so it will use the unsecureCopyToClipboard function * @param str - * @param callback */ -export function copyToClipboard(str, callback = noop) { - if (!str) { - return - } +export function copyToClipboard(str: string): Promise { + return new Promise((resolve, reject) => { + if (!str) { + resolve() - if (window.isSecureContext && navigator.clipboard) { - navigator.clipboard - .writeText(str) - .then(() => { - callback() - }) - .catch(() => { - ToastManager.showToast({ - variant: ToastVariantType.error, - description: 'Failed to copy to clipboard', + return + } + + if (window.isSecureContext && navigator.clipboard) { + navigator.clipboard + .writeText(str) + .then(() => { + resolve() }) - }) - } else { - unsecureCopyToClipboard(str, callback) - } + .catch(() => { + ToastManager.showToast({ + variant: ToastVariantType.error, + description: 'Failed to copy to clipboard', + }) + + reject() + }) + } else { + unsecureCopyToClipboard(str) + + resolve() + } + }) } export function useAsync( @@ -956,6 +969,29 @@ export const throttle = unknown>( } } +/** + * + * @param sourceType - SourceTypeMap + * @param _isRegex - boolean + * @param webhookEventName - WebhookEventNameType + * @returns - Icon + */ +export const getBranchIcon = (sourceType, _isRegex?: boolean, webhookEventName?: string) => { + if (sourceType === SourceTypeMap.WEBHOOK) { + if (webhookEventName === WebhookEventNameType.PULL_REQUEST) { + return pullRequest + } + if (webhookEventName === WebhookEventNameType.TAG_CREATION) { + return tagIcon + } + return webhookIcon + } + if (sourceType === SourceTypeMap.BranchRegex || _isRegex) { + return regexIcon + } + return branchIcon +} + // TODO: Might need to expose sandbox and referrer policy export const getSanitizedIframe = (iframeString: string) => DOMPurify.sanitize(iframeString, { diff --git a/src/Common/Hooks/UseRegisterShortcut/types.ts b/src/Common/Hooks/UseRegisterShortcut/types.ts index 7239058c2..4fd7cc636 100644 --- a/src/Common/Hooks/UseRegisterShortcut/types.ts +++ b/src/Common/Hooks/UseRegisterShortcut/types.ts @@ -21,6 +21,7 @@ export const KEYBOARD_KEYS_MAP = { Shift: '⇧', Meta: IS_PLATFORM_MAC_OS ? '⌘' : 'Win', Alt: IS_PLATFORM_MAC_OS ? '⌥' : 'Alt', + Escape: 'Escape', F: 'F', E: 'E', R: 'R', diff --git a/src/Common/Hooks/useUrlFilters/useUrlFilters.ts b/src/Common/Hooks/useUrlFilters/useUrlFilters.ts index 6abb0a39e..4e314e6e0 100644 --- a/src/Common/Hooks/useUrlFilters/useUrlFilters.ts +++ b/src/Common/Hooks/useUrlFilters/useUrlFilters.ts @@ -161,9 +161,12 @@ const useUrlFilters = ({ } useEffect(() => { + if (!localStorageKey) { + return + } // if we have search string, set secondary params in local storage accordingly if (location.search) { - localStorage.setItem(localStorageKey, JSON.stringify(parsedParams)) + setItemInLocalStorageIfKeyExists(localStorageKey, JSON.stringify(parsedParams)) return } const localStorageValue = localStorage.getItem(localStorageKey) diff --git a/src/Common/Policy.Types.ts b/src/Common/Policy.Types.ts index b18617026..c3200e20c 100644 --- a/src/Common/Policy.Types.ts +++ b/src/Common/Policy.Types.ts @@ -95,7 +95,7 @@ export type ProcessPluginDataParamsType = { /** * Would be sent in case we have to get data for steps */ - requiredPluginIds?: PluginDetailPayloadType['pluginId'] + requiredPluginIds?: PluginDetailPayloadType['pluginIds'] } & (ProcessPluginDataCIParamsType | ProcessPluginDataCDParamsType) export enum ConsequenceAction { diff --git a/src/Common/Types.ts b/src/Common/Types.ts index fbf837099..6071e8c4e 100644 --- a/src/Common/Types.ts +++ b/src/Common/Types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { ReactNode, CSSProperties, ReactElement } from 'react' +import React, { ReactNode, CSSProperties, ReactElement, MutableRefObject } from 'react' import { Placement } from 'tippy.js' import { UserGroupDTO } from '@Pages/GlobalConfigurations' import { ImageComment, ReleaseTag } from './ImageTags.Types' @@ -50,7 +50,11 @@ export interface ResponseType { export interface APIOptions { timeout?: number + /** + * @deprecated Use abortController instead + */ signal?: AbortSignal + abortControllerRef?: MutableRefObject preventAutoLogout?: boolean } @@ -1007,4 +1011,4 @@ export interface WidgetEventDetails { count: number age: string lastSeen: string -} \ No newline at end of file +} diff --git a/src/Common/index.ts b/src/Common/index.ts index fcc817351..05d2a2c14 100644 --- a/src/Common/index.ts +++ b/src/Common/index.ts @@ -55,7 +55,7 @@ export { default as DebouncedSearch } from './DebouncedSearch/DebouncedSearch' export { default as Grid } from './Grid/Grid' // export { default as CodeEditor } from './CodeEditor/CodeEditor' export { default as Select } from './Select/Select' -export { default as ClipboardButton } from './ClipboardButton/ClipboardButton' +export { ClipboardButton } from './ClipboardButton/ClipboardButton' export * from './Hooks' export * from './RJSF' export * from './DevtronProgressing' diff --git a/src/Pages/Applications/DevtronApps/Details/AppConfigurations/DeploymentTemplate/types.ts b/src/Pages/Applications/DevtronApps/Details/AppConfigurations/DeploymentTemplate/types.ts index 0b3729d0a..f7660c177 100644 --- a/src/Pages/Applications/DevtronApps/Details/AppConfigurations/DeploymentTemplate/types.ts +++ b/src/Pages/Applications/DevtronApps/Details/AppConfigurations/DeploymentTemplate/types.ts @@ -1,4 +1,5 @@ import { DraftMetadataDTO, TemplateListType } from '@Shared/Services' +import { ServerErrors } from '@Common/ServerError' import { OverrideMergeStrategyType } from '../types' export type DeploymentChartOptionkind = 'base' | 'env' | 'chartVersion' | 'deployment' @@ -162,6 +163,9 @@ export interface DeploymentTemplateConfigCommonState extends SelectedChartDetail * In current editor, this may be null initially */ mergedTemplateObject: Record | null + + isLoadingMergedTemplate: boolean + mergedTemplateError: ServerErrors | null } export type DeploymentTemplateConfigState = DeploymentTemplateConfigCommonState & diff --git a/src/Shared/Components/CICDHistory/CiPipelineSourceConfig.tsx b/src/Shared/Components/CICDHistory/CiPipelineSourceConfig.tsx index da874cb0f..a5f42a03f 100644 --- a/src/Shared/Components/CICDHistory/CiPipelineSourceConfig.tsx +++ b/src/Shared/Components/CICDHistory/CiPipelineSourceConfig.tsx @@ -16,24 +16,11 @@ import { useState, useEffect, ReactNode } from 'react' import Tippy from '@tippyjs/react' -import { getWebhookEventsForEventId, SourceTypeMap } from '../../../Common' +import { ReactComponent as Info } from '@Icons/ic-info-outlined.svg' +import { getBranchIcon, getWebhookEventsForEventId, SourceTypeMap } from '../../../Common' import { GIT_BRANCH_NOT_CONFIGURED, DEFAULT_GIT_BRANCH_VALUE } from './constants' -import webhookIcon from '../../../Assets/Icon/ic-webhook.svg' -import branchIcon from '../../../Assets/Icon/ic-branch.svg' -import { ReactComponent as Info } from '../../../Assets/Icon/ic-info-outlined.svg' -import regexIcon from '../../../Assets/Icon/ic-regex.svg' import { buildHoverHtmlForWebhook } from './utils' - -export interface CIPipelineSourceConfigInterface { - sourceType - sourceValue - showTooltip?: boolean - showIcons?: boolean - baseText?: string - regex?: any - isRegex?: boolean - primaryBranchAfterRegex?: string -} +import { CIPipelineSourceConfigInterface } from './types' export const CiPipelineSourceConfig = ({ sourceType, @@ -44,11 +31,12 @@ export const CiPipelineSourceConfig = ({ regex, isRegex, primaryBranchAfterRegex, + rootClassName = '', }: CIPipelineSourceConfigInterface) => { const _isWebhook = sourceType === SourceTypeMap.WEBHOOK const _isRegex = sourceType === SourceTypeMap.BranchRegex || !!regex || isRegex - const [sourceValueBase, setSourceValueBase] = useState('') + const [sourceValueBase, setSourceValueBase] = useState('') const [sourceValueAdv, setSourceValueAdv] = useState('') const [loading, setLoading] = useState(!!_isWebhook) @@ -97,7 +85,7 @@ export const CiPipelineSourceConfig = ({ )} ) - // for non webhook case, data is already set in use state initialisation + // for non webhook case, data is already set in use state initialization function _init() { if (!_isWebhook) { return @@ -130,16 +118,14 @@ export const CiPipelineSourceConfig = ({ regexTippyContent() }, []) - const isRegexOrBranchIcon = _isRegex ? regexIcon : branchIcon - return ( -
+
{loading && showIcons && loading} {!loading && (
{showIcons && ( branch @@ -170,7 +156,7 @@ export const CiPipelineSourceConfig = ({ )} {baseText && ( - + {baseText} )} diff --git a/src/Shared/Components/CICDHistory/DeploymentHistoryConfigDiff/DeploymentHistoryDiffView.tsx b/src/Shared/Components/CICDHistory/DeploymentHistoryConfigDiff/DeploymentHistoryDiffView.tsx index 2a61026bc..cd9c32aa8 100644 --- a/src/Shared/Components/CICDHistory/DeploymentHistoryConfigDiff/DeploymentHistoryDiffView.tsx +++ b/src/Shared/Components/CICDHistory/DeploymentHistoryConfigDiff/DeploymentHistoryDiffView.tsx @@ -18,6 +18,8 @@ import { useParams } from 'react-router-dom' import { useMemo, useState } from 'react' import Tippy from '@tippyjs/react' import { yamlComparatorBySortOrder } from '@Shared/Helpers' +import { DiffViewer } from '@Shared/Components/DiffViewer' +import { renderDiffViewNoDifferenceState } from '@Shared/Components/DeploymentConfigDiff' import { MODES, Toggle, YAMLStringify } from '../../../../Common' import { DeploymentHistoryParamsType } from './types' import { DeploymentHistorySingleValue, DeploymentTemplateHistoryType } from '../types' @@ -85,20 +87,26 @@ const DeploymentHistoryDiffView = ({ }) }, [convertVariables, currentConfiguration, sortOrder, isUnpublished]) - const renderDeploymentDiffViaCodeEditor = () => ( - - ) + const renderDeploymentDiffViaCodeEditor = () => + previousConfigAvailable ? ( + + ) : ( + + ) const handleShowVariablesClick = () => { setConvertVariables(!convertVariables) @@ -176,7 +184,7 @@ const DeploymentHistoryDiffView = ({
{(currentConfiguration?.codeEditorValue?.value || baseTemplateConfiguration?.codeEditorValue?.value) && ( -
+
{ const parsedDraftData = JSON.parse(data?.deploymentDraftData?.configData[0].draftMetadata.data || null) @@ -827,3 +829,21 @@ 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 a512705d2..6904f308e 100644 --- a/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiffMain.tsx +++ b/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiffMain.tsx @@ -5,11 +5,11 @@ import { ReactComponent as ICSortArrowDown } from '@Icons/ic-sort-arrow-down.svg import { ReactComponent as ICSort } from '@Icons/ic-arrow-up-down.svg' import { ReactComponent as ICViewVariableToggle } from '@Icons/ic-view-variable-toggle.svg' import { Progressing } from '@Common/Progressing' -import { CodeEditor } from '@Common/CodeEditor' -import { MODES, SortingOrder } from '@Common/Constants' +import { SortingOrder } from '@Common/Constants' import ErrorScreenManager from '@Common/ErrorScreenManager' import Toggle from '@Common/Toggle/Toggle' import { ComponentSizeType } from '@Shared/constants' +import { DiffViewer } from '@Shared/Components/DiffViewer' import { Button, ButtonStyleType, ButtonVariantType } from '../Button' import { SelectPicker } from '../SelectPicker' @@ -21,6 +21,7 @@ import { DeploymentConfigDiffState, DeploymentConfigDiffAccordionProps, } from './DeploymentConfigDiff.types' +import { renderDiffViewNoDifferenceState } from './DeploymentConfigDiff.utils' export const DeploymentConfigDiffMain = ({ isLoading, @@ -190,23 +191,16 @@ export const DeploymentConfigDiffMain = ({ hideDiffState={hideDiffState} > {singleView ? ( - <> -
-
{primaryHeading}
-
{secondaryHeading}
-
- - + ) : (
{primaryHeading && secondaryHeading && ( diff --git a/src/Shared/Components/DiffViewer/DiffViewer.component.tsx b/src/Shared/Components/DiffViewer/DiffViewer.component.tsx new file mode 100644 index 000000000..a3c19e2c4 --- /dev/null +++ b/src/Shared/Components/DiffViewer/DiffViewer.component.tsx @@ -0,0 +1,59 @@ +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 new file mode 100644 index 000000000..47aa4218c --- /dev/null +++ b/src/Shared/Components/DiffViewer/constants.ts @@ -0,0 +1,104 @@ +// Default variables and style keys + +import { ReactDiffViewerProps } from 'react-diff-viewer-continued' + +export const diffViewerStyles: ReactDiffViewerProps['styles'] = { + variables: { + light: { + diffViewerBackground: 'var(--N0)', + 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(--N50)', + gutterBackgroundDark: 'var(--N50)', + highlightBackground: 'var(--N100)', + highlightGutterBackground: 'var(--N100)', + codeFoldGutterBackground: 'var(--B100)', + codeFoldBackground: 'var(--B50)', + emptyLineBackground: 'var(--N0)', + 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 new file mode 100644 index 000000000..ac4ae4c0b --- /dev/null +++ b/src/Shared/Components/DiffViewer/index.ts @@ -0,0 +1 @@ +export { default as DiffViewer } from './DiffViewer.component' diff --git a/src/Shared/Components/DiffViewer/types.ts b/src/Shared/Components/DiffViewer/types.ts new file mode 100644 index 000000000..e27e4cbbf --- /dev/null +++ b/src/Shared/Components/DiffViewer/types.ts @@ -0,0 +1,12 @@ +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/GenericSectionErrorState/GenericSectionErrorState.component.tsx b/src/Shared/Components/GenericSectionErrorState/GenericSectionErrorState.component.tsx index 6c0c613f4..747669e70 100644 --- a/src/Shared/Components/GenericSectionErrorState/GenericSectionErrorState.component.tsx +++ b/src/Shared/Components/GenericSectionErrorState/GenericSectionErrorState.component.tsx @@ -14,8 +14,10 @@ * limitations under the License. */ +import { ComponentSizeType } from '@Shared/constants' import { ReactComponent as ErrorIcon } from '../../../Assets/Icon/ic-error-exclamation.svg' -import { ReactComponent as InfoIcon } from '../../../Assets/Icon/ic-exclamation.svg' +import { ReactComponent as ICInfoOutline } from '../../../Assets/Icon/ic-info-outline.svg' +import { Button, ButtonVariantType } from '../Button' import { GenericSectionErrorStateProps } from './types' const GenericSectionErrorState = ({ @@ -30,27 +32,28 @@ const GenericSectionErrorState = ({ }: GenericSectionErrorStateProps) => (
{useInfoIcon ? ( - + ) : ( )}

{title}

-
- {subTitle &&

{subTitle}

} - {description &&

{description}

} -
+ {(subTitle || description) && ( +
+ {subTitle &&

{subTitle}

} + {description &&

{description}

} +
+ )}
{reload && ( - + variant={ButtonVariantType.text} + size={ComponentSizeType.small} + dataTestId="generic-section-reload-button" + /> )}
) diff --git a/src/Shared/Components/GitCommitInfoGeneric/GitCommitInfoGeneric.tsx b/src/Shared/Components/GitCommitInfoGeneric/GitCommitInfoGeneric.tsx index 4da369fef..4ed68b767 100644 --- a/src/Shared/Components/GitCommitInfoGeneric/GitCommitInfoGeneric.tsx +++ b/src/Shared/Components/GitCommitInfoGeneric/GitCommitInfoGeneric.tsx @@ -15,23 +15,23 @@ */ /* eslint-disable eqeqeq */ -import { useState } from 'react' import moment from 'moment' import Tippy from '@tippyjs/react' -import ClipboardButton from '@Common/ClipboardButton/ClipboardButton' +import { ClipboardButton } from '@Common/ClipboardButton/ClipboardButton' import { ReactComponent as Circle } from '@Icons/ic-circle.svg' import { ReactComponent as Commit } from '@Icons/ic-commit.svg' -import { ReactComponent as CommitIcon } from '@Icons/ic-code-commit.svg' import { ReactComponent as PersonIcon } from '@Icons/ic-person.svg' import { ReactComponent as CalendarIcon } from '@Icons/ic-calendar.svg' import { ReactComponent as MessageIcon } from '@Icons/ic-message.svg' -import { ReactComponent as BranchIcon } from '@Icons/ic-branch.svg' -import { ReactComponent as BranchMain } from '@Icons/ic-branch-main.svg' +import { ReactComponent as PullRequestIcon } from '@Icons/ic-pull-request.svg' import { ReactComponent as Check } from '@Icons/ic-check-circle.svg' import { ReactComponent as Abort } from '@Icons/ic-abort.svg' -import { SourceTypeMap, createGitCommitUrl } from '@Common/Common.service' +import { SourceTypeMap, createGitCommitUrl, getGitBranchUrl } from '@Common/Common.service' import { stopPropagation } from '@Common/Helper' -import { DATE_TIME_FORMATS } from '@Common/Constants' +import { DATE_TIME_FORMATS, GitProviderType } from '@Common/Constants' +import { ReactComponent as Tag } from '@Icons/ic-tag.svg' +import { getLowerCaseObject, getWebhookDate } from '@Shared/Helpers' +import { ReactComponent as Hash } from '@Icons/ic-hash.svg' import GitMaterialInfoHeader from './GitMaterialInfoHeader' import { MATERIAL_EXCLUDE_TIPPY_TEXT } from '../../constants' import { WEBHOOK_EVENT_ACTION_TYPE } from './constants' @@ -44,141 +44,67 @@ const GitCommitInfoGeneric = ({ selectedCommitInfo, materialUrl, showMaterialInfoHeader, - canTriggerBuild = false, index, isExcluded = false, }: GitCommitInfoGenericProps) => { - const [showSeeMore, setShowSeeMore] = useState(true) - - function _lowerCaseObject(input): any { - const _output = {} - if (!input) { - return _output - } - Object.keys(input).forEach((_key) => { - const _modifiedKey = _key.toLowerCase() - const _value = input[_key] - if (_value && typeof _value === 'object') { - _output[_modifiedKey] = _lowerCaseObject(_value) - } else { - _output[_modifiedKey] = _value - } - }) - return _output - } - - const _lowerCaseCommitInfo = _lowerCaseObject(commitInfo) + const lowerCaseCommitInfo = getLowerCaseObject(commitInfo) const _isWebhook = materialSourceType === SourceTypeMap.WEBHOOK || - (_lowerCaseCommitInfo && _lowerCaseCommitInfo.webhookdata && _lowerCaseCommitInfo.webhookdata.id !== 0) - const _webhookData = _isWebhook ? _lowerCaseCommitInfo.webhookdata : {} + (lowerCaseCommitInfo && lowerCaseCommitInfo.webhookdata && lowerCaseCommitInfo.webhookdata.id !== 0) + const _webhookData = _isWebhook ? lowerCaseCommitInfo.webhookdata : {} // eslint-disable-next-line no-nested-ternary const _commitUrl = _isWebhook ? null - : _lowerCaseCommitInfo.commiturl - ? _lowerCaseCommitInfo.commiturl - : createGitCommitUrl(materialUrl, _lowerCaseCommitInfo.commit) + : lowerCaseCommitInfo.commiturl + ? lowerCaseCommitInfo.commiturl + : createGitCommitUrl(materialUrl, lowerCaseCommitInfo.commit) - function renderBasicGitCommitInfoForWebhook() { - let _date - if (_webhookData.data.date) { - const _moment = moment(_webhookData.data.date, 'YYYY-MM-DDTHH:mm:ssZ') - _date = _moment.isValid() ? _moment.format(DATE_TIME_FORMATS.TWELVE_HOURS_FORMAT) : _webhookData.data.date - } + const renderBranchName = (branchName: string) => + branchName ? ( + + {branchName} + + ) : null + + function renderBasicGitCommitInfoForWebhook(isPRWebhook?: boolean) { + const _date = getWebhookDate(materialSourceType, commitInfo) return ( - <> +
+ {isPRWebhook ? ( +
+ +
+ Merge commit into  + {renderBranchName(_webhookData.data['target branch name'])} +  from  + {renderBranchName(_webhookData.data['source branch name'])} +
+
+ ) : null} {_webhookData.data.author ? ( -
+
{_webhookData.data.author}
) : null} {_date ? ( -
+
) : null} {_webhookData.data.message ? ( -
+
{_webhookData.data.message}
) : null} - - ) - } - - function renderMoreDataForWebhook(_moreData) { - return !showSeeMore ? ( -
-
- {Object.keys(_moreData).map((_key, idx) => { - let classes - if (idx % 2 == 0) { - classes = 'bcn-1' - } - return ( -
-
{_key}
-
{_moreData[_key]}
-
- ) - })} -
- ) : null - } - - function renderSeeMoreButtonForWebhook() { - return ( - - ) - } - - function handleMoreDataForWebhook() { - const _moreData = {} - if (_webhookData.eventactiontype === WEBHOOK_EVENT_ACTION_TYPE.MERGED) { - Object.keys(_webhookData.data).forEach((_key) => { - if ( - _key != 'author' && - _key != 'date' && - _key != 'git url' && - _key != 'source branch name' && - _key != 'source checkout' && - _key != 'target branch name' && - _key != 'target checkout' && - _key != 'title' - ) { - _moreData[_key] = _webhookData.data[_key] - } - }) - } else if (_webhookData.eventactiontype === WEBHOOK_EVENT_ACTION_TYPE.NON_MERGED) { - Object.keys(_webhookData.data).forEach((_key) => { - if (_key !== 'author' && _key !== 'date' && _key !== 'target checkout') { - _moreData[_key] = _webhookData.data[_key] - } - }) - } - - const _hasMoreData = Object.keys(_moreData).length > 0 - - return ( - <> - {_hasMoreData && renderMoreDataForWebhook(_moreData)} - {_hasMoreData && renderSeeMoreButtonForWebhook()} - ) } @@ -207,15 +133,89 @@ const GitCommitInfoGeneric = ({ } const renderCommitStatus = () => { - if (_lowerCaseCommitInfo.isselected) { + if (lowerCaseCommitInfo.isselected) { return } return matSelectionText() } + const renderWebhookTitle = () => + _webhookData.data.title ? {_webhookData.data.title} : null + + const renderPullRequestId = (pullRequestUrl: string) => { + const pullRequestId = pullRequestUrl?.split('/').pop() + + return ( + + ) + } + + const renderTagCreationId = (tagRequestUrl: string) => ( +
+ + {tagRequestUrl} +
+ ) + + const getCheckUncheckIcon = () => { + if (selectedCommitInfo) { + if (lowerCaseCommitInfo.isselected) { + return + } + return + } + return null + } + + const renderPRInfoCard = () => ( +
+
+ {renderPullRequestId(_webhookData.data['git url'])} + {getCheckUncheckIcon()} +
+ {renderWebhookTitle()} + {renderBasicGitCommitInfoForWebhook(true)} +
+ ) + + const renderTagInfoCard = () => ( + <> +
+ {renderTagCreationId(_webhookData.data['target checkout'])} + {getCheckUncheckIcon()} +
+ {renderBasicGitCommitInfoForWebhook()} + + ) + + const renderWebhookGitInfoCard = () => { + if (!_isWebhook) return null + + const isMerged = _webhookData.eventactiontype === WEBHOOK_EVENT_ACTION_TYPE.MERGED + + if (materialUrl.includes(GitProviderType.GITLAB)) { + // TODO: This is a temporary fix for the issue where the eventActionType data incorrect + return isMerged ? renderTagInfoCard() : renderPRInfoCard() + } + return isMerged ? renderPRInfoCard() : renderTagInfoCard() + } + return (
- {showMaterialInfoHeader && (_isWebhook || _lowerCaseCommitInfo.commit) && ( + {showMaterialInfoHeader && (_isWebhook || lowerCaseCommitInfo.commit) && ( {!_isWebhook && ( <> - {_lowerCaseCommitInfo.commit && ( + {lowerCaseCommitInfo.commit && (
{_commitUrl ? (
@@ -243,163 +243,49 @@ const GitCommitInfoGeneric = ({ data-testid={`git-commit-credential${index}`} > - {_lowerCaseCommitInfo.commit.slice(0, 7)} + {lowerCaseCommitInfo.commit.slice(0, 7)}
- +
) : null} {selectedCommitInfo ? renderCommitStatus() : null}
)} - {_lowerCaseCommitInfo.message ? ( + {lowerCaseCommitInfo.message ? (
- {_lowerCaseCommitInfo.message} + {lowerCaseCommitInfo.message}
) : null}
- {_lowerCaseCommitInfo.author ? ( + {lowerCaseCommitInfo.author ? (
- {_lowerCaseCommitInfo.author} + {lowerCaseCommitInfo.author}
) : null} - {_lowerCaseCommitInfo.date ? ( + {lowerCaseCommitInfo.date ? (
- {moment(_lowerCaseCommitInfo.date).format( - DATE_TIME_FORMATS.TWELVE_HOURS_FORMAT, - )} + {moment(lowerCaseCommitInfo.date).format(DATE_TIME_FORMATS.TWELVE_HOURS_FORMAT)}
) : null}
)} - - {_isWebhook && _webhookData.eventactiontype === WEBHOOK_EVENT_ACTION_TYPE.MERGED && ( - <> -
-
- {_webhookData.data.title ? ( -
{_webhookData.data.title}
- ) : null} - {_webhookData.data['git url'] ? ( - - View git url - - ) : null} -
- {selectedCommitInfo ? ( -
- {_lowerCaseCommitInfo.isselected ? ( - - ) : ( - - )} -
- ) : null} -
- -
-
- -
-
- {_webhookData.data['source branch name'] ? ( -
- {' '} - {_webhookData.data['source branch name']} -
- ) : null} - {_webhookData.data['source checkout'] ? ( - - ) : null} -
-
-
- {_webhookData.data['target branch name'] ? ( - <> - {' '} - {_webhookData.data['target branch name']}{' '} - - ) : null} -
-
- {canTriggerBuild && ( -
- - HEAD -
- )} - {!canTriggerBuild && ( - - - {_webhookData.data['target checkout']} - - )} -
-
-
-
-
- {renderBasicGitCommitInfoForWebhook()} - {handleMoreDataForWebhook()} - - )} - {_isWebhook && _webhookData.eventactiontype === WEBHOOK_EVENT_ACTION_TYPE.NON_MERGED && ( - <> -
-
{_webhookData.data['target checkout']}
- {selectedCommitInfo ? ( -
- {_lowerCaseCommitInfo.isselected ? : 'Select'} -
- ) : null} -
- {renderBasicGitCommitInfoForWebhook()} - {handleMoreDataForWebhook()} - - )} + {renderWebhookGitInfoCard()}
) diff --git a/src/Shared/Components/KeyValueTable/KeyValueTable.scss b/src/Shared/Components/KeyValueTable/KeyValueTable.scss index efe43bb85..2aaba811c 100644 --- a/src/Shared/Components/KeyValueTable/KeyValueTable.scss +++ b/src/Shared/Components/KeyValueTable/KeyValueTable.scss @@ -19,11 +19,11 @@ gap: 1px; &.two-columns-top-row { - grid-template-columns: 20% 1fr; + grid-template-columns: 30% 1fr; } &.two-columns { - grid-template-columns: 20% 1fr; + grid-template-columns: 30% 1fr; .key-value-table__row:last-of-type { .key-value-table__cell:first-child, @@ -41,7 +41,7 @@ } &.three-columns { - grid-template-columns: 20% 1fr 32px; + grid-template-columns: 30% 1fr 32px; .key-value-table__row:last-of-type { .key-value-table__cell:first-child, diff --git a/src/Shared/Components/MaterialHistory/MaterialHistory.component.tsx b/src/Shared/Components/MaterialHistory/MaterialHistory.component.tsx index a1b534ee9..3d7c84b2c 100644 --- a/src/Shared/Components/MaterialHistory/MaterialHistory.component.tsx +++ b/src/Shared/Components/MaterialHistory/MaterialHistory.component.tsx @@ -14,6 +14,9 @@ * limitations under the License. */ +import { getWebhookDate } from '@Shared/Helpers' +import { MaterialHistoryType } from '@Shared/Services' +import { useMemo } from 'react' import { SourceTypeMap } from '../../../Common' import { GitCommitInfoGeneric } from '../GitCommitInfoGeneric' import { MaterialHistoryProps } from './types' @@ -32,34 +35,35 @@ const MaterialHistory = ({ } } - const getMaterialHistoryMapWithTime = () => { - const historyTimeMap = {} - - material.history?.forEach((history) => { - const newDate = history.date.substring(0, 16) - - if (!historyTimeMap[newDate]) { - historyTimeMap[newDate] = [] - } - historyTimeMap[newDate].push(history) - }) + const materialHistoryMapWithTime = useMemo( + () => + material.history.reduce>((acc, historyElem: MaterialHistoryType) => { + const isWebhook = material.type === SourceTypeMap.WEBHOOK + const newDate = isWebhook + ? getWebhookDate(material.type, historyElem).substring(0, 16) + : historyElem.date.substring(0, 16) + if (!acc[newDate]) { + acc[newDate] = [] + } + acc[newDate].push(historyElem) + return acc + }, {}), + [material.history, material.type], + ) - return historyTimeMap - } // Retrieve the history map - const materialHistoryMapWithTime = getMaterialHistoryMapWithTime() // Retrieve the keys of the history map const dateKeys = Object.keys(materialHistoryMapWithTime) return ( // added for consistent typing // eslint-disable-next-line react/jsx-no-useless-fragment - <> +
{dateKeys.map((date) => { const historyList = materialHistoryMapWithTime[date] return ( <> - {!isCommitInfoModal && material.type !== SourceTypeMap.WEBHOOK && ( + {!isCommitInfoModal && (
{date}
@@ -87,7 +91,6 @@ const MaterialHistory = ({ materialSourceType={material.type} selectedCommitInfo={selectCommit} materialSourceValue={material.value} - canTriggerBuild={!history.excluded} isExcluded={history.excluded} />
@@ -96,7 +99,7 @@ const MaterialHistory = ({ ) })} - +
) } export default MaterialHistory diff --git a/src/Shared/Components/Plugin/service.tsx b/src/Shared/Components/Plugin/service.tsx index 1dc8ae23f..4ce04b761 100644 --- a/src/Shared/Components/Plugin/service.tsx +++ b/src/Shared/Components/Plugin/service.tsx @@ -14,7 +14,15 @@ * limitations under the License. */ -import { get, getIsRequestAborted, getUrlWithSearchParams, ResponseType, ROUTES, showError } from '../../../Common' +import { + get, + getIsRequestAborted, + getUrlWithSearchParams, + post, + ResponseType, + ROUTES, + showError, +} from '../../../Common' import { stringComparatorBySortOrder } from '../../Helpers' import { GetParentPluginListPayloadType, @@ -41,13 +49,14 @@ export const getPluginsDetail = async ({ try { const payload: PluginDetailPayloadType = { appId, - parentPluginId: parentPluginIds, - pluginId: pluginIds, - parentPluginIdentifier: parentPluginIdentifiers ? `${parentPluginIdentifiers}` : null, + parentPluginIds, + pluginIds, + parentPluginIdentifiers, } - const { result } = await get( - getUrlWithSearchParams(ROUTES.PLUGIN_GLOBAL_LIST_DETAIL_V2, payload), + const { result } = await post( + ROUTES.PLUGIN_GLOBAL_LIST_DETAIL_V2, + payload, { signal }, ) diff --git a/src/Shared/Components/Plugin/types.ts b/src/Shared/Components/Plugin/types.ts index 972c2f8d2..cea743f08 100644 --- a/src/Shared/Components/Plugin/types.ts +++ b/src/Shared/Components/Plugin/types.ts @@ -94,11 +94,11 @@ export interface PluginDetailServiceParamsType { signal?: AbortSignal } -export interface PluginDetailPayloadType extends Pick { - pluginId?: PluginDetailServiceParamsType['pluginIds'] - parentPluginId?: PluginDetailServiceParamsType['parentPluginIds'] - parentPluginIdentifier?: PluginDetailServiceParamsType['parentPluginIdentifiers'][number] -} +export interface PluginDetailPayloadType + extends Pick< + PluginDetailServiceParamsType, + 'appId' | 'parentPluginIds' | 'pluginIds' | 'parentPluginIdentifiers' + > {} export interface PluginListFiltersType extends Pick, 'searchKey'> { selectedTags: string[] diff --git a/src/Shared/Components/index.ts b/src/Shared/Components/index.ts index 0cf6464b7..aa3ba6883 100644 --- a/src/Shared/Components/index.ts +++ b/src/Shared/Components/index.ts @@ -58,3 +58,4 @@ export * from './Security' export * from './Button' export * from './InvalidYAMLTippy' export * from './EnterpriseTag' +export * from './DiffViewer' diff --git a/src/Shared/Helpers.tsx b/src/Shared/Helpers.tsx index bb4779527..986d4d74c 100644 --- a/src/Shared/Helpers.tsx +++ b/src/Shared/Helpers.tsx @@ -20,6 +20,7 @@ import Tippy from '@tippyjs/react' import { Pair } from 'yaml' import moment from 'moment' import { StrictRJSFSchema } from '@rjsf/utils' +import { MaterialHistoryType } from '@Shared/Services/app.types' import { handleUTCTime, ManualApprovalType, @@ -31,6 +32,8 @@ import { PATTERNS, ZERO_TIME_STRING, noop, + SourceTypeMap, + DATE_TIME_FORMATS, } from '../Common' import { AggregationKeys, @@ -880,3 +883,39 @@ export const getNullValueFromType = (type: StrictRJSFSchema['type']) => { return null } } + +/* + * @description - Function to get the lower case object + * @param input - The input object + * @returns Record + */ +export const getLowerCaseObject = (input): Record => { + if (!input || typeof input !== 'object') { + return input + } + return Object.keys(input).reduce((acc, key) => { + const modifiedKey = key.toLowerCase() + const value = input[key] + if (value && typeof value === 'object') { + acc[modifiedKey] = getLowerCaseObject(value) + } else { + acc[modifiedKey] = value + } + return acc + }, {}) +} + +/** + * @description - Function to get the webhook date + * @param materialSourceType - The type of material source (e.g., WEBHOOK) + * @param history - The history object containing commit information + * @returns - Formatted webhook date if available, otherwise an empty string + */ +export const getWebhookDate = (materialSourceType: string, history: MaterialHistoryType): string => { + const lowerCaseCommitInfo = getLowerCaseObject(history) + const isWebhook = materialSourceType === SourceTypeMap.WEBHOOK || lowerCaseCommitInfo?.webhookdata?.id !== 0 + const webhookData = isWebhook ? lowerCaseCommitInfo.webhookdata : {} + + const _moment = moment(webhookData.data.date, 'YYYY-MM-DDTHH:mm:ssZ') + return _moment.isValid() ? _moment.format(DATE_TIME_FORMATS.TWELVE_HOURS_FORMAT) : webhookData.data.date +} diff --git a/src/Shared/Services/app.types.ts b/src/Shared/Services/app.types.ts index d2927d353..85c358d0b 100644 --- a/src/Shared/Services/app.types.ts +++ b/src/Shared/Services/app.types.ts @@ -32,7 +32,7 @@ interface MaterialHistoryDTO { WebhookData: WebhookDataType } -interface MaterialHistoryType { +export interface MaterialHistoryType { commitURL: string commit: MaterialHistoryDTO['Commit'] author: MaterialHistoryDTO['Author'] @@ -85,6 +85,7 @@ export interface CIMaterialType gitURL: CIMaterialDTO['url'] history: MaterialHistoryType[] isSelected: boolean + gitMaterialUrl?: string } interface ImageCommentDTO { diff --git a/src/Shared/types.ts b/src/Shared/types.ts index b6133017c..7a15c7ef0 100644 --- a/src/Shared/types.ts +++ b/src/Shared/types.ts @@ -748,23 +748,68 @@ export interface CustomRoleAndMeta { } interface CommonTabArgsType { + /** + * Name for the tab. + * + * Note: Used for the title + */ name: string kind?: string + /** + * URL for the tab + */ url: string + /** + * If true, the tab is selected + */ isSelected: boolean + /** + * Title for the tab + */ title?: string isDeleted?: boolean - position: number + /** + * Type for the tab + * + * Note: Fixed tabs are always places before dynamic tabs + */ + type: 'fixed' | 'dynamic' + /** + * Path of the icon for the tab + * + * @default '' + */ iconPath?: string + /** + * Dynamic title for the tab + * + * @default '' + */ dynamicTitle?: string + /** + * Whether to show the tab name when selected + * + * @default false + */ showNameOnSelect?: boolean /** + * Would remove the title/name from tab heading, but that does not mean name is not required, since it is used in other calculations * @default false */ hideName?: boolean + /** + * Indicates if showNameOnSelect tabs have been selected once + * + * @default false + */ isAlive?: boolean lastSyncMoment?: Dayjs componentKey?: string + /** + * Custom tippy config for the tab + * + * This overrides the tippy being computed from tab title + */ tippyConfig?: { title: string descriptions: { @@ -772,12 +817,39 @@ interface CommonTabArgsType { value: string }[] } -} - -export interface InitTabType extends CommonTabArgsType { - idPrefix: string -} + /** + * If true, the fixed tab remains mounted on initial load of the component + * + * Note: Not for dynamic tabs atm + * + * @default false + */ + shouldRemainMounted?: boolean +} + +export type InitTabType = Omit & + ( + | { + type: 'fixed' + /** + * Unique identifier for the fixed tab + * + * Note: Shouldn't contain '-' + */ + id: string + idPrefix?: never + } + | { + type: 'dynamic' + id?: never + idPrefix: string + } + ) export interface DynamicTabType extends CommonTabArgsType { id: string + /** + * Id of the last active tab before switching to current tab + */ + lastActiveTabId: string | null }