diff --git a/src/components/Navigation/constants.ts b/src/components/Navigation/constants.ts index 71890a7446..ea0861b7ee 100644 --- a/src/components/Navigation/constants.ts +++ b/src/components/Navigation/constants.ts @@ -295,6 +295,27 @@ export const NAVIGATION_LIST: NavigationGroupType[] = [ }, ], }, + { + id: 'observability', + title: 'Observability', + icon: 'ic-binoculars', + items: [ + { + title: 'Overview', + dataTestId: 'observability-overview', + id: 'infrastructure-management-overview', + icon: 'ic-speedometer', + href: COMMON_URLS.OBSERVABILITY_OVERVIEW, + }, + { + title: 'Customers', + dataTestId: 'observability-vms', + id: 'observability-vms', + icon: 'ic-cluster', + href: COMMON_URLS.OBSERVABILITY_CUSTOMER_LIST, + }, + ], + }, { id: 'software-release-management', title: 'Software Release Management', diff --git a/src/components/Navigation/types.ts b/src/components/Navigation/types.ts index a8f017b64c..c0233d5971 100644 --- a/src/components/Navigation/types.ts +++ b/src/components/Navigation/types.ts @@ -15,6 +15,7 @@ import { ViewType } from '@Config/constants' export type NavigationRootItemID = | 'application-management' | 'infrastructure-management' + | 'observability' | 'software-release-management' | 'cost-visibility' | 'security-center' diff --git a/src/components/common/navigation/NavigationRoutes.tsx b/src/components/common/navigation/NavigationRoutes.tsx index 57dbde651e..17ca0c8f2e 100644 --- a/src/components/common/navigation/NavigationRoutes.tsx +++ b/src/components/common/navigation/NavigationRoutes.tsx @@ -112,6 +112,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') @@ -560,6 +561,9 @@ const NavigationRoutes = ({ reloadVersionConfig }: Readonly + + + { + // 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..71584b72ce --- /dev/null +++ b/src/components/observability/Customer/CustomerListCellComponent.tsx @@ -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 +> = ({ + field, + row: { + data: { id, name, status, project, totalVms, activeVms, healthStatus }, + }, + 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 ProjectListFields.PROJECT_NAME: + return ( + + + {name} + + + ) + case ProjectListFields.STATUS: + return {status} + case ProjectListFields.PROJECTS: + return {project} + case ProjectListFields.TOTAL_VMS: + return {totalVms} + case ProjectListFields.ACTIVE_VMS: + return {activeVms} + case ProjectListFields.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..866b5179f3 --- /dev/null +++ b/src/components/observability/Customer/Customers.tsx @@ -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('') + 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 syncNow = (): void => { + setSyncListData(!syncListData) + } + + const renderLastSyncComponent = () => ( +
+ {lastDataSyncTimeString && ( + <> + {lastDataSyncTimeString} + {!isDataSyncing && ( + <> +   + + + )} + + )} + {fetchingExternalApps && renderDataSyncingText()} +
+ ) + + const { breadcrumbs } = useBreadcrumb({ + alias: { + observability: { + component: , + linked: true, + }, + customers: { + component: , + linked: false, + }, + }, + }) + const renderBreadcrumbs = () => + const searchKey = '' + const handleSearch = () => {} + return ( +
+ +
+
+ +
+ {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..620683f0c2 --- /dev/null +++ b/src/components/observability/Metrics/BarMetrics.tsx @@ -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) => ( +
+
+ TENANTS NAME + CPU + MEMORY + DISK +
+ {data?.map(({ disk, name, memory, cpu }) => ( +
+ + {name} + + + + +
+ ))} +
+) diff --git a/src/components/observability/MetricsInfoCard.tsx b/src/components/observability/MetricsInfoCard.tsx new file mode 100644 index 0000000000..6643f139cb --- /dev/null +++ b/src/components/observability/MetricsInfoCard.tsx @@ -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 ( + + +
+
+ + + {metricTitle} + + +
+ {metricValue} + {valueOutOf && ( + / {valueOutOf} + )} + {metricUnit && ( + {metricUnit} + )} +
+
+
+ {isHovering ? ( +
+
+
+
+ ) +} diff --git a/src/components/observability/ObservabilityIcon.tsx b/src/components/observability/ObservabilityIcon.tsx new file mode 100644 index 0000000000..4437780867 --- /dev/null +++ b/src/components/observability/ObservabilityIcon.tsx @@ -0,0 +1,25 @@ +import { + Button, + ButtonComponentType, + ButtonVariantType, + ComponentSizeType, + Icon, + URLS, +} from '@devtron-labs/devtron-fe-common-lib' + +const ObservabilityIconComponent = () => ( + + + )} + + )} + {fetchingExternalApps && renderDataSyncingText()} + + ) + + const renderProjectTabs = () => ( +
+
+
+ +
+
+
+ + + + + + +
+
+ ) + + const { breadcrumbs } = useBreadcrumb( + { + alias: { + observability: { + component: , + linked: true, + }, + ':projectId': { + component: , + linked: false, + }, + }, + }, + [], + ) + const renderBreadcrumbs = () => + const searchKey = '' + const handleSearch = () => {} + return ( +
+ +
+ +
+ {renderProjectTabs()} +
+ ) +} + +export default Project diff --git a/src/components/observability/ProjectObservability/ProjectList.tsx b/src/components/observability/ProjectObservability/ProjectList.tsx new file mode 100644 index 0000000000..6a29b7153b --- /dev/null +++ b/src/components/observability/ProjectObservability/ProjectList.tsx @@ -0,0 +1,53 @@ +import { useMemo } from 'react' + +import { FiltersTypeEnum, PaginationEnum, Table, useAsync } from '@devtron-labs/devtron-fe-common-lib' + +import { PROJECT_TABLE_COLUMNS } from '../constants' +import { getProjectList } from '../service' +import { ObservabilityProject, ProjectTableProps } from '../types' + +const ProjectList = () => { + // ASYNC CALLS + const [isFetching, projectData] = useAsync(() => getProjectList(), []) + + // CONFIGS + const rows = useMemo( + () => + (projectData || []).map((data) => ({ + id: `observe_project_${data.id.toString()}`, + data, + })), + [projectData], + ) + + const filter: ProjectTableProps['filter'] = ( + rowData: { id: string; data: ObservabilityProject }, + filterData: { searchKey: string }, + ) => rowData.data.name.toLowerCase().includes(filterData.searchKey.toLowerCase()) + + return ( +
+ + id="table__customer-list" + loading={isFetching} + stylesConfig={{ showSeparatorBetweenRows: true }} + columns={PROJECT_TABLE_COLUMNS} + rows={rows} + filtersVariant={FiltersTypeEnum.STATE} + paginationVariant={PaginationEnum.NOT_PAGINATED} + emptyStateConfig={{ + noRowsConfig: { + title: 'No resources found', + subTitle: `No resources found in this cluster for upgrade compatibility check`, + }, + }} + filter={filter} + additionalFilterProps={{ + initialSortKey: 'name', + }} + /> +
+ ) +} + +export default ProjectList diff --git a/src/components/observability/ProjectObservability/ProjectListCellComponent.tsx b/src/components/observability/ProjectObservability/ProjectListCellComponent.tsx new file mode 100644 index 0000000000..b594c833f9 --- /dev/null +++ b/src/components/observability/ProjectObservability/ProjectListCellComponent.tsx @@ -0,0 +1,77 @@ +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 { ObservabilityProject, ProjectListFields } from '../types' + +export const ProjectListCellComponent: FunctionComponent< + TableCellComponentProps +> = ({ + field, + row: { + data: { id, name, description, status, totalVms, activeVms, healthStatus }, + }, + 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 ProjectListFields.PROJECT_NAME: + return ( + + + {name} + + + ) + case ProjectListFields.PROJECT_DESCRIPTION: + return {description} + case ProjectListFields.STATUS: + return {status} + case ProjectListFields.TOTAL_VMS: + return {totalVms} + case ProjectListFields.ACTIVE_VMS: + return ( +
+ + {activeVms} + +
+ ) + case ProjectListFields.HEALTH_STATUS: + return ( +
+ + {healthStatus} + +
+ ) + default: + return null + } +} diff --git a/src/components/observability/ProjectObservability/ProjectOverview.tsx b/src/components/observability/ProjectObservability/ProjectOverview.tsx new file mode 100644 index 0000000000..d9954eb0e3 --- /dev/null +++ b/src/components/observability/ProjectObservability/ProjectOverview.tsx @@ -0,0 +1,53 @@ +import { GenericSectionErrorState } from '@devtron-labs/devtron-fe-common-lib' + +import { MetricsInfoCard } from '../MetricsInfoCard' +import { GlanceMetricsKeys } from '../types' +import { MetricsInfoLoadingCard, useGetGlanceConfig } from '../utils' + +export const ProjectOverview = () => { + const { isFetching, data, isError, refetch } = useGetGlanceConfig() + console.log(data) + + const renderBody = () => { + if (isFetching) { + return ( +
+ {Object.keys(GlanceMetricsKeys).map((key) => ( + + ))} +
+ ) + } + + if (isError) { + return ( + + ) + } + // alert(JSON.stringify(data)) + return ( +
+ {data.glanceConfig.map((value) => ( + + ))} +
+ ) + } + + return ( +
+
+
+

At a Glance

+
+
+
{renderBody()}
+
+ ) +} + +export default ProjectOverview diff --git a/src/components/observability/ProjectObservability/index.ts b/src/components/observability/ProjectObservability/index.ts new file mode 100644 index 0000000000..02e1b0e063 --- /dev/null +++ b/src/components/observability/ProjectObservability/index.ts @@ -0,0 +1 @@ +export { default as ProjectObservability } from './Project' diff --git a/src/components/observability/SingleVMOverview.tsx b/src/components/observability/SingleVMOverview.tsx new file mode 100644 index 0000000000..940d97277a --- /dev/null +++ b/src/components/observability/SingleVMOverview.tsx @@ -0,0 +1,175 @@ +import { useEffect, useState } from 'react' + +import { + BreadCrumb, + BreadcrumbText, + GenericSectionErrorState, + handleUTCTime, + PageHeader, + useBreadcrumb, +} from '@devtron-labs/devtron-fe-common-lib/dist' + +import { MetricsInfoCard } from './MetricsInfoCard' +import ObservabilityIconComponent from './ObservabilityIcon' +import { GlanceMetricsKeys } from './types' +import { MetricsInfoLoadingCard, useGetGlanceConfig } from './utils' + +import './styles.scss' + +let interval +const SingleVMOverview = () => { + const { isFetching, data, isError, refetch } = useGetGlanceConfig() + console.log(data) + + const [lastDataSyncTimeString, setLastDataSyncTimeString] = useState('') + const [isDataSyncing, setDataSyncing] = useState(false) + const [syncListData, setSyncListData] = useState() + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [fetchingExternalApps, setFetchingExternalApps] = useState(false) + 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 { breadcrumbs } = useBreadcrumb({ + alias: { + observability: { + component: , + linked: true, + }, + customer: { + component: , + linked: false, + }, + }, + }) + + const renderBody = () => { + if (isFetching) { + return ( +
+ {Object.keys(GlanceMetricsKeys).map((key) => ( + + ))} +
+ ) + } + + if (isError) { + return ( + + ) + } + // alert(JSON.stringify(data)) + return ( +
+ {data.glanceConfig.map((value) => ( + + ))} +
+ ) + } + + const renderBreadcrumbs = () => + + const syncNow = (): void => { + setSyncListData(!syncListData) + } + + const renderLastSyncComponent = () => ( +
+ {lastDataSyncTimeString && ( + <> + {lastDataSyncTimeString} + {!isDataSyncing && ( + <> +   + + + )} + + )} + {fetchingExternalApps && renderDataSyncingText()} +
+ ) + return ( +
+ +
+ {renderLastSyncComponent()} + +
+
+

At a Glance

+
+
+
{renderBody()}
+
+
+
+ CPU +
+
+ +
+
+
+
+ CPU +
+
+ +
+
+
+
+ CPU +
+
+ +
+
+
+
+ CPU +
+
+ +
+
+
+
+
+ ) +} + +export default SingleVMOverview diff --git a/src/components/observability/VMObservability/VM.tsx b/src/components/observability/VMObservability/VM.tsx new file mode 100644 index 0000000000..d6396f490b --- /dev/null +++ b/src/components/observability/VMObservability/VM.tsx @@ -0,0 +1,162 @@ +import { useEffect, useState } from 'react' +import { Redirect, Route, useRouteMatch } from 'react-router-dom' + +import { + BreadCrumb, + BreadcrumbText, + ComponentSizeType, + handleUTCTime, + PageHeader, + SearchBar, + TabGroup, + TabProps, + useBreadcrumb, +} from '@devtron-labs/devtron-fe-common-lib/dist' + +import ObservabilityIconComponent from '../ObservabilityIcon' +import VMList from './VMList' +import { VMOverview } from './VMOverview' + +import '../styles.scss' + +let interval + +const VM = () => { + const match = useRouteMatch() + const { breadcrumbs } = useBreadcrumb( + { + alias: { + observability: { + component: , + linked: true, + }, + ':projects': { + component: , + linked: false, + }, + }, + }, + [], + ) + + const [lastDataSyncTimeString, setLastDataSyncTimeString] = useState('') + const [isDataSyncing, setDataSyncing] = useState(false) + const [syncListData, setSyncListData] = useState() + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [fetchingExternalApps, setFetchingExternalApps] = useState(false) + 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 tabs: TabProps[] = [ + { + id: 'vm_overview', + label: 'Overview', + tabType: 'navLink', + props: { + to: `${match.url}/overview`, + }, + }, + { + id: 'vm_list', + label: 'VMs', + tabType: 'navLink', + props: { + to: `${match.url}/vms`, + }, + }, + ] + + const syncNow = (): void => { + setSyncListData(!syncListData) + } + + const renderVMTabs = () => { + const rightComponent = ( +
+ {lastDataSyncTimeString && ( + <> + {lastDataSyncTimeString} + {!isDataSyncing && ( + <> +   + + + )} + + )} + {fetchingExternalApps && renderDataSyncingText()} +
+ ) + + return ( +
+
+
+ +
+
+
+ + + + + + + + +
+
+ ) + } + + const renderBreadcrumbs = () => + const searchKey = '' + const handleSearch = () => {} + return ( +
+ +
+ +
+ {renderVMTabs()} +
+ ) +} + +export default VM diff --git a/src/components/observability/VMObservability/VMList.tsx b/src/components/observability/VMObservability/VMList.tsx new file mode 100644 index 0000000000..b00d730403 --- /dev/null +++ b/src/components/observability/VMObservability/VMList.tsx @@ -0,0 +1,51 @@ +import { useMemo } from 'react' + +import { FiltersTypeEnum, PaginationEnum, Table, useAsync } from '@devtron-labs/devtron-fe-common-lib' + +import { VM_TABLE_COLUMNS } from '../constants' +import { getVMList } from '../service' +import { ObservabilityVM, VMTableProps } from '../types' + +const VMList = () => { + // ASYNC CALLS + const [isFetching, vmData] = useAsync(() => getVMList(), []) + + // CONFIGS + const rows = useMemo( + () => + (vmData || []).map((data) => ({ + id: `observe_vm_${data.id.toString()}`, + data, + })), + [vmData], + ) + + const filter: VMTableProps['filter'] = (rowData, filterData) => + rowData.data.name.includes(filterData.searchKey.toLowerCase()) + + return ( +
+ + id="table__vm-list" + loading={isFetching} + stylesConfig={{ showSeparatorBetweenRows: true }} + columns={VM_TABLE_COLUMNS} + rows={rows} + filtersVariant={FiltersTypeEnum.STATE} + paginationVariant={PaginationEnum.NOT_PAGINATED} + emptyStateConfig={{ + noRowsConfig: { + title: 'No resources found', + subTitle: `No resources found in this cluster for upgrade compatibility check`, + }, + }} + filter={filter} + additionalFilterProps={{ + initialSortKey: 'name', + }} + /> +
+ ) +} + +export default VMList diff --git a/src/components/observability/VMObservability/VMListCellComponent.tsx b/src/components/observability/VMObservability/VMListCellComponent.tsx new file mode 100644 index 0000000000..4ab2bb7b27 --- /dev/null +++ b/src/components/observability/VMObservability/VMListCellComponent.tsx @@ -0,0 +1,77 @@ +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 { ObservabilityVM, VMListFields } from '../types' + +export const VMListCellComponent: FunctionComponent< + TableCellComponentProps +> = ({ + field, + row: { + data: { id, name, ipAddress, status, cpu, memory, disk }, + }, + 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 VMListFields.VM_NAME: + return ( + + + {name} + + + ) + case VMListFields.VM_IPADDRESS: + return {ipAddress} + case VMListFields.VM_STATUS: + return {status} + case VMListFields.VM_CPU: + return {cpu} + case VMListFields.VM_MEMORY: + return ( +
+ + {memory} + +
+ ) + case VMListFields.VM_DISK: + return ( +
+ + {disk} + +
+ ) + default: + return null + } +} diff --git a/src/components/observability/VMObservability/VMOverview.tsx b/src/components/observability/VMObservability/VMOverview.tsx new file mode 100644 index 0000000000..45536bf3e9 --- /dev/null +++ b/src/components/observability/VMObservability/VMOverview.tsx @@ -0,0 +1,90 @@ +import { GenericSectionErrorState } from '@devtron-labs/devtron-fe-common-lib' + +import { MetricsInfoCard } from '../MetricsInfoCard' +// import ObservabilityIconComponent from './ObservabilityIcon' +import { GlanceMetricsKeys } from '../types' +import { MetricsInfoLoadingCard, useGetGlanceConfig } from '../utils' + +import '../styles.scss' + +export const VMOverview = () => { + const { isFetching, data, isError, refetch } = useGetGlanceConfig() + console.log(data) + + const renderBody = () => { + if (isFetching) { + return ( +
+ {Object.keys(GlanceMetricsKeys).map((key) => ( + + ))} +
+ ) + } + + if (isError) { + return ( + + ) + } + // alert(JSON.stringify(data)) + return ( +
+ {data.glanceConfig.map((value) => ( + + ))} +
+ ) + } + + return ( +
+
+
+

At a Glance

+
+
+
{renderBody()}
+
+
+
+ CPU +
+
+ +
+
+
+
+ CPU +
+
+ +
+
+
+
+ CPU +
+
+ +
+
+
+
+ CPU +
+
+ +
+
+
+
+ ) +} + +export default VMOverview diff --git a/src/components/observability/VMObservability/index.ts b/src/components/observability/VMObservability/index.ts new file mode 100644 index 0000000000..3a44bfda27 --- /dev/null +++ b/src/components/observability/VMObservability/index.ts @@ -0,0 +1 @@ +export { default as VMObservability } from './VM' diff --git a/src/components/observability/constants.ts b/src/components/observability/constants.ts new file mode 100644 index 0000000000..f4a6e2fd9b --- /dev/null +++ b/src/components/observability/constants.ts @@ -0,0 +1,264 @@ +import { + numberComparatorBySortOrder, + SegmentedControlProps, + stringComparatorBySortOrder, +} from '@devtron-labs/devtron-fe-common-lib' + +import { CustomerListCellComponent } from './Customer/CustomerListCellComponent' +import { ProjectListCellComponent } from './ProjectObservability/ProjectListCellComponent' +import { VMListCellComponent } from './VMObservability/VMListCellComponent' +import { CustomerTableProps, MetricsInfoCardProps, ProjectTableProps, TabDetailsSegment, VMTableProps } from './types' + +export enum ObservabilityGlanceMetricKeys { + PROJECTS = 'totalProjects', + TOTAL_VMS = 'totalVms', + RUNNING_VMS = 'runningVms', + HEALTH_STATUS = 'healthStatus', + TOTAL_CLUSTER = 'totalClusters', + TOTAL_CUSTOMERS = 'totalCustomers', +} + +export const GLANCE_METRICS_CARDS_CONFIG: Partial> = { + [ObservabilityGlanceMetricKeys.TOTAL_CUSTOMERS]: { + iconName: 'ic-users', + metricTitle: 'Total Customers', + tooltipContent: 'Number of all Customers', + }, + [ObservabilityGlanceMetricKeys.PROJECTS]: { + iconName: 'ic-bg-project', + metricTitle: 'Projects', + tooltipContent: 'Number of all projects', + }, + [ObservabilityGlanceMetricKeys.TOTAL_VMS]: { + iconName: 'ic-bg-cluster', + metricTitle: 'Total VMs', + tooltipContent: 'Number of all Vms', + }, + + [ObservabilityGlanceMetricKeys.HEALTH_STATUS]: { + iconName: 'ic-check', + metricTitle: 'Status', + tooltipContent: 'Percentage of all health status', + }, +} + +export const TAB_DETAILS_SEGMENTS: SegmentedControlProps['segments'] = [ + { + label: 'Overview', + value: TabDetailsSegment.OVERVIEW, + }, + { + label: 'Projects', + value: TabDetailsSegment.PROJECTS, + }, +] + +export const CUSTOMER_TABLE_COLUMN: CustomerTableProps['columns'] = [ + { + field: 'icon', + size: { + fixed: 24, + }, + CellComponent: CustomerListCellComponent, + }, + { + field: 'name', + label: 'Customer', + size: { + fixed: 250, + }, + CellComponent: CustomerListCellComponent, + isSortable: true, + comparator: stringComparatorBySortOrder, + } as CustomerTableProps['columns'][0], + { + field: 'status', + label: 'Status', + size: { + fixed: 250, + }, + CellComponent: CustomerListCellComponent, + isSortable: true, + comparator: stringComparatorBySortOrder, + }, + { + field: 'project', + label: 'Projects', + size: { + fixed: 250, + }, + CellComponent: CustomerListCellComponent, + isSortable: true, + comparator: numberComparatorBySortOrder, + }, + { + field: 'totalVms', + label: 'Total VMs', + size: { + fixed: 150, + }, + CellComponent: CustomerListCellComponent, + isSortable: true, + comparator: numberComparatorBySortOrder, + }, + + { + field: 'activeVms', + label: 'Active VMs', + size: { + fixed: 150, + }, + CellComponent: CustomerListCellComponent, + isSortable: true, + comparator: numberComparatorBySortOrder, + }, + + { + field: 'healthStatus', + label: 'Health', + size: { + fixed: 250, + }, + CellComponent: CustomerListCellComponent, + isSortable: true, + comparator: stringComparatorBySortOrder, + }, +] + +export const PROJECT_TABLE_COLUMNS: ProjectTableProps['columns'] = [ + { + field: 'name', + label: 'Project name', + size: { + fixed: 250, + }, + isSortable: true, + comparator: stringComparatorBySortOrder, + CellComponent: ProjectListCellComponent, + }, + { + field: 'description', + label: 'Description', + size: { + fixed: 250, + }, + isSortable: true, + comparator: stringComparatorBySortOrder, + CellComponent: ProjectListCellComponent, + }, + { + field: 'status', + label: 'Status', + size: { + fixed: 250, + }, + isSortable: true, + comparator: stringComparatorBySortOrder, + CellComponent: ProjectListCellComponent, + }, + { + field: 'totalVms', + label: 'Total VM', + size: { + fixed: 200, + }, + isSortable: true, + comparator: numberComparatorBySortOrder, + CellComponent: ProjectListCellComponent, + }, + { + field: 'activeVms', + label: 'Active VM', + size: { + fixed: 200, + }, + isSortable: true, + comparator: numberComparatorBySortOrder, + CellComponent: ProjectListCellComponent, + }, + { + field: 'healthStatus', + label: 'Health Status', + size: { + fixed: 200, + }, + CellComponent: ProjectListCellComponent, + }, +] + +export const VM_TABLE_COLUMNS: VMTableProps['columns'] = [ + { + field: 'name', + label: 'VM name', + size: { + fixed: 250, + }, + isSortable: true, + comparator: stringComparatorBySortOrder, + CellComponent: VMListCellComponent, + }, + { + field: 'status', + label: 'Status', + size: { + fixed: 250, + }, + isSortable: true, + comparator: stringComparatorBySortOrder, + CellComponent: VMListCellComponent, + }, + { + field: 'ipAddress', + label: 'IP Address', + size: { + fixed: 250, + }, + isSortable: true, + comparator: stringComparatorBySortOrder, + CellComponent: VMListCellComponent, + }, + { + field: 'cpu', + label: 'CPU', + size: { + fixed: 200, + }, + isSortable: true, + comparator: numberComparatorBySortOrder, + CellComponent: VMListCellComponent, + }, + { + field: 'memory', + label: 'Memory', + size: { + fixed: 200, + }, + isSortable: true, + comparator: numberComparatorBySortOrder, + CellComponent: VMListCellComponent, + }, + { + field: 'disk', + label: 'Disk', + size: { + fixed: 200, + }, + isSortable: true, + comparator: numberComparatorBySortOrder, + CellComponent: VMListCellComponent, + }, +] + +export enum ObservabilityUtilizationKeys { + VM_RUNNING_STATUS = 'vmRUnningStatus', + CPU_UTILIZATION = 'cpuUtilization', + MEMORY_UTILIZATION = 'memoryUtilization', + DISK_UTILIZATION = 'diskUtilization', +} + +export const OBSERVABILITY_UTILIZATION_CARDS_TITLE: Record = { + [ObservabilityUtilizationKeys.VM_RUNNING_STATUS]: 'VMS Running Status', + [ObservabilityUtilizationKeys.CPU_UTILIZATION]: 'CPU Utilization', + [ObservabilityUtilizationKeys.MEMORY_UTILIZATION]: 'Memory Utilization', + [ObservabilityUtilizationKeys.DISK_UTILIZATION]: 'Disk Utilization', +} diff --git a/src/components/observability/index.ts b/src/components/observability/index.ts new file mode 100644 index 0000000000..d1371ea92b --- /dev/null +++ b/src/components/observability/index.ts @@ -0,0 +1 @@ +export { default as ObservabilityRouter } from './ObservabilityRouter' diff --git a/src/components/observability/service.ts b/src/components/observability/service.ts new file mode 100644 index 0000000000..617ad01efd --- /dev/null +++ b/src/components/observability/service.ts @@ -0,0 +1,187 @@ +import { ObservabilityGlanceMetricKeys } from './constants' +import { CustomerObservabilityDTO, ObservabilityOverviewDTO } from './types' + +export const getObservabilityData: () => Promise = () => + Promise.resolve({ + glanceConfig: { + [ObservabilityGlanceMetricKeys.TOTAL_CUSTOMERS]: 4, + [ObservabilityGlanceMetricKeys.TOTAL_VMS]: 20, + [ObservabilityGlanceMetricKeys.PROJECTS]: 2, + [ObservabilityGlanceMetricKeys.HEALTH_STATUS]: 50, + [ObservabilityGlanceMetricKeys.TOTAL_CLUSTER]: 2, + [ObservabilityGlanceMetricKeys.RUNNING_VMS]: 40, + }, + metrics: [ + { + id: 1, + name: 'Tenants1', + cpu: { capacity: 36, utilization: '10.75' }, + memory: { capacity: 141, utilization: '22.75' }, + disk: { capacity: 50, utilization: '32.75' }, + }, + { + id: 2, + name: 'Tenant2', + cpu: { capacity: 36, utilization: '20.75' }, + memory: { capacity: 121, utilization: '40.75' }, + disk: { capacity: 50, utilization: '60.75' }, + }, + { + id: 3, + name: 'Tenants3', + cpu: { capacity: 36, utilization: '6.75' }, + memory: { capacity: 141, utilization: '2.75' }, + disk: { capacity: 50, utilization: '22.75' }, + }, + { + id: 4, + name: 'Tenants4', + cpu: { capacity: 36, utilization: '6.75' }, + memory: { capacity: 141, utilization: '22.75' }, + disk: { capacity: 50, utilization: '22.75' }, + }, + ], + }) + +export const getProjectList: () => Promise = () => + Promise.resolve([ + { + id: 1, + name: 'Project-1', + description: 'Description of Project-1', + status: 'Active', + totalVms: 40, + activeVms: 50, + healthStatus: '80%', + }, + { + id: 2, + name: 'Project-2', + description: 'Description of Project-2', + status: 'Active', + totalVms: 40, + activeVms: 50, + healthStatus: '80%', + }, + { + id: 3, + name: 'Project-3', + description: 'Description of Project-3', + status: 'Active', + totalVms: 40, + activeVms: 50, + healthStatus: '80%', + }, + { + id: 4, + name: 'Project-4', + description: 'Description of Project-4', + status: 'Active', + totalVms: 40, + activeVms: 50, + healthStatus: '80%', + }, + { + id: 5, + name: 'Project-5', + description: 'Description of Project-5', + status: 'Active', + totalVms: 40, + activeVms: 50, + healthStatus: '80%', + }, + ]) + +export const getVMList: () => Promise = () => + Promise.resolve([ + { + id: 1, + name: 'PIS-Web-Server-01', + ipAddress: '192.168.1.101', + status: 'running', + cpu: 40, + memory: 50, + disk: 80, + }, + { + id: 2, + name: 'PIS-DB-Server-01', + ipAddress: '192.168.1.102', + status: 'running', + cpu: 40, + memory: 50, + disk: 80, + }, + ]) + +export const getCustomerListData: () => Promise = () => + Promise.resolve([ + { + id: 1, + name: 'Tenants1', + status: 'ACTIVE', + project: 2, + totalVms: 14, + healthStatus: '80%', + activeVms: 2, + }, + { + id: 2, + name: 'Tenant2', + status: 'INACTIVE', + project: 34, + totalVms: 4, + healthStatus: '20%', + activeVms: 1, + }, + { + id: 3, + name: 'Tenants3', + status: 'INACTIVE', + project: 34, + totalVms: 4, + healthStatus: '20%', + activeVms: 1, + }, + { + id: 4, + name: 'tenants4', + status: 'ACTIVE', + project: 34, + totalVms: 4, + healthStatus: '30%', + activeVms: 1, + }, + ]) + +export const getProjectOverViewCards: () => Promise = () => + Promise.resolve([ + { + tooltipContent: '', + dataTestId: 'cpu_id', + metricValue: '16', + metricTitle: 'CPU', + iconName: 'ic-bg-cpu', + }, + { + tooltipContent: '', + dataTestId: 'disk_id', + metricValue: '400', + metricTitle: 'DISK', + iconName: 'ic-bg-cpu', + }, + { + tooltipContent: '', + dataTestId: 'memory_id', + metricValue: '1000', + metricTitle: 'MEMORY', + iconName: 'ic-bg-cpu', + }, + { + tooltipContent: '', + dataTestId: 'running_id', + metricValue: '10', + metricTitle: 'RUNNING VMs', + iconName: 'ic-bg-cpu', + }, + ]) diff --git a/src/components/observability/styles.scss b/src/components/observability/styles.scss new file mode 100644 index 0000000000..020850c9f3 --- /dev/null +++ b/src/components/observability/styles.scss @@ -0,0 +1,45 @@ +.observability-overview { +flexbox-col dc__gap-32 bg__secondary p-20 + .cards-wrapper { + grid-template-columns: repeat(3, 1fr); + column-gap: 8px; + } + + .bar-chart-table-row { + grid-template-columns: 150px 1fr 1fr 1fr; + } + + .at-a-glance { + grid-template-columns: 1fr 1fr 1fr; + } + + .observability-table-wrapper { + + // Apply to every first child up to 4 levels deep + > :first-child, + > :first-child> :first-child, + > :first-child> :first-child> :first-child, + > :first-child> :first-child> :first-child> :first-child { + overflow: visible !important; + } + } + + .search-filter-section { + padding: 12px 20px; + display: flex; + flex-direction: row; + justify-content: space-between; + background-color: var(--bg-primary); + z-index: var(--filter-menu-index); + } + + .glance-cards-wrapper { + grid-template-columns: repeat(4, 1fr); + column-gap: 8px; + } + + .workflow-overview-cards-wrapper { + grid-template-columns: repeat(4, 1fr); + gap: 8px; + } +} diff --git a/src/components/observability/types.ts b/src/components/observability/types.ts new file mode 100644 index 0000000000..a59d59541c --- /dev/null +++ b/src/components/observability/types.ts @@ -0,0 +1,125 @@ +import { FiltersTypeEnum, IconName, TableProps } from '@devtron-labs/devtron-fe-common-lib/dist' + +import { ObservabilityGlanceMetricKeys } from './constants' + +export enum GlanceMetricsKeys { + REACHABLE_CUSTOMERS = 'customers', + TOTAL_PROJECTS = 'Projects', + TOTAL_VMs = 'vms', + HEALTH_STATUS = 'healthStatus', +} + +export type ObservabilityStatus = 'ACTIVE' | 'INACTIVE' + +export interface BaseObservability { + id: number + name: string + status: ObservabilityStatus +} + +export interface CustomerObservabilityDTO extends BaseObservability { + project: number + totalVms: number + activeVms: number + healthStatus: string +} + +export type CustomerTableProps = TableProps + +export interface ObservabilityProject + extends BaseObservability, + Pick { + description: string +} + +export type ProjectTableProps = TableProps + +export interface ObservabilityVM extends BaseObservability { + ipAddress: string + cpu: number + memory: number + disk: number +} + +export type VMTableProps = TableProps + +export enum ProjectListFields { + PROJECT_ID = 'id', + PROJECT_NAME = 'name', + PROJECT_DESCRIPTION = 'description', + STATUS = 'status', + TOTAL_VMS = 'totalVms', + ACTIVE_VMS = 'activeVms', + HEALTH_STATUS = 'healthStatus', + PROJECTS = 'projects', +} + +export enum VMListFields { + VM_ID = 'id', + VM_NAME = 'name', + VM_IPADDRESS = 'ipAddress', + VM_STATUS = 'status', + VM_CPU = 'cpu', + VM_MEMORY = 'memory', + VM_DISK = 'disk', +} + +export enum ObservabilityFilters { + customer = 'customer', + project = 'project', + vm = 'vm', +} + +export enum TabDetailsSegment { + 'OVERVIEW' = 'Overview', + 'PROJECTS' = 'Projects', +} + +export interface TabDetailsSearchParams { + tab: TabDetailsSegment +} + +export interface MetricsInfoCardProps { + iconName: IconName + metricTitle: string + dataTestId?: string + metricValue?: string + metricUnit?: string + valueOutOf?: string + tooltipContent?: string + redirectionLink?: string +} + +export interface GlanceConfigDTO { + [ObservabilityGlanceMetricKeys.TOTAL_CLUSTER]: number + [ObservabilityGlanceMetricKeys.TOTAL_VMS]: number + [ObservabilityGlanceMetricKeys.PROJECTS]: number + [ObservabilityGlanceMetricKeys.HEALTH_STATUS]: number + [ObservabilityGlanceMetricKeys.RUNNING_VMS]: number +} + +export interface CPUMemoryDiskUtilization { + capacity: number + utilization: string +} + +export interface ObservabilityMetricsDTO { + id: number + name: string + cpu: CPUMemoryDiskUtilization + memory: CPUMemoryDiskUtilization + disk: CPUMemoryDiskUtilization +} + +export interface ObservabilityOverviewDTO { + glanceConfig: Partial + metrics: ObservabilityMetricsDTO[] +} + +export interface BarMetricsProps { + data: ObservabilityOverviewDTO['metrics'] +} + +export interface ResourceCapacityDistributionTypes extends CPUMemoryDiskUtilization { + bgColor?: string +} diff --git a/src/components/observability/utils.tsx b/src/components/observability/utils.tsx new file mode 100644 index 0000000000..0423bb068f --- /dev/null +++ b/src/components/observability/utils.tsx @@ -0,0 +1,120 @@ +import { + CHART_COLORS, + ChartColorKey, + SelectPickerOptionType, + Tooltip, + useQuery, + useTheme, +} from '@devtron-labs/devtron-fe-common-lib' + +import { GLANCE_METRICS_CARDS_CONFIG, ObservabilityGlanceMetricKeys } from './constants' +import { getObservabilityData } from './service' +import { + MetricsInfoCardProps, + ObservabilityMetricsDTO, + ObservabilityOverviewDTO, + ResourceCapacityDistributionTypes, + TabDetailsSearchParams, + TabDetailsSegment, +} from './types' + +// Will be removing while importing to dashboard +export const MetricsInfoLoadingCard = () => ( +
+
+
+ + +
+
+
+
+) + +export const getObservabilityGlanceConfig = (result: Partial) => { + const glanceConfig = Object.entries(GLANCE_METRICS_CARDS_CONFIG).map( + ([key, config]: [ObservabilityGlanceMetricKeys, MetricsInfoCardProps]) => { + const entry = result.glanceConfig?.[key] + const isNumber = typeof entry === 'number' + const metricValue = isNumber ? entry : (entry as any)?.value + const metricTitle = config?.metricTitle + + return { + ...config, + dataTestId: key as string, + metricValue, + metricTitle, + } + }, + ) + return { + metrics: result?.metrics, + glanceConfig, + } +} + +export const useGetGlanceConfig = () => + useQuery({ + queryKey: ['observabilityGlanceConfig'], + // queryFn: () => getProjectOverViewCards(), // Mukesh has to update this + queryFn: async () => ({ + code: 200, + status: 'SUCCESS', + result: await getObservabilityData(), + }), + // queryFn: () => get(ROUTES.OBSERVABILITY_OVERVIEW), // Will be replacing later + select: ({ result }) => getObservabilityGlanceConfig(result), + }) + +export const parseChartDetailsSearchParams = (searchParams: URLSearchParams): TabDetailsSearchParams => ({ + tab: (searchParams.get('tab') as TabDetailsSegment) || TabDetailsSegment.OVERVIEW, +}) + +const AllocatedResource = ({ label, value }: SelectPickerOptionType) => ( +
+ {label} + {value} +
+) + +export const ResourceAllocationBar = ({ + capacity, + utilization, + bgColor = 'SkyBlue500', +}: ResourceCapacityDistributionTypes & { bgColor?: ChartColorKey }) => { + const { appTheme } = useTheme() + + return ( +
+
+ {/* Fill div up to utilisation percentage */} + +
+ +
+
+ +
+ +
+
+ ) +} + +export const CPUCapacityCellComponent = ({ cpu }: Pick) => ( + +) + +export const MemoryCapacityCellComponent = ({ memory }: Pick) => ( + +) + +export const DiskCapacityCellComponent = ({ disk }: Pick) => ( + +) diff --git a/src/components/project/ProjectList.tsx b/src/components/project/ProjectList.tsx index fd2b7dd8bc..799a63bbfc 100644 --- a/src/components/project/ProjectList.tsx +++ b/src/components/project/ProjectList.tsx @@ -20,7 +20,6 @@ import { Progressing, ErrorScreenManager, ErrorScreenNotAuthorized, - FeatureTitleWithInfo, ToastVariantType, ToastManager, PageHeader, diff --git a/src/config/routes.ts b/src/config/routes.ts index da95183c49..aa7a3baf14 100644 --- a/src/config/routes.ts +++ b/src/config/routes.ts @@ -106,6 +106,8 @@ export const URLS = { HELM_APP_LIST: generatePath(COMMON_URLS.INFRASTRUCTURE_MANAGEMENT_APP_LIST, { appType: InfrastructureManagementAppListType.HELM }), ARGO_APP_LIST: generatePath(COMMON_URLS.INFRASTRUCTURE_MANAGEMENT_APP_LIST, { appType: InfrastructureManagementAppListType.ARGO_CD }), FLUX_APP_LIST: generatePath(COMMON_URLS.INFRASTRUCTURE_MANAGEMENT_APP_LIST, { appType: InfrastructureManagementAppListType.FLUX_CD }), + PROJECT_OVERVIEW: `${COMMON_URLS.OBSERVABILITY_OVERVIEW}/project-overview`, + PROJECT_LIST: `${COMMON_URLS.OBSERVABILITY_OVERVIEW}/project-list`, BUILD: '/build', WEBHOOK_MODAL: 'webhook', MONITORING_DASHBOARD: 'monitoring-dashboard', diff --git a/vite.config.mts b/vite.config.mts index dcb785595b..e7d3de936c 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -31,7 +31,7 @@ import tsconfigPaths from 'vite-tsconfig-paths' import { compression, defineAlgorithm } from 'vite-plugin-compression2' const WRONG_CODE = `import { bpfrpt_proptype_WindowScroller } from "../WindowScroller.js";` -const TARGET_URL = 'https://preview.devtron.ai/' +const TARGET_URL = 'https://devtron-ent-7.devtron.info/' function reactVirtualized(): PluginOption { return {