mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-20 00:08:02 +01:00
feat: projects using this application (#6355)
![image](https://github.com/Unleash/unleash/assets/964450/bcb5a612-3202-46ae-be20-bd23cbd8d6e4)
This commit is contained in:
parent
c049374a25
commit
9cd324bd7c
@ -1,5 +1,5 @@
|
||||
/* eslint react/no-multi-comp:off */
|
||||
import React, { lazy, useContext, useState } from 'react';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Avatar,
|
||||
@ -31,6 +31,7 @@ import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import { ApplicationEdit } from './ApplicationEdit/ApplicationEdit';
|
||||
import ApplicationOverview from './ApplicationOverview';
|
||||
|
||||
type Tab = {
|
||||
title: string;
|
||||
@ -68,8 +69,6 @@ const StyledTab = styled(Tab)(({ theme }) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const ApplicationOverview = lazy(() => import('./ApplicationOverview'));
|
||||
|
||||
export const Application = () => {
|
||||
const useOldApplicationScreen = !useUiFlag('sdkReporting');
|
||||
const navigate = useNavigate();
|
||||
|
227
frontend/src/component/application/ApplicationChart.tsx
Normal file
227
frontend/src/component/application/ApplicationChart.tsx
Normal file
@ -0,0 +1,227 @@
|
||||
import { Box, Divider, styled, Typography, useTheme } from '@mui/material';
|
||||
import { ArcherContainer, ArcherElement } from 'react-archer';
|
||||
import { ConditionallyRender } from '../common/ConditionallyRender/ConditionallyRender';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { FC, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { ApplicationOverviewSchema } from '../../openapi';
|
||||
import { useRequiredPathParam } from '../../hooks/useRequiredPathParam';
|
||||
import { WarningAmberRounded } from '@mui/icons-material';
|
||||
|
||||
const StyledTable = styled('table')(({ theme }) => ({
|
||||
fontSize: theme.fontSizes.smallerBody,
|
||||
marginTop: theme.spacing(2),
|
||||
}));
|
||||
|
||||
const StyledCell = styled('td')(({ theme }) => ({
|
||||
verticalAlign: 'top',
|
||||
paddingLeft: 0,
|
||||
paddingRight: theme.spacing(1),
|
||||
}));
|
||||
|
||||
const StyleApplicationContainer = styled(Box)(({ theme }) => ({
|
||||
marginBottom: theme.spacing(18),
|
||||
display: 'flex',
|
||||
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 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),
|
||||
marginBottom: theme.spacing(2),
|
||||
width: '100%',
|
||||
}));
|
||||
|
||||
const StyledEnvironmentsContainer = styled(Box)({
|
||||
display: 'flex',
|
||||
justifyContent: 'start',
|
||||
gap: '20px',
|
||||
});
|
||||
|
||||
const EnvironmentHeader = styled(Typography)(({ theme }) => ({
|
||||
fontSize: theme.fontSizes.smallerBody,
|
||||
fontWeight: theme.fontWeight.bold,
|
||||
}));
|
||||
|
||||
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 useElementWidth = () => {
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
const [width, setWidth] = useState('100%');
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setWidth(`${elementRef.current?.scrollWidth}px`);
|
||||
}, [elementRef, setWidth]);
|
||||
|
||||
return {
|
||||
elementRef,
|
||||
width,
|
||||
};
|
||||
};
|
||||
const SuccessStatus = () => (
|
||||
<StyledStatus mode='success'>
|
||||
<WarningAmberRounded
|
||||
sx={(theme) => ({
|
||||
color: theme.palette.success.main,
|
||||
})}
|
||||
/>{' '}
|
||||
Everything looks good!
|
||||
</StyledStatus>
|
||||
);
|
||||
|
||||
const WarningStatus: FC = ({ children }) => (
|
||||
<StyledStatus mode='warning'>
|
||||
<WarningAmberRounded
|
||||
sx={(theme) => ({
|
||||
color: theme.palette.warning.main,
|
||||
})}
|
||||
/>{' '}
|
||||
{children}
|
||||
</StyledStatus>
|
||||
);
|
||||
|
||||
interface IApplicationChartProps {
|
||||
data: ApplicationOverviewSchema;
|
||||
}
|
||||
|
||||
export const ApplicationChart = ({ data }: IApplicationChartProps) => {
|
||||
const applicationName = useRequiredPathParam('name');
|
||||
const { elementRef, width } = useElementWidth();
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
|
||||
const mode: 'success' | 'warning' =
|
||||
data.issues.length === 0 ? 'success' : 'warning';
|
||||
|
||||
return (
|
||||
<Box sx={{ width }}>
|
||||
<ArcherContainer
|
||||
strokeColor={theme.palette.secondary.border}
|
||||
endMarker={false}
|
||||
>
|
||||
<StyleApplicationContainer>
|
||||
<ArcherElement
|
||||
id='application'
|
||||
relations={data.environments.map((environment) => ({
|
||||
targetId: environment.name,
|
||||
targetAnchor: 'top',
|
||||
sourceAnchor: 'bottom',
|
||||
style: {
|
||||
strokeColor:
|
||||
mode === 'success'
|
||||
? theme.palette.secondary.border
|
||||
: theme.palette.warning.border,
|
||||
},
|
||||
}))}
|
||||
>
|
||||
<StyledApplicationBox mode={mode}>
|
||||
<Typography
|
||||
sx={(theme) => ({
|
||||
fontSize: theme.fontSizes.smallerBody,
|
||||
})}
|
||||
color='text.secondary'
|
||||
>
|
||||
Application
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={(theme) => ({
|
||||
fontSize: theme.fontSizes.bodySize,
|
||||
fontWeight: theme.fontWeight.bold,
|
||||
})}
|
||||
>
|
||||
{applicationName}
|
||||
</Typography>
|
||||
|
||||
<StyledDivider />
|
||||
|
||||
<ConditionallyRender
|
||||
condition={mode === 'success'}
|
||||
show={<SuccessStatus />}
|
||||
elseShow={
|
||||
<WarningStatus>
|
||||
{data.issues.length} issues detected
|
||||
</WarningStatus>
|
||||
}
|
||||
/>
|
||||
</StyledApplicationBox>
|
||||
</ArcherElement>
|
||||
</StyleApplicationContainer>
|
||||
|
||||
<StyledEnvironmentsContainer ref={elementRef}>
|
||||
{data.environments.map((environment) => (
|
||||
<ArcherElement
|
||||
id={environment.name}
|
||||
key={environment.name}
|
||||
>
|
||||
<StyledEnvironmentBox
|
||||
mode={mode}
|
||||
key={environment.name}
|
||||
>
|
||||
<EnvironmentHeader>
|
||||
{environment.name} environment
|
||||
</EnvironmentHeader>
|
||||
|
||||
<StyledTable>
|
||||
<tbody>
|
||||
<tr>
|
||||
<StyledCell>Instances:</StyledCell>
|
||||
<StyledCell>
|
||||
{environment.instanceCount}
|
||||
</StyledCell>
|
||||
</tr>
|
||||
<tr>
|
||||
<StyledCell>SDK:</StyledCell>
|
||||
<StyledCell>
|
||||
{environment.sdks.map((sdk) => (
|
||||
<div key={sdk}>{sdk}</div>
|
||||
))}
|
||||
</StyledCell>
|
||||
</tr>
|
||||
<tr>
|
||||
<StyledCell>Last seen:</StyledCell>
|
||||
<StyledCell>
|
||||
{environment.lastSeen}
|
||||
</StyledCell>
|
||||
</tr>
|
||||
</tbody>
|
||||
</StyledTable>
|
||||
</StyledEnvironmentBox>
|
||||
</ArcherElement>
|
||||
))}
|
||||
</StyledEnvironmentsContainer>
|
||||
</ArcherContainer>
|
||||
</Box>
|
||||
);
|
||||
};
|
@ -6,7 +6,7 @@ import { ApplicationOverviewIssuesSchema } from 'openapi';
|
||||
const WarningContainer = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
paddingBottom: theme.spacing(8),
|
||||
alignSelf: 'stretch',
|
||||
}));
|
||||
|
||||
const WarningHeader = styled(Box)(({ theme }) => ({
|
||||
|
@ -1,73 +1,13 @@
|
||||
import { usePageTitle } from 'hooks/usePageTitle';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
Divider,
|
||||
styled,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from '@mui/material';
|
||||
import { Alert, Box, Divider, styled } from '@mui/material';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
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,
|
||||
marginTop: theme.spacing(2),
|
||||
}));
|
||||
|
||||
const StyledCell = styled('td')(({ theme }) => ({
|
||||
verticalAlign: 'top',
|
||||
paddingLeft: 0,
|
||||
paddingRight: theme.spacing(1),
|
||||
}));
|
||||
|
||||
const StyleApplicationContainer = styled(Box)(({ theme }) => ({
|
||||
marginBottom: theme.spacing(18),
|
||||
display: 'flex',
|
||||
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 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),
|
||||
}));
|
||||
import { ApplicationChart } from './ApplicationChart';
|
||||
import TopicOutlinedIcon from '@mui/icons-material/TopicOutlined';
|
||||
import { Badge } from '../common/Badge/Badge';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const StyledDivider = styled(Divider)(({ theme }) => ({
|
||||
marginTop: theme.spacing(2),
|
||||
@ -75,203 +15,54 @@ const StyledDivider = styled(Divider)(({ theme }) => ({
|
||||
width: '100%',
|
||||
}));
|
||||
|
||||
const StyledEnvironmentsContainer = styled(Box)({
|
||||
const ApplicationContainer = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
justifyContent: 'start',
|
||||
gap: '20px',
|
||||
});
|
||||
|
||||
const EnvironmentHeader = styled(Typography)(({ theme }) => ({
|
||||
fontSize: theme.fontSizes.smallerBody,
|
||||
fontWeight: theme.fontWeight.bold,
|
||||
padding: theme.spacing(1),
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing(2),
|
||||
alignSelf: 'stretch',
|
||||
}));
|
||||
|
||||
const SuccessStatus = () => (
|
||||
<StyledStatus mode='success'>
|
||||
<WarningAmberRounded
|
||||
sx={(theme) => ({
|
||||
color: theme.palette.success.main,
|
||||
})}
|
||||
/>{' '}
|
||||
Everything looks good!
|
||||
</StyledStatus>
|
||||
);
|
||||
const ProjectContainer = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing(2),
|
||||
alignSelf: 'stretch',
|
||||
}));
|
||||
|
||||
const WarningStatus: FC = ({ children }) => (
|
||||
<StyledStatus mode='warning'>
|
||||
<WarningAmberRounded
|
||||
sx={(theme) => ({
|
||||
color: theme.palette.warning.main,
|
||||
})}
|
||||
/>{' '}
|
||||
{children}
|
||||
</StyledStatus>
|
||||
);
|
||||
|
||||
const useElementWidth = () => {
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
const [width, setWidth] = useState('100%');
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setWidth(`${elementRef.current?.scrollWidth}px`);
|
||||
}, [elementRef, setWidth]);
|
||||
|
||||
return {
|
||||
elementRef,
|
||||
width,
|
||||
};
|
||||
};
|
||||
|
||||
export const ApplicationOverview = () => {
|
||||
const ApplicationOverview = () => {
|
||||
usePageTitle('Applications - Overview');
|
||||
const applicationName = useRequiredPathParam('name');
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const { data, loading } = useApplicationOverview(applicationName);
|
||||
|
||||
// @ts-ignore
|
||||
window.navigateToInstances = (environment: string) => {
|
||||
navigate(
|
||||
`/applications/${applicationName}/instances?environment=${environment}`,
|
||||
);
|
||||
};
|
||||
|
||||
const { elementRef, width } = useElementWidth();
|
||||
|
||||
const mode: 'success' | 'warning' =
|
||||
data.issues.length === 0 ? 'success' : 'warning';
|
||||
|
||||
return (
|
||||
<ConditionallyRender
|
||||
condition={!loading && data.environments.length === 0}
|
||||
show={<Alert severity='warning'>No data available.</Alert>}
|
||||
elseShow={
|
||||
<>
|
||||
<ApplicationContainer>
|
||||
<ProjectContainer>
|
||||
Projects using this application
|
||||
{data.projects.map((project) => (
|
||||
<Badge
|
||||
sx={{ cursor: 'pointer' }}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate(`/projects/${project}`);
|
||||
}}
|
||||
color='secondary'
|
||||
icon={<TopicOutlinedIcon />}
|
||||
>
|
||||
{project}
|
||||
</Badge>
|
||||
))}
|
||||
</ProjectContainer>
|
||||
<StyledDivider />
|
||||
<ApplicationIssues issues={data.issues} />
|
||||
<Box sx={{ width }}>
|
||||
<ArcherContainer
|
||||
strokeColor={theme.palette.secondary.border}
|
||||
endMarker={false}
|
||||
>
|
||||
<StyleApplicationContainer>
|
||||
<ArcherElement
|
||||
id='application'
|
||||
relations={data.environments.map(
|
||||
(environment) => ({
|
||||
targetId: environment.name,
|
||||
targetAnchor: 'top',
|
||||
sourceAnchor: 'bottom',
|
||||
style: {
|
||||
strokeColor:
|
||||
mode === 'success'
|
||||
? theme.palette
|
||||
.secondary.border
|
||||
: theme.palette.warning
|
||||
.border,
|
||||
},
|
||||
}),
|
||||
)}
|
||||
>
|
||||
<StyledApplicationBox mode={mode}>
|
||||
<Typography
|
||||
sx={(theme) => ({
|
||||
fontSize:
|
||||
theme.fontSizes.smallerBody,
|
||||
})}
|
||||
color='text.secondary'
|
||||
>
|
||||
Application
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={(theme) => ({
|
||||
fontSize:
|
||||
theme.fontSizes.bodySize,
|
||||
fontWeight:
|
||||
theme.fontWeight.bold,
|
||||
})}
|
||||
>
|
||||
{applicationName}
|
||||
</Typography>
|
||||
|
||||
<StyledDivider />
|
||||
|
||||
<ConditionallyRender
|
||||
condition={mode === 'success'}
|
||||
show={<SuccessStatus />}
|
||||
elseShow={
|
||||
<WarningStatus>
|
||||
{data.issues.length} issues
|
||||
detected
|
||||
</WarningStatus>
|
||||
}
|
||||
/>
|
||||
</StyledApplicationBox>
|
||||
</ArcherElement>
|
||||
</StyleApplicationContainer>
|
||||
|
||||
<StyledEnvironmentsContainer ref={elementRef}>
|
||||
{data.environments.map((environment) => (
|
||||
<ArcherElement
|
||||
id={environment.name}
|
||||
key={environment.name}
|
||||
>
|
||||
<StyledEnvironmentBox
|
||||
mode={mode}
|
||||
key={environment.name}
|
||||
>
|
||||
<EnvironmentHeader>
|
||||
{environment.name} environment
|
||||
</EnvironmentHeader>
|
||||
|
||||
<StyledTable>
|
||||
<tbody>
|
||||
<tr>
|
||||
<StyledCell>
|
||||
Instances:
|
||||
</StyledCell>
|
||||
<StyledCell>
|
||||
{
|
||||
environment.instanceCount
|
||||
}
|
||||
</StyledCell>
|
||||
</tr>
|
||||
<tr>
|
||||
<StyledCell>
|
||||
SDK:
|
||||
</StyledCell>
|
||||
<StyledCell>
|
||||
{environment.sdks.map(
|
||||
(sdk) => (
|
||||
<div
|
||||
key={
|
||||
sdk
|
||||
}
|
||||
>
|
||||
{sdk}
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</StyledCell>
|
||||
</tr>
|
||||
<tr>
|
||||
<StyledCell>
|
||||
Last seen:
|
||||
</StyledCell>
|
||||
<StyledCell>
|
||||
{
|
||||
environment.lastSeen
|
||||
}
|
||||
</StyledCell>
|
||||
</tr>
|
||||
</tbody>
|
||||
</StyledTable>
|
||||
</StyledEnvironmentBox>
|
||||
</ArcherElement>
|
||||
))}
|
||||
</StyledEnvironmentsContainer>
|
||||
</ArcherContainer>
|
||||
</Box>
|
||||
</>
|
||||
<ApplicationChart data={data} />
|
||||
</ApplicationContainer>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user