diff --git a/frontend/src/component/admin/adminRoutes.ts b/frontend/src/component/admin/adminRoutes.ts index 5f4fdd7dbf..6ab6f00b24 100644 --- a/frontend/src/component/admin/adminRoutes.ts +++ b/frontend/src/component/admin/adminRoutes.ts @@ -65,7 +65,6 @@ export const adminRoutes: INavigationMenuItem[] = [ path: '/admin/network/*', title: 'Network', menu: { adminSettings: true, mode: ['pro', 'enterprise'] }, - configFlag: 'networkViewEnabled', group: 'instance', }, { diff --git a/frontend/src/component/admin/network/Network.tsx b/frontend/src/component/admin/network/Network.tsx index 6a7ae1ccb0..cd82e8f547 100644 --- a/frontend/src/component/admin/network/Network.tsx +++ b/frontend/src/component/admin/network/Network.tsx @@ -4,8 +4,12 @@ import { Tab, Tabs } from '@mui/material'; import { Route, Routes, useLocation } from 'react-router-dom'; import { TabLink } from 'component/common/TabNav/TabLink'; import { PageContent } from 'component/common/PageContent/PageContent'; +import { useUiFlag } from 'hooks/useUiFlag'; const NetworkOverview = lazy(() => import('./NetworkOverview/NetworkOverview')); +const NetworkConnectedEdges = lazy( + () => import('./NetworkConnectedEdges/NetworkConnectedEdges'), +); const NetworkTraffic = lazy(() => import('./NetworkTraffic/NetworkTraffic')); const NetworkTrafficUsage = lazy( () => import('./NetworkTrafficUsage/NetworkTrafficUsage'), @@ -20,6 +24,10 @@ const tabs = [ label: 'Traffic', path: '/admin/network/traffic', }, + { + label: 'Connected Edges', + path: '/admin/network/connected-edges', + }, { label: 'Data Usage', path: '/admin/network/data-usage', @@ -28,6 +36,11 @@ const tabs = [ export const Network = () => { const { pathname } = useLocation(); + const edgeObservabilityEnabled = useUiFlag('edgeObservability'); + + const filteredTabs = tabs.filter( + ({ label }) => label !== 'Connected Edges' || edgeObservabilityEnabled, + ); return (
@@ -41,7 +54,7 @@ export const Network = () => { variant='scrollable' allowScrollButtonsMobile > - {tabs.map(({ label, path }) => ( + {filteredTabs.map(({ label, path }) => ( { } > - } /> } /> + } /> + {edgeObservabilityEnabled && ( + } + /> + )} } diff --git a/frontend/src/component/admin/network/NetworkConnectedEdges/NetworkConnectedEdgeInstance.tsx b/frontend/src/component/admin/network/NetworkConnectedEdges/NetworkConnectedEdgeInstance.tsx new file mode 100644 index 0000000000..4758424298 --- /dev/null +++ b/frontend/src/component/admin/network/NetworkConnectedEdges/NetworkConnectedEdgeInstance.tsx @@ -0,0 +1,255 @@ +import { useLocationSettings } from 'hooks/useLocationSettings'; +import type { ConnectedEdge } from 'interfaces/connectedEdge'; +import CircleIcon from '@mui/icons-material/Circle'; +import ExpandMore from '@mui/icons-material/ExpandMore'; +import { formatDateYMDHMS } from 'utils/formatDate'; +import { + Accordion, + AccordionDetails, + AccordionSummary, + styled, + Tooltip, +} from '@mui/material'; +import { Badge } from 'component/common/Badge/Badge'; +import { HelpIcon } from 'component/common/HelpIcon/HelpIcon'; +import { NetworkConnectedEdgeInstanceLatency } from './NetworkConnectedEdgeInstanceLatency'; + +const StyledInstance = styled('div')(({ theme }) => ({ + borderRadius: theme.shape.borderRadiusMedium, + border: '1px solid', + borderColor: theme.palette.secondary.border, + backgroundColor: theme.palette.secondary.light, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + padding: 0, + zIndex: 1, + marginTop: theme.spacing(1), +})); + +const StyledAccordion = styled(Accordion)({ + background: 'transparent', + boxShadow: 'none', +}); + +const StyledAccordionSummary = styled(AccordionSummary, { + shouldForwardProp: (prop) => prop !== 'connectionStatus', +})<{ connectionStatus: InstanceConnectionStatus }>( + ({ theme, connectionStatus }) => ({ + fontSize: theme.fontSizes.smallBody, + padding: theme.spacing(1), + minHeight: theme.spacing(3), + '& .MuiAccordionSummary-content': { + alignItems: 'center', + gap: theme.spacing(1), + margin: 0, + '&.Mui-expanded': { + margin: 0, + }, + '& svg': { + fontSize: theme.fontSizes.mainHeader, + color: + connectionStatus === 'Stale' + ? theme.palette.warning.main + : connectionStatus === 'Disconnected' + ? theme.palette.error.main + : theme.palette.success.main, + }, + }, + }), +); + +const StyledAccordionDetails = styled(AccordionDetails)(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + fontSize: theme.fontSizes.smallerBody, + gap: theme.spacing(2), +})); + +const StyledDetailRow = styled('div')(({ theme }) => ({ + display: 'flex', + justifyContent: 'space-between', + gap: theme.spacing(2), + '& > span': { + display: 'flex', + alignItems: 'center', + }, +})); + +const StyledBadge = styled(Badge)(({ theme }) => ({ + padding: theme.spacing(0, 1), +})); + +const getConnectionStatus = ({ + reportedAt, +}: ConnectedEdge): InstanceConnectionStatus => { + const reportedTime = new Date(reportedAt).getTime(); + const reportedSecondsAgo = (Date.now() - reportedTime) / 1000; + + if (reportedSecondsAgo > 360) return 'Disconnected'; + if (reportedSecondsAgo > 180) return 'Stale'; + + return 'Connected'; +}; + +const getCPUPercentage = ({ + started, + reportedAt, + cpuUsage, +}: ConnectedEdge): string => { + const cpuUsageSeconds = Number(cpuUsage); + if (!cpuUsageSeconds) return 'No usage'; + + const startedTimestamp = new Date(started).getTime(); + const reportedTimestamp = new Date(reportedAt).getTime(); + + const totalRuntimeSeconds = (reportedTimestamp - startedTimestamp) / 1000; + if (totalRuntimeSeconds === 0) return 'No usage'; + + return `${((cpuUsageSeconds / totalRuntimeSeconds) * 100).toFixed(2)} %`; +}; + +const getMemory = ({ memoryUsage }: ConnectedEdge): string => { + if (!memoryUsage) return 'No usage'; + + const units = ['B', 'KB', 'MB', 'GB']; + let size = memoryUsage; + let unitIndex = 0; + + while (size >= 1024 && unitIndex < units.length - 1) { + size /= 1024; + unitIndex++; + } + + return `${size.toFixed(2)} ${units[unitIndex]}`; +}; + +type InstanceConnectionStatus = 'Connected' | 'Stale' | 'Disconnected'; + +interface INetworkConnectedEdgeInstanceProps { + instance: ConnectedEdge; +} + +export const NetworkConnectedEdgeInstance = ({ + instance, +}: INetworkConnectedEdgeInstanceProps) => { + const { locationSettings } = useLocationSettings(); + + const connectionStatus = getConnectionStatus(instance); + const start = formatDateYMDHMS(instance.started, locationSettings?.locale); + const lastReport = formatDateYMDHMS( + instance.reportedAt, + locationSettings?.locale, + ); + const cpuPercentage = getCPUPercentage(instance); + const memory = getMemory(instance); + const archWarning = cpuPercentage === 'No usage' && + memory === 'No usage' && ( +

Resource metrics are only available when running on Linux

+ ); + + return ( + + + } + connectionStatus={connectionStatus} + > + + + + {instance.id || instance.instanceId} + + + + ID + {instance.instanceId} + + + Upstream + {instance.connectedVia || 'Unleash'} + + + Status + + {connectionStatus} + + + + Start + {start} + + + Last report + {lastReport} + + + App name + {instance.appName} + + + Region + {instance.region || 'Unknown'} + + + Version + {instance.edgeVersion} + + + CPU + + {cpuPercentage}{' '} + +

+ CPU average usage since instance + started +

+ {archWarning} + + } + size='16px' + /> +
+
+ + Memory + + {memory}{' '} + +

Current memory usage

+ {archWarning} + + } + size='16px' + /> +
+
+ + Stream clients + {instance.connectedStreamingClients} + + + + +
+
+
+ ); +}; diff --git a/frontend/src/component/admin/network/NetworkConnectedEdges/NetworkConnectedEdgeInstanceLatency.tsx b/frontend/src/component/admin/network/NetworkConnectedEdges/NetworkConnectedEdgeInstanceLatency.tsx new file mode 100644 index 0000000000..45a6ee213d --- /dev/null +++ b/frontend/src/component/admin/network/NetworkConnectedEdges/NetworkConnectedEdgeInstanceLatency.tsx @@ -0,0 +1,75 @@ +import { styled } from '@mui/material'; +import type { ConnectedEdge } from 'interfaces/connectedEdge'; + +const StyledTable = styled('table')(({ theme }) => ({ + width: '100%', + borderCollapse: 'collapse', + fontSize: theme.fontSizes.smallerBody, + '& > thead': { + borderBottom: `1px solid ${theme.palette.text.primary}`, + }, + '& tr': { + textAlign: 'right', + '& > th:first-of-type,td:first-of-type': { + textAlign: 'left', + }, + }, +})); + +const StyledUpstreamSection = styled('tr')(({ theme }) => ({ + fontWeight: theme.fontWeight.bold, + borderBottom: `1px solid ${theme.palette.text.primary}`, + '& > td:first-of-type': { + paddingTop: theme.spacing(1), + }, +})); + +interface INetworkConnectedEdgeInstanceLatencyProps { + instance: ConnectedEdge; +} + +export const NetworkConnectedEdgeInstanceLatency = ({ + instance, +}: INetworkConnectedEdgeInstanceLatencyProps) => { + return ( + + + + Latency (ms) + Avg + p99 + + + + + Client Features + {instance.clientFeaturesAverageLatencyMs} + {instance.clientFeaturesP99LatencyMs} + + + Frontend + {instance.frontendApiAverageLatencyMs} + {instance.frontendApiP99LatencyMs} + + + Upstream + + + Client Features + {instance.upstreamFeaturesAverageLatencyMs} + {instance.upstreamFeaturesP99LatencyMs} + + + Metrics + {instance.upstreamMetricsAverageLatencyMs} + {instance.upstreamMetricsP99LatencyMs} + + + Edge + {instance.upstreamEdgeAverageLatencyMs} + {instance.upstreamEdgeP99LatencyMs} + + + + ); +}; diff --git a/frontend/src/component/admin/network/NetworkConnectedEdges/NetworkConnectedEdges.tsx b/frontend/src/component/admin/network/NetworkConnectedEdges/NetworkConnectedEdges.tsx new file mode 100644 index 0000000000..0be86202b1 --- /dev/null +++ b/frontend/src/component/admin/network/NetworkConnectedEdges/NetworkConnectedEdges.tsx @@ -0,0 +1,226 @@ +import { usePageTitle } from 'hooks/usePageTitle'; +import { ArcherContainer, ArcherElement } from 'react-archer'; +import { Alert, Typography, styled, useTheme } from '@mui/material'; +import { unknownify } from 'utils/unknownify'; +import { useMemo } from 'react'; +import { ReactComponent as LogoIcon } from 'assets/icons/logoBg.svg'; +import { ReactComponent as LogoIconWhite } from 'assets/icons/logoWhiteBg.svg'; +import { ThemeMode } from 'component/common/ThemeMode/ThemeMode'; +import { useConnectedEdges } from 'hooks/api/getters/useConnectedEdges/useConnectedEdges'; +import type { ConnectedEdge } from 'interfaces/connectedEdge'; +import { NetworkConnectedEdgeInstance } from './NetworkConnectedEdgeInstance'; + +const UNLEASH = 'Unleash'; + +const StyledUnleashLevel = styled('div')(({ theme }) => ({ + marginBottom: theme.spacing(18), + display: 'flex', + justifyContent: 'center', +})); + +const StyledEdgeLevel = styled('div')(({ theme }) => ({ + display: 'flex', + justifyContent: 'center', + gap: theme.spacing(4), + flexWrap: 'wrap', + marginTop: theme.spacing(2), +})); + +const StyledNode = styled('div')(({ theme }) => ({ + borderRadius: theme.shape.borderRadiusMedium, + border: `1px solid ${theme.palette.secondary.border}`, + backgroundColor: theme.palette.secondary.light, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + padding: theme.spacing(1.5), + zIndex: 1, + marginTop: theme.spacing(1), + '& > svg': { + width: theme.spacing(9), + height: theme.spacing(9), + }, +})); + +const StyledGroup = styled(StyledNode)({ + backgroundColor: 'transparent', +}); + +const StyledNodeHeader = styled(Typography)(({ theme }) => ({ + fontWeight: theme.fontWeight.bold, +})); + +const StyledNodeDescription = styled(Typography)(({ theme }) => ({ + fontSize: theme.fontSizes.smallerBody, + color: theme.palette.text.secondary, +})); + +type AppNameGroup = { + appName: string; + instances: ConnectedEdge[]; + groupTargets: Set; + level: number; +}; + +const createGroups = (edges: ConnectedEdge[]): Map => + edges.reduce((groups, edge) => { + if (!groups.has(edge.appName)) { + groups.set(edge.appName, { + appName: edge.appName, + instances: [], + groupTargets: new Set(), + level: 0, + }); + } + groups.get(edge.appName)!.instances.push(edge); + return groups; + }, new Map()); + +const computeGroupLevels = ( + groups: Map, +): Map => { + const memo = new Map(); + + const getLevel = (group: AppNameGroup): number => { + if (memo.has(group.appName)) return memo.get(group.appName)!; + let level = 0; + group.groupTargets.forEach((target) => { + if (target !== UNLEASH) { + const targetGroup = groups.get(target); + if (targetGroup) { + level = Math.max(level, getLevel(targetGroup) + 1); + } + } + }); + memo.set(group.appName, level); + return level; + }; + + groups.forEach((group) => { + group.level = getLevel(group); + }); + + const levelsMap = new Map(); + groups.forEach((group) => { + const levelGroups = levelsMap.get(group.level) || []; + levelGroups.push(group); + levelsMap.set(group.level, levelGroups); + }); + return levelsMap; +}; + +const processEdges = (edges: ConnectedEdge[]): Map => { + const groups = createGroups(edges); + const instanceIdToId = new Map(); + const idToAppName = new Map(); + + groups.forEach((group) => { + group.instances = group.instances + .sort((a, b) => a.instanceId.localeCompare(b.instanceId)) + .map((instance, index) => { + const id = `${group.appName}-${index + 1}`; + instanceIdToId.set(instance.instanceId, id); + idToAppName.set(id, group.appName); + return { ...instance, id }; + }); + }); + + groups.forEach((group) => { + group.instances = group.instances.map((instance) => ({ + ...instance, + connectedVia: instance.connectedVia + ? instanceIdToId.get(instance.connectedVia) || + instance.connectedVia + : UNLEASH, + })); + const targets = new Set(); + group.instances.forEach((instance) => { + if (!instance.connectedVia || instance.connectedVia === UNLEASH) { + targets.add(UNLEASH); + } else { + const targetApp = idToAppName.get(instance.connectedVia); + if (targetApp && targetApp !== group.appName) { + targets.add(targetApp); + } + } + }); + group.groupTargets = targets; + }); + + return computeGroupLevels(groups); +}; + +export const NetworkConnectedEdges = () => { + usePageTitle('Network - Connected Edges'); + const theme = useTheme(); + const { connectedEdges } = useConnectedEdges({ refreshInterval: 30_000 }); + + const edgeLevels = useMemo( + () => processEdges(connectedEdges), + [connectedEdges], + ); + const levels = Array.from(edgeLevels.keys()).sort((a, b) => a - b); + + if (edgeLevels.size === 0) + return No data available.; + + return ( + + + + + } + lightmode={} + /> + {UNLEASH} + + + + {levels.map((level) => ( + + {edgeLevels + .get(level) + ?.map(({ appName, groupTargets, instances }) => ( + ({ + targetId: target, + targetAnchor: 'bottom', + sourceAnchor: 'top', + style: { + strokeColor: + theme.palette.secondary.border, + }, + }), + )} + > + + + {unknownify(appName)} + + + {instances.length} instance + {instances.length !== 1 && 's'} + + {instances.map((instance) => ( + + ))} + + + ))} + + ))} + + ); +}; + +export default NetworkConnectedEdges; diff --git a/frontend/src/component/admin/network/NetworkOverview/NetworkOverview.tsx b/frontend/src/component/admin/network/NetworkOverview/NetworkOverview.tsx index a2c395a5c8..754bc38b05 100644 --- a/frontend/src/component/admin/network/NetworkOverview/NetworkOverview.tsx +++ b/frontend/src/component/admin/network/NetworkOverview/NetworkOverview.tsx @@ -11,6 +11,7 @@ import type { import { ReactComponent as LogoIcon } from 'assets/icons/logoBg.svg'; import { ReactComponent as LogoIconWhite } from 'assets/icons/logoWhiteBg.svg'; import { ThemeMode } from 'component/common/ThemeMode/ThemeMode'; +import { NetworkPrometheusAPIWarning } from '../NetworkPrometheusAPIWarning'; const StyleUnleashContainer = styled('div')(({ theme }) => ({ marginBottom: theme.spacing(18), @@ -122,7 +123,12 @@ export const NetworkOverview = () => { }, [metrics]); if (apps.length === 0) { - return No data available.; + return ( + + No data available. + + + ); } return ( diff --git a/frontend/src/component/admin/network/NetworkPrometheusAPIWarning.tsx b/frontend/src/component/admin/network/NetworkPrometheusAPIWarning.tsx new file mode 100644 index 0000000000..8353a1fa7c --- /dev/null +++ b/frontend/src/component/admin/network/NetworkPrometheusAPIWarning.tsx @@ -0,0 +1,24 @@ +import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; + +export const NetworkPrometheusAPIWarning = () => { + const { + uiConfig: { networkViewEnabled }, + } = useUiConfig(); + + if (networkViewEnabled) return null; + + return ( +

+ This view requires the PROMETHEUS_API environment + variable to be set. Refer to our{' '} + + documentation + {' '} + for more information. +

+ ); +}; diff --git a/frontend/src/component/admin/network/NetworkTraffic/NetworkTraffic.tsx b/frontend/src/component/admin/network/NetworkTraffic/NetworkTraffic.tsx index bff36affe7..3feca79007 100644 --- a/frontend/src/component/admin/network/NetworkTraffic/NetworkTraffic.tsx +++ b/frontend/src/component/admin/network/NetworkTraffic/NetworkTraffic.tsx @@ -28,6 +28,7 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit import { usePageTitle } from 'hooks/usePageTitle'; import { unknownify } from 'utils/unknownify'; import type { Theme } from '@mui/material/styles/createTheme'; +import { NetworkPrometheusAPIWarning } from '../NetworkPrometheusAPIWarning'; const pointStyles = ['circle', 'rect', 'rectRounded', 'rectRot', 'triangle']; @@ -206,7 +207,12 @@ export const NetworkTraffic: VFC = () => { return ( No data available.} + show={ + + No data available. + + + } elseShow={
diff --git a/frontend/src/component/layout/MainLayout/NavigationSidebar/__snapshots__/NavigationSidebar.test.tsx.snap b/frontend/src/component/layout/MainLayout/NavigationSidebar/__snapshots__/NavigationSidebar.test.tsx.snap index 07aaa5b624..83eb5ebe97 100644 --- a/frontend/src/component/layout/MainLayout/NavigationSidebar/__snapshots__/NavigationSidebar.test.tsx.snap +++ b/frontend/src/component/layout/MainLayout/NavigationSidebar/__snapshots__/NavigationSidebar.test.tsx.snap @@ -78,6 +78,10 @@ exports[`order of items in navigation > menu for enterprise plan 1`] = ` "icon": "AssignmentOutlinedIcon", "text": "Single sign-on", }, + { + "icon": "HubOutlinedIcon", + "text": "Network", + }, { "icon": "BuildOutlinedIcon", "text": "Maintenance", @@ -256,6 +260,10 @@ exports[`order of items in navigation > menu for pro plan 1`] = ` "icon": "AssignmentOutlinedIcon", "text": "Single sign-on", }, + { + "icon": "HubOutlinedIcon", + "text": "Network", + }, { "icon": "BuildOutlinedIcon", "text": "Maintenance", diff --git a/frontend/src/hooks/api/getters/useConnectedEdges/useConnectedEdges.ts b/frontend/src/hooks/api/getters/useConnectedEdges/useConnectedEdges.ts new file mode 100644 index 0000000000..4e7afd89c1 --- /dev/null +++ b/frontend/src/hooks/api/getters/useConnectedEdges/useConnectedEdges.ts @@ -0,0 +1,39 @@ +import { useMemo } from 'react'; +import useUiConfig from '../useUiConfig/useUiConfig'; +import { formatApiPath } from 'utils/formatPath'; +import handleErrorResponses from '../httpErrorResponseHandler'; +import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR'; +import type { ConnectedEdge } from 'interfaces/connectedEdge'; +import type { SWRConfiguration } from 'swr'; +import { useUiFlag } from 'hooks/useUiFlag'; + +const DEFAULT_DATA: ConnectedEdge[] = []; + +export const useConnectedEdges = (options?: SWRConfiguration) => { + const { isEnterprise } = useUiConfig(); + const edgeObservabilityEnabled = useUiFlag('edgeObservability'); + + const { data, error, mutate } = useConditionalSWR( + isEnterprise() && edgeObservabilityEnabled, + DEFAULT_DATA, + formatApiPath('api/admin/metrics/edges'), + fetcher, + options, + ); + + return useMemo( + () => ({ + connectedEdges: data ?? [], + loading: !error && !data, + refetch: () => mutate(), + error, + }), + [data, error, mutate], + ); +}; + +const fetcher = (path: string) => { + return fetch(path) + .then(handleErrorResponses('Connected Edges')) + .then((res) => res.json()); +}; diff --git a/frontend/src/hooks/api/getters/useInstanceMetrics/useInstanceMetrics.ts b/frontend/src/hooks/api/getters/useInstanceMetrics/useInstanceMetrics.ts index 31890661f8..f47dc34bd1 100644 --- a/frontend/src/hooks/api/getters/useInstanceMetrics/useInstanceMetrics.ts +++ b/frontend/src/hooks/api/getters/useInstanceMetrics/useInstanceMetrics.ts @@ -1,8 +1,10 @@ -import useSWR, { type SWRConfiguration } from 'swr'; +import type { SWRConfiguration } from 'swr'; import { useMemo } from 'react'; import { formatApiPath } from 'utils/formatPath'; import handleErrorResponses from '../httpErrorResponseHandler'; import type { RequestsPerSecondSchema } from 'openapi'; +import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR'; +import useUiConfig from '../useUiConfig/useUiConfig'; export interface IInstanceMetricsResponse { metrics: RequestsPerSecondSchema; @@ -17,7 +19,13 @@ export interface IInstanceMetricsResponse { export const useInstanceMetrics = ( options: SWRConfiguration = {}, ): IInstanceMetricsResponse => { - const { data, error, mutate } = useSWR( + const { + uiConfig: { networkViewEnabled }, + } = useUiConfig(); + + const { data, error, mutate } = useConditionalSWR( + networkViewEnabled, + {}, formatApiPath(`api/admin/metrics/rps`), fetcher, options, diff --git a/frontend/src/interfaces/connectedEdge.ts b/frontend/src/interfaces/connectedEdge.ts new file mode 100644 index 0000000000..dc4d9bba6f --- /dev/null +++ b/frontend/src/interfaces/connectedEdge.ts @@ -0,0 +1,23 @@ +export type ConnectedEdge = { + id?: string; + appName: string; + connectedStreamingClients: number; + edgeVersion: string; + instanceId: string; + region: string | null; + reportedAt: string; + started: string; + connectedVia?: string; + cpuUsage: string; + memoryUsage: number; + clientFeaturesAverageLatencyMs: string; + clientFeaturesP99LatencyMs: string; + frontendApiAverageLatencyMs: string; + frontendApiP99LatencyMs: string; + upstreamFeaturesAverageLatencyMs: string; + upstreamFeaturesP99LatencyMs: string; + upstreamMetricsAverageLatencyMs: string; + upstreamMetricsP99LatencyMs: string; + upstreamEdgeAverageLatencyMs: string; + upstreamEdgeP99LatencyMs: string; +}; diff --git a/frontend/src/interfaces/uiConfig.ts b/frontend/src/interfaces/uiConfig.ts index 9a881e4569..a3931c9ad1 100644 --- a/frontend/src/interfaces/uiConfig.ts +++ b/frontend/src/interfaces/uiConfig.ts @@ -94,6 +94,7 @@ export type UiFlags = { dataUsageMultiMonthView?: boolean; uiGlobalFontSize?: boolean; connectionCount?: boolean; + edgeObservability?: boolean; }; export interface IVersionInfo { diff --git a/src/lib/openapi/spec/ui-config-schema.ts b/src/lib/openapi/spec/ui-config-schema.ts index 705b7acabb..f121a1d52d 100644 --- a/src/lib/openapi/spec/ui-config-schema.ts +++ b/src/lib/openapi/spec/ui-config-schema.ts @@ -101,7 +101,7 @@ export const uiConfigSchema = { }, networkViewEnabled: { type: 'boolean', - description: 'Whether to enable the Unleash network view or not.', + description: 'Whether a Prometheus API is available.', example: true, }, frontendApiOrigins: { diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts index e1504613a6..5c9a96f01c 100644 --- a/src/lib/types/experimental.ts +++ b/src/lib/types/experimental.ts @@ -67,7 +67,8 @@ export type IFlagKey = | 'dataUsageMultiMonthView' | 'uiGlobalFontSize' | 'connectionCount' - | 'teamsIntegrationChangeRequests'; + | 'teamsIntegrationChangeRequests' + | 'edgeObservability'; export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>; @@ -324,6 +325,10 @@ const flags: IFlags = { process.env.EXPERIMENTAL_TEAMS_INTEGRATION_CHANGE_REQUESTS, false, ), + edgeObservability: parseEnvVarBoolean( + process.env.EXPERIMENTAL_EDGE_OBSERVABILITY, + false, + ), }; export const defaultExperimentalOptions: IExperimentalOptions = {