diff --git a/package.json b/package.json index a7160e27e4..4cc27ce4eb 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "homepage": "/dashboard", "dependencies": { - "@devtron-labs/devtron-fe-common-lib": "0.6.0-patch-1-beta-7", + "@devtron-labs/devtron-fe-common-lib": "0.6.0-patch-1-beta-10", "@esbuild-plugins/node-globals-polyfill": "0.2.3", "@rjsf/core": "^5.13.3", "@rjsf/utils": "^5.13.3", diff --git a/src/Pages/ResourceBrowser/ClusterList/ClusterMap/ClusterMap.tsx b/src/Pages/ResourceBrowser/ClusterList/ClusterMap/ClusterMap.tsx new file mode 100644 index 0000000000..fd4c58dbe8 --- /dev/null +++ b/src/Pages/ResourceBrowser/ClusterList/ClusterMap/ClusterMap.tsx @@ -0,0 +1,96 @@ +import { Link } from 'react-router-dom' +import { ResponsiveContainer, Treemap, TreemapProps } from 'recharts' +import { followCursor } from 'tippy.js' + +import { ClusterStatusType, ConditionalWrap, Tooltip } from '@devtron-labs/devtron-fe-common-lib' + +import { getVisibleSvgTextWithEllipsis } from './utils' +import { ClusterMapProps } from './types' + +import './clustermap.scss' + +const renderWithLink = (href: string) => (children: JSX.Element) => ( + + {children} + +) + +const ClusterTreeMapContent = ({ + x, + y, + width, + height, + status, + name, + value, + href, +}: TreemapProps['content']['props']) => ( + + +
+ {name} + {`${value} Nodes`} +
+ + {status} + + + } + followCursor + plugins={[followCursor]} + > + + + + {getVisibleSvgTextWithEllipsis({ text: name, maxWidth: width, fontSize: 13, fontWeight: 600 })} + + + {value} + + +
+
+) + +export const ClusterMap = ({ treeMapData = [], isLoading = false }: ClusterMapProps) => + treeMapData.length ? ( +
+
+ {isLoading ? ( +
+
+
+
+ ) : ( + treeMapData.map(({ id, label, data }) => ( +
+ {label && ( + +

{label}

+
+ )} +
+ + } + isAnimationActive={false} + /> + +
+
+ )) + )} +
+
+ ) : null diff --git a/src/Pages/ResourceBrowser/ClusterList/ClusterMap/clustermap.scss b/src/Pages/ResourceBrowser/ClusterList/ClusterMap/clustermap.scss new file mode 100644 index 0000000000..905ad97709 --- /dev/null +++ b/src/Pages/ResourceBrowser/ClusterList/ClusterMap/clustermap.scss @@ -0,0 +1,35 @@ +.cluster-map { + $parent-selector: &; + + &__container { + height: 165px; + } + + &__bar:hover { + #{$parent-selector}__rect { + fill: var(--G400); + + &--unhealthy { + fill: var(--R400); + } + } + + #{$parent-selector}__text { + fill: var(--N0); + } + } + + &__rect { + stroke: var(--N0); + stroke-width: 2; + fill: var(--G200); + + &--unhealthy { + fill: var(--R200); + } + } + + &__text { + stroke-width: 0; + } +} diff --git a/src/Pages/ResourceBrowser/ClusterList/ClusterMap/index.ts b/src/Pages/ResourceBrowser/ClusterList/ClusterMap/index.ts new file mode 100644 index 0000000000..6f0b565fbd --- /dev/null +++ b/src/Pages/ResourceBrowser/ClusterList/ClusterMap/index.ts @@ -0,0 +1,2 @@ +export * from './ClusterMap' +export * from './types' diff --git a/src/Pages/ResourceBrowser/ClusterList/ClusterMap/types.ts b/src/Pages/ResourceBrowser/ClusterList/ClusterMap/types.ts new file mode 100644 index 0000000000..9f48bd5358 --- /dev/null +++ b/src/Pages/ResourceBrowser/ClusterList/ClusterMap/types.ts @@ -0,0 +1,19 @@ +import { ClusterStatusType } from '@devtron-labs/devtron-fe-common-lib' + +interface MapData { + name: string + value: number + status: Extract + href?: string +} + +export interface ClusterTreeMapData { + id: number + label?: string + data: MapData[] +} + +export interface ClusterMapProps { + isLoading?: boolean + treeMapData: ClusterTreeMapData[] +} diff --git a/src/Pages/ResourceBrowser/ClusterList/ClusterMap/utils.ts b/src/Pages/ResourceBrowser/ClusterList/ClusterMap/utils.ts new file mode 100644 index 0000000000..1c230d2e71 --- /dev/null +++ b/src/Pages/ResourceBrowser/ClusterList/ClusterMap/utils.ts @@ -0,0 +1,58 @@ +const createMeasurementSvg = (fontSize: number, fontWeight: number) => { + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg') + svg.setAttribute('width', '0') + svg.setAttribute('height', '0') + // Hide it from view + svg.style.position = 'absolute' + + const textElement = document.createElementNS('http://www.w3.org/2000/svg', 'text') + textElement.setAttribute('x', '0') + textElement.setAttribute('y', '0') + textElement.setAttribute('font-size', `${fontSize}px`) + textElement.setAttribute('font-weight', `${fontWeight}`) + textElement.setAttribute( + 'font-family', + "'Open Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif", + ) + + svg.appendChild(textElement) + document.body.appendChild(svg) + + return { svg, textElement } +} + +export const getVisibleSvgTextWithEllipsis = (() => { + let svgInstance: SVGSVGElement | null = null + let textElementInstance: SVGTextElement | null = null + + return ({ text = '', maxWidth = 0, fontSize = 16, fontWeight = 400 }) => { + if (!svgInstance || !textElementInstance) { + const { svg, textElement } = createMeasurementSvg(fontSize, fontWeight) + svgInstance = svg + textElementInstance = textElement + } + + const textElement = textElementInstance + textElement.textContent = '...' + const ellipsisWidth = textElement.getBBox().width + + let start = 0 + let end = text.length + + while (start < end) { + const mid = Math.floor((start + end) / 2) + textElement.textContent = text.slice(0, mid + 1) + + const currentWidth = textElement.getBBox().width + if (currentWidth + ellipsisWidth > maxWidth - 8) { + end = mid + } else { + start = mid + 1 + } + } + + const visibleText = text.slice(0, start) + (start < text.length ? '...' : '') + + return visibleText + } +})() diff --git a/src/Pages/ResourceBrowser/ClusterList/index.ts b/src/Pages/ResourceBrowser/ClusterList/index.ts new file mode 100644 index 0000000000..6b83e86ba6 --- /dev/null +++ b/src/Pages/ResourceBrowser/ClusterList/index.ts @@ -0,0 +1 @@ +export * from './ClusterMap' diff --git a/src/Pages/ResourceBrowser/index.ts b/src/Pages/ResourceBrowser/index.ts new file mode 100644 index 0000000000..771c6b4ca1 --- /dev/null +++ b/src/Pages/ResourceBrowser/index.ts @@ -0,0 +1 @@ +export * from './ClusterList' diff --git a/src/components/ClusterNodes/ClusterSelectionList.tsx b/src/components/ClusterNodes/ClusterSelectionList.tsx index 6faab29d45..cf6ee53fbf 100644 --- a/src/components/ClusterNodes/ClusterSelectionList.tsx +++ b/src/components/ClusterNodes/ClusterSelectionList.tsx @@ -16,7 +16,14 @@ import React, { useState, useMemo } from 'react' import { useHistory, useLocation, Link } from 'react-router-dom' -import { GenericEmptyState, SearchBar, useUrlFilters, Tooltip } from '@devtron-labs/devtron-fe-common-lib' +import { + GenericEmptyState, + SearchBar, + useUrlFilters, + Tooltip, + ClusterFiltersType, + ClusterStatusType, +} from '@devtron-labs/devtron-fe-common-lib' import dayjs, { Dayjs } from 'dayjs' import { importComponentFromFELibrary } from '@Components/common' import Timer from '@Components/common/DynamicTabs/DynamicTabs.timer' @@ -25,15 +32,31 @@ import { AddClusterButton } from '@Components/ResourceBrowser/PageHeader.buttons import { ReactComponent as Error } from '@Icons/ic-error-exclamation.svg' import { ReactComponent as Success } from '@Icons/appstatus/healthy.svg' import { ReactComponent as TerminalIcon } from '@Icons/ic-terminal-fill.svg' +import { ClusterMap, ClusterTreeMapData } from '@Pages/ResourceBrowser' import { ClusterDetail } from './types' import ClusterNodeEmptyState from './ClusterNodeEmptyStates' import { ClusterSelectionType } from '../ResourceBrowser/Types' import { AppDetailsTabs } from '../v2/appDetails/appDetails.store' import { ALL_NAMESPACE_OPTION, K8S_EMPTY_GROUP, SIDEBAR_KEYS } from '../ResourceBrowser/Constants' import { URLS } from '../../config' +import { ClusterStatusByFilter } from './constants' import './clusterNodes.scss' const KubeConfigButton = importComponentFromFELibrary('KubeConfigButton', null, 'function') +const ClusterStatusCell = importComponentFromFELibrary('ClusterStatus', null, 'function') +const ClusterFilters = importComponentFromFELibrary('ClusterFilters', null, 'function') + +const getClusterMapData = (data: ClusterDetail[]): ClusterTreeMapData['data'] => + data.map(({ name, id, nodeCount, status }) => ({ + name, + status: status as ClusterTreeMapData['data'][0]['status'], + href: `${URLS.RESOURCE_BROWSER}/${id}/${ALL_NAMESPACE_OPTION.value}/${SIDEBAR_KEYS.nodeGVK.Kind.toLowerCase()}/${K8S_EMPTY_GROUP}`, + value: nodeCount ?? 0, + })) + +const parseSearchParams = (searchParams: URLSearchParams) => ({ + clusterFilter: (searchParams.get('clusterFilter') as ClusterFiltersType) || ClusterFiltersType.ALL_CLUSTERS, +}) const ClusterSelectionList: React.FC = ({ clusterOptions, @@ -46,12 +69,63 @@ const ClusterSelectionList: React.FC = ({ const history = useHistory() const [lastSyncTime, setLastSyncTime] = useState(dayjs()) - const { searchKey, handleSearch, clearFilters } = useUrlFilters() + const { searchKey, clusterFilter, updateSearchParams, handleSearch, clearFilters } = useUrlFilters< + void, + { clusterFilter: ClusterFiltersType } + >({ parseSearchParams }) const filteredList = useMemo(() => { const loweredSearchKey = searchKey.toLowerCase() - return clusterOptions.filter((option) => !searchKey || option.name.toLowerCase().includes(loweredSearchKey)) - }, [searchKey, clusterOptions]) + return clusterOptions.filter((option) => { + const filterCondition = + clusterFilter === ClusterFiltersType.ALL_CLUSTERS || + !option.status || + option.status === ClusterStatusByFilter[clusterFilter] + + return (!searchKey || option.name.toLowerCase().includes(loweredSearchKey)) && filterCondition + }) + }, [searchKey, clusterOptions, `${clusterFilter}`]) + + const treeMapData = useMemo(() => { + const { prodClusters, nonProdClusters } = filteredList.reduce( + (acc, curr) => { + if (curr.status && curr.status !== ClusterStatusType.CONNECTION_FAILED) { + if (curr.isProd) { + acc.prodClusters.push(curr) + } else { + acc.nonProdClusters.push(curr) + } + } + + return acc + }, + { prodClusters: [], nonProdClusters: [] }, + ) + + const productionClustersData = getClusterMapData(prodClusters) + const nonProductionClustersData = getClusterMapData(nonProdClusters) + + return [ + ...(productionClustersData.length + ? [ + { + id: 0, + label: 'Production Clusters', + data: productionClustersData, + }, + ] + : []), + ...(nonProductionClustersData.length + ? [ + { + id: 1, + label: 'Non-Production Clusters', + data: nonProductionClustersData, + }, + ] + : []), + ] + }, [filteredList]) const handleFilterKeyPress = (value: string) => { handleSearch(value) @@ -62,6 +136,10 @@ const ClusterSelectionList: React.FC = ({ setLastSyncTime(dayjs()) } + const setClusterFilter = (_clusterFilter: ClusterFiltersType) => { + updateSearchParams({ clusterFilter: _clusterFilter }) + } + const getOpenTerminalHandler = (clusterData) => () => history.push(`${location.pathname}/${clusterData.id}/all/${AppDetailsTabs.terminal}/${K8S_EMPTY_GROUP}`) @@ -72,6 +150,28 @@ const ClusterSelectionList: React.FC = ({ return value } + const renderClusterStatus = ({ errorInNodeListing, status }: ClusterDetail) => { + if (ClusterStatusCell && status) { + return + } + + return ( +
+ {errorInNodeListing ? ( + <> + + Failed + + ) : ( + <> + + Connected + + )} +
+ ) + } + const renderClusterRow = (clusterData: ClusterDetail): JSX.Element => { const errorCount = clusterData.nodeErrors ? Object.keys(clusterData.nodeErrors).length : 0 return ( @@ -109,20 +209,11 @@ const ClusterSelectionList: React.FC = ({ alwaysShowTippyOnHover={!!clusterData.errorInNodeListing} content={clusterData.errorInNodeListing} > -
- {clusterData.errorInNodeListing ? ( - <> - - Failed - - ) : ( - <> - - Successful - - )} -
+ {renderClusterStatus(clusterData)} +
+ {hideDataOnLoad(clusterData.isProd ? 'Production' : 'Non Production')} +
{hideDataOnLoad(clusterData.nodeCount)}
{errorCount > 0 && @@ -158,19 +249,24 @@ const ClusterSelectionList: React.FC = ({ } return ( -
+
- -
+
+ + {ClusterFilters && ( + + )} +
+
{clusterListLoader ? ( Syncing ) : ( @@ -192,24 +288,26 @@ const ClusterSelectionList: React.FC = ({ )}
-
-
-
Cluster
-
Connection status
-
Nodes
-
NODE Errors
-
K8S version
-
CPU Capacity
-
Memory Capacity
+ + {!filteredList.length ? ( +
+
- {!filteredList.length ? ( -
- + ) : ( +
+
+
Cluster
+
Status
+
Type
+
Nodes
+
NODE Errors
+
K8S version
+
CPU Capacity
+
Memory Capacity
- ) : ( - filteredList.map((clusterData) => renderClusterRow(clusterData)) - )} -
+ {filteredList.map((clusterData) => renderClusterRow(clusterData))} +
+ )}
) } diff --git a/src/components/ClusterNodes/clusterNodes.scss b/src/components/ClusterNodes/clusterNodes.scss index 84e8944aeb..e290ec3834 100644 --- a/src/components/ClusterNodes/clusterNodes.scss +++ b/src/components/ClusterNodes/clusterNodes.scss @@ -17,7 +17,7 @@ .cluster-list-main-container { .cluster-list-row { display: grid; - grid-template-columns: auto 150px 50px 90px 100px 100px 120px; + grid-template-columns: auto 150px 100px 50px 90px 100px 100px 120px; column-gap: 16px; .cluster-status { height: 12px; diff --git a/src/components/ClusterNodes/constants.ts b/src/components/ClusterNodes/constants.ts index b0ac37405d..8a0083072f 100644 --- a/src/components/ClusterNodes/constants.ts +++ b/src/components/ClusterNodes/constants.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { ClusterFiltersType, ClusterStatusType } from '@devtron-labs/devtron-fe-common-lib' + import { multiSelectStyles } from '../v2/common/ReactSelectCustomization' import { ColumnMetadataType, EFFECT_TYPE } from './types' @@ -484,3 +486,9 @@ export const defaultManifestErrorText = "# Please edit the object below. Lines beginning with a '#' will be ignored,\n# and an empty file will abort the edit. If an error occurs while saving this file will be\n# reopened with the relevant failures.\n# \n" export const manifestCommentsRegex = /^(.*?apiVersion:)/s + +export const ClusterStatusByFilter: Record = { + [ClusterFiltersType.HEALTHY]: ClusterStatusType.HEALTHY, + [ClusterFiltersType.UNHEALTHY]: ClusterStatusType.UNHEALTHY, + [ClusterFiltersType.ALL_CLUSTERS]: null, +} diff --git a/src/components/ClusterNodes/types.ts b/src/components/ClusterNodes/types.ts index c6e146f9d9..afebbca9af 100644 --- a/src/components/ClusterNodes/types.ts +++ b/src/components/ClusterNodes/types.ts @@ -16,7 +16,12 @@ import React from 'react' import { MultiValue } from 'react-select' -import { ResponseType, ApiResourceGroupType, K8sResourceDetailDataType } from '@devtron-labs/devtron-fe-common-lib' +import { + ResponseType, + ApiResourceGroupType, + ClusterStatusType, + K8sResourceDetailDataType, +} from '@devtron-labs/devtron-fe-common-lib' import { LabelTag, OptionType } from '../app/types' import { CLUSTER_PAGE_TAB, NODE_SEARCH_TEXT } from './constants' import { EditModeType } from '../v2/appDetails/k8Resource/nodeDetail/NodeDetailTabs/terminal/constants' @@ -74,6 +79,8 @@ export interface ClusterCapacityType { serverVersion: string nodeDetails?: NodeDetailsType[] nodeErrors: Record[] + status?: ClusterStatusType + isProd: boolean } export interface ClusterDetail extends ClusterCapacityType { diff --git a/src/components/ResourceBrowser/Constants.ts b/src/components/ResourceBrowser/Constants.ts index e5270b8bf8..1e47313a6b 100644 --- a/src/components/ResourceBrowser/Constants.ts +++ b/src/components/ResourceBrowser/Constants.ts @@ -14,9 +14,10 @@ * limitations under the License. */ -import { GVKType, Nodes } from '@devtron-labs/devtron-fe-common-lib' +import { Nodes } from '@devtron-labs/devtron-fe-common-lib' import { AggregationKeys, AggregationKeysType } from '../app/types' import { multiSelectStyles } from '../v2/common/ReactSelectCustomization' +import { RBSidebarKeysType } from './Types' export const FILTER_SELECT_COMMON_STYLES = { ...multiSelectStyles, @@ -174,15 +175,7 @@ export const DELETE_MODAL_MESSAGING = { checkboxText: 'Force delete resource', } -export const SIDEBAR_KEYS: { - nodes: string - events: string - namespaces: string - eventGVK: GVKType - namespaceGVK: GVKType - nodeGVK: GVKType - overviewGVK: GVKType -} = { +export const SIDEBAR_KEYS: RBSidebarKeysType = { nodes: 'Nodes', events: 'Events', namespaces: 'Namespaces', @@ -204,7 +197,12 @@ export const SIDEBAR_KEYS: { overviewGVK: { Group: '', Version: '', - Kind: Nodes.Overview as Nodes, + Kind: Nodes.Overview, + }, + monitoringGVK: { + Group: '', + Version: '', + Kind: Nodes.MonitoringDashboard, }, } diff --git a/src/components/ResourceBrowser/ResourceBrowser.tsx b/src/components/ResourceBrowser/ResourceBrowser.tsx index 8edff6cce1..19f7079aa9 100644 --- a/src/components/ResourceBrowser/ResourceBrowser.tsx +++ b/src/components/ResourceBrowser/ResourceBrowser.tsx @@ -59,12 +59,7 @@ const ResourceBrowser: React.FC = () => { const sortedClusterList: ClusterDetail[] = useMemo( () => - ( - sortObjectArrayAlphabetically( - detailClusterList?.result || clusterListMinData?.result || [], - 'name', - ) as ClusterDetail[] - ).filter( + sortObjectArrayAlphabetically(detailClusterList?.result || clusterListMinData?.result || [], 'name').filter( (option) => !(window._env_.HIDE_DEFAULT_CLUSTER && option.id === DEFAULT_CLUSTER_ID) && !option.isVirtualCluster, @@ -95,7 +90,7 @@ const ResourceBrowser: React.FC = () => { } return ( -
+
= ({ onChange, clusterList, const defaultOption = filteredClusterList.find((item) => String(item.value) === clusterId) return ( - +
+ + {defaultOption?.isProd && ( + Production + )} +
) } diff --git a/src/components/ResourceBrowser/ResourceList/ResourceList.tsx b/src/components/ResourceBrowser/ResourceList/ResourceList.tsx index 29e8ef4d6d..6e1b1fb8d2 100644 --- a/src/components/ResourceBrowser/ResourceList/ResourceList.tsx +++ b/src/components/ResourceBrowser/ResourceList/ResourceList.tsx @@ -28,14 +28,14 @@ import { getResourceGroupListRaw, noop, } from '@devtron-labs/devtron-fe-common-lib' -import { ClusterOptionType, FIXED_TABS_INDICES, URLParams } from '../Types' +import { ClusterOptionType, URLParams } from '../Types' import { ALL_NAMESPACE_OPTION, K8S_EMPTY_GROUP, SIDEBAR_KEYS } from '../Constants' import { URLS } from '../../../config' -import { convertToOptionsList, sortObjectArrayAlphabetically } from '../../common' +import { convertToOptionsList, importComponentFromFELibrary, sortObjectArrayAlphabetically } from '../../common' import { AppDetailsTabs, AppDetailsTabsIdPrefix } from '../../v2/appDetails/appDetails.store' import NodeDetailComponent from '../../v2/appDetails/k8Resource/nodeDetail/NodeDetail.component' import { DynamicTabs, useTabs } from '../../common/DynamicTabs' -import { getTabsBasedOnRole } from '../Utils' +import { getFixedTabIndices, getTabsBasedOnRole } from '../Utils' import { getClusterListMin } from '../../ClusterNodes/clusterNodes.service' import ClusterSelector from './ClusterSelector' import ClusterOverview from '../../ClusterNodes/ClusterOverview' @@ -46,6 +46,8 @@ import AdminTerminal from './AdminTerminal' import { renderRefreshBar } from './ResourceList.component' import { renderCreateResourceButton } from '../PageHeader.buttons' +const MonitoringDashboard = importComponentFromFELibrary('MonitoringDashboard', null, 'function') + const ResourceList = () => { const { clusterId, namespace, nodeType, node, group } = useParams() const { replace } = useHistory() @@ -76,14 +78,15 @@ const ResourceList = () => { const clusterList = clusterListData?.result || null - const clusterOptions: ClusterOptionType[] = useMemo( + const clusterOptions = useMemo( () => clusterList && (convertToOptionsList( - sortObjectArrayAlphabetically(clusterList, 'name'), + sortObjectArrayAlphabetically(clusterList, 'name').filter(({ isVirtualCluster }) => !isVirtualCluster), 'name', 'id', 'nodeErrors', + 'isProd', ) as ClusterOptionType[]), [clusterList], ) @@ -95,6 +98,7 @@ const ResourceList = () => { label: '', value: clusterId, errorInConnecting: '', + isProd: false, }, [clusterId, clusterOptions], ) @@ -102,7 +106,9 @@ const ResourceList = () => { const isSuperAdmin = !!userRole?.result.superAdmin const isOverviewNodeType = nodeType === SIDEBAR_KEYS.overviewGVK.Kind.toLowerCase() + const isMonitoringNodeType = nodeType === SIDEBAR_KEYS.monitoringGVK.Kind.toLowerCase() const isTerminalNodeType = nodeType === AppDetailsTabs.terminal + const fixedTabIndices = getFixedTabIndices() const getDynamicTabData = () => { const isNodeTypeEvent = nodeType === SIDEBAR_KEYS.eventGVK.Kind.toLowerCase() @@ -127,15 +133,16 @@ const ResourceList = () => { const initTabsBasedOnRole = (reInit: boolean) => { /* NOTE: selectedCluster is not in useEffect dep list since it arrives with isSuperAdmin (Promise.all) */ - const _tabs = getTabsBasedOnRole( + const _tabs = getTabsBasedOnRole({ selectedCluster, namespace, isSuperAdmin, /* NOTE: if node is available in url but no associated dynamicTab we create a dynamicTab */ - node && getDynamicTabData(), - isTerminalNodeType, - isOverviewNodeType, - ) + dynamicTabData: node && getDynamicTabData(), + isTerminalSelected: isTerminalNodeType, + isOverviewSelected: isOverviewNodeType, + isMonitoringDashBoardSelected: isMonitoringNodeType, + }) initTabs( _tabs, reInit, @@ -166,21 +173,33 @@ const ResourceList = () => { addTab(idPrefix, kind, name, _url).then(noop).catch(noop) return } + + // These checks are wrong since tabs are sorted by position so index is not fixed /* NOTE: it is unlikely that tabs is empty when this is called but it can happen */ if (isOverviewNodeType) { - if (tabs[FIXED_TABS_INDICES.OVERVIEW] && !tabs[FIXED_TABS_INDICES.OVERVIEW].isSelected) { - markTabActiveById(tabs[FIXED_TABS_INDICES.OVERVIEW].id) + if (tabs[fixedTabIndices.OVERVIEW] && !tabs[fixedTabIndices.OVERVIEW].isSelected) { + markTabActiveById(tabs[fixedTabIndices.OVERVIEW].id) + } + return + } + + if (isMonitoringNodeType && MonitoringDashboard) { + if (tabs[fixedTabIndices.MONITORING_DASHBOARD] && !tabs[fixedTabIndices.MONITORING_DASHBOARD].isSelected) { + markTabActiveById(tabs[fixedTabIndices.MONITORING_DASHBOARD].id) } + return } + if (isTerminalNodeType) { - if (tabs[FIXED_TABS_INDICES.ADMIN_TERMINAL] && !tabs[FIXED_TABS_INDICES.ADMIN_TERMINAL].isSelected) { - markTabActiveById(tabs[FIXED_TABS_INDICES.ADMIN_TERMINAL].id) + if (tabs[fixedTabIndices.ADMIN_TERMINAL] && !tabs[fixedTabIndices.ADMIN_TERMINAL].isSelected) { + markTabActiveById(tabs[fixedTabIndices.ADMIN_TERMINAL].id) } return } - if (tabs[FIXED_TABS_INDICES.K8S_RESOURCE_LIST] && !tabs[FIXED_TABS_INDICES.K8S_RESOURCE_LIST].isSelected) { - markTabActiveById(tabs[FIXED_TABS_INDICES.K8S_RESOURCE_LIST].id) + + if (tabs[fixedTabIndices.K8S_RESOURCE_LIST] && !tabs[fixedTabIndices.K8S_RESOURCE_LIST].isSelected) { + markTabActiveById(tabs[fixedTabIndices.K8S_RESOURCE_LIST].id) } }, [location.pathname]) @@ -248,7 +267,7 @@ const ResourceList = () => { const renderBreadcrumbs = () => const updateTerminalTabUrl = (queryParams: string) => { - const terminalTab = tabs[FIXED_TABS_INDICES.ADMIN_TERMINAL] + const terminalTab = tabs[fixedTabIndices.ADMIN_TERMINAL] if (!terminalTab || terminalTab.name !== AppDetailsTabs.terminal) { return } @@ -256,7 +275,7 @@ const ResourceList = () => { } const updateK8sResourceTabLastSyncMoment = () => - updateTabLastSyncMoment(tabs[FIXED_TABS_INDICES.K8S_RESOURCE_LIST]?.id) + updateTabLastSyncMoment(tabs[fixedTabIndices.K8S_RESOURCE_LIST]?.id) const getUpdateTabUrlForId = (id: string) => (_url: string, dynamicTitle?: string) => updateTabUrl(id, _url, dynamicTitle) @@ -295,32 +314,41 @@ const ResourceList = () => { const fixedTabComponents = [ , , + ...(MonitoringDashboard + ? [ + isMonitoringNodeType ? ( + + ) : ( +
+ ), + ] + : []), ...(isSuperAdmin && - tabs[FIXED_TABS_INDICES.ADMIN_TERMINAL]?.name === AppDetailsTabs.terminal && - tabs[FIXED_TABS_INDICES.ADMIN_TERMINAL].isAlive + tabs[fixedTabIndices.ADMIN_TERMINAL]?.name === AppDetailsTabs.terminal && + tabs[fixedTabIndices.ADMIN_TERMINAL].isAlive ? [ , @@ -350,7 +378,7 @@ const ResourceList = () => { stopTabByIdentifier={stopTabByIdentifier} refreshData={refreshData} setIsDataStale={setIsDataStale} - isOverview={isOverviewNodeType} + hideTimer={isOverviewNodeType || isMonitoringNodeType} />
{/* NOTE: since the terminal is only visibly hidden; we need to make sure it is rendered at the end of the page */} diff --git a/src/components/ResourceBrowser/Types.ts b/src/components/ResourceBrowser/Types.ts index dbf2a9b389..48f260fb02 100644 --- a/src/components/ResourceBrowser/Types.ts +++ b/src/components/ResourceBrowser/Types.ts @@ -21,6 +21,7 @@ import { OptionType, ApiResourceGroupType, GVKType, + InitTabType, K8sResourceDetailType, K8sResourceDetailDataType, } from '@devtron-labs/devtron-fe-common-lib' @@ -114,6 +115,7 @@ export interface SidebarType { export interface ClusterOptionType extends OptionType { errorInConnecting: string + isProd: boolean } export interface ResourceFilterOptionsProps { @@ -221,12 +223,6 @@ export interface SidebarChildButtonPropsType { onClick: React.MouseEventHandler } -export enum FIXED_TABS_INDICES { - OVERVIEW = 0, - K8S_RESOURCE_LIST, - ADMIN_TERMINAL, -} - export interface ClusterSelectorType { onChange: ({ label, value }) => void clusterList: ClusterOptionType[] @@ -237,3 +233,33 @@ export interface CreateResourceButtonType { clusterId: string closeModal: CreateResourceType['closePopup'] } + +export interface RBSidebarKeysType { + nodes: string + events: string + namespaces: string + eventGVK: GVKType + namespaceGVK: GVKType + nodeGVK: GVKType + overviewGVK: GVKType + monitoringGVK: GVKType +} + +export interface GetTabsBasedOnRoleParamsType { + selectedCluster: ClusterOptionType + namespace: string + isSuperAdmin: boolean + dynamicTabData: InitTabType + /** + * @default false + */ + isTerminalSelected?: boolean + /** + * @default false + */ + isOverviewSelected?: boolean + /** + * @default false + */ + isMonitoringDashBoardSelected?: boolean +} diff --git a/src/components/ResourceBrowser/Utils.tsx b/src/components/ResourceBrowser/Utils.tsx index 04734b239c..fc64fd096f 100644 --- a/src/components/ResourceBrowser/Utils.tsx +++ b/src/components/ResourceBrowser/Utils.tsx @@ -21,26 +21,33 @@ import { ApiResourceGroupType, DATE_TIME_FORMAT_STRING, GVKType, + InitTabType, K8sResourceDetailDataType, } from '@devtron-labs/devtron-fe-common-lib' import moment from 'moment' import { URLS, LAST_SEEN } from '../../config' -import { eventAgeComparator, processK8SObjects } from '../common' +import { eventAgeComparator, importComponentFromFELibrary, processK8SObjects } from '../common' import { AppDetailsTabs, AppDetailsTabsIdPrefix } from '../v2/appDetails/appDetails.store' import { JUMP_TO_KIND_SHORT_NAMES, K8S_EMPTY_GROUP, ORDERED_AGGREGATORS, SIDEBAR_KEYS } from './Constants' import { - ClusterOptionType, + GetTabsBasedOnRoleParamsType, K8SObjectChildMapType, K8SObjectMapType, K8SObjectType, K8sObjectOptionType, - FIXED_TABS_INDICES, } from './Types' -import { InitTabType } from '../common/DynamicTabs/Types' import TerminalIcon from '../../assets/icons/ic-terminal-fill.svg' import K8ResourceIcon from '../../assets/icons/ic-object.svg' import ClusterIcon from '../../assets/icons/ic-world-black.svg' +const getMonitoringDashboardTabConfig = importComponentFromFELibrary( + 'getMonitoringDashboardTabConfig', + null, + 'function', +) + +const MONITORING_DASHBOARD_TAB_INDEX = importComponentFromFELibrary('MONITORING_DASHBOARD_TAB_INDEX', null, 'function') + // Converts k8SObjects list to grouped map export const getGroupedK8sObjectMap = ( _k8SObjectList: K8SObjectType[], @@ -267,39 +274,59 @@ export const updateQueryString = ( return queryString.stringify(query) } -export const getTabsBasedOnRole = ( - selectedCluster: ClusterOptionType, - namespace: string, - isSuperAdmin: boolean, - dynamicTabData: InitTabType, +const getURLBasedOnSidebarGVK = (kind: GVKType['Kind'], clusterId: string, namespace: string): string => + `${URLS.RESOURCE_BROWSER}/${clusterId}/${namespace}/${kind.toLowerCase()}/${K8S_EMPTY_GROUP}` + +export const getFixedTabIndices = () => ({ + OVERVIEW: 0, + K8S_RESOURCE_LIST: 1, + MONITORING_DASHBOARD: MONITORING_DASHBOARD_TAB_INDEX || 3, + ADMIN_TERMINAL: MONITORING_DASHBOARD_TAB_INDEX ? 3 : 2, +}) + +export const getTabsBasedOnRole = ({ + selectedCluster, + namespace, + isSuperAdmin, + dynamicTabData, isTerminalSelected = false, isOverviewSelected = false, -): InitTabType[] => { + isMonitoringDashBoardSelected = false, +}: GetTabsBasedOnRoleParamsType): InitTabType[] => { const clusterId = selectedCluster.value const tabs = [ { idPrefix: AppDetailsTabsIdPrefix.cluster_overview, name: AppDetailsTabs.cluster_overview, - url: `${ - URLS.RESOURCE_BROWSER - }/${clusterId}/${namespace}/${SIDEBAR_KEYS.overviewGVK.Kind.toLowerCase()}/${K8S_EMPTY_GROUP}`, + url: getURLBasedOnSidebarGVK(SIDEBAR_KEYS.overviewGVK.Kind, clusterId, namespace), isSelected: isOverviewSelected, - position: FIXED_TABS_INDICES.OVERVIEW, + position: getFixedTabIndices().OVERVIEW, iconPath: ClusterIcon, showNameOnSelect: false, }, { idPrefix: AppDetailsTabsIdPrefix.k8s_Resources, name: AppDetailsTabs.k8s_Resources, - url: `${ - URLS.RESOURCE_BROWSER - }/${clusterId}/${namespace}/${SIDEBAR_KEYS.nodeGVK.Kind.toLowerCase()}/${K8S_EMPTY_GROUP}`, - isSelected: (!isSuperAdmin || !isTerminalSelected) && !dynamicTabData && !isOverviewSelected, - position: FIXED_TABS_INDICES.K8S_RESOURCE_LIST, + url: getURLBasedOnSidebarGVK(SIDEBAR_KEYS.nodeGVK.Kind, clusterId, namespace), + isSelected: + (!isSuperAdmin || !isTerminalSelected) && + !dynamicTabData && + !isOverviewSelected && + !isMonitoringDashBoardSelected, + position: getFixedTabIndices().K8S_RESOURCE_LIST, iconPath: K8ResourceIcon, showNameOnSelect: false, dynamicTitle: SIDEBAR_KEYS.nodeGVK.Kind, }, + ...(getMonitoringDashboardTabConfig + ? [ + getMonitoringDashboardTabConfig( + getURLBasedOnSidebarGVK(SIDEBAR_KEYS.monitoringGVK.Kind, clusterId, namespace), + isMonitoringDashBoardSelected, + getFixedTabIndices().MONITORING_DASHBOARD, + ), + ] + : []), ...(!isSuperAdmin ? [] : [ @@ -308,7 +335,7 @@ export const getTabsBasedOnRole = ( name: AppDetailsTabs.terminal, url: `${URLS.RESOURCE_BROWSER}/${clusterId}/${namespace}/${AppDetailsTabs.terminal}/${K8S_EMPTY_GROUP}`, isSelected: isTerminalSelected, - position: FIXED_TABS_INDICES.ADMIN_TERMINAL, + position: getFixedTabIndices().ADMIN_TERMINAL, iconPath: TerminalIcon, showNameOnSelect: true, isAlive: isTerminalSelected, diff --git a/src/components/app/types.ts b/src/components/app/types.ts index 85135ce142..b24176ff4e 100644 --- a/src/components/app/types.ts +++ b/src/components/app/types.ts @@ -438,6 +438,7 @@ export enum Nodes { Event = 'Event', Namespace = 'Namespace', Overview = 'Overview', + MonitoringDashboard = 'MonitoringDashboard', } /** * @deprecated - use from fe-common diff --git a/src/components/cluster/Cluster.tsx b/src/components/cluster/Cluster.tsx index c638cf6d2e..d87a3f3f0c 100644 --- a/src/components/cluster/Cluster.tsx +++ b/src/components/cluster/Cluster.tsx @@ -309,6 +309,7 @@ export default class ClusterList extends Component { isKubeConfigFile={this.state.isKubeConfigFile} toggleClusterDetails={this.toggleClusterDetails} isVirtualCluster={false} + isProd={false} /> )} @@ -339,6 +340,7 @@ const Cluster = ({ toggleCheckTlsConnection, setTlsConnectionFalse, isVirtualCluster, + isProd, }) => { const [editMode, toggleEditMode] = useState(false) const [environment, setEnvironment] = useState(null) @@ -666,6 +668,7 @@ const Cluster = ({ title={cluster_name || 'Add cluster'} subtitle={subTitle} className="fw-6 dc__mxw-400 dc__truncate-text" + tag={isProd ? 'Prod' : null} /> {clusterId && (
@@ -845,6 +848,7 @@ const Cluster = ({ toggleEditMode={toggleEditMode} toggleClusterDetails isVirtualCluster={isVirtualCluster} + isProd={isProd} />
diff --git a/src/components/cluster/ClusterForm.tsx b/src/components/cluster/ClusterForm.tsx index 6b4d4b8fdd..f3d90d16ec 100644 --- a/src/components/cluster/ClusterForm.tsx +++ b/src/components/cluster/ClusterForm.tsx @@ -57,6 +57,7 @@ import { DEFAULT_CLUSTER_ID, SSHAuthenticationType, RemoteConnectionTypeCluster, + ClusterFormProps, } from './cluster.type' import { CLUSTER_COMMAND, AppCreationType, MODES, ModuleNameMap } from '../../config' @@ -132,7 +133,8 @@ export default function ClusterForm({ isClusterDetails, toggleClusterDetails, isVirtualCluster, -}) { + isProd, +}: ClusterFormProps) { const [prometheusToggleEnabled, setPrometheusToggleEnabled] = useState(!!prometheus_url) const [prometheusAuthenticationType, setPrometheusAuthenticationType] = useState({ type: prometheusAuth?.userName ? AuthenticationType.BASIC : AuthenticationType.ANONYMOUS, @@ -201,6 +203,7 @@ export default function ClusterForm({ token: { value: config?.bearer_token ? config.bearer_token : '', error: '' }, endpoint: { value: prometheus_url || '', error: '' }, authType: { value: authenTicationType, error: '' }, + isProd: { value: isProd.toString(), error: '' }, }, { cluster_name: { @@ -454,11 +457,14 @@ export default function ClusterForm({ cluster_name: state.cluster_name.value, config: { bearer_token: - state.token.value && state.token.value !== DEFAULT_SECRET_PLACEHOLDER ? state.token.value.trim() : '', + state.token.value && state.token.value !== DEFAULT_SECRET_PLACEHOLDER + ? state.token.value.trim() + : '', tls_key: state.tlsClientKey.value, cert_data: state.tlsClientCert.value, cert_auth_data: state.certificateAuthorityData.value, }, + isProd: state.isProd.value === 'true', active, remoteConnectionConfig: getRemoteConnectionConfig(state, remoteConnectionMethod, SSHConnectionType), prometheus_url: prometheusToggleEnabled ? state.endpoint.value : '', @@ -757,6 +763,15 @@ export default function ClusterForm({ )}
+ + Production + Non - Production + {id !== DEFAULT_CLUSTER_ID && RemoteConnectionRadio && ( <>
diff --git a/src/components/cluster/cluster.type.ts b/src/components/cluster/cluster.type.ts index 1e690ffaae..55d2a00b92 100644 --- a/src/components/cluster/cluster.type.ts +++ b/src/components/cluster/cluster.type.ts @@ -174,3 +174,7 @@ export interface ClusterFormType { } export const RemoteConnectionTypeCluster = 'cluster' + +export type ClusterFormProps = Record & { + isProd: boolean +} diff --git a/src/components/common/DynamicTabs/DynamicTabs.scss b/src/components/common/DynamicTabs/DynamicTabs.scss index d1a14079dd..ca6e8b7297 100644 --- a/src/components/common/DynamicTabs/DynamicTabs.scss +++ b/src/components/common/DynamicTabs/DynamicTabs.scss @@ -149,6 +149,10 @@ .dynamic-tab__deleted { text-decoration: line-through; } + + &--no-title { + padding: 10px 12px; + } } } } diff --git a/src/components/common/DynamicTabs/DynamicTabs.tsx b/src/components/common/DynamicTabs/DynamicTabs.tsx index fd98cc9455..df585be1a9 100644 --- a/src/components/common/DynamicTabs/DynamicTabs.tsx +++ b/src/components/common/DynamicTabs/DynamicTabs.tsx @@ -18,11 +18,11 @@ import React, { Fragment, useEffect, useRef, useState } from 'react' import { useHistory } from 'react-router-dom' import Tippy from '@tippyjs/react' import { Dayjs } from 'dayjs' -import { stopPropagation, ConditionalWrap, noop, OptionType } from '@devtron-labs/devtron-fe-common-lib' +import { stopPropagation, ConditionalWrap, noop, OptionType, DynamicTabType } from '@devtron-labs/devtron-fe-common-lib' import ReactSelect, { components, InputActionMeta, OptionProps } from 'react-select' import { getCustomOptionSelectionStyle } from '../../v2/common/ReactSelect.utils' import { COMMON_TABS_SELECT_STYLES, EMPTY_TABS_DATA, initTabsData, checkIfDataIsStale } from './Utils' -import { DynamicTabsProps, DynamicTabType, TabsDataType } from './Types' +import { DynamicTabsProps, TabsDataType } from './Types' import { MoreButtonWrapper, noMatchingTabs, TabsMenu, timerTransition } from './DynamicTabs.component' import { AppDetailsTabs } from '../../v2/appDetails/appDetails.store' import Timer from './DynamicTabs.timer' @@ -48,7 +48,7 @@ const DynamicTabs = ({ stopTabByIdentifier, refreshData, setIsDataStale, - isOverview, + hideTimer, }: DynamicTabsProps) => { const { push } = useHistory() const tabsSectionRef = useRef(null) @@ -76,7 +76,8 @@ const DynamicTabs = ({ } const getTabNavLink = (tab: DynamicTabType) => { - const { name, isDeleted, isSelected, iconPath, dynamicTitle, title, showNameOnSelect, isAlive } = tab + const { name, isDeleted, isSelected, iconPath, dynamicTitle, title, showNameOnSelect, isAlive, hideName } = tab + const shouldRenderTitle = (!showNameOnSelect || isAlive || isSelected) && !hideName const _title = dynamicTitle || title @@ -89,10 +90,10 @@ const DynamicTabs = ({ aria-label={`Select tab ${_title}`} >
{iconPath && {name}} - {(!showNameOnSelect || isAlive || isSelected) && ( + {shouldRenderTitle && ( {_title} @@ -128,7 +129,7 @@ const DynamicTabs = ({ } const renderTab = (tab: DynamicTabType, idx: number, isFixed?: boolean) => { - const _showNameOnSelect = tab.showNameOnSelect && tab.isAlive + const _showNameOnSelect = tab.showNameOnSelect && tab.isAlive && !tab.hideName const renderWithTippy: (children: JSX.Element) => React.ReactNode = (children) => ( renderTab(tab, idx))}
)} - {(tabsData.dynamicTabs.length > 0 || (!isOverview && selectedTab?.id !== CLUSTER_TERMINAL_TAB)) && ( + {(tabsData.dynamicTabs.length > 0 || (!hideTimer && selectedTab?.id !== CLUSTER_TERMINAL_TAB)) && (
- {!isOverview && selectedTab?.id !== CLUSTER_TERMINAL_TAB && ( + {!hideTimer && selectedTab?.id !== CLUSTER_TERMINAL_TAB && (
{timerForSync()}
)} diff --git a/src/components/common/DynamicTabs/Types.ts b/src/components/common/DynamicTabs/Types.ts index 18c813426b..aa8838ffc0 100644 --- a/src/components/common/DynamicTabs/Types.ts +++ b/src/components/common/DynamicTabs/Types.ts @@ -16,32 +16,9 @@ import { ReactNode } from 'react' import { Dayjs } from 'dayjs' +import { DynamicTabType } from '@devtron-labs/devtron-fe-common-lib' import { useTabs } from './useTabs' -interface CommonTabArgsType { - name: string - kind?: string - url: string - isSelected: boolean - title?: string - isDeleted?: boolean - position: number - iconPath?: string - dynamicTitle?: string - showNameOnSelect?: boolean - isAlive?: boolean - lastSyncMoment?: Dayjs - componentKey?: string -} - -export interface InitTabType extends CommonTabArgsType { - idPrefix: string -} - -export interface DynamicTabType extends CommonTabArgsType { - id: string -} - export interface DynamicTabsProps { tabs: DynamicTabType[] removeTabByIdentifier: ReturnType['removeTabByIdentifier'] @@ -49,7 +26,7 @@ export interface DynamicTabsProps { stopTabByIdentifier: ReturnType['stopTabByIdentifier'] setIsDataStale: React.Dispatch> refreshData: () => void - isOverview: boolean + hideTimer: boolean } export interface TabsDataType { @@ -77,3 +54,30 @@ export type ParsedTabsData = { key: string data: DynamicTabType[] } + +export interface PopulateTabDataPropsType { + id: string + name: string + url: string + isSelected: boolean + title: string + position: number + showNameOnSelect: boolean + /** + * @default '' + */ + iconPath?: string + /** + * @default '' + */ + dynamicTitle?: string + /** + * @default false + */ + isAlive?: boolean + /** + * @description Would remove the title/name from tab heading, but that does not mean name is not required, since it is used in other calculations + * @default false + */ + hideName?: boolean +} diff --git a/src/components/common/DynamicTabs/Utils.ts b/src/components/common/DynamicTabs/Utils.ts index 8bc56008d5..d08d77a195 100644 --- a/src/components/common/DynamicTabs/Utils.ts +++ b/src/components/common/DynamicTabs/Utils.ts @@ -15,8 +15,8 @@ */ import { Dayjs } from 'dayjs' -import { OptionType } from '@devtron-labs/devtron-fe-common-lib' -import { DynamicTabType, TabsDataType } from './Types' +import { OptionType, DynamicTabType } from '@devtron-labs/devtron-fe-common-lib' +import { TabsDataType } from './Types' import { MARK_AS_STALE_DATA_CUT_OFF_MINS } from '../../ResourceBrowser/Constants' export const COMMON_TABS_SELECT_STYLES = { diff --git a/src/components/common/DynamicTabs/useTabs.ts b/src/components/common/DynamicTabs/useTabs.ts index 3f7e429f56..0f47ad070d 100644 --- a/src/components/common/DynamicTabs/useTabs.ts +++ b/src/components/common/DynamicTabs/useTabs.ts @@ -16,8 +16,8 @@ import { useState } from 'react' import dayjs from 'dayjs' -import { noop } from '@devtron-labs/devtron-fe-common-lib' -import { DynamicTabType, InitTabType, ParsedTabsData } from './Types' +import { noop, InitTabType, DynamicTabType } from '@devtron-labs/devtron-fe-common-lib' +import { ParsedTabsData, PopulateTabDataPropsType } from './Types' const FALLBACK_TAB = 1 @@ -26,33 +26,34 @@ export function useTabs(persistanceKey: string) { const getNewTabComponentKey = (id) => `${id}-${dayjs().toString()}` - const populateTabData = ( - id: string, - name: string, - url: string, - isSelected: boolean, - title: string, - position: number, - showNameOnSelect: boolean, + const populateTabData = ({ + id, + name, + url, + isSelected, + title, + position, + showNameOnSelect, iconPath = '', dynamicTitle = '', isAlive = false, - ) => - ({ - id, - name, - url, - isSelected, - title: title || name, - isDeleted: false, - position, - iconPath, - dynamicTitle, - showNameOnSelect, - isAlive, - lastSyncMoment: dayjs(), - componentKey: getNewTabComponentKey(id), - }) as DynamicTabType + hideName = false, + }: PopulateTabDataPropsType): DynamicTabType => ({ + id, + name, + url, + isSelected, + title: title || name, + isDeleted: false, + position, + iconPath, + dynamicTitle, + showNameOnSelect, + hideName, + isAlive, + lastSyncMoment: dayjs(), + componentKey: getNewTabComponentKey(id), + }) /** * To serialize tab data and store it in localStorage. The stored data can be retrieved @@ -92,18 +93,19 @@ export function useTabs(persistanceKey: string) { const populateInitTab = (_initTab: InitTabType) => { const title = _initTab.kind ? `${_initTab.kind}/${_initTab.name}` : _initTab.name const _id = `${_initTab.idPrefix}-${title}` - return populateTabData( - _id, - _initTab.name, - _initTab.url, - _initTab.isSelected, + return populateTabData({ + id: _id, + name: _initTab.name, + url: _initTab.url, + isSelected: _initTab.isSelected, title, - _initTab.position, - _initTab.showNameOnSelect, - _initTab.iconPath, - _initTab.dynamicTitle, - !!_initTab.isAlive, - ) + position: _initTab.position, + showNameOnSelect: _initTab.showNameOnSelect, + iconPath: _initTab.iconPath, + dynamicTitle: _initTab.dynamicTitle, + isAlive: !!_initTab.isAlive, + hideName: _initTab.hideName, + }) } /** @@ -231,7 +233,17 @@ export function useTabs(persistanceKey: string) { }) if (!found) { - _tabs.push(populateTabData(_id, name, url, true, title, position, showNameOnSelect)) + _tabs.push( + populateTabData({ + id: _id, + name, + url, + isSelected: true, + title, + position, + showNameOnSelect, + }), + ) } resolve(!found) localStorage.setItem('persisted-tabs-data', stringifyData(_tabs)) diff --git a/src/components/common/helpers/Helpers.tsx b/src/components/common/helpers/Helpers.tsx index 5cd1b2d2f4..8eb8b84801 100644 --- a/src/components/common/helpers/Helpers.tsx +++ b/src/components/common/helpers/Helpers.tsx @@ -645,7 +645,7 @@ export function sortBySelected(selectedArray: any[], availableArray: any[], matc ] } -export function sortObjectArrayAlphabetically(arr: Object[], compareKey: string) { +export const sortObjectArrayAlphabetically = (arr: T[], compareKey: string) => { return arr.sort((a, b) => a[compareKey].localeCompare(b[compareKey])) } diff --git a/src/components/v2/values/common/chartValues.api.ts b/src/components/v2/values/common/chartValues.api.ts index 11702d46b2..77979b4784 100644 --- a/src/components/v2/values/common/chartValues.api.ts +++ b/src/components/v2/values/common/chartValues.api.ts @@ -184,7 +184,7 @@ export async function fetchProjectsAndEnvironments( serverMode === SERVER_MODE.FULL ? getEnvironmentListMin(true) : getEnvironmentListHelmApps(), ]).then((responses: { status: string; value?: any; reason?: any }[]) => { const projectListRes: Teams[] = responses[0].value?.result || [] - const environmentListRes: EnvironmentListMinType[] = responses[1].value?.result || [] + const environmentListRes: EnvironmentListMinType[] | EnvironmentListHelmResult[] = responses[1].value?.result || [] let envList = [] if (serverMode === SERVER_MODE.FULL) { @@ -205,8 +205,8 @@ export async function fetchProjectsAndEnvironments( ) } else { const _sortedResult = ( - environmentListRes ? sortObjectArrayAlphabetically(environmentListRes, 'clusterName') : [] - ) as EnvironmentListHelmResult[] + environmentListRes ? sortObjectArrayAlphabetically(environmentListRes as EnvironmentListHelmResult[], 'clusterName') : [] + ) envList = _sortedResult.map((cluster) => ({ label: cluster.clusterName, options: [ diff --git a/src/config/routes.ts b/src/config/routes.ts index 96901221e3..634905c616 100644 --- a/src/config/routes.ts +++ b/src/config/routes.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { URLS as COMMON_URLS } from '@devtron-labs/devtron-fe-common-lib' + export interface NavItem { title: string href: string @@ -28,7 +30,7 @@ export const URLS = { JOB: '/job', CREATE_JOB: 'create-job', APPLICATION_GROUP: '/application-group', - RESOURCE_BROWSER: '/resource-browser', + RESOURCE_BROWSER: COMMON_URLS.RESOURCE_BROWSER, EXTERNAL_APPS: 'ea', DEVTRON_CHARTS: 'dc', EXTERNAL_ARGO_APP: 'eaa', @@ -126,6 +128,7 @@ export const URLS = { FLUX_APP_LIST: '/app/list/f', BUILD: '/build', SOFTWARE_DISTRIBUTION_HUB: '/software-distribution-hub', + MONITORING_DASHBOARD: 'monitoring-dashboard', } export enum APP_COMPOSE_STAGE { diff --git a/yarn.lock b/yarn.lock index e054013428..c7775ddc5b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1061,10 +1061,10 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@devtron-labs/devtron-fe-common-lib@0.6.0-patch-1-beta-7": - version "0.6.0-patch-1-beta-7" - resolved "https://registry.yarnpkg.com/@devtron-labs/devtron-fe-common-lib/-/devtron-fe-common-lib-0.6.0-patch-1-beta-7.tgz#a389eb74f09d91e98a195db27e118c78bce469f6" - integrity sha512-Q8Gc6ZJ5KuJzCa8ScIIK+zvvS4DRnCGqBFfXwySeKLMb1/fOv96mCWTXR4xHY2Wl8f0Sm6315Y7TcHB+DF2xuw== +"@devtron-labs/devtron-fe-common-lib@0.6.0-patch-1-beta-10": + version "0.6.0-patch-1-beta-10" + resolved "https://registry.yarnpkg.com/@devtron-labs/devtron-fe-common-lib/-/devtron-fe-common-lib-0.6.0-patch-1-beta-10.tgz#af5952af9e5f9da2fdadb746acc2003c9ba04b23" + integrity sha512-SH2JV/ne4aK91maz1qPxwNauQff2FCdv1mZdyTOymfHY8fcGaffeCtvln2osxbX+5znwKYLOfaDkVHuDiNRiMg== dependencies: "@types/react-dates" "^21.8.6" ansi_up "^5.2.1" @@ -2481,7 +2481,7 @@ "@types/d3-path@*": version "3.1.0" - resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.1.0.tgz#2b907adce762a78e98828f0b438eaca339ae410a" + resolved "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz" integrity sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ== "@types/d3-path@^1": @@ -2710,7 +2710,7 @@ "@types/react-dates@^21.8.6": version "21.8.6" - resolved "https://registry.npmjs.org/@types/react-dates/-/react-dates-21.8.6.tgz" + resolved "https://registry.yarnpkg.com/@types/react-dates/-/react-dates-21.8.6.tgz#ec9314b59e9d8e1ad71ccf021a7634e8afd26135" integrity sha512-fDF322SOXAxstapv0/oruiPx9kY4DiiaEHYAVvXdPfQhi/hdaONsA9dFw5JBNPAWz7Niuwk+UUhxPU98h70TjA== dependencies: "@types/react" "*" @@ -2726,7 +2726,7 @@ "@types/react-outside-click-handler@*": version "1.3.4" - resolved "https://registry.npmjs.org/@types/react-outside-click-handler/-/react-outside-click-handler-1.3.4.tgz" + resolved "https://registry.yarnpkg.com/@types/react-outside-click-handler/-/react-outside-click-handler-1.3.4.tgz#999e61057a3a23c6dfc9159b28f96378749d6c42" integrity sha512-kLuYIa9nWk1n0ywJPbVWqOEIRg0mh3vumriCHbz6LUObJw4rXYx9inDm8G579BtnH8vC0wKfrTq5c2y/K/Xzww== dependencies: "@types/react" "*" @@ -3032,7 +3032,7 @@ "@vitest/pretty-format@^2.0.5": version "2.1.3" - resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-2.1.3.tgz#48b9b03de75507d1d493df7beb48dc39a1946a3e" + resolved "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.3.tgz" integrity sha512-XH1XdtoLZCpqV59KRbPrIhFCOO0hErxrQCMcvnQete3Vibb9UeIOX02uFPfVn3Z9ZXsq78etlfyhnkmIZSzIwQ== dependencies: tinyrainbow "^1.2.0" @@ -3135,9 +3135,9 @@ acorn@^7.4.1: integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== acorn@^8.11.0, acorn@^8.11.3, acorn@^8.12.1, acorn@^8.4.1, acorn@^8.8.2, acorn@^8.9.0: - version "8.13.0" - resolved "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz" - integrity sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w== + version "8.14.0" + resolved "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz" + integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== agent-base@6: version "6.0.2" @@ -3244,7 +3244,7 @@ ansi-styles@^6.0.0: ansi_up@^5.2.1: version "5.2.1" - resolved "https://registry.npmjs.org/ansi_up/-/ansi_up-5.2.1.tgz" + resolved "https://registry.yarnpkg.com/ansi_up/-/ansi_up-5.2.1.tgz#9437082c7ad4975c15ec57d30a6b55da295bee36" integrity sha512-5bz5T/7FRmlxA37zDXhG6cAwlcZtfnmNLDJra66EEIT3kYlw5aPJdbkJEhm59D6kA4Wi5ict6u6IDYHJaQlH+g== anymatch@~3.1.2: @@ -4270,7 +4270,7 @@ data-view-byte-offset@^1.0.0: dayjs@^1.11.13: version "1.11.13" - resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c" integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg== dayjs@^1.11.8: @@ -7984,7 +7984,7 @@ react-moment-proptypes@^1.6.0: react-monaco-editor@^0.54.0: version "0.54.0" - resolved "https://registry.npmjs.org/react-monaco-editor/-/react-monaco-editor-0.54.0.tgz" + resolved "https://registry.yarnpkg.com/react-monaco-editor/-/react-monaco-editor-0.54.0.tgz#ec9293249a991b08264be723c1ec0ca3a6d480d8" integrity sha512-9JwO69851mfpuhYLHlKbae7omQWJ/2ICE2lbL0VHyNyZR8rCOH7440u+zAtDgiOMpLwmYdY1sEZCdRefywX6GQ== dependencies: prop-types "^15.8.1" @@ -9459,7 +9459,7 @@ tsconfig-paths@^4.2.0: tslib@2.7.0, tslib@^2.0.1: version "2.7.0" - resolved "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== tslib@^1.13.0, tslib@^1.8.1, tslib@^1.9.3: