mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-17 01:17:29 +02:00
feat: application overview ux improvements (#6371)
1. Added navigation from environments to instances 2. Last seen is now shown as TimeAgo 3. Added icons for total environments and features 4. Fixed schema 
This commit is contained in:
parent
9a12257568
commit
7af7b32bd5
@ -5,12 +5,13 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { FC, useLayoutEffect, useRef, useState } from 'react';
|
import { FC, useLayoutEffect, useRef, useState } from 'react';
|
||||||
import { ApplicationOverviewSchema } from '../../openapi';
|
import { ApplicationOverviewSchema } from '../../openapi';
|
||||||
import { useRequiredPathParam } from '../../hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from '../../hooks/useRequiredPathParam';
|
||||||
import { WarningAmberRounded } from '@mui/icons-material';
|
|
||||||
import { HelpIcon } from '../common/HelpIcon/HelpIcon';
|
import { HelpIcon } from '../common/HelpIcon/HelpIcon';
|
||||||
|
import { CloudCircle, Flag, WarningAmberRounded } from '@mui/icons-material';
|
||||||
|
import TimeAgo from 'react-timeago';
|
||||||
|
|
||||||
const StyledTable = styled('table')(({ theme }) => ({
|
const StyledTable = styled('table')(({ theme }) => ({
|
||||||
fontSize: theme.fontSizes.smallerBody,
|
fontSize: theme.fontSizes.smallerBody,
|
||||||
marginTop: theme.spacing(2),
|
marginTop: theme.spacing(1),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledCell = styled('td')(({ theme }) => ({
|
const StyledCell = styled('td')(({ theme }) => ({
|
||||||
@ -78,6 +79,23 @@ const StyledStatus = styled(Typography)<{
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const StyledIconRow = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
gap: theme.spacing(3),
|
||||||
|
color: theme.palette.secondary.main,
|
||||||
|
paddingTop: theme.spacing(2),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledIconContainer = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
gap: theme.spacing(0.5),
|
||||||
|
}));
|
||||||
|
const StyledText = styled(Box)(({ theme }) => ({
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
}));
|
||||||
|
|
||||||
const useElementWidth = () => {
|
const useElementWidth = () => {
|
||||||
const elementRef = useRef<HTMLDivElement>(null);
|
const elementRef = useRef<HTMLDivElement>(null);
|
||||||
const [width, setWidth] = useState('100%');
|
const [width, setWidth] = useState('100%');
|
||||||
@ -117,6 +135,29 @@ interface IApplicationChartProps {
|
|||||||
data: ApplicationOverviewSchema;
|
data: ApplicationOverviewSchema;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IApplicationCountersProps {
|
||||||
|
environmentCount: number;
|
||||||
|
featureCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ApplicationCounters = ({
|
||||||
|
environmentCount,
|
||||||
|
featureCount,
|
||||||
|
}: IApplicationCountersProps) => {
|
||||||
|
return (
|
||||||
|
<StyledIconRow>
|
||||||
|
<StyledIconContainer>
|
||||||
|
<CloudCircle />
|
||||||
|
<StyledText>{environmentCount}</StyledText>
|
||||||
|
</StyledIconContainer>
|
||||||
|
<StyledIconContainer>
|
||||||
|
<Flag />
|
||||||
|
<StyledText>{featureCount}</StyledText>
|
||||||
|
</StyledIconContainer>
|
||||||
|
</StyledIconRow>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const ApplicationChart = ({ data }: IApplicationChartProps) => {
|
export const ApplicationChart = ({ data }: IApplicationChartProps) => {
|
||||||
const applicationName = useRequiredPathParam('name');
|
const applicationName = useRequiredPathParam('name');
|
||||||
const { elementRef, width } = useElementWidth();
|
const { elementRef, width } = useElementWidth();
|
||||||
@ -164,6 +205,10 @@ export const ApplicationChart = ({ data }: IApplicationChartProps) => {
|
|||||||
>
|
>
|
||||||
{applicationName}
|
{applicationName}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
<ApplicationCounters
|
||||||
|
environmentCount={data.environments.length}
|
||||||
|
featureCount={data.featureCount}
|
||||||
|
/>
|
||||||
|
|
||||||
<StyledDivider />
|
<StyledDivider />
|
||||||
|
|
||||||
@ -189,6 +234,12 @@ export const ApplicationChart = ({ data }: IApplicationChartProps) => {
|
|||||||
<StyledEnvironmentBox
|
<StyledEnvironmentBox
|
||||||
mode={mode}
|
mode={mode}
|
||||||
key={environment.name}
|
key={environment.name}
|
||||||
|
sx={{ cursor: 'pointer' }}
|
||||||
|
onClick={(e) => {
|
||||||
|
navigate(
|
||||||
|
`/applications/${applicationName}/instances?environment=${environment.name}`,
|
||||||
|
);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<EnvironmentHeader>
|
<EnvironmentHeader>
|
||||||
{environment.name} environment
|
{environment.name} environment
|
||||||
@ -224,7 +275,13 @@ export const ApplicationChart = ({ data }: IApplicationChartProps) => {
|
|||||||
<tr>
|
<tr>
|
||||||
<StyledCell>Last seen:</StyledCell>
|
<StyledCell>Last seen:</StyledCell>
|
||||||
<StyledCell>
|
<StyledCell>
|
||||||
{environment.lastSeen}
|
<TimeAgo
|
||||||
|
date={
|
||||||
|
new Date(
|
||||||
|
environment.lastSeen,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
</StyledCell>
|
</StyledCell>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -12,12 +12,13 @@ const WarningContainer = styled(Box)(({ theme }) => ({
|
|||||||
const WarningHeader = styled(Box)(({ theme }) => ({
|
const WarningHeader = styled(Box)(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
padding: theme.spacing(2, 3, 2, 3),
|
padding: theme.spacing(2, 3, 2, 3),
|
||||||
alignItems: 'flex-start',
|
alignItems: 'center',
|
||||||
gap: theme.spacing(1.5),
|
gap: theme.spacing(1.5),
|
||||||
alignSelf: 'stretch',
|
alignSelf: 'stretch',
|
||||||
borderRadius: `${theme.shape.borderRadiusLarge}px ${theme.shape.borderRadiusLarge}px 0 0`,
|
borderRadius: `${theme.shape.borderRadiusLarge}px ${theme.shape.borderRadiusLarge}px 0 0`,
|
||||||
border: `1px solid ${theme.palette.warning.border}`,
|
border: `1px solid ${theme.palette.warning.border}`,
|
||||||
background: theme.palette.warning.light,
|
background: theme.palette.warning.light,
|
||||||
|
color: theme.palette.warning.main,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const SmallText = styled(Box)(({ theme }) => ({
|
const SmallText = styled(Box)(({ theme }) => ({
|
||||||
|
@ -22,7 +22,7 @@ test('Display application overview with environments', async () => {
|
|||||||
{
|
{
|
||||||
name: 'development',
|
name: 'development',
|
||||||
instanceCount: 999,
|
instanceCount: 999,
|
||||||
lastSeen: '2024-02-22T20:20:24.740',
|
lastSeen: new Date().toISOString(),
|
||||||
sdks: ['unleash-client-node:5.5.0-beta.0'],
|
sdks: ['unleash-client-node:5.5.0-beta.0'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -47,7 +47,7 @@ test('Display application overview with environments', async () => {
|
|||||||
await screen.findByText('development environment');
|
await screen.findByText('development environment');
|
||||||
await screen.findByText('999');
|
await screen.findByText('999');
|
||||||
await screen.findByText('unleash-client-node:5.5.0-beta.0');
|
await screen.findByText('unleash-client-node:5.5.0-beta.0');
|
||||||
await screen.findByText('2024-02-22T20:20:24.740');
|
await screen.findByText('1 second ago');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Display application overview without environments', async () => {
|
test('Display application overview without environments', async () => {
|
||||||
|
@ -48,6 +48,7 @@ const ApplicationOverview = () => {
|
|||||||
{data.projects.map((project) => (
|
{data.projects.map((project) => (
|
||||||
<Badge
|
<Badge
|
||||||
sx={{ cursor: 'pointer' }}
|
sx={{ cursor: 'pointer' }}
|
||||||
|
key={project}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
navigate(`/projects/${project}`);
|
navigate(`/projects/${project}`);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { FC, useEffect, useMemo, useState } from 'react';
|
import { FC, useEffect, useMemo } from 'react';
|
||||||
import { formatDateYMDHMS } from 'utils/formatDate';
|
import { formatDateYMDHMS } from 'utils/formatDate';
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import { useConnectedInstancesTable } from './useConnectedInstancesTable';
|
import { useConnectedInstancesTable } from './useConnectedInstancesTable';
|
||||||
|
@ -2,6 +2,7 @@ import { useMemo } from 'react';
|
|||||||
import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell';
|
import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell';
|
||||||
import { useTable, useGlobalFilter, useSortBy } from 'react-table';
|
import { useTable, useGlobalFilter, useSortBy } from 'react-table';
|
||||||
import { sortTypes } from 'utils/sortTypes';
|
import { sortTypes } from 'utils/sortTypes';
|
||||||
|
import { TimeAgoCell } from '../../common/Table/cells/TimeAgoCell/TimeAgoCell';
|
||||||
|
|
||||||
type ConnectedInstancesTableData = {
|
type ConnectedInstancesTableData = {
|
||||||
instanceId: string;
|
instanceId: string;
|
||||||
@ -39,7 +40,7 @@ export const useConnectedInstancesTable = (
|
|||||||
{
|
{
|
||||||
Header: 'Last seen',
|
Header: 'Last seen',
|
||||||
accessor: 'lastSeen',
|
accessor: 'lastSeen',
|
||||||
Cell: HighlightCell,
|
Cell: TimeAgoCell,
|
||||||
styles: {
|
styles: {
|
||||||
width: '20%',
|
width: '20%',
|
||||||
},
|
},
|
||||||
|
@ -28,6 +28,7 @@ export const applicationOverviewEnvironmentSchema = {
|
|||||||
},
|
},
|
||||||
lastSeen: {
|
lastSeen: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
nullable: true,
|
||||||
format: 'date-time',
|
format: 'date-time',
|
||||||
example: '2023-04-19T08:15:14.000Z',
|
example: '2023-04-19T08:15:14.000Z',
|
||||||
description: 'The last time the application environment was seen',
|
description: 'The last time the application environment was seen',
|
||||||
|
Loading…
Reference in New Issue
Block a user