1
0
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:
Jaanus Sellin 2024-02-27 12:24:44 +02:00 committed by GitHub
parent c049374a25
commit 9cd324bd7c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 268 additions and 251 deletions

View File

@ -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();

View 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>
);
};

View File

@ -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 }) => ({

View File

@ -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>
}
/>
);