Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/components/Navigation/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,27 @@
},
],
},
{
id: 'observability',
title: 'Observability',
icon: 'ic-binoculars',

Check failure on line 301 in src/components/Navigation/constants.ts

View workflow job for this annotation

GitHub Actions / ci

Type '"ic-binoculars"' is not assignable to type '"ic-arrow-clockwise" | "ic-73strings" | "ic-aborted" | "ic-activity" | "ic-add" | "ic-ai" | "ic-alibaba" | "ic-amazon-eks" | "ic-app-group" | "ic-app-template" | "ic-application-group" | ... 283 more ... | "ic-world-globe"'.

Check failure on line 301 in src/components/Navigation/constants.ts

View workflow job for this annotation

GitHub Actions / ci

Type '"ic-binoculars"' is not assignable to type '"ic-arrow-clockwise" | "ic-73strings" | "ic-aborted" | "ic-activity" | "ic-add" | "ic-ai" | "ic-alibaba" | "ic-amazon-eks" | "ic-app-group" | "ic-app-template" | "ic-application-group" | ... 287 more ... | "ic-world-globe"'.
items: [
{
title: 'Overview',
dataTestId: 'observability-overview',
id: 'infrastructure-management-overview',
icon: 'ic-speedometer',
href: COMMON_URLS.OBSERVABILITY_OVERVIEW,

Check failure on line 308 in src/components/Navigation/constants.ts

View workflow job for this annotation

GitHub Actions / ci

Property 'OBSERVABILITY_OVERVIEW' does not exist on type '{ readonly LOGIN: "/login"; readonly LOGIN_SSO: "/login/sso"; readonly APP_LIST: "list"; readonly CREATE_JOB: "create-job"; readonly GETTING_STARTED: "getting-started"; readonly STACK_MANAGER_ABOUT: "/stack-manager/about"; ... 67 more ...; readonly EXTERNAL_APPS: "ea"; }'.

Check failure on line 308 in src/components/Navigation/constants.ts

View workflow job for this annotation

GitHub Actions / ci

Property 'OBSERVABILITY_OVERVIEW' does not exist on type '{ readonly LOGIN: "/login"; readonly LOGIN_SSO: "/login/sso"; readonly APP_LIST: "list"; readonly CREATE_JOB: "create-job"; readonly GETTING_STARTED: "getting-started"; readonly STACK_MANAGER_ABOUT: "/stack-manager/about"; ... 70 more ...; readonly EXTERNAL_APPS: "ea"; }'.
},
{
title: 'Customers',
dataTestId: 'observability-vms',
id: 'observability-vms',

Check failure on line 313 in src/components/Navigation/constants.ts

View workflow job for this annotation

GitHub Actions / ci

Type '"observability-vms"' is not assignable to type 'NavigationItemID'.

Check failure on line 313 in src/components/Navigation/constants.ts

View workflow job for this annotation

GitHub Actions / ci

Type '"observability-vms"' is not assignable to type 'NavigationItemID'.
icon: 'ic-cluster',
href: COMMON_URLS.OBSERVABILITY_CUSTOMER_LIST,

Check failure on line 315 in src/components/Navigation/constants.ts

View workflow job for this annotation

GitHub Actions / ci

Property 'OBSERVABILITY_CUSTOMER_LIST' does not exist on type '{ readonly LOGIN: "/login"; readonly LOGIN_SSO: "/login/sso"; readonly APP_LIST: "list"; readonly CREATE_JOB: "create-job"; readonly GETTING_STARTED: "getting-started"; readonly STACK_MANAGER_ABOUT: "/stack-manager/about"; ... 67 more ...; readonly EXTERNAL_APPS: "ea"; }'.

Check failure on line 315 in src/components/Navigation/constants.ts

View workflow job for this annotation

GitHub Actions / ci

Property 'OBSERVABILITY_CUSTOMER_LIST' does not exist on type '{ readonly LOGIN: "/login"; readonly LOGIN_SSO: "/login/sso"; readonly APP_LIST: "list"; readonly CREATE_JOB: "create-job"; readonly GETTING_STARTED: "getting-started"; readonly STACK_MANAGER_ABOUT: "/stack-manager/about"; ... 70 more ...; readonly EXTERNAL_APPS: "ea"; }'.
},
],
},
{
id: 'software-release-management',
title: 'Software Release Management',
Expand Down
1 change: 1 addition & 0 deletions src/components/Navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { ViewType } from '@Config/constants'
export type NavigationRootItemID =
| 'application-management'
| 'infrastructure-management'
| 'observability'
| 'software-release-management'
| 'cost-visibility'
| 'security-center'
Expand Down
4 changes: 4 additions & 0 deletions src/components/common/navigation/NavigationRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
const DevtronStackManager = lazy(() => import('../../v2/devtronStackManager/DevtronStackManager'))
const AppGroupRoute = lazy(() => import('../../ApplicationGroup/AppGroupRoute'))
const Jobs = lazy(() => import('../../Jobs/Jobs'))
const Observability = lazy(() => import('../../observability/ObservabilityRouter'))

const ResourceWatcherRouter = importComponentFromFELibrary('ResourceWatcherRouter')
const SoftwareDistributionHub = importComponentFromFELibrary('SoftwareDistributionHub', null, 'function')
Expand Down Expand Up @@ -560,6 +561,9 @@
>
<ResourceBrowser />
</Route>
<Route key={CommonURLS.OBSERVABILITY} path={CommonURLS.OBSERVABILITY}>

Check failure on line 564 in src/components/common/navigation/NavigationRoutes.tsx

View workflow job for this annotation

GitHub Actions / ci

Property 'OBSERVABILITY' does not exist on type '{ readonly LOGIN: "/login"; readonly LOGIN_SSO: "/login/sso"; readonly APP_LIST: "list"; readonly CREATE_JOB: "create-job"; readonly GETTING_STARTED: "getting-started"; readonly STACK_MANAGER_ABOUT: "/stack-manager/about"; ... 67 more ...; readonly EXTERNAL_APPS: "ea"; }'.

Check failure on line 564 in src/components/common/navigation/NavigationRoutes.tsx

View workflow job for this annotation

GitHub Actions / ci

Property 'OBSERVABILITY' does not exist on type '{ readonly LOGIN: "/login"; readonly LOGIN_SSO: "/login/sso"; readonly APP_LIST: "list"; readonly CREATE_JOB: "create-job"; readonly GETTING_STARTED: "getting-started"; readonly STACK_MANAGER_ABOUT: "/stack-manager/about"; ... 67 more ...; readonly EXTERNAL_APPS: "ea"; }'.

Check failure on line 564 in src/components/common/navigation/NavigationRoutes.tsx

View workflow job for this annotation

GitHub Actions / ci

Property 'OBSERVABILITY' does not exist on type '{ readonly LOGIN: "/login"; readonly LOGIN_SSO: "/login/sso"; readonly APP_LIST: "list"; readonly CREATE_JOB: "create-job"; readonly GETTING_STARTED: "getting-started"; readonly STACK_MANAGER_ABOUT: "/stack-manager/about"; ... 70 more ...; readonly EXTERNAL_APPS: "ea"; }'.

Check failure on line 564 in src/components/common/navigation/NavigationRoutes.tsx

View workflow job for this annotation

GitHub Actions / ci

Property 'OBSERVABILITY' does not exist on type '{ readonly LOGIN: "/login"; readonly LOGIN_SSO: "/login/sso"; readonly APP_LIST: "list"; readonly CREATE_JOB: "create-job"; readonly GETTING_STARTED: "getting-started"; readonly STACK_MANAGER_ABOUT: "/stack-manager/about"; ... 70 more ...; readonly EXTERNAL_APPS: "ea"; }'.
<Observability />
</Route>
<Route
key={CommonURLS.INFRASTRUCTURE_MANAGEMENT_APP}
path={[
Expand Down
51 changes: 51 additions & 0 deletions src/components/observability/Customer/CustomerList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { useMemo } from 'react'

import { FiltersTypeEnum, PaginationEnum, Table, useAsync } from '@devtron-labs/devtron-fe-common-lib'

import { CUSTOMER_TABLE_COLUMN } from '../constants'
import { getCustomerListData } from '../service'
import { CustomerObservabilityDTO, CustomerTableProps } from '../types'

export const CustomerList = () => {
// ASYNC CALLS
const [isFetching, customerData] = useAsync(() => getCustomerListData(), [])

// CONFIGS
const rows = useMemo<CustomerTableProps['rows']>(
() =>
(customerData || []).map((data) => ({
id: `observe_project_${data.id.toString()}`,
data,
})),
[customerData],
)

const filter: CustomerTableProps['filter'] = (
rowData: { id: string; data: CustomerObservabilityDTO },
filterData: { searchKey: string },
) => rowData.data.name.toLowerCase().includes(filterData.searchKey.toLowerCase())

return (
<div className="observability-table-wrapper flexbox-col flex-grow-1 dc__overflow-auto">
<Table<CustomerObservabilityDTO, FiltersTypeEnum.STATE, {}>
id="table__customer-list"
loading={isFetching}
stylesConfig={{ showSeparatorBetweenRows: true }}
columns={CUSTOMER_TABLE_COLUMN}
rows={rows}
filtersVariant={FiltersTypeEnum.STATE}
emptyStateConfig={{
noRowsConfig: {
title: 'No resources found',
subTitle: `No resources found in this cluster for upgrade compatibility check`,
},
}}
filter={filter}
additionalFilterProps={{
initialSortKey: 'name',
}}
paginationVariant={PaginationEnum.PAGINATED}
/>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { FunctionComponent, useEffect, useRef } from 'react'
import { Link, useRouteMatch } from 'react-router-dom'

import {
FiltersTypeEnum,
TableCellComponentProps,
TableSignalEnum,
Tooltip,
} from '@devtron-labs/devtron-fe-common-lib/dist'

import { CustomerObservabilityDTO, ProjectListFields } from '../types'

export const CustomerListCellComponent: FunctionComponent<
TableCellComponentProps<CustomerObservabilityDTO, FiltersTypeEnum.STATE, {}>
> = ({
field,
row: {
data: { id, name, status, project, totalVms, activeVms, healthStatus },
},
isRowActive,
signals,
}: TableCellComponentProps<CustomerObservabilityDTO, FiltersTypeEnum.STATE, {}>) => {
const linkRef = useRef<HTMLAnchorElement>(null)
const match = useRouteMatch()

useEffect(() => {
const handleEnter = ({ detail: { activeRowData } }) => {
if (activeRowData.data.id === id) {
linkRef.current?.click()
}
}

if (isRowActive) {
signals.addEventListener(TableSignalEnum.ENTER_PRESSED, handleEnter)
}

return () => {
signals.removeEventListener(TableSignalEnum.ENTER_PRESSED, handleEnter)
}
}, [isRowActive])

switch (field) {
case ProjectListFields.PROJECT_NAME:
return (
<Link ref={linkRef} to={`${match.path}/${name}/overview`} className="flex left py-10">
<Tooltip content={name}>
<span className="dc__truncate">{name}</span>
</Tooltip>
</Link>
)
case ProjectListFields.STATUS:
return <span className="flex left py-10">{status}</span>
case ProjectListFields.PROJECTS:
return <span className="flex left py-10">{project}</span>
case ProjectListFields.TOTAL_VMS:
return <span className="flex left py-10">{totalVms}</span>
case ProjectListFields.ACTIVE_VMS:
return <span className="flex left py-10">{activeVms}</span>
case ProjectListFields.HEALTH_STATUS:
return (
<div className="flex left py-10">
<Tooltip content={healthStatus}>
<span className="dc__truncate">{healthStatus}</span>
</Tooltip>
</div>
)
default:
return null
}
}
117 changes: 117 additions & 0 deletions src/components/observability/Customer/Customers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { useEffect, useState } from 'react'

import {
BreadCrumb,
BreadcrumbText,
ComponentSizeType,
handleUTCTime,
PageHeader,
SearchBar,
useBreadcrumb,
} from '@devtron-labs/devtron-fe-common-lib'

import ObservabilityIconComponent from '../ObservabilityIcon'
import { CustomerList } from './CustomerList'

let interval
const Customers = () => {
const [lastDataSyncTimeString, setLastDataSyncTimeString] = useState<React.ReactNode>('')
const [isDataSyncing, setDataSyncing] = useState(false)
const [syncListData, setSyncListData] = useState<boolean>()
// TODO: Remove later
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [fetchingExternalApps, setFetchingExternalApps] = useState<boolean>(false)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [selectedTabIndex, setSelectedTabIndex] = useState(0)
const renderDataSyncingText = () => <span className="dc__loading-dots">Syncing</span>
useEffect(() => {
if (isDataSyncing) {
setLastDataSyncTimeString(renderDataSyncingText)
} else {
const _lastDataSyncTime = Date()
setLastDataSyncTimeString(`Last synced ${handleUTCTime(_lastDataSyncTime, true)}`)
interval = setInterval(() => {
setLastDataSyncTimeString(`Last synced ${handleUTCTime(_lastDataSyncTime, true)}`)
}, 1000)
}
return () => {
if (interval) {
clearInterval(interval)
}
}
}, [isDataSyncing])

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const updateDataSyncing = (loading: boolean): void => {
setDataSyncing(loading)
}

const syncNow = (): void => {
setSyncListData(!syncListData)
}

const renderLastSyncComponent = () => (
<div className="flex fs-13">
{lastDataSyncTimeString && (
<>
<span data-testid="sync-now-text">{lastDataSyncTimeString}</span>
{!isDataSyncing && (
<>
&nbsp;
<button
className="btn btn-link p-0 fw-6 cb-5 mb-2"
type="button"
onClick={syncNow}
data-testid="sync-now-button"
>
Sync now
</button>
</>
)}
</>
)}
{fetchingExternalApps && renderDataSyncingText()}
</div>
)

const { breadcrumbs } = useBreadcrumb({
alias: {
observability: {
component: <ObservabilityIconComponent />,
linked: true,
},
customers: {
component: <BreadcrumbText heading="Customers" isActive />,
linked: false,
},
},
})
const renderBreadcrumbs = () => <BreadCrumb breadcrumbs={breadcrumbs} />
const searchKey = ''
const handleSearch = () => {}
return (
<div className="observability-overview flex-grow-1 dc__overflow-auto">
<PageHeader isBreadcrumbs breadCrumbs={renderBreadcrumbs} />
<div className="flex dc__content-space p-16 w-100 dc__gap-8 ">
<div className="flexbox dc__align-items-center dc__mxw-fit-content">
<SearchBar
containerClassName="w-250"
dataTestId="search-customer-env"
initialSearchText={searchKey}
inputProps={{
placeholder: 'Search customer',
}}
handleEnter={handleSearch}
size={ComponentSizeType.medium}
keyboardShortcut="/"
/>
</div>
{renderLastSyncComponent()}
</div>

<CustomerList />
</div>
)
}

export default Customers
1 change: 1 addition & 0 deletions src/components/observability/Customer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as Customers } from './Customers'
25 changes: 25 additions & 0 deletions src/components/observability/Metrics/BarMetrics.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Tooltip } from '@devtron-labs/devtron-fe-common-lib/dist'

import { BarMetricsProps } from '../types'
import { CPUCapacityCellComponent, DiskCapacityCellComponent, MemoryCapacityCellComponent } from '../utils'

export const BarMetrics = ({ data }: BarMetricsProps) => (
<div className="flexbox-col">
<div className="dc__grid dc__gap-24 bar-chart-table-row fs-12 fw-6 lh-20 cn-7 px-16 py-8">
<span>TENANTS NAME</span>
<span>CPU</span>
<span>MEMORY</span>
<span>DISK</span>
</div>
{data?.map(({ disk, name, memory, cpu }) => (
<div key={name} className="dc__grid dc__gap-24 bar-chart-table-row p-16 dc__hover-n50">
<Tooltip content={name}>
<span className="dc__truncate">{name}</span>
</Tooltip>
<CPUCapacityCellComponent cpu={cpu} />
<MemoryCapacityCellComponent memory={memory} />
<DiskCapacityCellComponent disk={disk} />
</div>
))}
</div>
)
74 changes: 74 additions & 0 deletions src/components/observability/MetricsInfoCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { useState } from 'react'

import {
Button,
ButtonStyleType,
ButtonVariantType,
ConditionalWrap,
Icon,
motion,
noop,
Tooltip,
} from '@devtron-labs/devtron-fe-common-lib'

import { MetricsInfoCardProps } from './types'

export const MetricsInfoCard = ({
dataTestId,
metricTitle,
metricValue,
metricUnit,
valueOutOf,
iconName,
redirectionLink,
tooltipContent,
}: MetricsInfoCardProps) => {
const [isHovering, setIsHovering] = useState(false)

const handleHoverStart = () => setIsHovering(true)
const handleHoverEnd = () => setIsHovering(false)

return (
<ConditionalWrap condition={!!redirectionLink} wrap={noop}>
<motion.div
data-testid={dataTestId}
onHoverStart={redirectionLink ? handleHoverStart : noop}
onHoverEnd={redirectionLink ? handleHoverEnd : noop}
className={`flexbox-col br-8 bg__primary border__secondary cn-9 ${isHovering ? 'shadow__card--10' : ''}`}
>
<div className="flexbox dc__gap-12 p-16 dc__content-space">
<div className="flexbox-col">
<Tooltip alwaysShowTippyOnHover content={tooltipContent}>
<span className={`fs-13 fw-4 lh-20 ${isHovering ? 'dc__underline-dotted' : ''}`}>
{metricTitle}
</span>
</Tooltip>
<div className="flexbox dc__gap-4 dc__align-baseline font-ibm-plex-sans">
<span className="fs-24 fw-6 lh-1-5">{metricValue}</span>
{valueOutOf && (
<span className="fs-16 fw-4 lh-20 dc__first-letter-capitalize">/ {valueOutOf}</span>
)}
{metricUnit && (
<span className="fs-16 fw-4 lh-20 dc__first-letter-capitalize">{metricUnit}</span>
)}
</div>
</div>
<div>
{isHovering ? (
<Button
dataTestId={`redirect-to-${metricTitle}`}
icon={<Icon name="ic-arrow-square-out" color="B500" />}
ariaLabel="redirect"
showAriaLabelInTippy={false}
style={ButtonStyleType.default}
variant={ButtonVariantType.borderLess}
/>
) : (
<Icon size={36} color={null} name={iconName} />
)}
</div>
</div>
</motion.div>
</ConditionalWrap>
)
}
Loading
Loading