Skip to content

Commit be88f69

Browse files
committed
feat: implement dynamic EndpointSelect with query-based fetching
1 parent 15cca6a commit be88f69

File tree

3 files changed

+64
-183
lines changed

3 files changed

+64
-183
lines changed

thingconnect.pulse.client/src/components/common/ComboboxSelect.tsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ type Option = {
77
value: string;
88
};
99

10-
interface ComboboxProps extends Omit<ComponentProps<typeof Combobox.Root>, 'onChange' | 'children' | 'value' | 'collection'
10+
export interface ComboboxProps extends Omit<ComponentProps<typeof Combobox.Root>, 'onChange' | 'children' | 'value' | 'collection'
1111
> {
1212
items: Option[];
1313
selectedValue?: string;
1414
onChange: (value: string) => void;
1515
placeholder?: string;
1616
isLoading?: boolean;
17+
defaultToFirst?: boolean;
1718
}
1819

1920
export function ComboboxSelect({
@@ -22,14 +23,15 @@ export function ComboboxSelect({
2223
onChange,
2324
placeholder = 'Select an option',
2425
isLoading = false,
26+
defaultToFirst = false,
2527
...rest
2628
}: ComboboxProps) {
2729

28-
const itemsWithAll = useMemo(() => {
29-
return items.length > 0
30-
? [{ label: 'All', value: '' }, ...items]
30+
const itemsWithAll = useMemo(() => {
31+
return items.length > 0 && !defaultToFirst
32+
? [{ label: 'All', value: '' }, ...items]
3133
: items;
32-
}, [items]);
34+
}, [items, defaultToFirst]);
3335

3436
const { contains } = useFilter({ sensitivity: 'base' });
3537
const { collection, set, filter } = useListCollection<Option>({
@@ -40,8 +42,14 @@ export function ComboboxSelect({
4042
});
4143

4244
useEffect(() => {
45+
// Always update the collection
4346
set(itemsWithAll);
44-
}, [itemsWithAll, set]);
47+
48+
// If defaultToFirst is true and nothing is selected yet, pick the first option
49+
if (defaultToFirst && !selectedValue && itemsWithAll.length > 0) {
50+
onChange(itemsWithAll[0].value);
51+
}
52+
}, [itemsWithAll, set, defaultToFirst, selectedValue, onChange]);
4553

4654
return (
4755
<Skeleton loading={isLoading} w='full'>
Lines changed: 36 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,123 +1,54 @@
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';
1+
import { useEffect, useMemo } from 'react';
2+
import { ComboboxSelect, type ComboboxProps } from './ComboboxSelect';
3+
import { useQuery } from '@tanstack/react-query';
4+
import { StatusService } from '@/api/services/status.service';
45

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;
6+
interface EndpointSelectProps extends Omit<ComboboxProps, 'items'> {
187
defaultToFirst?: boolean;
8+
setName?: (name: string) => void;
199
}
2010

2111
export function EndpointSelect({
22-
items,
2312
selectedValue,
2413
onChange,
25-
isLoading = false,
26-
error = false,
2714
placeholder = 'Select endpoint...',
28-
optionName = '',
15+
setName,
2916
defaultToFirst = false,
17+
...rest
3018
}: 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];
3719

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,
20+
// Live endpoints
21+
const {
22+
data: liveData,
23+
isLoading,
24+
} = useQuery({
25+
queryKey: ['live-status'],
26+
queryFn: () => StatusService.getLiveStatus({ pageSize: 100 }),
27+
staleTime: 30000,
4728
});
4829

49-
// Update collection whenever items change
50-
useEffect(() => {
51-
set(itemsWithAll);
30+
const selectedEndpointName = useMemo(() => {
31+
return liveData?.items?.find(item => item.endpoint.id === selectedValue)?.endpoint?.name || 'Unknown Endpoint';
32+
}, [liveData, selectedValue]);
5233

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('');
34+
useEffect(() => {
35+
if (setName) {
36+
setName(selectedEndpointName);
6037
}
61-
}, [items, set, selectedValue, cleared, onChange]);
38+
}, [selectedEndpointName, setName]);
6239

6340
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>
41+
<ComboboxSelect
42+
items={liveData?.items.map(item => ({
43+
label: item.endpoint.name,
44+
value: item.endpoint.id,
45+
})) || []}
46+
selectedValue={selectedValue}
47+
onChange={onChange}
48+
isLoading={isLoading}
49+
placeholder={placeholder}
50+
defaultToFirst={defaultToFirst}
51+
{...rest}
52+
/>
12253
);
123-
}
54+
}

thingconnect.pulse.client/src/pages/History.tsx

Lines changed: 14 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,9 @@ import {
77
HStack,
88
Button,
99
Card,
10-
Combobox,
11-
Portal,
12-
Span,
13-
Spinner,
14-
useFilter,
15-
useListCollection,
1610
IconButton,
1711
VStack,
1812
Tabs,
19-
Skeleton,
2013
} from '@chakra-ui/react';
2114
import { Download, TrendingUp, AlertCircle, RefreshCw } from 'lucide-react';
2215
import { Page } from '@/components/layout/Page';
@@ -29,9 +22,9 @@ import type { BucketType } from '@/types/bucket';
2922
import { AvailabilityChart } from '@/components/AvailabilityChart';
3023
import { HistoryTable } from '@/components/HistoryTable';
3124
import { HistoryService } from '@/api/services/history.service';
32-
import { StatusService } from '@/api/services/status.service';
3325
import { Tooltip } from '@/components/ui/tooltip';
3426
import { AvailabilityStats } from '@/components/AvailabilityStats';
27+
import { EndpointSelect } from '@/components/common/EndpointSelect';
3528
// import { EndpointCombobox } from '@/components/common/ComboboxSelect';
3629

3730
export default function History() {
@@ -41,7 +34,7 @@ export default function History() {
4134
const [selectedEndpoint, setSelectedEndpoint] = useState<string>(
4235
searchParams.get('endpoint') || ''
4336
);
44-
const [cleared, setCleared] = useState(false);
37+
const [selectedEndpointName, setSelectedEndpointName] = useState<string>('Unknown Endpoint');
4538
const [dateRange, setDateRange] = useState<DateRange>(() => {
4639
const defaultRange = HistoryService.getDefaultDateRange();
4740
return {
@@ -52,45 +45,6 @@ export default function History() {
5245
const [bucket, setBucket] = useState<BucketType>('15m');
5346
const [isExporting, setIsExporting] = useState(false);
5447

55-
// Live endpoints
56-
const {
57-
data: liveData,
58-
isLoading: isLiveDataLoading,
59-
error: liveDataError,
60-
} = useQuery({
61-
queryKey: ['live-status'],
62-
queryFn: () => StatusService.getLiveStatus({ pageSize: 100 }),
63-
staleTime: 30000,
64-
});
65-
66-
// Filtering + collection for Combobox
67-
// const { contains } = useFilter({ sensitivity: 'base' });
68-
// const { collection, set, filter } = useListCollection<{
69-
// label: string;
70-
// value: string;
71-
// }>({
72-
// initialItems: [],
73-
// itemToString: item => item.label,
74-
// itemToValue: item => item.value,
75-
// filter: contains,
76-
// });
77-
78-
// // Update collection when liveData changes
79-
// useEffect(() => {
80-
// if (liveData?.items) {
81-
// const items = liveData.items.map((item: any) => ({
82-
// label: `${item.endpoint.name} (${item.endpoint.host})`,
83-
// value: item.endpoint.id,
84-
// }));
85-
// set(items);
86-
87-
// // fallback only if not cleared manually
88-
// if (!selectedEndpoint && items.length > 0 && !cleared) {
89-
// setSelectedEndpoint(items[0].value);
90-
// }
91-
// }
92-
// }, [liveData, set, selectedEndpoint, cleared]);
93-
9448
// Track page view
9549
useEffect(() => {
9650
analytics.trackPageView('History', {
@@ -154,10 +108,6 @@ export default function History() {
154108
}
155109
};
156110

157-
const selectedEndpointName =
158-
liveData?.items?.find(item => item.endpoint.id === selectedEndpoint)?.endpoint?.name ||
159-
'Unknown Endpoint';
160-
161111
return (
162112
<Page
163113
title='History'
@@ -170,22 +120,14 @@ export default function History() {
170120
<Text fontSize='sm' fontWeight='medium'>
171121
Endpoint
172122
</Text>
173-
<Skeleton loading={isLiveDataLoading} w='md'>
174-
175-
{/* <EndpointCombobox
176-
items={liveData?.items.map((item: any) => ({
177-
label: `${item.endpoint.name} (${item.endpoint.host})`,
178-
value: item.endpoint.id,
179-
})) || []}
180-
selectedValue={selectedEndpoint ? selectedEndpoint : ''}
181-
onChange={setSelectedEndpoint}
182-
isLoading={false}
183-
error={undefined}
184-
placeholder='Select endpoint...'
185-
optionName='Endpoints'
186-
defaultToFirst={true}
187-
/> */}
188-
</Skeleton>
123+
<EndpointSelect
124+
selectedValue={selectedEndpoint}
125+
onChange={setSelectedEndpoint}
126+
setName={setSelectedEndpointName}
127+
defaultToFirst={true}
128+
w={'xs'}
129+
size='xs'
130+
/>
189131
</VStack>
190132
<VStack align='start' gap={1}>
191133
<Text fontSize='sm' fontWeight='medium'>
@@ -223,7 +165,7 @@ export default function History() {
223165
size='xs'
224166
colorPalette='blue'
225167
onClick={() => void handleExportCSV()}
226-
loading={isExporting || isHistoryDataLoading || isLiveDataLoading}
168+
loading={isExporting || isHistoryDataLoading}
227169
disabled={!historyData}
228170
>
229171
<Download size={16} />
@@ -237,7 +179,7 @@ export default function History() {
237179
<AvailabilityStats
238180
data={historyData}
239181
bucket={bucket}
240-
isLoading={isHistoryDataLoading || isLiveDataLoading}
182+
isLoading={isHistoryDataLoading }
241183
/>
242184
</PageSection>
243185
<Tabs.Root
@@ -270,7 +212,7 @@ export default function History() {
270212
<AvailabilityChart
271213
data={historyData}
272214
bucket={bucket}
273-
isLoading={isHistoryDataLoading || isLiveDataLoading}
215+
isLoading={isHistoryDataLoading}
274216
/>
275217
</Card.Body>
276218
</Card.Root>
@@ -293,7 +235,7 @@ export default function History() {
293235
data={historyData}
294236
bucket={bucket}
295237
pageSize={20}
296-
isLoading={isHistoryDataLoading || isLiveDataLoading}
238+
isLoading={isHistoryDataLoading}
297239
/>
298240
</Card.Body>
299241
</Card.Root>

0 commit comments

Comments
 (0)