diff --git a/package-lock.json b/package-lock.json index a8d998cee..5b2219979 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "0.5.6", + "version": "0.5.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "0.5.6", + "version": "0.5.8", "license": "ISC", "dependencies": { "@types/react-dates": "^21.8.6", @@ -70,7 +70,6 @@ "react-draggable": "^4.4.5", "react-ga4": "^1.4.1", "react-mde": "^11.5.0", - "react-router": "^5.3.0", "react-router-dom": "^5.3.0", "react-select": "5.8.0", "rxjs": "^7.8.1", diff --git a/package.json b/package.json index 5faedf0b3..71f17bf42 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "0.5.6", + "version": "0.5.8", "description": "Supporting common component library", "type": "module", "main": "dist/index.js", @@ -84,7 +84,6 @@ "react-draggable": "^4.4.5", "react-ga4": "^1.4.1", "react-mde": "^11.5.0", - "react-router": "^5.3.0", "react-router-dom": "^5.3.0", "react-select": "5.8.0", "rxjs": "^7.8.1", diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index f15e80ef6..237afd4da 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -66,6 +66,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..c8f797bef 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-dom' +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 } 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 === NodeFilters.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..06f2d77a0 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,11 @@ export const StatusFilterButtonComponent = ({ nodes, handleFilterClick }: Status isSelected: NodeStatus.Progressing == selectedTab, }, { status: NodeStatus.Healthy, count: healthyNodeCount, isSelected: NodeStatus.Healthy == selectedTab }, + window._env_.FEATURE_CONFIG_DRIFT_ENABLE && { + status: NodeFilters.drifted, + count: driftedNodeCount, + isSelected: selectedTab === NodeFilters.drifted, + }, ] const validFilterOptions = filterOptions.filter(({ count }) => count > 0) const displayedInlineFilters = validFilterOptions.slice( @@ -72,7 +81,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 558b5806a..ec1d2f6b9 100644 --- a/src/Shared/Components/CICDHistory/types.tsx +++ b/src/Shared/Components/CICDHistory/types.tsx @@ -519,6 +519,8 @@ export interface DeploymentHistorySidebarType { export interface AppStatusDetailsChartType { filterRemoveHealth?: boolean showFooter: boolean + showConfigDriftInfo?: boolean + onClose?: () => void } export interface StatusFilterButtonType { @@ -535,6 +537,10 @@ export enum NodeStatus { Unknown = 'unknown', } +export enum NodeFilters { + drifted = 'drifted', +} + type NodesMap = { [key in NodeType]?: Map } diff --git a/src/Shared/Components/CollapsibleList/CollapsibleList.component.tsx b/src/Shared/Components/CollapsibleList/CollapsibleList.component.tsx index 3928907f1..960ec0b36 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 { CollapsibleListProps } from './CollapsibleList.types' +import { CollapsibleListItem, CollapsibleListProps, TabOptions } from './CollapsibleList.types' import './CollapsibleList.scss' const renderWithTippy = (tippyProps: TippyProps) => (children: React.ReactElement) => ( @@ -14,9 +14,80 @@ 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 { title, subtitle, strikeThrough, iconConfig } = item + return ( + <> +
+ + {title} + + {subtitle && {subtitle}} +
+ {iconConfig && ( + + + + )} + + ) + } + + const getButtonTabItem = (item: CollapsibleListItem<'button'>) => { + const { title, isActive, onClick } = item + return ( + + ) + } + + const getNavLinkTabItem = (item: CollapsibleListItem<'navLink'>) => { + const { title, href, onClick } = item + return ( + { + // Prevent navigation to the same page + if (href === pathname) { + e.preventDefault() + } + onClick?.(e) + }} + > + {getTabContent(item)} + + ) + } + return (
{config.map(({ id, header, headerIconConfig, items, noItemsText, isExpanded }) => ( @@ -60,42 +131,11 @@ export const CollapsibleList = ({ config, onCollapseBtnClick }: CollapsibleListP
) : ( - items.map(({ title, strikeThrough, href, iconConfig, subtitle, onClick }) => ( - { - // Prevent navigation to the same page - if (href === pathname) { - e.preventDefault() - } - onClick?.(e) - }} - > -
- - {title} - - {subtitle && ( - {subtitle} - )} -
- {iconConfig && ( - - - - )} -
- )) + items.map((item) => + tabType === 'button' + ? getButtonTabItem(item as CollapsibleListItem<'button'>) + : getNavLinkTabItem(item as CollapsibleListItem<'navLink'>), + ) )} diff --git a/src/Shared/Components/CollapsibleList/CollapsibleList.types.ts b/src/Shared/Components/CollapsibleList/CollapsibleList.types.ts index e67b599ff..e3f35d492 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 { + /** + * 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 { + /** + * 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 +} + +export type TabOptions = 'button' | 'navLink' + +type ConditionalTabType = TabType extends 'button' ? ButtonTab : NavLinkTab + +export type CollapsibleListItem = ConditionalTabType & { /** * The title of the list item. */ @@ -31,17 +59,9 @@ 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 { +export interface CollapsibleListConfig { /** * The unique identifier for the collapsible list. */ @@ -79,18 +99,22 @@ export interface CollapsibleListConfig { /** * An array of items to be displayed in the collapsible list. */ - items: CollapsibleListItem[] + items: CollapsibleListItem[] /** * Boolean indicating whether the list is expanded or not. */ isExpanded?: boolean } -export interface CollapsibleListProps { +export interface CollapsibleListProps { /** * An array of collapsible list configurations. */ - config: CollapsibleListConfig[] + config: CollapsibleListConfig[] + /** + * Type of tab list: button or navLink + */ + tabType: TabType /** * Function to handle the collapse button click event. * diff --git a/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiff.types.ts b/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiff.types.ts index f23f7d9c8..a3c5b3519 100644 --- a/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiff.types.ts +++ b/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiff.types.ts @@ -52,13 +52,14 @@ export type DeploymentConfigDiffSelectPickerProps = selectPickerProps: SelectPickerProps } -export interface DeploymentConfigDiffNavigationItem extends Pick { +export interface DeploymentConfigDiffNavigationItem + extends Pick, 'href' | 'title' | 'onClick'> { Icon?: React.FunctionComponent> diffState: DeploymentConfigListItem['diffState'] } export interface DeploymentConfigDiffNavigationCollapsibleItem - extends Pick { + extends Pick, 'id' | 'header' | 'noItemsText'> { items: DeploymentConfigDiffNavigationItem[] } diff --git a/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiffNavigation.tsx b/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiffNavigation.tsx index 1211a2901..9028e8ae3 100644 --- a/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiffNavigation.tsx +++ b/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiffNavigation.tsx @@ -39,28 +39,30 @@ export const DeploymentConfigDiffNavigation = ({ }, [collapsibleNavList]) /** Collapsible List Config. */ - const collapsibleListConfig = collapsibleNavList.map(({ items, ...resListItem }) => ({ - ...resListItem, - isExpanded: expandedIds[resListItem.id], - items: items.map(({ diffState, ...resItem }) => ({ - ...resItem, - strikeThrough: showDetailedDiffState && diffState === DeploymentConfigDiffState.DELETED, - ...(!hideDiffState && diffState !== DeploymentConfigDiffState.NO_DIFF - ? { - iconConfig: { - Icon: showDetailedDiffState ? diffStateIconMap[diffState] : diffStateIconMap.hasDiff, - tooltipProps: { - content: showDetailedDiffState - ? diffStateTooltipTextMap[diffState] - : diffStateTooltipTextMap.hasDiff, - arrow: false, - placement: 'right' as const, + const collapsibleListConfig = collapsibleNavList.map>( + ({ items, ...resListItem }) => ({ + ...resListItem, + isExpanded: expandedIds[resListItem.id], + items: items.map['items'][0]>(({ diffState, ...resItem }) => ({ + ...resItem, + strikeThrough: showDetailedDiffState && diffState === DeploymentConfigDiffState.DELETED, + ...(!hideDiffState && diffState !== DeploymentConfigDiffState.NO_DIFF + ? { + iconConfig: { + Icon: showDetailedDiffState ? diffStateIconMap[diffState] : diffStateIconMap.hasDiff, + tooltipProps: { + content: showDetailedDiffState + ? diffStateTooltipTextMap[diffState] + : diffStateTooltipTextMap.hasDiff, + arrow: false, + placement: 'right' as const, + }, }, - }, - } - : {}), - })), - })) + } + : {}), + })), + }), + ) // METHODS /** Handles collapse button click. */ @@ -145,7 +147,7 @@ export const DeploymentConfigDiffNavigation = ({ ) })} - + {navHelpText && (
diff --git a/src/Shared/Store/IndexStore.tsx b/src/Shared/Store/IndexStore.tsx index 8a46e59a9..376345129 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 }) diff --git a/src/Shared/types.ts b/src/Shared/types.ts index 7ec58c71f..42082ea2e 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 diff --git a/src/index.ts b/src/index.ts index 57d0183d5..63ee96403 100644 --- a/src/index.ts +++ b/src/index.ts @@ -76,6 +76,7 @@ export interface customEnv { SYSTEM_CONTROLLER_LISTING_TIMEOUT?: number FEATURE_STEP_WISE_LOGS_ENABLE?: boolean FEATURE_IMAGE_PROMOTION_ENABLE?: boolean + FEATURE_CONFIG_DRIFT_ENABLE: boolean } declare global { interface Window {