diff --git a/src/components/app/details/appDetails/utils.tsx b/src/components/app/details/appDetails/utils.tsx index 339f68acb7..c20bb402ae 100644 --- a/src/components/app/details/appDetails/utils.tsx +++ b/src/components/app/details/appDetails/utils.tsx @@ -16,7 +16,7 @@ import moment from 'moment' import { AggregationKeys } from '../../types' -import { getVersionArr, isVersionLessThanOrEqualToTarget, DayPickerRangeControllerPresets } from '../../../common' +import { getVersionArr, isVersionLessThanOrEqualToTarget } from '../../../common' import { ChartTypes, AppMetricsTabType, StatusType, StatusTypes } from './appDetails.type' import { ZERO_TIME_STRING, @@ -31,6 +31,7 @@ import { AppEnvironment, SelectPickerOptionType, IconsProps, + DayPickerRangeControllerPresets, } from '@devtron-labs/devtron-fe-common-lib' import { GetIFrameSrcParamsType } from './types' diff --git a/src/components/common/DatePickers/DayPickerRangeController.tsx b/src/components/common/DatePickers/DayPickerRangeController.tsx index 81204d69a0..9136a5f183 100644 --- a/src/components/common/DatePickers/DayPickerRangeController.tsx +++ b/src/components/common/DatePickers/DayPickerRangeController.tsx @@ -24,101 +24,11 @@ import { isInclusivelyBeforeDay, DayPickerRangeController } from 'react-dates' import './calendar.css' import { ReactComponent as ArrowDown } from '../../../assets/icons/ic-chevron-down.svg' import { DA_APP_DETAILS_GA_EVENTS } from '@Components/app/details/appDetails/constants' +import { Button, customDayStyles, DatePickerRangeControllerProps, DayPickerCalendarInfoHorizontal, DayPickerRangeControllerPresets } from '@devtron-labs/devtron-fe-common-lib/dist' +import { customStyles } from './constants' -interface DatePickerType2Props { - calendar - calendarInputs - focusedInput - handleFocusChange - handleDatesChange - handleCalendarInputs? - calendarValue: string - handlePredefinedRange: (start: Moment, end: Moment, endStr: string) => void - handleDateInput: (key: 'startDate' | 'endDate', value: string) => void - handleApply: (...args) => void -} - -const hoveredSpanStyles = { - background: 'var(--B100)', - color: 'var(--B500)', -} - -const selectedStyles = { - background: 'var(--B100)', - color: 'var(--B500)', - hover: { - background: 'var(--B500)', - color: 'var(--N0)', - }, -} - -const selectedSpanStyles = { - background: 'var(--B100)', - color: 'var(--B500)', - - hover: { - background: 'var(--B500)', - color: 'var(--N0)', - }, -} - -const customDayStyles = { - selectedStartStyles: selectedStyles, - selectedEndStyles: selectedStyles, - hoveredSpanStyles, - selectedSpanStyles, - selectedStyles, - border: 'none', -} - -const styless = { - PresetDateRangePicker_panel: { - padding: '0px', - width: '200px', - height: '100%', - }, - PresetDateRangePicker_button: { - width: '188px', - background: 'var(--transparent)', - border: 'none', - color: 'var(--N900)', - padding: '8px', - font: 'inherit', - fontWeight: 500, - lineHeight: 'normal', - overflow: 'visible', - cursor: 'pointer', - ':active': { - outline: 0, - }, - }, - DayPicker__horizontal: { - borderRadius: '4px', - }, - PresetDateRangePicker_button__selected: { - color: 'var(--B500)', - fontWeight: 600, - background: 'var(--B100)', - outline: 'none', - }, -} - -const DayPicker_calendarInfo__horizontal = { - width: '532px', - boxShadow: 'none', -} - -export const DayPickerRangeControllerPresets = [ - { text: 'Last 5 minutes', endDate: moment(), startDate: moment().subtract(5, 'minutes'), endStr: 'now-5m' }, - { text: 'Last 30 minutes', endDate: moment(), startDate: moment().subtract(30, 'minutes'), endStr: 'now-30m' }, - { text: 'Last 1 hour', endDate: moment(), startDate: moment().subtract(1, 'hours'), endStr: 'now-1h' }, - { text: 'Last 24 hours', endDate: moment(), startDate: moment().subtract(24, 'hours'), endStr: 'now-24h' }, - { text: 'Last 7 days', endDate: moment(), startDate: moment().subtract(7, 'days'), endStr: 'now-7d' }, - { text: 'Last 1 month', endDate: moment(), startDate: moment().subtract(1, 'months'), endStr: 'now-1M' }, - { text: 'Last 6 months', endDate: moment(), startDate: moment().subtract(6, 'months'), endStr: 'now-6M' }, -] -export class DatePickerType2 extends Component { +export class DatePickerType2 extends Component { constructor(props) { super(props) this.state = { @@ -144,15 +54,15 @@ export class DatePickerType2 extends Component {
@@ -181,12 +91,10 @@ export class DatePickerType2 extends Component { }} /> - +
-
+
{DayPickerRangeControllerPresets.map(({ text, startDate, endDate, endStr }) => { const isSelected = startDate.isSame(this.props.calendar.startDate, 'minute') && @@ -194,12 +102,12 @@ export class DatePickerType2 extends Component { startDate.isSame(this.props.calendar.startDate, 'day') && endDate.isSame(this.props.calendar.endDate, 'day') let buttonStyles = { - ...styless.PresetDateRangePicker_button, + ...customStyles.PresetDateRangePicker_button, } if (isSelected) { buttonStyles = { ...buttonStyles, - ...styless.PresetDateRangePicker_button__selected, + ...customStyles.PresetDateRangePicker_button__selected, } } return ( diff --git a/src/components/common/DatePickers/constants.ts b/src/components/common/DatePickers/constants.ts new file mode 100644 index 0000000000..03e7abad51 --- /dev/null +++ b/src/components/common/DatePickers/constants.ts @@ -0,0 +1,31 @@ +export const customStyles = { + PresetDateRangePicker_panel: { + padding: '0px', + width: '200px', + height: '100%', + }, + PresetDateRangePicker_button: { + width: '188px', + background: 'var(--transparent)', + border: 'none', + color: 'var(--N900)', + padding: '8px', + font: 'inherit', + fontWeight: 500, + lineHeight: 'normal', + overflow: 'visible', + cursor: 'pointer', + ':active': { + outline: 0, + }, + }, + DayPicker__horizontal: { + borderRadius: '4px', + }, + PresetDateRangePicker_button__selected: { + color: 'var(--B500)', + fontWeight: 600, + background: 'var(--B100)', + outline: 'none', + }, +} diff --git a/src/components/common/navigation/Navigation.tsx b/src/components/common/navigation/Navigation.tsx index 2fc2259364..0ca6c8ccc8 100644 --- a/src/components/common/navigation/Navigation.tsx +++ b/src/components/common/navigation/Navigation.tsx @@ -17,7 +17,7 @@ import React, { Component } from 'react' import { NavLink, RouteComponentProps } from 'react-router-dom' import ReactGA from 'react-ga4' -import { URLS as CommonURLS, Icon, MainContext } from '@devtron-labs/devtron-fe-common-lib' +import { URLS as CommonURLS, Icon, MainContext, URLS as CommonUrls } from '@devtron-labs/devtron-fe-common-lib' import { ModuleNameMap, MODULE_STATUS_POLLING_INTERVAL, @@ -96,6 +96,16 @@ const NavigationList: NavigationListItemType[] = [ markAsBeta: false, isAvailableInDesktop: true, }, + { + title: 'Observability', + dataTestId: 'click-on-observability', + type: 'link', + icon: 'ic-binoculars', + href: CommonUrls.OBSERVABILITY, + isAvailableInEA: false, + markAsBeta: false, + isAvailableInDesktop: true, + }, { title: 'Resource Watcher', dataTestId: 'click-on-resource-watcher', diff --git a/src/components/common/navigation/NavigationRoutes.tsx b/src/components/common/navigation/NavigationRoutes.tsx index b9d174013d..ce85976fd2 100644 --- a/src/components/common/navigation/NavigationRoutes.tsx +++ b/src/components/common/navigation/NavigationRoutes.tsx @@ -104,6 +104,7 @@ const OnboardingGuide = lazy(() => import('../../onboardingGuide/OnboardingGuide 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') @@ -549,6 +550,9 @@ const NavigationRoutes = ({ reloadVersionConfig }: Readonly } />, + + + , ...(!window._env_.HIDE_RESOURCE_WATCHER && ResourceWatcherRouter ? [ diff --git a/src/components/observability/Customer/CustomerList.tsx b/src/components/observability/Customer/CustomerList.tsx new file mode 100644 index 0000000000..0df426c294 --- /dev/null +++ b/src/components/observability/Customer/CustomerList.tsx @@ -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( + () => + (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 ( +
+ + 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} + /> +
+ ) +} diff --git a/src/components/observability/Customer/CustomerListCellComponent.tsx b/src/components/observability/Customer/CustomerListCellComponent.tsx new file mode 100644 index 0000000000..c9d632eed7 --- /dev/null +++ b/src/components/observability/Customer/CustomerListCellComponent.tsx @@ -0,0 +1,86 @@ +import { FunctionComponent, useEffect, useRef } from 'react' +import { Link, useRouteMatch } from 'react-router-dom' + +import { + FiltersTypeEnum, + Icon, + TableCellComponentProps, + TableSignalEnum, + Tooltip, +} from '@devtron-labs/devtron-fe-common-lib/dist' + +import { CustomerObservabilityDTO, ObservabilityListFields } from '../types' + +export const CustomerListCellComponent: FunctionComponent< + TableCellComponentProps +> = ({ + field, + row: { + data: { id, name, status, projects, totalVms, activeVms, healthStatus, icon }, + }, + isRowActive, + signals, +}: TableCellComponentProps) => { + const linkRef = useRef(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 ObservabilityListFields.ICON: + return ( + + + + ) + case ObservabilityListFields.PROJECT_NAME: + return ( + + + {name} + + + ) + case ObservabilityListFields.STATUS: + return ( + + + {status} + + ) + case ObservabilityListFields.PROJECTS: + return {projects} + case ObservabilityListFields.TOTAL_VMS: + return {totalVms} + case ObservabilityListFields.ACTIVE_VMS: + return {activeVms} + case ObservabilityListFields.HEALTH_STATUS: + return ( +
+ + {healthStatus} + +
+ ) + default: + return null + } +} diff --git a/src/components/observability/Customer/Customers.tsx b/src/components/observability/Customer/Customers.tsx new file mode 100644 index 0000000000..3c5c1a83cf --- /dev/null +++ b/src/components/observability/Customer/Customers.tsx @@ -0,0 +1,138 @@ +import { useEffect, useState } from 'react' +import { useRouteMatch } from 'react-router-dom' + +import { + BreadCrumb, + ComponentSizeType, + handleUTCTime, + PageHeader, + SearchBar, + TabGroup, + useBreadcrumb, +} from '@devtron-labs/devtron-fe-common-lib' + +import { getBreadCrumbObj } from '../utils' +import { CustomerList } from './CustomerList' + +let interval +const Customers = () => { + const { url, path } = useRouteMatch() + const [lastDataSyncTimeString, setLastDataSyncTimeString] = useState('') + const [isDataSyncing, setDataSyncing] = useState(false) + const [syncListData, setSyncListData] = useState() + // TODO: Remove later + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [fetchingExternalApps, setFetchingExternalApps] = useState(false) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [selectedTabIndex, setSelectedTabIndex] = useState(0) + const renderDataSyncingText = () => Syncing + 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 getObservabilityTabs = () => ( + + ) + + const syncNow = (): void => { + setSyncListData(!syncListData) + } + + const renderLastSyncComponent = () => ( +
+ {lastDataSyncTimeString && ( + <> + {lastDataSyncTimeString} + {!isDataSyncing && ( + <> +   + + + )} + + )} + {fetchingExternalApps && renderDataSyncingText()} +
+ ) + + const { breadcrumbs } = useBreadcrumb(getBreadCrumbObj('project', url)) + + const renderBreadcrumbs = () => + + const renderPageHeader = () => ( + + ) + const searchKey = '' + const handleSearch = () => {} + return ( +
+ {renderPageHeader()} +
+
+ +
+ {renderLastSyncComponent()} +
+ + +
+ ) +} + +export default Customers diff --git a/src/components/observability/Customer/index.ts b/src/components/observability/Customer/index.ts new file mode 100644 index 0000000000..67c2f5def6 --- /dev/null +++ b/src/components/observability/Customer/index.ts @@ -0,0 +1 @@ +export { default as Customers } from './Customers' diff --git a/src/components/observability/Metrics/BarMetrics.tsx b/src/components/observability/Metrics/BarMetrics.tsx new file mode 100644 index 0000000000..c1818a4868 --- /dev/null +++ b/src/components/observability/Metrics/BarMetrics.tsx @@ -0,0 +1,30 @@ +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) => ( +
+
+ Tenants Capacity & Resource Allocation +
+
+
+ TENANTS NAME + CPU + MEMORY + DISK +
+ {data?.map(({ disk, name, memory, cpu }) => ( +
+ + {name} + + + + +
+ ))} +
+
+) diff --git a/src/components/observability/Metrics/ObservabilityGraphMetrics.tsx b/src/components/observability/Metrics/ObservabilityGraphMetrics.tsx new file mode 100644 index 0000000000..9ff5d8d5e7 --- /dev/null +++ b/src/components/observability/Metrics/ObservabilityGraphMetrics.tsx @@ -0,0 +1,195 @@ +import { useEffect, useState } from 'react' +import Tippy from '@tippyjs/react' +import moment, { Moment } from 'moment' + +import { useTheme } from '@devtron-labs/devtron-fe-common-lib' + +import { + CalendarFocusInput, + CalendarFocusInputType, + ChartType, +} from '@Components/app/details/appDetails/appDetails.type' +import { APP_METRICS_CALENDAR_INPUT_DATE_FORMAT } from '@Components/app/details/appDetails/constants' +import { GraphModalProps } from '@Components/app/details/appDetails/GraphsModal' +import { AppInfo, getCalendarValue, getIframeSrc } from '@Components/app/details/appDetails/utils' +import { DatePickerType2 } from '@Components/common' + +import { ReactComponent as Fullscreen } from '../../../assets/icons/ic-fullscreen-2.svg' + +import '../styles.scss' + +export const ObservabilityGraphMetrics = () => { + const { appTheme } = useTheme() + + const [dateRange, setDateRange] = useState<{ startDate: Moment; endDate: Moment }>({ + startDate: moment().subtract(5, 'minute'), + endDate: moment(), + }) + const [calendarInputs, setCalendarInput] = useState<{ startDate: string; endDate: string }>({ + startDate: 'now-5m', + endDate: 'now', + }) + const [focusedInput, setFocusedInput] = useState(CalendarFocusInput.StartDate) + const [calendarValue, setCalendarValue] = useState('') + const [graphs, setGraphs] = useState({ + cpu: '', + ram: '', + }) + + const getIframeSrcWrapper: GraphModalProps['getIframeSrcWrapper'] = (params) => + getIframeSrc({ + ...params, + grafanaTheme: appTheme, + }) + + const appInfo: AppInfo = { + appId: 740, + envId: 36, + dataSourceName: '', + newPodHash: '', + k8sVersion: '', + } + + function getNewGraphs(): void { + const cpu = getIframeSrcWrapper({ + appInfo, + chartName: ChartType.Cpu, + calendarInputs, + tab: 'aggregate', + isLegendRequired: true, + }) + const ram = getIframeSrcWrapper({ + appInfo, + chartName: ChartType.Ram, + calendarInputs, + tab: 'aggregate', + isLegendRequired: true, + }) + setGraphs({ + cpu, + ram, + }) + } + + useEffect(() => { + getNewGraphs() + }, [calendarValue, appTheme]) + + const handlePredefinedRange = (start: Moment, end: Moment, startStr: string): void => { + setDateRange({ + startDate: start, + endDate: end, + }) + setCalendarInput({ + startDate: startStr, + endDate: 'now', + }) + const str = getCalendarValue(startStr, 'now') + setCalendarValue(str) + } + + const handleDatesChange = ({ startDate, endDate }): void => { + setDateRange({ + startDate, + endDate, + }) + setCalendarInput({ + startDate: startDate?.format(APP_METRICS_CALENDAR_INPUT_DATE_FORMAT), + endDate: endDate?.format(APP_METRICS_CALENDAR_INPUT_DATE_FORMAT) || '', + }) + } + + const handleDateInput = (key: CalendarFocusInputType, value: string): void => { + setCalendarInput({ + ...calendarInputs, + [key]: value, + }) + } + + const handleFocusChange = (_focusedInput): void => { + setFocusedInput(_focusedInput || CalendarFocusInput.StartDate) + } + + const handleApply = (): void => { + const str = getCalendarValue(calendarInputs.startDate, calendarInputs.endDate) + setCalendarValue(str) + } + + return ( +
+
+ Observability Metrics + +
+ +
+
+
+ VMs Usage + +
+ +
+
+
+