mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-31 00:16:47 +01:00
Insights dashboard refactor (#6404)
- reorganized dashboard components - added share link - health chart aggregated data - refactored chart placeholders
This commit is contained in:
parent
493f8e8a5b
commit
4fc0a806f1
@ -12,7 +12,9 @@ export const allOption = { label: 'ALL', id: '*' };
|
||||
|
||||
interface IProjectSelectProps {
|
||||
selectedProjects: string[];
|
||||
onChange: Dispatch<SetStateAction<string[]>>;
|
||||
onChange:
|
||||
| Dispatch<SetStateAction<string[]>>
|
||||
| ((projects: string[]) => void);
|
||||
dataTestId?: string;
|
||||
sx?: SxProps;
|
||||
disabled?: boolean;
|
||||
|
@ -1,236 +1,195 @@
|
||||
import { useMemo, useState, VFC } from 'react';
|
||||
import { VFC } from 'react';
|
||||
import { Box, styled } from '@mui/material';
|
||||
import { ArrayParam, withDefault } from 'use-query-params';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { usePersistentTableState } from 'hooks/usePersistentTableState';
|
||||
import {
|
||||
Box,
|
||||
styled,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from '@mui/material';
|
||||
import { UsersChart } from './UsersChart/UsersChart';
|
||||
import { FlagsChart } from './FlagsChart/FlagsChart';
|
||||
import { useExecutiveDashboard } from 'hooks/api/getters/useExecutiveSummary/useExecutiveSummary';
|
||||
import { UserStats } from './UserStats/UserStats';
|
||||
import { FlagStats } from './FlagStats/FlagStats';
|
||||
import { Widget } from './Widget/Widget';
|
||||
import { FlagsProjectChart } from './FlagsProjectChart/FlagsProjectChart';
|
||||
import { ProjectHealthChart } from './ProjectHealthChart/ProjectHealthChart';
|
||||
import { TimeToProductionChart } from './TimeToProductionChart/TimeToProductionChart';
|
||||
import { TimeToProduction } from './TimeToProduction/TimeToProduction';
|
||||
import {
|
||||
ProjectSelect,
|
||||
allOption,
|
||||
} from '../common/ProjectSelect/ProjectSelect';
|
||||
import { MetricsSummaryChart } from './MetricsSummaryChart/MetricsSummaryChart';
|
||||
import {
|
||||
ExecutiveSummarySchemaMetricsSummaryTrendsItem,
|
||||
ExecutiveSummarySchemaProjectFlagTrendsItem,
|
||||
} from 'openapi';
|
||||
import { HealthStats } from './HealthStats/HealthStats';
|
||||
import { DashboardHeader } from './DashboardHeader/DashboardHeader';
|
||||
ProjectSelect,
|
||||
} from 'component/common/ProjectSelect/ProjectSelect';
|
||||
import { useExecutiveDashboard } from 'hooks/api/getters/useExecutiveSummary/useExecutiveSummary';
|
||||
|
||||
import { useFilteredFlagsSummary } from './hooks/useFilteredFlagsSummary';
|
||||
import { useFilteredTrends } from './hooks/useFilteredTrends';
|
||||
|
||||
import { Widget } from './components/Widget/Widget';
|
||||
import { DashboardHeader } from './components/DashboardHeader/DashboardHeader';
|
||||
|
||||
import { UserStats } from './componentsStat/UserStats/UserStats';
|
||||
import { FlagStats } from './componentsStat/FlagStats/FlagStats';
|
||||
import { HealthStats } from './componentsStat/HealthStats/HealthStats';
|
||||
import { TimeToProduction } from './componentsStat/TimeToProduction/TimeToProduction';
|
||||
|
||||
import { UsersChart } from './componentsChart/UsersChart/UsersChart';
|
||||
import { FlagsChart } from './componentsChart/FlagsChart/FlagsChart';
|
||||
import { FlagsProjectChart } from './componentsChart/FlagsProjectChart/FlagsProjectChart';
|
||||
import { ProjectHealthChart } from './componentsChart/ProjectHealthChart/ProjectHealthChart';
|
||||
import { TimeToProductionChart } from './componentsChart/TimeToProductionChart/TimeToProductionChart';
|
||||
import { MetricsSummaryChart } from './componentsChart/MetricsSummaryChart/MetricsSummaryChart';
|
||||
import { UsersPerProjectChart } from './componentsChart/UsersPerProjectChart/UsersPerProjectChart';
|
||||
|
||||
const StyledGrid = styled(Box)(({ theme }) => ({
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `300px 1fr`,
|
||||
gridTemplateColumns: `repeat(2, 1fr)`,
|
||||
gridAutoRows: 'auto',
|
||||
gap: theme.spacing(2),
|
||||
}));
|
||||
|
||||
const StyledBox = styled(Box)(({ theme }) => ({
|
||||
marginBottom: theme.spacing(4),
|
||||
marginTop: theme.spacing(4),
|
||||
[theme.breakpoints.down('lg')]: {
|
||||
width: '100%',
|
||||
marginLeft: 0,
|
||||
paddingBottom: theme.spacing(2),
|
||||
[theme.breakpoints.up('md')]: {
|
||||
gridTemplateColumns: `300px 1fr`,
|
||||
},
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}));
|
||||
|
||||
const useDashboardGrid = () => {
|
||||
const theme = useTheme();
|
||||
const isMediumScreen = useMediaQuery(theme.breakpoints.down('lg'));
|
||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
|
||||
if (isSmallScreen) {
|
||||
return {
|
||||
gridTemplateColumns: `1fr`,
|
||||
chartSpan: 1,
|
||||
userTrendsOrder: 3,
|
||||
flagStatsOrder: 2,
|
||||
largeChartSpan: 1,
|
||||
};
|
||||
}
|
||||
|
||||
if (isMediumScreen) {
|
||||
return {
|
||||
gridTemplateColumns: `1fr 1fr`,
|
||||
chartSpan: 2,
|
||||
userTrendsOrder: 3,
|
||||
flagStatsOrder: 2,
|
||||
largeChartSpan: 2,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
gridTemplateColumns: `300px auto`,
|
||||
chartSpan: 1,
|
||||
userTrendsOrder: 2,
|
||||
flagStatsOrder: 3,
|
||||
largeChartSpan: 2,
|
||||
};
|
||||
};
|
||||
|
||||
interface FilteredProjectData {
|
||||
filteredProjectFlagTrends: ExecutiveSummarySchemaProjectFlagTrendsItem[];
|
||||
filteredMetricsSummaryTrends: ExecutiveSummarySchemaMetricsSummaryTrendsItem[];
|
||||
}
|
||||
const ChartWidget = styled(Widget)(({ theme }) => ({
|
||||
[theme.breakpoints.down('md')]: {
|
||||
gridColumnStart: 'span 2',
|
||||
order: 2,
|
||||
},
|
||||
}));
|
||||
|
||||
export const ExecutiveDashboard: VFC = () => {
|
||||
const { executiveDashboardData, loading, error } = useExecutiveDashboard();
|
||||
const [projects, setProjects] = useState([allOption.id]);
|
||||
const stateConfig = {
|
||||
projects: withDefault(ArrayParam, [allOption.id]),
|
||||
};
|
||||
const [state, setState] = usePersistentTableState(`insights`, stateConfig);
|
||||
const setProjects = (projects: string[]) => {
|
||||
setState({ projects });
|
||||
};
|
||||
const projects = state.projects
|
||||
? (state.projects.filter(Boolean) as string[])
|
||||
: [];
|
||||
const showAllProjects = projects[0] === allOption.id;
|
||||
const projectsData = useFilteredTrends(
|
||||
executiveDashboardData.projectFlagTrends,
|
||||
projects,
|
||||
);
|
||||
const metricsData = useFilteredTrends(
|
||||
executiveDashboardData.metricsSummaryTrends,
|
||||
projects,
|
||||
);
|
||||
const { users } = executiveDashboardData;
|
||||
|
||||
const flagPerUsers = useMemo(() => {
|
||||
if (
|
||||
executiveDashboardData.users.total === 0 ||
|
||||
executiveDashboardData.flags.total === 0
|
||||
)
|
||||
return '0';
|
||||
|
||||
return (
|
||||
executiveDashboardData.flags.total /
|
||||
executiveDashboardData.users.total
|
||||
).toFixed(1);
|
||||
}, [executiveDashboardData]);
|
||||
|
||||
const { filteredProjectFlagTrends, filteredMetricsSummaryTrends } =
|
||||
useMemo<FilteredProjectData>(() => {
|
||||
if (projects[0] === allOption.id) {
|
||||
return {
|
||||
filteredProjectFlagTrends:
|
||||
executiveDashboardData.projectFlagTrends,
|
||||
filteredMetricsSummaryTrends:
|
||||
executiveDashboardData.metricsSummaryTrends,
|
||||
};
|
||||
}
|
||||
|
||||
const filteredProjectFlagTrends =
|
||||
executiveDashboardData.projectFlagTrends.filter((trend) =>
|
||||
projects.includes(trend.project),
|
||||
) as ExecutiveSummarySchemaProjectFlagTrendsItem[];
|
||||
|
||||
const filteredImpressionsSummary =
|
||||
executiveDashboardData.metricsSummaryTrends.filter((summary) =>
|
||||
projects.includes(summary.project),
|
||||
) as ExecutiveSummarySchemaMetricsSummaryTrendsItem[];
|
||||
|
||||
return {
|
||||
filteredProjectFlagTrends,
|
||||
filteredMetricsSummaryTrends: filteredImpressionsSummary,
|
||||
};
|
||||
}, [executiveDashboardData, projects]);
|
||||
|
||||
const {
|
||||
gridTemplateColumns,
|
||||
chartSpan,
|
||||
userTrendsOrder,
|
||||
flagStatsOrder,
|
||||
largeChartSpan,
|
||||
} = useDashboardGrid();
|
||||
const summary = useFilteredFlagsSummary(projectsData);
|
||||
const isOneProjectSelected = projects.length === 1;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box sx={(theme) => ({ paddingBottom: theme.spacing(4) })}>
|
||||
<DashboardHeader />
|
||||
<DashboardHeader
|
||||
actions={
|
||||
<ProjectSelect
|
||||
selectedProjects={projects}
|
||||
onChange={setProjects}
|
||||
dataTestId={'DASHBOARD_PROJECT_SELECT'}
|
||||
sx={{ flex: 1, maxWidth: '360px' }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
<StyledGrid sx={{ gridTemplateColumns }}>
|
||||
<Widget title='Total users' order={1}>
|
||||
<UserStats
|
||||
count={executiveDashboardData.users.total}
|
||||
active={executiveDashboardData.users.active}
|
||||
inactive={executiveDashboardData.users.inactive}
|
||||
/>
|
||||
</Widget>
|
||||
<Widget title='Users' order={userTrendsOrder} span={chartSpan}>
|
||||
<UsersChart
|
||||
userTrends={executiveDashboardData.userTrends}
|
||||
isLoading={loading}
|
||||
/>
|
||||
</Widget>
|
||||
<StyledGrid>
|
||||
<ConditionallyRender
|
||||
condition={showAllProjects}
|
||||
show={
|
||||
<Widget title='Total users'>
|
||||
<UserStats
|
||||
count={users.total}
|
||||
active={users.active}
|
||||
inactive={users.inactive}
|
||||
/>
|
||||
</Widget>
|
||||
}
|
||||
elseShow={
|
||||
<Widget
|
||||
title={
|
||||
isOneProjectSelected
|
||||
? 'Users in project'
|
||||
: 'Users per project on average'
|
||||
}
|
||||
>
|
||||
<UserStats count={summary.averageUsers} />
|
||||
</Widget>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={showAllProjects}
|
||||
show={
|
||||
<ChartWidget title='Users'>
|
||||
<UsersChart
|
||||
userTrends={executiveDashboardData.userTrends}
|
||||
isLoading={loading}
|
||||
/>
|
||||
</ChartWidget>
|
||||
}
|
||||
elseShow={
|
||||
<ChartWidget title='Users per project'>
|
||||
<UsersPerProjectChart
|
||||
projectFlagTrends={projectsData}
|
||||
/>
|
||||
</ChartWidget>
|
||||
}
|
||||
/>
|
||||
<Widget
|
||||
title='Total flags'
|
||||
tooltip='Total flags represent the total active flags (not archived) that currently exist across all projects of your application.'
|
||||
order={flagStatsOrder}
|
||||
tooltip='Active flags (not archived) that currently exist across selected projects.'
|
||||
>
|
||||
<FlagStats
|
||||
count={executiveDashboardData.flags.total}
|
||||
flagsPerUser={flagPerUsers}
|
||||
count={summary.total}
|
||||
flagsPerUser={
|
||||
showAllProjects
|
||||
? (summary.total / users.total).toFixed(2)
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
</Widget>
|
||||
<Widget title='Number of flags' order={4} span={chartSpan}>
|
||||
<FlagsChart
|
||||
flagTrends={executiveDashboardData.flagTrends}
|
||||
isLoading={loading}
|
||||
/>
|
||||
</Widget>
|
||||
</StyledGrid>
|
||||
<StyledBox>
|
||||
<Typography variant='h2' component='span'>
|
||||
Insights per project
|
||||
</Typography>
|
||||
<ProjectSelect
|
||||
selectedProjects={projects}
|
||||
onChange={setProjects}
|
||||
dataTestId={'DASHBOARD_PROJECT_SELECT'}
|
||||
sx={{ flex: 1, maxWidth: '360px' }}
|
||||
<ConditionallyRender
|
||||
condition={showAllProjects}
|
||||
show={
|
||||
<ChartWidget title='Number of flags'>
|
||||
<FlagsChart
|
||||
flagTrends={executiveDashboardData.flagTrends}
|
||||
isLoading={loading}
|
||||
/>
|
||||
</ChartWidget>
|
||||
}
|
||||
elseShow={
|
||||
<ChartWidget title='Flags per project'>
|
||||
<FlagsProjectChart
|
||||
projectFlagTrends={projectsData}
|
||||
/>
|
||||
</ChartWidget>
|
||||
}
|
||||
/>
|
||||
</StyledBox>
|
||||
<StyledGrid>
|
||||
<Widget
|
||||
title='Number of flags per project'
|
||||
order={5}
|
||||
span={largeChartSpan}
|
||||
>
|
||||
<FlagsProjectChart
|
||||
projectFlagTrends={filteredProjectFlagTrends}
|
||||
/>
|
||||
</Widget>
|
||||
<Widget title='Average health' order={6}>
|
||||
<Widget title='Average health'>
|
||||
<HealthStats
|
||||
// FIXME: data from API
|
||||
value={80}
|
||||
healthy={4}
|
||||
stale={1}
|
||||
potentiallyStale={0}
|
||||
value={summary.averageHealth}
|
||||
healthy={summary.active}
|
||||
stale={summary.stale}
|
||||
potentiallyStale={summary.potentiallyStale}
|
||||
/>
|
||||
</Widget>
|
||||
<Widget title='Health per project' order={7} span={chartSpan}>
|
||||
<ProjectHealthChart
|
||||
projectFlagTrends={filteredProjectFlagTrends}
|
||||
/>
|
||||
</Widget>
|
||||
<Widget
|
||||
title='Metrics over time per project'
|
||||
order={8}
|
||||
span={largeChartSpan}
|
||||
<ChartWidget
|
||||
title={
|
||||
showAllProjects ? 'Healthy flags' : 'Health per project'
|
||||
}
|
||||
>
|
||||
<MetricsSummaryChart
|
||||
metricsSummaryTrends={filteredMetricsSummaryTrends}
|
||||
<ProjectHealthChart
|
||||
projectFlagTrends={projectsData}
|
||||
isAggregate={showAllProjects}
|
||||
/>
|
||||
</Widget>
|
||||
|
||||
<Widget title='Average time to production' order={9}>
|
||||
</ChartWidget>
|
||||
{/* <Widget title='Average time to production'>
|
||||
<TimeToProduction
|
||||
//FIXME: data from API
|
||||
//FIXME: data from API
|
||||
daysToProduction={5.2}
|
||||
/>
|
||||
</Widget>
|
||||
<Widget title='Time to production' order={10} span={chartSpan}>
|
||||
<TimeToProductionChart
|
||||
projectFlagTrends={filteredProjectFlagTrends}
|
||||
/>
|
||||
</Widget>
|
||||
<ChartWidget title='Time to production'>
|
||||
<TimeToProductionChart projectFlagTrends={projectsData} />
|
||||
</ChartWidget> */}
|
||||
</StyledGrid>
|
||||
<Widget title='Metrics'>
|
||||
<MetricsSummaryChart metricsSummaryTrends={metricsData} />
|
||||
</Widget>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,27 +0,0 @@
|
||||
import { type VFC } from 'react';
|
||||
import 'chartjs-adapter-date-fns';
|
||||
import { ExecutiveSummarySchema } from 'openapi';
|
||||
import { LineChart } from '../LineChart/LineChart';
|
||||
import { useProjectChartData } from '../useProjectChartData';
|
||||
|
||||
interface IFlagsProjectChartProps {
|
||||
projectFlagTrends: ExecutiveSummarySchema['projectFlagTrends'];
|
||||
}
|
||||
|
||||
export const FlagsProjectChart: VFC<IFlagsProjectChartProps> = ({
|
||||
projectFlagTrends,
|
||||
}) => {
|
||||
const data = useProjectChartData(projectFlagTrends);
|
||||
return (
|
||||
<LineChart
|
||||
data={data}
|
||||
isLocalTooltip
|
||||
overrideOptions={{
|
||||
parsing: {
|
||||
yAxisKey: 'total',
|
||||
xAxisKey: 'date',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
@ -1,27 +0,0 @@
|
||||
import 'chartjs-adapter-date-fns';
|
||||
import { type VFC } from 'react';
|
||||
import { type ExecutiveSummarySchema } from 'openapi';
|
||||
import { HealthTooltip } from './HealthChartTooltip/HealthChartTooltip';
|
||||
import { LineChart } from '../LineChart/LineChart';
|
||||
import { useProjectChartData } from '../useProjectChartData';
|
||||
|
||||
interface IFlagsProjectChartProps {
|
||||
projectFlagTrends: ExecutiveSummarySchema['projectFlagTrends'];
|
||||
}
|
||||
|
||||
export const ProjectHealthChart: VFC<IFlagsProjectChartProps> = ({
|
||||
projectFlagTrends,
|
||||
}) => {
|
||||
const data = useProjectChartData(projectFlagTrends);
|
||||
|
||||
return (
|
||||
<LineChart
|
||||
data={data}
|
||||
isLocalTooltip
|
||||
TooltipComponent={HealthTooltip}
|
||||
overrideOptions={{
|
||||
parsing: { yAxisKey: 'health', xAxisKey: 'date' },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
@ -1,11 +1,17 @@
|
||||
import { VFC } from 'react';
|
||||
import { ReactNode, VFC } from 'react';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import { useFeedback } from 'component/feedbackNew/useFeedback';
|
||||
import { ReviewsOutlined } from '@mui/icons-material';
|
||||
import { Badge, Button, Typography } from '@mui/material';
|
||||
import { Button, Typography } from '@mui/material';
|
||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||
import { Badge } from 'component/common/Badge/Badge';
|
||||
import { ShareLink } from './ShareLink/ShareLink';
|
||||
|
||||
export const DashboardHeader: VFC = () => {
|
||||
type DashboardHeaderProps = {
|
||||
actions?: ReactNode;
|
||||
};
|
||||
|
||||
export const DashboardHeader: VFC<DashboardHeaderProps> = ({ actions }) => {
|
||||
const showInactiveUsers = useUiFlag('showInactiveUsers');
|
||||
|
||||
const { openFeedback } = useFeedback(
|
||||
@ -34,18 +40,21 @@ export const DashboardHeader: VFC = () => {
|
||||
gap: theme.spacing(1),
|
||||
})}
|
||||
>
|
||||
<span>Insights</span> <Badge color='warning'>Beta</Badge>
|
||||
<span>Insights</span> <Badge color='success'>Beta</Badge>
|
||||
</Typography>
|
||||
}
|
||||
actions={
|
||||
<Button
|
||||
startIcon={<ReviewsOutlined />}
|
||||
variant='outlined'
|
||||
onClick={createFeedbackContext}
|
||||
size='small'
|
||||
>
|
||||
Provide feedback
|
||||
</Button>
|
||||
<>
|
||||
{actions}
|
||||
<ShareLink />
|
||||
<Button
|
||||
startIcon={<ReviewsOutlined />}
|
||||
variant='outlined'
|
||||
onClick={createFeedbackContext}
|
||||
>
|
||||
Provide feedback
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
@ -0,0 +1,41 @@
|
||||
import { VFC, useState } from 'react';
|
||||
import { Share } from '@mui/icons-material';
|
||||
import { Box, Button, Typography } from '@mui/material';
|
||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||
import { LinkField } from 'component/admin/users/LinkField/LinkField';
|
||||
|
||||
export const ShareLink: VFC = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('share', 'true');
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
startIcon={<Share />}
|
||||
variant='outlined'
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
Share
|
||||
</Button>
|
||||
<Dialogue
|
||||
open={isOpen}
|
||||
onClick={() => setIsOpen(false)}
|
||||
primaryButtonText='Close'
|
||||
title='Share insights'
|
||||
>
|
||||
<Box>
|
||||
<Typography variant='body1'>
|
||||
Link below will lead to insights dashboard with
|
||||
currently selected filter.
|
||||
</Typography>
|
||||
<LinkField
|
||||
inviteLink={url.toString()}
|
||||
successTitle='Successfully copied the link.'
|
||||
errorTitle='Could not copy the link.'
|
||||
/>
|
||||
</Box>
|
||||
</Dialogue>
|
||||
</>
|
||||
);
|
||||
};
|
@ -92,7 +92,7 @@ const LineChartComponent: VFC<{
|
||||
}: { tooltip: TooltipState | null }) => ReturnType<VFC>;
|
||||
}> = ({
|
||||
data,
|
||||
aspectRatio,
|
||||
aspectRatio = 2.5,
|
||||
cover,
|
||||
isLocalTooltip,
|
||||
overrideOptions,
|
||||
@ -122,8 +122,8 @@ const LineChartComponent: VFC<{
|
||||
options={options}
|
||||
data={data}
|
||||
plugins={[customHighlightPlugin]}
|
||||
height={aspectRatio ? 100 : undefined}
|
||||
width={aspectRatio ? 100 * aspectRatio : undefined}
|
||||
height={100}
|
||||
width={100 * aspectRatio}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={!cover}
|
@ -62,7 +62,7 @@ export const createOptions = (
|
||||
x: {
|
||||
type: 'time',
|
||||
time: {
|
||||
unit: 'day',
|
||||
unit: 'week',
|
||||
tooltipFormat: 'PPP',
|
||||
},
|
||||
grid: {
|
||||
@ -72,6 +72,9 @@ export const createOptions = (
|
||||
ticks: {
|
||||
color: theme.palette.text.secondary,
|
||||
display: !isPlaceholder,
|
||||
source: 'data',
|
||||
maxRotation: 90,
|
||||
minRotation: 23.5,
|
||||
},
|
||||
min: format(subMonths(new Date(), 3), 'yyyy-MM-dd'),
|
||||
},
|
@ -12,13 +12,14 @@ export const legendOptions = {
|
||||
} = chart?.legend?.options || {
|
||||
labels: {},
|
||||
};
|
||||
|
||||
return (chart as any)._getSortedDatasetMetas().map((meta: any) => {
|
||||
const style = meta.controller.getStyle(
|
||||
usePointStyle ? 0 : undefined,
|
||||
);
|
||||
return {
|
||||
text: datasets[meta.index].label,
|
||||
fillStyle: style.backgroundColor,
|
||||
fillStyle: style.borderColor,
|
||||
fontColor: color,
|
||||
hidden: !meta.visible,
|
||||
lineWidth: 0,
|
@ -12,17 +12,9 @@ const StyledPaper = styled(Paper)(({ theme }) => ({
|
||||
|
||||
export const Widget: FC<{
|
||||
title: ReactNode;
|
||||
order?: number;
|
||||
span?: number;
|
||||
tooltip?: ReactNode;
|
||||
}> = ({ title, order, children, span = 1, tooltip }) => (
|
||||
<StyledPaper
|
||||
elevation={0}
|
||||
sx={{
|
||||
order,
|
||||
gridColumn: `span ${span}`,
|
||||
}}
|
||||
>
|
||||
}> = ({ title, children, tooltip, ...rest }) => (
|
||||
<StyledPaper elevation={0} {...rest}>
|
||||
<Typography
|
||||
variant='h3'
|
||||
sx={(theme) => ({
|
@ -2,7 +2,8 @@ import { useMemo, type VFC } from 'react';
|
||||
import 'chartjs-adapter-date-fns';
|
||||
import { useTheme } from '@mui/material';
|
||||
import { ExecutiveSummarySchema } from 'openapi';
|
||||
import { LineChart, NotEnoughData } from '../LineChart/LineChart';
|
||||
import { LineChart, NotEnoughData } from '../../components/LineChart/LineChart';
|
||||
import { usePlaceholderData } from 'component/executiveDashboard/hooks/usePlaceholderData';
|
||||
|
||||
interface IFlagsChartProps {
|
||||
flagTrends: ExecutiveSummarySchema['flagTrends'];
|
||||
@ -15,32 +16,7 @@ export const FlagsChart: VFC<IFlagsChartProps> = ({
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const notEnoughData = flagTrends.length < 2;
|
||||
const placeholderData = useMemo(
|
||||
() => ({
|
||||
labels: Array.from({ length: 15 }, (_, i) => i + 1).map(
|
||||
(i) =>
|
||||
new Date(Date.now() - (15 - i) * 7 * 24 * 60 * 60 * 1000),
|
||||
),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Total flags',
|
||||
data: [
|
||||
43, 66, 55, 65, 62, 72, 75, 73, 80, 65, 62, 61, 69, 70,
|
||||
77,
|
||||
],
|
||||
borderColor: theme.palette.primary.light,
|
||||
backgroundColor: theme.palette.primary.light,
|
||||
},
|
||||
{
|
||||
label: 'Stale',
|
||||
data: [3, 5, 4, 6, 2, 7, 5, 3, 8, 3, 5, 11, 8, 4, 3],
|
||||
borderColor: theme.palette.warning.border,
|
||||
backgroundColor: theme.palette.warning.border,
|
||||
},
|
||||
],
|
||||
}),
|
||||
[theme],
|
||||
);
|
||||
const placeholderData = usePlaceholderData({ fill: true, type: 'double' });
|
||||
|
||||
const data = useMemo(
|
||||
() => ({
|
@ -0,0 +1,38 @@
|
||||
import { useMemo, type VFC } from 'react';
|
||||
import 'chartjs-adapter-date-fns';
|
||||
import { ExecutiveSummarySchema } from 'openapi';
|
||||
import { LineChart, NotEnoughData } from '../../components/LineChart/LineChart';
|
||||
import { useProjectChartData } from 'component/executiveDashboard/hooks/useProjectChartData';
|
||||
import { usePlaceholderData } from 'component/executiveDashboard/hooks/usePlaceholderData';
|
||||
|
||||
interface IFlagsProjectChartProps {
|
||||
projectFlagTrends: ExecutiveSummarySchema['projectFlagTrends'];
|
||||
}
|
||||
|
||||
export const FlagsProjectChart: VFC<IFlagsProjectChartProps> = ({
|
||||
projectFlagTrends,
|
||||
}) => {
|
||||
const placeholderData = usePlaceholderData({
|
||||
type: 'constant',
|
||||
});
|
||||
|
||||
const data = useProjectChartData(projectFlagTrends);
|
||||
const notEnoughData = useMemo(
|
||||
() => (data.datasets.some((d) => d.data.length > 1) ? false : true),
|
||||
[data],
|
||||
);
|
||||
|
||||
return (
|
||||
<LineChart
|
||||
data={notEnoughData ? placeholderData : data}
|
||||
isLocalTooltip
|
||||
overrideOptions={{
|
||||
parsing: {
|
||||
yAxisKey: 'total',
|
||||
xAxisKey: 'date',
|
||||
},
|
||||
}}
|
||||
cover={notEnoughData ? <NotEnoughData /> : false}
|
||||
/>
|
||||
);
|
||||
};
|
@ -1,7 +1,7 @@
|
||||
import { type VFC } from 'react';
|
||||
import { ExecutiveSummarySchemaMetricsSummaryTrendsItem } from 'openapi';
|
||||
import { Box, Divider, Paper, styled, Typography } from '@mui/material';
|
||||
import { TooltipState } from '../../LineChart/ChartTooltip/ChartTooltip';
|
||||
import { TooltipState } from '../../../components/LineChart/ChartTooltip/ChartTooltip';
|
||||
|
||||
const StyledTooltipItemContainer = styled(Paper)(({ theme }) => ({
|
||||
padding: theme.spacing(2),
|
@ -1,9 +1,9 @@
|
||||
import { type VFC } from 'react';
|
||||
import 'chartjs-adapter-date-fns';
|
||||
import { ExecutiveSummarySchema } from 'openapi';
|
||||
import { LineChart } from '../LineChart/LineChart';
|
||||
import { useMetricsSummary } from '../useMetricsSummary';
|
||||
import { LineChart } from '../../components/LineChart/LineChart';
|
||||
import { MetricsSummaryTooltip } from './MetricsChartTooltip/MetricsChartTooltip';
|
||||
import { useMetricsSummary } from '../../hooks/useMetricsSummary';
|
||||
|
||||
interface IMetricsSummaryChartProps {
|
||||
metricsSummaryTrends: ExecutiveSummarySchema['metricsSummaryTrends'];
|
||||
@ -13,6 +13,7 @@ export const MetricsSummaryChart: VFC<IMetricsSummaryChartProps> = ({
|
||||
metricsSummaryTrends,
|
||||
}) => {
|
||||
const data = useMetricsSummary(metricsSummaryTrends);
|
||||
|
||||
return (
|
||||
<LineChart
|
||||
data={data}
|
@ -2,8 +2,8 @@ import { type VFC } from 'react';
|
||||
import { type ExecutiveSummarySchemaProjectFlagTrendsItem } from 'openapi';
|
||||
import { Box, Divider, Paper, Typography, styled } from '@mui/material';
|
||||
import { Badge } from 'component/common/Badge/Badge';
|
||||
import { TooltipState } from '../../LineChart/ChartTooltip/ChartTooltip';
|
||||
import { HorizontalDistributionChart } from '../../HorizontalDistributionChart/HorizontalDistributionChart';
|
||||
import { TooltipState } from '../../../components/LineChart/ChartTooltip/ChartTooltip';
|
||||
import { HorizontalDistributionChart } from '../../../components/HorizontalDistributionChart/HorizontalDistributionChart';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
|
||||
const StyledTooltipItemContainer = styled(Paper)(({ theme }) => ({
|
@ -0,0 +1,103 @@
|
||||
import 'chartjs-adapter-date-fns';
|
||||
import { useMemo, type VFC } from 'react';
|
||||
import { type ExecutiveSummarySchema } from 'openapi';
|
||||
import { HealthTooltip } from './HealthChartTooltip/HealthChartTooltip';
|
||||
import { useProjectChartData } from 'component/executiveDashboard/hooks/useProjectChartData';
|
||||
import {
|
||||
LineChart,
|
||||
NotEnoughData,
|
||||
} from 'component/executiveDashboard/components/LineChart/LineChart';
|
||||
import { useTheme } from '@mui/material';
|
||||
|
||||
interface IFlagsProjectChartProps {
|
||||
projectFlagTrends: ExecutiveSummarySchema['projectFlagTrends'];
|
||||
isAggregate?: boolean;
|
||||
}
|
||||
|
||||
export const ProjectHealthChart: VFC<IFlagsProjectChartProps> = ({
|
||||
projectFlagTrends,
|
||||
isAggregate,
|
||||
}) => {
|
||||
const projectsData = useProjectChartData(projectFlagTrends);
|
||||
const theme = useTheme();
|
||||
|
||||
const aggregateHealthData = useMemo(() => {
|
||||
const labels = Array.from(
|
||||
new Set(
|
||||
projectsData.datasets.flatMap((d) =>
|
||||
d.data.map((item) => item.week),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const weeks = labels
|
||||
.map((label) => {
|
||||
return projectsData.datasets
|
||||
.map((d) => d.data.find((item) => item.week === label))
|
||||
.reduce(
|
||||
(acc, item) => {
|
||||
if (item) {
|
||||
acc.total += item.total;
|
||||
acc.stale += item.stale + item.potentiallyStale;
|
||||
}
|
||||
if (!acc.date) {
|
||||
acc.date = item?.date;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
total: 0,
|
||||
stale: 0,
|
||||
week: label,
|
||||
} as {
|
||||
total: number;
|
||||
stale: number;
|
||||
week: string;
|
||||
date?: string;
|
||||
},
|
||||
);
|
||||
})
|
||||
.sort((a, b) => (a.week > b.week ? 1 : -1));
|
||||
|
||||
return {
|
||||
datasets: [
|
||||
{
|
||||
label: 'Health',
|
||||
data: weeks.map((item) => ({
|
||||
health: item.total
|
||||
? ((item.total - item.stale) / item.total) * 100
|
||||
: undefined,
|
||||
date: item.date,
|
||||
})),
|
||||
borderColor: theme.palette.primary.light,
|
||||
fill: false,
|
||||
order: 3,
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [projectsData, theme]);
|
||||
|
||||
const data = isAggregate ? aggregateHealthData : projectsData;
|
||||
const notEnoughData = useMemo(
|
||||
() =>
|
||||
projectsData.datasets.some((d) => d.data.length > 1) ? false : true,
|
||||
[projectsData],
|
||||
);
|
||||
|
||||
return (
|
||||
<LineChart
|
||||
key={isAggregate ? 'aggregate' : 'project'}
|
||||
data={data}
|
||||
isLocalTooltip
|
||||
TooltipComponent={isAggregate ? undefined : HealthTooltip}
|
||||
overrideOptions={
|
||||
notEnoughData
|
||||
? {}
|
||||
: {
|
||||
parsing: { yAxisKey: 'health', xAxisKey: 'date' },
|
||||
}
|
||||
}
|
||||
cover={notEnoughData ? <NotEnoughData /> : false}
|
||||
/>
|
||||
);
|
||||
};
|
@ -1,8 +1,8 @@
|
||||
import { type VFC } from 'react';
|
||||
import 'chartjs-adapter-date-fns';
|
||||
import { ExecutiveSummarySchema } from 'openapi';
|
||||
import { LineChart } from '../LineChart/LineChart';
|
||||
import { useProjectChartData } from '../useProjectChartData';
|
||||
import { LineChart } from '../../components/LineChart/LineChart';
|
||||
import { useProjectChartData } from '../../hooks/useProjectChartData';
|
||||
|
||||
interface IFlagsProjectChartProps {
|
||||
projectFlagTrends: ExecutiveSummarySchema['projectFlagTrends'];
|
||||
@ -12,7 +12,6 @@ export const TimeToProductionChart: VFC<IFlagsProjectChartProps> = ({
|
||||
projectFlagTrends,
|
||||
}) => {
|
||||
const data = useProjectChartData(projectFlagTrends);
|
||||
|
||||
return (
|
||||
<LineChart
|
||||
data={data}
|
@ -6,8 +6,9 @@ import {
|
||||
fillGradientPrimary,
|
||||
LineChart,
|
||||
NotEnoughData,
|
||||
} from '../LineChart/LineChart';
|
||||
} from '../../components/LineChart/LineChart';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import { usePlaceholderData } from 'component/executiveDashboard/hooks/usePlaceholderData';
|
||||
|
||||
interface IUsersChartProps {
|
||||
userTrends: ExecutiveSummarySchema['userTrends'];
|
||||
@ -21,28 +22,7 @@ export const UsersChart: VFC<IUsersChartProps> = ({
|
||||
const showInactiveUsers = useUiFlag('showInactiveUsers');
|
||||
const theme = useTheme();
|
||||
const notEnoughData = userTrends.length < 2;
|
||||
const placeholderData = useMemo(
|
||||
() => ({
|
||||
labels: Array.from({ length: 15 }, (_, i) => i + 1).map(
|
||||
(i) =>
|
||||
new Date(Date.now() - (15 - i) * 7 * 24 * 60 * 60 * 1000),
|
||||
),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Total users',
|
||||
data: [
|
||||
3, 5, 15, 17, 25, 40, 47, 48, 55, 65, 62, 72, 75, 73,
|
||||
80,
|
||||
],
|
||||
borderColor: theme.palette.primary.light,
|
||||
backgroundColor: fillGradientPrimary,
|
||||
fill: true,
|
||||
order: 3,
|
||||
},
|
||||
],
|
||||
}),
|
||||
[theme],
|
||||
);
|
||||
const placeholderData = usePlaceholderData({ fill: true, type: 'rising' });
|
||||
const data = useMemo(
|
||||
() => ({
|
||||
labels: userTrends.map((item) => item.date),
|
@ -0,0 +1,38 @@
|
||||
import { useMemo, type VFC } from 'react';
|
||||
import 'chartjs-adapter-date-fns';
|
||||
import { ExecutiveSummarySchema } from 'openapi';
|
||||
import { LineChart, NotEnoughData } from '../../components/LineChart/LineChart';
|
||||
import { useProjectChartData } from 'component/executiveDashboard/hooks/useProjectChartData';
|
||||
import { usePlaceholderData } from 'component/executiveDashboard/hooks/usePlaceholderData';
|
||||
|
||||
interface IUsersPerProjectChartProps {
|
||||
projectFlagTrends: ExecutiveSummarySchema['projectFlagTrends'];
|
||||
}
|
||||
|
||||
export const UsersPerProjectChart: VFC<IUsersPerProjectChartProps> = ({
|
||||
projectFlagTrends,
|
||||
}) => {
|
||||
const placeholderData = usePlaceholderData({
|
||||
type: 'constant',
|
||||
});
|
||||
|
||||
const data = useProjectChartData(projectFlagTrends);
|
||||
const notEnoughData = useMemo(
|
||||
() => (data.datasets.some((d) => d.data.length > 1) ? false : true),
|
||||
[data],
|
||||
);
|
||||
|
||||
return (
|
||||
<LineChart
|
||||
data={notEnoughData ? placeholderData : data}
|
||||
isLocalTooltip
|
||||
overrideOptions={{
|
||||
parsing: {
|
||||
yAxisKey: 'users',
|
||||
xAxisKey: 'date',
|
||||
},
|
||||
}}
|
||||
cover={notEnoughData ? <NotEnoughData /> : false}
|
||||
/>
|
||||
);
|
||||
};
|
@ -1,20 +1,6 @@
|
||||
import { Settings } from '@mui/icons-material';
|
||||
import { Box, Typography, styled } from '@mui/material';
|
||||
|
||||
const StyledContent = styled(Box)(({ theme }) => ({
|
||||
borderRadius: `${theme.shape.borderRadiusLarge}px`,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
maxWidth: 300,
|
||||
padding: theme.spacing(3),
|
||||
}));
|
||||
|
||||
const StyledHeader = styled(Typography)(({ theme }) => ({
|
||||
marginBottom: theme.spacing(3),
|
||||
fontSize: theme.fontSizes.bodySize,
|
||||
fontWeight: 'bold',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}));
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
|
||||
const StyledRingContainer = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
@ -79,7 +65,7 @@ const StyledSettingsIcon = styled(Settings)(({ theme }) => ({
|
||||
|
||||
interface IFlagStatsProps {
|
||||
count: number;
|
||||
flagsPerUser: string;
|
||||
flagsPerUser?: string;
|
||||
}
|
||||
|
||||
export const FlagStats: React.FC<IFlagStatsProps> = ({
|
||||
@ -94,22 +80,31 @@ export const FlagStats: React.FC<IFlagStatsProps> = ({
|
||||
</StyledRing>
|
||||
</StyledRingContainer>
|
||||
|
||||
<StyledInsightsContainer>
|
||||
<StyledTextContainer>
|
||||
<StyledHeaderContainer>
|
||||
<StyledSettingsIcon />
|
||||
<Typography
|
||||
fontWeight='bold'
|
||||
variant='body2'
|
||||
color='primary'
|
||||
>
|
||||
Insights
|
||||
</Typography>
|
||||
</StyledHeaderContainer>
|
||||
<Typography variant='body2'>Flags per user</Typography>
|
||||
</StyledTextContainer>
|
||||
<StyledFlagCountPerUser>{flagsPerUser}</StyledFlagCountPerUser>
|
||||
</StyledInsightsContainer>
|
||||
<ConditionallyRender
|
||||
condition={flagsPerUser !== undefined && flagsPerUser !== ''}
|
||||
show={
|
||||
<StyledInsightsContainer>
|
||||
<StyledTextContainer>
|
||||
<StyledHeaderContainer>
|
||||
<StyledSettingsIcon />
|
||||
<Typography
|
||||
fontWeight='bold'
|
||||
variant='body2'
|
||||
color='primary'
|
||||
>
|
||||
Insights
|
||||
</Typography>
|
||||
</StyledHeaderContainer>
|
||||
<Typography variant='body2'>
|
||||
Flags per user
|
||||
</Typography>
|
||||
</StyledTextContainer>
|
||||
<StyledFlagCountPerUser>
|
||||
{flagsPerUser}
|
||||
</StyledFlagCountPerUser>
|
||||
</StyledInsightsContainer>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
@ -3,7 +3,7 @@ import { useThemeMode } from 'hooks/useThemeMode';
|
||||
import { useTheme } from '@mui/material';
|
||||
|
||||
interface IHealthStatsProps {
|
||||
value: number;
|
||||
value?: string | number;
|
||||
healthy: number;
|
||||
stale: number;
|
||||
potentiallyStale: number;
|
@ -1,6 +1,6 @@
|
||||
import { VFC } from 'react';
|
||||
import { Typography, styled } from '@mui/material';
|
||||
import { Gauge } from '../Gauge/Gauge';
|
||||
import { Gauge } from '../../components/Gauge/Gauge';
|
||||
|
||||
const StyledContainer = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
@ -4,7 +4,7 @@ import { Box, Typography, styled } from '@mui/material';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { HorizontalDistributionChart } from '../HorizontalDistributionChart/HorizontalDistributionChart';
|
||||
import { HorizontalDistributionChart } from '../../components/HorizontalDistributionChart/HorizontalDistributionChart';
|
||||
import { UserDistributionInfo } from './UserDistributionInfo';
|
||||
|
||||
const StyledUserContainer = styled(Box)(({ theme }) => ({
|
||||
@ -81,7 +81,11 @@ export const UserStats: FC<IUserStatsProps> = ({ count, active, inactive }) => {
|
||||
<>
|
||||
<StyledUserContainer>
|
||||
<StyledUserBox>
|
||||
<StyledUserCount variant='h2'>{count}</StyledUserCount>
|
||||
<StyledUserCount variant='h2'>
|
||||
{parseInt(`${count}`, 10) === count
|
||||
? count
|
||||
: count.toFixed(2)}
|
||||
</StyledUserCount>
|
||||
</StyledUserBox>
|
||||
<StyledCustomShadow />
|
||||
</StyledUserContainer>
|
@ -53,7 +53,7 @@ describe('useFilteredFlagTrends', () => {
|
||||
active: 11,
|
||||
stale: 2,
|
||||
potentiallyStale: 1,
|
||||
averageUsers: '2.00',
|
||||
averageUsers: 2,
|
||||
averageHealth: '79',
|
||||
});
|
||||
});
|
||||
@ -79,7 +79,7 @@ describe('useFilteredFlagTrends', () => {
|
||||
active: 5,
|
||||
stale: 0,
|
||||
potentiallyStale: 0,
|
||||
averageUsers: '0.00',
|
||||
averageUsers: 0,
|
||||
averageHealth: '100',
|
||||
});
|
||||
});
|
||||
@ -104,7 +104,7 @@ describe('useFilteredFlagTrends', () => {
|
||||
active: 5,
|
||||
stale: 0,
|
||||
potentiallyStale: 0,
|
||||
users: 2,
|
||||
users: 3,
|
||||
date: '',
|
||||
},
|
||||
]),
|
||||
@ -115,8 +115,34 @@ describe('useFilteredFlagTrends', () => {
|
||||
active: 10,
|
||||
stale: 0,
|
||||
potentiallyStale: 0,
|
||||
averageUsers: '1.00',
|
||||
averageUsers: 1.5,
|
||||
averageHealth: '100',
|
||||
});
|
||||
});
|
||||
|
||||
it('should set health of a project without feature toggles to undefined', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useFilteredFlagsSummary([
|
||||
{
|
||||
week: '2024-01',
|
||||
project: 'project1',
|
||||
total: 0,
|
||||
active: 0,
|
||||
stale: 0,
|
||||
potentiallyStale: 0,
|
||||
users: 0,
|
||||
date: '',
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
expect(result.current).toEqual({
|
||||
total: 0,
|
||||
active: 0,
|
||||
stale: 0,
|
||||
potentiallyStale: 0,
|
||||
averageUsers: 0,
|
||||
averageHealth: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { useMemo } from 'react';
|
||||
import { ExecutiveSummarySchemaProjectFlagTrendsItem } from 'openapi';
|
||||
|
||||
// NOTE: should we move project filtering to the backend?
|
||||
export const useFilteredFlagsSummary = (
|
||||
filteredProjectFlagTrends: ExecutiveSummarySchemaProjectFlagTrendsItem[],
|
||||
) =>
|
||||
@ -14,12 +15,11 @@ export const useFilteredFlagsSummary = (
|
||||
(summary) => summary.week === lastWeekId,
|
||||
);
|
||||
|
||||
const averageUsers = (
|
||||
const averageUsers =
|
||||
lastWeekSummary.reduce(
|
||||
(acc, current) => acc + (current.users || 0),
|
||||
0,
|
||||
) / lastWeekSummary.length
|
||||
).toFixed(2);
|
||||
) / lastWeekSummary.length || 0;
|
||||
|
||||
const sum = lastWeekSummary.reduce(
|
||||
(acc, current) => ({
|
||||
@ -41,6 +41,8 @@ export const useFilteredFlagsSummary = (
|
||||
return {
|
||||
...sum,
|
||||
averageUsers,
|
||||
averageHealth: ((sum.active / (sum.total || 1)) * 100).toFixed(0),
|
||||
averageHealth: sum.total
|
||||
? ((sum.active / (sum.total || 1)) * 100).toFixed(0)
|
||||
: undefined,
|
||||
};
|
||||
}, [filteredProjectFlagTrends]);
|
||||
|
@ -4,7 +4,7 @@ import {
|
||||
ExecutiveSummarySchema,
|
||||
ExecutiveSummarySchemaMetricsSummaryTrendsItem,
|
||||
} from 'openapi';
|
||||
import { getProjectColor } from './executive-dashboard-utils';
|
||||
import { getProjectColor } from '../executive-dashboard-utils';
|
||||
|
||||
type MetricsSummaryTrends = ExecutiveSummarySchema['metricsSummaryTrends'];
|
||||
|
@ -0,0 +1,64 @@
|
||||
import { useTheme } from '@mui/material';
|
||||
import { useMemo } from 'react';
|
||||
import { fillGradientPrimary } from '../components/LineChart/LineChart';
|
||||
|
||||
export const usePlaceholderData = ({
|
||||
fill = false,
|
||||
type = 'constant',
|
||||
}: {
|
||||
fill?: boolean;
|
||||
type?: 'rising' | 'constant' | 'double';
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
labels: Array.from({ length: 15 }, (_, i) => i + 1).map(
|
||||
(i) =>
|
||||
new Date(Date.now() - (15 - i) * 7 * 24 * 60 * 60 * 1000),
|
||||
),
|
||||
datasets:
|
||||
type === 'double'
|
||||
? [
|
||||
{
|
||||
label: 'Total flags',
|
||||
data: [
|
||||
43, 66, 55, 65, 62, 72, 75, 73, 80, 65, 62,
|
||||
61, 69, 70, 77,
|
||||
],
|
||||
borderColor: theme.palette.primary.light,
|
||||
backgroundColor: theme.palette.primary.light,
|
||||
},
|
||||
{
|
||||
label: 'Stale',
|
||||
data: [
|
||||
3, 5, 4, 6, 2, 7, 5, 3, 8, 3, 5, 11, 8, 4, 3,
|
||||
],
|
||||
borderColor: theme.palette.warning.border,
|
||||
backgroundColor: theme.palette.warning.border,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
label: '',
|
||||
data:
|
||||
type === 'rising'
|
||||
? [
|
||||
3, 5, 15, 17, 25, 40, 47, 48, 55,
|
||||
65, 62, 72, 75, 73, 80,
|
||||
]
|
||||
: [
|
||||
54, 52, 53, 49, 54, 50, 47, 46,
|
||||
51, 51, 50, 51, 49, 49, 51,
|
||||
],
|
||||
borderColor: theme.palette.primary.light,
|
||||
backgroundColor: fill
|
||||
? fillGradientPrimary
|
||||
: theme.palette.primary.light,
|
||||
fill,
|
||||
},
|
||||
],
|
||||
}),
|
||||
[theme, fill],
|
||||
);
|
||||
};
|
@ -2,8 +2,8 @@ import { useMemo } from 'react';
|
||||
import {
|
||||
ExecutiveSummarySchema,
|
||||
ExecutiveSummarySchemaProjectFlagTrendsItem,
|
||||
} from '../../openapi';
|
||||
import { getProjectColor } from './executive-dashboard-utils';
|
||||
} from '../../../openapi';
|
||||
import { getProjectColor } from '../executive-dashboard-utils';
|
||||
import { useTheme } from '@mui/material';
|
||||
|
||||
type ProjectFlagTrends = ExecutiveSummarySchema['projectFlagTrends'];
|
@ -1,17 +0,0 @@
|
||||
/**
|
||||
* Generated by Orval
|
||||
* Do not edit manually.
|
||||
* See `gen:api` script in package.json
|
||||
*/
|
||||
|
||||
/**
|
||||
* The name of this action.
|
||||
*/
|
||||
export type ApplicationOverviewIssuesSchemaType =
|
||||
(typeof ApplicationOverviewIssuesSchemaType)[keyof typeof ApplicationOverviewIssuesSchemaType];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||
export const ApplicationOverviewIssuesSchemaType = {
|
||||
missingFeatures: 'missingFeatures',
|
||||
missingStrategies: 'missingStrategies',
|
||||
} as const;
|
@ -575,6 +575,7 @@ export * from './getAllToggles403';
|
||||
export * from './getApiTokensByName401';
|
||||
export * from './getApiTokensByName403';
|
||||
export * from './getApplication404';
|
||||
export * from './getApplicationEnvironmentInstances404';
|
||||
export * from './getApplicationOverview404';
|
||||
export * from './getApplicationsParams';
|
||||
export * from './getArchivedFeatures401';
|
||||
|
Loading…
Reference in New Issue
Block a user