Skip to content

Commit 11df615

Browse files
committed
refactor: Modularize Dashboard component
- Extract SystemOverviewStats component - Rename and enhance EndpointFilters component - Improve component separation and readability
1 parent 74b93fc commit 11df615

File tree

7 files changed

+453
-363
lines changed

7 files changed

+453
-363
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { Box } from '@chakra-ui/react';
2+
import type { LiveStatusItem } from '@/api/types';
3+
import { StatusTable } from './StatusTable';
4+
import { StatusGroupAccordion } from './StatusGroupAccordion';
5+
import { GroupAccordion } from './GroupAccordion';
6+
import { StatusAccordion } from './StatusAccordion';
7+
8+
type GroupedEndpointsType =
9+
| LiveStatusItem[]
10+
| Record<string, LiveStatusItem[]>
11+
| Record<string, Record<string, LiveStatusItem[]>>;
12+
13+
type EndpointAccordionProps = {
14+
groupedEndpoints: GroupedEndpointsType | null;
15+
isLoading: boolean;
16+
groupByOptions: string[];
17+
};
18+
19+
function isGroupedByStatusAndGroup(
20+
endpoints: any
21+
): endpoints is Record<string, Record<string, LiveStatusItem[]>> {
22+
return (
23+
typeof endpoints === 'object' &&
24+
!Array.isArray(endpoints) &&
25+
Object.values(endpoints).every(
26+
value =>
27+
typeof value === 'object' &&
28+
!Array.isArray(value) &&
29+
Object.values(value as any).every(Array.isArray)
30+
)
31+
);
32+
}
33+
34+
export function EndpointAccordion({
35+
groupedEndpoints,
36+
isLoading,
37+
groupByOptions,
38+
}: EndpointAccordionProps) {
39+
return (
40+
<>
41+
{groupedEndpoints !== null ? (
42+
Array.isArray(groupedEndpoints) ? (
43+
<Box pb={4}>
44+
<StatusTable items={groupedEndpoints} isLoading={isLoading} />
45+
</Box>
46+
) : isGroupedByStatusAndGroup(groupedEndpoints) ? (
47+
<StatusGroupAccordion groupedEndpoints={groupedEndpoints} isLoading={isLoading} />
48+
) : groupByOptions.includes('group') ? (
49+
<GroupAccordion groupedEndpoints={groupedEndpoints} isLoading={isLoading} />
50+
) : groupByOptions.includes('status') ? (
51+
<StatusAccordion groupedEndpoints={groupedEndpoints} isLoading={isLoading} />
52+
) : (
53+
<StatusTable items={Object.values(groupedEndpoints).flat()} isLoading={isLoading} />
54+
)
55+
) : (
56+
<StatusTable items={groupedEndpoints} isLoading={isLoading} />
57+
)}
58+
</>
59+
);
60+
}

thingconnect.pulse.client/src/components/status/StatusFilters.tsx renamed to thingconnect.pulse.client/src/components/status/EndpointFilters.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { LiveStatusParams } from '@/api/types';
44
import { MdSearch, MdExpandMore } from 'react-icons/md';
55
import { ComboboxSelect } from '../common/ComboboxSelect';
66

