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 {
|
interface IProjectSelectProps {
|
||||||
selectedProjects: string[];
|
selectedProjects: string[];
|
||||||
onChange: Dispatch<SetStateAction<string[]>>;
|
onChange:
|
||||||
|
| Dispatch<SetStateAction<string[]>>
|
||||||
|
| ((projects: string[]) => void);
|
||||||
dataTestId?: string;
|
dataTestId?: string;
|
||||||
sx?: SxProps;
|
sx?: SxProps;
|
||||||
disabled?: boolean;
|
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 {
|
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,
|
allOption,
|
||||||
} from '../common/ProjectSelect/ProjectSelect';
|
ProjectSelect,
|
||||||
import { MetricsSummaryChart } from './MetricsSummaryChart/MetricsSummaryChart';
|
} from 'component/common/ProjectSelect/ProjectSelect';
|
||||||
import {
|
import { useExecutiveDashboard } from 'hooks/api/getters/useExecutiveSummary/useExecutiveSummary';
|
||||||
ExecutiveSummarySchemaMetricsSummaryTrendsItem,
|
|
||||||
ExecutiveSummarySchemaProjectFlagTrendsItem,
|
import { useFilteredFlagsSummary } from './hooks/useFilteredFlagsSummary';
|
||||||
} from 'openapi';
|
import { useFilteredTrends } from './hooks/useFilteredTrends';
|
||||||
import { HealthStats } from './HealthStats/HealthStats';
|
|
||||||
import { DashboardHeader } from './DashboardHeader/DashboardHeader';
|
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 }) => ({
|
const StyledGrid = styled(Box)(({ theme }) => ({
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: `300px 1fr`,
|
gridTemplateColumns: `repeat(2, 1fr)`,
|
||||||
gridAutoRows: 'auto',
|
gridAutoRows: 'auto',
|
||||||
gap: theme.spacing(2),
|
gap: theme.spacing(2),
|
||||||
}));
|
paddingBottom: theme.spacing(2),
|
||||||
|
[theme.breakpoints.up('md')]: {
|
||||||
const StyledBox = styled(Box)(({ theme }) => ({
|
gridTemplateColumns: `300px 1fr`,
|
||||||
marginBottom: theme.spacing(4),
|
|
||||||
marginTop: theme.spacing(4),
|
|
||||||
[theme.breakpoints.down('lg')]: {
|
|
||||||
width: '100%',
|
|
||||||
marginLeft: 0,
|
|
||||||
},
|
},
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const useDashboardGrid = () => {
|
const ChartWidget = styled(Widget)(({ theme }) => ({
|
||||||
const theme = useTheme();
|
[theme.breakpoints.down('md')]: {
|
||||||
const isMediumScreen = useMediaQuery(theme.breakpoints.down('lg'));
|
gridColumnStart: 'span 2',
|
||||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
order: 2,
|
||||||
|
},
|
||||||
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[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ExecutiveDashboard: VFC = () => {
|
export const ExecutiveDashboard: VFC = () => {
|
||||||
const { executiveDashboardData, loading, error } = useExecutiveDashboard();
|
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(() => {
|
const summary = useFilteredFlagsSummary(projectsData);
|
||||||
if (
|
const isOneProjectSelected = projects.length === 1;
|
||||||
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();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box sx={(theme) => ({ paddingBottom: theme.spacing(4) })}>
|
<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>
|
</Box>
|
||||||
<StyledGrid sx={{ gridTemplateColumns }}>
|
<StyledGrid>
|
||||||
<Widget title='Total users' order={1}>
|
<ConditionallyRender
|
||||||
<UserStats
|
condition={showAllProjects}
|
||||||
count={executiveDashboardData.users.total}
|
show={
|
||||||
active={executiveDashboardData.users.active}
|
<Widget title='Total users'>
|
||||||
inactive={executiveDashboardData.users.inactive}
|
<UserStats
|
||||||
/>
|
count={users.total}
|
||||||
</Widget>
|
active={users.active}
|
||||||
<Widget title='Users' order={userTrendsOrder} span={chartSpan}>
|
inactive={users.inactive}
|
||||||
<UsersChart
|
/>
|
||||||
userTrends={executiveDashboardData.userTrends}
|
</Widget>
|
||||||
isLoading={loading}
|
}
|
||||||
/>
|
elseShow={
|
||||||
</Widget>
|
<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
|
<Widget
|
||||||
title='Total flags'
|
title='Total flags'
|
||||||
tooltip='Total flags represent the total active flags (not archived) that currently exist across all projects of your application.'
|
tooltip='Active flags (not archived) that currently exist across selected projects.'
|
||||||
order={flagStatsOrder}
|
|
||||||
>
|
>
|
||||||
<FlagStats
|
<FlagStats
|
||||||
count={executiveDashboardData.flags.total}
|
count={summary.total}
|
||||||
flagsPerUser={flagPerUsers}
|
flagsPerUser={
|
||||||
|
showAllProjects
|
||||||
|
? (summary.total / users.total).toFixed(2)
|
||||||
|
: ''
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Widget>
|
</Widget>
|
||||||
<Widget title='Number of flags' order={4} span={chartSpan}>
|
<ConditionallyRender
|
||||||
<FlagsChart
|
condition={showAllProjects}
|
||||||
flagTrends={executiveDashboardData.flagTrends}
|
show={
|
||||||
isLoading={loading}
|
<ChartWidget title='Number of flags'>
|
||||||
/>
|
<FlagsChart
|
||||||
</Widget>
|
flagTrends={executiveDashboardData.flagTrends}
|
||||||
</StyledGrid>
|
isLoading={loading}
|
||||||
<StyledBox>
|
/>
|
||||||
<Typography variant='h2' component='span'>
|
</ChartWidget>
|
||||||
Insights per project
|
}
|
||||||
</Typography>
|
elseShow={
|
||||||
<ProjectSelect
|
<ChartWidget title='Flags per project'>
|
||||||
selectedProjects={projects}
|
<FlagsProjectChart
|
||||||
onChange={setProjects}
|
projectFlagTrends={projectsData}
|
||||||
dataTestId={'DASHBOARD_PROJECT_SELECT'}
|
/>
|
||||||
sx={{ flex: 1, maxWidth: '360px' }}
|
</ChartWidget>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</StyledBox>
|
<Widget title='Average health'>
|
||||||
<StyledGrid>
|
|
||||||
<Widget
|
|
||||||
title='Number of flags per project'
|
|
||||||
order={5}
|
|
||||||
span={largeChartSpan}
|
|
||||||
>
|
|
||||||
<FlagsProjectChart
|
|
||||||
projectFlagTrends={filteredProjectFlagTrends}
|
|
||||||
/>
|
|
||||||
</Widget>
|
|
||||||
<Widget title='Average health' order={6}>
|
|
||||||
<HealthStats
|
<HealthStats
|
||||||
// FIXME: data from API
|
value={summary.averageHealth}
|
||||||
value={80}
|
healthy={summary.active}
|
||||||
healthy={4}
|
stale={summary.stale}
|
||||||
stale={1}
|
potentiallyStale={summary.potentiallyStale}
|
||||||
potentiallyStale={0}
|
|
||||||
/>
|
/>
|
||||||
</Widget>
|
</Widget>
|
||||||
<Widget title='Health per project' order={7} span={chartSpan}>
|
<ChartWidget
|
||||||
<ProjectHealthChart
|
title={
|
||||||
projectFlagTrends={filteredProjectFlagTrends}
|
showAllProjects ? 'Healthy flags' : 'Health per project'
|
||||||
/>
|
}
|
||||||
</Widget>
|
|
||||||
<Widget
|
|
||||||
title='Metrics over time per project'
|
|
||||||
order={8}
|
|
||||||
span={largeChartSpan}
|
|
||||||
>
|
>
|
||||||
<MetricsSummaryChart
|
<ProjectHealthChart
|
||||||
metricsSummaryTrends={filteredMetricsSummaryTrends}
|
projectFlagTrends={projectsData}
|
||||||
|
isAggregate={showAllProjects}
|
||||||
/>
|
/>
|
||||||
</Widget>
|
</ChartWidget>
|
||||||
|
{/* <Widget title='Average time to production'>
|
||||||
<Widget title='Average time to production' order={9}>
|
|
||||||
<TimeToProduction
|
<TimeToProduction
|
||||||
//FIXME: data from API
|
//FIXME: data from API
|
||||||
daysToProduction={5.2}
|
daysToProduction={5.2}
|
||||||
/>
|
/>
|
||||||
</Widget>
|
</Widget>
|
||||||
<Widget title='Time to production' order={10} span={chartSpan}>
|
<ChartWidget title='Time to production'>
|
||||||
<TimeToProductionChart
|
<TimeToProductionChart projectFlagTrends={projectsData} />
|
||||||
projectFlagTrends={filteredProjectFlagTrends}
|
</ChartWidget> */}
|
||||||
/>
|
|
||||||
</Widget>
|
|
||||||
</StyledGrid>
|
</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 { useUiFlag } from 'hooks/useUiFlag';
|
||||||
import { useFeedback } from 'component/feedbackNew/useFeedback';
|
import { useFeedback } from 'component/feedbackNew/useFeedback';
|
||||||
import { ReviewsOutlined } from '@mui/icons-material';
|
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 { 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 showInactiveUsers = useUiFlag('showInactiveUsers');
|
||||||
|
|
||||||
const { openFeedback } = useFeedback(
|
const { openFeedback } = useFeedback(
|
||||||
@ -34,18 +40,21 @@ export const DashboardHeader: VFC = () => {
|
|||||||
gap: theme.spacing(1),
|
gap: theme.spacing(1),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<span>Insights</span> <Badge color='warning'>Beta</Badge>
|
<span>Insights</span> <Badge color='success'>Beta</Badge>
|
||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
actions={
|
actions={
|
||||||
<Button
|
<>
|
||||||
startIcon={<ReviewsOutlined />}
|
{actions}
|
||||||
variant='outlined'
|
<ShareLink />
|
||||||
onClick={createFeedbackContext}
|
<Button
|
||||||
size='small'
|
startIcon={<ReviewsOutlined />}
|
||||||
>
|
variant='outlined'
|
||||||
Provide feedback
|
onClick={createFeedbackContext}
|
||||||
</Button>
|
>
|
||||||
|
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>;
|
}: { tooltip: TooltipState | null }) => ReturnType<VFC>;
|
||||||
}> = ({
|
}> = ({
|
||||||
data,
|
data,
|
||||||
aspectRatio,
|
aspectRatio = 2.5,
|
||||||
cover,
|
cover,
|
||||||
isLocalTooltip,
|
isLocalTooltip,
|
||||||
overrideOptions,
|
overrideOptions,
|
||||||
@ -122,8 +122,8 @@ const LineChartComponent: VFC<{
|
|||||||
options={options}
|
options={options}
|
||||||
data={data}
|
data={data}
|
||||||
plugins={[customHighlightPlugin]}
|
plugins={[customHighlightPlugin]}
|
||||||
height={aspectRatio ? 100 : undefined}
|
height={100}
|
||||||
width={aspectRatio ? 100 * aspectRatio : undefined}
|
width={100 * aspectRatio}
|
||||||
/>
|
/>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={!cover}
|
condition={!cover}
|
@ -62,7 +62,7 @@ export const createOptions = (
|
|||||||
x: {
|
x: {
|
||||||
type: 'time',
|
type: 'time',
|
||||||
time: {
|
time: {
|
||||||
unit: 'day',
|
unit: 'week',
|
||||||
tooltipFormat: 'PPP',
|
tooltipFormat: 'PPP',
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
@ -72,6 +72,9 @@ export const createOptions = (
|
|||||||
ticks: {
|
ticks: {
|
||||||
color: theme.palette.text.secondary,
|
color: theme.palette.text.secondary,
|
||||||
display: !isPlaceholder,
|
display: !isPlaceholder,
|
||||||
|
source: 'data',
|
||||||
|
maxRotation: 90,
|
||||||
|
minRotation: 23.5,
|
||||||
},
|
},
|
||||||
min: format(subMonths(new Date(), 3), 'yyyy-MM-dd'),
|
min: format(subMonths(new Date(), 3), 'yyyy-MM-dd'),
|
||||||
},
|
},
|
@ -12,13 +12,14 @@ export const legendOptions = {
|
|||||||
} = chart?.legend?.options || {
|
} = chart?.legend?.options || {
|
||||||
labels: {},
|
labels: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
return (chart as any)._getSortedDatasetMetas().map((meta: any) => {
|
return (chart as any)._getSortedDatasetMetas().map((meta: any) => {
|
||||||
const style = meta.controller.getStyle(
|
const style = meta.controller.getStyle(
|
||||||
usePointStyle ? 0 : undefined,
|
usePointStyle ? 0 : undefined,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
text: datasets[meta.index].label,
|
text: datasets[meta.index].label,
|
||||||
fillStyle: style.backgroundColor,
|
fillStyle: style.borderColor,
|
||||||
fontColor: color,
|
fontColor: color,
|
||||||
hidden: !meta.visible,
|
hidden: !meta.visible,
|
||||||
lineWidth: 0,
|
lineWidth: 0,
|
@ -12,17 +12,9 @@ const StyledPaper = styled(Paper)(({ theme }) => ({
|
|||||||
|
|
||||||
export const Widget: FC<{
|
export const Widget: FC<{
|
||||||
title: ReactNode;
|
title: ReactNode;
|
||||||
order?: number;
|
|
||||||
span?: number;
|
|
||||||
tooltip?: ReactNode;
|
tooltip?: ReactNode;
|
||||||
}> = ({ title, order, children, span = 1, tooltip }) => (
|
}> = ({ title, children, tooltip, ...rest }) => (
|
||||||
<StyledPaper
|
<StyledPaper elevation={0} {...rest}>
|
||||||
elevation={0}
|
|
||||||
sx={{
|
|
||||||
order,
|
|
||||||
gridColumn: `span ${span}`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
<Typography
|
||||||
variant='h3'
|
variant='h3'
|
||||||
sx={(theme) => ({
|
sx={(theme) => ({
|
@ -2,7 +2,8 @@ import { useMemo, type VFC } from 'react';
|
|||||||
import 'chartjs-adapter-date-fns';
|
import 'chartjs-adapter-date-fns';
|
||||||
import { useTheme } from '@mui/material';
|
import { useTheme } from '@mui/material';
|
||||||
import { ExecutiveSummarySchema } from 'openapi';
|
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 {
|
interface IFlagsChartProps {
|
||||||
flagTrends: ExecutiveSummarySchema['flagTrends'];
|
flagTrends: ExecutiveSummarySchema['flagTrends'];
|
||||||
@ -15,32 +16,7 @@ export const FlagsChart: VFC<IFlagsChartProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const notEnoughData = flagTrends.length < 2;
|
const notEnoughData = flagTrends.length < 2;
|
||||||
const placeholderData = useMemo(
|
const placeholderData = usePlaceholderData({ fill: true, type: 'double' });
|
||||||
() => ({
|
|
||||||
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 data = useMemo(
|
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 { type VFC } from 'react';
|
||||||
import { ExecutiveSummarySchemaMetricsSummaryTrendsItem } from 'openapi';
|
import { ExecutiveSummarySchemaMetricsSummaryTrendsItem } from 'openapi';
|
||||||
import { Box, Divider, Paper, styled, Typography } from '@mui/material';
|
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 }) => ({
|
const StyledTooltipItemContainer = styled(Paper)(({ theme }) => ({
|
||||||
padding: theme.spacing(2),
|
padding: theme.spacing(2),
|
@ -1,9 +1,9 @@
|
|||||||
import { type VFC } from 'react';
|
import { type VFC } from 'react';
|
||||||
import 'chartjs-adapter-date-fns';
|
import 'chartjs-adapter-date-fns';
|
||||||
import { ExecutiveSummarySchema } from 'openapi';
|
import { ExecutiveSummarySchema } from 'openapi';
|
||||||
import { LineChart } from '../LineChart/LineChart';
|
import { LineChart } from '../../components/LineChart/LineChart';
|
||||||
import { useMetricsSummary } from '../useMetricsSummary';
|
|
||||||
import { MetricsSummaryTooltip } from './MetricsChartTooltip/MetricsChartTooltip';
|
import { MetricsSummaryTooltip } from './MetricsChartTooltip/MetricsChartTooltip';
|
||||||
|
import { useMetricsSummary } from '../../hooks/useMetricsSummary';
|
||||||
|
|
||||||
interface IMetricsSummaryChartProps {
|
interface IMetricsSummaryChartProps {
|
||||||
metricsSummaryTrends: ExecutiveSummarySchema['metricsSummaryTrends'];
|
metricsSummaryTrends: ExecutiveSummarySchema['metricsSummaryTrends'];
|
||||||
@ -13,6 +13,7 @@ export const MetricsSummaryChart: VFC<IMetricsSummaryChartProps> = ({
|
|||||||
metricsSummaryTrends,
|
metricsSummaryTrends,
|
||||||
}) => {
|
}) => {
|
||||||
const data = useMetricsSummary(metricsSummaryTrends);
|
const data = useMetricsSummary(metricsSummaryTrends);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LineChart
|
<LineChart
|
||||||
data={data}
|
data={data}
|
@ -2,8 +2,8 @@ import { type VFC } from 'react';
|
|||||||
import { type ExecutiveSummarySchemaProjectFlagTrendsItem } from 'openapi';
|
import { type ExecutiveSummarySchemaProjectFlagTrendsItem } from 'openapi';
|
||||||
import { Box, Divider, Paper, Typography, styled } from '@mui/material';
|
import { Box, Divider, Paper, Typography, styled } from '@mui/material';
|
||||||
import { Badge } from 'component/common/Badge/Badge';
|
import { Badge } from 'component/common/Badge/Badge';
|
||||||
import { TooltipState } from '../../LineChart/ChartTooltip/ChartTooltip';
|
import { TooltipState } from '../../../components/LineChart/ChartTooltip/ChartTooltip';
|
||||||
import { HorizontalDistributionChart } from '../../HorizontalDistributionChart/HorizontalDistributionChart';
|
import { HorizontalDistributionChart } from '../../../components/HorizontalDistributionChart/HorizontalDistributionChart';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
const StyledTooltipItemContainer = styled(Paper)(({ theme }) => ({
|
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 { type VFC } from 'react';
|
||||||
import 'chartjs-adapter-date-fns';
|
import 'chartjs-adapter-date-fns';
|
||||||
import { ExecutiveSummarySchema } from 'openapi';
|
import { ExecutiveSummarySchema } from 'openapi';
|
||||||
import { LineChart } from '../LineChart/LineChart';
|
import { LineChart } from '../../components/LineChart/LineChart';
|
||||||
import { useProjectChartData } from '../useProjectChartData';
|
import { useProjectChartData } from '../../hooks/useProjectChartData';
|
||||||
|
|
||||||
interface IFlagsProjectChartProps {
|
interface IFlagsProjectChartProps {
|
||||||
projectFlagTrends: ExecutiveSummarySchema['projectFlagTrends'];
|
projectFlagTrends: ExecutiveSummarySchema['projectFlagTrends'];
|
||||||
@ -12,7 +12,6 @@ export const TimeToProductionChart: VFC<IFlagsProjectChartProps> = ({
|
|||||||
projectFlagTrends,
|
projectFlagTrends,
|
||||||
}) => {
|
}) => {
|
||||||
const data = useProjectChartData(projectFlagTrends);
|
const data = useProjectChartData(projectFlagTrends);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LineChart
|
<LineChart
|
||||||
data={data}
|
data={data}
|
@ -6,8 +6,9 @@ import {
|
|||||||
fillGradientPrimary,
|
fillGradientPrimary,
|
||||||
LineChart,
|
LineChart,
|
||||||
NotEnoughData,
|
NotEnoughData,
|
||||||
} from '../LineChart/LineChart';
|
} from '../../components/LineChart/LineChart';
|
||||||
import { useUiFlag } from 'hooks/useUiFlag';
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
import { usePlaceholderData } from 'component/executiveDashboard/hooks/usePlaceholderData';
|
||||||
|
|
||||||
interface IUsersChartProps {
|
interface IUsersChartProps {
|
||||||
userTrends: ExecutiveSummarySchema['userTrends'];
|
userTrends: ExecutiveSummarySchema['userTrends'];
|
||||||
@ -21,28 +22,7 @@ export const UsersChart: VFC<IUsersChartProps> = ({
|
|||||||
const showInactiveUsers = useUiFlag('showInactiveUsers');
|
const showInactiveUsers = useUiFlag('showInactiveUsers');
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const notEnoughData = userTrends.length < 2;
|
const notEnoughData = userTrends.length < 2;
|
||||||
const placeholderData = useMemo(
|
const placeholderData = usePlaceholderData({ fill: true, type: 'rising' });
|
||||||
() => ({
|
|
||||||
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 data = useMemo(
|
const data = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
labels: userTrends.map((item) => item.date),
|
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 { Settings } from '@mui/icons-material';
|
||||||
import { Box, Typography, styled } from '@mui/material';
|
import { Box, Typography, styled } from '@mui/material';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
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',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledRingContainer = styled(Box)(({ theme }) => ({
|
const StyledRingContainer = styled(Box)(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -79,7 +65,7 @@ const StyledSettingsIcon = styled(Settings)(({ theme }) => ({
|
|||||||
|
|
||||||
interface IFlagStatsProps {
|
interface IFlagStatsProps {
|
||||||
count: number;
|
count: number;
|
||||||
flagsPerUser: string;
|
flagsPerUser?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FlagStats: React.FC<IFlagStatsProps> = ({
|
export const FlagStats: React.FC<IFlagStatsProps> = ({
|
||||||
@ -94,22 +80,31 @@ export const FlagStats: React.FC<IFlagStatsProps> = ({
|
|||||||
</StyledRing>
|
</StyledRing>
|
||||||
</StyledRingContainer>
|
</StyledRingContainer>
|
||||||
|
|
||||||
<StyledInsightsContainer>
|
<ConditionallyRender
|
||||||
<StyledTextContainer>
|
condition={flagsPerUser !== undefined && flagsPerUser !== ''}
|
||||||
<StyledHeaderContainer>
|
show={
|
||||||
<StyledSettingsIcon />
|
<StyledInsightsContainer>
|
||||||
<Typography
|
<StyledTextContainer>
|
||||||
fontWeight='bold'
|
<StyledHeaderContainer>
|
||||||
variant='body2'
|
<StyledSettingsIcon />
|
||||||
color='primary'
|
<Typography
|
||||||
>
|
fontWeight='bold'
|
||||||
Insights
|
variant='body2'
|
||||||
</Typography>
|
color='primary'
|
||||||
</StyledHeaderContainer>
|
>
|
||||||
<Typography variant='body2'>Flags per user</Typography>
|
Insights
|
||||||
</StyledTextContainer>
|
</Typography>
|
||||||
<StyledFlagCountPerUser>{flagsPerUser}</StyledFlagCountPerUser>
|
</StyledHeaderContainer>
|
||||||
</StyledInsightsContainer>
|
<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';
|
import { useTheme } from '@mui/material';
|
||||||
|
|
||||||
interface IHealthStatsProps {
|
interface IHealthStatsProps {
|
||||||
value: number;
|
value?: string | number;
|
||||||
healthy: number;
|
healthy: number;
|
||||||
stale: number;
|
stale: number;
|
||||||
potentiallyStale: number;
|
potentiallyStale: number;
|
@ -1,6 +1,6 @@
|
|||||||
import { VFC } from 'react';
|
import { VFC } from 'react';
|
||||||
import { Typography, styled } from '@mui/material';
|
import { Typography, styled } from '@mui/material';
|
||||||
import { Gauge } from '../Gauge/Gauge';
|
import { Gauge } from '../../components/Gauge/Gauge';
|
||||||
|
|
||||||
const StyledContainer = styled('div')(({ theme }) => ({
|
const StyledContainer = styled('div')(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
@ -4,7 +4,7 @@ import { Box, Typography, styled } from '@mui/material';
|
|||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { useUiFlag } from 'hooks/useUiFlag';
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { HorizontalDistributionChart } from '../HorizontalDistributionChart/HorizontalDistributionChart';
|
import { HorizontalDistributionChart } from '../../components/HorizontalDistributionChart/HorizontalDistributionChart';
|
||||||
import { UserDistributionInfo } from './UserDistributionInfo';
|
import { UserDistributionInfo } from './UserDistributionInfo';
|
||||||
|
|
||||||
const StyledUserContainer = styled(Box)(({ theme }) => ({
|
const StyledUserContainer = styled(Box)(({ theme }) => ({
|
||||||
@ -81,7 +81,11 @@ export const UserStats: FC<IUserStatsProps> = ({ count, active, inactive }) => {
|
|||||||
<>
|
<>
|
||||||
<StyledUserContainer>
|
<StyledUserContainer>
|
||||||
<StyledUserBox>
|
<StyledUserBox>
|
||||||
<StyledUserCount variant='h2'>{count}</StyledUserCount>
|
<StyledUserCount variant='h2'>
|
||||||
|
{parseInt(`${count}`, 10) === count
|
||||||
|
? count
|
||||||
|
: count.toFixed(2)}
|
||||||
|
</StyledUserCount>
|
||||||
</StyledUserBox>
|
</StyledUserBox>
|
||||||
<StyledCustomShadow />
|
<StyledCustomShadow />
|
||||||
</StyledUserContainer>
|
</StyledUserContainer>
|
@ -53,7 +53,7 @@ describe('useFilteredFlagTrends', () => {
|
|||||||
active: 11,
|
active: 11,
|
||||||
stale: 2,
|
stale: 2,
|
||||||
potentiallyStale: 1,
|
potentiallyStale: 1,
|
||||||
averageUsers: '2.00',
|
averageUsers: 2,
|
||||||
averageHealth: '79',
|
averageHealth: '79',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -79,7 +79,7 @@ describe('useFilteredFlagTrends', () => {
|
|||||||
active: 5,
|
active: 5,
|
||||||
stale: 0,
|
stale: 0,
|
||||||
potentiallyStale: 0,
|
potentiallyStale: 0,
|
||||||
averageUsers: '0.00',
|
averageUsers: 0,
|
||||||
averageHealth: '100',
|
averageHealth: '100',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -104,7 +104,7 @@ describe('useFilteredFlagTrends', () => {
|
|||||||
active: 5,
|
active: 5,
|
||||||
stale: 0,
|
stale: 0,
|
||||||
potentiallyStale: 0,
|
potentiallyStale: 0,
|
||||||
users: 2,
|
users: 3,
|
||||||
date: '',
|
date: '',
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
@ -115,8 +115,34 @@ describe('useFilteredFlagTrends', () => {
|
|||||||
active: 10,
|
active: 10,
|
||||||
stale: 0,
|
stale: 0,
|
||||||
potentiallyStale: 0,
|
potentiallyStale: 0,
|
||||||
averageUsers: '1.00',
|
averageUsers: 1.5,
|
||||||
averageHealth: '100',
|
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 { useMemo } from 'react';
|
||||||
import { ExecutiveSummarySchemaProjectFlagTrendsItem } from 'openapi';
|
import { ExecutiveSummarySchemaProjectFlagTrendsItem } from 'openapi';
|
||||||
|
|
||||||
|
// NOTE: should we move project filtering to the backend?
|
||||||
export const useFilteredFlagsSummary = (
|
export const useFilteredFlagsSummary = (
|
||||||
filteredProjectFlagTrends: ExecutiveSummarySchemaProjectFlagTrendsItem[],
|
filteredProjectFlagTrends: ExecutiveSummarySchemaProjectFlagTrendsItem[],
|
||||||
) =>
|
) =>
|
||||||
@ -14,12 +15,11 @@ export const useFilteredFlagsSummary = (
|
|||||||
(summary) => summary.week === lastWeekId,
|
(summary) => summary.week === lastWeekId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const averageUsers = (
|
const averageUsers =
|
||||||
lastWeekSummary.reduce(
|
lastWeekSummary.reduce(
|
||||||
(acc, current) => acc + (current.users || 0),
|
(acc, current) => acc + (current.users || 0),
|
||||||
0,
|
0,
|
||||||
) / lastWeekSummary.length
|
) / lastWeekSummary.length || 0;
|
||||||
).toFixed(2);
|
|
||||||
|
|
||||||
const sum = lastWeekSummary.reduce(
|
const sum = lastWeekSummary.reduce(
|
||||||
(acc, current) => ({
|
(acc, current) => ({
|
||||||
@ -41,6 +41,8 @@ export const useFilteredFlagsSummary = (
|
|||||||
return {
|
return {
|
||||||
...sum,
|
...sum,
|
||||||
averageUsers,
|
averageUsers,
|
||||||
averageHealth: ((sum.active / (sum.total || 1)) * 100).toFixed(0),
|
averageHealth: sum.total
|
||||||
|
? ((sum.active / (sum.total || 1)) * 100).toFixed(0)
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
}, [filteredProjectFlagTrends]);
|
}, [filteredProjectFlagTrends]);
|
||||||
|
@ -4,7 +4,7 @@ import {
|
|||||||
ExecutiveSummarySchema,
|
ExecutiveSummarySchema,
|
||||||
ExecutiveSummarySchemaMetricsSummaryTrendsItem,
|
ExecutiveSummarySchemaMetricsSummaryTrendsItem,
|
||||||
} from 'openapi';
|
} from 'openapi';
|
||||||
import { getProjectColor } from './executive-dashboard-utils';
|
import { getProjectColor } from '../executive-dashboard-utils';
|
||||||
|
|
||||||
type MetricsSummaryTrends = ExecutiveSummarySchema['metricsSummaryTrends'];
|
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 {
|
import {
|
||||||
ExecutiveSummarySchema,
|
ExecutiveSummarySchema,
|
||||||
ExecutiveSummarySchemaProjectFlagTrendsItem,
|
ExecutiveSummarySchemaProjectFlagTrendsItem,
|
||||||
} from '../../openapi';
|
} from '../../../openapi';
|
||||||
import { getProjectColor } from './executive-dashboard-utils';
|
import { getProjectColor } from '../executive-dashboard-utils';
|
||||||
import { useTheme } from '@mui/material';
|
import { useTheme } from '@mui/material';
|
||||||
|
|
||||||
type ProjectFlagTrends = ExecutiveSummarySchema['projectFlagTrends'];
|
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 './getApiTokensByName401';
|
||||||
export * from './getApiTokensByName403';
|
export * from './getApiTokensByName403';
|
||||||
export * from './getApplication404';
|
export * from './getApplication404';
|
||||||
|
export * from './getApplicationEnvironmentInstances404';
|
||||||
export * from './getApplicationOverview404';
|
export * from './getApplicationOverview404';
|
||||||
export * from './getApplicationsParams';
|
export * from './getApplicationsParams';
|
||||||
export * from './getArchivedFeatures401';
|
export * from './getArchivedFeatures401';
|
||||||
|
Loading…
Reference in New Issue
Block a user