From 5482003b73dcb843620220fc0614b18a468af463 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20G=C3=B3is?= Date: Thu, 29 May 2025 18:18:47 +0100 Subject: [PATCH] chore!: remove deprecated get project health report --- .../src/component/project/Project/Project.tsx | 2 - .../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 ---- .../models/getProjectHealthReport401.ts | 14 -- .../models/getProjectHealthReport403.ts | 14 -- .../models/getProjectHealthReport404.ts | 14 -- .../src/openapi/models/healthReportSchema.ts | 66 ----- .../openapi/models/healthReportSchemaMode.ts | 18 -- frontend/src/openapi/models/index.ts | 5 - .../feature-toggle-controller.ts | 1 - .../features/project/project-controller.ts | 2 - src/lib/openapi/spec/health-report-schema.ts | 35 --- src/lib/openapi/spec/index.ts | 1 - .../routes/admin-api/project/health-report.ts | 73 ------ src/lib/services/index.ts | 6 +- src/lib/services/project-health-service.ts | 34 +-- .../admin/project/project.health.e2e.test.ts | 187 -------------- .../project-health-service.e2e.test.ts | 6 +- 25 files changed, 4 insertions(+), 1175 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 delete mode 100644 frontend/src/openapi/models/getProjectHealthReport401.ts delete mode 100644 frontend/src/openapi/models/getProjectHealthReport403.ts delete mode 100644 frontend/src/openapi/models/getProjectHealthReport404.ts delete mode 100644 frontend/src/openapi/models/healthReportSchema.ts delete mode 100644 frontend/src/openapi/models/healthReportSchemaMode.ts delete mode 100644 src/lib/openapi/spec/health-report-schema.ts delete mode 100644 src/lib/routes/admin-api/project/health-report.ts diff --git a/frontend/src/component/project/Project/Project.tsx b/frontend/src/component/project/Project/Project.tsx index 350f0e4316..3ededdb0d1 100644 --- a/frontend/src/component/project/Project/Project.tsx +++ b/frontend/src/component/project/Project/Project.tsx @@ -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 = () => { }} /> - } /> { - 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()); -}; diff --git a/frontend/src/openapi/models/getProjectHealthReport401.ts b/frontend/src/openapi/models/getProjectHealthReport401.ts deleted file mode 100644 index 41cc08d7cc..0000000000 --- a/frontend/src/openapi/models/getProjectHealthReport401.ts +++ /dev/null @@ -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; -}; diff --git a/frontend/src/openapi/models/getProjectHealthReport403.ts b/frontend/src/openapi/models/getProjectHealthReport403.ts deleted file mode 100644 index fd8f6da75a..0000000000 --- a/frontend/src/openapi/models/getProjectHealthReport403.ts +++ /dev/null @@ -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; -}; diff --git a/frontend/src/openapi/models/getProjectHealthReport404.ts b/frontend/src/openapi/models/getProjectHealthReport404.ts deleted file mode 100644 index 46f5b7e3d5..0000000000 --- a/frontend/src/openapi/models/getProjectHealthReport404.ts +++ /dev/null @@ -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; -}; diff --git a/frontend/src/openapi/models/healthReportSchema.ts b/frontend/src/openapi/models/healthReportSchema.ts deleted file mode 100644 index a59430c6b8..0000000000 --- a/frontend/src/openapi/models/healthReportSchema.ts +++ /dev/null @@ -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; -} diff --git a/frontend/src/openapi/models/healthReportSchemaMode.ts b/frontend/src/openapi/models/healthReportSchemaMode.ts deleted file mode 100644 index a6a077964b..0000000000 --- a/frontend/src/openapi/models/healthReportSchemaMode.ts +++ /dev/null @@ -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; diff --git a/frontend/src/openapi/models/index.ts b/frontend/src/openapi/models/index.ts index 6a94c07a44..d8fda08e26 100644 --- a/frontend/src/openapi/models/index.ts +++ b/frontend/src/openapi/models/index.ts @@ -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'; diff --git a/src/lib/features/feature-toggle/feature-toggle-controller.ts b/src/lib/features/feature-toggle/feature-toggle-controller.ts index 1b2bdcd6cf..0cf13132a9 100644 --- a/src/lib/features/feature-toggle/feature-toggle-controller.ts +++ b/src/lib/features/feature-toggle/feature-toggle-controller.ts @@ -104,7 +104,6 @@ const PATH_STRATEGY = `${PATH_STRATEGIES}/:strategyId`; type ProjectFeaturesServices = Pick< IUnleashServices, | 'featureToggleService' - | 'projectHealthService' | 'openApiService' | 'transactionalFeatureToggleService' | 'featureTagService' diff --git a/src/lib/features/project/project-controller.ts b/src/lib/features/project/project-controller.ts index 34d2272f3c..a7fb713fa3 100644 --- a/src/lib/features/project/project-controller.ts +++ b/src/lib/features/project/project-controller.ts @@ -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); diff --git a/src/lib/openapi/spec/health-report-schema.ts b/src/lib/openapi/spec/health-report-schema.ts deleted file mode 100644 index 4f1f7d5bc4..0000000000 --- a/src/lib/openapi/spec/health-report-schema.ts +++ /dev/null @@ -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; diff --git a/src/lib/openapi/spec/index.ts b/src/lib/openapi/spec/index.ts index be6c1f84f3..77223301c2 100644 --- a/src/lib/openapi/spec/index.ts +++ b/src/lib/openapi/spec/index.ts @@ -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'; diff --git a/src/lib/routes/admin-api/project/health-report.ts b/src/lib/routes/admin-api/project/health-report.ts deleted file mode 100644 index f7f707660d..0000000000 --- a/src/lib/routes/admin-api/project/health-report.ts +++ /dev/null @@ -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, - ) { - 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, - res: Response, - ): Promise { - const { projectId } = req.params; - const overview = - await this.projectHealthService.getProjectHealthReport(projectId); - this.openApiService.respondWithValidation( - 200, - res, - healthReportSchema.$id, - serializeDates(overview), - ); - } -} diff --git a/src/lib/services/index.ts b/src/lib/services/index.ts index 0f80b77f38..7d249461a4 100644 --- a/src/lib/services/index.ts +++ b/src/lib/services/index.ts @@ -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) diff --git a/src/lib/services/project-health-service.ts b/src/lib/services/project-health-service.ts index 1f23e46bc9..1dab54a655 100644 --- a/src/lib/services/project-health-service.ts +++ b/src/lib/services/project-health-service.ts @@ -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) => Promise; private timer: Function; @@ -39,14 +33,12 @@ export default class ProjectHealthService { 'projectStore' | 'featureTypeStore' | 'featureToggleStore' >, { getLogger, eventBus }: Pick, - 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 { - 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 { const projects = await this.projectStore.getAll(); diff --git a/src/test/e2e/api/admin/project/project.health.e2e.test.ts b/src/test/e2e/api/admin/project/project.health.e2e.test.ts index bad978efab..6612c2dc96 100644 --- a/src/test/e2e/api/admin/project/project.health.e2e.test.ts +++ b/src/test/e2e/api/admin/project/project.health.e2e.test.ts @@ -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); - }); -}); diff --git a/src/test/e2e/services/project-health-service.e2e.test.ts b/src/test/e2e/services/project-health-service.e2e.test.ts index 65c974bbd7..7714e7eff4 100644 --- a/src/test/e2e/services/project-health-service.e2e.test.ts +++ b/src/test/e2e/services/project-health-service.e2e.test.ts @@ -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 () => {