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 &&
}
- {(!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: