1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-10-27 11:02:16 +01:00
unleash.unleash/frontend/src/component/feature/FeatureView/FeatureMetrics/FeatureExposureMetrics.tsx
2025-08-27 13:24:41 +02:00

201 lines
7.7 KiB
TypeScript

import { useFeatureMetricsRaw } from 'hooks/api/getters/useFeatureMetricsRaw/useFeatureMetricsRaw';
import { PageContent } from 'component/common/PageContent/PageContent';
import { useEffect, useMemo, useState } from 'react';
import {
FEATURE_METRIC_HOURS_BACK_DEFAULT,
FeatureMetricsHours,
} from './FeatureMetricsHours/FeatureMetricsHours.tsx';
import type { IFeatureMetricsRaw } from 'interfaces/featureToggle';
import { Grid } from '@mui/material';
import { FeatureMetricsContent } from './FeatureMetricsContent/FeatureMetricsContent.tsx';
import { FeatureMetricsChips } from './FeatureMetricsChips/FeatureMetricsChips.tsx';
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { usePageTitle } from 'hooks/usePageTitle';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import {
ArrayParam,
NumberParam,
StringParam,
useQueryParams,
withDefault,
} from 'use-query-params';
import { aggregateFeatureMetrics } from './aggregateFeatureMetrics.ts';
import { PageHeader } from 'component/common/PageHeader/PageHeader.tsx';
export const FeatureExposureMetrics = () => {
const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId');
const environments = useFeatureMetricsEnvironments(projectId, featureId);
usePageTitle('Metrics');
const defaultEnvironment = Array.from(environments)[0];
const [query, setQuery] = useQueryParams({
environment: withDefault(StringParam, defaultEnvironment),
applications: withDefault(ArrayParam, []),
hoursBack: withDefault(NumberParam, FEATURE_METRIC_HOURS_BACK_DEFAULT),
});
const applications = useFeatureMetricsApplications(
featureId,
query.hoursBack || FEATURE_METRIC_HOURS_BACK_DEFAULT,
);
const allApplications = Array.from(applications);
const defaultApplication = allApplications[0];
const { environment: selectedEnvironment, hoursBack } = query;
const selectedApplications = query.applications.filter(
(item) => item !== null,
) as string[];
useEffect(() => {
if (query.applications && query.applications.length === 0) {
setQuery({ applications: allApplications });
}
}, [JSON.stringify(allApplications)]);
const allSelected = [...applications].every((element) =>
selectedApplications.includes(element),
);
const { featureMetrics } = useFeatureMetricsRaw(featureId, hoursBack);
// Keep a cache of the fetched metrics so that we can
// show the cached result while fetching new metrics.
const [cachedMetrics, setCachedMetrics] = useState<
Readonly<IFeatureMetricsRaw[]> | undefined
>(featureMetrics);
useEffect(() => {
featureMetrics && setCachedMetrics(featureMetrics);
}, [featureMetrics]);
const filteredMetrics = useMemo(() => {
return aggregateFeatureMetrics(
cachedMetrics
?.filter((metric) => selectedEnvironment === metric.environment)
.filter((metric) =>
selectedApplications.includes(metric.appName),
) || [],
).map((metric) => ({
...metric,
appName:
selectedApplications.length > 1
? 'all selected'
: metric.appName,
selectedApplications,
}));
}, [
cachedMetrics,
selectedEnvironment,
JSON.stringify(selectedApplications),
]);
if (!filteredMetrics) {
return null;
}
return (
<PageContent header={<PageHeader title='Exposure metrics' />}>
<Grid container component='header' spacing={2}>
<Grid item xs={12} md={4}>
<ConditionallyRender
condition={environments.size > 0}
show={
<FeatureMetricsChips
title='Environments'
values={environments}
selectedValues={[selectedEnvironment]}
toggleValue={(value) => {
setQuery({ environment: value });
}}
/>
}
/>
</Grid>
<Grid item xs={12} md={6}>
<ConditionallyRender
condition={applications.size > 0}
show={
<FeatureMetricsChips
title='Applications'
values={applications}
selectedValues={selectedApplications}
toggleValues={() => {
if (allSelected) {
setQuery({
applications: [defaultApplication],
});
} else {
setQuery({
applications: [...applications],
});
}
}}
toggleValue={(value) => {
if (selectedApplications.includes(value)) {
setQuery({
applications:
selectedApplications.filter(
(app) => app !== value,
),
});
} else {
setQuery({
applications: [
...selectedApplications,
value,
],
});
}
}}
/>
}
/>
</Grid>
<Grid item xs={12} md={2}>
<FeatureMetricsHours
hoursBack={hoursBack}
setHoursBack={(value) => setQuery({ hoursBack: value })}
/>
</Grid>
</Grid>
<FeatureMetricsContent
metrics={filteredMetrics}
hoursBack={hoursBack}
/>
</PageContent>
);
};
// Get all the environment names for a feature,
// not just the one's we have metrics for.
const useFeatureMetricsEnvironments = (
projectId: string,
featureId: string,
): Set<string> => {
const { feature } = useFeature(projectId, featureId);
const environments = feature.environments.map((environment) => {
return environment.name;
});
return new Set(environments);
};
// Get all application names for a feature. Respect current hoursBack since
// we can have different apps in hourly time spans and daily time spans
const useFeatureMetricsApplications = (
featureId: string,
hoursBack = FEATURE_METRIC_HOURS_BACK_DEFAULT,
): Set<string> => {
const { featureMetrics = [] } = useFeatureMetricsRaw(featureId, hoursBack);
const applications = featureMetrics.map((metric) => {
return metric.appName;
});
return new Set(applications);
};