diff --git a/package-lock.json b/package-lock.json index ed6c61b06..b09819c3e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.13.0-pre-1", + "version": "1.13.0-pre-2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.13.0-pre-1", + "version": "1.13.0-pre-2", "hasInstallScript": true, "license": "ISC", "dependencies": { diff --git a/package.json b/package.json index e81850c89..b58d4c174 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.13.0-pre-1", + "version": "1.13.0-pre-2", "description": "Supporting common component library", "type": "module", "main": "dist/index.js", diff --git a/src/Assets/IconV2/ic-expand-sm.svg b/src/Assets/IconV2/ic-expand-sm.svg new file mode 100644 index 000000000..8c5411a7c --- /dev/null +++ b/src/Assets/IconV2/ic-expand-sm.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index ad1b89398..86964dd9d 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -30,6 +30,7 @@ export const DOCUMENTATION = { GLOBAL_CONFIG_BUILD_INFRA: `${DOCUMENTATION_HOME_PAGE}${DOCUMENTATION_VERSION}/global-configurations/build-infra`, ENTERPRISE_LICENSE: `${DOCUMENTATION_HOME_PAGE}/enterprise-license`, KUBE_CONFIG: `${DOCUMENTATION_HOME_PAGE}${DOCUMENTATION_VERSION}/usage/resource-browser#running-kubectl-commands-locally`, + TENANT_INSTALLATION: `${DOCUMENTATION_HOME_PAGE}${DOCUMENTATION_VERSION}/usage/software-distribution-hub/tenants`, } export const PATTERNS = { @@ -86,7 +87,6 @@ export const URLS = { COMPARE_CLUSTERS: '/compare-clusters', APP_CONFIG: 'edit', GLOBAL_CONFIG: '/global-config', - CONFIG_DRIFT: 'config-drift', GLOBAL_CONFIG_TEMPLATES_DEVTRON_APP, GLOBAL_CONFIG_TEMPLATES_DEVTRON_APP_CREATE: `${GLOBAL_CONFIG_TEMPLATES_DEVTRON_APP}/create`, // NOTE: using appId since we are re-using AppConfig component @@ -135,6 +135,7 @@ export const ROUTES = { ATTRIBUTES_CREATE: 'attributes/create', ATTRIBUTES_UPDATE: 'attributes/update', APP_LIST_MIN: 'app/min', + APP_DETAIL: 'app/detail', CLUSTER_LIST_MIN: 'cluster/autocomplete', CLUSTER_LIST_RAW: 'k8s/capacity/cluster/list/raw', PLUGIN_GLOBAL_LIST_DETAIL_V2: 'plugin/global/list/detail/v2', diff --git a/src/Common/Drawer/Drawer.tsx b/src/Common/Drawer/Drawer.tsx index 57fefae9c..d33d57acb 100644 --- a/src/Common/Drawer/Drawer.tsx +++ b/src/Common/Drawer/Drawer.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { useRef, useEffect } from 'react' +import { useRef, useEffect } from 'react' import { preventBodyScroll } from '../../Shared' import { VisibleModal } from '../Modals/VisibleModal' import './Drawer.scss' diff --git a/src/Common/Drawer/index.ts b/src/Common/Drawer/index.ts new file mode 100644 index 000000000..04766cfab --- /dev/null +++ b/src/Common/Drawer/index.ts @@ -0,0 +1 @@ +export * from './Drawer' diff --git a/src/Common/EmptyState/index.ts b/src/Common/EmptyState/index.ts new file mode 100644 index 000000000..69d94377b --- /dev/null +++ b/src/Common/EmptyState/index.ts @@ -0,0 +1,2 @@ +export { default as GenericEmptyState } from './GenericEmptyState' +export { default as GenericFilterEmptyState } from './GenericFilterEmptyState' diff --git a/src/Common/Helper.tsx b/src/Common/Helper.tsx index 76cd26c9d..2180951a3 100644 --- a/src/Common/Helper.tsx +++ b/src/Common/Helper.tsx @@ -43,6 +43,7 @@ import { ToastVariantType, versionComparatorBySortOrder, WebhookEventNameType, + AppType, } from '../Shared' import { ReactComponent as ArrowDown } from '@Icons/ic-chevron-down.svg' import { ReactComponent as ICWebhook } from '@Icons/ic-webhook.svg' @@ -679,6 +680,7 @@ export const applyCompareDiffOnUneditedDocument = (uneditedDocument: object, edi /** * Returns a debounced variant of the function + * @deprecated - It should use useRef instead, pls use useDebounce */ export const debounce = (func, timeout = 500) => { let timer @@ -693,6 +695,17 @@ export const debounce = (func, timeout = 500) => { } } +export const useDebounce = void>(cb: Callback, delay: number) => { + const timeoutId = useRef>(null) + + return (...args: Parameters) => { + if (timeoutId.current) { + clearTimeout(timeoutId.current) + } + timeoutId.current = setTimeout(() => cb(...args), delay) + } +} + /** * Returns a capitalized string with first letter in uppercase and rest in lowercase */ @@ -1083,4 +1096,23 @@ export const getTTLInHumanReadableFormat = (ttl: number): string => { const humanizedDuration = moment.duration(absoluteTTL, 'seconds').humanize(false) // Since moment.js return "a" or "an" for singular values so replacing with 1. return humanizedDuration.replace(/^(a|an) /, '1 '); -} \ No newline at end of file +} + +const getAppTypeCategory = (appType: AppType) => { + switch (appType) { + case AppType.DEVTRON_APP: + return 'DA' + case AppType.DEVTRON_HELM_CHART: + case AppType.EXTERNAL_HELM_CHART: + return 'HA' + case AppType.EXTERNAL_ARGO_APP: + return 'ACD' + case AppType.EXTERNAL_FLUX_APP: + return 'FCD' + default: + return 'DA' + } +} + +export const getAIAnalyticsEvents = (context: string, appType?: AppType) => + `AI_${appType ? `${getAppTypeCategory(appType)}_` : ''}${context}` diff --git a/src/Common/SegmentedControl/Segment.tsx b/src/Common/SegmentedControl/Segment.tsx index 19f28853b..437fc9020 100644 --- a/src/Common/SegmentedControl/Segment.tsx +++ b/src/Common/SegmentedControl/Segment.tsx @@ -1,8 +1,9 @@ -import { ReactElement } from 'react' +import { ReactElement, useMemo } from 'react' import { Tooltip } from '@Common/Tooltip' import { Icon } from '@Shared/Components' import { ComponentSizeType } from '@Shared/constants' +import { getUniqueId } from '@Shared/Helpers' import { ConditionalWrap } from '../Helper' import { COMPONENT_SIZE_TO_ICON_CLASS_MAP, COMPONENT_SIZE_TO_SEGMENT_CLASS_MAP } from './constants' @@ -24,6 +25,8 @@ const Segment = ({ size, disabled, }: SegmentProps) => { + const inputId = useMemo(getUniqueId, []) + const { value, icon, isError, label, tooltipProps, ariaLabel } = segment const handleChange = () => { onChange(segment) @@ -32,13 +35,13 @@ const Segment = ({ return (
) @@ -180,7 +182,9 @@ export const DeploymentStatusDetailRow = ({ {statusBreakDownType.displayText} {statusBreakDownType.displaySubText && ( - + {statusBreakDownType.displaySubText} )} @@ -200,7 +204,7 @@ export const DeploymentStatusDetailRow = ({ )} {((type === TIMELINE_STATUS.KUBECTL_APPLY && statusBreakDownType.kubeList?.length) || (type === TIMELINE_STATUS.APP_HEALTH && - appHealthDropDownlist.includes(statusBreakDownType.icon)) || + APP_HEALTH_DROP_DOWN_LIST.includes(statusBreakDownType.icon)) || ((type === TIMELINE_STATUS.GIT_COMMIT || type === TIMELINE_STATUS.ARGOCD_SYNC) && statusBreakDownType.icon === 'failed')) && ( .radio__item-label { - border-right: none; - border-top-right-radius: 0 !important; - border-bottom-right-radius: 0 !important; - } - - .radio:first-child > .radio__item-label { - padding: 2px 4px; - } - } -} diff --git a/src/Shared/Components/CICDHistory/StatusFilterButtonComponent.tsx b/src/Shared/Components/CICDHistory/StatusFilterButtonComponent.tsx index 918276109..6b0890b48 100644 --- a/src/Shared/Components/CICDHistory/StatusFilterButtonComponent.tsx +++ b/src/Shared/Components/CICDHistory/StatusFilterButtonComponent.tsx @@ -14,15 +14,17 @@ * limitations under the License. */ -import { ChangeEvent, useEffect, useMemo, useState } from 'react' +import { useEffect, useState } from 'react' import { ReactComponent as ICCaretDown } from '@Icons/ic-caret-down.svg' +import { SegmentType } from '@Common/SegmentedControl/types' +import { ComponentSizeType } from '@Shared/constants' -import { PopupMenu, StyledRadioGroup as RadioGroup } from '../../../Common' +import { PopupMenu, SegmentedControl } from '../../../Common' import { StatusFilterButtonType } from './types' import { getAppStatusIcon, getNodesCount, getStatusFilters } from './utils' -import './StatusFilterButtonComponent.scss' +import './StatusFilterButton.scss' export const StatusFilterButtonComponent = ({ nodes, @@ -34,16 +36,21 @@ export const StatusFilterButtonComponent = ({ const [overflowFilterIndex, setOverflowFilterIndex] = useState(0) // STATUS FILTERS - const { allResourceKindFilter, statusFilters } = useMemo(() => getStatusFilters(getNodesCount(nodes)), [nodes]) + const { allResourceKindFilter, statusFilters } = getStatusFilters(getNodesCount(nodes)) useEffect(() => { const filterIndex = statusFilters.findIndex(({ status }) => status === selectedTab) + + if (filterIndex === -1) { + handleFilterClick(allResourceKindFilter.status) + } + setOverflowFilterIndex(Math.max(filterIndex, 0)) - }, [statusFilters]) + }, [JSON.stringify(statusFilters)]) const showOverflowFilters = maxInlineFiltersCount > 0 && statusFilters.length > maxInlineFiltersCount - const inlineFilters = useMemo(() => { + const getInlineFilters = () => { if (showOverflowFilters) { const min = Math.max(0, Math.min(overflowFilterIndex - 1, statusFilters.length - maxInlineFiltersCount)) const max = Math.min(min + maxInlineFiltersCount, statusFilters.length) @@ -52,15 +59,18 @@ export const StatusFilterButtonComponent = ({ } return statusFilters - }, [statusFilters.length, overflowFilterIndex, maxInlineFiltersCount]) + } + + const inlineFilters = getInlineFilters() + + const handleInlineFilterClick = (segment: SegmentType) => { + const { value } = segment - const handleInlineFilterClick = (e: ChangeEvent) => { - const { value } = e.target if (value === allResourceKindFilter.status) { setOverflowFilterIndex(0) } if (selectedTab !== value) { - handleFilterClick(value) + handleFilterClick(value as string) } } @@ -71,37 +81,50 @@ export const StatusFilterButtonComponent = ({ } } + const segments: SegmentType[] = [ + { + value: allResourceKindFilter.status, + label: `All (${allResourceKindFilter.count})`, + }, + + ...inlineFilters.map(({ status, count }) => ({ + value: status, + label: ( + + {getAppStatusIcon(status, true)} + {count} + + ), + tooltipProps: { + content: status, + className: 'w-100 dc__first-letter-capitalize', + }, + })), + ] + + const segmentValue = segments.find(({ value }) => value === selectedTab)?.value || null + + const segmentControlKey = inlineFilters.reduce( + (acc, inlineFilter) => `${acc}-${inlineFilter.status}`, + `${allResourceKindFilter.status}`, + ) + return ( -
- + - - {`${allResourceKindFilter.status} (${allResourceKindFilter.count})`} - - {inlineFilters.map(({ status, count }) => ( - - {getAppStatusIcon(status, true)} - {count} - - ))} - + name="status-filter-button" + size={ComponentSizeType.small} + /> + {showOverflowFilters && ( diff --git a/src/Shared/Components/CICDHistory/constants.tsx b/src/Shared/Components/CICDHistory/constants.tsx index 6e427ba19..9d3d366f5 100644 --- a/src/Shared/Components/CICDHistory/constants.tsx +++ b/src/Shared/Components/CICDHistory/constants.tsx @@ -168,3 +168,5 @@ export const FAILED_WORKFLOW_STAGE_STATUS_MAP: Record< [WorkflowStageStatusType.FAILED]: true, [WorkflowStageStatusType.TIMEOUT]: true, } + +export const APP_HEALTH_DROP_DOWN_LIST = ['inprogress', 'failed', 'disconnect', 'timed_out'] diff --git a/src/Shared/Components/CICDHistory/index.tsx b/src/Shared/Components/CICDHistory/index.tsx index 6a3fd3394..1c08f250f 100644 --- a/src/Shared/Components/CICDHistory/index.tsx +++ b/src/Shared/Components/CICDHistory/index.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ -export { default as AppStatusDetailsChart } from './AppStatusDetailsChart' export { default as Artifacts } from './Artifacts' export { default as CDEmptyState } from './CDEmptyState' export * from './CiPipelineSourceConfig' diff --git a/src/Shared/Components/CICDHistory/types.tsx b/src/Shared/Components/CICDHistory/types.tsx index 417f5ed8b..96d353dae 100644 --- a/src/Shared/Components/CICDHistory/types.tsx +++ b/src/Shared/Components/CICDHistory/types.tsx @@ -631,13 +631,6 @@ export interface DeploymentHistorySidebarType { setDeploymentHistoryList: React.Dispatch> } -export interface AppStatusDetailsChartType { - filterRemoveHealth?: boolean - showFooter: boolean - showConfigDriftInfo?: boolean - onClose?: () => void -} - export interface StatusFilterButtonType { nodes: Array selectedTab: string diff --git a/src/Shared/Components/Error/ErrorBar.tsx b/src/Shared/Components/Error/ErrorBar.tsx index 4fc517ff5..1cf0b7bc1 100644 --- a/src/Shared/Components/Error/ErrorBar.tsx +++ b/src/Shared/Components/Error/ErrorBar.tsx @@ -14,45 +14,15 @@ * limitations under the License. */ -import { useEffect, useState } from 'react' import { NavLink } from 'react-router-dom' import { ReactComponent as ErrorInfo } from '../../../Assets/Icon/ic-errorInfo.svg' import { URLS } from '../../../Common' import { AppType } from '../../types' -import { AppDetailsErrorType, ErrorBarType } from './types' -import { renderErrorHeaderMessage } from './utils' - -const ErrorBar = ({ appDetails }: ErrorBarType) => { - const [isImagePullBackOff, setIsImagePullBackOff] = useState(false) - - useEffect(() => { - if (appDetails.appType === AppType.DEVTRON_APP && appDetails.resourceTree?.nodes?.length) { - for (let index = 0; index < appDetails.resourceTree.nodes.length; index++) { - const node = appDetails.resourceTree.nodes[index] - let _isImagePullBackOff = false - if (node.info?.length) { - for (let idx = 0; idx < node.info.length; idx++) { - const info = node.info[idx] - if ( - info.value && - (info.value.toLowerCase() === AppDetailsErrorType.ERRIMAGEPULL || - info.value.toLowerCase() === AppDetailsErrorType.IMAGEPULLBACKOFF) - ) { - _isImagePullBackOff = true - break - } - } - - if (_isImagePullBackOff) { - setIsImagePullBackOff(true) - break - } - } - } - } - }, [appDetails]) +import { ErrorBarType } from './types' +import { getIsImagePullBackOff, renderErrorHeaderMessage } from './utils' +const ErrorBar = ({ appDetails, useParentMargin = true }: ErrorBarType) => { if ( !appDetails || appDetails.appType !== AppType.DEVTRON_APP || @@ -63,10 +33,12 @@ const ErrorBar = ({ appDetails }: ErrorBarType) => { return null } + const isImagePullBackOff = getIsImagePullBackOff(appDetails) + return ( isImagePullBackOff && ( -
-
+
+
IMAGEPULLBACKOFF: {renderErrorHeaderMessage(appDetails, 'error-bar')}
diff --git a/src/Shared/Components/Error/types.tsx b/src/Shared/Components/Error/types.tsx index ce1fd62a9..f80a98ebb 100644 --- a/src/Shared/Components/Error/types.tsx +++ b/src/Shared/Components/Error/types.tsx @@ -18,6 +18,10 @@ import { AppDetails } from '../../types' export interface ErrorBarType { appDetails: AppDetails + /** + * @default true + */ + useParentMargin?: boolean } export enum AppDetailsErrorType { diff --git a/src/Shared/Components/Error/utils.tsx b/src/Shared/Components/Error/utils.tsx index aadaa48c3..55cb563f3 100644 --- a/src/Shared/Components/Error/utils.tsx +++ b/src/Shared/Components/Error/utils.tsx @@ -14,7 +14,24 @@ * limitations under the License. */ -import { AppDetails } from '../../types' +import { AppDetails, AppType } from '../../types' +import { AppDetailsErrorType } from './types' + +export const getIsImagePullBackOff = (appDetails: AppDetails): boolean => { + if (appDetails?.appType === AppType.DEVTRON_APP && appDetails.resourceTree?.nodes?.length) { + return appDetails.resourceTree.nodes.some((node) => + (node.info || []).some((info) => { + const infoValueLowerCase = info.value?.toLowerCase() + return ( + infoValueLowerCase === AppDetailsErrorType.ERRIMAGEPULL || + infoValueLowerCase === AppDetailsErrorType.IMAGEPULLBACKOFF + ) + }), + ) + } + + return false +} export const renderErrorHeaderMessage = (appDetails: AppDetails, key: string, onClickActionButton?): JSX.Element => (
diff --git a/src/Shared/Components/Icon/Icon.tsx b/src/Shared/Components/Icon/Icon.tsx index 0ed7ebaeb..caf41168e 100644 --- a/src/Shared/Components/Icon/Icon.tsx +++ b/src/Shared/Components/Icon/Icon.tsx @@ -54,6 +54,7 @@ import { ReactComponent as ICEcr } from '@IconsV2/ic-ecr.svg' import { ReactComponent as ICEnv } from '@IconsV2/ic-env.svg' import { ReactComponent as ICError } from '@IconsV2/ic-error.svg' import { ReactComponent as ICExpandRightSm } from '@IconsV2/ic-expand-right-sm.svg' +import { ReactComponent as ICExpandSm } from '@IconsV2/ic-expand-sm.svg' import { ReactComponent as ICFailure } from '@IconsV2/ic-failure.svg' import { ReactComponent as ICFileKey } from '@IconsV2/ic-file-key.svg' import { ReactComponent as ICFolderUser } from '@IconsV2/ic-folder-user.svg' @@ -199,6 +200,7 @@ export const iconMap = { 'ic-env': ICEnv, 'ic-error': ICError, 'ic-expand-right-sm': ICExpandRightSm, + 'ic-expand-sm': ICExpandSm, 'ic-failure': ICFailure, 'ic-file-key': ICFileKey, 'ic-folder-user': ICFolderUser, diff --git a/src/Shared/Components/InfoIconTippy/InfoIconTippy.tsx b/src/Shared/Components/InfoIconTippy/InfoIconTippy.tsx index 437b05def..eb79c7339 100644 --- a/src/Shared/Components/InfoIconTippy/InfoIconTippy.tsx +++ b/src/Shared/Components/InfoIconTippy/InfoIconTippy.tsx @@ -31,6 +31,7 @@ const InfoIconTippy = ({ dataTestid = 'info-tippy-button', children, headingInfo, + buttonPadding = 'p-0', }: InfoIconTippyProps) => ( diff --git a/src/Shared/Components/index.ts b/src/Shared/Components/index.ts index 897056ef4..3747ac491 100644 --- a/src/Shared/Components/index.ts +++ b/src/Shared/Components/index.ts @@ -20,6 +20,7 @@ export * from './ActivityIndicator' export * from './AnimatedDeployButton' export * from './AnimatedTimer' export * from './APIResponseHandler' +export * from './AppStatusModal' export * from './ArtifactInfoModal' export * from './Backdrop' export * from './BulkOperations' diff --git a/src/Shared/Store/IndexStore.tsx b/src/Shared/Store/IndexStore.tsx index baa1e38b0..376344a78 100644 --- a/src/Shared/Store/IndexStore.tsx +++ b/src/Shared/Store/IndexStore.tsx @@ -245,6 +245,7 @@ export const IndexStore = { const podMetadata = data.resourceTree?.podMetadata || [] + // Not brave enough to remove this method but seems like its not doing anything getiNodesByRootNodeWithChildNodes( _nodes, _nodes.filter((_n) => (_n.parentRefs ?? []).length == 0).map((_n) => _n as iNode), diff --git a/src/Shared/types.ts b/src/Shared/types.ts index 869bd4357..e266bdee0 100644 --- a/src/Shared/types.ts +++ b/src/Shared/types.ts @@ -153,6 +153,7 @@ export interface Node { canBeHibernated: boolean isHibernated: boolean hasDrift?: boolean + status?: string } // eslint-disable-next-line no-use-before-define @@ -246,6 +247,11 @@ export interface AppDetails { FluxAppStatusDetail?: FluxAppStatusDetail } +export interface ConfigDriftModalProps extends Required> { + envId: number + handleCloseModal?: () => void +} + export enum RegistryType { GIT = 'git', GITHUB = 'github',