1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-15 01:16:22 +02:00

feat: Fetch backend api data insights (#6622)

This commit is contained in:
Mateusz Kwasniewski 2024-03-20 10:54:21 +01:00 committed by GitHub
parent ae921aed69
commit f0e5d075a7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 140 additions and 50 deletions

View File

@ -22,11 +22,23 @@ const setupOssApi = () => {
}); });
}; };
const changeRequests = {
applied: 0,
total: 0,
approved: 0,
scheduled: 0,
reviewRequired: 0,
rejected: 0,
};
test('Show enterprise hints', async () => { test('Show enterprise hints', async () => {
setupOssApi(); setupOssApi();
render( render(
<Routes> <Routes>
<Route path={'/projects/:projectId'} element={<ChangeRequests />} /> <Route
path={'/projects/:projectId'}
element={<ChangeRequests changeRequests={changeRequests} />}
/>
</Routes>, </Routes>,
{ {
route: '/projects/default', route: '/projects/default',
@ -40,7 +52,10 @@ test('Show change requests info', async () => {
setupEnterpriseApi(); setupEnterpriseApi();
render( render(
<Routes> <Routes>
<Route path={'/projects/:projectId'} element={<ChangeRequests />} /> <Route
path={'/projects/:projectId'}
element={<ChangeRequests changeRequests={changeRequests} />}
/>
</Routes>, </Routes>,
{ {
route: '/projects/default', route: '/projects/default',

View File

@ -4,6 +4,8 @@ import { Link } from 'react-router-dom';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature'; import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
import type { ProjectInsightsSchemaChangeRequests } from '../../../../../openapi';
import type { FC } from 'react';
const Container = styled(Box)(({ theme }) => ({ const Container = styled(Box)(({ theme }) => ({
display: 'flex', display: 'flex',
@ -81,15 +83,15 @@ const BigNumber = styled(Typography)(({ theme }) => ({
color: theme.palette.text.primary, color: theme.palette.text.primary,
})); }));
export const ChangeRequests = () => { export const ChangeRequests: FC<{
changeRequests: ProjectInsightsSchemaChangeRequests;
}> = ({ changeRequests }) => {
const projectId = useRequiredPathParam('projectId'); const projectId = useRequiredPathParam('projectId');
const { isOss, isPro } = useUiConfig(); const { isOss, isPro } = useUiConfig();
const toBeApplied = 12; const { total, applied, rejected, reviewRequired, scheduled, approved } =
const toBeReviewed = 3; changeRequests;
const total = 32; const toBeApplied = scheduled + approved;
const applied = 28;
const rejected = 4;
if (isOss() || isPro()) { if (isOss() || isPro()) {
return ( return (
@ -109,7 +111,7 @@ export const ChangeRequests = () => {
<KeyboardArrowRight /> <KeyboardArrowRight />
</ChangeRequestNavigation> </ChangeRequestNavigation>
<BoxesContainer> <BoxesContainer data-loading>
<OpenBox> <OpenBox>
<ChangeRequestNavigation <ChangeRequestNavigation
to={`/projects/${projectId}/change-requests`} to={`/projects/${projectId}/change-requests`}
@ -123,7 +125,7 @@ export const ChangeRequests = () => {
</ApplyBox> </ApplyBox>
<ReviewBox> <ReviewBox>
<span>To be reviewed</span> <span>To be reviewed</span>
<MediumNumber>{toBeReviewed}</MediumNumber> <MediumNumber>{reviewRequired}</MediumNumber>
</ReviewBox> </ReviewBox>
</OpenBox> </OpenBox>
<NumberBox> <NumberBox>

View File

@ -24,7 +24,14 @@ test('Show outdated SDKs and apps using them', async () => {
}); });
render( render(
<Routes> <Routes>
<Route path={'/projects/:projectId'} element={<FlagTypesUsed />} /> <Route
path={'/projects/:projectId'}
element={
<FlagTypesUsed
featureTypeCounts={[{ type: 'release', count: 57 }]}
/>
}
/>
</Routes>, </Routes>,
{ {
route: '/projects/default', route: '/projects/default',

View File

@ -1,10 +1,9 @@
import { useMemo } from 'react'; import { type FC, useMemo } from 'react';
import { styled, type SvgIconTypeMap, Typography } from '@mui/material'; import { styled, type SvgIconTypeMap, Typography } from '@mui/material';
import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons'; import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons';
import type { OverridableComponent } from '@mui/material/OverridableComponent'; import type { OverridableComponent } from '@mui/material/OverridableComponent';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import type { FeatureTypeCountSchema } from '../../../../../openapi';
import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview';
export const StyledProjectInfoWidgetContainer = styled('div')(({ theme }) => ({ export const StyledProjectInfoWidgetContainer = styled('div')(({ theme }) => ({
margin: '0', margin: '0',
@ -64,12 +63,9 @@ const FlagTypesRow = ({ type, Icon, count }: IFlagTypeRowProps) => {
); );
}; };
export const FlagTypesUsed = () => { export const FlagTypesUsed: FC<{
const projectId = useRequiredPathParam('projectId'); featureTypeCounts: FeatureTypeCountSchema[];
const { project } = useProjectOverview(projectId); }> = ({ featureTypeCounts }) => {
const { featureTypeCounts } = project;
const featureTypeStats = useMemo(() => { const featureTypeStats = useMemo(() => {
const release = const release =
featureTypeCounts.find( featureTypeCounts.find(

View File

@ -2,6 +2,8 @@ import { ProjectHealthChart } from './ProjectHealthChart';
import { Alert, Box, styled, Typography, useTheme } from '@mui/material'; import { Alert, Box, styled, Typography, useTheme } from '@mui/material';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import type { ProjectInsightsSchemaHealth } from '../../../../../openapi';
import type { FC } from 'react';
const Dot = styled('span', { const Dot = styled('span', {
shouldForwardProp: (prop) => prop !== 'color', shouldForwardProp: (prop) => prop !== 'color',
@ -36,13 +38,12 @@ const StatusWithDot = styled(Box)(({ theme }) => ({
gap: theme.spacing(1), gap: theme.spacing(1),
})); }));
export const ProjectHealth = () => { export const ProjectHealth: FC<{ health: ProjectInsightsSchemaHealth }> = ({
health,
}) => {
const theme = useTheme(); const theme = useTheme();
const projectId = useRequiredPathParam('projectId'); const projectId = useRequiredPathParam('projectId');
const active = 15; const { staleCount, potentiallyStaleCount, activeCount, rating } = health;
const stale = 10;
const potentiallyStale = 3;
const health = 93;
return ( return (
<Container> <Container>
@ -52,6 +53,7 @@ export const ProjectHealth = () => {
flags flags
</Alert> </Alert>
<Box <Box
data-loading
sx={(theme) => ({ sx={(theme) => ({
display: 'flex', display: 'flex',
gap: theme.spacing(4), gap: theme.spacing(4),
@ -59,10 +61,10 @@ export const ProjectHealth = () => {
})} })}
> >
<ProjectHealthChart <ProjectHealthChart
active={active} active={activeCount}
stale={stale} stale={staleCount}
potentiallyStale={potentiallyStale} potentiallyStale={potentiallyStaleCount}
health={health} health={rating}
/> />
<FlagCounts> <FlagCounts>
<Box> <Box>
@ -70,7 +72,7 @@ export const ProjectHealth = () => {
<Dot color={theme.palette.success.border} /> <Dot color={theme.palette.success.border} />
<Box sx={{ fontWeight: 'bold' }}>Active</Box> <Box sx={{ fontWeight: 'bold' }}>Active</Box>
</StatusWithDot> </StatusWithDot>
<FlagsCount>{active} feature flags</FlagsCount> <FlagsCount>{activeCount} feature flags</FlagsCount>
</Box> </Box>
<Box> <Box>
<StatusWithDot> <StatusWithDot>
@ -81,7 +83,7 @@ export const ProjectHealth = () => {
<Link to='/feature-toggle-type'>(configure)</Link> <Link to='/feature-toggle-type'>(configure)</Link>
</StatusWithDot> </StatusWithDot>
<FlagsCount> <FlagsCount>
{potentiallyStale} feature flags {potentiallyStaleCount} feature flags
</FlagsCount> </FlagsCount>
</Box> </Box>
<Box> <Box>
@ -92,7 +94,7 @@ export const ProjectHealth = () => {
(view flags) (view flags)
</Link> </Link>
</StatusWithDot> </StatusWithDot>
<FlagsCount>{stale} feature flags</FlagsCount> <FlagsCount>{staleCount} feature flags</FlagsCount>
</Box> </Box>
</FlagCounts> </FlagCounts>
</Box> </Box>

View File

@ -4,6 +4,9 @@ import { LeadTimeForChanges } from './LeadTimeForChanges/LeadTimeForChanges';
import { ProjectHealth } from './ProjectHealth/ProjectHealth'; import { ProjectHealth } from './ProjectHealth/ProjectHealth';
import { FlagTypesUsed } from './FlagTypesUsed/FlagTypesUsed'; import { FlagTypesUsed } from './FlagTypesUsed/FlagTypesUsed';
import { ProjectInsightsStats } from './ProjectInsightsStats/ProjectInsightsStats'; import { ProjectInsightsStats } from './ProjectInsightsStats/ProjectInsightsStats';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { useProjectInsights } from 'hooks/api/getters/useProjectInsights/useProjectInsights';
import useLoading from 'hooks/useLoading';
const Container = styled(Box)(({ theme }) => ({ const Container = styled(Box)(({ theme }) => ({
backgroundColor: theme.palette.background.paper, backgroundColor: theme.palette.background.paper,
@ -33,37 +36,31 @@ const NarrowContainer = styled(Container)(() => ({
gridColumn: 'span 2', gridColumn: 'span 2',
})); }));
const statsData = {
stats: {
archivedCurrentWindow: 5,
archivedPastWindow: 3,
avgTimeToProdCurrentWindow: 2.5,
createdCurrentWindow: 7,
createdPastWindow: 4,
projectActivityCurrentWindow: 10,
projectActivityPastWindow: 8,
projectMembersAddedCurrentWindow: 2,
},
};
export const ProjectInsights = () => { export const ProjectInsights = () => {
const projectId = useRequiredPathParam('projectId');
const { data, loading } = useProjectInsights(projectId);
const ref = useLoading(loading);
return ( return (
<Grid> <Grid ref={ref}>
<FullWidthContainer> <FullWidthContainer>
<ProjectInsightsStats {...statsData} /> <ProjectInsightsStats stats={data.stats} />
</FullWidthContainer> </FullWidthContainer>
<MediumWideContainer> <MediumWideContainer>
<ProjectHealth /> <ProjectHealth health={data.health} />
</MediumWideContainer> </MediumWideContainer>
<WideContainer> <WideContainer>
<LeadTimeForChanges /> <LeadTimeForChanges />
</WideContainer> </WideContainer>
<NarrowContainer> <NarrowContainer>
<FlagTypesUsed /> <FlagTypesUsed featureTypeCounts={data.featureTypeCounts} />
</NarrowContainer> </NarrowContainer>
<NarrowContainer>Project members</NarrowContainer> <NarrowContainer>Project members</NarrowContainer>
<WideContainer> <WideContainer>
<ChangeRequests /> {data.changeRequests && (
<ChangeRequests changeRequests={data.changeRequests} />
)}
</WideContainer> </WideContainer>
</Grid> </Grid>
); );

View File

@ -0,0 +1,71 @@
import { fetcher, useApiGetter } from '../useApiGetter/useApiGetter';
import type { ProjectInsightsSchema } from '../../../../openapi';
import { formatApiPath } from 'utils/formatPath';
const path = (projectId: string) => `api/admin/projects/${projectId}/insights`;
const placeholderData: ProjectInsightsSchema = {
stats: {
avgTimeToProdCurrentWindow: 0,
createdCurrentWindow: 0,
createdPastWindow: 0,
archivedCurrentWindow: 0,
archivedPastWindow: 0,
projectActivityCurrentWindow: 0,
projectActivityPastWindow: 0,
projectMembersAddedCurrentWindow: 0,
},
featureTypeCounts: [
{
type: 'experiment',
count: 0,
},
{
type: 'permission',
count: 0,
},
{
type: 'release',
count: 0,
},
],
leadTime: {
projectAverage: 0,
features: [
{ name: 'feature1', timeToProduction: 0 },
{ name: 'feature2', timeToProduction: 0 },
{ name: 'feature3', timeToProduction: 0 },
{ name: 'feature4', timeToProduction: 0 },
{ name: 'feature5', timeToProduction: 2 },
],
},
health: {
rating: 0,
activeCount: 0,
potentiallyStaleCount: 0,
staleCount: 0,
},
members: {
active: 0,
inactive: 0,
totalPreviousMonth: 0,
},
changeRequests: {
total: 0,
applied: 0,
approved: 0,
rejected: 0,
reviewRequired: 0,
scheduled: 0,
},
};
export const useProjectInsights = (projectId: string) => {
const projectPath = formatApiPath(path(projectId));
const { data, refetch, loading, error } =
useApiGetter<ProjectInsightsSchema>(projectPath, () =>
fetcher(projectPath, 'Outdated SDKs'),
);
return { data: data || placeholderData, refetch, loading, error };
};