7-
interface StatusFiltersProps {
7+
interface EndpointFiltersProps {
88
filters: LiveStatusParams;
99
onFiltersChange: (filters: LiveStatusParams & { group?: string }) => void; // single group
1010
groups?: {
@@ -20,7 +20,7 @@ interface StatusFiltersProps {
2020
onToggleGroupBy?: (groupBy: string, isSelected: boolean) => void;
2121
}
2222

23-
export function StatusFilters({
23+
export function EndpointFilters({
2424
filters,
2525
onFiltersChange,
2626
groups = [],
@@ -30,7 +30,7 @@ export function StatusFilters({
3030
onToggleGroupBy,
3131
selectedGroup,
3232
onSelectedGroupChange,
33-
}: StatusFiltersProps) {
33+
}: EndpointFiltersProps) {
3434
const handleGroupChange = (value: string) => {
3535
const newGroup = value || undefined;
3636
onSelectedGroupChange?.(newGroup);
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Accordion, Box, HStack, Text } from '@chakra-ui/react';
2+
import type { LiveStatusItem } from '@/api/types';
3+
import { StatusTable } from './StatusTable';
4+
5+
type Props = {
6+
groupedEndpoints: Record<string, LiveStatusItem[]>;
7+
isLoading: boolean;
8+
};
9+
10+
export function GroupAccordion({ groupedEndpoints, isLoading }: Props) {
11+
return (
12+
<Accordion.Root multiple variant='plain'>
13+
{Object.entries(groupedEndpoints).map(([group, items]) => {
14+
const typedItems = items as LiveStatusItem[] | Record<string, LiveStatusItem[]>;
15+
const itemsArray = Array.isArray(typedItems)
16+
? typedItems
17+
: Object.values(typedItems).flat();
18+
19+
return (
20+
<Accordion.Item key={group} value={group} my={2}>
21+
<Accordion.ItemTrigger borderWidth={1} _dark={{ borderColor: 'gray.200' }}>
22+
<HStack w='full' justify='space-between'>
23+
<HStack px={'10px'}>
24+
<Accordion.ItemIndicator fontSize={'md'} fontWeight={'bolder'} />
25+
<Text fontSize='sm' fontWeight='semibold' pl={4}>
26+
{group}
27+
</Text>
28+
<Text fontSize='12px' color={'gray.400'} px={2}>
29+
{items?.length
30+
? items?.length > 1
31+
? `${items?.length} Endpoints`
32+
: '1 Endpoint'
33+
: 'No Endpoints'}
34+
</Text>
35+
</HStack>
36+
</HStack>
37+
</Accordion.ItemTrigger>
38+
<Accordion.ItemContent borderWidth={1}>
39+
<Accordion.ItemBody>
40+
<Box pl={10}>
41+
<StatusTable items={itemsArray} isLoading={isLoading} />
42+
</Box>
43+
</Accordion.ItemBody>
44+
</Accordion.ItemContent>
45+
</Accordion.Item>
46+
);
47+
})}
48+
</Accordion.Root>
49+
);
50+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { Accordion, Box, Flex, HStack, Text } from '@chakra-ui/react';
2+
import type { LiveStatusItem } from '@/api/types';
3+
import { StatusTable } from './StatusTable';
4+
5+
type Props = {
6+
groupedEndpoints: Record<'up' | 'down' | 'flapping', LiveStatusItem[]>;
7+
isLoading: boolean;
8+
};
9+
10+
export function StatusAccordion({ groupedEndpoints, isLoading }: Props) {
11+
return (
12+
<Accordion.Root multiple variant='plain'>
13+
{Object.entries(groupedEndpoints).map(([status, items]) => {
14+
const typedItems = items as LiveStatusItem[] | Record<string, LiveStatusItem[]>;
15+
const itemsArray = Array.isArray(typedItems)
16+
? typedItems
17+
: Object.values(typedItems).flat();
18+
19+
const statusColorMap: Record<'up' | 'down' | 'flapping', string> = {
20+
up: 'green',
21+
down: 'red',
22+
flapping: 'yellow',
23+
} as const;
24+
25+
return (
26+
<Accordion.Item key={status} value={status} my={2}>
27+
<Accordion.ItemTrigger
28+
bg={`${statusColorMap[status as 'up' | 'down' | 'flapping']}.100`}
29+
_dark={{
30+
bg: `${statusColorMap[status as 'up' | 'down' | 'flapping']}.900`,
31+
borderColor: `${statusColorMap[status as 'up' | 'down' | 'flapping']}.800`,
32+
}}
33+
borderColor={`${statusColorMap[status as 'up' | 'down' | 'flapping']}.200`}
34+
borderWidth={1}
35+
>
36+
<HStack w='full' justify='space-between'>
37+
<HStack px={'10px'}>
38+
<Accordion.ItemIndicator
39+
fontSize={'md'}
40+
fontWeight={'bolder'}
41+
color={`${statusColorMap[status as 'up' | 'down' | 'flapping']}.600`}
42+
/>
43+
<Flex
44+
as='span'
45+
bg={`${statusColorMap[status as 'up' | 'down' | 'flapping']}.200`}
46+
textTransform='uppercase'
47+
borderRadius='30px'
48+
px={4}
49+
py={1}
50+
fontSize='11px'
51+
alignItems='center'
52+
justifyContent='center'
53+
display='inline-flex'
54+
color={`${statusColorMap[status as 'up' | 'down' | 'flapping']}.600`}
55+
_dark={{
56+
bg: `${statusColorMap[status as 'up' | 'down' | 'flapping']}.700`,
57+
color: `${statusColorMap[status as 'up' | 'down' | 'flapping']}.200`,
58+
}}
59+
>
60+
{status}
61+
</Flex>
62+
<Text
63+
fontSize='sm'
64+
fontWeight='semibold'
65+
color={`${statusColorMap[status as 'up' | 'down' | 'flapping']}.600`}
66+
>
67+
{itemsArray?.length
68+
? itemsArray?.length > 1
69+
? `${itemsArray?.length} Endpoints`
70+
: '1 Endpoint'
71+
: 'No Endpoints'}
72+
</Text>
73+
</HStack>
74+
</HStack>
75+
</Accordion.ItemTrigger>
76+
77+
<Accordion.ItemContent borderWidth={1}>
78+
<Accordion.ItemBody>
79+
{itemsArray.length > 0 ? (
80+
<Box pl={10}>
81+
<StatusTable items={itemsArray} isLoading={isLoading} />
82+
</Box>
83+
) : (
84+
<Box textAlign='center' color='gray.500' py={8} borderRadius='md'>
85+
<Text>No endpoints available</Text>
86+
</Box>
87+
)}
88+
</Accordion.ItemBody>
89+
</Accordion.ItemContent>
90+
</Accordion.Item>
91+
);
92+
})}
93+
</Accordion.Root>
94+
);
95+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { Accordion, Box, Flex, HStack, Text } from '@chakra-ui/react';
2+
import type { LiveStatusItem } from '@/api/types';
3+
import { StatusTable } from './StatusTable';
4+
5+
type Props = {
6+
groupedEndpoints: Record<'up' | 'down' | 'flapping', Record<string, LiveStatusItem[]>>;
7+
isLoading: boolean;
8+
};
9+
10+
export function StatusGroupAccordion({ groupedEndpoints, isLoading }: Props) {
11+
return (
12+
<Accordion.Root multiple variant='plain'>
13+
{Object.entries(groupedEndpoints).map(([status, groupItems]) => {
14+
const totalEndpoints = Object.values(groupItems || {}).reduce(
15+
(sum, group) => sum + (group?.length || 0),
16+
0
17+
);
18+
const statusColorMap: Record<'up' | 'down' | 'flapping', string> = {
19+
up: 'green',
20+
down: 'red',
21+
flapping: 'yellow',
22+
} as const;
23+
24+
// Narrow the type for TypeScript
25+
const typedGroupItems = groupItems || {};
26+
27+
return (
28+
<Accordion.Item key={status} value={status} my={2}>
29+
<Accordion.ItemTrigger
30+
bg={`${statusColorMap[status as 'up' | 'down' | 'flapping']}.100`}
31+
_dark={{
32+
bg: `${statusColorMap[status as 'up' | 'down' | 'flapping']}.900`,
33+
borderColor: `${statusColorMap[status as 'up' | 'down' | 'flapping']}.800`,
34+
}}
35+
borderColor={`${statusColorMap[status as 'up' | 'down' | 'flapping']}.200`}
36+
borderWidth={1}
37+
>
38+
<HStack w='full' justify='space-between'>
39+
<HStack px={'10px'}>
40+
<Accordion.ItemIndicator
41+
fontSize={'md'}
42+
fontWeight={'bolder'}
43+
color={`${statusColorMap[status as 'up' | 'down' | 'flapping']}.600`}
44+
/>
45+
<Flex
46+
as='span'
47+
bg={`${statusColorMap[status as 'up' | 'down' | 'flapping']}.200`}
48+
color={`${statusColorMap[status as 'up' | 'down' | 'flapping']}.600`}
49+
_dark={{
50+
bg: `${statusColorMap[status as 'up' | 'down' | 'flapping']}.700`,
51+
color: `${statusColorMap[status as 'up' | 'down' | 'flapping']}.200`,
52+
}}
53+
textTransform='uppercase'
54+
borderRadius='30px'
55+
px={4}
56+
py={1}
57+
fontSize='11px'
58+
alignItems='center'
59+
justifyContent='center'
60+
display='inline-flex'
61+
>
62+
{status}
63+
</Flex>
64+
<Text
65+
fontSize='sm'
66+
fontWeight='semibold'
67+
color={`${statusColorMap[status as 'up' | 'down' | 'flapping']}.600`}
68+
>
69+
{totalEndpoints ? `${totalEndpoints} Endpoints` : 'No Endpoints'}
70+
</Text>
71+
</HStack>
72+
</HStack>
73+
</Accordion.ItemTrigger>
74+
<Accordion.ItemContent borderWidth={1} borderRadius={1}>
75+
<Accordion.ItemBody py={0}>
76+
<Accordion.Root multiple variant='plain' pl={4}>
77+
{Object.entries(typedGroupItems).map(([group, items]) => (
78+
<Accordion.Item key={group} value={group}>
79+
<Accordion.ItemTrigger>
80+
<HStack w='full' justify='space-between'>
81+
<HStack px={'10px'}>
82+
<Accordion.ItemIndicator fontSize={'md'} fontWeight={'bolder'} />
83+
<Text fontSize='sm' fontWeight='semibold' pl={4}>
84+
{group}
85+
</Text>
86+
<Text fontSize='12px' color={'gray.400'} px={2}>
87+
{items?.length
88+
? items?.length > 1
89+
? `${items?.length} Endpoints`
90+
: '1 Endpoint'
91+
: 'No Endpoints'}
92+
</Text>
93+
</HStack>
94+
</HStack>
95+
</Accordion.ItemTrigger>
96+
97+
<Accordion.ItemContent borderLeftWidth={1} borderRadius={2} ml={5}>
98+
<Accordion.ItemBody pl={6} py={0}>
99+
{items && items.length > 0 ? (
100+
<StatusTable items={items} isLoading={isLoading} />
101+
) : (
102+
<Box textAlign='center' color='gray.500' py={8} borderRadius='md'>
103+
<Text>No endpoints available</Text>
104+
</Box>
105+
)}
106+
</Accordion.ItemBody>
107+
</Accordion.ItemContent>
108+
</Accordion.Item>
109+
))}
110+
</Accordion.Root>
111+
</Accordion.ItemBody>
112+
</Accordion.ItemContent>
113+
</Accordion.Item>
114+
);
115+
})}
116+
</Accordion.Root>
117+
);
118+
}

0 commit comments

Comments
 (0)