From 7b0cae2b3e6fde5296b2afaefd47781441e4ebb7 Mon Sep 17 00:00:00 2001 From: David Leek Date: Tue, 10 Jun 2025 14:56:41 +0200 Subject: [PATCH] chore!: remove project health report frontend (#10101) --- .../src/component/project/Project/Project.tsx | 3 - .../Project/ProjectHealth/ProjectHealth.tsx | 46 ---- .../ReportTable/ReportCard/ReportCard.tsx | 219 ---------------- .../ReportExpiredCell/ReportExpiredCell.tsx | 27 -- .../ReportExpiredCell/formatExpiredAt.ts | 34 --- .../ReportStatusCell/ReportStatusCell.tsx | 52 ---- .../ReportStatusCell/formatStatus.ts | 32 --- .../ProjectHealth/ReportTable/ReportTable.tsx | 237 ------------------ .../ProjectHealth/ReportTable/utils.ts | 12 - .../useHealthReport/useHealthReport.ts | 42 ---- 10 files changed, 704 deletions(-) delete mode 100644 frontend/src/component/project/Project/ProjectHealth/ProjectHealth.tsx delete mode 100644 frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportCard/ReportCard.tsx delete mode 100644 frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportExpiredCell/ReportExpiredCell.tsx delete mode 100644 frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportExpiredCell/formatExpiredAt.ts delete mode 100644 frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportStatusCell/ReportStatusCell.tsx delete mode 100644 frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportStatusCell/formatStatus.ts delete mode 100644 frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportTable.tsx delete mode 100644 frontend/src/component/project/Project/ProjectHealth/ReportTable/utils.ts delete mode 100644 frontend/src/hooks/api/getters/useHealthReport/useHealthReport.ts diff --git a/frontend/src/component/project/Project/Project.tsx b/frontend/src/component/project/Project/Project.tsx index b7f7584b1a..dacc2db65c 100644 --- a/frontend/src/component/project/Project/Project.tsx +++ b/frontend/src/component/project/Project/Project.tsx @@ -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 = () => { }} /> - {/* FIXME: remove /health with `healthToTechDebt` flag - redirect to project status */} - } /> { - 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 ( -
- - } - /> - - -
- ); -}; - -export default ProjectHealth; diff --git a/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportCard/ReportCard.tsx b/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportCard/ReportCard.tsx deleted file mode 100644 index ed9965c418..0000000000 --- a/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportCard/ReportCard.tsx +++ /dev/null @@ -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 = () => ( - - If your flag exceeds the expected lifetime of its flag type - it will be marked as potentially stale. - - - Read more in the documentation - - - - } - > - theme.palette.text.secondary, ml: 1 }} - /> - - ); - - return ( - - - Health rating - -1} - show={ - <> - - {healthReport.health}% - - - Last updated:{' '} - - - - } - /> - - - Flag report - -
  • - - - - {healthReport.activeCount} active flags - - - } - /> -
  • - - Also includes potentially stale flags. - - } - /> - -
  • - - - - {healthReport.staleCount} stale flags - - - } - /> -
  • -
    -
    - - - Potential actions{' '} - - - - - -
  • - - - - {healthReport.potentiallyStaleCount}{' '} - potentially stale flags - - - } - /> -
  • -
    - - - Review your feature flags and delete unused - flags. - - - - Configure feature types lifetime - - - - } - elseShow={No action is required} - /> -
    -
    - ); -}; diff --git a/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportExpiredCell/ReportExpiredCell.tsx b/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportExpiredCell/ReportExpiredCell.tsx deleted file mode 100644 index ac243fadd2..0000000000 --- a/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportExpiredCell/ReportExpiredCell.tsx +++ /dev/null @@ -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 = ({ row }) => { - const theme = useTheme(); - - if (row.original.expiredAt) { - return ; - } - - return ( - - - N/A - - - ); -}; diff --git a/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportExpiredCell/formatExpiredAt.ts b/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportExpiredCell/formatExpiredAt.ts deleted file mode 100644 index 325d738f3a..0000000000 --- a/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportExpiredCell/formatExpiredAt.ts +++ /dev/null @@ -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; -}; diff --git a/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportStatusCell/ReportStatusCell.tsx b/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportStatusCell/ReportStatusCell.tsx deleted file mode 100644 index 5c6d0a8c7b..0000000000 --- a/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportStatusCell/ReportStatusCell.tsx +++ /dev/null @@ -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 = ({ - row, -}): ReactElement => { - if (row.original.status === 'potentially-stale') { - return ( - - - - Potentially stale - - - ); - } - - return ( - - - - Healthy - - - ); -}; diff --git a/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportStatusCell/formatStatus.ts b/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportStatusCell/formatStatus.ts deleted file mode 100644 index 5e174110be..0000000000 --- a/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportStatusCell/formatStatus.ts +++ /dev/null @@ -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'; -}; diff --git a/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportTable.tsx b/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportTable.tsx deleted file mode 100644 index d2b4faf2d4..0000000000 --- a/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportTable.tsx +++ /dev/null @@ -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( - () => - 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 ; - }, - 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 ( - - } - /> - } - > - - - - 0} - show={ - - No feature flags found matching “ - {globalFilter} - ” - - } - elseShow={ - - No feature flags available. Get started by - adding a new feature flag. - - } - /> - } - /> - - ); -}; diff --git a/frontend/src/component/project/Project/ProjectHealth/ReportTable/utils.ts b/frontend/src/component/project/Project/ProjectHealth/ReportTable/utils.ts deleted file mode 100644 index c1a995733f..0000000000 --- a/frontend/src/component/project/Project/ProjectHealth/ReportTable/utils.ts +++ /dev/null @@ -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; -}; diff --git a/frontend/src/hooks/api/getters/useHealthReport/useHealthReport.ts b/frontend/src/hooks/api/getters/useHealthReport/useHealthReport.ts deleted file mode 100644 index bd1d69af04..0000000000 --- a/frontend/src/hooks/api/getters/useHealthReport/useHealthReport.ts +++ /dev/null @@ -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( - 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 => { - return fetch(path) - .then(handleErrorResponses('Health report')) - .then((res) => res.json()); -};