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,
};