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:
parent
ae921aed69
commit
f0e5d075a7
@ -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',
|
||||||
|
@ -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>
|
||||||
|
@ -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',
|
||||||
|
@ -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(
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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 };
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user