From c16992d919866a5a41c52a40182e1c7182e23571 Mon Sep 17 00:00:00 2001 From: andreas-unleash Date: Mon, 2 Oct 2023 15:00:40 +0300 Subject: [PATCH 01/13] fix: update potentially-stale status dynamically Signed-off-by: andreas-unleash --- .../ReportExpiredCell/formatExpiredAt.ts | 23 +++++++++++++------ .../ReportStatusCell/formatStatus.ts | 21 +++++++++++++---- .../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, 52 insertions(+), 47 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 82c392a34df0..c8afc7c9b6dd 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 + 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 a3775c3b3df2..210935d57e93 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 -): ReportingStatus => { + feature: IFeatureToggleListItem, + featureTypes: FeatureTypeSchema[] +): string => { 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 abddbd1e5050..44fc22ff5f78 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 { From cab0140f2d2d4eeb56cb1157287822912721ec6d Mon Sep 17 00:00:00 2001 From: andreas-unleash Date: Mon, 2 Oct 2023 15:09:53 +0300 Subject: [PATCH 02/13] fix: bug Signed-off-by: andreas-unleash --- .../ProjectHealth/ReportTable/ReportStatusCell/formatStatus.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 210935d57e93..65e595832e62 100644 --- a/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportStatusCell/formatStatus.ts +++ b/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportStatusCell/formatStatus.ts @@ -9,7 +9,7 @@ export type ReportingStatus = 'potentially-stale' | 'healthy'; export const formatStatus = ( feature: IFeatureToggleListItem, featureTypes: FeatureTypeSchema[] -): string => { +): ReportingStatus => { const { type, createdAt } = feature; const featureType = featureTypes.find( From 4ca742f6df30be9ef82c809f8fdb5b02a93a6c24 Mon Sep 17 00:00:00 2001 From: andreas-unleash Date: Mon, 2 Oct 2023 16:03:12 +0300 Subject: [PATCH 03/13] fix: formatting Signed-off-by: andreas-unleash --- .../ReportTable/ReportExpiredCell/formatExpiredAt.ts | 4 ++-- .../ReportTable/ReportStatusCell/formatStatus.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 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 c8afc7c9b6dd..c0a3a83cc44c 100644 --- a/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportExpiredCell/formatExpiredAt.ts +++ b/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportExpiredCell/formatExpiredAt.ts @@ -6,12 +6,12 @@ import { FeatureTypeSchema } from 'openapi'; export const formatExpiredAt = ( feature: IFeatureToggleListItem, - featureTypes: FeatureTypeSchema[] + featureTypes: FeatureTypeSchema[], ): string | undefined => { const { type, createdAt } = feature; const featureType = featureTypes.find( - featureType => featureType.name === type + (featureType) => featureType.name === type, ); if ( 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 65e595832e62..2d7a2999a358 100644 --- a/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportStatusCell/formatStatus.ts +++ b/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportStatusCell/formatStatus.ts @@ -8,12 +8,12 @@ export type ReportingStatus = 'potentially-stale' | 'healthy'; export const formatStatus = ( feature: IFeatureToggleListItem, - featureTypes: FeatureTypeSchema[] + featureTypes: FeatureTypeSchema[], ): ReportingStatus => { const { type, createdAt } = feature; const featureType = featureTypes.find( - featureType => featureType.name === type + (featureType) => featureType.name === type, ); const date = parseISO(createdAt); const now = new Date(); From fa00831a0ed5b7bc15978b8a83bdeba36ce45af0 Mon Sep 17 00:00:00 2001 From: andreas-unleash Date: Mon, 2 Oct 2023 15:00:40 +0300 Subject: [PATCH 04/13] fix: update potentially-stale status dynamically Signed-off-by: andreas-unleash --- .../ReportExpiredCell/formatExpiredAt.ts | 21 +++++++++++++----- .../ReportStatusCell/formatStatus.ts | 19 ++++++++++++---- .../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, 50 insertions(+), 45 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..c8afc7c9b6dd 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..210935d57e93 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, -): ReportingStatus => { + featureTypes: FeatureTypeSchema[] +): string => { 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 { From 9a647d3ac4017be6191f899e946bf959e43d6c02 Mon Sep 17 00:00:00 2001 From: andreas-unleash Date: Mon, 2 Oct 2023 15:09:53 +0300 Subject: [PATCH 05/13] fix: bug Signed-off-by: andreas-unleash --- .../ProjectHealth/ReportTable/ReportStatusCell/formatStatus.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 210935d57e93..65e595832e62 100644 --- a/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportStatusCell/formatStatus.ts +++ b/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportStatusCell/formatStatus.ts @@ -9,7 +9,7 @@ export type ReportingStatus = 'potentially-stale' | 'healthy'; export const formatStatus = ( feature: IFeatureToggleListItem, featureTypes: FeatureTypeSchema[] -): string => { +): ReportingStatus => { const { type, createdAt } = feature; const featureType = featureTypes.find( From 3ffb05ecbd2544e92aaadd2be7784175c3f7785f Mon Sep 17 00:00:00 2001 From: andreas-unleash Date: Mon, 2 Oct 2023 16:03:12 +0300 Subject: [PATCH 06/13] fix: formatting Signed-off-by: andreas-unleash --- .../ReportTable/ReportExpiredCell/formatExpiredAt.ts | 4 ++-- .../ReportTable/ReportStatusCell/formatStatus.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 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 c8afc7c9b6dd..c0a3a83cc44c 100644 --- a/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportExpiredCell/formatExpiredAt.ts +++ b/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportExpiredCell/formatExpiredAt.ts @@ -6,12 +6,12 @@ import { FeatureTypeSchema } from 'openapi'; export const formatExpiredAt = ( feature: IFeatureToggleListItem, - featureTypes: FeatureTypeSchema[] + featureTypes: FeatureTypeSchema[], ): string | undefined => { const { type, createdAt } = feature; const featureType = featureTypes.find( - featureType => featureType.name === type + (featureType) => featureType.name === type, ); if ( 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 65e595832e62..2d7a2999a358 100644 --- a/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportStatusCell/formatStatus.ts +++ b/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportStatusCell/formatStatus.ts @@ -8,12 +8,12 @@ export type ReportingStatus = 'potentially-stale' | 'healthy'; export const formatStatus = ( feature: IFeatureToggleListItem, - featureTypes: FeatureTypeSchema[] + featureTypes: FeatureTypeSchema[], ): ReportingStatus => { const { type, createdAt } = feature; const featureType = featureTypes.find( - featureType => featureType.name === type + (featureType) => featureType.name === type, ); const date = parseISO(createdAt); const now = new Date(); From 28a164ce3ad4a2a60a2610f746f7c3b16562f87c Mon Sep 17 00:00:00 2001 From: andreas-unleash Date: Mon, 2 Oct 2023 15:00:40 +0300 Subject: [PATCH 07/13] fix: update potentially-stale status dynamically Signed-off-by: andreas-unleash --- .../ReportExpiredCell/formatExpiredAt.ts | 21 +++++++++++++----- .../ReportStatusCell/formatStatus.ts | 19 ++++++++++++---- .../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, 50 insertions(+), 45 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..c8afc7c9b6dd 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..210935d57e93 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, -): ReportingStatus => { + featureTypes: FeatureTypeSchema[] +): string => { 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 { From ba39f9167588786733cbc7fdc94ec51215928107 Mon Sep 17 00:00:00 2001 From: andreas-unleash Date: Mon, 2 Oct 2023 15:09:53 +0300 Subject: [PATCH 08/13] fix: bug Signed-off-by: andreas-unleash --- .../ProjectHealth/ReportTable/ReportStatusCell/formatStatus.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 210935d57e93..65e595832e62 100644 --- a/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportStatusCell/formatStatus.ts +++ b/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportStatusCell/formatStatus.ts @@ -9,7 +9,7 @@ export type ReportingStatus = 'potentially-stale' | 'healthy'; export const formatStatus = ( feature: IFeatureToggleListItem, featureTypes: FeatureTypeSchema[] -): string => { +): ReportingStatus => { const { type, createdAt } = feature; const featureType = featureTypes.find( From 33cfd5d70b72c75522d8223f87f04026ea84346e Mon Sep 17 00:00:00 2001 From: andreas-unleash Date: Mon, 2 Oct 2023 16:03:12 +0300 Subject: [PATCH 09/13] fix: formatting Signed-off-by: andreas-unleash --- .../ReportTable/ReportExpiredCell/formatExpiredAt.ts | 4 ++-- .../ReportTable/ReportStatusCell/formatStatus.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 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 c8afc7c9b6dd..c0a3a83cc44c 100644 --- a/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportExpiredCell/formatExpiredAt.ts +++ b/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportExpiredCell/formatExpiredAt.ts @@ -6,12 +6,12 @@ import { FeatureTypeSchema } from 'openapi'; export const formatExpiredAt = ( feature: IFeatureToggleListItem, - featureTypes: FeatureTypeSchema[] + featureTypes: FeatureTypeSchema[], ): string | undefined => { const { type, createdAt } = feature; const featureType = featureTypes.find( - featureType => featureType.name === type + (featureType) => featureType.name === type, ); if ( 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 65e595832e62..2d7a2999a358 100644 --- a/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportStatusCell/formatStatus.ts +++ b/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportStatusCell/formatStatus.ts @@ -8,12 +8,12 @@ export type ReportingStatus = 'potentially-stale' | 'healthy'; export const formatStatus = ( feature: IFeatureToggleListItem, - featureTypes: FeatureTypeSchema[] + featureTypes: FeatureTypeSchema[], ): ReportingStatus => { const { type, createdAt } = feature; const featureType = featureTypes.find( - featureType => featureType.name === type + (featureType) => featureType.name === type, ); const date = parseISO(createdAt); const now = new Date(); From d223e4cbb91aae23baa732c0f7561f24438646b1 Mon Sep 17 00:00:00 2001 From: andreas-unleash Date: Mon, 2 Oct 2023 15:00:40 +0300 Subject: [PATCH 10/13] fix: update potentially-stale status dynamically Signed-off-by: andreas-unleash --- .../ReportTable/ReportExpiredCell/formatExpiredAt.ts | 4 ++-- .../ReportTable/ReportStatusCell/formatStatus.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 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 c0a3a83cc44c..c8afc7c9b6dd 100644 --- a/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportExpiredCell/formatExpiredAt.ts +++ b/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportExpiredCell/formatExpiredAt.ts @@ -6,12 +6,12 @@ import { FeatureTypeSchema } from 'openapi'; export const formatExpiredAt = ( feature: IFeatureToggleListItem, - featureTypes: FeatureTypeSchema[], + featureTypes: FeatureTypeSchema[] ): string | undefined => { const { type, createdAt } = feature; const featureType = featureTypes.find( - (featureType) => featureType.name === type, + featureType => featureType.name === type ); if ( 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 2d7a2999a358..210935d57e93 100644 --- a/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportStatusCell/formatStatus.ts +++ b/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportStatusCell/formatStatus.ts @@ -8,12 +8,12 @@ export type ReportingStatus = 'potentially-stale' | 'healthy'; export const formatStatus = ( feature: IFeatureToggleListItem, - featureTypes: FeatureTypeSchema[], -): ReportingStatus => { + featureTypes: FeatureTypeSchema[] +): string => { const { type, createdAt } = feature; const featureType = featureTypes.find( - (featureType) => featureType.name === type, + featureType => featureType.name === type ); const date = parseISO(createdAt); const now = new Date(); From 4db65996f364df2115ddd3acbfe5e68e5dacc7b9 Mon Sep 17 00:00:00 2001 From: andreas-unleash Date: Mon, 2 Oct 2023 15:09:53 +0300 Subject: [PATCH 11/13] fix: bug Signed-off-by: andreas-unleash --- .../ProjectHealth/ReportTable/ReportStatusCell/formatStatus.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 210935d57e93..65e595832e62 100644 --- a/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportStatusCell/formatStatus.ts +++ b/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportStatusCell/formatStatus.ts @@ -9,7 +9,7 @@ export type ReportingStatus = 'potentially-stale' | 'healthy'; export const formatStatus = ( feature: IFeatureToggleListItem, featureTypes: FeatureTypeSchema[] -): string => { +): ReportingStatus => { const { type, createdAt } = feature; const featureType = featureTypes.find( From e88b4ace5f9632f95023ff3716a5242530fb88c3 Mon Sep 17 00:00:00 2001 From: andreas-unleash Date: Mon, 2 Oct 2023 16:03:12 +0300 Subject: [PATCH 12/13] fix: formatting Signed-off-by: andreas-unleash --- .../ReportTable/ReportExpiredCell/formatExpiredAt.ts | 4 ++-- .../ReportTable/ReportStatusCell/formatStatus.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 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 c8afc7c9b6dd..c0a3a83cc44c 100644 --- a/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportExpiredCell/formatExpiredAt.ts +++ b/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportExpiredCell/formatExpiredAt.ts @@ -6,12 +6,12 @@ import { FeatureTypeSchema } from 'openapi'; export const formatExpiredAt = ( feature: IFeatureToggleListItem, - featureTypes: FeatureTypeSchema[] + featureTypes: FeatureTypeSchema[], ): string | undefined => { const { type, createdAt } = feature; const featureType = featureTypes.find( - featureType => featureType.name === type + (featureType) => featureType.name === type, ); if ( 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 65e595832e62..2d7a2999a358 100644 --- a/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportStatusCell/formatStatus.ts +++ b/frontend/src/component/project/Project/ProjectHealth/ReportTable/ReportStatusCell/formatStatus.ts @@ -8,12 +8,12 @@ export type ReportingStatus = 'potentially-stale' | 'healthy'; export const formatStatus = ( feature: IFeatureToggleListItem, - featureTypes: FeatureTypeSchema[] + featureTypes: FeatureTypeSchema[], ): ReportingStatus => { const { type, createdAt } = feature; const featureType = featureTypes.find( - featureType => featureType.name === type + (featureType) => featureType.name === type, ); const date = parseISO(createdAt); const now = new Date(); From d4009e0bbc8ec60f2765d17dae8491cec1519eac Mon Sep 17 00:00:00 2001 From: andreas-unleash Date: Wed, 4 Oct 2023 10:50:09 +0300 Subject: [PATCH 13/13] fix: merge Signed-off-by: andreas-unleash --- .../ChangeRequestPermissions.test.tsx | 198 +++---- .../common/FormTemplate/FormTemplate.tsx | 148 ++--- .../Project/CreateProject/CreateProject.tsx | 58 +- .../CollaborationModeTooltip.tsx | 16 +- .../FeatureFlagNamingTooltip.tsx | 10 +- .../ProjectEnterpriseSettingsForm.tsx | 543 +++++++++--------- .../Project/ProjectForm/ProjectForm.tsx | 110 ++-- .../EditProject/DeleteProjectForm.tsx | 22 +- .../Settings/EditProject/EditProject.tsx | 32 +- .../EditProject/UpdateEnterpriseSettings.tsx | 92 +-- .../Settings/EditProject/UpdateProject.tsx | 74 +-- 11 files changed, 652 insertions(+), 651 deletions(-) diff --git a/frontend/src/component/changeRequest/ChangeRequestPermissions.test.tsx b/frontend/src/component/changeRequest/ChangeRequestPermissions.test.tsx index 7a35172ca116..a777c092dcb6 100644 --- a/frontend/src/component/changeRequest/ChangeRequestPermissions.test.tsx +++ b/frontend/src/component/changeRequest/ChangeRequestPermissions.test.tsx @@ -1,57 +1,57 @@ -import { render, screen, waitFor } from "@testing-library/react"; -import { MemoryRouter, Routes, Route } from "react-router-dom"; -import { FeatureView } from "../feature/FeatureView/FeatureView"; -import { ThemeProvider } from "themes/ThemeProvider"; -import { AccessProvider } from "../providers/AccessProvider/AccessProvider"; -import { AnnouncerProvider } from "../common/Announcer/AnnouncerProvider/AnnouncerProvider"; -import { testServerRoute, testServerSetup } from "../../utils/testServer"; -import { UIProviderContainer } from "../providers/UIProvider/UIProviderContainer"; -import { FC } from "react"; -import { IPermission } from "../../interfaces/user"; -import { SWRConfig } from "swr"; -import { ProjectMode } from "../project/Project/hooks/useProjectEnterpriseSettingsForm"; +import { render, screen, waitFor } from '@testing-library/react'; +import { MemoryRouter, Routes, Route } from 'react-router-dom'; +import { FeatureView } from '../feature/FeatureView/FeatureView'; +import { ThemeProvider } from 'themes/ThemeProvider'; +import { AccessProvider } from '../providers/AccessProvider/AccessProvider'; +import { AnnouncerProvider } from '../common/Announcer/AnnouncerProvider/AnnouncerProvider'; +import { testServerRoute, testServerSetup } from '../../utils/testServer'; +import { UIProviderContainer } from '../providers/UIProvider/UIProviderContainer'; +import { FC } from 'react'; +import { IPermission } from '../../interfaces/user'; +import { SWRConfig } from 'swr'; +import { ProjectMode } from '../project/Project/hooks/useProjectEnterpriseSettingsForm'; const server = testServerSetup(); const projectWithCollaborationMode = (mode: ProjectMode) => - testServerRoute(server, "/api/admin/projects/default", { mode }); + testServerRoute(server, '/api/admin/projects/default', { mode }); const changeRequestsEnabledIn = ( - env: "development" | "production" | "custom" + env: 'development' | 'production' | 'custom', ) => testServerRoute( server, - "/api/admin/projects/default/change-requests/config", + '/api/admin/projects/default/change-requests/config', [ { - environment: "development", - type: "development", + environment: 'development', + type: 'development', requiredApprovals: null, - changeRequestEnabled: env === "development", + changeRequestEnabled: env === 'development', }, { - environment: "production", - type: "production", + environment: 'production', + type: 'production', requiredApprovals: 1, - changeRequestEnabled: env === "production", + changeRequestEnabled: env === 'production', }, { - environment: "custom", - type: "production", + environment: 'custom', + type: 'production', requiredApprovals: null, - changeRequestEnabled: env === "custom", + changeRequestEnabled: env === 'custom', }, - ] + ], ); const uiConfigForEnterprise = () => - testServerRoute(server, "/api/admin/ui-config", { - environment: "Open Source", + testServerRoute(server, '/api/admin/ui-config', { + environment: 'Open Source', flags: { changeRequests: true, }, versionInfo: { - current: { oss: "4.18.0-beta.5", enterprise: "4.17.0-beta.1" }, + current: { oss: '4.18.0-beta.5', enterprise: '4.17.0-beta.1' }, }, disablePasswordAuth: false, }); @@ -59,12 +59,12 @@ const uiConfigForEnterprise = () => const setupOtherRoutes = (feature: string) => { testServerRoute( server, - "api/admin/projects/default/change-requests/pending", - [] + 'api/admin/projects/default/change-requests/pending', + [], ); testServerRoute(server, `api/admin/client-metrics/features/${feature}`, { version: 1, - maturity: "stable", + maturity: 'stable', featureName: feature, lastHourUsage: [], seenApplications: [], @@ -86,25 +86,25 @@ const setupOtherRoutes = (feature: string) => { version: 1, strategies: [ { - displayName: "Standard", - name: "default", + displayName: 'Standard', + name: 'default', editable: false, description: - "The standard strategy is strictly on / off for your entire userbase.", + 'The standard strategy is strictly on / off for your entire userbase.', parameters: [], deprecated: false, }, { - displayName: "UserIDs", - name: "userWithId", + displayName: 'UserIDs', + name: 'userWithId', editable: false, description: - "Enable the feature for a specific set of userIds.", + 'Enable the feature for a specific set of userIds.', parameters: [ { - name: "userIds", - type: "list", - description: "", + name: 'userIds', + type: 'list', + description: '', required: false, }, ], @@ -115,17 +115,17 @@ const setupOtherRoutes = (feature: string) => { }; const userHasPermissions = (permissions: Array) => { - testServerRoute(server, "api/admin/user", { + testServerRoute(server, 'api/admin/user', { user: { isAPI: false, id: 2, - name: "Test", - email: "test@getunleash.ai", + name: 'Test', + email: 'test@getunleash.ai', imageUrl: - "https://gravatar.com/avatar/e55646b526ff342ff8b43721f0cbdd8e?size=42&default=retro", - seenAt: "2022-11-29T08:21:52.581Z", + 'https://gravatar.com/avatar/e55646b526ff342ff8b43721f0cbdd8e?size=42&default=retro', + seenAt: '2022-11-29T08:21:52.581Z', loginAttempts: 0, - createdAt: "2022-11-21T10:10:33.074Z", + createdAt: '2022-11-21T10:10:33.074Z', }, permissions, feedback: [], @@ -136,21 +136,21 @@ const userIsMemberOfProjects = (projects: string[]) => { userHasPermissions( projects.map((project) => ({ project, - environment: "irrelevant", - permission: "irrelevant", - })) + environment: 'irrelevant', + permission: 'irrelevant', + })), ); }; const featureEnvironments = ( feature: string, - environments: Array<{ name: string; strategies: Array }> + environments: Array<{ name: string; strategies: Array }>, ) => { testServerRoute(server, `/api/admin/projects/default/features/${feature}`, { environments: environments.map((env) => ({ name: env.name, enabled: false, - type: "production", + type: 'production', sortOrder: 1, strategies: env.strategies.map((strategy) => ({ name: strategy, @@ -162,13 +162,13 @@ const featureEnvironments = ( })), name: feature, impressionData: false, - description: "", - project: "default", + description: '', + project: 'default', stale: false, variants: [], - createdAt: "2022-11-14T08:16:33.338Z", + createdAt: '2022-11-14T08:16:33.338Z', lastSeenAt: null, - type: "release", + type: 'release', archived: false, children: [], dependencies: [], @@ -199,7 +199,7 @@ const UnleashUiSetup: FC<{ path: string; pathTemplate: string }> = ({ const strategiesAreDisplayed = async ( firstStrategy: string, - secondStrategy: string + secondStrategy: string, ) => { await screen.findByText(firstStrategy); await screen.findByText(secondStrategy); @@ -213,10 +213,10 @@ const getDeleteButtons = async () => { removeMenus.map(async (menu) => { menu.click(); const removeButton = screen.getAllByTestId( - "STRATEGY_FORM_REMOVE_ID" + 'STRATEGY_FORM_REMOVE_ID', ); deleteButtons.push(...removeButton); - }) + }), ); return deleteButtons; }; @@ -229,12 +229,12 @@ const deleteButtonsActiveInChangeRequestEnv = async () => { await waitFor(() => { // production const productionStrategyDeleteButton = deleteButtons[1]; - expect(productionStrategyDeleteButton).not.toHaveClass("Mui-disabled"); + expect(productionStrategyDeleteButton).not.toHaveClass('Mui-disabled'); }); await waitFor(() => { // custom env const customEnvStrategyDeleteButton = deleteButtons[2]; - expect(customEnvStrategyDeleteButton).toHaveClass("Mui-disabled"); + expect(customEnvStrategyDeleteButton).toHaveClass('Mui-disabled'); }); }; @@ -246,17 +246,17 @@ const deleteButtonsInactiveInChangeRequestEnv = async () => { await waitFor(() => { // production const productionStrategyDeleteButton = deleteButtons[1]; - expect(productionStrategyDeleteButton).toHaveClass("Mui-disabled"); + expect(productionStrategyDeleteButton).toHaveClass('Mui-disabled'); }); await waitFor(() => { // custom env const customEnvStrategyDeleteButton = deleteButtons[2]; - expect(customEnvStrategyDeleteButton).toHaveClass("Mui-disabled"); + expect(customEnvStrategyDeleteButton).toHaveClass('Mui-disabled'); }); }; const copyButtonsActiveInOtherEnv = async () => { - const copyButtons = screen.getAllByTestId("STRATEGY_FORM_COPY_ID"); + const copyButtons = screen.getAllByTestId('STRATEGY_FORM_COPY_ID'); expect(copyButtons.length).toBe(2); // production @@ -274,92 +274,92 @@ const openEnvironments = async (envNames: string[]) => { } }; -test("open mode + non-project member can perform basic change request actions", async () => { - const project = "default"; - const featureName = "test"; +test('open mode + non-project member can perform basic change request actions', async () => { + const project = 'default'; + const featureName = 'test'; featureEnvironments(featureName, [ - { name: "development", strategies: [] }, - { name: "production", strategies: ["userWithId"] }, - { name: "custom", strategies: ["default"] }, + { name: 'development', strategies: [] }, + { name: 'production', strategies: ['userWithId'] }, + { name: 'custom', strategies: ['default'] }, ]); userIsMemberOfProjects([]); - changeRequestsEnabledIn("production"); - projectWithCollaborationMode("open"); + changeRequestsEnabledIn('production'); + projectWithCollaborationMode('open'); uiConfigForEnterprise(); setupOtherRoutes(featureName); render( - + , ); - await openEnvironments(["development", "production", "custom"]); + await openEnvironments(['development', 'production', 'custom']); - await strategiesAreDisplayed("UserIDs", "Standard"); + await strategiesAreDisplayed('UserIDs', 'Standard'); await deleteButtonsActiveInChangeRequestEnv(); await copyButtonsActiveInOtherEnv(); }); -test("protected mode + project member can perform basic change request actions", async () => { - const project = "default"; - const featureName = "test"; +test('protected mode + project member can perform basic change request actions', async () => { + const project = 'default'; + const featureName = 'test'; featureEnvironments(featureName, [ - { name: "development", strategies: [] }, - { name: "production", strategies: ["userWithId"] }, - { name: "custom", strategies: ["default"] }, + { name: 'development', strategies: [] }, + { name: 'production', strategies: ['userWithId'] }, + { name: 'custom', strategies: ['default'] }, ]); userIsMemberOfProjects([project]); - changeRequestsEnabledIn("production"); - projectWithCollaborationMode("protected"); + changeRequestsEnabledIn('production'); + projectWithCollaborationMode('protected'); uiConfigForEnterprise(); setupOtherRoutes(featureName); render( - + , ); - await openEnvironments(["development", "production", "custom"]); + await openEnvironments(['development', 'production', 'custom']); - await strategiesAreDisplayed("UserIDs", "Standard"); + await strategiesAreDisplayed('UserIDs', 'Standard'); await deleteButtonsActiveInChangeRequestEnv(); await copyButtonsActiveInOtherEnv(); }); -test("protected mode + non-project member cannot perform basic change request actions", async () => { - const project = "default"; - const featureName = "test"; +test('protected mode + non-project member cannot perform basic change request actions', async () => { + const project = 'default'; + const featureName = 'test'; featureEnvironments(featureName, [ - { name: "development", strategies: [] }, - { name: "production", strategies: ["userWithId"] }, - { name: "custom", strategies: ["default"] }, + { name: 'development', strategies: [] }, + { name: 'production', strategies: ['userWithId'] }, + { name: 'custom', strategies: ['default'] }, ]); userIsMemberOfProjects([]); - changeRequestsEnabledIn("production"); - projectWithCollaborationMode("protected"); + changeRequestsEnabledIn('production'); + projectWithCollaborationMode('protected'); uiConfigForEnterprise(); setupOtherRoutes(featureName); render( - + , ); - await openEnvironments(["development", "production", "custom"]); + await openEnvironments(['development', 'production', 'custom']); - await strategiesAreDisplayed("UserIDs", "Standard"); + await strategiesAreDisplayed('UserIDs', 'Standard'); await deleteButtonsInactiveInChangeRequestEnv(); await copyButtonsActiveInOtherEnv(); }); diff --git a/frontend/src/component/common/FormTemplate/FormTemplate.tsx b/frontend/src/component/common/FormTemplate/FormTemplate.tsx index 783e91ed8c30..1c0b6220c77c 100644 --- a/frontend/src/component/common/FormTemplate/FormTemplate.tsx +++ b/frontend/src/component/common/FormTemplate/FormTemplate.tsx @@ -1,5 +1,5 @@ -import MenuBookIcon from "@mui/icons-material/MenuBook"; -import Codebox from "../Codebox/Codebox"; +import MenuBookIcon from '@mui/icons-material/MenuBook'; +import Codebox from '../Codebox/Codebox'; import { Collapse, IconButton, @@ -7,16 +7,16 @@ import { Tooltip, Divider, styled, -} from "@mui/material"; -import { FileCopy, Info } from "@mui/icons-material"; -import { ConditionallyRender } from "component/common/ConditionallyRender/ConditionallyRender"; -import Loader from "../Loader/Loader"; -import copy from "copy-to-clipboard"; -import useToast from "hooks/useToast"; -import React, { ReactNode, useState } from "react"; -import { ReactComponent as MobileGuidanceBG } from "assets/img/mobileGuidanceBg.svg"; -import { formTemplateSidebarWidth } from "./FormTemplate.styles"; -import { relative } from "themes/themeStyles"; +} from '@mui/material'; +import { FileCopy, Info } from '@mui/icons-material'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import Loader from '../Loader/Loader'; +import copy from 'copy-to-clipboard'; +import useToast from 'hooks/useToast'; +import React, { ReactNode, useState } from 'react'; +import { ReactComponent as MobileGuidanceBG } from 'assets/img/mobileGuidanceBg.svg'; +import { formTemplateSidebarWidth } from './FormTemplate.styles'; +import { relative } from 'themes/themeStyles'; interface ICreateProps { title?: ReactNode; @@ -34,66 +34,66 @@ interface ICreateProps { compact?: boolean; } -const StyledContainer = styled("section", { +const StyledContainer = styled('section', { shouldForwardProp: (prop) => - !["modal", "compact"].includes(prop.toString()), + !['modal', 'compact'].includes(prop.toString()), })<{ modal?: boolean; compact?: boolean }>(({ theme, modal, compact }) => ({ - minHeight: modal ? "100vh" : compact ? 0 : "80vh", + minHeight: modal ? '100vh' : compact ? 0 : '80vh', borderRadius: modal ? 0 : theme.spacing(2), - width: "100%", - display: "flex", - margin: "0 auto", - overflow: modal ? "unset" : "hidden", + width: '100%', + display: 'flex', + margin: '0 auto', + overflow: modal ? 'unset' : 'hidden', [theme.breakpoints.down(1100)]: { - flexDirection: "column", + flexDirection: 'column', minHeight: 0, }, })); -const StyledRelativeDiv = styled("div")(({ theme }) => relative); +const StyledRelativeDiv = styled('div')(({ theme }) => relative); -const StyledMain = styled("div")(({ theme }) => ({ - display: "flex", - flexDirection: "column", +const StyledMain = styled('div')(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', flexGrow: 1, flexShrink: 1, - width: "100%", + width: '100%', [theme.breakpoints.down(1100)]: { - width: "100%", + width: '100%', }, })); -const StyledFormContent = styled("div", { +const StyledFormContent = styled('div', { shouldForwardProp: (prop) => { - return !["disablePadding", "compactPadding"].includes(prop.toString()); + return !['disablePadding', 'compactPadding'].includes(prop.toString()); }, })<{ disablePadding?: boolean; compactPadding?: boolean }>( ({ theme, disablePadding, compactPadding }) => ({ backgroundColor: theme.palette.background.paper, - display: "flex", - flexDirection: "column", + display: 'flex', + flexDirection: 'column', flexGrow: 1, padding: disablePadding ? 0 : compactPadding ? theme.spacing(4) : theme.spacing(6), - [theme.breakpoints.down("lg")]: { + [theme.breakpoints.down('lg')]: { padding: disablePadding ? 0 : theme.spacing(4), }, [theme.breakpoints.down(1100)]: { - width: "100%", + width: '100%', }, [theme.breakpoints.down(500)]: { padding: disablePadding ? 0 : theme.spacing(4, 2), }, - }) + }), ); -const StyledFooter = styled("div")(({ theme }) => ({ +const StyledFooter = styled('div')(({ theme }) => ({ backgroundColor: theme.palette.background.paper, padding: theme.spacing(4, 6), - [theme.breakpoints.down("lg")]: { + [theme.breakpoints.down('lg')]: { padding: theme.spacing(4), }, [theme.breakpoints.down(500)]: { @@ -101,9 +101,9 @@ const StyledFooter = styled("div")(({ theme }) => ({ }, })); -const StyledTitle = styled("h1")(({ theme }) => ({ +const StyledTitle = styled('h1')(({ theme }) => ({ marginBottom: theme.fontSizes.mainHeader, - fontWeight: "normal", + fontWeight: 'normal', })); const StyledSidebarDivider = styled(Divider)(({ theme }) => ({ @@ -111,12 +111,12 @@ const StyledSidebarDivider = styled(Divider)(({ theme }) => ({ marginBottom: theme.spacing(0.5), })); -const StyledSubtitle = styled("h2")(({ theme }) => ({ +const StyledSubtitle = styled('h2')(({ theme }) => ({ color: theme.palette.common.white, marginBottom: theme.spacing(2), - display: "flex", - justifyContent: "space-between", - alignItems: "center", + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', fontWeight: theme.fontWeight.bold, fontSize: theme.fontSizes.bodySize, })); @@ -125,20 +125,20 @@ const StyledIcon = styled(FileCopy)(({ theme }) => ({ fill: theme.palette.primary.contrastText, })); -const StyledMobileGuidanceContainer = styled("div")(() => ({ +const StyledMobileGuidanceContainer = styled('div')(() => ({ zIndex: 1, - position: "absolute", + position: 'absolute', right: -3, top: -3, })); const StyledMobileGuidanceBackground = styled(MobileGuidanceBG)(() => ({ - width: "75px", - height: "75px", + width: '75px', + height: '75px', })); const StyledMobileGuidanceButton = styled(IconButton)(() => ({ - position: "absolute", + position: 'absolute', zIndex: 400, right: 0, })); @@ -147,31 +147,31 @@ const StyledInfoIcon = styled(Info)(({ theme }) => ({ fill: theme.palette.primary.contrastText, })); -const StyledSidebar = styled("aside")(({ theme }) => ({ +const StyledSidebar = styled('aside')(({ theme }) => ({ backgroundColor: theme.palette.background.sidebar, padding: theme.spacing(4), flexGrow: 0, flexShrink: 0, width: formTemplateSidebarWidth, [theme.breakpoints.down(1100)]: { - width: "100%", - color: "red", + width: '100%', + color: 'red', }, [theme.breakpoints.down(500)]: { padding: theme.spacing(4, 2), }, })); -const StyledDescription = styled("p")(({ theme }) => ({ +const StyledDescription = styled('p')(({ theme }) => ({ color: theme.palette.common.white, zIndex: 1, - position: "relative", + position: 'relative', })); -const StyledLinkContainer = styled("div")(({ theme }) => ({ +const StyledLinkContainer = styled('div')(({ theme }) => ({ margin: theme.spacing(3, 0), - display: "flex", - alignItems: "center", + display: 'flex', + alignItems: 'center', })); const StyledLinkIcon = styled(MenuBookIcon)(({ theme }) => ({ @@ -179,11 +179,11 @@ const StyledLinkIcon = styled(MenuBookIcon)(({ theme }) => ({ color: theme.palette.primary.contrastText, })); -const StyledDocumentationLink = styled("a")(({ theme }) => ({ +const StyledDocumentationLink = styled('a')(({ theme }) => ({ color: theme.palette.primary.contrastText, - display: "block", - "&:hover": { - textDecoration: "none", + display: 'block', + '&:hover': { + textDecoration: 'none', }, })); @@ -209,18 +209,18 @@ const FormTemplate: React.FC = ({ if (formatApiCode !== undefined) { if (copy(formatApiCode())) { setToastData({ - title: "Successfully copied the command", - text: "The command should now be automatically copied to your clipboard", + title: 'Successfully copied the command', + text: 'The command should now be automatically copied to your clipboard', autoHideDuration: 6000, - type: "success", + type: 'success', show: true, }); } else { setToastData({ - title: "Could not copy the command", - text: "Sorry, but we could not copy the command.", + title: 'Could not copy the command', + text: 'Sorry, but we could not copy the command.', autoHideDuration: 6000, - type: "error", + type: 'error', show: true, }); } @@ -236,14 +236,14 @@ const FormTemplate: React.FC = ({ show={} /> - API Command{" "} - - + API Command{' '} + + - {" "} + {' '} ); } @@ -304,7 +304,7 @@ const FormTemplate: React.FC = ({ > {renderApiInfo( formatApiCode === undefined, - !(showDescription || showLink) + !(showDescription || showLink), )} } @@ -331,10 +331,10 @@ const MobileGuidance = ({ - + setOpen((prev) => !prev)} - size="large" + size='large' > @@ -362,7 +362,7 @@ const Guidance: React.FC = ({ description, children, documentationLink, - documentationLinkLabel = "Learn more", + documentationLinkLabel = 'Learn more', showDescription = true, showLink = true, }) => { @@ -380,8 +380,8 @@ const Guidance: React.FC = ({ {documentationLinkLabel} diff --git a/frontend/src/component/project/Project/CreateProject/CreateProject.tsx b/frontend/src/component/project/Project/CreateProject/CreateProject.tsx index acc0185defc2..91afdcd7c931 100644 --- a/frontend/src/component/project/Project/CreateProject/CreateProject.tsx +++ b/frontend/src/component/project/Project/CreateProject/CreateProject.tsx @@ -1,21 +1,21 @@ -import { useNavigate } from "react-router-dom"; -import ProjectForm from "../ProjectForm/ProjectForm"; +import { useNavigate } from 'react-router-dom'; +import ProjectForm from '../ProjectForm/ProjectForm'; import useProjectForm, { DEFAULT_PROJECT_STICKINESS, -} from "../hooks/useProjectForm"; -import { CreateButton } from "component/common/CreateButton/CreateButton"; -import FormTemplate from "component/common/FormTemplate/FormTemplate"; -import { CREATE_PROJECT } from "component/providers/AccessProvider/permissions"; -import useProjectApi from "hooks/api/actions/useProjectApi/useProjectApi"; -import { useAuthUser } from "hooks/api/getters/useAuth/useAuthUser"; -import useUiConfig from "hooks/api/getters/useUiConfig/useUiConfig"; -import useToast from "hooks/useToast"; -import { formatUnknownError } from "utils/formatUnknownError"; -import { GO_BACK } from "constants/navigate"; -import { usePlausibleTracker } from "hooks/usePlausibleTracker"; -import { Button, styled } from "@mui/material"; +} from '../hooks/useProjectForm'; +import { CreateButton } from 'component/common/CreateButton/CreateButton'; +import FormTemplate from 'component/common/FormTemplate/FormTemplate'; +import { CREATE_PROJECT } from 'component/providers/AccessProvider/permissions'; +import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi'; +import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser'; +import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; +import useToast from 'hooks/useToast'; +import { formatUnknownError } from 'utils/formatUnknownError'; +import { GO_BACK } from 'constants/navigate'; +import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; +import { Button, styled } from '@mui/material'; -const CREATE_PROJECT_BTN = "CREATE_PROJECT_BTN"; +const CREATE_PROJECT_BTN = 'CREATE_PROJECT_BTN'; const StyledButton = styled(Button)(({ theme }) => ({ marginLeft: theme.spacing(3), @@ -60,17 +60,17 @@ const CreateProject = () => { refetchUser(); navigate(`/projects/${projectId}`); setToastData({ - title: "Project created", - text: "Now you can add toggles to this project", + title: 'Project created', + text: 'Now you can add toggles to this project', confetti: true, - type: "success", + type: 'success', }); if (projectStickiness !== DEFAULT_PROJECT_STICKINESS) { - trackEvent("project_stickiness_set"); + trackEvent('project_stickiness_set'); } - trackEvent("project-mode", { - props: { mode: projectMode, action: "added" }, + trackEvent('project-mode', { + props: { mode: projectMode, action: 'added' }, }); } catch (error: unknown) { setToastApiError(formatUnknownError(error)); @@ -79,9 +79,7 @@ const CreateProject = () => { }; const formatApiCode = () => { - return `curl --location --request POST '${ - uiConfig.unleashUrl - }/api/admin/projects' \\ + return `curl --location --request POST '${uiConfig.unleashUrl}/api/admin/projects' \\ --header 'Authorization: INSERT_API_KEY' \\ --header 'Content-Type: application/json' \\ --data-raw '${JSON.stringify(getCreateProjectPayload(), undefined, 2)}'`; @@ -94,10 +92,10 @@ const CreateProject = () => { return ( { setProjectName={setProjectName} projectDesc={projectDesc} setProjectDesc={setProjectDesc} - mode="Create" + mode='Create' clearErrors={clearErrors} validateProjectId={validateProjectId} > diff --git a/frontend/src/component/project/Project/ProjectEnterpriseSettingsForm/CollaborationModeTooltip.tsx b/frontend/src/component/project/Project/ProjectEnterpriseSettingsForm/CollaborationModeTooltip.tsx index 5557a6110727..0afaab9e1206 100644 --- a/frontend/src/component/project/Project/ProjectEnterpriseSettingsForm/CollaborationModeTooltip.tsx +++ b/frontend/src/component/project/Project/ProjectEnterpriseSettingsForm/CollaborationModeTooltip.tsx @@ -1,20 +1,20 @@ -import { Box, styled, Typography } from "@mui/material"; -import { FC } from "react"; -import { HelpIcon } from "component/common/HelpIcon/HelpIcon"; -import { useUiFlag } from "hooks/useUiFlag"; -import { ConditionallyRender } from "component/common/ConditionallyRender/ConditionallyRender"; +import { Box, styled, Typography } from '@mui/material'; +import { FC } from 'react'; +import { HelpIcon } from 'component/common/HelpIcon/HelpIcon'; +import { useUiFlag } from 'hooks/useUiFlag'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; const StyledTitle = styled(Typography)(({ theme }) => ({ fontWeight: theme.fontWeight.bold, - display: "inline", + display: 'inline', })); const StyledDescription = styled(Typography)(({ theme }) => ({ - display: "inline", + display: 'inline', color: theme.palette.text.secondary, })); export const CollaborationModeTooltip: FC = () => { - const privateProjects = useUiFlag("privateProjects"); + const privateProjects = useUiFlag('privateProjects'); return ( { return ( @@ -9,8 +9,8 @@ export const FeatureFlagNamingTooltip: FC = () => { tooltip={

- For example, the pattern{" "} - {"[a-z0-9]{2}\\.[a-z]{4,12}"} matches + For example, the pattern{' '} + {'[a-z0-9]{2}\\.[a-z]{4,12}'} matches 'a1.project', but not 'a1.project.feature-1'.

diff --git a/frontend/src/component/project/Project/ProjectEnterpriseSettingsForm/ProjectEnterpriseSettingsForm.tsx b/frontend/src/component/project/Project/ProjectEnterpriseSettingsForm/ProjectEnterpriseSettingsForm.tsx index bd2216129403..3fedbd3f6d0f 100644 --- a/frontend/src/component/project/Project/ProjectEnterpriseSettingsForm/ProjectEnterpriseSettingsForm.tsx +++ b/frontend/src/component/project/Project/ProjectEnterpriseSettingsForm/ProjectEnterpriseSettingsForm.tsx @@ -1,13 +1,13 @@ -import React, { useEffect } from "react"; -import { ConditionallyRender } from "component/common/ConditionallyRender/ConditionallyRender"; -import Select from "component/common/select"; -import { ProjectMode } from "../hooks/useProjectEnterpriseSettingsForm"; -import { Box, InputAdornment, styled, TextField } from "@mui/material"; -import { CollaborationModeTooltip } from "./CollaborationModeTooltip"; -import Input from "component/common/Input/Input"; -import { FeatureFlagNamingTooltip } from "./FeatureFlagNamingTooltip"; -import { usePlausibleTracker } from "hooks/usePlausibleTracker"; -import { useUiFlag } from "hooks/useUiFlag"; +import React, { useEffect } from 'react'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import Select from 'component/common/select'; +import { ProjectMode } from '../hooks/useProjectEnterpriseSettingsForm'; +import { Box, InputAdornment, styled, TextField } from '@mui/material'; +import { CollaborationModeTooltip } from './CollaborationModeTooltip'; +import Input from 'component/common/Input/Input'; +import { FeatureFlagNamingTooltip } from './FeatureFlagNamingTooltip'; +import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; +import { useUiFlag } from 'hooks/useUiFlag'; interface IProjectEnterpriseSettingsForm { projectId: string; @@ -24,12 +24,12 @@ interface IProjectEnterpriseSettingsForm { clearErrors: () => void; } -const StyledForm = styled("form")(({ theme }) => ({ - height: "100%", +const StyledForm = styled('form')(({ theme }) => ({ + height: '100%', paddingBottom: theme.spacing(4), })); -const StyledSubtitle = styled("div")(({ theme }) => ({ +const StyledSubtitle = styled('div')(({ theme }) => ({ color: theme.palette.text.secondary, fontSize: theme.fontSizes.smallerBody, lineHeight: 1.25, @@ -37,42 +37,42 @@ const StyledSubtitle = styled("div")(({ theme }) => ({ })); const StyledInput = styled(Input)(({ theme }) => ({ - width: "100%", + width: '100%', marginBottom: theme.spacing(2), paddingRight: theme.spacing(1), })); const StyledTextField = styled(TextField)(({ theme }) => ({ - width: "100%", + width: '100%', marginBottom: theme.spacing(2), })); -const StyledFieldset = styled("fieldset")(() => ({ +const StyledFieldset = styled('fieldset')(() => ({ padding: 0, - border: "none", + border: 'none', })); const StyledSelect = styled(Select)(({ theme }) => ({ marginBottom: theme.spacing(2), - minWidth: "200px", + minWidth: '200px', })); -const StyledButtonContainer = styled("div")(() => ({ - marginTop: "auto", - display: "flex", - justifyContent: "flex-end", +const StyledButtonContainer = styled('div')(() => ({ + marginTop: 'auto', + display: 'flex', + justifyContent: 'flex-end', })); -const StyledFlagNamingContainer = styled("div")(({ theme }) => ({ - display: "flex", - flexDirection: "column", - alignItems: "flex-start", +const StyledFlagNamingContainer = styled('div')(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', gap: theme.spacing(1), - "& > *": { width: "100%" }, + '& > *': { width: '100%' }, })); -const StyledPatternNamingExplanation = styled("div")(({ theme }) => ({ - "p + p": { marginTop: theme.spacing(1) }, +const StyledPatternNamingExplanation = styled('div')(({ theme }) => ({ + 'p + p': { marginTop: theme.spacing(1) }, })); export const validateFeatureNamingExample = ({ @@ -83,285 +83,288 @@ export const validateFeatureNamingExample = ({ pattern: string; example: string; featureNamingPatternError: string | undefined; -}): { state: "valid" } | { state: "invalid"; reason: string } => { +}): { state: 'valid' } | { state: 'invalid'; reason: string } => { if (featureNamingPatternError || !example || !pattern) { - return { state: "valid" }; + return { state: 'valid' }; } else if (example && pattern) { const regex = new RegExp(`^${pattern}$`); const matches = regex.test(example); if (!matches) { - return { state: "invalid", reason: "Example does not match regex" }; + return { state: 'invalid', reason: 'Example does not match regex' }; } else { - return { state: "valid" }; + return { state: 'valid' }; } } - return { state: "valid" }; + return { state: 'valid' }; }; const useFeatureNamePatternTracking = () => { - const [previousPattern, setPreviousPattern] = React.useState(""); + const [previousPattern, setPreviousPattern] = React.useState(''); const { trackEvent } = usePlausibleTracker(); - const eventName = "feature-naming-pattern" as const; + const eventName = 'feature-naming-pattern' as const; - const trackPattern = (pattern: string = "") => { + const trackPattern = (pattern: string = '') => { if (pattern === previousPattern) { // do nothing; they've probably updated something else in the // project. - } else if (pattern === "" && previousPattern !== "") { - trackEvent(eventName, { props: { action: "removed" } }); - } else if (pattern !== "" && previousPattern === "") { - trackEvent(eventName, { props: { action: "added" } }); - } else if (pattern !== "" && previousPattern !== "") { - trackEvent(eventName, { props: { action: "edited" } }); + } else if (pattern === '' && previousPattern !== '') { + trackEvent(eventName, { props: { action: 'removed' } }); + } else if (pattern !== '' && previousPattern === '') { + trackEvent(eventName, { props: { action: 'added' } }); + } else if (pattern !== '' && previousPattern !== '') { + trackEvent(eventName, { props: { action: 'edited' } }); } }; return { trackPattern, setPreviousPattern }; }; -const ProjectEnterpriseSettingsForm: React.FC< - IProjectEnterpriseSettingsForm -> = ({ - children, - handleSubmit, - projectId, - projectMode, - featureNamingExample, - featureNamingPattern, - featureNamingDescription, - setFeatureNamingExample, - setFeatureNamingPattern, - setFeatureNamingDescription, - setProjectMode, - errors, - clearErrors, -}) => { - const privateProjects = useUiFlag("privateProjects"); - const shouldShowFlagNaming = useUiFlag("featureNamingPattern"); +const ProjectEnterpriseSettingsForm: React.FC = + ({ + children, + handleSubmit, + projectId, + projectMode, + featureNamingExample, + featureNamingPattern, + featureNamingDescription, + setFeatureNamingExample, + setFeatureNamingPattern, + setFeatureNamingDescription, + setProjectMode, + errors, + clearErrors, + }) => { + const privateProjects = useUiFlag('privateProjects'); + const shouldShowFlagNaming = useUiFlag('featureNamingPattern'); - const { setPreviousPattern, trackPattern } = - useFeatureNamePatternTracking(); + const { setPreviousPattern, trackPattern } = + useFeatureNamePatternTracking(); - const projectModeOptions = privateProjects - ? [ - { key: "open", label: "open" }, - { key: "protected", label: "protected" }, - { key: "private", label: "private" }, - ] - : [ - { key: "open", label: "open" }, - { key: "protected", label: "protected" }, - ]; + const projectModeOptions = privateProjects + ? [ + { key: 'open', label: 'open' }, + { key: 'protected', label: 'protected' }, + { key: 'private', label: 'private' }, + ] + : [ + { key: 'open', label: 'open' }, + { key: 'protected', label: 'protected' }, + ]; - useEffect(() => { - setPreviousPattern(featureNamingPattern || ""); - }, [projectId]); + useEffect(() => { + setPreviousPattern(featureNamingPattern || ''); + }, [projectId]); - const updateNamingExampleError = ({ - example, - pattern, - }: { - example: string; - pattern: string; - }) => { - const validationResult = validateFeatureNamingExample({ - pattern, + const updateNamingExampleError = ({ example, - featureNamingPatternError: errors.featureNamingPattern, - }); + pattern, + }: { + example: string; + pattern: string; + }) => { + const validationResult = validateFeatureNamingExample({ + pattern, + example, + featureNamingPatternError: errors.featureNamingPattern, + }); - switch (validationResult.state) { - case "invalid": - errors.namingExample = validationResult.reason; - break; - case "valid": - delete errors.namingExample; - break; - } - }; + switch (validationResult.state) { + case 'invalid': + errors.namingExample = validationResult.reason; + break; + case 'valid': + delete errors.namingExample; + break; + } + }; - const onSetFeatureNamingPattern = (regex: string) => { - const disallowedStrings = [ - " ", - "\\t", - "\\s", - "\\n", - "\\r", - "\\f", - "\\v", - ]; - if ( - disallowedStrings.some((blockedString) => - regex.includes(blockedString) - ) - ) { - errors.featureNamingPattern = - "Whitespace is not allowed in the expression"; - } else { - try { - new RegExp(regex); - delete errors.featureNamingPattern; - } catch (e) { - errors.featureNamingPattern = "Invalid regular expression"; + const onSetFeatureNamingPattern = (regex: string) => { + const disallowedStrings = [ + ' ', + '\\t', + '\\s', + '\\n', + '\\r', + '\\f', + '\\v', + ]; + if ( + disallowedStrings.some((blockedString) => + regex.includes(blockedString), + ) + ) { + errors.featureNamingPattern = + 'Whitespace is not allowed in the expression'; + } else { + try { + new RegExp(regex); + delete errors.featureNamingPattern; + } catch (e) { + errors.featureNamingPattern = 'Invalid regular expression'; + } } - } - setFeatureNamingPattern?.(regex); - updateNamingExampleError({ - pattern: regex, - example: featureNamingExample || "", - }); - }; + setFeatureNamingPattern?.(regex); + updateNamingExampleError({ + pattern: regex, + example: featureNamingExample || '', + }); + }; - const onSetFeatureNamingExample = (example: string) => { - setFeatureNamingExample && setFeatureNamingExample(example); - updateNamingExampleError({ - pattern: featureNamingPattern || "", - example, - }); - }; + const onSetFeatureNamingExample = (example: string) => { + setFeatureNamingExample && setFeatureNamingExample(example); + updateNamingExampleError({ + pattern: featureNamingPattern || '', + example, + }); + }; - const onSetFeatureNamingDescription = (description: string) => { - setFeatureNamingDescription?.(description); - }; + const onSetFeatureNamingDescription = (description: string) => { + setFeatureNamingDescription?.(description); + }; - return ( - { - handleSubmit(submitEvent); - trackPattern(featureNamingPattern); - }} - > - <> - -

What is your project collaboration mode?

- -
- { - setProjectMode?.(e.target.value as ProjectMode); - }} - options={projectModeOptions} - /> - - - - Feature flag naming pattern? - - - - -

- Define a{" "} - - JavaScript RegEx - {" "} - used to enforce feature flag names within - this project. The regex will be surrounded - by a leading ^ and a trailing{" "} - $. -

-

- Leave it empty if you don’t want to add a - naming pattern. -

-
-
- - - ^ - - ), - endAdornment: ( - - $ - - ), + return ( + { + handleSubmit(submitEvent); + trackPattern(featureNamingPattern); + }} + > + <> + +

What is your project collaboration mode?

+ +
+ { + setProjectMode?.(e.target.value as ProjectMode); + }} + options={projectModeOptions} + /> + + + - onSetFeatureNamingPattern(e.target.value) - } - /> + > + Feature flag naming pattern? + + -

- The example and description will be shown to - users when they create a new feature flag in - this project. -

+ +

+ Define a{' '} + + JavaScript RegEx + {' '} + used to enforce feature flag names + within this project. The regex will be + surrounded by a leading ^{' '} + and a trailing $. +

+

+ Leave it empty if you don’t want to add + a naming pattern. +

+
+ + + ^ + + ), + endAdornment: ( + + $ + + ), + }} + type={'text'} + value={featureNamingPattern || ''} + error={Boolean(errors.featureNamingPattern)} + errorText={errors.featureNamingPattern} + onChange={(e) => + onSetFeatureNamingPattern( + e.target.value, + ) + } + /> + +

+ The example and description will be + shown to users when they create a new + feature flag in this project. +

+
- - onSetFeatureNamingExample(e.target.value) - } - /> - .. + + onSetFeatureNamingExample( + e.target.value, + ) + } + /> + .. The flag name should contain the project name, the feature name, and the ticket number, each separated by a dot.`} - multiline - minRows={5} - value={featureNamingDescription || ""} - onChange={(e) => - onSetFeatureNamingDescription( - e.target.value - ) - } - /> -
- - } - /> - {children} -
- ); -}; + multiline + minRows={5} + value={featureNamingDescription || ''} + onChange={(e) => + onSetFeatureNamingDescription( + e.target.value, + ) + } + /> +
+ + } + /> + {children} +
+ ); + }; export default ProjectEnterpriseSettingsForm; diff --git a/frontend/src/component/project/Project/ProjectForm/ProjectForm.tsx b/frontend/src/component/project/Project/ProjectForm/ProjectForm.tsx index 2e7c7dbc2d3c..c30ed7d29268 100644 --- a/frontend/src/component/project/Project/ProjectForm/ProjectForm.tsx +++ b/frontend/src/component/project/Project/ProjectForm/ProjectForm.tsx @@ -1,15 +1,15 @@ -import React from "react"; -import { trim } from "component/common/util"; -import { StickinessSelect } from "component/feature/StrategyTypes/FlexibleStrategy/StickinessSelect/StickinessSelect"; -import { ConditionallyRender } from "component/common/ConditionallyRender/ConditionallyRender"; -import { Box, styled, TextField } from "@mui/material"; -import Input from "component/common/Input/Input"; -import { FeatureTogglesLimitTooltip } from "./FeatureTogglesLimitTooltip"; -import { ProjectMode } from "../hooks/useProjectEnterpriseSettingsForm"; -import useUiConfig from "hooks/api/getters/useUiConfig/useUiConfig"; -import { CollaborationModeTooltip } from "../ProjectEnterpriseSettingsForm/CollaborationModeTooltip"; -import Select from "component/common/select"; -import { useUiFlag } from "hooks/useUiFlag"; +import React from 'react'; +import { trim } from 'component/common/util'; +import { StickinessSelect } from 'component/feature/StrategyTypes/FlexibleStrategy/StickinessSelect/StickinessSelect'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { Box, styled, TextField } from '@mui/material'; +import Input from 'component/common/Input/Input'; +import { FeatureTogglesLimitTooltip } from './FeatureTogglesLimitTooltip'; +import { ProjectMode } from '../hooks/useProjectEnterpriseSettingsForm'; +import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; +import { CollaborationModeTooltip } from '../ProjectEnterpriseSettingsForm/CollaborationModeTooltip'; +import Select from 'component/common/select'; +import { useUiFlag } from 'hooks/useUiFlag'; interface IProjectForm { projectId: string; @@ -27,32 +27,32 @@ interface IProjectForm { setProjectMode?: React.Dispatch>; handleSubmit: (e: any) => void; errors: { [key: string]: string }; - mode: "Create" | "Edit"; + mode: 'Create' | 'Edit'; clearErrors: () => void; validateProjectId: () => void; } -const PROJECT_STICKINESS_SELECT = "PROJECT_STICKINESS_SELECT"; -const PROJECT_ID_INPUT = "PROJECT_ID_INPUT"; -const PROJECT_NAME_INPUT = "PROJECT_NAME_INPUT"; -const PROJECT_DESCRIPTION_INPUT = "PROJECT_DESCRIPTION_INPUT"; +const PROJECT_STICKINESS_SELECT = 'PROJECT_STICKINESS_SELECT'; +const PROJECT_ID_INPUT = 'PROJECT_ID_INPUT'; +const PROJECT_NAME_INPUT = 'PROJECT_NAME_INPUT'; +const PROJECT_DESCRIPTION_INPUT = 'PROJECT_DESCRIPTION_INPUT'; -const StyledForm = styled("form")(({ theme }) => ({ - height: "100%", +const StyledForm = styled('form')(({ theme }) => ({ + height: '100%', paddingBottom: theme.spacing(1), })); -const StyledDescription = styled("p")(({ theme }) => ({ +const StyledDescription = styled('p')(({ theme }) => ({ marginBottom: theme.spacing(1), marginRight: theme.spacing(1), })); const StyledSelect = styled(Select)(({ theme }) => ({ marginBottom: theme.spacing(2), - minWidth: "200px", + minWidth: '200px', })); -const StyledSubtitle = styled("div")(({ theme }) => ({ +const StyledSubtitle = styled('div')(({ theme }) => ({ color: theme.palette.text.secondary, fontSize: theme.fontSizes.smallerBody, lineHeight: 1.25, @@ -60,25 +60,25 @@ const StyledSubtitle = styled("div")(({ theme }) => ({ })); const StyledInput = styled(Input)(({ theme }) => ({ - width: "100%", + width: '100%', marginBottom: theme.spacing(2), paddingRight: theme.spacing(1), })); const StyledTextField = styled(TextField)(({ theme }) => ({ - width: "100%", + width: '100%', marginBottom: theme.spacing(2), })); -const StyledButtonContainer = styled("div")(() => ({ - marginTop: "auto", - display: "flex", - justifyContent: "flex-end", +const StyledButtonContainer = styled('div')(() => ({ + marginTop: 'auto', + display: 'flex', + justifyContent: 'flex-end', })); -const StyledInputContainer = styled("div")(() => ({ - display: "flex", - alignItems: "center", +const StyledInputContainer = styled('div')(() => ({ + display: 'flex', + alignItems: 'center', })); const ProjectForm: React.FC = ({ @@ -103,17 +103,17 @@ const ProjectForm: React.FC = ({ clearErrors, }) => { const { isEnterprise } = useUiConfig(); - const privateProjects = useUiFlag("privateProjects"); + const privateProjects = useUiFlag('privateProjects'); const projectModeOptions = privateProjects ? [ - { key: "open", label: "open" }, - { key: "protected", label: "protected" }, - { key: "private", label: "private" }, + { key: 'open', label: 'open' }, + { key: 'protected', label: 'protected' }, + { key: 'private', label: 'private' }, ] : [ - { key: "open", label: "open" }, - { key: "protected", label: "protected" }, + { key: 'open', label: 'open' }, + { key: 'protected', label: 'protected' }, ]; return ( @@ -124,14 +124,14 @@ const ProjectForm: React.FC = ({ > What is your project Id? setProjectId(trim(e.target.value))} error={Boolean(errors.id)} errorText={errors.id} onFocus={() => clearErrors()} onBlur={validateProjectId} - disabled={mode === "Edit"} + disabled={mode === 'Edit'} data-testid={PROJECT_ID_INPUT} autoFocus required @@ -139,7 +139,7 @@ const ProjectForm: React.FC = ({ What is your project name? setProjectName(e.target.value)} error={Boolean(errors.name)} @@ -155,8 +155,8 @@ const ProjectForm: React.FC = ({ What is your project description? = ({ What is the default stickiness for the project? @@ -184,13 +184,13 @@ const ProjectForm: React.FC = ({ } /> = ({ {featureLimit && setFeatureLimit && ( setFeatureLimit(e.target.value) @@ -229,13 +229,13 @@ const ProjectForm: React.FC = ({ } /> = ({ { setProjectMode?.(e.target.value as ProjectMode); }} diff --git a/frontend/src/component/project/Project/ProjectSettings/Settings/EditProject/DeleteProjectForm.tsx b/frontend/src/component/project/Project/ProjectSettings/Settings/EditProject/DeleteProjectForm.tsx index a40bee0f973c..9df7eae10ef6 100644 --- a/frontend/src/component/project/Project/ProjectSettings/Settings/EditProject/DeleteProjectForm.tsx +++ b/frontend/src/component/project/Project/ProjectSettings/Settings/EditProject/DeleteProjectForm.tsx @@ -1,15 +1,15 @@ -import React from "react"; -import { DeleteProject } from "../DeleteProject"; -import FormTemplate from "component/common/FormTemplate/FormTemplate"; -import { useRequiredPathParam } from "hooks/useRequiredPathParam"; -import useProjectApi from "hooks/api/actions/useProjectApi/useProjectApi"; -import useUiConfig from "hooks/api/getters/useUiConfig/useUiConfig"; +import React from 'react'; +import { DeleteProject } from '../DeleteProject'; +import FormTemplate from 'component/common/FormTemplate/FormTemplate'; +import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; +import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi'; +import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; interface IDeleteProjectForm { featureCount: number; } export const DeleteProjectForm = ({ featureCount }: IDeleteProjectForm) => { - const id = useRequiredPathParam("projectId"); + const id = useRequiredPathParam('projectId'); const { uiConfig } = useUiConfig(); const { loading } = useProjectApi(); const formatProjectDeleteApiCode = () => { @@ -20,10 +20,10 @@ export const DeleteProjectForm = ({ featureCount }: IDeleteProjectForm) => { return ( ({ - display: "flex", - flexDirection: "column", +const StyledFormContainer = styled('div')(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', gap: theme.spacing(2), })); const EditProject = () => { const { isEnterprise } = useUiConfig(); const { hasAccess } = useContext(AccessContext); - const id = useRequiredPathParam("projectId"); + const id = useRequiredPathParam('projectId'); const { project } = useProject(id); const accessDeniedAlert = !hasAccess(UPDATE_PROJECT, id) && ( - + You do not have the required permissions to edit this project. ); diff --git a/frontend/src/component/project/Project/ProjectSettings/Settings/EditProject/UpdateEnterpriseSettings.tsx b/frontend/src/component/project/Project/ProjectSettings/Settings/EditProject/UpdateEnterpriseSettings.tsx index bbc8aa35432e..6c349cd8d35e 100644 --- a/frontend/src/component/project/Project/ProjectSettings/Settings/EditProject/UpdateEnterpriseSettings.tsx +++ b/frontend/src/component/project/Project/ProjectSettings/Settings/EditProject/UpdateEnterpriseSettings.tsx @@ -1,34 +1,34 @@ -import React, { useEffect } from "react"; -import useUiConfig from "hooks/api/getters/useUiConfig/useUiConfig"; -import useToast from "hooks/useToast"; -import { useRequiredPathParam } from "hooks/useRequiredPathParam"; -import useProjectEnterpriseSettingsForm from "component/project/Project/hooks/useProjectEnterpriseSettingsForm"; -import useProject from "hooks/api/getters/useProject/useProject"; -import useProjectApi from "hooks/api/actions/useProjectApi/useProjectApi"; -import { formatUnknownError } from "utils/formatUnknownError"; -import FormTemplate from "component/common/FormTemplate/FormTemplate"; -import ProjectEnterpriseSettingsForm from "component/project/Project/ProjectEnterpriseSettingsForm/ProjectEnterpriseSettingsForm"; -import PermissionButton from "component/common/PermissionButton/PermissionButton"; -import { UPDATE_PROJECT } from "component/providers/AccessProvider/permissions"; -import { IProject } from "component/../interfaces/project"; -import { styled } from "@mui/material"; -import { usePlausibleTracker } from "hooks/usePlausibleTracker"; - -const StyledContainer = styled("div")(({ theme }) => ({ +import React, { useEffect } from 'react'; +import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; +import useToast from 'hooks/useToast'; +import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; +import useProjectEnterpriseSettingsForm from 'component/project/Project/hooks/useProjectEnterpriseSettingsForm'; +import useProject from 'hooks/api/getters/useProject/useProject'; +import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi'; +import { formatUnknownError } from 'utils/formatUnknownError'; +import FormTemplate from 'component/common/FormTemplate/FormTemplate'; +import ProjectEnterpriseSettingsForm from 'component/project/Project/ProjectEnterpriseSettingsForm/ProjectEnterpriseSettingsForm'; +import PermissionButton from 'component/common/PermissionButton/PermissionButton'; +import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions'; +import { IProject } from 'component/../interfaces/project'; +import { styled } from '@mui/material'; +import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; + +const StyledContainer = styled('div')(({ theme }) => ({ minHeight: 0, borderRadius: theme.spacing(2), border: `1px solid ${theme.palette.divider}`, - width: "100%", - display: "flex", - margin: "0 auto", - overflow: "hidden", + width: '100%', + display: 'flex', + margin: '0 auto', + overflow: 'hidden', [theme.breakpoints.down(1100)]: { - flexDirection: "column", + flexDirection: 'column', minHeight: 0, }, })); -const StyledFormContainer = styled("div")(({ theme }) => ({ +const StyledFormContainer = styled('div')(({ theme }) => ({ borderTop: `1px solid ${theme.palette.divider}`, paddingTop: theme.spacing(4), })); @@ -36,17 +36,17 @@ const StyledFormContainer = styled("div")(({ theme }) => ({ interface IUpdateEnterpriseSettings { project: IProject; } -const EDIT_PROJECT_SETTINGS_BTN = "EDIT_PROJECT_SETTINGS_BTN"; +const EDIT_PROJECT_SETTINGS_BTN = 'EDIT_PROJECT_SETTINGS_BTN'; export const useModeTracking = () => { - const [previousMode, setPreviousMode] = React.useState(""); + const [previousMode, setPreviousMode] = React.useState(''); const { trackEvent } = usePlausibleTracker(); - const eventName = "project-mode" as const; + const eventName = 'project-mode' as const; const trackModePattern = (newMode: string) => { if (newMode !== previousMode) { trackEvent(eventName, { - props: { mode: newMode, action: "updated" }, + props: { mode: newMode, action: 'updated' }, }); } }; @@ -59,7 +59,7 @@ export const UpdateEnterpriseSettings = ({ }: IUpdateEnterpriseSettings) => { const { uiConfig } = useUiConfig(); const { setToastData, setToastApiError } = useToast(); - const id = useRequiredPathParam("projectId"); + const id = useRequiredPathParam('projectId'); const { projectMode, @@ -77,7 +77,7 @@ export const UpdateEnterpriseSettings = ({ project.mode, project?.featureNaming?.pattern, project?.featureNaming?.example, - project?.featureNaming?.description + project?.featureNaming?.description, ); const formatProjectSettingsApiCode = () => { @@ -94,20 +94,20 @@ export const UpdateEnterpriseSettings = ({ const useFeatureNamePatternTracking = () => { const [previousPattern, setPreviousPattern] = - React.useState(""); + React.useState(''); const { trackEvent } = usePlausibleTracker(); - const eventName = "feature-naming-pattern" as const; + const eventName = 'feature-naming-pattern' as const; - const trackPattern = (newPattern: string = "") => { + const trackPattern = (newPattern: string = '') => { if (newPattern === previousPattern) { // do nothing; they've probably updated something else in the // project. - } else if (newPattern === "" && previousPattern !== "") { - trackEvent(eventName, { props: { action: "removed" } }); - } else if (newPattern !== "" && previousPattern === "") { - trackEvent(eventName, { props: { action: "added" } }); - } else if (newPattern !== "" && previousPattern !== "") { - trackEvent(eventName, { props: { action: "edited" } }); + } else if (newPattern === '' && previousPattern !== '') { + trackEvent(eventName, { props: { action: 'removed' } }); + } else if (newPattern !== '' && previousPattern === '') { + trackEvent(eventName, { props: { action: 'added' } }); + } else if (newPattern !== '' && previousPattern !== '') { + trackEvent(eventName, { props: { action: 'edited' } }); } }; @@ -126,8 +126,8 @@ export const UpdateEnterpriseSettings = ({ await editProjectSettings(id, payload); refetch(); setToastData({ - title: "Project information updated", - type: "success", + title: 'Project information updated', + type: 'success', }); trackPattern(featureNamingPattern); trackModePattern(projectMode); @@ -137,7 +137,7 @@ export const UpdateEnterpriseSettings = ({ }; useEffect(() => { - setPreviousPattern(featureNamingPattern || ""); + setPreviousPattern(featureNamingPattern || ''); setPreviousMode(projectMode); }, [project]); @@ -145,10 +145,10 @@ export const UpdateEnterpriseSettings = ({ ( +const StyledContainer = styled('div')<{ isPro: boolean }>( ({ theme, isPro }) => ({ minHeight: 0, borderRadius: theme.spacing(2), - border: isPro ? "0" : `1px solid ${theme.palette.divider}`, - width: "100%", - display: "flex", - margin: "0 auto", - overflow: "hidden", + border: isPro ? '0' : `1px solid ${theme.palette.divider}`, + width: '100%', + display: 'flex', + margin: '0 auto', + overflow: 'hidden', [theme.breakpoints.down(1100)]: { - flexDirection: "column", + flexDirection: 'column', minHeight: 0, }, - }) + }), ); -const StyledFormContainer = styled("div")(({ theme }) => ({ +const StyledFormContainer = styled('div')(({ theme }) => ({ borderTop: `1px solid ${theme.palette.divider}`, paddingTop: theme.spacing(4), })); @@ -41,9 +41,9 @@ const StyledFormContainer = styled("div")(({ theme }) => ({ interface IUpdateProject { project: IProject; } -const EDIT_PROJECT_BTN = "EDIT_PROJECT_BTN"; +const EDIT_PROJECT_BTN = 'EDIT_PROJECT_BTN'; export const UpdateProject = ({ project }: IUpdateProject) => { - const id = useRequiredPathParam("projectId"); + const id = useRequiredPathParam('projectId'); const { uiConfig, isPro } = useUiConfig(); const { setToastData, setToastApiError } = useToast(); const { defaultStickiness } = useDefaultProjectSettings(id); @@ -69,7 +69,7 @@ export const UpdateProject = ({ project }: IUpdateProject) => { project.name, project.description, defaultStickiness, - String(project.featureLimit) + String(project.featureLimit), ); const { editProject, loading } = useProjectApi(); @@ -94,11 +94,11 @@ export const UpdateProject = ({ project }: IUpdateProject) => { await editProject(id, payload); refetch(); setToastData({ - title: "Project information updated", - type: "success", + title: 'Project information updated', + type: 'success', }); if (projectStickiness !== DEFAULT_PROJECT_STICKINESS) { - trackEvent("project_stickiness_set"); + trackEvent('project_stickiness_set'); } } catch (error: unknown) { setToastApiError(formatUnknownError(error)); @@ -110,10 +110,10 @@ export const UpdateProject = ({ project }: IUpdateProject) => { { featureLimit={featureLimit} projectDesc={projectDesc} setProjectDesc={setProjectDesc} - mode="Edit" + mode='Edit' clearErrors={clearErrors} validateProjectId={validateProjectId} >