From 8bffec15965d4a2912073858170ea916eb54f9ae Mon Sep 17 00:00:00 2001 From: Arun Devtron Date: Fri, 27 Sep 2024 15:30:31 +0530 Subject: [PATCH 01/15] feat: add support for state based collapsible list --- .../CollapsibleList.component.tsx | 99 ++++++++++++------- .../CollapsibleList/CollapsibleList.types.ts | 38 +++++-- .../DeploymentConfigDiff.types.ts | 4 +- .../DeploymentConfigDiffNavigation.tsx | 5 +- 4 files changed, 99 insertions(+), 47 deletions(-) diff --git a/src/Shared/Components/CollapsibleList/CollapsibleList.component.tsx b/src/Shared/Components/CollapsibleList/CollapsibleList.component.tsx index 8b2569c3e..d3111c619 100644 --- a/src/Shared/Components/CollapsibleList/CollapsibleList.component.tsx +++ b/src/Shared/Components/CollapsibleList/CollapsibleList.component.tsx @@ -6,7 +6,7 @@ import { ConditionalWrap } from '@Common/Helper' import { ReactComponent as ICExpand } from '@Icons/ic-expand.svg' import { Collapse } from '../Collapse' -import { CollapsibleListProps } from './CollapsibleList.types' +import { CollapsibleListItem, CollapsibleListProps } from './CollapsibleList.types' import './CollapsibleList.scss' const renderWithTippy = (tippyProps: TippyProps) => (children: React.ReactElement) => ( @@ -18,6 +18,68 @@ const renderWithTippy = (tippyProps: TippyProps) => (children: React.ReactElemen export const CollapsibleList = ({ config, onCollapseBtnClick }: CollapsibleListProps) => { const { pathname } = useLocation() + const getTabContent = (item: CollapsibleListItem) => { + const { title, subtitle, iconConfig } = item + return ( + <> +
+ {title} + {subtitle && {subtitle}} +
+ {iconConfig && ( + + + + )} + + ) + } + + const getTabItem = (item: CollapsibleListItem) => { + const { title, href, isActive, onClick, tabType } = item + if (tabType === 'navLink') { + return ( + { + // Prevent navigation to the same page + if (href === pathname) { + e.preventDefault() + } + onClick?.(e) + }} + > + {getTabContent(item)} + + ) + } + // Since is active is boolean we need to explicitly handle for null + return ( + + ) + } + return (
{config.map(({ id, header, headerIconConfig, items, noItemsText, isExpanded }) => ( @@ -61,40 +123,7 @@ export const CollapsibleList = ({ config, onCollapseBtnClick }: CollapsibleListP
) : ( - items.map(({ title, href, iconConfig, subtitle, onClick }) => ( - { - // Prevent navigation to the same page - if (href === pathname) { - e.preventDefault() - } - onClick?.(e) - }} - > -
- - {title} - - {subtitle && ( - {subtitle} - )} -
- {iconConfig && ( - - - - )} -
- )) + items.map((item) => getTabItem(item)) )} diff --git a/src/Shared/Components/CollapsibleList/CollapsibleList.types.ts b/src/Shared/Components/CollapsibleList/CollapsibleList.types.ts index be4944f82..f8668399d 100644 --- a/src/Shared/Components/CollapsibleList/CollapsibleList.types.ts +++ b/src/Shared/Components/CollapsibleList/CollapsibleList.types.ts @@ -1,7 +1,35 @@ import React from 'react' import { TippyProps } from '@tippyjs/react' -export interface CollapsibleListItem { +interface ButtonTab { + tabType: 'button' + /** + * Is tab active ( for button tab ) + */ + isActive: boolean + /** + * The callback function to handle click events on the button. + */ + onClick?: (e: React.MouseEvent) => void + href?: never +} + +interface NavLinkTab { + tabType: 'navLink' + /** + * The URL of the nav link. + */ + href: string + /** + * The callback function to handle click events on the nav link. + */ + onClick?: (e: React.MouseEvent) => void + isActive?: never +} + +type ConditionalTabType = ButtonTab | NavLinkTab + +export type CollapsibleListItem = ConditionalTabType & { /** * The title of the list item. */ @@ -27,14 +55,6 @@ export interface CollapsibleListItem { */ tooltipProps?: TippyProps } - /** - * The URL of the nav link. - */ - href?: string - /** - * The callback function to handle click events on the nav link. - */ - onClick?: (e: React.MouseEvent) => void } export interface CollapsibleListConfig { diff --git a/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiff.types.ts b/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiff.types.ts index 9064785a4..0d4e32808 100644 --- a/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiff.types.ts +++ b/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiff.types.ts @@ -41,8 +41,10 @@ export type DeploymentConfigDiffSelectPickerProps = selectPickerProps: SelectPickerProps } -export interface DeploymentConfigDiffNavigationItem extends Pick { +export interface DeploymentConfigDiffNavigationItem extends Pick { hasDiff?: boolean + href: string + onClick: (e: React.MouseEvent) => void } export interface DeploymentConfigDiffNavigationCollapsibleItem diff --git a/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiffNavigation.tsx b/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiffNavigation.tsx index b8f2d8307..c1aeaeaa1 100644 --- a/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiffNavigation.tsx +++ b/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiffNavigation.tsx @@ -7,7 +7,7 @@ import { ReactComponent as ICInfoOutlined } from '@Icons/ic-info-outlined.svg' import { ReactComponent as ICDiffFileUpdated } from '@Icons/ic-diff-file-updated.svg' import { StyledRadioGroup } from '@Common/index' -import { CollapsibleList } from '../CollapsibleList' +import { CollapsibleList, CollapsibleListConfig } from '../CollapsibleList' import { DeploymentConfigDiffNavigationProps } from './DeploymentConfigDiff.types' // LOADING SHIMMER @@ -34,10 +34,11 @@ export const DeploymentConfigDiffNavigation = ({ }, [collapsibleNavList]) /** Collapsible List Config. */ - const collapsibleListConfig = collapsibleNavList.map(({ items, ...resListItem }) => ({ + const collapsibleListConfig: CollapsibleListConfig[] = collapsibleNavList.map(({ items, ...resListItem }) => ({ ...resListItem, isExpanded: expandedIds[resListItem.id], items: items.map(({ hasDiff, ...resItem }) => ({ + tabType: 'navLink', ...resItem, ...(hasDiff ? { From da3edaaecf4863347eea42c003160e2cde39ba85 Mon Sep 17 00:00:00 2001 From: Arun Devtron Date: Tue, 8 Oct 2024 19:32:52 +0530 Subject: [PATCH 02/15] feat: add key for drift in Node --- src/Shared/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Shared/types.ts b/src/Shared/types.ts index 513ec54da..01adbefbd 100644 --- a/src/Shared/types.ts +++ b/src/Shared/types.ts @@ -140,6 +140,7 @@ export interface Node { port: number canBeHibernated: boolean isHibernated: boolean + hasDrift?: boolean } // eslint-disable-next-line no-use-before-define From 02b55426e0128424006a1d071dcc7640feda5bbf Mon Sep 17 00:00:00 2001 From: Arun Devtron Date: Wed, 9 Oct 2024 12:07:25 +0530 Subject: [PATCH 03/15] feat: add filter for drifted nodes in status filter button --- src/Common/Constants.ts | 1 + .../CICDHistory/AppStatusDetailsChart.tsx | 40 +++++++++++++++++-- .../StatusFilterButtonComponent.tsx | 12 ++++-- src/Shared/Components/CICDHistory/types.tsx | 6 +++ src/Shared/Store/IndexStore.tsx | 5 +++ 5 files changed, 58 insertions(+), 6 deletions(-) diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index ebb957440..68f80f5c7 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -65,6 +65,7 @@ export const URLS = { GLOBAL_CONFIG_SCOPED_VARIABLES: '/global-config/scoped-variables', GLOBAL_CONFIG_DEPLOYMENT_CHARTS_LIST: '/global-config/deployment-charts', NETWORK_STATUS_INTERFACE: '/network-status-interface', + CONFIG_DRIFT: 'config-drift', } export const ROUTES = { diff --git a/src/Shared/Components/CICDHistory/AppStatusDetailsChart.tsx b/src/Shared/Components/CICDHistory/AppStatusDetailsChart.tsx index b704befad..6df226e19 100644 --- a/src/Shared/Components/CICDHistory/AppStatusDetailsChart.tsx +++ b/src/Shared/Components/CICDHistory/AppStatusDetailsChart.tsx @@ -16,18 +16,34 @@ import { useMemo, useState } from 'react' import Tippy from '@tippyjs/react' +import { useHistory } from 'react-router' +import { URLS } from '@Common/Constants' import { ReactComponent as InfoIcon } from '../../../Assets/Icon/ic-info-filled.svg' import { ReactComponent as Chat } from '../../../Assets/Icon/ic-chat-circle-dots.svg' import { AppStatusDetailsChartType, AggregatedNodes, STATUS_SORTING_ORDER } from './types' import { StatusFilterButtonComponent } from './StatusFilterButtonComponent' -import { DEPLOYMENT_STATUS, APP_STATUS_HEADERS } from '../../constants' +import { DEPLOYMENT_STATUS, APP_STATUS_HEADERS, ComponentSizeType } from '../../constants' import { IndexStore } from '../../Store' import { aggregateNodes } from '../../Helpers' +import { Button, ButtonStyleType, ButtonVariantType } from '../Button' -const AppStatusDetailsChart = ({ filterRemoveHealth = false, showFooter }: AppStatusDetailsChartType) => { +const AppStatusDetailsChart = ({ + filterRemoveHealth = false, + showFooter, + showConfigDriftInfo = false, + onClose, +}: AppStatusDetailsChartType) => { + const history = useHistory() const _appDetails = IndexStore.getAppDetails() const [currentFilter, setCurrentFilter] = useState('') + const { appId, environmentId: envId } = _appDetails + + const handleCompareDesiredManifest = () => { + onClose() + history.push(`${URLS.APP}/${appId}${URLS.DETAILS}/${envId}/${URLS.APP_DETAILS_K8}/${URLS.CONFIG_DRIFT}`) + } + const nodes: AggregatedNodes = useMemo( () => aggregateNodes(_appDetails.resourceTree?.nodes || [], _appDetails.resourceTree?.podMetadata || []), [_appDetails], @@ -100,6 +116,7 @@ const AppStatusDetailsChart = ({ filterRemoveHealth = false, showFooter }: AppSt .filter( (nodeDetails) => currentFilter === 'all' || + (currentFilter === 'drifted' && nodeDetails.hasDrift) || nodeDetails.health.status?.toLowerCase() === currentFilter, ) .map((nodeDetails) => ( @@ -123,7 +140,24 @@ const AppStatusDetailsChart = ({ filterRemoveHealth = false, showFooter }: AppSt > {nodeDetails.status ? nodeDetails.status : nodeDetails.health.status} -
{getNodeMessage(nodeDetails.kind, nodeDetails.name)}
+
+ {showConfigDriftInfo && nodeDetails.hasDrift && ( +
+ Config drift detected + {onClose && appId && envId && ( +
+ )} +
{getNodeMessage(nodeDetails.kind, nodeDetails.name)}
+
)) ) : ( diff --git a/src/Shared/Components/CICDHistory/StatusFilterButtonComponent.tsx b/src/Shared/Components/CICDHistory/StatusFilterButtonComponent.tsx index 0ece827ea..126122070 100644 --- a/src/Shared/Components/CICDHistory/StatusFilterButtonComponent.tsx +++ b/src/Shared/Components/CICDHistory/StatusFilterButtonComponent.tsx @@ -18,9 +18,8 @@ import { useEffect, useState } from 'react' import { ReactComponent as ICCaretDown } from '@Icons/ic-caret-down.svg' import { PopupMenu, StyledRadioGroup as RadioGroup } from '../../../Common' -import { NodeStatus, StatusFilterButtonType } from './types' +import { NodeFilters, NodeStatus, StatusFilterButtonType } from './types' import { IndexStore } from '../../Store' - import './StatusFilterButtonComponent.scss' export const StatusFilterButtonComponent = ({ nodes, handleFilterClick }: StatusFilterButtonType) => { @@ -32,10 +31,15 @@ export const StatusFilterButtonComponent = ({ nodes, handleFilterClick }: Status let progressingNodeCount: number = 0 let failedNodeCount: number = 0 let missingNodeCount: number = 0 + let driftedNodeCount: number = 0 nodes?.forEach((_node) => { const _nodeHealth = _node.health?.status + if (_node.hasDrift) { + driftedNodeCount += 1 + } + if (_nodeHealth?.toLowerCase() === NodeStatus.Healthy) { healthyNodeCount += 1 } else if (_nodeHealth?.toLowerCase() === NodeStatus.Degraded) { @@ -58,6 +62,7 @@ export const StatusFilterButtonComponent = ({ nodes, handleFilterClick }: Status isSelected: NodeStatus.Progressing == selectedTab, }, { status: NodeStatus.Healthy, count: healthyNodeCount, isSelected: NodeStatus.Healthy == selectedTab }, + { status: NodeFilters.Drifted, count: driftedNodeCount, isSelected: selectedTab === NodeFilters.Drifted }, ] const validFilterOptions = filterOptions.filter(({ count }) => count > 0) const displayedInlineFilters = validFilterOptions.slice( @@ -72,7 +77,8 @@ export const StatusFilterButtonComponent = ({ nodes, handleFilterClick }: Status (selectedTab === NodeStatus.Healthy && healthyNodeCount === 0) || (selectedTab === NodeStatus.Degraded && failedNodeCount === 0) || (selectedTab === NodeStatus.Progressing && progressingNodeCount === 0) || - (selectedTab === NodeStatus.Missing && missingNodeCount === 0) + (selectedTab === NodeStatus.Missing && missingNodeCount === 0) || + (selectedTab === NodeFilters.Drifted && driftedNodeCount === 0) ) { setSelectedTab('all') } else if (handleFilterClick) { diff --git a/src/Shared/Components/CICDHistory/types.tsx b/src/Shared/Components/CICDHistory/types.tsx index 4ef8ce948..bdbf9000e 100644 --- a/src/Shared/Components/CICDHistory/types.tsx +++ b/src/Shared/Components/CICDHistory/types.tsx @@ -517,6 +517,8 @@ export interface DeploymentHistorySidebarType { export interface AppStatusDetailsChartType { filterRemoveHealth?: boolean showFooter: boolean + showConfigDriftInfo?: boolean + onClose?: () => void } export interface StatusFilterButtonType { @@ -533,6 +535,10 @@ export enum NodeStatus { Unknown = 'unknown', } +export enum NodeFilters { + Drifted = 'drifted', +} + type NodesMap = { [key in NodeType]?: Map } diff --git a/src/Shared/Store/IndexStore.tsx b/src/Shared/Store/IndexStore.tsx index 8a46e59a9..5a42342df 100644 --- a/src/Shared/Store/IndexStore.tsx +++ b/src/Shared/Store/IndexStore.tsx @@ -17,6 +17,7 @@ /* eslint-disable eqeqeq */ /* eslint-disable array-callback-return */ import { BehaviorSubject } from 'rxjs' +import { NodeFilters } from '@Shared/Components' import { AppDetails, AppType, EnvDetails, EnvType, Node, Nodes, PodMetaData, iNode } from '../types' const _appDetailsSubject: BehaviorSubject = new BehaviorSubject({} as AppDetails) @@ -43,6 +44,10 @@ const publishFilteredNodes = () => { return true } + if (_nodeFilter.filterType.toLowerCase() === NodeFilters.Drifted && _node.hasDrift) { + return true + } + return false }) From df994f699272256866751e11adfbff6ff9b8a41c Mon Sep 17 00:00:00 2001 From: Arun Devtron Date: Wed, 9 Oct 2024 12:33:01 +0530 Subject: [PATCH 04/15] chore: use enum for drifted node --- src/Shared/Components/CICDHistory/AppStatusDetailsChart.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Shared/Components/CICDHistory/AppStatusDetailsChart.tsx b/src/Shared/Components/CICDHistory/AppStatusDetailsChart.tsx index 6df226e19..e966d6df4 100644 --- a/src/Shared/Components/CICDHistory/AppStatusDetailsChart.tsx +++ b/src/Shared/Components/CICDHistory/AppStatusDetailsChart.tsx @@ -20,7 +20,7 @@ import { useHistory } from 'react-router' import { URLS } from '@Common/Constants' import { ReactComponent as InfoIcon } from '../../../Assets/Icon/ic-info-filled.svg' import { ReactComponent as Chat } from '../../../Assets/Icon/ic-chat-circle-dots.svg' -import { AppStatusDetailsChartType, AggregatedNodes, STATUS_SORTING_ORDER } from './types' +import { AppStatusDetailsChartType, AggregatedNodes, STATUS_SORTING_ORDER, NodeFilters } from './types' import { StatusFilterButtonComponent } from './StatusFilterButtonComponent' import { DEPLOYMENT_STATUS, APP_STATUS_HEADERS, ComponentSizeType } from '../../constants' import { IndexStore } from '../../Store' @@ -116,7 +116,7 @@ const AppStatusDetailsChart = ({ .filter( (nodeDetails) => currentFilter === 'all' || - (currentFilter === 'drifted' && nodeDetails.hasDrift) || + (currentFilter === NodeFilters.Drifted && nodeDetails.hasDrift) || nodeDetails.health.status?.toLowerCase() === currentFilter, ) .map((nodeDetails) => ( From 9551c44ccbe551da85dc5ec6b7cffd3232ce88ef Mon Sep 17 00:00:00 2001 From: Arun Devtron Date: Wed, 9 Oct 2024 13:01:49 +0530 Subject: [PATCH 05/15] chore: version bump --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 04f57b8a4..d6bf241fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "4.0.2", + "version": "0.4.5-beta-1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "4.0.2", + "version": "0.4.5-beta-1", "license": "ISC", "dependencies": { "@types/react-dates": "^21.8.6", diff --git a/package.json b/package.json index 74d4e34cd..e2df83f6d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "4.0.2", + "version": "0.4.5-beta-1", "description": "Supporting common component library", "type": "module", "main": "dist/index.js", From 711526232129e15d66314a216851e71ad1763c42 Mon Sep 17 00:00:00 2001 From: Arun Devtron Date: Wed, 9 Oct 2024 14:57:04 +0530 Subject: [PATCH 06/15] chore: edit typing for collapsible list --- .../CollapsibleList.component.tsx | 59 +++++++++++-------- .../CollapsibleList/CollapsibleList.types.ts | 20 ++++--- .../DeploymentConfigDiff.types.ts | 4 +- .../DeploymentConfigDiffNavigation.tsx | 41 +++++++------ 4 files changed, 71 insertions(+), 53 deletions(-) diff --git a/src/Shared/Components/CollapsibleList/CollapsibleList.component.tsx b/src/Shared/Components/CollapsibleList/CollapsibleList.component.tsx index c539ef38f..1fe3274f1 100644 --- a/src/Shared/Components/CollapsibleList/CollapsibleList.component.tsx +++ b/src/Shared/Components/CollapsibleList/CollapsibleList.component.tsx @@ -5,7 +5,7 @@ import { ConditionalWrap } from '@Common/Helper' import { ReactComponent as ICExpand } from '@Icons/ic-expand.svg' import { Collapse } from '../Collapse' -import { CollapsibleListItem, CollapsibleListProps } from './CollapsibleList.types' +import { CollapsibleListItem, CollapsibleListProps, TabOptions } from './CollapsibleList.types' import './CollapsibleList.scss' const renderWithTippy = (tippyProps: TippyProps) => (children: React.ReactElement) => ( @@ -14,10 +14,14 @@ const renderWithTippy = (tippyProps: TippyProps) => (children: React.ReactElemen ) -export const CollapsibleList = ({ config, onCollapseBtnClick }: CollapsibleListProps) => { +export const CollapsibleList = ({ + config, + tabType, + onCollapseBtnClick, +}: CollapsibleListProps) => { const { pathname } = useLocation() - const getTabContent = (item: CollapsibleListItem) => { + const getTabContent = (item: CollapsibleListItem) => { const { title, subtitle, iconConfig } = item return ( <> @@ -40,27 +44,8 @@ export const CollapsibleList = ({ config, onCollapseBtnClick }: CollapsibleListP ) } - const getTabItem = (item: CollapsibleListItem) => { - const { title, href, isActive, onClick, tabType } = item - if (tabType === 'navLink') { - return ( - { - // Prevent navigation to the same page - if (href === pathname) { - e.preventDefault() - } - onClick?.(e) - }} - > - {getTabContent(item)} - - ) - } - // Since is active is boolean we need to explicitly handle for null + const getButtonTabItem = (item: CollapsibleListItem<'button'>) => { + const { title, isActive, onClick } = item return (