From 7cebf7b8feca548041fc4a2493864a8916d888c8 Mon Sep 17 00:00:00 2001 From: Jaanus Sellin Date: Tue, 27 Feb 2024 09:57:50 +0200 Subject: [PATCH] feat: application issues (#6347) ![image](https://github.com/Unleash/unleash/assets/964450/90153533-322c-46fd-8a1b-5853cbe0c35c) --- .../ApplicationIssues.test.tsx | 27 ++ .../ApplicationIssues/ApplicationIssues.tsx | 117 +++++++ .../ApplicationList/ApplicationList.tsx | 2 +- .../ApplicationUsageCell.tsx | 2 +- .../PaginatedApplicationList.test.tsx | 2 +- .../PaginatedApplicationList.tsx | 2 +- .../application/ApplicationOverview.test.tsx | 2 +- .../application/ApplicationOverview.tsx | 312 +++++++++--------- .../executiveDashboard/ExecutiveDashboard.tsx | 2 +- .../feedbackNew/FeedbackComponent.tsx | 2 +- .../component/feedbackNew/FeedbackList.tsx | 2 +- .../ProjectApplications.test.tsx | 2 +- .../ProjectApplications.tsx | 2 +- .../useApplicationOverview.ts | 8 +- 14 files changed, 325 insertions(+), 159 deletions(-) create mode 100644 frontend/src/component/application/ApplicationIssues/ApplicationIssues.test.tsx create mode 100644 frontend/src/component/application/ApplicationIssues/ApplicationIssues.tsx diff --git a/frontend/src/component/application/ApplicationIssues/ApplicationIssues.test.tsx b/frontend/src/component/application/ApplicationIssues/ApplicationIssues.test.tsx new file mode 100644 index 0000000000..1a185de7af --- /dev/null +++ b/frontend/src/component/application/ApplicationIssues/ApplicationIssues.test.tsx @@ -0,0 +1,27 @@ +import { screen } from '@testing-library/react'; +import { render } from 'utils/testRenderer'; +import { ApplicationIssues } from './ApplicationIssues'; +import { ApplicationOverviewIssuesSchema } from 'openapi'; + +test('Display all application issues', async () => { + const issues: ApplicationOverviewIssuesSchema[] = [ + { + type: 'missingFeatures', + items: ['my-app'], + }, + { + type: 'missingStrategies', + items: ['defaultStrategy', 'mainStrategy'], + }, + ]; + render(); + + await screen.findByText('my-app'); + await screen.findByText('mainStrategy'); + await screen.findByText( + `We detected 1 feature flag defined in the SDK that does not exist in Unleash`, + ); + await screen.findByText( + `We detected 2 strategy types defined in the SDK that do not exist in Unleash`, + ); +}); diff --git a/frontend/src/component/application/ApplicationIssues/ApplicationIssues.tsx b/frontend/src/component/application/ApplicationIssues/ApplicationIssues.tsx new file mode 100644 index 0000000000..6deae0987c --- /dev/null +++ b/frontend/src/component/application/ApplicationIssues/ApplicationIssues.tsx @@ -0,0 +1,117 @@ +import { Box, styled } from '@mui/material'; +import { ConditionallyRender } from '../../common/ConditionallyRender/ConditionallyRender'; +import { WarningAmberRounded } from '@mui/icons-material'; +import { ApplicationOverviewIssuesSchema } from 'openapi'; + +const WarningContainer = styled(Box)(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + paddingBottom: theme.spacing(8), +})); + +const WarningHeader = styled(Box)(({ theme }) => ({ + display: 'flex', + padding: theme.spacing(2, 3, 2, 3), + alignItems: 'flex-start', + gap: theme.spacing(1.5), + alignSelf: 'stretch', + borderRadius: `${theme.shape.borderRadiusLarge}px ${theme.shape.borderRadiusLarge}px 0 0`, + border: `1px solid ${theme.palette.warning.border}`, + background: theme.palette.warning.light, +})); + +const SmallText = styled(Box)(({ theme }) => ({ + fontSize: theme.fontSizes.smallBody, +})); + +const WarningHeaderText = styled(SmallText)(({ theme }) => ({ + color: theme.palette.warning.dark, + fontWeight: theme.fontWeight.bold, +})); + +const StyledList = styled('ul')(({ theme }) => ({ + padding: theme.spacing(0, 0, 0, 2), +})); + +const StyledListElement = styled('li')(({ theme }) => ({ + fontWeight: theme.fontWeight.bold, + fontSize: theme.fontSizes.smallBody, +})); + +const IssueContainer = styled(Box)(({ theme }) => ({ + display: 'flex', + padding: theme.spacing(3), + flexDirection: 'column', + alignItems: 'flex-start', + alignSelf: 'stretch', + gap: theme.spacing(3), + borderRadius: ` 0 0 ${theme.shape.borderRadiusLarge}px ${theme.shape.borderRadiusLarge}px`, + border: `1px solid ${theme.palette.warning.border}`, +})); + +const IssueTextContainer = styled(Box)(({ theme }) => ({ + display: 'flex', + padding: theme.spacing(2), + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'flex-start', + alignSelf: 'stretch', + gap: theme.spacing(0.5), + borderRadius: theme.spacing(1), + border: `1px solid ${theme.palette.divider}`, +})); + +export interface IApplicationIssuesProps { + issues: ApplicationOverviewIssuesSchema[]; +} + +const resolveIssueText = (issue: ApplicationOverviewIssuesSchema) => { + const issueCount = issue.items.length; + let issueText = ''; + + switch (issue.type) { + case 'missingFeatures': + issueText = `feature flag${issueCount !== 1 ? 's' : ''}`; + break; + case 'missingStrategies': + issueText = `strategy type${issueCount !== 1 ? 's' : ''}`; + break; + } + + return `We detected ${issueCount} ${issueText} defined in the SDK that ${ + issueCount !== 1 ? 'do' : 'does' + } not exist in Unleash`; +}; + +export const ApplicationIssues = ({ issues }: IApplicationIssuesProps) => { + return ( + 0} + show={ + + + + + We detected {issues.length} issues in this + application + + + + {issues.map((issue) => ( + + {resolveIssueText(issue)} + + {issue.items.map((item) => ( + + {item} + + ))} + + + ))} + + + } + /> + ); +}; diff --git a/frontend/src/component/application/ApplicationList/ApplicationList.tsx b/frontend/src/component/application/ApplicationList/ApplicationList.tsx index c316390430..dff71ee6c6 100644 --- a/frontend/src/component/application/ApplicationList/ApplicationList.tsx +++ b/frontend/src/component/application/ApplicationList/ApplicationList.tsx @@ -20,7 +20,7 @@ import { sortTypes } from 'utils/sortTypes'; import { IconCell } from 'component/common/Table/cells/IconCell/IconCell'; import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell'; import { ApplicationUsageCell } from './ApplicationUsageCell/ApplicationUsageCell'; -import { ApplicationSchema } from '../../../openapi'; +import { ApplicationSchema } from 'openapi'; export const ApplicationList = () => { const { applications: data, loading } = useApplications(); diff --git a/frontend/src/component/application/ApplicationList/ApplicationUsageCell/ApplicationUsageCell.tsx b/frontend/src/component/application/ApplicationList/ApplicationUsageCell/ApplicationUsageCell.tsx index 85b2e00572..2b69dd90dc 100644 --- a/frontend/src/component/application/ApplicationList/ApplicationUsageCell/ApplicationUsageCell.tsx +++ b/frontend/src/component/application/ApplicationList/ApplicationUsageCell/ApplicationUsageCell.tsx @@ -2,7 +2,7 @@ import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { styled, Typography, useTheme } from '@mui/material'; import { Link } from 'react-router-dom'; -import { ApplicationUsageSchema } from '../../../../openapi'; +import { ApplicationUsageSchema } from 'openapi'; export interface IApplicationUsageCellProps { usage: ApplicationUsageSchema[] | undefined; diff --git a/frontend/src/component/application/ApplicationList/PaginatedApplicationList.test.tsx b/frontend/src/component/application/ApplicationList/PaginatedApplicationList.test.tsx index 67d9bb3d81..86ad377c8f 100644 --- a/frontend/src/component/application/ApplicationList/PaginatedApplicationList.test.tsx +++ b/frontend/src/component/application/ApplicationList/PaginatedApplicationList.test.tsx @@ -2,7 +2,7 @@ import { screen } from '@testing-library/react'; import { render } from 'utils/testRenderer'; import { testServerRoute, testServerSetup } from 'utils/testServer'; import { PaginatedApplicationList } from './PaginatedApplicationList'; -import { ApplicationSchema } from '../../../openapi'; +import { ApplicationSchema } from 'openapi'; const server = testServerSetup(); diff --git a/frontend/src/component/application/ApplicationList/PaginatedApplicationList.tsx b/frontend/src/component/application/ApplicationList/PaginatedApplicationList.tsx index b81727336f..717fa03623 100644 --- a/frontend/src/component/application/ApplicationList/PaginatedApplicationList.tsx +++ b/frontend/src/component/application/ApplicationList/PaginatedApplicationList.tsx @@ -12,7 +12,7 @@ import { PaginatedTable } from 'component/common/Table'; import { IconCell } from 'component/common/Table/cells/IconCell/IconCell'; import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell'; import { ApplicationUsageCell } from './ApplicationUsageCell/ApplicationUsageCell'; -import { ApplicationSchema } from '../../../openapi'; +import { ApplicationSchema } from 'openapi'; import { encodeQueryParams, NumberParam, diff --git a/frontend/src/component/application/ApplicationOverview.test.tsx b/frontend/src/component/application/ApplicationOverview.test.tsx index ca037f0b91..7eea7239bf 100644 --- a/frontend/src/component/application/ApplicationOverview.test.tsx +++ b/frontend/src/component/application/ApplicationOverview.test.tsx @@ -2,7 +2,7 @@ import { screen } from '@testing-library/react'; import { render } from 'utils/testRenderer'; import { testServerRoute, testServerSetup } from 'utils/testServer'; import { Route, Routes } from 'react-router-dom'; -import { ApplicationOverviewSchema } from '../../openapi'; +import { ApplicationOverviewSchema } from 'openapi'; import ApplicationOverview from './ApplicationOverview'; const server = testServerSetup(); diff --git a/frontend/src/component/application/ApplicationOverview.tsx b/frontend/src/component/application/ApplicationOverview.tsx index a6d98c3c29..c1c74f2e68 100644 --- a/frontend/src/component/application/ApplicationOverview.tsx +++ b/frontend/src/component/application/ApplicationOverview.tsx @@ -10,11 +10,11 @@ import { } from '@mui/material'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useNavigate } from 'react-router-dom'; -import Check from '@mui/icons-material/CheckCircle'; -import Warning from '@mui/icons-material/Warning'; import { ArcherContainer, ArcherElement } from 'react-archer'; import { FC, useLayoutEffect, useRef, useState } from 'react'; import { useApplicationOverview } from 'hooks/api/getters/useApplicationOverview/useApplicationOverview'; +import { WarningAmberRounded } from '@mui/icons-material'; +import { ApplicationIssues } from './ApplicationIssues/ApplicationIssues'; const StyledTable = styled('table')(({ theme }) => ({ fontSize: theme.fontSizes.smallerBody, @@ -33,41 +33,41 @@ const StyleApplicationContainer = styled(Box)(({ theme }) => ({ justifyContent: 'center', })); -const StyledApplicationBox = styled(Box)<{ mode: 'success' | 'warning' }>( - ({ theme, mode }) => ({ - borderRadius: theme.shape.borderRadiusMedium, - border: '1px solid', - borderColor: theme.palette[mode].border, - backgroundColor: theme.palette[mode].light, - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - padding: theme.spacing(1.5, 3, 2, 3), - }), -); +const StyledApplicationBox = styled(Box)<{ + mode: 'success' | 'warning'; +}>(({ theme, mode }) => ({ + borderRadius: theme.shape.borderRadiusMedium, + border: '1px solid', + borderColor: theme.palette[mode].border, + backgroundColor: theme.palette[mode].light, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + padding: theme.spacing(1.5, 3, 2, 3), +})); -const StyledStatus = styled(Typography)<{ mode: 'success' | 'warning' }>( - ({ theme, mode }) => ({ - gap: theme.spacing(1), - fontSize: theme.fontSizes.smallBody, - color: theme.palette[mode].dark, - display: 'flex', - alignItems: 'center', - }), -); +const StyledStatus = styled(Typography)<{ + mode: 'success' | 'warning'; +}>(({ theme, mode }) => ({ + gap: theme.spacing(1), + fontSize: theme.fontSizes.smallBody, + color: theme.palette[mode].dark, + display: 'flex', + alignItems: 'center', +})); -const StyledEnvironmentBox = styled(Box)<{ mode: 'success' | 'warning' }>( - ({ theme, mode }) => ({ - borderRadius: theme.shape.borderRadiusMedium, - border: '1px solid', - borderColor: - theme.palette[mode === 'success' ? 'secondary' : 'warning'].border, - backgroundColor: - theme.palette[mode === 'success' ? 'secondary' : 'warning'].light, - display: 'inline-block', - padding: theme.spacing(1.5, 1.5, 1.5, 1.5), - }), -); +const StyledEnvironmentBox = styled(Box)<{ + mode: 'success' | 'warning'; +}>(({ theme, mode }) => ({ + borderRadius: theme.shape.borderRadiusMedium, + border: '1px solid', + borderColor: + theme.palette[mode === 'success' ? 'secondary' : 'warning'].border, + backgroundColor: + theme.palette[mode === 'success' ? 'secondary' : 'warning'].light, + display: 'inline-block', + padding: theme.spacing(1.5, 1.5, 1.5, 1.5), +})); const StyledDivider = styled(Divider)(({ theme }) => ({ marginTop: theme.spacing(2), @@ -88,7 +88,7 @@ const EnvironmentHeader = styled(Typography)(({ theme }) => ({ const SuccessStatus = () => ( - ({ color: theme.palette.success.main, })} @@ -99,7 +99,7 @@ const SuccessStatus = () => ( const WarningStatus: FC = ({ children }) => ( - ({ color: theme.palette.warning.main, })} @@ -116,7 +116,10 @@ const useElementWidth = () => { setWidth(`${elementRef.current?.scrollWidth}px`); }, [elementRef, setWidth]); - return { elementRef, width }; + return { + elementRef, + width, + }; }; export const ApplicationOverview = () => { @@ -135,127 +138,140 @@ export const ApplicationOverview = () => { const { elementRef, width } = useElementWidth(); - const mode: 'success' | 'warning' = 'success'; + const mode: 'success' | 'warning' = + data.issues.length === 0 ? 'success' : 'warning'; return ( No data available.} elseShow={ - - - - ({ - targetId: environment.name, - targetAnchor: 'top', - sourceAnchor: 'bottom', - style: { - strokeColor: - mode === 'success' - ? theme.palette.secondary - .border - : theme.palette.warning - .border, - }, - }), - )} - > - - ({ - fontSize: - theme.fontSizes.smallerBody, - })} - color='text.secondary' - > - Application - - ({ - fontSize: theme.fontSizes.bodySize, - fontWeight: theme.fontWeight.bold, - })} - > - {applicationName} - - - - - } - elseShow={ - - 3 issues detected - - } - /> - - - - - - {data.environments.map((environment) => ( + <> + + + + ({ + targetId: environment.name, + targetAnchor: 'top', + sourceAnchor: 'bottom', + style: { + strokeColor: + mode === 'success' + ? theme.palette + .secondary.border + : theme.palette.warning + .border, + }, + }), + )} > - + ({ + fontSize: + theme.fontSizes.smallerBody, + })} + color='text.secondary' + > + Application + + ({ + fontSize: + theme.fontSizes.bodySize, + fontWeight: + theme.fontWeight.bold, + })} + > + {applicationName} + + + + + } + elseShow={ + + {data.issues.length} issues + detected + + } + /> + + + + + + {data.environments.map((environment) => ( + - - {environment.name} environment - + + + {environment.name} environment + - - - - - Instances: - - - { - environment.instanceCount - } - - - - - SDK: - - - {environment.sdks.map( - (sdk) => ( -
- {sdk} -
- ), - )} -
- - - - Last seen: - - - {environment.lastSeen} - - - -
-
-
- ))} -
-
-
+ + + + + Instances: + + + { + environment.instanceCount + } + + + + + SDK: + + + {environment.sdks.map( + (sdk) => ( +
+ {sdk} +
+ ), + )} +
+ + + + Last seen: + + + { + environment.lastSeen + } + + + +
+ + + ))} +
+
+
+ } /> ); diff --git a/frontend/src/component/executiveDashboard/ExecutiveDashboard.tsx b/frontend/src/component/executiveDashboard/ExecutiveDashboard.tsx index d517208ac9..c946a45c93 100644 --- a/frontend/src/component/executiveDashboard/ExecutiveDashboard.tsx +++ b/frontend/src/component/executiveDashboard/ExecutiveDashboard.tsx @@ -22,7 +22,7 @@ import { MetricsSummaryChart } from './MetricsSummaryChart/MetricsSummaryChart'; import { ExecutiveSummarySchemaMetricsSummaryTrendsItem, ExecutiveSummarySchemaProjectFlagTrendsItem, -} from '../../openapi'; +} from 'openapi'; import { HealthStats } from './HealthStats/HealthStats'; import { Badge } from 'component/common/Badge/Badge'; diff --git a/frontend/src/component/feedbackNew/FeedbackComponent.tsx b/frontend/src/component/feedbackNew/FeedbackComponent.tsx index 28db21f045..4892414389 100644 --- a/frontend/src/component/feedbackNew/FeedbackComponent.tsx +++ b/frontend/src/component/feedbackNew/FeedbackComponent.tsx @@ -12,7 +12,7 @@ import { useFeedbackContext } from './useFeedback'; import React, { useState } from 'react'; import CloseIcon from '@mui/icons-material/Close'; import useToast from 'hooks/useToast'; -import { ProvideFeedbackSchema } from '../../openapi'; +import { ProvideFeedbackSchema } from 'openapi'; import { useUserFeedbackApi } from 'hooks/api/actions/useUserFeedbackApi/useUserFeedbackApi'; import { useUserSubmittedFeedback } from 'hooks/useSubmittedFeedback'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; diff --git a/frontend/src/component/feedbackNew/FeedbackList.tsx b/frontend/src/component/feedbackNew/FeedbackList.tsx index 94315566d5..8c47a78d45 100644 --- a/frontend/src/component/feedbackNew/FeedbackList.tsx +++ b/frontend/src/component/feedbackNew/FeedbackList.tsx @@ -13,7 +13,7 @@ import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightC import { useSearch } from 'hooks/useSearch'; import theme from 'themes/theme'; import { useState } from 'react'; -import { FeedbackSchema } from '../../openapi'; +import { FeedbackSchema } from 'openapi'; interface IFeedbackSchemaCellProps { value?: string | null; // FIXME: proper type diff --git a/frontend/src/component/project/ProjectApplications/ProjectApplications.test.tsx b/frontend/src/component/project/ProjectApplications/ProjectApplications.test.tsx index 3b1236d452..216622abb0 100644 --- a/frontend/src/component/project/ProjectApplications/ProjectApplications.test.tsx +++ b/frontend/src/component/project/ProjectApplications/ProjectApplications.test.tsx @@ -2,7 +2,7 @@ import { screen } from '@testing-library/react'; import { render } from 'utils/testRenderer'; import { testServerRoute, testServerSetup } from 'utils/testServer'; import { ProjectApplications } from './ProjectApplications'; -import { ProjectApplicationSchema } from '../../../openapi'; +import { ProjectApplicationSchema } from 'openapi'; import { Route, Routes } from 'react-router-dom'; import { SEARCH_INPUT } from 'utils/testIds'; diff --git a/frontend/src/component/project/ProjectApplications/ProjectApplications.tsx b/frontend/src/component/project/ProjectApplications/ProjectApplications.tsx index 703a3935a7..3b54214eeb 100644 --- a/frontend/src/component/project/ProjectApplications/ProjectApplications.tsx +++ b/frontend/src/component/project/ProjectApplications/ProjectApplications.tsx @@ -19,7 +19,7 @@ import useLoading from 'hooks/useLoading'; import { createColumnHelper, useReactTable } from '@tanstack/react-table'; import { withTableState } from 'utils/withTableState'; import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell'; -import { ProjectApplicationSchema } from '../../../openapi'; +import { ProjectApplicationSchema } from 'openapi'; import mapValues from 'lodash.mapvalues'; import { DEFAULT_PAGE_LIMIT, diff --git a/frontend/src/hooks/api/getters/useApplicationOverview/useApplicationOverview.ts b/frontend/src/hooks/api/getters/useApplicationOverview/useApplicationOverview.ts index ab1529ebb4..27a2681d0d 100644 --- a/frontend/src/hooks/api/getters/useApplicationOverview/useApplicationOverview.ts +++ b/frontend/src/hooks/api/getters/useApplicationOverview/useApplicationOverview.ts @@ -3,6 +3,12 @@ import { formatApiPath } from 'utils/formatPath'; import handleErrorResponses from '../httpErrorResponseHandler'; import { ApplicationOverviewSchema } from 'openapi'; +const placeHolderApplication: ApplicationOverviewSchema = { + environments: [], + featureCount: 0, + projects: [], + issues: [], +}; export const useApplicationOverview = ( application: string, options: SWRConfiguration = {}, @@ -17,7 +23,7 @@ export const useApplicationOverview = ( ); return { - data: data || { environments: [], featureCount: 0, projects: [] }, + data: data || placeHolderApplication, error, loading: !error && !data, };