From ed73f760922cb6f14309f94f2e69b9b4b0c91315 Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Mon, 30 Jun 2025 13:49:48 +0200 Subject: [PATCH] refactor: move impact metrics to a separate page --- .../common/BreadcrumbNav/BreadcrumbNav.tsx | 5 + .../impact-metrics/ImpactMetrics.tsx | 207 +++++++++++++++++ .../impact-metrics/ImpactMetricsControls.tsx | 0 .../impact-metrics/ImpactMetricsPage.tsx | 33 +++ .../impact-metrics/hooks/useChartData.ts | 0 .../impact-metrics/hooks/useSeriesColor.ts | 0 .../{insights => }/impact-metrics/utils.ts | 0 frontend/src/component/insights/Insights.tsx | 26 +-- .../insights/impact-metrics/ImpactMetrics.tsx | 216 ------------------ .../NavigationSidebar/IconRenderer.tsx | 2 + .../NavigationSidebar/NavigationList.tsx | 18 +- .../__snapshots__/routes.test.tsx.snap | 11 + frontend/src/component/menu/routes.ts | 12 + 13 files changed, 289 insertions(+), 241 deletions(-) create mode 100644 frontend/src/component/impact-metrics/ImpactMetrics.tsx rename frontend/src/component/{insights => }/impact-metrics/ImpactMetricsControls.tsx (100%) create mode 100644 frontend/src/component/impact-metrics/ImpactMetricsPage.tsx rename frontend/src/component/{insights => }/impact-metrics/hooks/useChartData.ts (100%) rename frontend/src/component/{insights => }/impact-metrics/hooks/useSeriesColor.ts (100%) rename frontend/src/component/{insights => }/impact-metrics/utils.ts (100%) delete mode 100644 frontend/src/component/insights/impact-metrics/ImpactMetrics.tsx diff --git a/frontend/src/component/common/BreadcrumbNav/BreadcrumbNav.tsx b/frontend/src/component/common/BreadcrumbNav/BreadcrumbNav.tsx index 937dd38814..12a05f4ab3 100644 --- a/frontend/src/component/common/BreadcrumbNav/BreadcrumbNav.tsx +++ b/frontend/src/component/common/BreadcrumbNav/BreadcrumbNav.tsx @@ -57,6 +57,11 @@ const BreadcrumbNav = () => { return null; } + if (location.pathname === '/impact-metrics') { + // Hide breadcrumb on Impact Metrics page + return null; + } + if (paths.length === 1 && paths[0] === 'projects-archive') { // It's not possible to use `projects/archive`, because it's :projectId path paths = ['projects', 'archive']; diff --git a/frontend/src/component/impact-metrics/ImpactMetrics.tsx b/frontend/src/component/impact-metrics/ImpactMetrics.tsx new file mode 100644 index 0000000000..c5043dee58 --- /dev/null +++ b/frontend/src/component/impact-metrics/ImpactMetrics.tsx @@ -0,0 +1,207 @@ +import type { FC } from 'react'; +import { useMemo, useState } from 'react'; +import { Box, Typography, Alert } from '@mui/material'; +import { + LineChart, + NotEnoughData, +} from '../insights/components/LineChart/LineChart.tsx'; +import { + StyledChartContainer, + StyledWidget, + StyledWidgetStats, +} from 'component/insights/InsightsCharts.styles'; +import { useImpactMetricsMetadata } from 'hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata'; +import { useImpactMetricsData } from 'hooks/api/getters/useImpactMetricsData/useImpactMetricsData'; +import { usePlaceholderData } from '../insights/hooks/usePlaceholderData.js'; +import { ImpactMetricsControls } from './ImpactMetricsControls.tsx'; +import { getDisplayFormat, getTimeUnit, formatLargeNumbers } from './utils.ts'; +import { fromUnixTime } from 'date-fns'; +import { useChartData } from './hooks/useChartData.ts'; + +export const ImpactMetrics: FC = () => { + const [selectedSeries, setSelectedSeries] = useState(''); + const [selectedRange, setSelectedRange] = useState< + 'hour' | 'day' | 'week' | 'month' + >('day'); + const [beginAtZero, setBeginAtZero] = useState(false); + const [selectedLabels, setSelectedLabels] = useState< + Record + >({}); + + const handleSeriesChange = (series: string) => { + setSelectedSeries(series); + setSelectedLabels({}); // labels are series-specific + }; + + const { + metadata, + loading: metadataLoading, + error: metadataError, + } = useImpactMetricsMetadata(); + const { + data: { start, end, series: timeSeriesData, labels: availableLabels }, + loading: dataLoading, + error: dataError, + } = useImpactMetricsData( + selectedSeries + ? { + series: selectedSeries, + range: selectedRange, + labels: + Object.keys(selectedLabels).length > 0 + ? selectedLabels + : undefined, + } + : undefined, + ); + + const placeholderData = usePlaceholderData({ + fill: true, + type: 'constant', + }); + + const metricSeries = useMemo(() => { + if (!metadata?.series) { + return []; + } + return Object.entries(metadata.series).map(([name, rest]) => ({ + name, + ...rest, + })); + }, [metadata]); + + const data = useChartData(timeSeriesData); + + const hasError = metadataError || dataError; + const isLoading = metadataLoading || dataLoading; + const shouldShowPlaceholder = !selectedSeries || isLoading || hasError; + const notEnoughData = useMemo( + () => + !isLoading && + (!timeSeriesData || + timeSeriesData.length === 0 || + !data.datasets.some((d) => d.data.length > 1)), + [data, isLoading, timeSeriesData], + ); + + const minTime = start + ? fromUnixTime(Number.parseInt(start, 10)) + : undefined; + const maxTime = end ? fromUnixTime(Number.parseInt(end, 10)) : undefined; + + const placeholder = selectedSeries ? ( + + ) : ( + + ); + const cover = notEnoughData ? placeholder : isLoading; + + return ( + + + ({ + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(2), + width: '100%', + })} + > + + + {!selectedSeries && !isLoading ? ( + + Select a metric series to view the chart + + ) : null} + + + + + {hasError ? ( + + Failed to load impact metrics. Please check if + Prometheus is configured and the feature flag is + enabled. + + ) : null} + + typeof value === 'number' + ? formatLargeNumbers( + value, + ) + : (value as number), + }, + }, + }, + plugins: { + legend: { + display: + timeSeriesData && + timeSeriesData.length > 1, + position: 'bottom' as const, + labels: { + usePointStyle: true, + boxWidth: 8, + padding: 12, + }, + }, + }, + animations: { + x: { duration: 0 }, + y: { duration: 0 }, + }, + } + } + cover={cover} + /> + + + ); +}; diff --git a/frontend/src/component/insights/impact-metrics/ImpactMetricsControls.tsx b/frontend/src/component/impact-metrics/ImpactMetricsControls.tsx similarity index 100% rename from frontend/src/component/insights/impact-metrics/ImpactMetricsControls.tsx rename to frontend/src/component/impact-metrics/ImpactMetricsControls.tsx diff --git a/frontend/src/component/impact-metrics/ImpactMetricsPage.tsx b/frontend/src/component/impact-metrics/ImpactMetricsPage.tsx new file mode 100644 index 0000000000..9c03a8889b --- /dev/null +++ b/frontend/src/component/impact-metrics/ImpactMetricsPage.tsx @@ -0,0 +1,33 @@ +import type { FC } from 'react'; +import { styled, Typography } from '@mui/material'; +import { ImpactMetrics } from './ImpactMetrics.tsx'; +import { PageHeader } from 'component/common/PageHeader/PageHeader.tsx'; + +const StyledWrapper = styled('div')(({ theme }) => ({ + paddingTop: theme.spacing(2), +})); + +const StyledContainer = styled('div')(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(4), + paddingBottom: theme.spacing(4), +})); + +const pageName = 'Impact Metrics'; + +export const ImpactMetricsPage: FC = () => ( + + + + {pageName} + + } + /> + + + +); diff --git a/frontend/src/component/insights/impact-metrics/hooks/useChartData.ts b/frontend/src/component/impact-metrics/hooks/useChartData.ts similarity index 100% rename from frontend/src/component/insights/impact-metrics/hooks/useChartData.ts rename to frontend/src/component/impact-metrics/hooks/useChartData.ts diff --git a/frontend/src/component/insights/impact-metrics/hooks/useSeriesColor.ts b/frontend/src/component/impact-metrics/hooks/useSeriesColor.ts similarity index 100% rename from frontend/src/component/insights/impact-metrics/hooks/useSeriesColor.ts rename to frontend/src/component/impact-metrics/hooks/useSeriesColor.ts diff --git a/frontend/src/component/insights/impact-metrics/utils.ts b/frontend/src/component/impact-metrics/utils.ts similarity index 100% rename from frontend/src/component/insights/impact-metrics/utils.ts rename to frontend/src/component/impact-metrics/utils.ts diff --git a/frontend/src/component/insights/Insights.tsx b/frontend/src/component/insights/Insights.tsx index f5936916d9..bfe5cc50aa 100644 --- a/frontend/src/component/insights/Insights.tsx +++ b/frontend/src/component/insights/Insights.tsx @@ -7,27 +7,21 @@ import { StyledContainer } from './InsightsCharts.styles.ts'; import { LifecycleInsights } from './sections/LifecycleInsights.tsx'; import { PerformanceInsights } from './sections/PerformanceInsights.tsx'; import { UserInsights } from './sections/UserInsights.tsx'; -import { ImpactMetrics } from './impact-metrics/ImpactMetrics.tsx'; const StyledWrapper = styled('div')(({ theme }) => ({ paddingTop: theme.spacing(2), })); -const NewInsights: FC = () => { - const impactMetricsEnabled = useUiFlag('impactMetrics'); - - return ( - - - - {impactMetricsEnabled ? : null} - - - - - - ); -}; +const NewInsights: FC = () => ( + + + + + + + + +); export const Insights: FC<{ withCharts?: boolean }> = (props) => { const useNewInsights = useUiFlag('lifecycleMetrics'); diff --git a/frontend/src/component/insights/impact-metrics/ImpactMetrics.tsx b/frontend/src/component/insights/impact-metrics/ImpactMetrics.tsx deleted file mode 100644 index 816cb4521e..0000000000 --- a/frontend/src/component/insights/impact-metrics/ImpactMetrics.tsx +++ /dev/null @@ -1,216 +0,0 @@ -import type { FC } from 'react'; -import { useMemo, useState } from 'react'; -import { Box, Typography, Alert } from '@mui/material'; -import { - LineChart, - NotEnoughData, -} from '../components/LineChart/LineChart.tsx'; -import { InsightsSection } from '../sections/InsightsSection.tsx'; -import { - StyledChartContainer, - StyledWidget, - StyledWidgetStats, -} from 'component/insights/InsightsCharts.styles'; -import { useImpactMetricsMetadata } from 'hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata'; -import { useImpactMetricsData } from 'hooks/api/getters/useImpactMetricsData/useImpactMetricsData'; -import { usePlaceholderData } from '../hooks/usePlaceholderData.js'; -import { ImpactMetricsControls } from './ImpactMetricsControls.tsx'; -import { getDisplayFormat, getTimeUnit, formatLargeNumbers } from './utils.ts'; -import { fromUnixTime } from 'date-fns'; -import { useChartData } from './hooks/useChartData.ts'; - -export const ImpactMetrics: FC = () => { - const [selectedSeries, setSelectedSeries] = useState(''); - const [selectedRange, setSelectedRange] = useState< - 'hour' | 'day' | 'week' | 'month' - >('day'); - const [beginAtZero, setBeginAtZero] = useState(false); - const [selectedLabels, setSelectedLabels] = useState< - Record - >({}); - - const handleSeriesChange = (series: string) => { - setSelectedSeries(series); - setSelectedLabels({}); // labels are series-specific - }; - - const { - metadata, - loading: metadataLoading, - error: metadataError, - } = useImpactMetricsMetadata(); - const { - data: { start, end, series: timeSeriesData, labels: availableLabels }, - loading: dataLoading, - error: dataError, - } = useImpactMetricsData( - selectedSeries - ? { - series: selectedSeries, - range: selectedRange, - labels: - Object.keys(selectedLabels).length > 0 - ? selectedLabels - : undefined, - } - : undefined, - ); - - const placeholderData = usePlaceholderData({ - fill: true, - type: 'constant', - }); - - const metricSeries = useMemo(() => { - if (!metadata?.series) { - return []; - } - return Object.entries(metadata.series).map(([name, rest]) => ({ - name, - ...rest, - })); - }, [metadata]); - - const data = useChartData(timeSeriesData); - - const hasError = metadataError || dataError; - const isLoading = metadataLoading || dataLoading; - const shouldShowPlaceholder = !selectedSeries || isLoading || hasError; - const notEnoughData = useMemo( - () => - !isLoading && - (!timeSeriesData || - timeSeriesData.length === 0 || - !data.datasets.some((d) => d.data.length > 1)), - [data, isLoading, timeSeriesData], - ); - - const minTime = start - ? fromUnixTime(Number.parseInt(start, 10)) - : undefined; - const maxTime = end ? fromUnixTime(Number.parseInt(end, 10)) : undefined; - - const placeholder = selectedSeries ? ( - - ) : ( - - ); - const cover = notEnoughData ? placeholder : isLoading; - - return ( - - - - ({ - display: 'flex', - flexDirection: 'column', - gap: theme.spacing(2), - width: '100%', - })} - > - - - {!selectedSeries && !isLoading ? ( - - Select a metric series to view the chart - - ) : null} - - - - - {hasError ? ( - - Failed to load impact metrics. Please check if - Prometheus is configured and the feature flag is - enabled. - - ) : null} - - typeof value === 'number' - ? formatLargeNumbers( - value, - ) - : (value as number), - }, - }, - }, - plugins: { - legend: { - display: - timeSeriesData && - timeSeriesData.length > 1, - position: 'bottom' as const, - labels: { - usePointStyle: true, - boxWidth: 8, - padding: 12, - }, - }, - }, - animations: { - x: { duration: 0 }, - y: { duration: 0 }, - }, - } - } - cover={cover} - /> - - - - ); -}; diff --git a/frontend/src/component/layout/MainLayout/NavigationSidebar/IconRenderer.tsx b/frontend/src/component/layout/MainLayout/NavigationSidebar/IconRenderer.tsx index bec601a2af..447d542d22 100644 --- a/frontend/src/component/layout/MainLayout/NavigationSidebar/IconRenderer.tsx +++ b/frontend/src/component/layout/MainLayout/NavigationSidebar/IconRenderer.tsx @@ -15,6 +15,7 @@ import GroupsIcon from '@mui/icons-material/GroupsOutlined'; import RoleIcon from '@mui/icons-material/AdminPanelSettingsOutlined'; import SettingsIcon from '@mui/icons-material/Settings'; import InsightsIcon from '@mui/icons-material/Insights'; +import ImpactMetricsIcon from '@mui/icons-material/TrendingUpOutlined'; import ApiAccessIcon from '@mui/icons-material/KeyOutlined'; import SingleSignOnIcon from '@mui/icons-material/AssignmentOutlined'; import NetworkIcon from '@mui/icons-material/HubOutlined'; @@ -44,6 +45,7 @@ const icons: Record< > = { '/search': FlagOutlinedIcon, '/insights': InsightsIcon, + '/impact-metrics': ImpactMetricsIcon, '/applications': ApplicationsIcon, '/context': ContextFieldsIcon, '/feature-toggle-type': FlagTypesIcon, diff --git a/frontend/src/component/layout/MainLayout/NavigationSidebar/NavigationList.tsx b/frontend/src/component/layout/MainLayout/NavigationSidebar/NavigationList.tsx index bd77f94970..80f71f3310 100644 --- a/frontend/src/component/layout/MainLayout/NavigationSidebar/NavigationList.tsx +++ b/frontend/src/component/layout/MainLayout/NavigationSidebar/NavigationList.tsx @@ -12,6 +12,7 @@ import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import { useNewAdminMenu } from 'hooks/useNewAdminMenu'; import { AdminMenuNavigation } from '../AdminMenu/AdminNavigationItems.tsx'; import { ConfigurationAccordion } from './ConfigurationAccordion.tsx'; +import { useRoutes } from './useRoutes.ts'; export const OtherLinksList = () => { const { uiConfig } = useUiConfig(); @@ -38,6 +39,7 @@ export const PrimaryNavigationList: FC<{ onClick: (activeItem: string) => void; activeItem?: string; }> = ({ mode, setMode, onClick, activeItem }) => { + const { routes } = useRoutes(); const PrimaryListItem = ({ href, text, @@ -52,17 +54,15 @@ export const PrimaryNavigationList: FC<{ /> ); - const { isOss } = useUiConfig(); - return ( - - - - - {!isOss() ? ( - - ) : null} + {routes.primaryRoutes.map((route) => ( + + ))}