Skip to content

Commit

Permalink
feat: personal flag metrics display
Browse files Browse the repository at this point in the history
  • Loading branch information
kwasniew committed Sep 24, 2024
1 parent fee2143 commit ac7bd89
Show file tree
Hide file tree
Showing 5 changed files with 363 additions and 70 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import GeneralSelect, {
type IGeneralSelectProps,
} from 'component/common/GeneralSelect/GeneralSelect';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import { useEffect } from 'react';
import { type ReactNode, useEffect } from 'react';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';

const StyledTitle = styled('h2')(({ theme }) => ({
Expand All @@ -17,13 +17,15 @@ const StyledTitle = styled('h2')(({ theme }) => ({
interface IFeatureMetricsHoursProps {
hoursBack: number;
setHoursBack: (value: number) => void;
label?: ReactNode;
}

export const FEATURE_METRIC_HOURS_BACK_DEFAULT = 48;

export const FeatureMetricsHours = ({
hoursBack,
setHoursBack,
label = <StyledTitle>Period</StyledTitle>,
}: IFeatureMetricsHoursProps) => {
const { trackEvent } = usePlausibleTracker();

Expand Down Expand Up @@ -55,7 +57,7 @@ export const FeatureMetricsHours = ({

return (
<div>
<StyledTitle>Period</StyledTitle>
{label}
<GeneralSelect
name='feature-metrics-period'
id='feature-metrics-period'
Expand Down
201 changes: 140 additions & 61 deletions frontend/src/component/personalDashboard/FlagMetricsChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,27 @@ import {
BarElement,
CategoryScale,
Chart as ChartJS,
type ChartOptions,
Legend,
LinearScale,
Title,
Tooltip,
} from 'chart.js';
import annotationPlugin from 'chartjs-plugin-annotation';
import { Bar } from 'react-chartjs-2';
import type { Theme } from '@mui/material/styles/createTheme';
import useTheme from '@mui/material/styles/useTheme';
import { useMemo } from 'react';
import { formatTickValue } from 'component/common/Chart/formatTickValue';
import { type FC, useEffect, useMemo, useState } from 'react';
import { Box, Typography } from '@mui/material';
import { FeatureMetricsHours } from '../feature/FeatureView/FeatureMetrics/FeatureMetricsHours/FeatureMetricsHours';
import GeneralSelect from '../common/GeneralSelect/GeneralSelect';
import { useProjectEnvironments } from 'hooks/api/getters/useProjectEnvironments/useProjectEnvironments';
import { useFeatureMetricsRaw } from 'hooks/api/getters/useFeatureMetricsRaw/useFeatureMetricsRaw';
import { useLocationSettings } from 'hooks/useLocationSettings';
import { createChartData } from './createChartData';
import { aggregateFeatureMetrics } from '../feature/FeatureView/FeatureMetrics/aggregateFeatureMetrics';
import {
createBarChartOptions,
createPlaceholderBarChartOptions,
} from './createChartOptions';

const defaultYes = [
45_000_000, 28_000_000, 28_000_000, 25_000_000, 50_000_000, 27_000_000,
Expand All @@ -30,7 +39,7 @@ const defaultNo = [
3_000_000, 8_000_000, 2_000_000,
];

const data = {
const placeholderData = {
labels: Array.from({ length: 30 }, (_, i) => i + 1),
datasets: [
{
Expand All @@ -48,73 +57,143 @@ const data = {
],
};

const createBarChartOptions = (theme: Theme): ChartOptions<'bar'> => ({
plugins: {
legend: {
position: 'bottom',
labels: {
color: theme.palette.text.primary,
pointStyle: 'circle',
usePointStyle: true,
boxHeight: 6,
padding: 15,
boxPadding: 5,
},
},
tooltip: {
enabled: false,
},
},
responsive: true,
scales: {
x: {
stacked: true,
ticks: {
color: theme.palette.text.secondary,
},
grid: {
display: false,
},
},
y: {
stacked: true,
ticks: {
color: theme.palette.text.secondary,
maxTicksLimit: 5,
callback: formatTickValue,
},
grid: {
drawBorder: false,
},
},
},
elements: {
bar: {
borderRadius: 5,
},
},
interaction: {
mode: 'index',
intersect: false,
},
});

export const PlaceholderFlagMetricsChart = () => {
const theme = useTheme();

const options = useMemo(() => {
return createBarChartOptions(theme);
return createPlaceholderBarChartOptions(theme);
}, [theme]);

return (
<Bar
data={data}
options={options}
aria-label='A bar chart with a single feature flag exposure metrics'
<>
<Typography sx={{ mb: 4 }}>Feature flag metrics</Typography>
<Bar
data={placeholderData}
options={options}
aria-label='A placeholder bar chart with a single feature flag exposure metrics'
/>
</>
);
};

const useMetricsEnvironments = (project: string) => {
const [environment, setEnvironment] = useState<string | null>(null);
const { environments } = useProjectEnvironments(project);
const activeEnvironments = environments.filter((env) => env.enabled);
const firstProductionEnvironment = activeEnvironments.find(
(env) => env.type === 'production',
);

useEffect(() => {
if (firstProductionEnvironment) {
setEnvironment(firstProductionEnvironment.name);
} else if (activeEnvironments.length > 0) {
setEnvironment(activeEnvironments[0].name);
}
}, [JSON.stringify(activeEnvironments)]);

return { environment, setEnvironment, activeEnvironments };
};

const useFlagMetrics = (
flagName: string,
environment: string,
hoursBack: number,
) => {
const { featureMetrics: metrics = [] } = useFeatureMetricsRaw(
flagName,
hoursBack,
);
const sortedMetrics = useMemo(() => {
return [...metrics].sort((metricA, metricB) => {
return metricA.timestamp.localeCompare(metricB.timestamp);
});
}, [metrics]);
const filteredMetrics = useMemo(() => {
return aggregateFeatureMetrics(
sortedMetrics?.filter(
(metric) => environment === metric.environment,
),
).map((metric) => ({
...metric,
appName: 'all selected',
}));
}, [sortedMetrics, environment]);

const data = useMemo(() => {
return createChartData(filteredMetrics);
}, [filteredMetrics]);

const theme = useTheme();
const { locationSettings } = useLocationSettings();
const options = useMemo(() => {
return createBarChartOptions(theme, hoursBack, locationSettings);
}, [theme, hoursBack, locationSettings]);

return { data, options };
};

const EnvironmentSelect: FC<{
activeEnvironments: { name: string }[];
environment: string;
setEnvironment: () => void;
}> = ({ activeEnvironments, environment, setEnvironment }) => {
return (
<GeneralSelect
name='feature-environments'
id='feature-environments'
options={activeEnvironments.map((env) => ({
key: env.name,
label: env.name,
}))}
value={String(environment)}
onChange={setEnvironment}
/>
);
};

const MetricsSelectors = styled(Box)(({ theme }) => ({

Check failure on line 155 in frontend/src/component/personalDashboard/FlagMetricsChart.tsx

View workflow job for this annotation

GitHub Actions / build

src/hooks/useRecentlyVisited.test.tsx

ReferenceError: styled is not defined ❯ src/component/personalDashboard/FlagMetricsChart.tsx:155:26 ❯ src/component/personalDashboard/PersonalDashboard.tsx:10:31

Check failure on line 155 in frontend/src/component/personalDashboard/FlagMetricsChart.tsx

View workflow job for this annotation

GitHub Actions / build

src/component/changeRequest/ChangeRequest.test.tsx

ReferenceError: styled is not defined ❯ src/component/personalDashboard/FlagMetricsChart.tsx:155:26 ❯ src/component/personalDashboard/PersonalDashboard.tsx:10:31

Check failure on line 155 in frontend/src/component/personalDashboard/FlagMetricsChart.tsx

View workflow job for this annotation

GitHub Actions / build

src/component/menu/__tests__/routes.test.tsx

ReferenceError: styled is not defined ❯ src/component/personalDashboard/FlagMetricsChart.tsx:155:26 ❯ src/component/personalDashboard/PersonalDashboard.tsx:10:31

Check failure on line 155 in frontend/src/component/personalDashboard/FlagMetricsChart.tsx

View workflow job for this annotation

GitHub Actions / build

src/component/layout/MainLayout/NavigationSidebar/NavigationSidebar.test.tsx

ReferenceError: styled is not defined ❯ src/component/personalDashboard/FlagMetricsChart.tsx:155:26 ❯ src/component/personalDashboard/PersonalDashboard.tsx:10:31
display: 'flex',
justifyContent: 'flex-end',
gap: theme.spacing(2),
mb: theme.spacing(6),
}));

export const FlagMetricsChart: FC<{
flag: { name: string; project: string };
}> = ({ flag }) => {
const [hoursBack, setHoursBack] = useState(48);

const { environment, setEnvironment, activeEnvironments } =
useMetricsEnvironments(flag.project);

const { data, options } = useFlagMetrics(flag.name, environment, hoursBack);

return (
<>
<MetricsSelectors>
<EnvironmentSelect
project={flag.project}
environment={environment}
setEnvironment={setEnvironment}
activeEnvironments={activeEnvironments}
/>
<FeatureMetricsHours
hoursBack={hoursBack}
setHoursBack={setHoursBack}
label={null}
/>
</MetricsSelectors>

<Bar
data={data}
options={options}
aria-label='A bar chart with a single feature flag exposure metrics'
/>
</>
);
};

ChartJS.register(
annotationPlugin,
CategoryScale,
Expand Down
23 changes: 16 additions & 7 deletions frontend/src/component/personalDashboard/PersonalDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@ import { useProfile } from 'hooks/api/getters/useProfile/useProfile';
import LinkIcon from '@mui/icons-material/Link';
import { Badge } from '../common/Badge/Badge';
import { ConnectSDK, CreateFlag } from './ConnectSDK';
import { PlaceholderFlagMetricsChart } from './FlagMetricsChart';
import {
FlagMetricsChart,
PlaceholderFlagMetricsChart,
} from './FlagMetricsChart';
import { WelcomeDialog } from './WelcomeDialog';
import { useLocalStorageState } from 'hooks/useLocalStorageState';
import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview';
import { ProjectSetupComplete } from './ProjectSetupComplete';
import { usePersonalDashboard } from 'hooks/api/getters/usePersonalDashboard/usePersonalDashboard';
import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons';
import type { PersonalDashboardSchema } from '../../openapi';

const ScreenExplanation = styled(Typography)(({ theme }) => ({
marginTop: theme.spacing(1),
Expand Down Expand Up @@ -178,10 +182,12 @@ export const PersonalDashboard = () => {
const { projects, activeProject, setActiveProject } = useProjects();

const { personalDashboard } = usePersonalDashboard();
const [activeFlag, setActiveFlag] = useState<string | null>(null);
const [activeFlag, setActiveFlag] = useState<
PersonalDashboardSchema['flags'][0] | null
>(null);
useEffect(() => {
if (personalDashboard?.flags.length) {
setActiveFlag(personalDashboard.flags[0].name);
setActiveFlag(personalDashboard.flags[0]);
}
}, [JSON.stringify(personalDashboard)]);

Expand Down Expand Up @@ -307,8 +313,8 @@ export const PersonalDashboard = () => {
<FlagListItem
key={flag.name}
flag={flag}
selected={flag.name === activeFlag}
onClick={() => setActiveFlag(flag.name)}
selected={flag.name === activeFlag?.name}
onClick={() => setActiveFlag(flag)}
/>
))}
</List>
Expand All @@ -321,8 +327,11 @@ export const PersonalDashboard = () => {
</SpacedGridItem>

<SpacedGridItem item lg={8} md={1}>
<Typography sx={{ mb: 4 }}>Feature flag metrics</Typography>
<PlaceholderFlagMetricsChart />
{activeFlag ? (
<FlagMetricsChart flag={activeFlag} />
) : (
<PlaceholderFlagMetricsChart />
)}
</SpacedGridItem>
</ContentGrid>
<WelcomeDialog
Expand Down
42 changes: 42 additions & 0 deletions frontend/src/component/personalDashboard/createChartData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { IFeatureMetricsRaw } from 'interfaces/featureToggle';
import type { ChartData } from 'chart.js';
import 'chartjs-adapter-date-fns';

export interface IPoint {
x: string;
y: number;
variants: Record<string, number>;
}

export const createChartData = (
metrics: IFeatureMetricsRaw[],
): ChartData<'bar', IPoint[], string> => {
const yesSeries = {
label: 'Exposed',
hoverBackgroundColor: '#A39EFF',
backgroundColor: '#A39EFF',
data: createChartPoints(metrics, (m) => m.yes),
};

const noSeries = {
label: 'Not exposed',
hoverBackgroundColor: '#D8D6FF',
backgroundColor: '#D8D6FF',
data: createChartPoints(metrics, (m) => m.no),
};

return {
datasets: [yesSeries, noSeries],
};
};

const createChartPoints = (
metrics: IFeatureMetricsRaw[],
y: (m: IFeatureMetricsRaw) => number,
): IPoint[] => {
return metrics.map((metric) => ({
x: metric.timestamp,
y: y(metric),
variants: metric.variants || {},
}));
};
Loading

0 comments on commit ac7bd89

Please sign in to comment.