From b07c032d56a428edecd5af5be21d6c981f16e800 Mon Sep 17 00:00:00 2001 From: andreas-unleash Date: Wed, 4 Oct 2023 12:47:16 +0300 Subject: [PATCH] fix: update potentially-stale status dynamically (#4905) Fixes 2 bugs: - project-health-service keeping the feature types as an instance variable and only updating it once was preventing real calculation to happen if the lifetime value changed for a feature toggle type - the ui was reading from a predefined map for the lifetime values so they would never reflect the BE change Closes # [SR-66](https://linear.app/unleash/issue/SR-66/slack-question-around-potentially-stale-and-its-uses) Screenshot 2023-10-02 at 14 37 17 Screenshot 2023-10-02 at 14 37 06 --------- Signed-off-by: andreas-unleash --- .../ReportExpiredCell/formatExpiredAt.ts | 21 +++++++++++++----- .../ReportStatusCell/formatStatus.ts | 17 +++++++++++--- .../ProjectHealth/ReportTable/ReportTable.tsx | 10 +++++---- .../ProjectHealth/ReportTable/utils.ts | 17 +++++--------- .../useFeatureTypes/useFeatureTypes.ts | 6 ++--- src/lib/services/project-health-service.ts | 22 +++++-------------- 6 files changed, 49 insertions(+), 44 deletions(-) diff --git a/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportExpiredCell/formatExpiredAt.ts b/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportExpiredCell/formatExpiredAt.ts index 6cf8d27b5039..c0a3a83cc44c 100644 --- a/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportExpiredCell/formatExpiredAt.ts +++ b/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportExpiredCell/formatExpiredAt.ts @@ -1,14 +1,23 @@ import { IFeatureToggleListItem } from 'interfaces/featureToggle'; -import { PERMISSION, KILLSWITCH } from 'constants/featureToggleTypes'; -import { getDiffInDays, expired, toggleExpiryByTypeMap } from '../utils'; -import { subDays, parseISO } from 'date-fns'; +import { KILLSWITCH, PERMISSION } from 'constants/featureToggleTypes'; +import { expired, getDiffInDays } from '../utils'; +import { parseISO, subDays } from 'date-fns'; +import { FeatureTypeSchema } from 'openapi'; export const formatExpiredAt = ( feature: IFeatureToggleListItem, + featureTypes: FeatureTypeSchema[], ): string | undefined => { const { type, createdAt } = feature; - if (type === KILLSWITCH || type === PERMISSION) { + const featureType = featureTypes.find( + (featureType) => featureType.name === type, + ); + + if ( + featureType && + (featureType.name === KILLSWITCH || featureType.name === PERMISSION) + ) { return; } @@ -16,8 +25,8 @@ export const formatExpiredAt = ( const now = new Date(); const diff = getDiffInDays(date, now); - if (expired(diff, type)) { - const result = diff - toggleExpiryByTypeMap[type]; + if (featureType && expired(diff, featureType)) { + const result = diff - (featureType?.lifetimeDays?.valueOf() || 0); return subDays(now, result).toISOString(); } diff --git a/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportStatusCell/formatStatus.ts b/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportStatusCell/formatStatus.ts index 3654597059a4..2d7a2999a358 100644 --- a/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportStatusCell/formatStatus.ts +++ b/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportStatusCell/formatStatus.ts @@ -1,19 +1,30 @@ import { IFeatureToggleListItem } from 'interfaces/featureToggle'; -import { getDiffInDays, expired } from '../utils'; -import { PERMISSION, KILLSWITCH } from 'constants/featureToggleTypes'; +import { expired, getDiffInDays } from '../utils'; +import { KILLSWITCH, PERMISSION } from 'constants/featureToggleTypes'; import { parseISO } from 'date-fns'; +import { FeatureTypeSchema } from 'openapi'; export type ReportingStatus = 'potentially-stale' | 'healthy'; export const formatStatus = ( feature: IFeatureToggleListItem, + featureTypes: FeatureTypeSchema[], ): ReportingStatus => { const { type, createdAt } = feature; + + const featureType = featureTypes.find( + (featureType) => featureType.name === type, + ); const date = parseISO(createdAt); const now = new Date(); const diff = getDiffInDays(date, now); - if (expired(diff, type) && type !== KILLSWITCH && type !== PERMISSION) { + if ( + featureType && + expired(diff, featureType) && + type !== KILLSWITCH && + type !== PERMISSION + ) { return 'potentially-stale'; } diff --git a/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportTable.tsx b/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportTable.tsx index 70c33aa0a31d..43daaf87c460 100644 --- a/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportTable.tsx +++ b/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportTable.tsx @@ -9,10 +9,10 @@ import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightC import { PageHeader } from 'component/common/PageHeader/PageHeader'; import { sortTypes } from 'utils/sortTypes'; import { - useSortBy, + useFlexLayout, useGlobalFilter, + useSortBy, useTable, - useFlexLayout, } from 'react-table'; import { useMediaQuery, useTheme } from '@mui/material'; import { FeatureSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureSeenCell'; @@ -29,6 +29,7 @@ import { formatExpiredAt } from './ReportExpiredCell/formatExpiredAt'; 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; @@ -56,6 +57,7 @@ export const ReportTable = ({ projectId, features }: IReportTableProps) => { const showEnvironmentLastSeen = Boolean( uiConfig.flags.lastSeenByEnvironment, ); + const { featureTypes } = useFeatureTypes(); const data: IReportTableRow[] = useMemo( () => @@ -65,10 +67,10 @@ export const ReportTable = ({ projectId, features }: IReportTableProps) => { type: report.type, stale: report.stale, environments: report.environments, - status: formatStatus(report), + status: formatStatus(report, featureTypes), lastSeenAt: report.lastSeenAt, createdAt: report.createdAt, - expiredAt: formatExpiredAt(report), + expiredAt: formatExpiredAt(report, featureTypes), })), [projectId, features], ); diff --git a/frontend/src/component/project/Project/ProjectHealth/ReportTable/utils.ts b/frontend/src/component/project/Project/ProjectHealth/ReportTable/utils.ts index ab6876044615..fa01f8828599 100644 --- a/frontend/src/component/project/Project/ProjectHealth/ReportTable/utils.ts +++ b/frontend/src/component/project/Project/ProjectHealth/ReportTable/utils.ts @@ -1,19 +1,12 @@ import differenceInDays from 'date-fns/differenceInDays'; -import { EXPERIMENT, OPERATIONAL, RELEASE } from 'constants/featureToggleTypes'; - -const FORTY_DAYS = 40; -const SEVEN_DAYS = 7; - -export const toggleExpiryByTypeMap: Record = { - [EXPERIMENT]: FORTY_DAYS, - [RELEASE]: FORTY_DAYS, - [OPERATIONAL]: SEVEN_DAYS, -}; +import { FeatureTypeSchema } from 'openapi'; export const getDiffInDays = (date: Date, now: Date) => { return Math.abs(differenceInDays(date, now)); }; -export const expired = (diff: number, type: string) => { - return diff >= toggleExpiryByTypeMap[type]; +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/useFeatureTypes/useFeatureTypes.ts b/frontend/src/hooks/api/getters/useFeatureTypes/useFeatureTypes.ts index 71f4e7b001e5..e22e3d414a37 100644 --- a/frontend/src/hooks/api/getters/useFeatureTypes/useFeatureTypes.ts +++ b/frontend/src/hooks/api/getters/useFeatureTypes/useFeatureTypes.ts @@ -1,8 +1,8 @@ import useSWR, { mutate, SWRConfiguration } from 'swr'; -import { useState, useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { formatApiPath } from 'utils/formatPath'; -import { IFeatureType } from 'interfaces/featureTypes'; import handleErrorResponses from '../httpErrorResponseHandler'; +import { FeatureTypeSchema } from '../../../../openapi'; const useFeatureTypes = (options: SWRConfiguration = {}) => { const fetcher = async () => { @@ -27,7 +27,7 @@ const useFeatureTypes = (options: SWRConfiguration = {}) => { }, [data, error]); return { - featureTypes: (data?.types as IFeatureType[]) || [], + featureTypes: (data?.types as FeatureTypeSchema[]) || [], error, loading, refetch, diff --git a/src/lib/services/project-health-service.ts b/src/lib/services/project-health-service.ts index ede477bc8525..ab85391ce113 100644 --- a/src/lib/services/project-health-service.ts +++ b/src/lib/services/project-health-service.ts @@ -3,15 +3,12 @@ import { IUnleashConfig } from '../types/option'; import { Logger } from '../logger'; import type { IProject, IProjectHealthReport } from '../types/model'; import type { IFeatureToggleStore } from '../types/stores/feature-toggle-store'; -import type { - IFeatureType, - IFeatureTypeStore, -} from '../types/stores/feature-type-store'; +import type { IFeatureTypeStore } from '../types/stores/feature-type-store'; import type { IProjectStore } from '../types/stores/project-store'; import ProjectService from './project-service'; import { - calculateProjectHealth, calculateHealthRating, + calculateProjectHealth, } from '../domain/project-health/project-health'; export default class ProjectHealthService { @@ -23,8 +20,6 @@ export default class ProjectHealthService { private featureToggleStore: IFeatureToggleStore; - private featureTypes: IFeatureType[]; - private projectService: ProjectService; constructor( @@ -43,7 +38,6 @@ export default class ProjectHealthService { this.projectStore = projectStore; this.featureTypeStore = featureTypeStore; this.featureToggleStore = featureToggleStore; - this.featureTypes = []; this.projectService = projectService; } @@ -51,9 +45,7 @@ export default class ProjectHealthService { async getProjectHealthReport( projectId: string, ): Promise { - if (this.featureTypes.length === 0) { - this.featureTypes = await this.featureTypeStore.getAll(); - } + const featureTypes = await this.featureTypeStore.getAll(); const overview = await this.projectService.getProjectOverview( projectId, @@ -63,7 +55,7 @@ export default class ProjectHealthService { const healthRating = calculateProjectHealth( overview.features, - this.featureTypes, + featureTypes, ); return { @@ -73,16 +65,14 @@ export default class ProjectHealthService { } async calculateHealthRating(project: IProject): Promise { - if (this.featureTypes.length === 0) { - this.featureTypes = await this.featureTypeStore.getAll(); - } + const featureTypes = await this.featureTypeStore.getAll(); const toggles = await this.featureToggleStore.getAll({ project: project.id, archived: false, }); - return calculateHealthRating(toggles, this.featureTypes); + return calculateHealthRating(toggles, featureTypes); } async setHealthRating(): Promise {