diff --git a/package-lock.json b/package-lock.json index 35f2bb79a..823038901 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.19.4", + "version": "1.20.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.19.4", + "version": "1.20.0", "hasInstallScript": true, "license": "ISC", "dependencies": { diff --git a/package.json b/package.json index 14afc9e7d..dfb1520f8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.19.4", + "version": "1.20.0", "description": "Supporting common component library", "type": "module", "main": "dist/index.js", diff --git a/src/Assets/IconV2/ic-aws-codecommit.svg b/src/Assets/IconV2/ic-aws-codecommit.svg new file mode 100644 index 000000000..716c885a0 --- /dev/null +++ b/src/Assets/IconV2/ic-aws-codecommit.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/Assets/IconV2/ic-diff-added.svg b/src/Assets/IconV2/ic-diff-added.svg new file mode 100644 index 000000000..9a19936ec --- /dev/null +++ b/src/Assets/IconV2/ic-diff-added.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Assets/IconV2/ic-diff-deleted.svg b/src/Assets/IconV2/ic-diff-deleted.svg new file mode 100644 index 000000000..11b3b4bf2 --- /dev/null +++ b/src/Assets/IconV2/ic-diff-deleted.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Assets/IconV2/ic-diff-updated.svg b/src/Assets/IconV2/ic-diff-updated.svg new file mode 100644 index 000000000..0a0176c6a --- /dev/null +++ b/src/Assets/IconV2/ic-diff-updated.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/Assets/IconV2/ic-group-filter-applied.svg b/src/Assets/IconV2/ic-group-filter-applied.svg new file mode 100644 index 000000000..6d04f2f0d --- /dev/null +++ b/src/Assets/IconV2/ic-group-filter-applied.svg @@ -0,0 +1,20 @@ + + + + + + diff --git a/src/Assets/IconV2/ic-group-filter.svg b/src/Assets/IconV2/ic-group-filter.svg new file mode 100644 index 000000000..a5e01038e --- /dev/null +++ b/src/Assets/IconV2/ic-group-filter.svg @@ -0,0 +1,19 @@ + + + + + diff --git a/src/Assets/IconV2/ic-input.svg b/src/Assets/IconV2/ic-input.svg new file mode 100644 index 000000000..a4322d171 --- /dev/null +++ b/src/Assets/IconV2/ic-input.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Assets/IconV2/ic-locked.svg b/src/Assets/IconV2/ic-locked.svg new file mode 100644 index 000000000..091002ba2 --- /dev/null +++ b/src/Assets/IconV2/ic-locked.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Assets/IconV2/ic-storage.svg b/src/Assets/IconV2/ic-storage.svg new file mode 100644 index 000000000..3e2383825 --- /dev/null +++ b/src/Assets/IconV2/ic-storage.svg @@ -0,0 +1,21 @@ + + + + + + + diff --git a/src/Assets/IconV2/ic-tag.svg b/src/Assets/IconV2/ic-tag.svg new file mode 100644 index 000000000..914ffecf8 --- /dev/null +++ b/src/Assets/IconV2/ic-tag.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Assets/IconV2/ic-visibility-off.svg b/src/Assets/IconV2/ic-visibility-off.svg new file mode 100644 index 000000000..3f75690db --- /dev/null +++ b/src/Assets/IconV2/ic-visibility-off.svg @@ -0,0 +1,19 @@ + + + + + diff --git a/src/Assets/IconV2/ic-visibility-on.svg b/src/Assets/IconV2/ic-visibility-on.svg new file mode 100644 index 000000000..14ca7e888 --- /dev/null +++ b/src/Assets/IconV2/ic-visibility-on.svg @@ -0,0 +1,20 @@ + + + + + + diff --git a/src/Assets/IconV2/ic-warning-fill.svg b/src/Assets/IconV2/ic-warning-fill.svg new file mode 100644 index 000000000..e47a01ad7 --- /dev/null +++ b/src/Assets/IconV2/ic-warning-fill.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/Assets/Illustration/img-mechanical-operation.svg b/src/Assets/Illustration/img-mechanical-operation.svg new file mode 100644 index 000000000..df7abdf64 --- /dev/null +++ b/src/Assets/Illustration/img-mechanical-operation.svg @@ -0,0 +1,17 @@ + + + diff --git a/src/Common/CIPipeline.Types.ts b/src/Common/CIPipeline.Types.ts index c0bcb8aa0..7fb61ff02 100644 --- a/src/Common/CIPipeline.Types.ts +++ b/src/Common/CIPipeline.Types.ts @@ -27,6 +27,7 @@ export interface MaterialType { gitProviderId: number regex?: string isRegex: boolean + url: string } export interface Githost { diff --git a/src/Common/Common.service.ts b/src/Common/Common.service.ts index c99e0a6ff..73c2efe1d 100644 --- a/src/Common/Common.service.ts +++ b/src/Common/Common.service.ts @@ -17,6 +17,7 @@ import { MutableRefObject } from 'react' import moment from 'moment' import { + getIsApprovalPolicyConfigured, sanitizeApprovalConfigData, sanitizeTargetPlatforms, sanitizeUserApprovalList, @@ -49,6 +50,7 @@ import { EnvAppsMetaDTO, GetAppsInfoForEnvProps, AppMeta, + ApprovalRuntimeStateType, } from './Types' import { ApiResourceType, STAGE_MAP } from '../Pages' import { RefVariableType, VariableTypeFormat } from './CIPipeline.Types' @@ -116,6 +118,8 @@ const cdMaterialListModal = ({ artifactId, artifactStatus, disableDefaultSelection, + isExceptionUser, + isApprovalConfigured, }: CDMaterialListModalServiceUtilProps) => { if (!artifacts || !artifacts.length) return [] @@ -131,8 +135,15 @@ const cdMaterialListModal = ({ artifactStatusValue = artifactStatus } + const isConsumedNonApprovedImage = + !isExceptionUser && isApprovalConfigured && + (!material.userApprovalMetadata || + material.userApprovalMetadata.approvalRuntimeState !== ApprovalRuntimeStateType.approved) + const selectImage = - !isImageMarked && markFirstSelected && filterState === FilterStates.ALLOWED ? !material.vulnerable : false + !isImageMarked && markFirstSelected && filterState === FilterStates.ALLOWED && !isConsumedNonApprovedImage + ? !material.vulnerable + : false if (selectImage) { isImageMarked = true } @@ -328,17 +339,27 @@ export const processCDMaterialServiceResponse = ( } } + const approvalInfo = processCDMaterialsApprovalInfo( + stage === DeploymentNodeType.CD || stage === DeploymentNodeType.APPROVAL, + cdMaterialsResult, + ) + + const isApprovalConfigured = getIsApprovalPolicyConfigured( + approvalInfo?.deploymentApprovalInfo?.approvalConfigData, + ) + + const isExceptionUser = approvalInfo?.deploymentApprovalInfo?.approvalConfigData?.isExceptionUser ?? false + const materials = cdMaterialListModal({ artifacts: cdMaterialsResult.ci_artifacts, offset: offset ?? 0, artifactId: cdMaterialsResult.latest_wf_artifact_id, artifactStatus: cdMaterialsResult.latest_wf_artifact_status, disableDefaultSelection, + isApprovalConfigured, + isExceptionUser, }) - const approvalInfo = processCDMaterialsApprovalInfo( - stage === DeploymentNodeType.CD || stage === DeploymentNodeType.APPROVAL, - cdMaterialsResult, - ) + const metaInfo = processCDMaterialsMetaInfo(cdMaterialsResult) const imagePromotionInfo = processImagePromotionInfo(cdMaterialsResult) @@ -439,10 +460,12 @@ export function fetchChartTemplateVersions() { export const getDefaultConfig = (): Promise => get(`${ROUTES.NOTIFIER}/channel/config`) -export function getEnvironmentListMinPublic(includeAllowedDeploymentTypes?: boolean) { - return get( - `${ROUTES.ENVIRONMENT_LIST_MIN}?auth=false${includeAllowedDeploymentTypes ? '&showDeploymentOptions=true' : ''}`, - ) +export function getEnvironmentListMinPublic(includeAllowedDeploymentTypes?: boolean, options?: APIOptions) { + const url = getUrlWithSearchParams(ROUTES.ENVIRONMENT_LIST_MIN, { + auth: false, + ...(includeAllowedDeploymentTypes ? { showDeploymentOptions: true } : {}), + }) + return get(url, options) } export function getClusterListMin() { @@ -453,7 +476,10 @@ export function getClusterListMin() { export const getResourceGroupListRaw = (clusterId: string): Promise> => get(`${ROUTES.API_RESOURCE}/${ROUTES.GVK}/${clusterId}`) -export function getNamespaceListMin(clusterIdsCsv: string, abortControllerRef?: APIOptions['abortControllerRef']): Promise { +export function getNamespaceListMin( + clusterIdsCsv: string, + abortControllerRef?: APIOptions['abortControllerRef'], +): Promise { const URL = `${ROUTES.NAMESPACE}/autocomplete?ids=${clusterIdsCsv}` return get(URL, { abortControllerRef }) } diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index deefd5a8e..22219c2e0 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { SelectPickerOptionType } from '@Shared/Components' + export const FALLBACK_REQUEST_TIMEOUT = 60000 export const Host = window?.__ORCHESTRATOR_ROOT__ ?? '/orchestrator' @@ -467,3 +469,13 @@ export enum SSOProvider { oidc = 'oidc', openshift = 'openshift', } + +export const BULK_DEPLOY_LATEST_IMAGE_TAG: SelectPickerOptionType = { + value: 'latest', + label: 'latest', +} + +export const BULK_DEPLOY_ACTIVE_IMAGE_TAG: SelectPickerOptionType = { + value: 'active', + label: 'active', +} diff --git a/src/Common/ErrorPage.tsx b/src/Common/ErrorPage.tsx index 23cb6d01c..30589eb9c 100644 --- a/src/Common/ErrorPage.tsx +++ b/src/Common/ErrorPage.tsx @@ -21,9 +21,13 @@ import { ERROR_EMPTY_SCREEN, ERROR_STATUS_CODE, ROUTES } from './Constants' import { noop, refresh, reportIssue } from './Helper' import { ErrorPageType } from './Types' -const ErrorPage = ({ code, image, title, subTitle, imageType, redirectURL, reload }: ErrorPageType) => { +const ErrorPage = ({ code, image, title, subTitle, imageType, redirectURL, reload, on404Redirect }: ErrorPageType) => { const { push } = useHistory() const redirectToHome = () => { + if (on404Redirect) { + on404Redirect() + return + } push(redirectURL || `/${ROUTES.APP_LIST}`) } diff --git a/src/Common/ErrorScreenManager.tsx b/src/Common/ErrorScreenManager.tsx index e434eeecd..c03399d7f 100644 --- a/src/Common/ErrorScreenManager.tsx +++ b/src/Common/ErrorScreenManager.tsx @@ -28,6 +28,7 @@ const ErrorScreenManager = ({ subtitle, reloadClass, redirectURL, + on404Redirect, imageType = ImageType.Large, }: ErrorScreenManagerProps) => { const getMessage = () => { @@ -72,6 +73,7 @@ const ErrorScreenManager = ({ image={notFound} imageType={imageType} redirectURL={redirectURL} + on404Redirect={on404Redirect} /> ) case ERROR_STATUS_CODE.INTERNAL_SERVER_ERROR: diff --git a/src/Common/Helper.tsx b/src/Common/Helper.tsx index d060a1042..7406c4d19 100644 --- a/src/Common/Helper.tsx +++ b/src/Common/Helper.tsx @@ -123,11 +123,11 @@ export function sortCallback(key: string, a: any, b: any, isCaseSensitive?: bool } export const stopPropagation = (event): void => { - event.stopPropagation() + event?.stopPropagation() } export const preventDefault = (event: SyntheticEvent): void => { - event.preventDefault() + event?.preventDefault() } export function useThrottledEffect(callback, delay, deps = []) { @@ -1133,3 +1133,5 @@ export const findRight = (arr: T[], predicate: (item: T) => boolean): T | nu return null } + + diff --git a/src/Common/Hooks/UseRegisterShortcut/index.ts b/src/Common/Hooks/UseRegisterShortcut/index.ts index b859660cb..49f665a51 100644 --- a/src/Common/Hooks/UseRegisterShortcut/index.ts +++ b/src/Common/Hooks/UseRegisterShortcut/index.ts @@ -14,5 +14,6 @@ * limitations under the License. */ +export type { SupportedKeyboardKeysType } from './types' export { default as useRegisterShortcut } from './UseRegisterShortcut' export { default as UseRegisterShortcutProvider } from './UseRegisterShortcutProvider' diff --git a/src/Common/ImageTags.tsx b/src/Common/ImageTags.tsx index a34de4f9f..372d502f8 100644 --- a/src/Common/ImageTags.tsx +++ b/src/Common/ImageTags.tsx @@ -30,6 +30,7 @@ import { showError, stopPropagation } from './Helper' import { setImageTags } from './Common.service' import { Progressing } from './Progressing' import { InfoIconTippy, Textarea, ToastManager, ToastVariantType } from '../Shared' +import { BULK_DEPLOY_LATEST_IMAGE_TAG } from './Constants' export const ImageTagsContainer = ({ // Setting it to zero in case of external pipeline @@ -122,7 +123,13 @@ export const ImageTagsContainer = ({ for (let i = 0; i < displayedTags?.length; i++) { if (displayedTags[i].tagName.toLowerCase() === lowercaseValue) isTagExistsInDisplayedTags = true } - if (isTagExistsInExistingTags || isTagExistsInDisplayedTags || lowercaseValue === 'latest') { + + if (lowercaseValue === BULK_DEPLOY_LATEST_IMAGE_TAG.value || lowercaseValue === BULK_DEPLOY_LATEST_IMAGE_TAG.value) { + setTagErrorMessage('Label name cannot be "latest" or "active"') + return false + } + + if (isTagExistsInExistingTags || isTagExistsInDisplayedTags) { setTagErrorMessage('This label is already being used in this application') return false } diff --git a/src/Common/InteractiveCellText/InteractiveCellText.tsx b/src/Common/InteractiveCellText/InteractiveCellText.tsx new file mode 100644 index 000000000..6e01e0c64 --- /dev/null +++ b/src/Common/InteractiveCellText/InteractiveCellText.tsx @@ -0,0 +1,79 @@ +/* + * 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 { Tooltip } from '@Common/Tooltip' + +import { InteractiveCellTextProps } from './types' +/** + * A reusable component for rendering text within a tooltip. The text can be interactive (clickable) or static. + * + * @param {Object} props - The props for the component. + * @param {string} props.text - The text to display inside the component. Defaults to `'-'` if not provided. + * @param {function} [props.onClickHandler] - Optional click handler function. If provided, the text will be rendered as a button. + * @param {string} [props.dataTestId] - Optional test ID for the component, useful for testing purposes. + * @param {string} [props.rootClassName] - Additional CSS class names to apply to the root element. + * @param {boolean} [props.interactive=false] - Whether the tooltip content is interactive. + * @param {number} [props.fontSize=13] - Font size for the text. Defaults to `13`. + * @param {React.ReactNode} [props.tippyContent=null] - Custom content for the tooltip. If not provided, `text` will be used. + * @returns {JSX.Element} The rendered `InteractiveCellText` component. + * + * @example + * // Example usage: + * alert('Clicked!')} + * dataTestId="interactive-cell" + * rootClassName="custom-class" + * interactive={true} + * fontSize={14} + * tippyContent="Tooltip content" + * /> + */ + +export const InteractiveCellText = ({ + text, + onClickHandler, + dataTestId, + rootClassName, + interactive = false, + fontSize = 13, + tippyContent = null, +}: InteractiveCellTextProps) => ( + + {typeof onClickHandler === 'function' ? ( + + ) : ( +

+ {text || '-'} +

+ )} +
+) diff --git a/src/Common/InteractiveCellText/index.ts b/src/Common/InteractiveCellText/index.ts new file mode 100644 index 000000000..1e2222661 --- /dev/null +++ b/src/Common/InteractiveCellText/index.ts @@ -0,0 +1,2 @@ +export * from './InteractiveCellText' +export * from './types' diff --git a/src/Common/InteractiveCellText/types.ts b/src/Common/InteractiveCellText/types.ts new file mode 100644 index 000000000..ba4f0a66c --- /dev/null +++ b/src/Common/InteractiveCellText/types.ts @@ -0,0 +1,27 @@ +/* + * 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 { TippyCustomizedProps } from '@Common/Types' + +export interface InteractiveCellTextProps { + text: string + onClickHandler?: () => void + dataTestId?: string + rootClassName?: string + interactive?: boolean + fontSize?: number + tippyContent?: TippyCustomizedProps['additionalContent'] +} diff --git a/src/Common/PopupMenu.tsx b/src/Common/PopupMenu.tsx index 12d4e3107..57a41a8d6 100644 --- a/src/Common/PopupMenu.tsx +++ b/src/Common/PopupMenu.tsx @@ -189,7 +189,7 @@ const Body = ({ style = {}, autoWidth = false, preventWheelDisable = false, - noBackDrop, + noBackDrop = true, }: PopupMenuBodyType) => { const { handleClose, popupPosition, opacity, callbackRef, buttonWidth } = usePopupContext() return popupPosition ? ( diff --git a/src/Common/Types.ts b/src/Common/Types.ts index b993729a7..17f355e99 100644 --- a/src/Common/Types.ts +++ b/src/Common/Types.ts @@ -206,21 +206,32 @@ export interface ErrorPageType Pick { code: number redirectURL?: string + on404Redirect?: () => void reload?: () => void } -export interface ErrorScreenManagerProps { +export type ErrorScreenManagerProps = { code?: number imageType?: ImageType reload?: (...args) => any subtitle?: React.ReactChild reloadClass?: string - /** - * Would be used to redirect URL in case of 404 - * @default - APP_LIST - */ - redirectURL?: string -} +} & ( + | { + /** + * Would be used to redirect URL in case of 404 + * @default - APP_LIST + */ + redirectURL?: string + on404Redirect?: never + } | { + redirectURL?: never + on404Redirect: () => void + } | { + redirectURL?: never + on404Redirect?: never + } +) export interface ErrorScreenNotAuthorizedProps { subtitle?: React.ReactChild @@ -482,6 +493,8 @@ export interface CDMaterialListModalServiceUtilProps { artifactId?: number artifactStatus?: string disableDefaultSelection?: boolean + isExceptionUser: boolean + isApprovalConfigured: boolean } export interface CDMaterialType { @@ -1089,4 +1102,4 @@ export interface ClusterEnvironmentCategoryDTO { description?: string } -export interface ClusterEnvironmentCategoryType extends ClusterEnvironmentCategoryDTO {} +export interface ClusterEnvironmentCategoryType extends ClusterEnvironmentCategoryDTO {} \ No newline at end of file diff --git a/src/Common/index.ts b/src/Common/index.ts index b0e642893..44b595cc9 100644 --- a/src/Common/index.ts +++ b/src/Common/index.ts @@ -41,6 +41,7 @@ export * from './Helper' export * from './Hooks' export * from './ImageTags' export * from './ImageTags.Types' +export * from './InteractiveCellText' export * from './Markdown' export * from './Modals/Modal' export * from './Modals/VisibleModal' diff --git a/src/Shared/Components/ArtifactInfoModal/ArtifactInfoModal.component.tsx b/src/Shared/Components/ArtifactInfoModal/ArtifactInfoModal.component.tsx index cbdda4a98..7bf43132d 100644 --- a/src/Shared/Components/ArtifactInfoModal/ArtifactInfoModal.component.tsx +++ b/src/Shared/Components/ArtifactInfoModal/ArtifactInfoModal.component.tsx @@ -14,9 +14,11 @@ * limitations under the License. */ +import { getParsedCIMaterialInfo } from '@Shared/Services/utils' + import { ReactComponent as ICArrowDown } from '../../../Assets/Icon/ic-arrow-down.svg' import { ReactComponent as ICClose } from '../../../Assets/Icon/ic-close.svg' -import { Drawer, GenericEmptyState, useAsync } from '../../../Common' +import { Drawer, GenericEmptyState, useQuery } from '../../../Common' import { getArtifactInfo, getCITriggerInfo } from '../../Services/app.service' import { APIResponseHandler } from '../APIResponseHandler' import { Artifacts } from '../CICDHistory' @@ -30,8 +32,14 @@ const ArtifactInfoModal = ({ renderCIListHeader, fetchOnlyArtifactInfo = false, }: ArtifactInfoModalProps) => { - const [isInfoLoading, artifactInfo, infoError, refetchArtifactInfo] = useAsync( - () => + const { + isLoading, + isFetching, + data: artifactInfo, + error: infoError, + refetch: refetchArtifactInfo, + } = useQuery({ + queryFn: () => fetchOnlyArtifactInfo ? getArtifactInfo({ ciArtifactId, @@ -40,8 +48,11 @@ const ArtifactInfoModal = ({ ciArtifactId, envId, }), - [ciArtifactId, envId, fetchOnlyArtifactInfo], - ) + queryKey: [ciArtifactId, envId, fetchOnlyArtifactInfo], + select: ({ result }) => getParsedCIMaterialInfo(result), + }) + + const isInfoLoading = isLoading || isFetching const isArtifactInfoAvailable = !!artifactInfo?.materials?.length const showDescription = isArtifactInfoAvailable && !fetchOnlyArtifactInfo diff --git a/src/Shared/Components/CICDHistory/Artifacts.tsx b/src/Shared/Components/CICDHistory/Artifacts.tsx index 0c4436ba9..7d9de107f 100644 --- a/src/Shared/Components/CICDHistory/Artifacts.tsx +++ b/src/Shared/Components/CICDHistory/Artifacts.tsx @@ -18,9 +18,7 @@ import { useParams } from 'react-router-dom' import { ReactComponent as Down } from '@Icons/ic-arrow-forward.svg' import { ReactComponent as OpenInNew } from '@Icons/ic-arrow-out.svg' -import docker from '@Icons/ic-docker.svg' import { ReactComponent as Download } from '@Icons/ic-download.svg' -import folder from '@Icons/ic-folder.svg' import { ReactComponent as ICHelpOutline } from '@Icons/ic-help.svg' import { ReactComponent as MechanicalOperation } from '@Icons/ic-mechanical-operation.svg' import noartifact from '@Images/no-artifact.webp' @@ -30,6 +28,8 @@ import { useDownload } from '@Shared/Hooks' import { ClipboardButton, extractImage, GenericEmptyState, ImageTagsContainer, useGetUserRoles } from '../../../Common' import { EMPTY_STATE_STATUS } from '../../constants' import { DocLink } from '../DocLink' +import { Icon } from '../Icon' +import { RegistryIcon } from '../RegistryIcon' import { TargetPlatformBadgeList } from '../TargetPlatforms' import { TERMINAL_STATUS_MAP } from './constants' import { ArtifactType, CIListItemType } from './types' @@ -62,6 +62,7 @@ export const CIListItem = ({ renderCIListHeader, targetPlatforms, isDeploymentWithoutApproval, + artifact, }: CIListItemType) => { const showCIListHeader = !!renderCIListHeader && @@ -99,7 +100,11 @@ export const CIListItem = ({ >
- type + {type === 'report' ? ( + + ) : ( + + )}
{children}
@@ -228,6 +233,7 @@ const Artifacts = ({ isSuperAdmin={isSuperAdmin} renderCIListHeader={renderCIListHeader} targetPlatforms={targetPlatforms} + artifact={artifact} >
diff --git a/src/Shared/Components/CICDHistory/History.components.tsx b/src/Shared/Components/CICDHistory/History.components.tsx index 712a6d8d2..be9022f0b 100644 --- a/src/Shared/Components/CICDHistory/History.components.tsx +++ b/src/Shared/Components/CICDHistory/History.components.tsx @@ -204,6 +204,7 @@ export const GitChanges = ({ renderCIListHeader={renderCIListHeader} targetPlatforms={targetPlatforms} isDeploymentWithoutApproval={isDeploymentWithoutApproval} + artifact={artifact} >
diff --git a/src/Shared/Components/CICDHistory/types.tsx b/src/Shared/Components/CICDHistory/types.tsx index 4c63f94e8..7f6f8483f 100644 --- a/src/Shared/Components/CICDHistory/types.tsx +++ b/src/Shared/Components/CICDHistory/types.tsx @@ -435,6 +435,7 @@ export type CIListItemType = Pick { + const selectRef = useRef>(null) + const shouldShowToastRef = useRef(true) + const selectedOptions = options?.map((section) => ({ ...section, options: section?.label === 'Recently Visited' ? section.options?.slice(1) : section.options, })) + + const { registerShortcut, unregisterShortcut } = useRegisterShortcut() + + const handleOpenShortcutToast: SelectPickerProps['onMenuOpen'] = () => { + if (shouldShowToastRef.current) { + ToastManager.showToast({ + variant: ToastVariantType.shortcut, + text: 'to switch Applications', + shortcuts: ['S'], + }) + } + shouldShowToastRef.current = true + } + + useEffect(() => { + registerShortcut({ + keys: ['S'], + callback: () => { + shouldShowToastRef.current = false + selectRef.current?.focus() + selectRef.current?.openMenu('first') + }, + }) + + return () => { + unregisterShortcut(['S']) + } + }, []) + + const onKeyDown: SelectPickerProps['onKeyDown'] = (e) => { + if (e.key === 'Escape' || e.key === 'Esc') { + e.preventDefault() + selectRef.current?.blurInput() + selectRef.current?.blur() + } + } + return ( ) } diff --git a/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiffRadioSelect.tsx b/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiffRadioSelect.tsx index 64d532114..2257a6209 100644 --- a/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiffRadioSelect.tsx +++ b/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiffRadioSelect.tsx @@ -57,8 +57,7 @@ const DeploymentConfigDiffRadioSelect = ({ overlayProps={overlayProps} triggerProps={triggerProps} > - {/* TODO: Remove any after syncing with develop */} -
+
Deployment with Configuration
- {showPagination && ( + {showPagination && !areFilteredRowsLoading && ( )} diff --git a/src/Shared/Components/Table/styles.scss b/src/Shared/Components/Table/styles.scss index 2df9df9aa..ed2fb7c1e 100644 --- a/src/Shared/Components/Table/styles.scss +++ b/src/Shared/Components/Table/styles.scss @@ -61,6 +61,10 @@ } &__row { + & &__hover-item { + display: none; + } + &:focus { outline: none; } @@ -83,6 +87,13 @@ &--bulk-selected.generic-table__row--active > * { background-color: var(--B100); } + + &:hover, + &--active { + .generic-table__row__hover-item { + display: inherit; + } + } } .sortable-table-header__resize-btn:hover, diff --git a/src/Shared/Components/Table/types.ts b/src/Shared/Components/Table/types.ts index f7ece4cce..e4faa7a64 100644 --- a/src/Shared/Components/Table/types.ts +++ b/src/Shared/Components/Table/types.ts @@ -23,7 +23,7 @@ import { UseUrlFiltersProps, UseUrlFiltersReturnType, } from '@Common/Hooks' -import { GenericEmptyStateType } from '@Common/index' +import { APIOptions, GenericEmptyStateType } from '@Common/index' import { PageSizeOption } from '@Common/Pagination/types' import { SortableTableHeaderCellProps, useResizableTableConfig } from '@Common/SortableTableHeaderCell' @@ -209,7 +209,7 @@ export type ViewWrapperProps< ? {} : Pick< UseFiltersReturnType, - 'offset' | 'handleSearch' | 'searchKey' | 'sortBy' | 'sortOrder' | 'clearFilters' + 'offset' | 'handleSearch' | 'searchKey' | 'sortBy' | 'sortOrder' | 'clearFilters' | 'areFiltersApplied' >) & AdditionalProps & Partial> & { @@ -317,7 +317,10 @@ export type InternalTableProps< | { rows?: never /** NOTE: Sorting on frontend is only handled if rows is provided instead of getRows */ - getRows: (props: GetRowsProps) => Promise> + getRows: ( + props: GetRowsProps, + abortControllerRef: APIOptions['abortControllerRef'], + ) => Promise<{ rows: RowsType; totalRows: number }> } ) & ( @@ -427,7 +430,9 @@ export interface TableContentProps< | 'paginationVariant' | 'RowActionsOnHoverComponent' | 'pageSizeOptions' + | 'getRows' > { filteredRows: RowsType areFilteredRowsLoading: boolean + totalRows: number } diff --git a/src/Shared/Components/Table/utils.ts b/src/Shared/Components/Table/utils.ts index d0c3f6d14..8e9ff9580 100644 --- a/src/Shared/Components/Table/utils.ts +++ b/src/Shared/Components/Table/utils.ts @@ -43,17 +43,21 @@ export const searchAndSortRows = < filter: TableProps['filter'], filterData: UseFiltersReturnType, comparator?: Column['comparator'], -) => { +): Awaited['getRows']>> => { const { sortBy, sortOrder } = filterData ?? {} const filteredRows = filter ? rows.filter((row) => filter(row, filterData)) : rows - return comparator && sortBy - ? filteredRows.sort( - (rowA, rowB) => - (sortOrder === SortingOrder.ASC ? 1 : -1) * comparator(rowA.data[sortBy], rowB.data[sortBy]), - ) - : filteredRows + return { + rows: + comparator && sortBy + ? filteredRows.sort( + (rowA, rowB) => + (sortOrder === SortingOrder.ASC ? 1 : -1) * comparator(rowA.data[sortBy], rowB.data[sortBy]), + ) + : filteredRows, + totalRows: filteredRows.length, + } } export const getVisibleColumnsFromLocalStorage = < @@ -199,5 +203,5 @@ export const scrollToShowActiveElementIfNeeded = ( scrollTop += bottom - parentBottom } - parent.scrollTo({ top: scrollTop, behavior: 'smooth' }) + parent.scrollTo({ top: scrollTop, behavior: 'auto' }) } diff --git a/src/Shared/Services/ToastManager/ToastContent.tsx b/src/Shared/Services/ToastManager/ToastContent.tsx index 2e9ce70a5..7ae0705d8 100644 --- a/src/Shared/Services/ToastManager/ToastContent.tsx +++ b/src/Shared/Services/ToastManager/ToastContent.tsx @@ -17,7 +17,7 @@ import { Button, ButtonStyleType, ButtonVariantType } from '@Shared/Components' import { ComponentSizeType } from '@Shared/constants' -import { ToastProps } from './types' +import { ShortcutToastContentProps, ToastProps } from './types' export const ToastContent = ({ title, @@ -39,3 +39,17 @@ export const ToastContent = ({ )}
) + +export const ShortcutToastContent = ({ shortcuts, text }: ShortcutToastContentProps) => ( +
+ {shortcuts.map((shortcutKey) => ( + + {shortcutKey} + + ))} + {text} +
+) diff --git a/src/Shared/Services/ToastManager/constants.tsx b/src/Shared/Services/ToastManager/constants.tsx index 6d9a23758..0310c041e 100644 --- a/src/Shared/Services/ToastManager/constants.tsx +++ b/src/Shared/Services/ToastManager/constants.tsx @@ -26,7 +26,7 @@ import { ReactComponent as ICWarning } from '@Icons/ic-warning.svg' import { Button, ButtonStyleType, ButtonVariantType } from '@Shared/Components' import { ALLOW_ACTION_OUTSIDE_FOCUS_TRAP, ComponentSizeType } from '@Shared/constants' -import { ToastProps, ToastVariantType } from './types' +import { BaseToastProps, ToastProps, ToastVariantType } from './types' export const TOAST_BASE_CONFIG: ToastContainerProps = { autoClose: 5000, @@ -55,7 +55,7 @@ export const TOAST_BASE_CONFIG: ToastContainerProps = { } export const TOAST_VARIANT_TO_CONFIG_MAP: Record< - ToastVariantType, + BaseToastProps['variant'], Required> & Pick > = { [ToastVariantType.info]: { diff --git a/src/Shared/Services/ToastManager/toastManager.scss b/src/Shared/Services/ToastManager/toastManager.scss index 4a44a59bb..8ad85df33 100644 --- a/src/Shared/Services/ToastManager/toastManager.scss +++ b/src/Shared/Services/ToastManager/toastManager.scss @@ -98,3 +98,15 @@ } } } + +div.shortcut-toast { + background: transparent; + border-radius: 12px; + display: flex; + margin: 0; + min-height: 0; + padding: 0; + box-shadow: none; + align-items: center; + justify-content: center; +} \ No newline at end of file diff --git a/src/Shared/Services/ToastManager/toastManager.service.tsx b/src/Shared/Services/ToastManager/toastManager.service.tsx index 3b32d585e..5d3e3c215 100644 --- a/src/Shared/Services/ToastManager/toastManager.service.tsx +++ b/src/Shared/Services/ToastManager/toastManager.service.tsx @@ -18,7 +18,7 @@ import { toast, ToastContainer, ToastOptions } from 'react-toastify' import { TOAST_BASE_CONFIG, TOAST_VARIANT_TO_CONFIG_MAP } from './constants' -import { ToastContent } from './ToastContent' +import { ShortcutToastContent, ToastContent } from './ToastContent' import { ToastProps, ToastVariantType } from './types' import './toastManager.scss' @@ -106,9 +106,23 @@ class ToastManager { description, buttonProps, progressBarBg: customProgressBarBg, + text, + shortcuts, }: ToastProps, options: Pick = {}, ) => { + if (variant === ToastVariantType.shortcut) { + return toast(, { + position: 'top-center', + containerId: 'devtron-shortcut-toast', + className: 'shortcut-toast', + hideProgressBar: true, + closeButton: false, + autoClose: 3000, + closeOnClick: true, + }) + } + const { icon, type, title: defaultTitle, progressBarBg } = TOAST_VARIANT_TO_CONFIG_MAP[variant] return toast( diff --git a/src/Shared/Services/ToastManager/types.ts b/src/Shared/Services/ToastManager/types.ts index 4413f7759..ed2d891d6 100644 --- a/src/Shared/Services/ToastManager/types.ts +++ b/src/Shared/Services/ToastManager/types.ts @@ -16,6 +16,7 @@ import { ReactElement } from 'react' +import { SupportedKeyboardKeysType } from '@Common/Hooks' import { ButtonComponentType, ButtonProps } from '@Shared/Components' export enum ToastVariantType { @@ -24,9 +25,24 @@ export enum ToastVariantType { error = 'error', warn = 'warn', notAuthorized = 'notAuthorized', + shortcut = 'shortcut', } -export interface ToastProps { +export interface ShortcutToastProps { + variant: ToastVariantType.shortcut + /** + * Text to display along the shortcuts in the toast + * Example: "Press [S] to switch context" + */ + text: string + /** + * The shortcuts to be displayed in the toast + * Note: The shortcuts should be in the format of `['Ctrl', 'A'] + */ + shortcuts: SupportedKeyboardKeysType[] +} + +export interface BaseToastProps { /** * Title for the toast * If not provided, defaults to a value based on the selected variant @@ -45,7 +61,7 @@ export interface ToastProps { * * @default ToastVariantType.info */ - variant?: ToastVariantType + variant?: Exclude /** * Props for the action button to be displayed in the toast * @@ -57,3 +73,12 @@ export interface ToastProps { */ progressBarBg?: string } + +export type ToastProps = + | (BaseToastProps & Partial, never>>) + | (ShortcutToastProps & Partial, never>>) + +export interface ShortcutToastContentProps { + text: string + shortcuts: SupportedKeyboardKeysType[] +} diff --git a/src/Shared/Services/app.service.ts b/src/Shared/Services/app.service.ts index 7be3abbff..50964de93 100644 --- a/src/Shared/Services/app.service.ts +++ b/src/Shared/Services/app.service.ts @@ -16,47 +16,23 @@ import { AppConfigProps, GetTemplateAPIRouteType } from '@Pages/index' -import { get, getUrlWithSearchParams, ResponseType, ROUTES, showError } from '../../Common' +import { get, getUrlWithSearchParams, ResponseType, ROUTES } from '../../Common' import { getTemplateAPIRoute } from '..' import { AppEnvDeploymentConfigDTO, AppEnvDeploymentConfigPayloadType, CIMaterialInfoDTO, - CIMaterialInfoType, GetCITriggerInfoParamsType, } from './app.types' -import { getParsedCIMaterialInfo } from './utils' -export const getCITriggerInfo = async (params: GetCITriggerInfoParamsType): Promise => { - try { - const { result } = (await get( - `${ROUTES.APP}/material-info/${params.envId}/${params.ciArtifactId}`, - )) as ResponseType - - return getParsedCIMaterialInfo(result) - } catch (err) { - showError(err) - throw err - } -} +export const getCITriggerInfo = async (params: GetCITriggerInfoParamsType) => + get(`${ROUTES.APP}/material-info/${params.envId}/${params.ciArtifactId}`) /** * The only difference between this and getCITriggerInfo is it doesn't have env and trigger related meta info */ -export const getArtifactInfo = async ( - params: Pick, -): Promise => { - try { - const { result } = (await get( - `${ROUTES.APP}/material-info/${params.ciArtifactId}`, - )) as ResponseType - - return getParsedCIMaterialInfo(result) - } catch (err) { - showError(err) - throw err - } -} +export const getArtifactInfo = (params: Pick) => + get(`${ROUTES.APP}/material-info/${params.ciArtifactId}`) export const getAppEnvDeploymentConfig = ({ params,