From 6d70265edde0ecd470026657a63ca65beead562a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20G=C3=B3is?= Date: Tue, 3 Jun 2025 09:21:55 +0100 Subject: [PATCH] chore: clean up project related tech debt (#10065) https://linear.app/unleash/issue/2-3581/remove-project-related-legacy-code Identified some clean up opportunities during deprecated endpoint removal, mostly related to project insights. --- .../src/component/project/Project/Project.tsx | 4 - .../ChangeRequests/ChangeRequests.test.tsx | 66 ----- .../ChangeRequests/ChangeRequests.tsx | 144 ---------- .../FlagTypesUsed/FlagTypesUsed.test.tsx | 46 --- .../FlagTypesUsed/FlagTypesUsed.tsx | 121 -------- .../LeadTimeForChanges.test.tsx | 36 --- .../LeadTimeForChanges/LeadTimeForChanges.tsx | 267 ------------------ .../ProjectHealth/FlagCounts.tsx | 80 ------ .../ProjectHealth/ProjectHealth.tsx | 58 ---- .../ProjectHealth/ProjectHealthChart.test.tsx | 143 ---------- .../ProjectHealth/ProjectHealthChart.tsx | 142 ---------- .../ProjectInsights/ProjectInsights.tsx | 71 ----- .../ProjectInsightsStats/HelpPopper.tsx | 78 ----- .../ProjectInsightsStats.tsx | 119 -------- .../ProjectInsightsStats/StatusBox.tsx | 149 ---------- .../ProjectMembers/ProjectMembers.test.tsx | 15 - .../ProjectMembers/ProjectMembers.tsx | 43 --- .../useProjectInsights/useProjectInsights.ts | 56 ---- .../project-insights-controller.ts | 2 + .../features/project/project-controller.ts | 1 + 20 files changed, 3 insertions(+), 1638 deletions(-) delete mode 100644 frontend/src/component/project/Project/ProjectInsights/ChangeRequests/ChangeRequests.test.tsx delete mode 100644 frontend/src/component/project/Project/ProjectInsights/ChangeRequests/ChangeRequests.tsx delete mode 100644 frontend/src/component/project/Project/ProjectInsights/FlagTypesUsed/FlagTypesUsed.test.tsx delete mode 100644 frontend/src/component/project/Project/ProjectInsights/FlagTypesUsed/FlagTypesUsed.tsx delete mode 100644 frontend/src/component/project/Project/ProjectInsights/LeadTimeForChanges/LeadTimeForChanges.test.tsx delete mode 100644 frontend/src/component/project/Project/ProjectInsights/LeadTimeForChanges/LeadTimeForChanges.tsx delete mode 100644 frontend/src/component/project/Project/ProjectInsights/ProjectHealth/FlagCounts.tsx delete mode 100644 frontend/src/component/project/Project/ProjectInsights/ProjectHealth/ProjectHealth.tsx delete mode 100644 frontend/src/component/project/Project/ProjectInsights/ProjectHealth/ProjectHealthChart.test.tsx delete mode 100644 frontend/src/component/project/Project/ProjectInsights/ProjectHealth/ProjectHealthChart.tsx delete mode 100644 frontend/src/component/project/Project/ProjectInsights/ProjectInsights.tsx delete mode 100644 frontend/src/component/project/Project/ProjectInsights/ProjectInsightsStats/HelpPopper.tsx delete mode 100644 frontend/src/component/project/Project/ProjectInsights/ProjectInsightsStats/ProjectInsightsStats.tsx delete mode 100644 frontend/src/component/project/Project/ProjectInsights/ProjectInsightsStats/StatusBox.tsx delete mode 100644 frontend/src/component/project/Project/ProjectInsights/ProjectMembers/ProjectMembers.test.tsx delete mode 100644 frontend/src/component/project/Project/ProjectInsights/ProjectMembers/ProjectMembers.tsx delete mode 100644 frontend/src/hooks/api/getters/useProjectInsights/useProjectInsights.ts diff --git a/frontend/src/component/project/Project/Project.tsx b/frontend/src/component/project/Project/Project.tsx index 350f0e4316..bcaaf80949 100644 --- a/frontend/src/component/project/Project/Project.tsx +++ b/frontend/src/component/project/Project/Project.tsx @@ -26,7 +26,6 @@ import { import useToast from 'hooks/useToast'; import useQueryParams from 'hooks/useQueryParams'; import { useEffect, useState, type ReactNode } from 'react'; -import ProjectEnvironment from '../ProjectEnvironment/ProjectEnvironment.tsx'; import ProjectFlags from './ProjectFlags.tsx'; import ProjectHealth from './ProjectHealth/ProjectHealth.tsx'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; @@ -51,7 +50,6 @@ import type { UiFlags } from 'interfaces/uiConfig'; import { HiddenProjectIconWithTooltip } from './HiddenProjectIconWithTooltip/HiddenProjectIconWithTooltip.tsx'; import { ChangeRequestPlausibleProvider } from 'component/changeRequest/ChangeRequestContext'; import { ProjectApplications } from '../ProjectApplications/ProjectApplications.tsx'; -import { ProjectInsights } from './ProjectInsights/ProjectInsights.tsx'; import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview'; import { ProjectArchived } from './ArchiveProject/ProjectArchived.tsx'; import { usePlausibleTracker } from '../../../hooks/usePlausibleTracker.ts'; @@ -385,8 +383,6 @@ export const Project = () => { /> } /> - } /> - } /> } /> { - testServerRoute(server, '/api/admin/ui-config', { - versionInfo: { - current: { enterprise: 'present' }, - }, - }); - testServerRoute( - server, - '/api/admin/projects/default/change-requests/count', - { - total: 14, - approved: 2, - applied: 0, - rejected: 0, - reviewRequired: 10, - scheduled: 2, - }, - ); -}; - -const setupOssApi = () => { - testServerRoute(server, '/api/admin/ui-config', { - versionInfo: { - current: { oss: 'present' }, - }, - }); -}; - -test('Show enterprise hints', async () => { - setupOssApi(); - render( - - } /> - , - { - route: '/projects/default', - }, - ); - - await screen.findByText('Enterprise feature'); -}); - -test('Show change requests info', async () => { - setupEnterpriseApi(); - render( - - } /> - , - { - route: '/projects/default', - }, - ); - - await screen.findByText('To be applied'); - await screen.findByText('10'); - await screen.findByText('4'); - await screen.findByText('14'); -}); diff --git a/frontend/src/component/project/Project/ProjectInsights/ChangeRequests/ChangeRequests.tsx b/frontend/src/component/project/Project/ProjectInsights/ChangeRequests/ChangeRequests.tsx deleted file mode 100644 index ff25afb72e..0000000000 --- a/frontend/src/component/project/Project/ProjectInsights/ChangeRequests/ChangeRequests.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import { Box, styled, Typography } from '@mui/material'; -import KeyboardArrowRight from '@mui/icons-material/KeyboardArrowRight'; -import { Link } from 'react-router-dom'; -import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; -import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; -import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature'; -import { useChangeRequestsCount } from 'hooks/api/getters/useChangeRequestsCount/useChangeRequestsCount'; - -const Container = styled(Box)(({ theme }) => ({ - display: 'flex', - flexDirection: 'column', - gap: theme.spacing(2.5), -})); - -const BoxesContainer = styled(Box)(({ theme }) => ({ - display: 'flex', - gap: theme.spacing(1), - justifyContent: 'space-between', - flexWrap: 'wrap', -})); - -const NumberBox = styled(Box)(({ theme }) => ({ - display: 'flex', - flexDirection: 'column', - flex: 1, - alignItems: 'center', - justifyContent: 'center', - padding: theme.spacing(1), - borderRadius: theme.shape.borderRadiusMedium, - border: `1px solid ${theme.palette.divider}`, -})); - -const OpenBox = styled(Box)(({ theme }) => ({ - display: 'flex', - flexDirection: 'column', - flex: 1, - borderRadius: theme.shape.borderRadiusMedium, - padding: theme.spacing(3), - border: `2px solid ${theme.palette.primary.main}`, -})); - -const ColorBox = styled(Box)(({ theme }) => ({ - borderRadius: '8px', - padding: theme.spacing(1, 2), - display: 'flex', - gap: theme.spacing(6), - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: theme.spacing(1.5), - whiteSpace: 'nowrap', -})); - -const ApplyBox = styled(ColorBox)(({ theme }) => ({ - background: theme.palette.success.light, - marginTop: theme.spacing(2.5), -})); - -const ReviewBox = styled(ColorBox)(({ theme }) => ({ - background: theme.palette.secondary.light, -})); - -const ChangeRequestNavigation = styled(Link)(({ theme }) => ({ - display: 'flex', - justifyContent: 'space-between', - textDecoration: 'none', - color: theme.palette.text.primary, -})); - -const Title = styled(Typography)(({ theme }) => ({ - fontSize: theme.spacing(2), - color: theme.palette.text.secondary, - marginBottom: theme.spacing(1), -})); - -const MediumNumber = styled(Typography)(({ theme }) => ({ - fontSize: theme.spacing(3), - color: theme.palette.text.primary, -})); - -const BigNumber = styled(Typography)(({ theme }) => ({ - fontSize: theme.spacing(5.5), - color: theme.palette.text.primary, -})); - -export const ChangeRequests = () => { - const projectId = useRequiredPathParam('projectId'); - const { isOss, isPro } = useUiConfig(); - const { data } = useChangeRequestsCount(projectId); - - const { total, applied, rejected, reviewRequired, scheduled, approved } = - data; - const toBeApplied = scheduled + approved; - - if (isOss() || isPro()) { - return ( - - Change requests - - - ); - } - - return ( - - - Change requests - - - - - - - Open - - - - To be applied - {toBeApplied} - - - To be reviewed - {reviewRequired} - - - - Total - {total} - - - Applied - {applied} - - - Rejected - {rejected} - - - - ); -}; diff --git a/frontend/src/component/project/Project/ProjectInsights/FlagTypesUsed/FlagTypesUsed.test.tsx b/frontend/src/component/project/Project/ProjectInsights/FlagTypesUsed/FlagTypesUsed.test.tsx deleted file mode 100644 index 96d49dfa44..0000000000 --- a/frontend/src/component/project/Project/ProjectInsights/FlagTypesUsed/FlagTypesUsed.test.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { screen } from '@testing-library/react'; -import { render } from 'utils/testRenderer'; -import { testServerRoute, testServerSetup } from 'utils/testServer'; -import type { ProjectOverviewSchema } from 'openapi'; -import { Route, Routes } from 'react-router-dom'; -import { FlagTypesUsed } from './FlagTypesUsed.tsx'; - -const server = testServerSetup(); - -const setupApi = (overview: ProjectOverviewSchema) => { - testServerRoute(server, '/api/admin/projects/default/overview', overview); -}; - -test('Show outdated SDKs and apps using them', async () => { - setupApi({ - name: 'default', - version: 2, - featureTypeCounts: [ - { - type: 'release', - count: 57, - }, - ], - onboardingStatus: { - status: 'onboarded', - }, - }); - render( - - - } - /> - , - { - route: '/projects/default', - }, - ); - - await screen.findByText('Release'); - await screen.findByText('57'); -}); diff --git a/frontend/src/component/project/Project/ProjectInsights/FlagTypesUsed/FlagTypesUsed.tsx b/frontend/src/component/project/Project/ProjectInsights/FlagTypesUsed/FlagTypesUsed.tsx deleted file mode 100644 index 55b0bbacb2..0000000000 --- a/frontend/src/component/project/Project/ProjectInsights/FlagTypesUsed/FlagTypesUsed.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { type FC, useMemo } from 'react'; -import { styled, type SvgIconTypeMap, Typography } from '@mui/material'; -import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons'; - -import type { OverridableComponent } from '@mui/material/OverridableComponent'; -import type { FeatureTypeCountSchema } from 'openapi'; - -export const StyledProjectInfoWidgetContainer = styled('div')(({ theme }) => ({ - margin: '0', - [theme.breakpoints.down('md')]: { - display: 'flex', - flexDirection: 'column', - position: 'relative', - }, -})); - -export const StyledWidgetTitle = styled(Typography)(({ theme }) => ({ - marginBottom: theme.spacing(2.5), -})); - -export const StyledCount = styled('span')(({ theme }) => ({ - fontSize: theme.typography.h2.fontSize, - fontWeight: 'bold', - color: theme.palette.text.primary, -})); - -const StyledTypeCount = styled(StyledCount)(({ theme }) => ({ - marginLeft: 'auto', - fontWeight: theme.typography.fontWeightRegular, - color: theme.palette.text.secondary, -})); - -interface IFlagTypeRowProps { - type: string; - Icon: OverridableComponent; - count: number; -} - -const StyledParagraphGridRow = styled('div')(({ theme }) => ({ - display: 'flex', - gap: theme.spacing(1.5), - width: '100%', - margin: theme.spacing(1, 0), - fontSize: theme.fontSizes.smallBody, - color: theme.palette.text.secondary, - alignItems: 'center', - [theme.breakpoints.down('md')]: { - margin: 0, - }, -})); - -const FlagTypesRow = ({ type, Icon, count }: IFlagTypeRowProps) => { - const getTitleText = (str: string) => { - return str.charAt(0).toUpperCase() + str.slice(1).replace('-', ' '); - }; - - return ( - - -
{getTitleText(type)}
- {count} -
- ); -}; - -export const FlagTypesUsed: FC<{ - featureTypeCounts: FeatureTypeCountSchema[]; -}> = ({ featureTypeCounts }) => { - const featureTypeStats = useMemo(() => { - const release = - featureTypeCounts.find( - (featureType) => featureType.type === 'release', - )?.count || 0; - - const experiment = - featureTypeCounts.find( - (featureType) => featureType.type === 'experiment', - )?.count || 0; - - const operational = - featureTypeCounts.find( - (featureType) => featureType.type === 'operational', - )?.count || 0; - - const kill = - featureTypeCounts.find( - (featureType) => featureType.type === 'kill-switch', - )?.count || 0; - - const permission = - featureTypeCounts.find( - (featureType) => featureType.type === 'permission', - )?.count || 0; - - return { - release, - experiment, - operational, - 'kill-switch': kill, - permission, - }; - }, [featureTypeCounts]); - - return ( - - - Flag types used - - {Object.keys(featureTypeStats).map((type) => ( - - ))} - - ); -}; diff --git a/frontend/src/component/project/Project/ProjectInsights/LeadTimeForChanges/LeadTimeForChanges.test.tsx b/frontend/src/component/project/Project/ProjectInsights/LeadTimeForChanges/LeadTimeForChanges.test.tsx deleted file mode 100644 index eca097c1cf..0000000000 --- a/frontend/src/component/project/Project/ProjectInsights/LeadTimeForChanges/LeadTimeForChanges.test.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { screen } from '@testing-library/react'; -import { render } from 'utils/testRenderer'; -import type { ProjectDoraMetricsSchema } from 'openapi'; -import { LeadTimeForChanges } from './LeadTimeForChanges.tsx'; -import { Route, Routes } from 'react-router-dom'; - -test('Show outdated SDKs and apps using them', async () => { - const leadTime: ProjectDoraMetricsSchema = { - features: [ - { - name: 'ABCD', - timeToProduction: 57, - }, - ], - projectAverage: 67, - }; - render( - - - } - /> - , - { - route: '/projects/default', - }, - ); - - await screen.findByText('Lead time for changes (per release flag)'); - await screen.findByText('ABCD'); - await screen.findByText('57 days'); - await screen.findByText('Low'); - await screen.findByText('10 days'); -}); diff --git a/frontend/src/component/project/Project/ProjectInsights/LeadTimeForChanges/LeadTimeForChanges.tsx b/frontend/src/component/project/Project/ProjectInsights/LeadTimeForChanges/LeadTimeForChanges.tsx deleted file mode 100644 index 655dce8164..0000000000 --- a/frontend/src/component/project/Project/ProjectInsights/LeadTimeForChanges/LeadTimeForChanges.tsx +++ /dev/null @@ -1,267 +0,0 @@ -import { Box, styled, Tooltip, Typography, useMediaQuery } from '@mui/material'; -import { useMemo } from 'react'; -import { useTable, useGlobalFilter, useSortBy } from 'react-table'; -import { - Table, - SortableTableHeader, - TableBody, - TableCell, - TableRow, - TablePlaceholder, -} from 'component/common/Table'; -import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -import { Badge } from 'component/common/Badge/Badge'; -import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns'; -import theme from 'themes/theme'; -import type { ProjectDoraMetricsSchema } from 'openapi'; - -const Container = styled(Box)(({ theme }) => ({ - display: 'flex', - flexDirection: 'column', - gap: theme.spacing(2), -})); - -const TableContainer = styled(Box)(({ theme }) => ({ - overflowY: 'auto', - maxHeight: theme.spacing(45), -})); - -const resolveDoraMetrics = (input: number) => { - const ONE_MONTH = 30; - const ONE_WEEK = 7; - - if (input >= ONE_MONTH) { - return Low; - } - - if (input <= ONE_MONTH && input >= ONE_WEEK + 1) { - return Medium; - } - - if (input <= ONE_WEEK) { - return High; - } -}; - -interface ILeadTimeForChangesProps { - leadTime: ProjectDoraMetricsSchema; - loading: boolean; -} - -const loadingLeadTimeFeatures = [ - { name: 'feature1', timeToProduction: 0 }, - { name: 'feature2', timeToProduction: 0 }, - { name: 'feature3', timeToProduction: 0 }, - { name: 'feature4', timeToProduction: 0 }, - { name: 'feature5', timeToProduction: 2 }, -]; - -export const LeadTimeForChanges = ({ - leadTime, - loading, -}: ILeadTimeForChangesProps) => { - const columns = useMemo( - () => [ - { - Header: 'Name', - accessor: 'name', - width: '40%', - Cell: ({ - row: { - original: { name }, - }, - }: any) => { - return ( - - {name} - - ); - }, - sortType: 'alphanumeric', - }, - { - Header: 'Time to production', - id: 'timetoproduction', - align: 'center', - Cell: ({ row: { original } }: any) => ( - - - {original.timeToProduction} days - - - ), - width: 220, - disableGlobalFilter: true, - disableSortBy: true, - }, - { - Header: `Deviation`, - id: 'deviation', - align: 'center', - Cell: ({ row: { original } }: any) => ( - - - {Math.round( - (leadTime.projectAverage - ? leadTime.projectAverage - : 0) - original.timeToProduction, - )}{' '} - days - - - ), - width: 300, - disableGlobalFilter: true, - disableSortBy: true, - }, - { - Header: 'DORA', - id: 'dora', - align: 'center', - Cell: ({ row: { original } }: any) => ( - - - {resolveDoraMetrics(original.timeToProduction)} - - - ), - width: 200, - disableGlobalFilter: true, - disableSortBy: true, - }, - ], - [JSON.stringify(leadTime.features), loading], - ); - - const initialState = useMemo( - () => ({ - sortBy: [ - { - id: 'name', - desc: false, - }, - ], - }), - [], - ); - - const isExtraSmallScreen = useMediaQuery(theme.breakpoints.down('sm')); - - const { - getTableProps, - getTableBodyProps, - headerGroups, - rows, - prepareRow, - state: { globalFilter }, - setHiddenColumns, - } = useTable( - { - columns: columns as any[], - data: loading ? loadingLeadTimeFeatures : leadTime.features, - initialState, - autoResetGlobalFilter: false, - autoResetSortBy: false, - disableSortRemove: true, - }, - useGlobalFilter, - useSortBy, - ); - - useConditionallyHiddenColumns( - [ - { - condition: isExtraSmallScreen, - columns: ['deviation'], - }, - ], - setHiddenColumns, - columns, - ); - - return ( - - - Lead time for changes (per release flag) - - - - - - {rows.map((row) => { - prepareRow(row); - const { key, ...rowProps } = row.getRowProps(); - return ( - - {row.cells.map((cell) => { - const { key, ...cellProps } = - cell.getCellProps(); - return ( - - {cell.render('Cell')} - - ); - })} - - ); - })} - -
-
- 0} - show={ - - No features with data found “ - {globalFilter} - ” - - } - /> - } - /> -
- ); -}; diff --git a/frontend/src/component/project/Project/ProjectInsights/ProjectHealth/FlagCounts.tsx b/frontend/src/component/project/Project/ProjectInsights/ProjectHealth/FlagCounts.tsx deleted file mode 100644 index 226f8c910b..0000000000 --- a/frontend/src/component/project/Project/ProjectInsights/ProjectHealth/FlagCounts.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { Box, styled, useTheme } from '@mui/material'; -import { Link } from 'react-router-dom'; -import type { FC } from 'react'; - -const Dot = styled('span', { - shouldForwardProp: (prop) => prop !== 'color', -})<{ color?: string }>(({ theme, color }) => ({ - height: '15px', - width: '15px', - borderRadius: '50%', - display: 'inline-block', - backgroundColor: color, -})); - -const FlagCountsWrapper = styled(Box)(({ theme }) => ({ - display: 'flex', - flexDirection: 'column', - gap: theme.spacing(2), -})); - -const FlagsCount = styled('p')(({ theme }) => ({ - color: theme.palette.text.secondary, - marginLeft: theme.spacing(3), -})); - -const StatusWithDot = styled(Box)(({ theme }) => ({ - display: 'flex', - alignItems: 'center', - gap: theme.spacing(1), -})); - -export const FlagCounts: FC<{ - projectId: string; - activeCount: number; - potentiallyStaleCount: number; - staleCount: number; - hideLinks?: boolean; -}> = ({ - projectId, - activeCount, - potentiallyStaleCount, - staleCount, - hideLinks = false, -}) => { - const theme = useTheme(); - - return ( - - - - - Active - - {activeCount} feature flags - - - - - Potentially stale - {hideLinks ? null : ( - (configure) - )} - - {potentiallyStaleCount} feature flags - - - - - Stale - {hideLinks ? null : ( - - (view flags) - - )} - - {staleCount} feature flags - - - ); -}; diff --git a/frontend/src/component/project/Project/ProjectInsights/ProjectHealth/ProjectHealth.tsx b/frontend/src/component/project/Project/ProjectInsights/ProjectHealth/ProjectHealth.tsx deleted file mode 100644 index dc0d42fa4d..0000000000 --- a/frontend/src/component/project/Project/ProjectInsights/ProjectHealth/ProjectHealth.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { ProjectHealthChart } from './ProjectHealthChart.tsx'; -import { Alert, Box, styled, Typography } from '@mui/material'; -import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; -import type { ProjectInsightsSchemaHealth } from 'openapi'; -import type { FC } from 'react'; -import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -import { FlagCounts } from './FlagCounts.tsx'; - -const Container = styled(Box)(({ theme }) => ({ - display: 'flex', - flexDirection: 'column', - gap: theme.spacing(2), -})); - -export const ProjectHealth: FC<{ health: ProjectInsightsSchemaHealth }> = ({ - health, -}) => { - const projectId = useRequiredPathParam('projectId'); - const { staleCount, potentiallyStaleCount, activeCount, rating } = health; - - return ( - - Project Health - 0} - show={ - - Health alert! Review your flags and delete the - stale flags - - } - /> - - ({ - display: 'flex', - gap: theme.spacing(4), - marginTop: theme.spacing(3), - })} - > - - - - - - ); -}; diff --git a/frontend/src/component/project/Project/ProjectInsights/ProjectHealth/ProjectHealthChart.test.tsx b/frontend/src/component/project/Project/ProjectInsights/ProjectHealth/ProjectHealthChart.test.tsx deleted file mode 100644 index 2cd962485c..0000000000 --- a/frontend/src/component/project/Project/ProjectInsights/ProjectHealth/ProjectHealthChart.test.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import '@testing-library/jest-dom'; -import { ProjectHealthChart } from './ProjectHealthChart.tsx'; -import { render } from 'utils/testRenderer'; -import { screen } from '@testing-library/react'; - -describe('ProjectHealthChart', () => { - test('renders correctly with no flags', () => { - const { container } = render( - , - ); - - const activeCircle = container.querySelector( - 'circle[data-testid="active-circle"]', - ); - const staleCircle = container.querySelector( - 'circle[data-testid="stale-circle"]', - ); - const potentiallyStaleCircle = container.querySelector( - 'circle[data-testid="potentially-stale-circle"]', - ); - - expect(activeCircle).toBeInTheDocument(); - expect(staleCircle).not.toBeInTheDocument(); - expect(potentiallyStaleCircle).not.toBeInTheDocument(); - }); - - test('renders correctly with 1 active and 0 stale', () => { - const { container } = render( - , - ); - - const activeCircle = container.querySelector( - 'circle[data-testid="active-circle"]', - ); - const staleCircle = container.querySelector( - 'circle[data-testid="stale-circle"]', - ); - const potentiallyStaleCircle = container.querySelector( - 'circle[data-testid="potentially-stale-circle"]', - ); - - expect(activeCircle).toBeInTheDocument(); - expect(staleCircle).not.toBeInTheDocument(); - expect(potentiallyStaleCircle).not.toBeInTheDocument(); - }); - - test('renders correctly with 0 active and 1 stale', () => { - const { container } = render( - , - ); - - const staleCircle = container.querySelector( - 'circle[data-testid="stale-circle"]', - ); - - expect(staleCircle).toBeInTheDocument(); - }); - - test('renders correctly with active, stale and potentially stale', () => { - const { container } = render( - , - ); - - const activeCircle = container.querySelector( - 'circle[data-testid="active-circle"]', - ); - const staleCircle = container.querySelector( - 'circle[data-testid="stale-circle"]', - ); - const potentiallyStaleCircle = container.querySelector( - 'circle[data-testid="potentially-stale-circle"]', - ); - - expect(activeCircle).toBeInTheDocument(); - expect(staleCircle).toBeInTheDocument(); - expect(potentiallyStaleCircle).toBeInTheDocument(); - }); - - test('renders flags count and health', () => { - const { container } = render( - , - ); - - expect(screen.queryByText('3 flags')).toBeInTheDocument(); - expect(screen.queryByText('50%')).toBeInTheDocument(); - }); - - test('renders small values without negative stroke dasharray', () => { - const { container } = render( - , - ); - - const activeCircle = container.querySelector( - 'circle[data-testid="active-circle"]', - ); - const staleCircle = container.querySelector( - 'circle[data-testid="stale-circle"]', - ); - const potentiallyStaleCircle = container.querySelector( - 'circle[data-testid="potentially-stale-circle"]', - ); - - expect( - activeCircle?.getAttribute('stroke-dasharray')?.charAt(0), - ).not.toBe('-'); - expect( - staleCircle?.getAttribute('stroke-dasharray')?.charAt(0), - ).not.toBe('-'); - expect( - potentiallyStaleCircle?.getAttribute('stroke-dasharray')?.charAt(0), - ).not.toBe('-'); - }); -}); diff --git a/frontend/src/component/project/Project/ProjectInsights/ProjectHealth/ProjectHealthChart.tsx b/frontend/src/component/project/Project/ProjectInsights/ProjectHealth/ProjectHealthChart.tsx deleted file mode 100644 index 45ddccdd7a..0000000000 --- a/frontend/src/component/project/Project/ProjectInsights/ProjectHealth/ProjectHealthChart.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import type React from 'react'; -import { useTheme } from '@mui/material'; -import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; - -interface ProgressComponentProps { - active: number; - stale: number; - potentiallyStale: number; - health: number; -} - -export const ProjectHealthChart: React.FC = ({ - active, - stale, - potentiallyStale, - health, -}) => { - const theme = useTheme(); - const gap = - active === 0 || - stale === 0 || - active / stale > 30 || - stale / active > 30 - ? 0 - : 10; - const strokeWidth = 6; - const radius = 50 - strokeWidth / 2; - const circumference = 2 * Math.PI * radius; - const gapAngle = (gap / circumference) * 360; - - const totalCount = active + stale; - const activePercentage = - totalCount === 0 ? 100 : (active / totalCount) * 100; - const stalePercentage = totalCount === 0 ? 0 : (stale / totalCount) * 100; - const potentiallyStalePercentage = - active === 0 ? 0 : (potentiallyStale / totalCount) * 100; - - const activeLength = Math.max( - (activePercentage / 100) * circumference - gap, - 1, - ); - const staleLength = Math.max( - (stalePercentage / 100) * circumference - gap, - 1, - ); - const potentiallyStaleLength = Math.max( - (potentiallyStalePercentage / 100) * circumference - gap, - 1, - ); - - const activeRotation = -90 + gapAngle / 2; - const potentiallyStaleRotation = - activeRotation + - ((activeLength - potentiallyStaleLength) / circumference) * 360; - const staleRotation = - activeRotation + (activeLength / circumference) * 360 + gapAngle; - - const innerRadius = radius / 1.2; - - return ( - - Project Health Chart - - - 0} - show={ - - } - /> - - 0} - show={ - - } - /> - - - - - {health}% - - - {active + stale} flags - - - ); -}; diff --git a/frontend/src/component/project/Project/ProjectInsights/ProjectInsights.tsx b/frontend/src/component/project/Project/ProjectInsights/ProjectInsights.tsx deleted file mode 100644 index a70a13c738..0000000000 --- a/frontend/src/component/project/Project/ProjectInsights/ProjectInsights.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { Box, styled } from '@mui/material'; -import { ChangeRequests } from './ChangeRequests/ChangeRequests.tsx'; -import { LeadTimeForChanges } from './LeadTimeForChanges/LeadTimeForChanges.tsx'; -import { ProjectHealth } from './ProjectHealth/ProjectHealth.tsx'; -import { FlagTypesUsed } from './FlagTypesUsed/FlagTypesUsed.tsx'; -import { ProjectInsightsStats } from './ProjectInsightsStats/ProjectInsightsStats.tsx'; -import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; -import { useProjectInsights } from 'hooks/api/getters/useProjectInsights/useProjectInsights'; -import useLoading from 'hooks/useLoading'; -import { ProjectMembers } from './ProjectMembers/ProjectMembers.tsx'; - -const Container = styled(Box)(({ theme }) => ({ - backgroundColor: theme.palette.background.paper, - padding: theme.spacing(3), - borderRadius: theme.shape.borderRadiusLarge, -})); - -const Grid = styled(Box)(({ theme }) => ({ - display: 'grid', - gap: theme.spacing(2), - gridTemplateColumns: 'repeat(10, 1fr)', -})); - -const FullWidthContainer = styled(Box)(() => ({ - gridColumn: '1 / -1', -})); - -const WideContainer = styled(Container)(() => ({ - gridColumn: 'span 6', -})); - -const MediumWideContainer = styled(Container)(() => ({ - gridColumn: 'span 4', -})); - -const NarrowContainer = styled(Container)(() => ({ - gridColumn: 'span 2', -})); - -export const ProjectInsights = () => { - const projectId = useRequiredPathParam('projectId'); - const { data, loading } = useProjectInsights(projectId); - - const ref = useLoading(loading); - - return ( - - - - - - - - - - - - - - - - - - - - - ); -}; diff --git a/frontend/src/component/project/Project/ProjectInsights/ProjectInsightsStats/HelpPopper.tsx b/frontend/src/component/project/Project/ProjectInsights/ProjectInsightsStats/HelpPopper.tsx deleted file mode 100644 index ebff44a7a6..0000000000 --- a/frontend/src/component/project/Project/ProjectInsights/ProjectInsightsStats/HelpPopper.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import type React from 'react'; -import { type FC, useState } from 'react'; -import Close from '@mui/icons-material/Close'; -import HelpOutline from '@mui/icons-material/HelpOutline'; -import { - Box, - IconButton, - Popper, - Paper, - ClickAwayListener, - styled, -} from '@mui/material'; -import { Feedback } from 'component/common/Feedback/Feedback'; - -interface IHelpPopperProps { - id: string; - children?: React.ReactNode; -} - -const StyledPaper = styled(Paper)(({ theme }) => ({ - padding: theme.spacing(3, 3), - maxWidth: '350px', - borderRadius: `${theme.shape.borderRadiusMedium}px`, - border: `1px solid ${theme.palette.neutral.border}`, - fontSize: theme.typography.body2.fontSize, -})); - -export const HelpPopper: FC = ({ children, id }) => { - const [anchor, setAnchorEl] = useState(null); - - const onOpen = (event: React.FormEvent) => - setAnchorEl(event.currentTarget); - - const onClose = () => setAnchorEl(null); - - const open = Boolean(anchor); - - return ( - - - theme.typography.body1.fontSize, - }} - /> - - - ({ zIndex: theme.zIndex.tooltip })} - > - - - - - theme.typography.body1.fontSize, - }} - /> - - {children} - - - - - - ); -}; diff --git a/frontend/src/component/project/Project/ProjectInsights/ProjectInsightsStats/ProjectInsightsStats.tsx b/frontend/src/component/project/Project/ProjectInsights/ProjectInsightsStats/ProjectInsightsStats.tsx deleted file mode 100644 index fc5e923ab3..0000000000 --- a/frontend/src/component/project/Project/ProjectInsights/ProjectInsightsStats/ProjectInsightsStats.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { Box, styled, Typography } from '@mui/material'; -import type { ProjectStatsSchema } from 'openapi/models'; -import { HelpPopper } from './HelpPopper.tsx'; -import { StatusBox } from './StatusBox.tsx'; -import KeyboardArrowRight from '@mui/icons-material/KeyboardArrowRight'; -import { Link } from 'react-router-dom'; -import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; - -const StyledBox = styled(Box)(({ theme }) => ({ - display: 'grid', - gap: theme.spacing(2), - gridTemplateColumns: 'repeat(4, 1fr)', - flexWrap: 'wrap', - [theme.breakpoints.down('lg')]: { - gridTemplateColumns: 'repeat(2, 1fr)', - }, - [theme.breakpoints.down('sm')]: { - flexDirection: 'column', - }, -})); - -const StyledTimeToProductionDescription = styled(Typography)(({ theme }) => ({ - color: theme.palette.text.secondary, - fontSize: theme.typography.body2.fontSize, - lineHeight: theme.typography.body2.lineHeight, -})); - -const NavigationBar = styled(Link)(({ theme }) => ({ - marginLeft: 'auto', - display: 'flex', - justifyContent: 'space-between', - textDecoration: 'none', - color: theme.palette.text.primary, -})); - -interface IProjectStatsProps { - stats: ProjectStatsSchema; -} - -export const ProjectInsightsStats = ({ stats }: IProjectStatsProps) => { - const projectId = useRequiredPathParam('projectId'); - if (Object.keys(stats).length === 0) { - return null; - } - - const { - avgTimeToProdCurrentWindow, - projectActivityCurrentWindow, - projectActivityPastWindow, - createdCurrentWindow, - createdPastWindow, - archivedCurrentWindow, - archivedPastWindow, - } = stats; - - return ( - - - - Sum of all configuration and state modifications in the - project. - - - theme.spacing(1), - }} - > - {avgTimeToProdCurrentWindow}{' '} - days - - } - customChangeElement={ - - In project life - - } - percentage - > - - How long did it take on average from a feature flag was - created until it was enabled in an environment of type - production. This is calculated only from feature flags with - the type of "release". - - - - - - - - - - - - - - - ); -}; diff --git a/frontend/src/component/project/Project/ProjectInsights/ProjectInsightsStats/StatusBox.tsx b/frontend/src/component/project/Project/ProjectInsights/ProjectInsightsStats/StatusBox.tsx deleted file mode 100644 index f4597c1b69..0000000000 --- a/frontend/src/component/project/Project/ProjectInsights/ProjectInsightsStats/StatusBox.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import type React from 'react'; -import type { FC, ReactNode } from 'react'; -import CallMade from '@mui/icons-material/CallMade'; -import SouthEast from '@mui/icons-material/SouthEast'; -import { Box, Typography, styled } from '@mui/material'; -import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -import { flexRow } from 'themes/themeStyles'; - -const StyledTypographyCount = styled(Box)(({ theme }) => ({ - fontSize: theme.fontSizes.largeHeader, -})); - -const StyledBoxChangeContainer = styled(Box)(({ theme }) => ({ - ...flexRow, - flexDirection: 'column', - alignItems: 'center', - marginLeft: theme.spacing(2.5), -})); - -const StyledTypographySubtext = styled(Typography)(({ theme }) => ({ - color: theme.palette.text.secondary, - fontSize: theme.typography.body2.fontSize, -})); - -const StyledTypographyChange = styled(Typography)(({ theme }) => ({ - marginLeft: theme.spacing(1), - fontSize: theme.typography.body1.fontSize, - fontWeight: theme.typography.fontWeightBold, -})); - -const RowContainer = styled(Box)(({ theme }) => ({ - ...flexRow, -})); - -const StyledWidget = styled(Box)(({ theme }) => ({ - padding: theme.spacing(3), - backgroundColor: theme.palette.background.paper, - flex: 1, - display: 'flex', - flexDirection: 'column', - gap: theme.spacing(2.5), - borderRadius: `${theme.shape.borderRadiusLarge}px`, - [theme.breakpoints.down('lg')]: { - padding: theme.spacing(2), - }, -})); - -interface IStatusBoxProps { - title: string; - boxText: ReactNode; - change?: number; - percentage?: boolean; - customChangeElement?: ReactNode; - children?: React.ReactNode; -} - -const resolveIcon = (change: number) => { - if (change > 0) { - return ( - - ); - } - return ( - - ); -}; - -const resolveColor = (change: number) => { - if (change > 0) { - return 'success.dark'; - } - return 'warning.dark'; -}; - -export const StatusBox: FC = ({ - title, - boxText, - change, - percentage, - children, - customChangeElement, -}) => ( - - - - {title} - - {children} - - - - {boxText} - - - {customChangeElement} - - } - elseShow={ - - - {resolveIcon(change as number)} - - {(change as number) > 0 ? '+' : ''} - {change} - {percentage ? '%' : ''} - - - - this month - - - } - elseShow={ - - - No change - - - } - /> - } - /> - - -); diff --git a/frontend/src/component/project/Project/ProjectInsights/ProjectMembers/ProjectMembers.test.tsx b/frontend/src/component/project/Project/ProjectInsights/ProjectMembers/ProjectMembers.test.tsx deleted file mode 100644 index cb3513792b..0000000000 --- a/frontend/src/component/project/Project/ProjectInsights/ProjectMembers/ProjectMembers.test.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { screen } from '@testing-library/react'; -import { render } from 'utils/testRenderer'; -import { ProjectMembers } from './ProjectMembers.tsx'; - -test('Show outdated project members', async () => { - const members = { - currentMembers: 10, - change: 2, - }; - - render(); - - await screen.findByText('10'); - await screen.findByText('+2'); -}); diff --git a/frontend/src/component/project/Project/ProjectInsights/ProjectMembers/ProjectMembers.tsx b/frontend/src/component/project/Project/ProjectInsights/ProjectMembers/ProjectMembers.tsx deleted file mode 100644 index 7f0cd154dc..0000000000 --- a/frontend/src/component/project/Project/ProjectInsights/ProjectMembers/ProjectMembers.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; -import { styled } from '@mui/material'; -import { StatusBox } from '../ProjectInsightsStats/StatusBox.tsx'; -import KeyboardArrowRight from '@mui/icons-material/KeyboardArrowRight'; -import { Link } from 'react-router-dom'; -import type { ProjectInsightsSchemaMembers } from 'openapi'; - -interface IProjectMembersProps { - members: ProjectInsightsSchemaMembers; - projectId: string; -} - -const NavigationBar = styled(Link)(({ theme }) => ({ - marginLeft: 'auto', - display: 'flex', - justifyContent: 'space-between', - textDecoration: 'none', - color: theme.palette.text.primary, -})); - -export const ProjectMembers = ({ - members, - projectId, -}: IProjectMembersProps) => { - const { uiConfig } = useUiConfig(); - - const link = uiConfig?.versionInfo?.current?.enterprise - ? `/projects/${projectId}/settings/access` - : `/admin/users`; - - const { currentMembers, change } = members; - return ( - - - - - - ); -}; diff --git a/frontend/src/hooks/api/getters/useProjectInsights/useProjectInsights.ts b/frontend/src/hooks/api/getters/useProjectInsights/useProjectInsights.ts deleted file mode 100644 index ed6e48edf5..0000000000 --- a/frontend/src/hooks/api/getters/useProjectInsights/useProjectInsights.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { fetcher, useApiGetter } from '../useApiGetter/useApiGetter.js'; -import type { ProjectInsightsSchema } from 'openapi'; -import { formatApiPath } from 'utils/formatPath'; - -const path = (projectId: string) => `api/admin/projects/${projectId}/insights`; - -const placeholderData: ProjectInsightsSchema = { - stats: { - avgTimeToProdCurrentWindow: 0, - createdCurrentWindow: 0, - createdPastWindow: 0, - archivedCurrentWindow: 0, - archivedPastWindow: 0, - projectActivityCurrentWindow: 0, - projectActivityPastWindow: 0, - projectMembersAddedCurrentWindow: 0, - }, - featureTypeCounts: [ - { - type: 'experiment', - count: 0, - }, - { - type: 'permission', - count: 0, - }, - { - type: 'release', - count: 0, - }, - ], - leadTime: { - projectAverage: 0, - features: [], - }, - health: { - rating: 0, - activeCount: 0, - potentiallyStaleCount: 0, - staleCount: 0, - }, - members: { - currentMembers: 0, - change: 0, - }, -}; - -export const useProjectInsights = (projectId: string) => { - const projectPath = formatApiPath(path(projectId)); - const { data, refetch, loading, error } = - useApiGetter(projectPath, () => - fetcher(projectPath, 'Project Insights'), - ); - - return { data: data || placeholderData, refetch, loading, error }; -}; diff --git a/src/lib/features/project-insights/project-insights-controller.ts b/src/lib/features/project-insights/project-insights-controller.ts index 9365cc217d..4c7138e4c3 100644 --- a/src/lib/features/project-insights/project-insights-controller.ts +++ b/src/lib/features/project-insights/project-insights-controller.ts @@ -30,6 +30,7 @@ export default class ProjectInsightsController extends Controller { this.openApiService = services.openApiService; this.flagResolver = config.flagResolver; + // TODO: Remove in v8. This endpoint is deprecated and no longer used by the UI. this.route({ method: 'get', path: '/:projectId/insights', @@ -37,6 +38,7 @@ export default class ProjectInsightsController extends Controller { permission: NONE, middleware: [ this.openApiService.validPath({ + deprecated: true, tags: ['Projects'], operationId: 'getProjectInsights', summary: 'Get an overview of a project insights.', diff --git a/src/lib/features/project/project-controller.ts b/src/lib/features/project/project-controller.ts index 34d2272f3c..ba96477732 100644 --- a/src/lib/features/project/project-controller.ts +++ b/src/lib/features/project/project-controller.ts @@ -146,6 +146,7 @@ export default class ProjectController extends Controller { permission: NONE, middleware: [ this.openApiService.validPath({ + deprecated: true, tags: ['Projects'], operationId: 'getProjectDora', summary: 'Get an overview project dora metrics.',