diff --git a/package-lock.json b/package-lock.json index 4a10229ea..4995bef46 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "0.6.0-patch-1", + "version": "0.6.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "0.6.0-patch-1", + "version": "0.6.5", "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 5678e1dad..8b3b3cf5e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "0.6.0-patch-1", + "version": "0.6.5", "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/Common/Hooks/useUrlFilters/types.ts b/src/Common/Hooks/useUrlFilters/types.ts index 335828637..5dc12d8c8 100644 --- a/src/Common/Hooks/useUrlFilters/types.ts +++ b/src/Common/Hooks/useUrlFilters/types.ts @@ -25,6 +25,7 @@ export interface UseUrlFiltersProps { * Callback function for parsing the search params */ parseSearchParams?: (searchParams: URLSearchParams) => K + localStorageKey?: `${string}__${string}` } export type UseUrlFiltersReturnType = K & { diff --git a/src/Common/Hooks/useUrlFilters/useUrlFilters.ts b/src/Common/Hooks/useUrlFilters/useUrlFilters.ts index ea00146a3..6abb0a39e 100644 --- a/src/Common/Hooks/useUrlFilters/useUrlFilters.ts +++ b/src/Common/Hooks/useUrlFilters/useUrlFilters.ts @@ -14,11 +14,12 @@ * limitations under the License. */ -import { useMemo } from 'react' +import { useEffect, useMemo } from 'react' import { useHistory, useLocation } from 'react-router-dom' import { DEFAULT_BASE_PAGE_SIZE, EXCLUDED_FALSY_VALUES, SortingOrder } from '../../Constants' import { DEFAULT_PAGE_NUMBER, URL_FILTER_KEYS } from './constants' import { UseUrlFiltersProps, UseUrlFiltersReturnType } from './types' +import { setItemInLocalStorageIfKeyExists } from './utils' const { PAGE_SIZE, PAGE_NUMBER, SEARCH_KEY, SORT_BY, SORT_ORDER } = URL_FILTER_KEYS @@ -42,11 +43,20 @@ const { PAGE_SIZE, PAGE_NUMBER, SEARCH_KEY, SORT_BY, SORT_ORDER } = URL_FILTER_K const useUrlFilters = ({ initialSortKey, parseSearchParams, + localStorageKey, }: UseUrlFiltersProps = {}): UseUrlFiltersReturnType => { const location = useLocation() const history = useHistory() const searchParams = new URLSearchParams(location.search) + const getParsedSearchParams: UseUrlFiltersProps['parseSearchParams'] = (searchParamsToParse) => { + if (parseSearchParams) { + return parseSearchParams(searchParamsToParse) + } + + return {} as K + } + const { pageSize, pageNumber, searchKey, sortBy, sortOrder, parsedParams } = useMemo(() => { const _pageSize = searchParams.get(PAGE_SIZE) const _pageNumber = searchParams.get(PAGE_NUMBER) @@ -58,7 +68,7 @@ const useUrlFilters = ({ // Fallback to ascending order const sortByOrder = Object.values(SortingOrder).includes(_sortOrder) ? _sortOrder : SortingOrder.ASC - const _parsedParams = parseSearchParams ? parseSearchParams(searchParams) : ({} as K) + const _parsedParams = getParsedSearchParams(searchParams) return { pageSize: Number(_pageSize) || DEFAULT_BASE_PAGE_SIZE, @@ -126,6 +136,7 @@ const useUrlFilters = ({ const clearFilters = () => { history.replace({ search: '' }) + setItemInLocalStorageIfKeyExists(localStorageKey, '') } const updateSearchParams = (paramsToSerialize: Partial) => { @@ -143,10 +154,24 @@ const useUrlFilters = ({ searchParams.delete(key) } }) + // Skipping primary params => pageSize, pageNumber, searchKey, sortBy, sortOrder + setItemInLocalStorageIfKeyExists(localStorageKey, JSON.stringify(getParsedSearchParams(searchParams))) // Not replacing the params as it is being done by _resetPageNumber _resetPageNumber() } + useEffect(() => { + // if we have search string, set secondary params in local storage accordingly + if (location.search) { + localStorage.setItem(localStorageKey, JSON.stringify(parsedParams)) + return + } + const localStorageValue = localStorage.getItem(localStorageKey) + if (localStorageValue) { + updateSearchParams(JSON.parse(localStorageValue)) + } + }, []) + return { pageSize, changePage, diff --git a/src/Common/Hooks/useUrlFilters/utils.tsx b/src/Common/Hooks/useUrlFilters/utils.tsx new file mode 100644 index 000000000..e587cb113 --- /dev/null +++ b/src/Common/Hooks/useUrlFilters/utils.tsx @@ -0,0 +1,5 @@ +export const setItemInLocalStorageIfKeyExists = (localStorageKey: string, value: string) => { + if (localStorageKey) { + localStorage.setItem(localStorageKey, value) + } +} diff --git a/src/Common/SortableTableHeaderCell/SortableTableHeaderCell.tsx b/src/Common/SortableTableHeaderCell/SortableTableHeaderCell.tsx index 622dcd945..8d2f34600 100644 --- a/src/Common/SortableTableHeaderCell/SortableTableHeaderCell.tsx +++ b/src/Common/SortableTableHeaderCell/SortableTableHeaderCell.tsx @@ -15,11 +15,13 @@ */ import { Tooltip } from '@Common/Tooltip' -import { ReactComponent as SortIcon } from '../../Assets/Icon/ic-arrow-up-down.svg' -import { ReactComponent as SortArrowDown } from '../../Assets/Icon/ic-sort-arrow-down.svg' +import Draggable, { DraggableProps } from 'react-draggable' +import { ReactComponent as SortIcon } from '@Icons/ic-arrow-up-down.svg' +import { ReactComponent as SortArrowDown } from '@Icons/ic-sort-arrow-down.svg' import { SortingOrder } from '../Constants' import { noop } from '../Helper' import { SortableTableHeaderCellProps } from './types' +import './sortableTableHeaderCell.scss' /** * Reusable component for the table header cell with support for sorting icons @@ -34,6 +36,23 @@ import { SortableTableHeaderCellProps } from './types' * disabled={isDisabled} * /> * ``` + * + * @example Non-sortable cell + * ```tsx + * + * ``` + * + * * @example Resizable cell (Layout to be controlled externally using useResizableTableConfig) + * ```tsx + * + * ``` */ const SortableTableHeaderCell = ({ isSorted, @@ -43,7 +62,12 @@ const SortableTableHeaderCell = ({ disabled, isSortable = true, showTippyOnTruncate = false, + id, + handleResize, + isResizable, }: SortableTableHeaderCellProps) => { + const isCellResizable = !!(isResizable && id && handleResize) + const renderSortIcon = () => { if (!isSortable) { return null @@ -60,18 +84,46 @@ const SortableTableHeaderCell = ({ return } + const handleDrag: DraggableProps['onDrag'] = (_, data) => { + if (isCellResizable) { + handleResize(id, data.deltaX) + } + } + return ( - +
+ + {isCellResizable && ( + +
+
+
+ + )} +
) } diff --git a/src/Common/SortableTableHeaderCell/constants.ts b/src/Common/SortableTableHeaderCell/constants.ts new file mode 100644 index 000000000..76cc77447 --- /dev/null +++ b/src/Common/SortableTableHeaderCell/constants.ts @@ -0,0 +1,3 @@ +export const DEFAULT_MINIMUM_HEADER_WIDTH = 70 + +export const DEFAULT_MAXIMUM_HEADER_WIDTH = 600 diff --git a/src/Common/SortableTableHeaderCell/index.ts b/src/Common/SortableTableHeaderCell/index.ts index fecef0781..807ff93b2 100644 --- a/src/Common/SortableTableHeaderCell/index.ts +++ b/src/Common/SortableTableHeaderCell/index.ts @@ -15,4 +15,5 @@ */ export { default as SortableTableHeaderCell } from './SortableTableHeaderCell' +export { default as useResizableTableConfig } from './useResizableTableConfig' export type { SortableTableHeaderCellProps } from './types' diff --git a/src/Common/SortableTableHeaderCell/sortableTableHeaderCell.scss b/src/Common/SortableTableHeaderCell/sortableTableHeaderCell.scss new file mode 100644 index 000000000..a2a852f58 --- /dev/null +++ b/src/Common/SortableTableHeaderCell/sortableTableHeaderCell.scss @@ -0,0 +1,14 @@ +.sortable-table-header { + &__resize-btn { + > div { + transition: all 0.1s ease-out; + } + + &:hover, &--dragging { + > div { + height: 100% !important; + background-color: var(--B500); + } + } + } +} diff --git a/src/Common/SortableTableHeaderCell/types.ts b/src/Common/SortableTableHeaderCell/types.ts index 05a52a0c0..15f3f058f 100644 --- a/src/Common/SortableTableHeaderCell/types.ts +++ b/src/Common/SortableTableHeaderCell/types.ts @@ -16,37 +16,78 @@ import { SortingOrder } from '../Constants' -export interface SortableTableHeaderCellProps { - /** - * If true, the cell is sorted - */ - isSorted: boolean - /** - * Callback for handling the sorting of the cell - */ - triggerSorting: () => void - /** - * Current sort order - * - * Note: On click, the sort order should be updated as required - */ - sortOrder: SortingOrder +export type SortableTableHeaderCellProps = { /** * Label for the cell */ title: string - /** - * If true, the cell is disabled - */ - disabled: boolean - /** - * If false, the cell acts like normal table header cell - * @default true - */ - isSortable?: boolean /** * If true, the tippy is shown on Sortable header if text is truncated * @default false */ showTippyOnTruncate?: boolean +} & ( + | { + /** + * Unique identifier for the column + */ + id: string | number + /** + * If true, the cell is resizable + * + * @default false + */ + isResizable: true | boolean + /** + * Resize handler for the table + */ + handleResize: (id: string | number, deltaChange: number) => void + } + | { + id?: never + isResizable?: false | undefined + handleResize?: never + } +) & + ( + | { + /** + * If false, the cell acts like normal table header cell + * @default true + */ + isSortable?: boolean | undefined + /** + * If true, the cell is disabled + */ + disabled: boolean + /** + * If true, the cell is sorted + */ + isSorted: boolean + /** + * Callback for handling the sorting of the cell + */ + triggerSorting: () => void + /** + * Current sort order + * + * Note: On click, the sort order should be updated as required + */ + sortOrder: SortingOrder + } + | { + isSortable: false + disabled?: never + isSorted?: never + triggerSorting?: never + sortOrder?: never + } + ) + +export interface UseResizableTableConfigProps { + headersConfig: (Pick & { + width: number | string + maxWidth?: number + minWidth?: number + })[] } diff --git a/src/Common/SortableTableHeaderCell/useResizableTableConfig.tsx b/src/Common/SortableTableHeaderCell/useResizableTableConfig.tsx new file mode 100644 index 000000000..183f9fe3a --- /dev/null +++ b/src/Common/SortableTableHeaderCell/useResizableTableConfig.tsx @@ -0,0 +1,54 @@ +import { useEffect, useState } from 'react' +import { UseResizableTableConfigProps } from './types' +import { DEFAULT_MAXIMUM_HEADER_WIDTH, DEFAULT_MINIMUM_HEADER_WIDTH } from './constants' + +const useResizableTableConfig = ({ headersConfig }: UseResizableTableConfigProps) => { + const [headerDimensionsConfig, setHeaderDimensionsConfig] = useState< + UseResizableTableConfigProps['headersConfig'][number]['width'][] + >([]) + + useEffect(() => { + setHeaderDimensionsConfig(headersConfig.map((config) => config.width)) + }, [JSON.stringify(headersConfig)]) + + const handleResize = ( + headerCellId: UseResizableTableConfigProps['headersConfig'][number]['id'], + deltaChange: number, + ) => { + const headerCellIndexInConfig = headersConfig.findIndex((config) => config.id === headerCellId) + + if (headerCellIndexInConfig < 0) { + return + } + + setHeaderDimensionsConfig((prev) => { + const updatedHeaderDimensionsConfig = structuredClone(prev) + // Only numbers are supported for v1 + if (typeof updatedHeaderDimensionsConfig[headerCellIndexInConfig] !== 'number') { + return prev + } + + const updatedCellDimension = updatedHeaderDimensionsConfig[headerCellIndexInConfig] + deltaChange + const currentHeaderCellConfig = headersConfig[headerCellIndexInConfig] + + if ( + updatedCellDimension < (currentHeaderCellConfig.minWidth ?? DEFAULT_MINIMUM_HEADER_WIDTH) || + updatedCellDimension > (currentHeaderCellConfig.maxWidth ?? DEFAULT_MAXIMUM_HEADER_WIDTH) + ) { + return prev + } + + updatedHeaderDimensionsConfig[headerCellIndexInConfig] = updatedCellDimension + return updatedHeaderDimensionsConfig + }) + } + + return { + gridTemplateColumns: headerDimensionsConfig + .map((config) => (typeof config === 'number' ? `${config}px` : config)) + .join(' '), + handleResize, + } +} + +export default useResizableTableConfig diff --git a/src/Shared/Components/CICDHistory/AppStatusDetailsChart.tsx b/src/Shared/Components/CICDHistory/AppStatusDetailsChart.tsx index b704befad..34010f661 100644 --- a/src/Shared/Components/CICDHistory/AppStatusDetailsChart.tsx +++ b/src/Shared/Components/CICDHistory/AppStatusDetailsChart.tsx @@ -14,48 +14,70 @@ * limitations under the License. */ -import { useMemo, useState } from 'react' +import { useEffect, 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, ALL_RESOURCE_KIND_FILTER } 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 [currentFilter, setCurrentFilter] = useState(ALL_RESOURCE_KIND_FILTER) + const [flattenedNodes, setFlattenedNodes] = 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], ) - const nodesKeyArray = Object.keys(nodes?.nodes || {}) - let flattenedNodes = [] - if (nodesKeyArray.length > 0) { - for (let index = 0; index < nodesKeyArray.length; index++) { - const element = nodes.nodes[nodesKeyArray[index]] - // eslint-disable-next-line no-loop-func - element.forEach((childElement) => { - if (childElement.health) { - flattenedNodes.push(childElement) - } - }) - } - flattenedNodes.sort( - (a, b) => - STATUS_SORTING_ORDER[a.health.status?.toLowerCase()] - - STATUS_SORTING_ORDER[b.health.status?.toLowerCase()], - ) - if (filterRemoveHealth) { - flattenedNodes = flattenedNodes.filter( - (node) => node.health.status?.toLowerCase() !== DEPLOYMENT_STATUS.HEALTHY, + useEffect(() => { + const nodesKeyArray = Object.keys(nodes?.nodes || {}) + let newFlattenedNodes = [] + if (nodesKeyArray.length > 0) { + for (let index = 0; index < nodesKeyArray.length; index++) { + const element = nodes.nodes[nodesKeyArray[index]] + // eslint-disable-next-line no-loop-func + element.forEach((childElement) => { + if (childElement.health) { + newFlattenedNodes.push(childElement) + } + }) + } + newFlattenedNodes.sort( + (a, b) => + STATUS_SORTING_ORDER[a.health.status?.toLowerCase()] - + STATUS_SORTING_ORDER[b.health.status?.toLowerCase()], ) + + if (filterRemoveHealth) { + newFlattenedNodes = newFlattenedNodes.filter( + (node) => node.health.status?.toLowerCase() !== DEPLOYMENT_STATUS.HEALTHY, + ) + } + + setFlattenedNodes(newFlattenedNodes) } - } + }, [`${nodes}`]) function getNodeMessage(kind: string, name: string) { if ( @@ -80,7 +102,11 @@ const AppStatusDetailsChart = ({ filterRemoveHealth = false, showFooter }: AppSt
- +
@@ -99,7 +125,8 @@ const AppStatusDetailsChart = ({ filterRemoveHealth = false, showFooter }: AppSt flattenedNodes .filter( (nodeDetails) => - currentFilter === 'all' || + currentFilter === ALL_RESOURCE_KIND_FILTER || + (currentFilter === NodeFilters.drifted && nodeDetails.hasDrift) || nodeDetails.health.status?.toLowerCase() === currentFilter, ) .map((nodeDetails) => ( @@ -123,7 +150,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/History.components.tsx b/src/Shared/Components/CICDHistory/History.components.tsx index 5fa8b4e81..282be6114 100644 --- a/src/Shared/Components/CICDHistory/History.components.tsx +++ b/src/Shared/Components/CICDHistory/History.components.tsx @@ -34,7 +34,7 @@ import { ReactComponent as ZoomOut } from '../../../Assets/Icon/ic-exit-fullscre import './cicdHistory.scss' export const LogResizeButton = ({ - shortcutCombo = ['F'], + shortcutCombo = ['Shift', 'F'], showOnlyWhenPathIncludesLogs = true, fullScreenView, setFullScreenView, diff --git a/src/Shared/Components/CICDHistory/StatusFilterButtonComponent.tsx b/src/Shared/Components/CICDHistory/StatusFilterButtonComponent.tsx index 0ece827ea..f4550311b 100644 --- a/src/Shared/Components/CICDHistory/StatusFilterButtonComponent.tsx +++ b/src/Shared/Components/CICDHistory/StatusFilterButtonComponent.tsx @@ -15,27 +15,28 @@ */ /* eslint-disable eqeqeq */ -import { useEffect, useState } from 'react' +import { ALL_RESOURCE_KIND_FILTER } from '@Shared/constants' import { ReactComponent as ICCaretDown } from '@Icons/ic-caret-down.svg' import { PopupMenu, StyledRadioGroup as RadioGroup } from '../../../Common' -import { NodeStatus, StatusFilterButtonType } from './types' -import { IndexStore } from '../../Store' - +import { NodeFilters, NodeStatus, StatusFilterButtonType } from './types' import './StatusFilterButtonComponent.scss' -export const StatusFilterButtonComponent = ({ nodes, handleFilterClick }: StatusFilterButtonType) => { - const [selectedTab, setSelectedTab] = useState('all') - +export const StatusFilterButtonComponent = ({ nodes, selectedTab, handleFilterClick }: StatusFilterButtonType) => { const maxInlineFilterCount = 4 let allNodeCount: number = 0 let healthyNodeCount: number = 0 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) { @@ -48,8 +49,12 @@ export const StatusFilterButtonComponent = ({ nodes, handleFilterClick }: Status allNodeCount += 1 }) + const handleInlineFilterClick = (e) => { + handleFilterClick(e.target.value) + } + const filterOptions = [ - { status: 'all', count: allNodeCount, isSelected: selectedTab == 'all' }, + { status: ALL_RESOURCE_KIND_FILTER, count: allNodeCount, isSelected: selectedTab == ALL_RESOURCE_KIND_FILTER }, { status: NodeStatus.Missing, count: missingNodeCount, isSelected: NodeStatus.Missing == selectedTab }, { status: NodeStatus.Degraded, count: failedNodeCount, isSelected: NodeStatus.Degraded == selectedTab }, { @@ -58,6 +63,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( @@ -67,27 +77,6 @@ export const StatusFilterButtonComponent = ({ nodes, handleFilterClick }: Status const overflowFilters = validFilterOptions.length > maxInlineFilterCount ? validFilterOptions.slice(maxInlineFilterCount) : null - useEffect(() => { - if ( - (selectedTab === NodeStatus.Healthy && healthyNodeCount === 0) || - (selectedTab === NodeStatus.Degraded && failedNodeCount === 0) || - (selectedTab === NodeStatus.Progressing && progressingNodeCount === 0) || - (selectedTab === NodeStatus.Missing && missingNodeCount === 0) - ) { - setSelectedTab('all') - } else if (handleFilterClick) { - handleFilterClick(selectedTab) - } else { - IndexStore.updateFilterType(selectedTab.toUpperCase()) - } - }, [nodes, selectedTab]) - - const handleTabSwitch = (event): void => { - setSelectedTab(event.target.value) - } - - const handleMenuOptionClick = (status: string) => () => setSelectedTab(status) - const renderOverflowFilters = () => overflowFilters ? ( @@ -103,7 +92,7 @@ export const StatusFilterButtonComponent = ({ nodes, handleFilterClick }: Status key={filter.status} type="button" className={`dc__transparent w-100 py-6 px-8 flex left dc__gap-8 fs-13 lh-20 fw-4 cn-9 ${filter.isSelected ? 'bcb-1' : 'bcn-0 dc__hover-n50'}`} - onClick={handleMenuOptionClick(filter.status)} + onClick={() => handleFilterClick(filter.status)} > {displayedInlineFilters.map((filter, index) => ( void } export interface StatusFilterButtonType { nodes: Array + selectedTab: string handleFilterClick?: (selectedFilter: string) => void } @@ -535,6 +538,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/Components/SelectPicker/FilterSelectPicker.tsx b/src/Shared/Components/SelectPicker/FilterSelectPicker.tsx index b3c0be39b..314c97492 100644 --- a/src/Shared/Components/SelectPicker/FilterSelectPicker.tsx +++ b/src/Shared/Components/SelectPicker/FilterSelectPicker.tsx @@ -5,8 +5,10 @@ import { useEffect, useMemo, useState } from 'react' import { ReactComponent as ICFilter } from '@Icons/ic-filter.svg' import { ReactComponent as ICFilterApplied } from '@Icons/ic-filter-applied.svg' +import { ComponentSizeType } from '@Shared/constants' import SelectPicker from './SelectPicker.component' import { FilterSelectPickerProps, SelectPickerOptionType, SelectPickerProps } from './type' +import { Button } from '../Button' const FilterSelectPicker = ({ appliedFilterOptions, @@ -58,14 +60,13 @@ const FilterSelectPicker = ({ return (
- + size={ComponentSizeType.small} + fullWidth + />
) } diff --git a/src/Shared/Components/SelectPicker/type.ts b/src/Shared/Components/SelectPicker/type.ts index 38eef3603..8506813c3 100644 --- a/src/Shared/Components/SelectPicker/type.ts +++ b/src/Shared/Components/SelectPicker/type.ts @@ -266,6 +266,7 @@ export interface FilterSelectPickerProps | 'shouldMenuAlignRight' | 'optionListError' | 'reloadOptionList' + | 'isOptionDisabled' > { appliedFilterOptions: SelectPickerOptionType[] handleApplyFilter: (filtersToApply: SelectPickerOptionType[]) => void 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/constants.tsx b/src/Shared/constants.tsx index 0b4f098e9..f8e8ca04b 100644 --- a/src/Shared/constants.tsx +++ b/src/Shared/constants.tsx @@ -497,3 +497,5 @@ export const DEFAULT_LOCKED_KEYS_CONFIG: Readonly = { config: [], allowed: false, } + +export const ALL_RESOURCE_KIND_FILTER = 'all' 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 2ea131b96..a5464716a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -82,6 +82,7 @@ export interface customEnv { * @default false */ FEATURE_HIDE_USER_DIRECT_PERMISSIONS_FOR_NON_SUPER_ADMINS?: boolean + FEATURE_CONFIG_DRIFT_ENABLE: boolean FEATURE_PROMO_EMBEDDED_BUTTON_TEXT?: string FEATURE_PROMO_EMBEDDED_MODAL_TITLE?: string FEATURE_PROMO_EMBEDDED_IFRAME_URL?: string