@bardioc/create-bardioc-app 0.7.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/templates/_base/.changeset/README.md +4 -6
- package/templates/_base/.claude/commands/changeset-app.md +11 -12
- package/templates/_base/.claude/commands/refresh-bundle.md +1 -1
- package/templates/_base/AGENTS.md +128 -0
- package/templates/_base/CLAUDE.md +1 -0
- package/templates/_base/README.md +11 -17
- package/templates/_opt-link/scripts/link-source.mjs +1 -1
- package/templates/_opt-pipeline/bitbucket-pipelines.yml +12 -56
- package/templates/angular/package.json +1 -1
- package/templates/nextjs/package.json +1 -1
- package/templates/preact/package.json +1 -1
- package/templates/solid/package.json +1 -1
- package/templates/svelte/package.json +1 -1
- package/templates/vite/CLAUDE.md +149 -135
- package/templates/vite/package.json +3 -3
- package/templates/vite/src/App.tsx +6 -3
- package/templates/vite/src/auth/onboarding.tsx +19 -23
- package/templates/vite/src/climate/climate-data.ts +19 -0
- package/templates/vite/src/climate/conditions-chart.tsx +11 -6
- package/templates/vite/src/climate/location-card.tsx +2 -5
- package/templates/vite/src/climate/location-select.tsx +7 -2
- package/templates/vite/src/climate/metrics-grid.tsx +6 -1
- package/templates/vite/src/climate/overview-view.tsx +26 -7
- package/templates/vite/src/climate/stations-view.tsx +73 -9
- package/templates/vite/src/climate/types.ts +5 -5
- package/templates/vite/src/dashboard-provider.tsx +15 -5
- package/templates/vite/src/events/live-events-view.tsx +67 -37
- package/templates/vite/src/events/mock-events.ts +33 -0
- package/templates/vite/src/graph/explorer-view.tsx +81 -63
- package/templates/vite/src/i18n.tsx +6 -2
- package/templates/vite/src/index.css +0 -6
- package/templates/vite/src/locales/de.json +55 -51
- package/templates/vite/src/locales/en.json +44 -40
- package/templates/vite/src/preview-state.tsx +29 -0
- package/templates/vite/src/profile/profile-view.tsx +43 -25
- package/templates/vite/src/shell/about-dialog.tsx +6 -5
- package/templates/vite/src/shell/app-shell.tsx +46 -45
- package/templates/vite/src/shell/debug-menu.tsx +86 -0
- package/templates/vite/src/shell/header.tsx +6 -4
- package/templates/vite/src/shell/view-content.tsx +69 -0
- package/templates/vue/package.json +1 -1
|
@@ -18,5 +18,10 @@ export function MetricsGrid({ reading }: Readonly<MetricsGridProps>) {
|
|
|
18
18
|
color: def.color,
|
|
19
19
|
}));
|
|
20
20
|
|
|
21
|
-
return
|
|
21
|
+
return (
|
|
22
|
+
<GenericMetrics
|
|
23
|
+
metrics={metrics}
|
|
24
|
+
className="grid grid-cols-1 gap-4 @sm/page:grid-cols-2 @3xl/page:grid-cols-5"
|
|
25
|
+
/>
|
|
26
|
+
);
|
|
22
27
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Empty } from '@bardioc/ui/components';
|
|
2
|
+
import { CloudOff, Database } from 'lucide-react';
|
|
2
3
|
import { useDashboard } from '../dashboard-provider';
|
|
3
4
|
import { useTranslation } from '../i18n';
|
|
4
5
|
import { ConditionsChart } from './conditions-chart';
|
|
@@ -9,7 +10,20 @@ export function OverviewView() {
|
|
|
9
10
|
const { t } = useTranslation();
|
|
10
11
|
const { state, activeStation, historyCount, persisted } = useDashboard();
|
|
11
12
|
|
|
12
|
-
if (!state
|
|
13
|
+
if (!state) return null;
|
|
14
|
+
|
|
15
|
+
if (!activeStation) {
|
|
16
|
+
return (
|
|
17
|
+
<Empty
|
|
18
|
+
icon={CloudOff}
|
|
19
|
+
iconVariant="chip"
|
|
20
|
+
title={t('overview.emptyTitle', { fallback: 'No Station Data' })}
|
|
21
|
+
description={t('overview.emptyDescription', {
|
|
22
|
+
fallback: 'Stations appear here once they report.',
|
|
23
|
+
})}
|
|
24
|
+
/>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
13
27
|
|
|
14
28
|
const savedTime = new Date(state.updatedAt).toLocaleTimeString([], {
|
|
15
29
|
hour: '2-digit',
|
|
@@ -33,16 +47,21 @@ export function OverviewView() {
|
|
|
33
47
|
</span>
|
|
34
48
|
<span>·</span>
|
|
35
49
|
<span>
|
|
36
|
-
{
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
50
|
+
{historyCount === 1
|
|
51
|
+
? t('overview.historyOne', {
|
|
52
|
+
fallback: '{count} snapshot in history',
|
|
53
|
+
params: { count: historyCount },
|
|
54
|
+
})
|
|
55
|
+
: t('overview.historyOther', {
|
|
56
|
+
fallback: '{count} snapshots in history',
|
|
57
|
+
params: { count: historyCount },
|
|
58
|
+
})}
|
|
40
59
|
</span>
|
|
41
60
|
</>
|
|
42
61
|
) : (
|
|
43
62
|
<span>
|
|
44
63
|
{t('overview.notPersisted', {
|
|
45
|
-
fallback: 'Preview — not persisted (open inside Bardioc to persist)',
|
|
64
|
+
fallback: 'Preview — not persisted (open inside Bardioc to persist).',
|
|
46
65
|
})}
|
|
47
66
|
</span>
|
|
48
67
|
)}
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BadgeStatus,
|
|
3
|
+
Button,
|
|
3
4
|
CheckboxGroup,
|
|
4
5
|
DataTable,
|
|
6
|
+
Empty,
|
|
5
7
|
FilterDropdown,
|
|
6
8
|
SortableColumnHeader,
|
|
9
|
+
StableWidthText,
|
|
7
10
|
TruncatedCellText,
|
|
8
11
|
} from '@bardioc/ui/components';
|
|
9
12
|
import type { TReactTable } from '@bardioc/ui/types';
|
|
13
|
+
import { MapPin, MapPinCheck, SearchX } from 'lucide-react';
|
|
10
14
|
import { useMemo, useState } from 'react';
|
|
11
15
|
import { useDashboard } from '../dashboard-provider';
|
|
12
16
|
import { useTranslation } from '../i18n';
|
|
@@ -27,21 +31,34 @@ interface ConditionFilterProps {
|
|
|
27
31
|
function ConditionFilter({ selected, onToggle }: Readonly<ConditionFilterProps>) {
|
|
28
32
|
const { t } = useTranslation();
|
|
29
33
|
const [open, setOpen] = useState(false);
|
|
30
|
-
|
|
34
|
+
|
|
35
|
+
// The trigger says "Filter", or how many are active; what is filtered is the section title.
|
|
36
|
+
// StableWidthText reserves the widest label, so the button keeps its width as the count changes.
|
|
37
|
+
const buttonLabel = (
|
|
38
|
+
<StableWidthText
|
|
39
|
+
placeholder={t('common.filter.activeFilter', { count: 9, fallback: '9 Filters' })}
|
|
40
|
+
>
|
|
41
|
+
{selected.length > 0
|
|
42
|
+
? t('common.filter.activeFilter', {
|
|
43
|
+
count: selected.length,
|
|
44
|
+
fallback: '{count} Filters',
|
|
45
|
+
})
|
|
46
|
+
: t('common.filter.filter', { fallback: 'Filter' })}
|
|
47
|
+
</StableWidthText>
|
|
48
|
+
);
|
|
31
49
|
|
|
32
50
|
return (
|
|
33
51
|
<FilterDropdown
|
|
34
52
|
immediate
|
|
35
53
|
open={open}
|
|
36
54
|
setOpen={setOpen}
|
|
37
|
-
buttonLabel={
|
|
55
|
+
buttonLabel={buttonLabel}
|
|
38
56
|
sections={[
|
|
39
57
|
{
|
|
40
58
|
id: 'condition',
|
|
41
|
-
title:
|
|
59
|
+
title: t('stations.filter', { fallback: 'Condition' }),
|
|
42
60
|
content: (
|
|
43
61
|
<CheckboxGroup
|
|
44
|
-
className="gap-3 capitalize"
|
|
45
62
|
items={CONDITIONS.map(condition => ({
|
|
46
63
|
id: condition,
|
|
47
64
|
label: t(`conditions.${condition}`, { fallback: condition }),
|
|
@@ -59,6 +76,7 @@ function ConditionFilter({ selected, onToggle }: Readonly<ConditionFilterProps>)
|
|
|
59
76
|
export function StationsView() {
|
|
60
77
|
const { t } = useTranslation();
|
|
61
78
|
const { state, setActiveStation } = useDashboard();
|
|
79
|
+
const activeStationId = state?.activeStationId;
|
|
62
80
|
const [conditions, setConditions] = useState<Condition[]>([]);
|
|
63
81
|
|
|
64
82
|
const onToggle = (condition: Condition) =>
|
|
@@ -70,6 +88,7 @@ export function StationsView() {
|
|
|
70
88
|
() => [
|
|
71
89
|
{
|
|
72
90
|
accessorKey: 'name',
|
|
91
|
+
size: 220,
|
|
73
92
|
enableSorting: true,
|
|
74
93
|
header: ({ column }) => (
|
|
75
94
|
<SortableColumnHeader
|
|
@@ -81,11 +100,13 @@ export function StationsView() {
|
|
|
81
100
|
},
|
|
82
101
|
{
|
|
83
102
|
accessorKey: 'region',
|
|
103
|
+
size: 160,
|
|
84
104
|
header: () => t('stations.colRegion', { fallback: 'Region' }),
|
|
85
105
|
cell: ({ row }) => <TruncatedCellText value={row.original.region} />,
|
|
86
106
|
},
|
|
87
107
|
{
|
|
88
108
|
accessorKey: 'condition',
|
|
109
|
+
size: 130,
|
|
89
110
|
header: () => t('stations.colCondition', { fallback: 'Condition' }),
|
|
90
111
|
cell: ({ row }) => (
|
|
91
112
|
<BadgeStatus variant={STATUS[row.original.condition]}>
|
|
@@ -95,21 +116,55 @@ export function StationsView() {
|
|
|
95
116
|
},
|
|
96
117
|
{
|
|
97
118
|
id: 'temp',
|
|
119
|
+
size: 110,
|
|
98
120
|
header: () => t('stations.colTemp', { fallback: 'Temp' }),
|
|
99
121
|
cell: ({ row }) => `${row.original.reading.temperature}°C`,
|
|
100
122
|
},
|
|
101
123
|
{
|
|
102
124
|
id: 'humidity',
|
|
125
|
+
size: 110,
|
|
103
126
|
header: () => t('stations.colHumidity', { fallback: 'Humidity' }),
|
|
104
127
|
cell: ({ row }) => `${row.original.reading.humidity}%`,
|
|
105
128
|
},
|
|
106
129
|
{
|
|
107
130
|
id: 'vegetation',
|
|
131
|
+
size: 110,
|
|
108
132
|
header: () => t('stations.colVegetation', { fallback: 'Vegetation' }),
|
|
109
133
|
cell: ({ row }) => `${row.original.reading.vegetation}%`,
|
|
110
134
|
},
|
|
135
|
+
{
|
|
136
|
+
// The trailing column reserved for row actions: fixed width, no header, never sorted or
|
|
137
|
+
// resized. The current station's button is filled and checked, so the selection shows here.
|
|
138
|
+
id: 'actions',
|
|
139
|
+
size: 40,
|
|
140
|
+
minSize: 40,
|
|
141
|
+
maxSize: 40,
|
|
142
|
+
enableSorting: false,
|
|
143
|
+
enableResizing: false,
|
|
144
|
+
header: '',
|
|
145
|
+
cell: ({ row }) => {
|
|
146
|
+
const isActive = row.original.id === activeStationId;
|
|
147
|
+
return (
|
|
148
|
+
<div className="flex justify-end">
|
|
149
|
+
<Button
|
|
150
|
+
variant={isActive ? 'default' : 'ghost'}
|
|
151
|
+
size="icon-xs"
|
|
152
|
+
title={
|
|
153
|
+
isActive
|
|
154
|
+
? t('stations.current', { fallback: 'Current Station' })
|
|
155
|
+
: t('stations.select', { fallback: 'Select Station' })
|
|
156
|
+
}
|
|
157
|
+
aria-pressed={isActive}
|
|
158
|
+
onClick={() => setActiveStation(row.original.id)}
|
|
159
|
+
>
|
|
160
|
+
{isActive ? <MapPinCheck aria-hidden="true" /> : <MapPin aria-hidden="true" />}
|
|
161
|
+
</Button>
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
},
|
|
165
|
+
},
|
|
111
166
|
],
|
|
112
|
-
[t]
|
|
167
|
+
[t, activeStationId, setActiveStation]
|
|
113
168
|
);
|
|
114
169
|
|
|
115
170
|
if (!state) return null;
|
|
@@ -120,10 +175,17 @@ export function StationsView() {
|
|
|
120
175
|
|
|
121
176
|
return (
|
|
122
177
|
<div className="flex flex-col gap-3">
|
|
123
|
-
<p className="text-muted-foreground text-xs">
|
|
178
|
+
<p className="text-muted-foreground text-xs">
|
|
179
|
+
{t('stations.description', {
|
|
180
|
+
fallback: 'All stations reporting to this instance. Search, filter, or select one.',
|
|
181
|
+
})}
|
|
182
|
+
</p>
|
|
124
183
|
<DataTable
|
|
125
184
|
boxed
|
|
126
185
|
data={data}
|
|
186
|
+
itemsPerPage={10}
|
|
187
|
+
// Drag a header edge to resize; double-click it to reset the column to its `size`.
|
|
188
|
+
enableColumnResizing
|
|
127
189
|
columns={columns}
|
|
128
190
|
filterKey={['name', 'region']}
|
|
129
191
|
showSearchBar
|
|
@@ -132,9 +194,11 @@ export function StationsView() {
|
|
|
132
194
|
onRowClick={station => setActiveStation(station.id)}
|
|
133
195
|
filterSlot={<ConditionFilter selected={conditions} onToggle={onToggle} />}
|
|
134
196
|
emptyState={
|
|
135
|
-
<
|
|
136
|
-
{
|
|
137
|
-
|
|
197
|
+
<Empty
|
|
198
|
+
icon={SearchX}
|
|
199
|
+
iconVariant="chip"
|
|
200
|
+
description={t('stations.empty', { fallback: 'No stations match your filters.' })}
|
|
201
|
+
/>
|
|
138
202
|
}
|
|
139
203
|
/>
|
|
140
204
|
</div>
|
|
@@ -52,7 +52,7 @@ export const METRICS: MetricDef[] = [
|
|
|
52
52
|
hintKey: 'metrics.oxygenHint',
|
|
53
53
|
icon: Gauge,
|
|
54
54
|
unit: '%',
|
|
55
|
-
color: 'text-
|
|
55
|
+
color: 'text-warning',
|
|
56
56
|
},
|
|
57
57
|
{
|
|
58
58
|
key: 'temperature',
|
|
@@ -60,7 +60,7 @@ export const METRICS: MetricDef[] = [
|
|
|
60
60
|
hintKey: 'metrics.temperatureHint',
|
|
61
61
|
icon: Thermometer,
|
|
62
62
|
unit: '°C',
|
|
63
|
-
color: 'text-
|
|
63
|
+
color: 'text-destructive',
|
|
64
64
|
},
|
|
65
65
|
{
|
|
66
66
|
key: 'humidity',
|
|
@@ -68,7 +68,7 @@ export const METRICS: MetricDef[] = [
|
|
|
68
68
|
hintKey: 'metrics.humidityHint',
|
|
69
69
|
icon: Droplets,
|
|
70
70
|
unit: '%',
|
|
71
|
-
color: 'text-
|
|
71
|
+
color: 'text-info',
|
|
72
72
|
},
|
|
73
73
|
{
|
|
74
74
|
key: 'windSpeed',
|
|
@@ -76,7 +76,7 @@ export const METRICS: MetricDef[] = [
|
|
|
76
76
|
hintKey: 'metrics.windSpeedHint',
|
|
77
77
|
icon: Wind,
|
|
78
78
|
unit: 'km/h',
|
|
79
|
-
color: 'text-
|
|
79
|
+
color: 'text-chart-4',
|
|
80
80
|
},
|
|
81
81
|
{
|
|
82
82
|
key: 'vegetation',
|
|
@@ -84,7 +84,7 @@ export const METRICS: MetricDef[] = [
|
|
|
84
84
|
hintKey: 'metrics.vegetationHint',
|
|
85
85
|
icon: Leaf,
|
|
86
86
|
unit: '%',
|
|
87
|
-
color: 'text-
|
|
87
|
+
color: 'text-success',
|
|
88
88
|
},
|
|
89
89
|
];
|
|
90
90
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createContext, useContext, type ReactNode } from 'react';
|
|
2
2
|
import { useClimate } from './climate/use-climate';
|
|
3
|
+
import { usePreviewState } from './preview-state';
|
|
3
4
|
import { useProfile, type ProfileState } from './profile/use-profile';
|
|
4
5
|
|
|
5
6
|
type ClimateApi = ReturnType<typeof useClimate>;
|
|
@@ -10,12 +11,21 @@ const DashboardContext = createContext<DashboardContextValue | null>(null);
|
|
|
10
11
|
export function DashboardProvider({ children }: Readonly<{ children: ReactNode }>) {
|
|
11
12
|
const climate = useClimate();
|
|
12
13
|
const profile = useProfile();
|
|
14
|
+
const { previewState } = usePreviewState();
|
|
13
15
|
|
|
14
|
-
return
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
// The "Empty" preview hands every view the data an empty instance would return, so each one
|
|
17
|
+
// renders its own empty state rather than a generic placeholder.
|
|
18
|
+
const value: DashboardContextValue =
|
|
19
|
+
previewState === 'empty'
|
|
20
|
+
? {
|
|
21
|
+
...climate,
|
|
22
|
+
state: climate.state && { ...climate.state, stations: [] },
|
|
23
|
+
activeStation: null,
|
|
24
|
+
profile: { loading: false, profile: null, error: null },
|
|
25
|
+
}
|
|
26
|
+
: { ...climate, profile };
|
|
27
|
+
|
|
28
|
+
return <DashboardContext.Provider value={value}>{children}</DashboardContext.Provider>;
|
|
19
29
|
}
|
|
20
30
|
|
|
21
31
|
export function useDashboard(): DashboardContextValue {
|
|
@@ -1,19 +1,44 @@
|
|
|
1
1
|
import type { GraphEvent } from '@bardioc/app-sdk';
|
|
2
2
|
import { isInsideIframe } from '@bardioc/app-sdk/dev';
|
|
3
3
|
import { useGraphEvents } from '@bardioc/app-sdk/react';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
Badge,
|
|
6
|
+
Card,
|
|
7
|
+
Empty,
|
|
8
|
+
Item,
|
|
9
|
+
ItemActions,
|
|
10
|
+
ItemContent,
|
|
11
|
+
ItemGroup,
|
|
12
|
+
ItemMeta,
|
|
13
|
+
ItemTitle,
|
|
14
|
+
} from '@bardioc/ui/components';
|
|
5
15
|
import { Activity } from 'lucide-react';
|
|
6
16
|
import { useState } from 'react';
|
|
7
17
|
import { useTranslation } from '../i18n';
|
|
18
|
+
import { usePreviewState, type PreviewState } from '../preview-state';
|
|
19
|
+
import { MOCK_GRAPH_EVENTS } from './mock-events';
|
|
8
20
|
|
|
9
21
|
// OGIT types to watch. Broad `ogit/Timeseries` writes are the most common signal in a live
|
|
10
22
|
// instance; swap these for the entity types your app cares about.
|
|
11
23
|
const WATCHED_TYPES = ['ogit/Timeseries', 'ogit/Automation/Issue'];
|
|
12
24
|
const MAX_EVENTS = 50;
|
|
13
25
|
|
|
26
|
+
function eventsForPreview(previewState: PreviewState, events: GraphEvent[]): GraphEvent[] {
|
|
27
|
+
switch (previewState) {
|
|
28
|
+
case 'empty':
|
|
29
|
+
return [];
|
|
30
|
+
case 'mock':
|
|
31
|
+
return MOCK_GRAPH_EVENTS;
|
|
32
|
+
default:
|
|
33
|
+
return events;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
14
37
|
export function LiveEventsView() {
|
|
15
38
|
const { t } = useTranslation();
|
|
16
39
|
const [events, setEvents] = useState<GraphEvent[]>([]);
|
|
40
|
+
const { previewState } = usePreviewState();
|
|
41
|
+
const shown = eventsForPreview(previewState, events);
|
|
17
42
|
|
|
18
43
|
// Subscribes for the mounted lifetime; the host streams matching graph events over SSE.
|
|
19
44
|
// Outside a valid session no events arrive and the empty state stays.
|
|
@@ -21,50 +46,55 @@ export function LiveEventsView() {
|
|
|
21
46
|
setEvents(prev => [event, ...prev].slice(0, MAX_EVENTS));
|
|
22
47
|
});
|
|
23
48
|
|
|
49
|
+
// The empty state sits on the page like every other one, and only the list gets a card: inside
|
|
50
|
+
// a card, `Empty`'s page background shows as a darker block in dark mode.
|
|
24
51
|
return (
|
|
25
|
-
<div className="flex
|
|
52
|
+
<div className="flex min-w-0 flex-1 flex-col gap-3">
|
|
26
53
|
<p className="text-muted-foreground text-xs">
|
|
27
54
|
{t('events.description', {
|
|
28
55
|
fallback: 'Live graph events streamed from the OS over SSE (permission "events").',
|
|
29
56
|
})}
|
|
30
57
|
</p>
|
|
31
58
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
<
|
|
51
|
-
{
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
{shown.length === 0 ? (
|
|
60
|
+
<Empty
|
|
61
|
+
icon={Activity}
|
|
62
|
+
iconVariant="chip"
|
|
63
|
+
title={t('events.emptyTitle', { fallback: 'Waiting for Events' })}
|
|
64
|
+
description={
|
|
65
|
+
isInsideIframe()
|
|
66
|
+
? t('events.emptyHost', {
|
|
67
|
+
fallback: 'No graph events yet — activity will appear here as it happens.',
|
|
68
|
+
})
|
|
69
|
+
: t('events.emptyStandalone', {
|
|
70
|
+
fallback:
|
|
71
|
+
'Open inside Bardioc (or run standalone with dev auth) to receive live events.',
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
/>
|
|
75
|
+
) : (
|
|
76
|
+
<Card className="min-w-0">
|
|
77
|
+
<ItemGroup>
|
|
78
|
+
{shown.map(event => (
|
|
79
|
+
// A long id truncates (`flex-nowrap` + `ItemTitle truncate`); badge and time keep
|
|
80
|
+
// their width.
|
|
81
|
+
<Item key={`${event.id}-${event.timestamp}`} size="sm" className="flex-nowrap">
|
|
82
|
+
<ItemContent>
|
|
83
|
+
<ItemTitle truncate className="font-mono">
|
|
84
|
+
{event.id}
|
|
85
|
+
</ItemTitle>
|
|
86
|
+
</ItemContent>
|
|
87
|
+
<ItemActions>
|
|
88
|
+
<Badge variant="info" numerical>
|
|
89
|
+
{event.type}
|
|
90
|
+
</Badge>
|
|
91
|
+
</ItemActions>
|
|
92
|
+
<ItemMeta>{new Date(event.timestamp).toLocaleTimeString()}</ItemMeta>
|
|
93
|
+
</Item>
|
|
64
94
|
))}
|
|
65
|
-
</
|
|
66
|
-
|
|
67
|
-
|
|
95
|
+
</ItemGroup>
|
|
96
|
+
</Card>
|
|
97
|
+
)}
|
|
68
98
|
</div>
|
|
69
99
|
);
|
|
70
100
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { GraphEvent } from '@bardioc/app-sdk';
|
|
2
|
+
|
|
3
|
+
const NOW = Date.now();
|
|
4
|
+
const SECOND = 1000;
|
|
5
|
+
|
|
6
|
+
// Sample graph events for the debug menu's "Mock Data" preview: one of each event type, newest
|
|
7
|
+
// first, with a few long ids so the row truncation can be checked without a live stream.
|
|
8
|
+
export const MOCK_GRAPH_EVENTS: GraphEvent[] = [
|
|
9
|
+
{
|
|
10
|
+
id: 'cju16o7cg0001mz775fnoq66i_cln8olk8k014p0186lue9pl3l',
|
|
11
|
+
type: 'WRITE_TIMESERIES',
|
|
12
|
+
timestamp: NOW - 4 * SECOND,
|
|
13
|
+
},
|
|
14
|
+
{ id: 'station-lagoon', type: 'UPDATE', timestamp: NOW - 11 * SECOND },
|
|
15
|
+
{
|
|
16
|
+
id: 'cju16o7cg0001mz775fnoq66i_cjnu3p5tf001bs934wf10n26n',
|
|
17
|
+
type: 'CREATE',
|
|
18
|
+
timestamp: NOW - 27 * SECOND,
|
|
19
|
+
},
|
|
20
|
+
{ id: 'issue-2582', type: 'REPLACE', timestamp: NOW - 48 * SECOND },
|
|
21
|
+
{
|
|
22
|
+
id: 'cju16o7cg0001mz775fnoq66i_edge-station-monitors-sensor',
|
|
23
|
+
type: 'CONNECT',
|
|
24
|
+
timestamp: NOW - 73 * SECOND,
|
|
25
|
+
},
|
|
26
|
+
{ id: 'station-aurora', type: 'UPDATE', timestamp: NOW - 2 * 60 * SECOND },
|
|
27
|
+
{
|
|
28
|
+
id: 'cju16o7cg0001mz775fnoq66i_edge-sensor-reports-station',
|
|
29
|
+
type: 'DISCONNECT',
|
|
30
|
+
timestamp: NOW - 3 * 60 * SECOND,
|
|
31
|
+
},
|
|
32
|
+
{ id: 'sensor-obsolete-7', type: 'DELETE', timestamp: NOW - 5 * 60 * SECOND },
|
|
33
|
+
];
|