Skip to content

Commit 4ae5179

Browse files
committed
feat: add status components and common selects
1 parent 5476df3 commit 4ae5179

File tree

4 files changed

+326
-110
lines changed

4 files changed

+326
-110
lines changed
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { useEffect, useState } from 'react';
2+
import { Combobox, HStack, Portal, Span, Spinner, Skeleton } from '@chakra-ui/react';
3+
import { useFilter, useListCollection } from '@chakra-ui/react';
4+
5+
type EndpointOption = {
6+
label: string;
7+
value: string;
8+
};
9+
10+
interface ComboboxProps {
11+
items: { label: string; value: string }[];
12+
selectedValue: string;
13+
onChange: (value: string) => void;
14+
isLoading?: boolean;
15+
error?: boolean;
16+
placeholder?: string;
17+
optionName?: string;
18+
defaultToFirst?: boolean;
19+
}
20+
21+
export function ComboboxSelect({
22+
items,
23+
selectedValue,
24+
onChange,
25+
isLoading = false,
26+
error = false,
27+
placeholder = 'Select endpoint...',
28+
optionName = '',
29+
defaultToFirst = false,
30+
}: ComboboxProps) {
31+
const [cleared, setCleared] = useState(false);
32+
33+
const itemsWithAll: EndpointOption[] =
34+
defaultToFirst && items.length > 0
35+
? items
36+
: [{ label: `All ${optionName}`, value: '' }, ...items];
37+
38+
console.log('Items with All option:', itemsWithAll);
39+
40+
// Filtering
41+
const { contains } = useFilter({ sensitivity: 'base' });
42+
const { collection, set, filter } = useListCollection<EndpointOption>({
43+
initialItems: [],
44+
itemToString: item => item.label,
45+
itemToValue: item => item.value,
46+
filter: contains,
47+
});
48+
49+
// Update collection whenever items change
50+
useEffect(() => {
51+
set(itemsWithAll);
52+
53+
if (defaultToFirst && itemsWithAll.length > 0 && !selectedValue && !cleared) {
54+
console.log('Defaulting to first item:', itemsWithAll[0]);
55+
onChange(itemsWithAll[0].value);
56+
57+
} else if (!defaultToFirst && !selectedValue && !cleared) {
58+
console.log('Defaulting to "All" option');
59+
onChange('');
60+
}
61+
}, [items, set, selectedValue, cleared, onChange]);
62+
63+
return (
64+
<Skeleton loading={isLoading} w='md'>
65+
<Combobox.Root
66+
size='xs'
67+
w='xs'
68+
collection={collection}
69+
// value={selectedValue ? [selectedValue] : []}
70+
value={[selectedValue]}
71+
onValueChange={e => {
72+
onChange(e.value[0] ?? '');
73+
setCleared(false);
74+
}}
75+
onInputValueChange={e => filter(e.inputValue)}
76+
onOpenChange={open => {
77+
if (open) filter('');
78+
}}
79+
openOnClick
80+
>
81+
<Combobox.Control>
82+
<Combobox.Input placeholder={placeholder} />
83+
<Combobox.IndicatorGroup>
84+
<Combobox.ClearTrigger
85+
onClick={() => {
86+
onChange('');
87+
setCleared(true);
88+
}}
89+
/>
90+
<Combobox.Trigger />
91+
</Combobox.IndicatorGroup>
92+
</Combobox.Control>
93+
<Portal>
94+
<Combobox.Positioner>
95+
<Combobox.Content minW='sm'>
96+
{isLoading ? (
97+
<HStack p='2'>
98+
<Spinner size='xs' borderWidth='1px' />
99+
<Span>Loading endpoints...</Span>
100+
</HStack>
101+
) : error ? (
102+
<Span p='2' color='fg.error'>
103+
Failed to load endpoints
104+
</Span>
105+
) : collection.items.length === 0 ? (
106+
<Combobox.Empty>No endpoints found</Combobox.Empty>
107+
) : (
108+
collection.items.map(item => (
109+
<Combobox.Item key={item.value} item={item}>
110+
<HStack justify='space-between' textStyle='sm'>
111+
{item.label}
112+
</HStack>
113+
<Combobox.ItemIndicator />
114+
</Combobox.Item>
115+
))
116+
)}
117+
</Combobox.Content>
118+
</Combobox.Positioner>
119+
</Portal>
120+
</Combobox.Root>
121+
</Skeleton>
122+
);
123+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { useEffect, useState } from 'react';
2+
import { Combobox, HStack, Portal, Span, Spinner, Skeleton } from '@chakra-ui/react';
3+
import { useFilter, useListCollection } from '@chakra-ui/react';
4+
5+
type EndpointOption = {
6+
label: string;
7+
value: string;
8+
};
9+
10+
interface EndpointSelectProps {
11+
items: { label: string; value: string }[];
12+
selectedValue: string;
13+
onChange: (value: string) => void;
14+
isLoading?: boolean;
15+
error?: boolean;
16+
placeholder?: string;
17+
optionName?: string;
18+
defaultToFirst?: boolean;
19+
}
20+
21+
export function EndpointSelect({
22+
items,
23+
selectedValue,
24+
onChange,
25+
isLoading = false,
26+
error = false,
27+
placeholder = 'Select endpoint...',
28+
optionName = '',
29+
defaultToFirst = false,
30+
}: EndpointSelectProps) {
31+
const [cleared, setCleared] = useState(false);
32+
33+
const itemsWithAll: EndpointOption[] =
34+
defaultToFirst && items.length > 0
35+
? items
36+
: [{ label: `All ${optionName}`, value: '' }, ...items];
37+
38+
console.log('Items with All option:', itemsWithAll);
39+
40+
// Filtering
41+
const { contains } = useFilter({ sensitivity: 'base' });
42+
const { collection, set, filter } = useListCollection<EndpointOption>({
43+
initialItems: [],
44+
itemToString: item => item.label,
45+
itemToValue: item => item.value,
46+
filter: contains,
47+
});
48+
49+
// Update collection whenever items change
50+
useEffect(() => {
51+
set(itemsWithAll);
52+
53+
if (defaultToFirst && itemsWithAll.length > 0 && !selectedValue && !cleared) {
54+
console.log('Defaulting to first item:', itemsWithAll[0]);
55+
onChange(itemsWithAll[0].value);
56+
57+
} else if (!defaultToFirst && !selectedValue && !cleared) {
58+
console.log('Defaulting to "All" option');
59+
onChange('');
60+
}
61+
}, [items, set, selectedValue, cleared, onChange]);
62+
63+
return (
64+
<Skeleton loading={isLoading} w='md'>
65+
<Combobox.Root
66+
size='xs'
67+
w='xs'
68+
collection={collection}
69+
// value={selectedValue ? [selectedValue] : []}
70+
value={[selectedValue]}
71+
onValueChange={e => {
72+
onChange(e.value[0] ?? '');
73+
setCleared(false);
74+
}}
75+
onInputValueChange={e => filter(e.inputValue)}
76+
onOpenChange={open => {
77+
if (open) filter('');
78+
}}
79+
openOnClick
80+
>
81+
<Combobox.Control>
82+
<Combobox.Input placeholder={placeholder} />
83+
<Combobox.IndicatorGroup>
84+
<Combobox.ClearTrigger
85+
onClick={() => {
86+
onChange('');
87+
setCleared(true);
88+
}}
89+
/>
90+
<Combobox.Trigger />
91+
</Combobox.IndicatorGroup>
92+
</Combobox.Control>
93+
<Portal>
94+
<Combobox.Positioner>
95+
<Combobox.Content minW='sm'>
96+
{isLoading ? (
97+
<HStack p='2'>
98+
<Spinner size='xs' borderWidth='1px' />
99+
<Span>Loading endpoints...</Span>
100+
</HStack>
101+
) : error ? (
102+
<Span p='2' color='fg.error'>
103+
Failed to load endpoints
104+
</Span>
105+
) : collection.items.length === 0 ? (
106+
<Combobox.Empty>No endpoints found</Combobox.Empty>
107+
) : (
108+
collection.items.map(item => (
109+
<Combobox.Item key={item.value} item={item}>
110+
<HStack justify='space-between' textStyle='sm'>
111+
{item.label}
112+
</HStack>
113+
<Combobox.ItemIndicator />
114+
</Combobox.Item>
115+
))
116+
)}
117+
</Combobox.Content>
118+
</Combobox.Positioner>
119+
</Portal>
120+
</Combobox.Root>
121+
</Skeleton>
122+
);
123+
}

thingconnect.pulse.client/src/components/status/StatusFilters.tsx

Lines changed: 41 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { X } from 'lucide-react';
1515
import type { LiveStatusParams } from '@/api/types';
1616
import { MdSearch, MdExpandMore } from 'react-icons/md';
1717
import { useEffect, useState } from 'react';
18+
// import { EndpointCombobox } from '../common/ComboboxSelect';
1819

1920
interface StatusFiltersProps {
2021
filters: LiveStatusParams;
@@ -43,27 +44,27 @@ export function StatusFilters({
4344
selectedGroup,
4445
onSelectedGroupChange,
4546
}: StatusFiltersProps) {
46-
const [cleared, setCleared] = useState(false);
47+
// const [cleared, setCleared] = useState(false);
4748

48-
const { contains } = useFilter({ sensitivity: 'base' });
49-
const { collection, filter, set } = useListCollection<{
50-
label: string;
51-
value: string;
52-
}>({
53-
initialItems: [], // start empty
54-
itemToString: item => item.label,
55-
itemToValue: item => item.value,
56-
filter: contains,
57-
});
49+
// const { contains } = useFilter({ sensitivity: 'base' });
50+
// const { collection, filter, set } = useListCollection<{
51+
// label: string;
52+
// value: string;
53+
// }>({
54+
// initialItems: [], // start empty
55+
// itemToString: item => item.label,
56+
// itemToValue: item => item.value,
57+
// filter: contains,
58+
// });
5859

59-
useEffect(() => {
60-
const newItems = groups.map(g => ({
61-
label: g.name,
62-
value: g.id,
63-
}));
60+
// useEffect(() => {
61+
// const newItems = groups.map(g => ({
62+
// label: g.name,
63+
// value: g.id,
64+
// }));
6465

65-
set(newItems);
66-
}, [groups, set, cleared]);
66+
// set(newItems);
67+
// }, [groups, set, cleared]);
6768

6869
const handleGroupChange = (value: string) => {
6970
const newGroup = value || undefined;
@@ -87,19 +88,18 @@ export function StatusFilters({
8788

8889
const clearSearch = () => {
8990
onSearchChange?.('');
90-
// Optional: reset local cleared state
91-
setCleared(true);
91+
// Optional: reset local cleared state
9292
};
9393

94-
const clearFilter = () => {
95-
onFiltersChange({
96-
...filters,
97-
group: undefined,
98-
search: undefined,
99-
});
100-
onSelectedGroupChange?.(undefined);
101-
setCleared(true);
102-
};
94+
// const clearFilter = () => {
95+
// onFiltersChange({
96+
// ...filters,
97+
// group: undefined,
98+
// search: undefined,
99+
// });
100+
// onSelectedGroupChange?.(undefined);
101+
// setCleared(true);
102+
// };
103103

104104
return (
105105
<Box
@@ -125,7 +125,7 @@ export function StatusFilters({
125125
<HStack gap={{ base: 2, md: 4 }} align='center' flexWrap={{ base: 'wrap', md: 'nowrap' }}>
126126
{/* Group Filter */}
127127
<Box w={'25%'}>
128-
<Combobox.Root
128+
{/* <Combobox.Root
129129
size='md'
130130
collection={collection}
131131
value={selectedGroup ? [selectedGroup] : []}
@@ -167,7 +167,17 @@ export function StatusFilters({
167167
})}
168168
</Combobox.Content>
169169
</Combobox.Positioner>
170-
</Combobox.Root>
170+
</Combobox.Root> */}
171+
{/* <EndpointCombobox
172+
items={groups.map(g => ({ label: g.name, value: g.id }))}
173+
selectedValue={selectedGroup ? selectedGroup : ''}
174+
onChange={handleGroupChange}
175+
isLoading={false}
176+
error={undefined}
177+
placeholder='Select Group...'
178+
optionName='Groups'
179+
defaultToFirst={false}
180+
/> */}
171181
</Box>
172182
{/* Search Input */}
173183
<Flex w='80' position='relative' align='center'>

0 commit comments

Comments
 (0)