mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-09 13:47:13 +02:00
chore!: remove deprecated get project health report
This commit is contained in:
parent
eef32b7cf5
commit
5482003b73
@ -28,7 +28,6 @@ import useQueryParams from 'hooks/useQueryParams';
|
||||
import { useEffect, useState, type ReactNode } from 'react';
|
||||
import ProjectEnvironment from '../ProjectEnvironment/ProjectEnvironment.tsx';
|
||||
import ProjectFlags from './ProjectFlags.tsx';
|
||||
import ProjectHealth from './ProjectHealth/ProjectHealth.tsx';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import {
|
||||
@ -375,7 +374,6 @@ export const Project = () => {
|
||||
}}
|
||||
/>
|
||||
<Routes>
|
||||
<Route path='health' element={<ProjectHealth />} />
|
||||
<Route
|
||||
path='access/*'
|
||||
element={
|
||||
|
@ -1,46 +0,0 @@
|
||||
import { useHealthReport } from 'hooks/api/getters/useHealthReport/useHealthReport';
|
||||
import ApiError from 'component/common/ApiError/ApiError';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { usePageTitle } from 'hooks/usePageTitle';
|
||||
import { ReportCard } from './ReportTable/ReportCard/ReportCard.tsx';
|
||||
import { ReportTable } from './ReportTable/ReportTable.tsx';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { useProjectOverviewNameOrId } from 'hooks/api/getters/useProjectOverview/useProjectOverview';
|
||||
|
||||
const ProjectHealth = () => {
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const projectName = useProjectOverviewNameOrId(projectId);
|
||||
usePageTitle(`Project health – ${projectName}`);
|
||||
|
||||
const { healthReport, refetchHealthReport, error } = useHealthReport(
|
||||
projectId,
|
||||
{ refreshInterval: 15 * 1000 },
|
||||
);
|
||||
|
||||
if (!healthReport) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(error)}
|
||||
show={
|
||||
<ApiError
|
||||
data-loading
|
||||
style={{ maxWidth: '500px', marginTop: '1rem' }}
|
||||
onClick={refetchHealthReport}
|
||||
text={`Could not fetch health rating for ${projectId}`}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ReportCard healthReport={healthReport} />
|
||||
<ReportTable
|
||||
projectId={projectId}
|
||||
features={healthReport.features}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectHealth;
|
@ -1,219 +0,0 @@
|
||||
import { Box, Link, Paper, styled } from '@mui/material';
|
||||
import CheckIcon from '@mui/icons-material/Check';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import ReportProblemOutlinedIcon from '@mui/icons-material/ReportProblemOutlined';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import type { IProjectHealthReport } from 'interfaces/project';
|
||||
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
|
||||
import InfoOutlined from '@mui/icons-material/InfoOutlined';
|
||||
import { TimeAgo } from 'component/common/TimeAgo/TimeAgo';
|
||||
|
||||
const StyledBoxActive = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
color: theme.palette.success.dark,
|
||||
'& svg': {
|
||||
color: theme.palette.success.main,
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledBoxStale = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
color: theme.palette.warning.dark,
|
||||
'& svg': {
|
||||
color: theme.palette.warning.main,
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledPaper = styled(Paper)(({ theme }) => ({
|
||||
padding: theme.spacing(4),
|
||||
marginBottom: theme.spacing(2),
|
||||
borderRadius: theme.shape.borderRadiusLarge,
|
||||
boxShadow: 'none',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
[theme.breakpoints.down('md')]: {
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(2),
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledHeader = styled('h2')(({ theme }) => ({
|
||||
fontSize: theme.fontSizes.mainHeader,
|
||||
marginBottom: theme.spacing(1),
|
||||
justifyItems: 'center',
|
||||
display: 'flex',
|
||||
}));
|
||||
|
||||
const StyledHealthRating = styled('p')(({ theme }) => ({
|
||||
fontSize: '2rem',
|
||||
fontWeight: theme.fontWeight.bold,
|
||||
}));
|
||||
|
||||
const StyledLastUpdated = styled('p')(({ theme }) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
}));
|
||||
|
||||
const StyledList = styled('ul')(({ theme }) => ({
|
||||
listStyleType: 'none',
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
'& svg': {
|
||||
marginRight: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledAlignedItem = styled('p')(({ theme }) => ({
|
||||
marginLeft: theme.spacing(4),
|
||||
}));
|
||||
|
||||
interface IReportCardProps {
|
||||
healthReport: IProjectHealthReport;
|
||||
}
|
||||
|
||||
export const ReportCard = ({ healthReport }: IReportCardProps) => {
|
||||
const healthRatingColor =
|
||||
healthReport.health < 50
|
||||
? 'error.main'
|
||||
: healthReport.health < 75
|
||||
? 'warning.main'
|
||||
: 'success.main';
|
||||
|
||||
const StalenessInfoIcon = () => (
|
||||
<HtmlTooltip
|
||||
title={
|
||||
<>
|
||||
If your flag exceeds the expected lifetime of its flag type
|
||||
it will be marked as potentially stale.
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<a
|
||||
href='https://docs.getunleash.io/reference/technical-debt#stale-and-potentially-stale-flags'
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
Read more in the documentation
|
||||
</a>
|
||||
</Box>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<InfoOutlined
|
||||
sx={{ color: (theme) => theme.palette.text.secondary, ml: 1 }}
|
||||
/>
|
||||
</HtmlTooltip>
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledPaper>
|
||||
<Box>
|
||||
<StyledHeader>Health rating</StyledHeader>
|
||||
<ConditionallyRender
|
||||
condition={healthReport.health > -1}
|
||||
show={
|
||||
<>
|
||||
<StyledHealthRating
|
||||
sx={{ color: healthRatingColor }}
|
||||
>
|
||||
{healthReport.health}%
|
||||
</StyledHealthRating>
|
||||
<StyledLastUpdated>
|
||||
Last updated:{' '}
|
||||
<TimeAgo
|
||||
date={healthReport.updatedAt}
|
||||
refresh={false}
|
||||
/>
|
||||
</StyledLastUpdated>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<StyledHeader>Flag report</StyledHeader>
|
||||
<StyledList>
|
||||
<li>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(healthReport.activeCount)}
|
||||
show={
|
||||
<StyledBoxActive>
|
||||
<CheckIcon />
|
||||
<span>
|
||||
{healthReport.activeCount} active flags
|
||||
</span>
|
||||
</StyledBoxActive>
|
||||
}
|
||||
/>
|
||||
</li>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(healthReport.activeCount)}
|
||||
show={
|
||||
<StyledAlignedItem>
|
||||
Also includes potentially stale flags.
|
||||
</StyledAlignedItem>
|
||||
}
|
||||
/>
|
||||
|
||||
<li>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(healthReport.staleCount)}
|
||||
show={
|
||||
<StyledBoxStale>
|
||||
<ReportProblemOutlinedIcon />
|
||||
<span>
|
||||
{healthReport.staleCount} stale flags
|
||||
</span>
|
||||
</StyledBoxStale>
|
||||
}
|
||||
/>
|
||||
</li>
|
||||
</StyledList>
|
||||
</Box>
|
||||
<Box sx={{ flexBasis: '40%' }}>
|
||||
<StyledHeader>
|
||||
Potential actions{' '}
|
||||
<span>
|
||||
<StalenessInfoIcon />
|
||||
</span>
|
||||
</StyledHeader>
|
||||
<StyledList>
|
||||
<li>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(
|
||||
healthReport.potentiallyStaleCount,
|
||||
)}
|
||||
show={
|
||||
<StyledBoxStale>
|
||||
<ReportProblemOutlinedIcon />
|
||||
<span>
|
||||
{healthReport.potentiallyStaleCount}{' '}
|
||||
potentially stale flags
|
||||
</span>
|
||||
</StyledBoxStale>
|
||||
}
|
||||
/>
|
||||
</li>
|
||||
</StyledList>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(healthReport.potentiallyStaleCount)}
|
||||
show={
|
||||
<>
|
||||
<StyledAlignedItem>
|
||||
Review your feature flags and delete unused
|
||||
flags.
|
||||
</StyledAlignedItem>
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to={'/feature-toggle-type'}
|
||||
>
|
||||
Configure feature types lifetime
|
||||
</Link>
|
||||
</Box>
|
||||
</>
|
||||
}
|
||||
elseShow={<span>No action is required</span>}
|
||||
/>
|
||||
</Box>
|
||||
</StyledPaper>
|
||||
);
|
||||
};
|
@ -1,27 +0,0 @@
|
||||
import type { VFC } from 'react';
|
||||
import { Typography, useTheme } from '@mui/material';
|
||||
import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
|
||||
import type { IReportTableRow } from 'component/project/Project/ProjectHealth/ReportTable/ReportTable';
|
||||
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
||||
|
||||
interface IReportExpiredCellProps {
|
||||
row: {
|
||||
original: IReportTableRow;
|
||||
};
|
||||
}
|
||||
|
||||
export const ReportExpiredCell: VFC<IReportExpiredCellProps> = ({ row }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
if (row.original.expiredAt) {
|
||||
return <DateCell value={row.original.expiredAt} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<TextCell>
|
||||
<Typography variant='body2' color={theme.palette.text.secondary}>
|
||||
N/A
|
||||
</Typography>
|
||||
</TextCell>
|
||||
);
|
||||
};
|
@ -1,34 +0,0 @@
|
||||
import type { IFeatureFlagListItem } from 'interfaces/featureToggle';
|
||||
import { KILLSWITCH, PERMISSION } from 'constants/featureToggleTypes';
|
||||
import { expired, getDiffInDays } from '../utils.js';
|
||||
import { parseISO, subDays } from 'date-fns';
|
||||
import type { FeatureTypeSchema } from 'openapi';
|
||||
|
||||
export const formatExpiredAt = (
|
||||
feature: IFeatureFlagListItem,
|
||||
featureTypes: FeatureTypeSchema[],
|
||||
): string | undefined => {
|
||||
const { type, createdAt } = feature;
|
||||
|
||||
const featureType = featureTypes.find(
|
||||
(featureType) => featureType.id === type,
|
||||
);
|
||||
|
||||
if (
|
||||
featureType &&
|
||||
(featureType.name === KILLSWITCH || featureType.name === PERMISSION)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const date = parseISO(createdAt);
|
||||
const now = new Date();
|
||||
const diff = getDiffInDays(date, now);
|
||||
|
||||
if (featureType && expired(diff, featureType)) {
|
||||
const result = diff - (featureType?.lifetimeDays?.valueOf() || 0);
|
||||
return subDays(now, result).toISOString();
|
||||
}
|
||||
|
||||
return;
|
||||
};
|
@ -1,52 +0,0 @@
|
||||
import type { VFC, ReactElement } from 'react';
|
||||
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
||||
import Check from '@mui/icons-material/Check';
|
||||
import ReportProblemOutlined from '@mui/icons-material/ReportProblemOutlined';
|
||||
import { styled } from '@mui/material';
|
||||
import type { IReportTableRow } from 'component/project/Project/ProjectHealth/ReportTable/ReportTable';
|
||||
|
||||
const StyledTextPotentiallyStale = styled('span')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
gap: '1ch',
|
||||
alignItems: 'center',
|
||||
color: theme.palette.warning.dark,
|
||||
'& svg': { color: theme.palette.warning.main },
|
||||
}));
|
||||
|
||||
const StyledTextHealthy = styled('span')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
gap: '1ch',
|
||||
alignItems: 'center',
|
||||
color: theme.palette.success.dark,
|
||||
'& svg': { color: theme.palette.success.main },
|
||||
}));
|
||||
|
||||
interface IReportStatusCellProps {
|
||||
row: {
|
||||
original: IReportTableRow;
|
||||
};
|
||||
}
|
||||
|
||||
export const ReportStatusCell: VFC<IReportStatusCellProps> = ({
|
||||
row,
|
||||
}): ReactElement => {
|
||||
if (row.original.status === 'potentially-stale') {
|
||||
return (
|
||||
<TextCell>
|
||||
<StyledTextPotentiallyStale>
|
||||
<ReportProblemOutlined />
|
||||
<span>Potentially stale</span>
|
||||
</StyledTextPotentiallyStale>
|
||||
</TextCell>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TextCell>
|
||||
<StyledTextHealthy>
|
||||
<Check />
|
||||
<span>Healthy</span>
|
||||
</StyledTextHealthy>
|
||||
</TextCell>
|
||||
);
|
||||
};
|
@ -1,32 +0,0 @@
|
||||
import type { IFeatureFlagListItem } from 'interfaces/featureToggle';
|
||||
import { expired, getDiffInDays } from '../utils.js';
|
||||
import { KILLSWITCH, PERMISSION } from 'constants/featureToggleTypes';
|
||||
import { parseISO } from 'date-fns';
|
||||
import type { FeatureTypeSchema } from 'openapi';
|
||||
|
||||
export type ReportingStatus = 'potentially-stale' | 'healthy';
|
||||
|
||||
export const formatStatus = (
|
||||
feature: IFeatureFlagListItem,
|
||||
featureTypes: FeatureTypeSchema[],
|
||||
): ReportingStatus => {
|
||||
const { type, createdAt } = feature;
|
||||
|
||||
const featureType = featureTypes.find(
|
||||
(featureType) => featureType.id === type,
|
||||
);
|
||||
const date = parseISO(createdAt);
|
||||
const now = new Date();
|
||||
const diff = getDiffInDays(date, now);
|
||||
|
||||
if (
|
||||
featureType &&
|
||||
expired(diff, featureType) &&
|
||||
type !== KILLSWITCH &&
|
||||
type !== PERMISSION
|
||||
) {
|
||||
return 'potentially-stale';
|
||||
}
|
||||
|
||||
return 'healthy';
|
||||
};
|
@ -1,237 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import type {
|
||||
IEnvironments,
|
||||
IFeatureFlagListItem,
|
||||
} from 'interfaces/featureToggle';
|
||||
import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
|
||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||
import { sortTypes } from 'utils/sortTypes';
|
||||
import {
|
||||
useFlexLayout,
|
||||
useGlobalFilter,
|
||||
useSortBy,
|
||||
useTable,
|
||||
} from 'react-table';
|
||||
import { useMediaQuery, useTheme } from '@mui/material';
|
||||
import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell';
|
||||
import { FeatureNameCell } from 'component/common/Table/cells/FeatureNameCell/FeatureNameCell';
|
||||
import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
|
||||
import { FeatureStaleCell } from 'component/feature/FeatureToggleList/FeatureStaleCell/FeatureStaleCell';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { Search } from 'component/common/Search/Search';
|
||||
import { ReportExpiredCell } from './ReportExpiredCell/ReportExpiredCell.tsx';
|
||||
import { ReportStatusCell } from './ReportStatusCell/ReportStatusCell.tsx';
|
||||
import {
|
||||
formatStatus,
|
||||
type ReportingStatus,
|
||||
} from './ReportStatusCell/formatStatus.ts';
|
||||
import { formatExpiredAt } from './ReportExpiredCell/formatExpiredAt.ts';
|
||||
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import { FeatureEnvironmentSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell';
|
||||
import useFeatureTypes from 'hooks/api/getters/useFeatureTypes/useFeatureTypes';
|
||||
|
||||
interface IReportTableProps {
|
||||
projectId: string;
|
||||
features: IFeatureFlagListItem[];
|
||||
}
|
||||
|
||||
export interface IReportTableRow {
|
||||
project: string;
|
||||
name: string;
|
||||
type: string;
|
||||
stale?: boolean;
|
||||
status: ReportingStatus;
|
||||
lastSeenAt?: string;
|
||||
environments?: IEnvironments[];
|
||||
createdAt: string;
|
||||
expiredAt?: string;
|
||||
}
|
||||
|
||||
export const ReportTable = ({ projectId, features }: IReportTableProps) => {
|
||||
const theme = useTheme();
|
||||
const isExtraSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const isMediumScreen = useMediaQuery(theme.breakpoints.down('lg'));
|
||||
const { uiConfig } = useUiConfig();
|
||||
|
||||
const { featureTypes } = useFeatureTypes();
|
||||
|
||||
const data: IReportTableRow[] = useMemo<IReportTableRow[]>(
|
||||
() =>
|
||||
features.map((report) => ({
|
||||
project: projectId,
|
||||
name: report.name,
|
||||
type: report.type,
|
||||
stale: report.stale,
|
||||
environments: report.environments,
|
||||
status: formatStatus(report, featureTypes),
|
||||
lastSeenAt: report.lastSeenAt,
|
||||
createdAt: report.createdAt,
|
||||
expiredAt: formatExpiredAt(report, featureTypes),
|
||||
})),
|
||||
[projectId, features, featureTypes],
|
||||
);
|
||||
|
||||
const initialState = useMemo(
|
||||
() => ({
|
||||
hiddenColumns: [],
|
||||
sortBy: [{ id: 'createdAt', desc: true }],
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const COLUMNS = useMemo(
|
||||
() => [
|
||||
{
|
||||
Header: 'Seen',
|
||||
accessor: 'lastSeenAt',
|
||||
Cell: ({ value, row: { original: feature } }: any) => {
|
||||
return <FeatureEnvironmentSeenCell feature={feature} />;
|
||||
},
|
||||
align: 'center',
|
||||
maxWidth: 80,
|
||||
},
|
||||
{
|
||||
Header: 'Type',
|
||||
accessor: 'type',
|
||||
align: 'center',
|
||||
Cell: FeatureTypeCell,
|
||||
disableGlobalFilter: true,
|
||||
maxWidth: 85,
|
||||
},
|
||||
{
|
||||
Header: 'Name',
|
||||
accessor: 'name',
|
||||
sortType: 'alphanumeric',
|
||||
Cell: FeatureNameCell,
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
Header: 'Created',
|
||||
accessor: 'createdAt',
|
||||
Cell: DateCell,
|
||||
disableGlobalFilter: true,
|
||||
maxWidth: 150,
|
||||
},
|
||||
{
|
||||
Header: 'Expired',
|
||||
accessor: 'expiredAt',
|
||||
Cell: ReportExpiredCell,
|
||||
disableGlobalFilter: true,
|
||||
maxWidth: 150,
|
||||
},
|
||||
{
|
||||
Header: 'Status',
|
||||
id: 'status',
|
||||
accessor: 'status',
|
||||
Cell: ReportStatusCell,
|
||||
disableGlobalFilter: true,
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
Header: 'State',
|
||||
accessor: 'stale',
|
||||
sortType: 'boolean',
|
||||
Cell: FeatureStaleCell,
|
||||
disableGlobalFilter: true,
|
||||
maxWidth: 120,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
const {
|
||||
headerGroups,
|
||||
rows,
|
||||
prepareRow,
|
||||
state: { globalFilter },
|
||||
setGlobalFilter,
|
||||
setHiddenColumns,
|
||||
} = useTable(
|
||||
{
|
||||
columns: COLUMNS as any,
|
||||
data: data as any,
|
||||
initialState,
|
||||
sortTypes,
|
||||
autoResetGlobalFilter: false,
|
||||
autoResetHiddenColumns: false,
|
||||
autoResetSortBy: false,
|
||||
disableSortRemove: true,
|
||||
},
|
||||
useGlobalFilter,
|
||||
useFlexLayout,
|
||||
useSortBy,
|
||||
);
|
||||
|
||||
useConditionallyHiddenColumns(
|
||||
[
|
||||
{
|
||||
condition: isExtraSmallScreen,
|
||||
columns: ['stale'],
|
||||
},
|
||||
{
|
||||
condition: isSmallScreen,
|
||||
columns: ['expiredAt', 'lastSeenAt'],
|
||||
},
|
||||
{
|
||||
condition: isMediumScreen,
|
||||
columns: ['createdAt'],
|
||||
},
|
||||
],
|
||||
setHiddenColumns,
|
||||
COLUMNS,
|
||||
);
|
||||
|
||||
const title =
|
||||
rows.length < data.length
|
||||
? `Feature flags (${rows.length} of ${data.length})`
|
||||
: `Feature flags (${data.length})`;
|
||||
|
||||
return (
|
||||
<PageContent
|
||||
header={
|
||||
<PageHeader
|
||||
title={title}
|
||||
actions={
|
||||
<Search
|
||||
initialValue={globalFilter}
|
||||
onChange={setGlobalFilter}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<SearchHighlightProvider value={globalFilter}>
|
||||
<VirtualizedTable
|
||||
headerGroups={headerGroups}
|
||||
prepareRow={prepareRow}
|
||||
rows={rows}
|
||||
/>
|
||||
</SearchHighlightProvider>
|
||||
<ConditionallyRender
|
||||
condition={rows.length === 0}
|
||||
show={
|
||||
<ConditionallyRender
|
||||
condition={globalFilter?.length > 0}
|
||||
show={
|
||||
<TablePlaceholder>
|
||||
No feature flags found matching “
|
||||
{globalFilter}
|
||||
”
|
||||
</TablePlaceholder>
|
||||
}
|
||||
elseShow={
|
||||
<TablePlaceholder>
|
||||
No feature flags available. Get started by
|
||||
adding a new feature flag.
|
||||
</TablePlaceholder>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</PageContent>
|
||||
);
|
||||
};
|
@ -1,12 +0,0 @@
|
||||
import differenceInDays from 'date-fns/differenceInDays';
|
||||
import type { FeatureTypeSchema } from 'openapi';
|
||||
|
||||
export const getDiffInDays = (date: Date, now: Date) => {
|
||||
return Math.abs(differenceInDays(date, now));
|
||||
};
|
||||
|
||||
export const expired = (diff: number, type: FeatureTypeSchema) => {
|
||||
if (type.lifetimeDays) return diff >= type?.lifetimeDays?.valueOf();
|
||||
|
||||
return false;
|
||||
};
|
@ -1,42 +0,0 @@
|
||||
import useSWR, { mutate, type SWRConfiguration } from 'swr';
|
||||
import { useCallback } from 'react';
|
||||
import type { IProjectHealthReport } from 'interfaces/project';
|
||||
import { formatApiPath } from 'utils/formatPath';
|
||||
import handleErrorResponses from '../httpErrorResponseHandler.js';
|
||||
|
||||
interface IUseHealthReportOutput {
|
||||
healthReport: IProjectHealthReport | undefined;
|
||||
refetchHealthReport: () => void;
|
||||
loading: boolean;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
export const useHealthReport = (
|
||||
projectId: string,
|
||||
options?: SWRConfiguration,
|
||||
): IUseHealthReportOutput => {
|
||||
const path = formatApiPath(`api/admin/projects/${projectId}/health-report`);
|
||||
|
||||
const { data, error } = useSWR<IProjectHealthReport>(
|
||||
path,
|
||||
fetchHealthReport,
|
||||
options,
|
||||
);
|
||||
|
||||
const refetchHealthReport = useCallback(() => {
|
||||
mutate(path).catch(console.warn);
|
||||
}, [path]);
|
||||
|
||||
return {
|
||||
healthReport: data,
|
||||
refetchHealthReport,
|
||||
loading: !error && !data,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
const fetchHealthReport = (path: string): Promise<IProjectHealthReport> => {
|
||||
return fetch(path)
|
||||
.then(handleErrorResponses('Health report'))
|
||||
.then((res) => res.json());
|
||||
};
|
@ -1,14 +0,0 @@
|
||||
/**
|
||||
* Generated by Orval
|
||||
* Do not edit manually.
|
||||
* See `gen:api` script in package.json
|
||||
*/
|
||||
|
||||
export type GetProjectHealthReport401 = {
|
||||
/** The ID of the error instance */
|
||||
id?: string;
|
||||
/** A description of what went wrong. */
|
||||
message?: string;
|
||||
/** The name of the error kind */
|
||||
name?: string;
|
||||
};
|
@ -1,14 +0,0 @@
|
||||
/**
|
||||
* Generated by Orval
|
||||
* Do not edit manually.
|
||||
* See `gen:api` script in package.json
|
||||
*/
|
||||
|
||||
export type GetProjectHealthReport403 = {
|
||||
/** The ID of the error instance */
|
||||
id?: string;
|
||||
/** A description of what went wrong. */
|
||||
message?: string;
|
||||
/** The name of the error kind */
|
||||
name?: string;
|
||||
};
|
@ -1,14 +0,0 @@
|
||||
/**
|
||||
* Generated by Orval
|
||||
* Do not edit manually.
|
||||
* See `gen:api` script in package.json
|
||||
*/
|
||||
|
||||
export type GetProjectHealthReport404 = {
|
||||
/** The ID of the error instance */
|
||||
id?: string;
|
||||
/** A description of what went wrong. */
|
||||
message?: string;
|
||||
/** The name of the error kind */
|
||||
name?: string;
|
||||
};
|
@ -1,66 +0,0 @@
|
||||
/**
|
||||
* Generated by Orval
|
||||
* Do not edit manually.
|
||||
* See `gen:api` script in package.json
|
||||
*/
|
||||
import type { ProjectEnvironmentSchema } from './projectEnvironmentSchema.js';
|
||||
import type { CreateFeatureNamingPatternSchema } from './createFeatureNamingPatternSchema.js';
|
||||
import type { FeatureSchema } from './featureSchema.js';
|
||||
import type { HealthReportSchemaMode } from './healthReportSchemaMode.js';
|
||||
import type { ProjectStatsSchema } from './projectStatsSchema.js';
|
||||
|
||||
/**
|
||||
* A report of the current health of the requested project, with datapoints like counters of currently active, stale, and potentially stale feature flags.
|
||||
*/
|
||||
export interface HealthReportSchema {
|
||||
/** The number of active feature flags. */
|
||||
activeCount: number;
|
||||
/**
|
||||
* When the project was last updated.
|
||||
* @nullable
|
||||
*/
|
||||
createdAt?: string | null;
|
||||
/** A default stickiness for the project affecting the default stickiness value for variants and Gradual Rollout strategy */
|
||||
defaultStickiness: string;
|
||||
/**
|
||||
* The project's description
|
||||
* @nullable
|
||||
*/
|
||||
description?: string | null;
|
||||
/** An array containing the names of all the environments configured for the project. */
|
||||
environments: ProjectEnvironmentSchema[];
|
||||
/** Indicates if the project has been marked as a favorite by the current user requesting the project health overview. */
|
||||
favorite?: boolean;
|
||||
/**
|
||||
* A limit on the number of features allowed in the project. Null if no limit.
|
||||
* @nullable
|
||||
*/
|
||||
featureLimit?: number | null;
|
||||
featureNaming?: CreateFeatureNamingPatternSchema;
|
||||
/** An array containing an overview of all the features of the project and their individual status */
|
||||
features: FeatureSchema[];
|
||||
/** The overall [health rating](https://docs.getunleash.io/reference/technical-debt#project-status) of the project. */
|
||||
health: number;
|
||||
/**
|
||||
* The number of users/members in the project.
|
||||
* @minimum 0
|
||||
*/
|
||||
members: number;
|
||||
/** The project's [collaboration mode](https://docs.getunleash.io/reference/project-collaboration-mode). Determines whether non-project members can submit change requests or not. */
|
||||
mode: HealthReportSchemaMode;
|
||||
/** The project's name */
|
||||
name: string;
|
||||
/** The number of potentially stale feature flags. */
|
||||
potentiallyStaleCount: number;
|
||||
/** The number of stale feature flags. */
|
||||
staleCount: number;
|
||||
/** Project statistics */
|
||||
stats?: ProjectStatsSchema;
|
||||
/**
|
||||
* When the project was last updated.
|
||||
* @nullable
|
||||
*/
|
||||
updatedAt?: string | null;
|
||||
/** The project overview version. */
|
||||
version: number;
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
/**
|
||||
* Generated by Orval
|
||||
* Do not edit manually.
|
||||
* See `gen:api` script in package.json
|
||||
*/
|
||||
|
||||
/**
|
||||
* The project's [collaboration mode](https://docs.getunleash.io/reference/project-collaboration-mode). Determines whether non-project members can submit change requests or not.
|
||||
*/
|
||||
export type HealthReportSchemaMode =
|
||||
(typeof HealthReportSchemaMode)[keyof typeof HealthReportSchemaMode];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||
export const HealthReportSchemaMode = {
|
||||
open: 'open',
|
||||
protected: 'protected',
|
||||
private: 'private',
|
||||
} as const;
|
@ -805,9 +805,6 @@ export * from './getProjectEnvironments404.js';
|
||||
export * from './getProjectFlagCreators401.js';
|
||||
export * from './getProjectFlagCreators403.js';
|
||||
export * from './getProjectFlagCreators404.js';
|
||||
export * from './getProjectHealthReport401.js';
|
||||
export * from './getProjectHealthReport403.js';
|
||||
export * from './getProjectHealthReport404.js';
|
||||
export * from './getProjectInsights401.js';
|
||||
export * from './getProjectInsights403.js';
|
||||
export * from './getProjectInsights404.js';
|
||||
@ -893,8 +890,6 @@ export * from './healthCheckSchema.js';
|
||||
export * from './healthCheckSchemaHealth.js';
|
||||
export * from './healthOverviewSchema.js';
|
||||
export * from './healthOverviewSchemaMode.js';
|
||||
export * from './healthReportSchema.js';
|
||||
export * from './healthReportSchemaMode.js';
|
||||
export * from './idSchema.js';
|
||||
export * from './idsSchema.js';
|
||||
export * from './importToggles404.js';
|
||||
|
@ -104,7 +104,6 @@ const PATH_STRATEGY = `${PATH_STRATEGIES}/:strategyId`;
|
||||
type ProjectFeaturesServices = Pick<
|
||||
IUnleashServices,
|
||||
| 'featureToggleService'
|
||||
| 'projectHealthService'
|
||||
| 'openApiService'
|
||||
| 'transactionalFeatureToggleService'
|
||||
| 'featureTagService'
|
||||
|
@ -10,7 +10,6 @@ import {
|
||||
} from '../../types/index.js';
|
||||
import ProjectFeaturesController from '../feature-toggle/feature-toggle-controller.js';
|
||||
import ProjectEnvironmentsController from '../project-environments/project-environments-controller.js';
|
||||
import ProjectHealthReport from '../../routes/admin-api/project/health-report.js';
|
||||
import type ProjectService from './project-service.js';
|
||||
import VariantsController from '../../routes/admin-api/project/variants.js';
|
||||
import {
|
||||
@ -226,7 +225,6 @@ export default class ProjectController extends Controller {
|
||||
'/',
|
||||
new ProjectEnvironmentsController(config, services).router,
|
||||
);
|
||||
this.use('/', new ProjectHealthReport(config, services).router);
|
||||
this.use('/', new VariantsController(config, services).router);
|
||||
this.use('/', new ProjectApiTokenController(config, services).router);
|
||||
this.use('/', new ProjectArchiveController(config, services).router);
|
||||
|
@ -1,35 +0,0 @@
|
||||
import type { FromSchema } from 'json-schema-to-ts';
|
||||
import { healthOverviewSchema } from './health-overview-schema.js';
|
||||
|
||||
export const healthReportSchema = {
|
||||
...healthOverviewSchema,
|
||||
$id: '#/components/schemas/healthReportSchema',
|
||||
description:
|
||||
'A report of the current health of the requested project, with datapoints like counters of currently active, stale, and potentially stale feature flags.',
|
||||
required: [
|
||||
...healthOverviewSchema.required,
|
||||
'potentiallyStaleCount',
|
||||
'activeCount',
|
||||
'staleCount',
|
||||
],
|
||||
properties: {
|
||||
...healthOverviewSchema.properties,
|
||||
potentiallyStaleCount: {
|
||||
type: 'number',
|
||||
description: 'The number of potentially stale feature flags.',
|
||||
example: 5,
|
||||
},
|
||||
activeCount: {
|
||||
type: 'number',
|
||||
description: 'The number of active feature flags.',
|
||||
example: 2,
|
||||
},
|
||||
staleCount: {
|
||||
type: 'number',
|
||||
description: 'The number of stale feature flags.',
|
||||
example: 10,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type HealthReportSchema = FromSchema<typeof healthReportSchema>;
|
@ -110,7 +110,6 @@ export * from './group-user-model-schema.js';
|
||||
export * from './groups-schema.js';
|
||||
export * from './health-check-schema.js';
|
||||
export * from './health-overview-schema.js';
|
||||
export * from './health-report-schema.js';
|
||||
export * from './id-schema.js';
|
||||
export * from './ids-schema.js';
|
||||
export * from './import-toggles-schema.js';
|
||||
|
@ -1,73 +0,0 @@
|
||||
import type { Request, Response } from 'express';
|
||||
import Controller from '../../controller.js';
|
||||
import type { IUnleashServices } from '../../../services/index.js';
|
||||
import type { IUnleashConfig } from '../../../types/option.js';
|
||||
import type ProjectHealthService from '../../../services/project-health-service.js';
|
||||
import type { Logger } from '../../../logger.js';
|
||||
import type { IProjectParam } from '../../../types/model.js';
|
||||
import { NONE } from '../../../types/permissions.js';
|
||||
import type { OpenApiService } from '../../../services/openapi-service.js';
|
||||
import { createResponseSchema } from '../../../openapi/util/create-response-schema.js';
|
||||
import { getStandardResponses } from '../../../openapi/util/standard-responses.js';
|
||||
import { serializeDates } from '../../../types/serialize-dates.js';
|
||||
import {
|
||||
healthReportSchema,
|
||||
type HealthReportSchema,
|
||||
} from '../../../openapi/spec/health-report-schema.js';
|
||||
|
||||
export default class ProjectHealthReport extends Controller {
|
||||
private projectHealthService: ProjectHealthService;
|
||||
|
||||
private openApiService: OpenApiService;
|
||||
|
||||
private logger: Logger;
|
||||
|
||||
constructor(
|
||||
config: IUnleashConfig,
|
||||
{
|
||||
projectHealthService,
|
||||
openApiService,
|
||||
}: Pick<IUnleashServices, 'projectHealthService' | 'openApiService'>,
|
||||
) {
|
||||
super(config);
|
||||
this.logger = config.getLogger('/admin-api/project/health-report');
|
||||
this.projectHealthService = projectHealthService;
|
||||
this.openApiService = openApiService;
|
||||
|
||||
this.route({
|
||||
method: 'get',
|
||||
path: '/:projectId/health-report',
|
||||
handler: this.getProjectHealthReport,
|
||||
permission: NONE,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['Projects'],
|
||||
deprecated: true,
|
||||
operationId: 'getProjectHealthReport',
|
||||
summary: 'Get a health report for a project.',
|
||||
description:
|
||||
'This endpoint returns a health report for the specified project. This data is used for [the technical debt insights](https://docs.getunleash.io/reference/technical-debt)',
|
||||
responses: {
|
||||
200: createResponseSchema('healthReportSchema'),
|
||||
...getStandardResponses(401, 403, 404),
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async getProjectHealthReport(
|
||||
req: Request<IProjectParam>,
|
||||
res: Response<HealthReportSchema>,
|
||||
): Promise<void> {
|
||||
const { projectId } = req.params;
|
||||
const overview =
|
||||
await this.projectHealthService.getProjectHealthReport(projectId);
|
||||
this.openApiService.respondWithValidation(
|
||||
200,
|
||||
res,
|
||||
healthReportSchema.$id,
|
||||
serializeDates(overview),
|
||||
);
|
||||
}
|
||||
}
|
@ -326,11 +326,7 @@ export const createServices = (
|
||||
? createProjectStatusService(db, config)
|
||||
: createFakeProjectStatusService().projectStatusService;
|
||||
|
||||
const projectHealthService = new ProjectHealthService(
|
||||
stores,
|
||||
config,
|
||||
projectService,
|
||||
);
|
||||
const projectHealthService = new ProjectHealthService(stores, config);
|
||||
|
||||
const exportImportService = db
|
||||
? createExportImportTogglesService(db, config)
|
||||
|
@ -1,15 +1,11 @@
|
||||
import type { IUnleashStores } from '../types/stores.js';
|
||||
import type { IUnleashConfig } from '../types/option.js';
|
||||
import type { Logger } from '../logger.js';
|
||||
import type { IProject, IProjectHealthReport } from '../types/model.js';
|
||||
import type { IProject } from '../types/model.js';
|
||||
import type { IFeatureToggleStore } from '../features/feature-toggle/types/feature-toggle-store-type.js';
|
||||
import type { IFeatureTypeStore } from '../types/stores/feature-type-store.js';
|
||||
import type { IProjectStore } from '../features/project/project-store-type.js';
|
||||
import type ProjectService from '../features/project/project-service.js';
|
||||
import {
|
||||
calculateProjectHealth,
|
||||
calculateProjectHealthRating,
|
||||
} from '../domain/project-health/project-health.js';
|
||||
import { calculateProjectHealthRating } from '../domain/project-health/project-health.js';
|
||||
import { batchExecute } from '../util/index.js';
|
||||
import metricsHelper from '../util/metrics-helper.js';
|
||||
import { FUNCTION_TIME } from '../metric-events.js';
|
||||
@ -23,8 +19,6 @@ export default class ProjectHealthService {
|
||||
|
||||
private featureToggleStore: IFeatureToggleStore;
|
||||
|
||||
private projectService: ProjectService;
|
||||
|
||||
calculateHealthRating: (project: Pick<IProject, 'id'>) => Promise<number>;
|
||||
|
||||
private timer: Function;
|
||||
@ -39,14 +33,12 @@ export default class ProjectHealthService {
|
||||
'projectStore' | 'featureTypeStore' | 'featureToggleStore'
|
||||
>,
|
||||
{ getLogger, eventBus }: Pick<IUnleashConfig, 'getLogger' | 'eventBus'>,
|
||||
projectService: ProjectService,
|
||||
) {
|
||||
this.logger = getLogger('services/project-health-service.ts');
|
||||
this.projectStore = projectStore;
|
||||
this.featureTypeStore = featureTypeStore;
|
||||
this.featureToggleStore = featureToggleStore;
|
||||
|
||||
this.projectService = projectService;
|
||||
this.calculateHealthRating = calculateProjectHealthRating(
|
||||
this.featureTypeStore,
|
||||
this.featureToggleStore,
|
||||
@ -58,28 +50,6 @@ export default class ProjectHealthService {
|
||||
});
|
||||
}
|
||||
|
||||
async getProjectHealthReport(
|
||||
projectId: string,
|
||||
): Promise<IProjectHealthReport> {
|
||||
const featureTypes = await this.featureTypeStore.getAll();
|
||||
|
||||
const overview = await this.projectService.getProjectHealth(
|
||||
projectId,
|
||||
false,
|
||||
undefined,
|
||||
);
|
||||
|
||||
const healthRating = calculateProjectHealth(
|
||||
overview.features,
|
||||
featureTypes,
|
||||
);
|
||||
|
||||
return {
|
||||
...overview,
|
||||
...healthRating,
|
||||
};
|
||||
}
|
||||
|
||||
async setHealthRating(batchSize = 1): Promise<void> {
|
||||
const projects = await this.projectStore.getAll();
|
||||
|
||||
|
@ -77,180 +77,6 @@ test('Project with no stale toggles should have 100% health rating', async () =>
|
||||
});
|
||||
});
|
||||
|
||||
test('Health rating endpoint yields stale, potentially stale and active count on top of health', async () => {
|
||||
const project = {
|
||||
id: 'test-health',
|
||||
name: 'Health rating',
|
||||
description: 'Fancy',
|
||||
};
|
||||
await app.services.projectService.createProject(
|
||||
project,
|
||||
user,
|
||||
extractAuditInfoFromUser(user),
|
||||
);
|
||||
await app.request
|
||||
.post(`/api/admin/projects/${project.id}/features`)
|
||||
.send({
|
||||
name: 'health-report-new',
|
||||
description: 'new',
|
||||
stale: false,
|
||||
})
|
||||
.expect(201);
|
||||
await app.request
|
||||
.post(`/api/admin/projects/${project.id}/features`)
|
||||
.send({
|
||||
name: 'health-report-new-2',
|
||||
description: 'new too',
|
||||
stale: false,
|
||||
})
|
||||
.expect(201);
|
||||
await app.request
|
||||
.post(`/api/admin/projects/${project.id}/features`)
|
||||
.send({
|
||||
name: 'health-report-stale',
|
||||
description: 'new too',
|
||||
stale: true,
|
||||
})
|
||||
.expect(201);
|
||||
await app.services.projectHealthService.setProjectHealthRating(project.id);
|
||||
await app.request
|
||||
.get(`/api/admin/projects/${project.id}/health-report`)
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect((res) => {
|
||||
expect(res.body.health).toBe(67);
|
||||
expect(res.body.activeCount).toBe(2);
|
||||
expect(res.body.staleCount).toBe(1);
|
||||
expect(res.body.potentiallyStaleCount).toBe(0);
|
||||
});
|
||||
});
|
||||
test('Health rating endpoint does not include archived toggles when calculating potentially stale toggles', async () => {
|
||||
const project = {
|
||||
id: 'potentially-stale-archived',
|
||||
name: 'Health rating',
|
||||
description: 'Fancy',
|
||||
};
|
||||
await app.services.projectService.createProject(
|
||||
project,
|
||||
user,
|
||||
extractAuditInfoFromUser(user),
|
||||
);
|
||||
await app.request
|
||||
.post(`/api/admin/projects/${project.id}/features`)
|
||||
.send({
|
||||
name: 'potentially-stale-archive-fresh',
|
||||
description: 'new',
|
||||
stale: false,
|
||||
})
|
||||
.expect(201);
|
||||
await app.request
|
||||
.post(`/api/admin/projects/${project.id}/features`)
|
||||
.send({
|
||||
name: 'potentially-stale-archive-fresh-2',
|
||||
description: 'new too',
|
||||
stale: false,
|
||||
})
|
||||
.expect(201);
|
||||
await app.request
|
||||
.post(`/api/admin/projects/${project.id}/features`)
|
||||
.send({
|
||||
name: 'potentially-stale-archive-stale',
|
||||
description: 'stale',
|
||||
stale: true,
|
||||
})
|
||||
.expect(201);
|
||||
await app.request
|
||||
.post(`/api/admin/projects/${project.id}/features`)
|
||||
.send({
|
||||
name: 'potentially-archive-stale',
|
||||
description: 'Really Old',
|
||||
createdAt: new Date(2019, 5, 1),
|
||||
})
|
||||
.expect(201);
|
||||
await app.request
|
||||
.post(`/api/admin/projects/${project.id}/features`)
|
||||
.send({
|
||||
name: 'potentially-archive-stale-archived',
|
||||
description: 'Really Old',
|
||||
createdAt: new Date(2019, 5, 1),
|
||||
archived: true,
|
||||
})
|
||||
.expect(201);
|
||||
|
||||
await app.services.projectHealthService.setProjectHealthRating(project.id);
|
||||
await app.request
|
||||
.get(`/api/admin/projects/${project.id}/health-report`)
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect((res) => {
|
||||
expect(res.body.health).toBe(50);
|
||||
expect(res.body.activeCount).toBe(3);
|
||||
expect(res.body.staleCount).toBe(1);
|
||||
expect(res.body.potentiallyStaleCount).toBe(1);
|
||||
});
|
||||
});
|
||||
test('Health rating endpoint correctly handles potentially stale toggles', async () => {
|
||||
const project = {
|
||||
id: 'potentially-stale',
|
||||
name: 'Health rating',
|
||||
description: 'Fancy',
|
||||
};
|
||||
await app.services.projectService.createProject(
|
||||
project,
|
||||
user,
|
||||
extractAuditInfoFromUser(user),
|
||||
);
|
||||
await app.request
|
||||
.post(`/api/admin/projects/${project.id}/features`)
|
||||
.send({
|
||||
name: 'potentially-stale-fresh',
|
||||
description: 'new',
|
||||
stale: false,
|
||||
})
|
||||
.expect(201);
|
||||
await app.request
|
||||
.post(`/api/admin/projects/${project.id}/features`)
|
||||
.send({
|
||||
name: 'potentially-stale-fresh-2',
|
||||
description: 'new too',
|
||||
stale: false,
|
||||
})
|
||||
.expect(201);
|
||||
await app.request
|
||||
.post(`/api/admin/projects/${project.id}/features`)
|
||||
.send({
|
||||
name: 'potentially-stale-stale',
|
||||
description: 'stale',
|
||||
stale: true,
|
||||
})
|
||||
.expect(201);
|
||||
await app.request
|
||||
.post(`/api/admin/projects/${project.id}/features`)
|
||||
.send({
|
||||
name: 'potentially-stale',
|
||||
description: 'Really Old',
|
||||
createdAt: new Date(2019, 5, 1),
|
||||
})
|
||||
.expect(201);
|
||||
await app.services.projectHealthService.setProjectHealthRating(project.id);
|
||||
await app.request
|
||||
.get(`/api/admin/projects/${project.id}/health-report`)
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect((res) => {
|
||||
expect(res.body.health).toBe(50);
|
||||
expect(res.body.activeCount).toBe(3);
|
||||
expect(res.body.staleCount).toBe(1);
|
||||
expect(res.body.potentiallyStaleCount).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('Health report for non-existing project yields 404', async () => {
|
||||
await app.request
|
||||
.get('/api/admin/projects/some-crazy-project-name/health-report')
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
test('Sorts environments by sort order', async () => {
|
||||
const envOne = 'my-sorted-env1';
|
||||
const envTwo = 'my-sorted-env2';
|
||||
@ -337,16 +163,3 @@ test('Sorts environments correctly if sort order is equal', async () => {
|
||||
expect(feature.environments[1].name).toBe(envTwo);
|
||||
});
|
||||
});
|
||||
|
||||
test('Update update_at when setHealth runs', async () => {
|
||||
await app.services.projectHealthService.setProjectHealthRating('default');
|
||||
await app.request
|
||||
.get('/api/admin/projects/default/health-report')
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect((res) => {
|
||||
const now = new Date().getTime();
|
||||
const updatedAt = new Date(res.body.updatedAt).getTime();
|
||||
expect(now - updatedAt).toBeLessThan(5000);
|
||||
});
|
||||
});
|
||||
|
@ -25,11 +25,7 @@ beforeAll(async () => {
|
||||
email: 'test@getunleash.io',
|
||||
});
|
||||
projectService = createProjectService(db.rawDatabase, config);
|
||||
projectHealthService = new ProjectHealthService(
|
||||
stores,
|
||||
config,
|
||||
projectService,
|
||||
);
|
||||
projectHealthService = new ProjectHealthService(stores, config);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
|
Loading…
Reference in New Issue
Block a user