1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-21 13:47:39 +02:00

chore!: remove project health report frontend (#10101)

This commit is contained in:
David Leek 2025-06-10 14:56:41 +02:00 committed by GitHub
parent d5acbea711
commit 7b0cae2b3e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 0 additions and 704 deletions

View File

@ -27,7 +27,6 @@ import useToast from 'hooks/useToast';
import useQueryParams from 'hooks/useQueryParams';
import { useEffect, useState, type ReactNode } from 'react';
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 {
@ -373,8 +372,6 @@ export const Project = () => {
}}
/>
<Routes>
{/* FIXME: remove /health with `healthToTechDebt` flag - redirect to project status */}
<Route path='health' element={<ProjectHealth />} />
<Route
path='access/*'
element={

View File

@ -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;

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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;
};

View File

@ -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>
);
};

View File

@ -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';
};

View File

@ -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 &ldquo;
{globalFilter}
&rdquo;
</TablePlaceholder>
}
elseShow={
<TablePlaceholder>
No feature flags available. Get started by
adding a new feature flag.
</TablePlaceholder>
}
/>
}
/>
</PageContent>
);
};

View File

@ -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;
};

View File

@ -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());
};