@checkstack/healthcheck-frontend 0.12.0 → 0.13.0
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/CHANGELOG.md +103 -0
- package/package.json +12 -11
- package/src/auto-charts/schema-parser.ts +0 -7
- package/src/components/HealthCheckRunsTable.tsx +19 -1
- package/src/components/HealthCheckSystemOverview.tsx +80 -71
- package/src/components/SystemHealthCheckAssignment.tsx +33 -824
- package/src/components/assignments/AssignmentTree.tsx +123 -0
- package/src/components/assignments/ExecutionPanel.tsx +135 -0
- package/src/components/assignments/GeneralPanel.tsx +100 -0
- package/src/components/assignments/RetentionPanel.tsx +157 -0
- package/src/components/assignments/ThresholdsPanel.tsx +254 -0
- package/src/components/editor/EditorTree.tsx +10 -75
- package/src/components/editor/IDEStatusBar.tsx +5 -58
- package/src/hooks/useCollectors.ts +1 -1
- package/src/hooks/useHealthCheckData.ts +4 -0
- package/src/index.tsx +7 -0
- package/src/pages/AssignmentIDEPage.tsx +477 -0
- package/src/pages/HealthCheckConfigPage.tsx +4 -4
- package/src/pages/HealthCheckHistoryDetailPage.tsx +53 -6
- package/src/pages/HealthCheckIDEPage.tsx +10 -18
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,108 @@
|
|
|
1
1
|
# @checkstack/healthcheck-frontend
|
|
2
2
|
|
|
3
|
+
## 0.13.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 26d8bae: Distributed satellite health checks and Assignment IDE page
|
|
8
|
+
|
|
9
|
+
**Satellite System**
|
|
10
|
+
|
|
11
|
+
- New `satellite-backend`, `satellite-common`, `satellite-frontend`, and `satellite` agent packages for distributed health check execution
|
|
12
|
+
- WebSocket-based satellite connectivity with authentication, heartbeats, and live configuration push
|
|
13
|
+
- Satellite management UI with create dialog, status badges, and list page
|
|
14
|
+
|
|
15
|
+
**Live Configuration Updates**
|
|
16
|
+
|
|
17
|
+
- Added `assignmentChanged` hook to `healthcheck-backend` for cross-plugin communication
|
|
18
|
+
- `satellite-backend` subscribes to assignment changes and pushes config updates to connected satellites in real-time
|
|
19
|
+
|
|
20
|
+
**Assignment IDE Page**
|
|
21
|
+
|
|
22
|
+
- Replaced the 1028-line modal-based `SystemHealthCheckAssignment` component with a full-page IDE layout
|
|
23
|
+
- New modular components: `AssignmentTree`, `GeneralPanel`, `ThresholdsPanel`, `RetentionPanel`, `ExecutionPanel`
|
|
24
|
+
- Added unassign capability and sorted assignment lists for stable ordering
|
|
25
|
+
|
|
26
|
+
**Shared IDE Primitives**
|
|
27
|
+
|
|
28
|
+
- Extracted `IDETreeNode`, `IDETreeSection`, `IDEStatusBar`, `IDELayout` to `@checkstack/ui` for cross-plugin reuse
|
|
29
|
+
- Migrated existing health check IDE editor to use shared primitives
|
|
30
|
+
|
|
31
|
+
**Infrastructure**
|
|
32
|
+
|
|
33
|
+
- Added `Dockerfile.satellite` for containerized satellite deployment
|
|
34
|
+
- WebSocket route registry in `@checkstack/backend` and `@checkstack/backend-api`
|
|
35
|
+
|
|
36
|
+
- 26d8bae: Source attribution and filtering for satellite health checks
|
|
37
|
+
|
|
38
|
+
**Source Attribution**
|
|
39
|
+
|
|
40
|
+
- Fixed satellite result attribution: runs from satellites now correctly display their source instead of defaulting to "Local"
|
|
41
|
+
- Added `sourceId` and `sourceLabel` to both public and detailed history API responses
|
|
42
|
+
|
|
43
|
+
**Source Filtering**
|
|
44
|
+
|
|
45
|
+
- Added `sourceFilter` parameter to `getHistory`, `getDetailedHistory`, and `getDetailedAggregatedHistory` RPC endpoints
|
|
46
|
+
- Source filter supports "local" (core-only), specific satellite UUID, or all sources
|
|
47
|
+
- Filter applies to all three aggregation tiers (raw, hourly, daily)
|
|
48
|
+
|
|
49
|
+
**Frontend**
|
|
50
|
+
|
|
51
|
+
- System detail accordion shows source filter buttons (All / Local / per-satellite) next to date range filter
|
|
52
|
+
- Filter applies to both charts and recent runs table
|
|
53
|
+
- Source column added to the recent runs table with Local/Remote badges
|
|
54
|
+
- Health check history detail page includes per-satellite source filter buttons
|
|
55
|
+
|
|
56
|
+
### Patch Changes
|
|
57
|
+
|
|
58
|
+
- Updated dependencies [26d8bae]
|
|
59
|
+
- Updated dependencies [26d8bae]
|
|
60
|
+
- @checkstack/ui@1.3.0
|
|
61
|
+
- @checkstack/healthcheck-common@0.11.0
|
|
62
|
+
- @checkstack/satellite-common@0.2.0
|
|
63
|
+
- @checkstack/auth-frontend@0.5.19
|
|
64
|
+
- @checkstack/dashboard-frontend@0.3.27
|
|
65
|
+
|
|
66
|
+
## 0.12.1
|
|
67
|
+
|
|
68
|
+
### Patch Changes
|
|
69
|
+
|
|
70
|
+
- d1a2796: Enforce stricter code quality standards and eliminate AI slop anti-patterns.
|
|
71
|
+
|
|
72
|
+
**New utility**
|
|
73
|
+
|
|
74
|
+
- `extractErrorMessage(error, fallback?)` in `@checkstack/common` for consistent error extraction
|
|
75
|
+
|
|
76
|
+
**ESLint rules**
|
|
77
|
+
|
|
78
|
+
- `react-hooks/rules-of-hooks` and `exhaustive-deps` for hook correctness
|
|
79
|
+
- `no-console` in frontend packages — forces `toast` over silent `console.error`
|
|
80
|
+
- `no-restricted-syntax` banning `instanceof Error` — forces `extractErrorMessage`
|
|
81
|
+
- Custom `no-eslint-disable-any` rule preventing `@typescript-eslint/no-explicit-any` circumvention
|
|
82
|
+
|
|
83
|
+
**Refactoring**
|
|
84
|
+
|
|
85
|
+
- Replace 141 `instanceof Error` boilerplate patterns across the codebase
|
|
86
|
+
- Replace swallowed `console.error` with user-visible `toast.error()` feedback
|
|
87
|
+
- Remove 15 redundant `as` type casts in IntegrationsPage and ProviderConnectionsPage
|
|
88
|
+
- Consolidate 3 identical callback handlers into `handleDialogClose`
|
|
89
|
+
- Fix conditional React hook call in `FormField.tsx`
|
|
90
|
+
- Fix unstable useMemo deps in `Dashboard.tsx`
|
|
91
|
+
- Replace `useEffect`→`setState` with derived `useMemo` in `RegisterPage.tsx`
|
|
92
|
+
- Rewrite `keystore.test.ts` with typed `DrizzleMockChain` (eliminating 7 `any` suppressions)
|
|
93
|
+
- Delete obvious comments in `encryption.ts` and Teams `provider.ts`
|
|
94
|
+
|
|
95
|
+
- Updated dependencies [d1a2796]
|
|
96
|
+
- Updated dependencies [3c34b07]
|
|
97
|
+
- @checkstack/common@0.6.5
|
|
98
|
+
- @checkstack/ui@1.2.1
|
|
99
|
+
- @checkstack/auth-frontend@0.5.18
|
|
100
|
+
- @checkstack/dashboard-frontend@0.3.26
|
|
101
|
+
- @checkstack/frontend-api@0.3.9
|
|
102
|
+
- @checkstack/catalog-common@1.3.1
|
|
103
|
+
- @checkstack/healthcheck-common@0.10.1
|
|
104
|
+
- @checkstack/signal-frontend@0.0.15
|
|
105
|
+
|
|
3
106
|
## 0.12.0
|
|
4
107
|
|
|
5
108
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/healthcheck-frontend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.tsx",
|
|
6
6
|
"checkstack": {
|
|
@@ -12,14 +12,14 @@
|
|
|
12
12
|
"lint:code": "eslint . --max-warnings 0"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@checkstack/auth-frontend": "0.5.
|
|
16
|
-
"@checkstack/catalog-common": "1.
|
|
17
|
-
"@checkstack/common": "0.6.
|
|
18
|
-
"@checkstack/dashboard-frontend": "0.3.
|
|
19
|
-
"@checkstack/frontend-api": "0.3.
|
|
20
|
-
"@checkstack/healthcheck-common": "0.
|
|
21
|
-
"@checkstack/signal-frontend": "0.0.
|
|
22
|
-
"@checkstack/ui": "1.2.
|
|
15
|
+
"@checkstack/auth-frontend": "0.5.18",
|
|
16
|
+
"@checkstack/catalog-common": "1.3.1",
|
|
17
|
+
"@checkstack/common": "0.6.5",
|
|
18
|
+
"@checkstack/dashboard-frontend": "0.3.26",
|
|
19
|
+
"@checkstack/frontend-api": "0.3.9",
|
|
20
|
+
"@checkstack/healthcheck-common": "0.10.1",
|
|
21
|
+
"@checkstack/signal-frontend": "0.0.15",
|
|
22
|
+
"@checkstack/ui": "1.2.1",
|
|
23
23
|
"ajv": "^8.18.0",
|
|
24
24
|
"ajv-formats": "^3.0.1",
|
|
25
25
|
"date-fns": "^4.1.0",
|
|
@@ -28,11 +28,12 @@
|
|
|
28
28
|
"react-router-dom": "^6.20.0",
|
|
29
29
|
"recharts": "^3.6.0",
|
|
30
30
|
"uuid": "^13.0.0",
|
|
31
|
-
"zod": "^4.2.1"
|
|
31
|
+
"zod": "^4.2.1",
|
|
32
|
+
"@checkstack/satellite-common": "0.1.0"
|
|
32
33
|
},
|
|
33
34
|
"devDependencies": {
|
|
34
35
|
"@checkstack/scripts": "0.1.2",
|
|
35
|
-
"@checkstack/tsconfig": "0.0.
|
|
36
|
+
"@checkstack/tsconfig": "0.0.5",
|
|
36
37
|
"@types/react": "^18.2.0",
|
|
37
38
|
"typescript": "^5.0.0"
|
|
38
39
|
}
|
|
@@ -230,10 +230,6 @@ function extractComputedValue(value: unknown): unknown {
|
|
|
230
230
|
|
|
231
231
|
// _type is required for all aggregated state objects
|
|
232
232
|
if (!("_type" in obj)) {
|
|
233
|
-
console.error(
|
|
234
|
-
"[AutoChart] Missing _type discriminator in aggregated state:",
|
|
235
|
-
obj,
|
|
236
|
-
);
|
|
237
233
|
return value;
|
|
238
234
|
}
|
|
239
235
|
|
|
@@ -252,9 +248,6 @@ function extractComputedValue(value: unknown): unknown {
|
|
|
252
248
|
return obj.max;
|
|
253
249
|
}
|
|
254
250
|
default: {
|
|
255
|
-
console.error(
|
|
256
|
-
`[AutoChart] Unrecognized aggregated state type: ${String(obj._type)}`,
|
|
257
|
-
);
|
|
258
251
|
return value;
|
|
259
252
|
}
|
|
260
253
|
}
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
Pagination,
|
|
11
11
|
} from "@checkstack/ui";
|
|
12
12
|
import { formatDistanceToNow, format } from "date-fns";
|
|
13
|
-
import { ExternalLink, Loader2 } from "lucide-react";
|
|
13
|
+
import { ExternalLink, Loader2, Satellite, Server } from "lucide-react";
|
|
14
14
|
import { useNavigate } from "react-router-dom";
|
|
15
15
|
import { healthcheckRoutes } from "@checkstack/healthcheck-common";
|
|
16
16
|
import { resolveRoute } from "@checkstack/common";
|
|
@@ -22,6 +22,10 @@ export interface HealthCheckRunDetailed {
|
|
|
22
22
|
status: "healthy" | "unhealthy" | "degraded";
|
|
23
23
|
result: Record<string, unknown>;
|
|
24
24
|
timestamp: Date;
|
|
25
|
+
/** Source ID for result attribution (undefined = local core, UUID = satellite) */
|
|
26
|
+
sourceId?: string;
|
|
27
|
+
/** Human-readable source label (e.g. "Local" or "EU West (eu-west-1)") */
|
|
28
|
+
sourceLabel?: string;
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
export interface HealthCheckRunsTableProps {
|
|
@@ -93,6 +97,7 @@ export const HealthCheckRunsTable: React.FC<HealthCheckRunsTableProps> = ({
|
|
|
93
97
|
</>
|
|
94
98
|
)}
|
|
95
99
|
<TableHead>Timestamp</TableHead>
|
|
100
|
+
<TableHead>Source</TableHead>
|
|
96
101
|
{showFilterColumns && <TableHead className="w-16"></TableHead>}
|
|
97
102
|
</TableRow>
|
|
98
103
|
</TableHeader>
|
|
@@ -123,6 +128,19 @@ export const HealthCheckRunsTable: React.FC<HealthCheckRunsTableProps> = ({
|
|
|
123
128
|
})}
|
|
124
129
|
</span>
|
|
125
130
|
</TableCell>
|
|
131
|
+
<TableCell>
|
|
132
|
+
{run.sourceId ? (
|
|
133
|
+
<span className="inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded-full bg-orange-500/10 text-orange-600">
|
|
134
|
+
<Satellite className="h-3 w-3" />
|
|
135
|
+
{run.sourceLabel ?? "Remote"}
|
|
136
|
+
</span>
|
|
137
|
+
) : (
|
|
138
|
+
<span className="inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded-full bg-muted text-muted-foreground">
|
|
139
|
+
<Server className="h-3 w-3" />
|
|
140
|
+
{run.sourceLabel ?? "Local"}
|
|
141
|
+
</span>
|
|
142
|
+
)}
|
|
143
|
+
</TableCell>
|
|
126
144
|
{showFilterColumns && (
|
|
127
145
|
<TableCell>
|
|
128
146
|
<ExternalLink className="h-4 w-4 text-muted-foreground" />
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
healthCheckAccess,
|
|
16
16
|
healthcheckRoutes,
|
|
17
17
|
} from "@checkstack/healthcheck-common";
|
|
18
|
+
import { SatelliteApi, satelliteAccess } from "@checkstack/satellite-common";
|
|
18
19
|
import { resolveRoute } from "@checkstack/common";
|
|
19
20
|
import {
|
|
20
21
|
HealthBadge,
|
|
@@ -36,10 +37,14 @@ import {
|
|
|
36
37
|
CardContent,
|
|
37
38
|
CardHeader,
|
|
38
39
|
CardTitle,
|
|
39
|
-
AnimatedNumber,
|
|
40
40
|
} from "@checkstack/ui";
|
|
41
41
|
import { formatDistanceToNow } from "date-fns";
|
|
42
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
ChevronDown,
|
|
44
|
+
ChevronRight,
|
|
45
|
+
Satellite as SatelliteIcon,
|
|
46
|
+
Server,
|
|
47
|
+
} from "lucide-react";
|
|
43
48
|
import { useNavigate } from "react-router-dom";
|
|
44
49
|
import { HealthCheckSparkline } from "./HealthCheckSparkline";
|
|
45
50
|
import { HealthCheckLatencyChart } from "./HealthCheckLatencyChart";
|
|
@@ -71,30 +76,24 @@ interface ExpandedRowProps {
|
|
|
71
76
|
systemId: string;
|
|
72
77
|
}
|
|
73
78
|
|
|
74
|
-
// Helper to get color class for availability percentage
|
|
75
|
-
const getAvailabilityColorClass = (
|
|
76
|
-
value: number | null,
|
|
77
|
-
totalRuns: number,
|
|
78
|
-
): string => {
|
|
79
|
-
if (value === null || totalRuns === 0) {
|
|
80
|
-
return "text-muted-foreground";
|
|
81
|
-
}
|
|
82
|
-
if (value >= 99.9) {
|
|
83
|
-
return "text-green-600 dark:text-green-400";
|
|
84
|
-
}
|
|
85
|
-
if (value >= 99) {
|
|
86
|
-
return "text-yellow-600 dark:text-yellow-400";
|
|
87
|
-
}
|
|
88
|
-
return "text-red-600 dark:text-red-400";
|
|
89
|
-
};
|
|
90
|
-
|
|
91
79
|
const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
|
|
92
80
|
const healthCheckClient = usePluginClient(HealthCheckApi);
|
|
81
|
+
const satelliteClient = usePluginClient(SatelliteApi);
|
|
93
82
|
const navigate = useNavigate();
|
|
94
83
|
const accessApi = useApi(accessApiRef);
|
|
95
84
|
const { allowed: canViewDetails } = accessApi.useAccess(
|
|
96
85
|
healthCheckAccess.details,
|
|
97
86
|
);
|
|
87
|
+
const { allowed: canReadSatellites } = accessApi.useAccess(
|
|
88
|
+
satelliteAccess.satellite.read,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// Fetch satellites for source filter (only if user has access)
|
|
92
|
+
const { data: satellitesData } = satelliteClient.listSatellites.useQuery(
|
|
93
|
+
{},
|
|
94
|
+
{ enabled: canReadSatellites },
|
|
95
|
+
);
|
|
96
|
+
const satellites = satellitesData?.satellites ?? [];
|
|
98
97
|
|
|
99
98
|
// Date range state for filtering - default to last 24 hours
|
|
100
99
|
const [dateRange, setDateRange] = useState(() =>
|
|
@@ -102,6 +101,7 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
|
|
|
102
101
|
);
|
|
103
102
|
// Track if a rolling preset is active (vs custom range)
|
|
104
103
|
const [isRollingPreset, setIsRollingPreset] = useState(true);
|
|
104
|
+
const [sourceFilter, setSourceFilter] = useState<string | undefined>();
|
|
105
105
|
|
|
106
106
|
// Callback to handle date range changes from the filter
|
|
107
107
|
const handleDateRangeChange = useCallback(
|
|
@@ -155,6 +155,7 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
|
|
|
155
155
|
configurationId: item.configurationId,
|
|
156
156
|
strategyId: item.strategyId,
|
|
157
157
|
dateRange,
|
|
158
|
+
sourceFilter,
|
|
158
159
|
isRollingPreset,
|
|
159
160
|
// Update endDate to current time when new runs are detected (only for rolling presets)
|
|
160
161
|
onDateRangeRefresh: (newEndDate) => {
|
|
@@ -177,6 +178,7 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
|
|
|
177
178
|
offset: pagination.offset,
|
|
178
179
|
startDate: dateRange.startDate,
|
|
179
180
|
// Don't pass endDate - backend defaults to 'now' so new runs are included
|
|
181
|
+
sourceFilter,
|
|
180
182
|
sortOrder: "desc",
|
|
181
183
|
});
|
|
182
184
|
|
|
@@ -193,12 +195,11 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
|
|
|
193
195
|
}
|
|
194
196
|
const runs = displayRuns;
|
|
195
197
|
|
|
196
|
-
// Listen for realtime health check updates to refresh history table
|
|
198
|
+
// Listen for realtime health check updates to refresh history table
|
|
197
199
|
// Charts are refreshed automatically by useHealthCheckData
|
|
198
200
|
useSignal(HEALTH_CHECK_RUN_COMPLETED, ({ systemId: changedId }) => {
|
|
199
201
|
if (changedId === systemId) {
|
|
200
202
|
void refetch();
|
|
201
|
-
void refetchAvailability();
|
|
202
203
|
}
|
|
203
204
|
});
|
|
204
205
|
|
|
@@ -208,13 +209,6 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
|
|
|
208
209
|
: `Window mode (${item.stateThresholds.windowSize} runs): Degraded at ${item.stateThresholds.degraded.minFailureCount}+ failures, Unhealthy at ${item.stateThresholds.unhealthy.minFailureCount}+ failures`
|
|
209
210
|
: "Using default thresholds";
|
|
210
211
|
|
|
211
|
-
// Fetch availability stats
|
|
212
|
-
const { data: availabilityData, refetch: refetchAvailability } =
|
|
213
|
-
healthCheckClient.getAvailabilityStats.useQuery({
|
|
214
|
-
systemId,
|
|
215
|
-
configurationId: item.configurationId,
|
|
216
|
-
});
|
|
217
|
-
|
|
218
212
|
// Render charts - charts handle data transformation internally
|
|
219
213
|
const renderCharts = () => {
|
|
220
214
|
if (chartLoading) {
|
|
@@ -289,49 +283,6 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
|
|
|
289
283
|
</div>
|
|
290
284
|
</div>
|
|
291
285
|
|
|
292
|
-
{/* Availability Stats - Prominent Display */}
|
|
293
|
-
{availabilityData && (
|
|
294
|
-
<div className="grid grid-cols-2 gap-4">
|
|
295
|
-
<div className="flex flex-col gap-1 p-4 rounded-lg border bg-card">
|
|
296
|
-
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
|
297
|
-
31-Day Availability
|
|
298
|
-
</span>
|
|
299
|
-
<div className="flex items-baseline gap-2">
|
|
300
|
-
<AnimatedNumber
|
|
301
|
-
value={availabilityData.availability31Days ?? undefined}
|
|
302
|
-
suffix="%"
|
|
303
|
-
className={`text-2xl font-bold ${getAvailabilityColorClass(availabilityData.availability31Days, availabilityData.totalRuns31Days)}`}
|
|
304
|
-
/>
|
|
305
|
-
{availabilityData.totalRuns31Days > 0 && (
|
|
306
|
-
<span className="text-sm text-muted-foreground">
|
|
307
|
-
({availabilityData.totalRuns31Days.toLocaleString()} runs)
|
|
308
|
-
</span>
|
|
309
|
-
)}
|
|
310
|
-
</div>
|
|
311
|
-
</div>
|
|
312
|
-
<div className="flex flex-col gap-1 p-4 rounded-lg border bg-card">
|
|
313
|
-
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
|
314
|
-
365-Day Availability
|
|
315
|
-
</span>
|
|
316
|
-
<div className="flex items-baseline gap-2">
|
|
317
|
-
<AnimatedNumber
|
|
318
|
-
value={availabilityData.availability365Days ?? undefined}
|
|
319
|
-
suffix="%"
|
|
320
|
-
className={`text-2xl font-bold ${getAvailabilityColorClass(availabilityData.availability365Days, availabilityData.totalRuns365Days)}`}
|
|
321
|
-
/>
|
|
322
|
-
{availabilityData.totalRuns365Days > 0 && (
|
|
323
|
-
<span className="text-sm text-muted-foreground">
|
|
324
|
-
({availabilityData.totalRuns365Days.toLocaleString()} runs)
|
|
325
|
-
</span>
|
|
326
|
-
)}
|
|
327
|
-
</div>
|
|
328
|
-
</div>
|
|
329
|
-
</div>
|
|
330
|
-
)}
|
|
331
|
-
|
|
332
|
-
{/* Divider */}
|
|
333
|
-
<div className="h-px bg-border" />
|
|
334
|
-
|
|
335
286
|
{/* Date Range Filter with Loading Spinner */}
|
|
336
287
|
<div className="flex items-center gap-3 flex-wrap">
|
|
337
288
|
<DateRangeFilter
|
|
@@ -356,6 +307,49 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
|
|
|
356
307
|
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
|
357
308
|
)}
|
|
358
309
|
</div>
|
|
310
|
+
{/* Source filter (visible when satellites exist and user has read access) */}
|
|
311
|
+
{canReadSatellites && satellites.length > 0 && (
|
|
312
|
+
<div className="flex items-center gap-2">
|
|
313
|
+
<span className="text-xs text-muted-foreground">Source:</span>
|
|
314
|
+
<div className="flex items-center gap-1">
|
|
315
|
+
<button
|
|
316
|
+
onClick={() => setSourceFilter(undefined)}
|
|
317
|
+
className={`inline-flex items-center gap-1 text-xs px-2 py-1 rounded-full transition-colors ${
|
|
318
|
+
sourceFilter === undefined
|
|
319
|
+
? "bg-primary text-primary-foreground"
|
|
320
|
+
: "bg-muted text-muted-foreground hover:bg-muted/80"
|
|
321
|
+
}`}
|
|
322
|
+
>
|
|
323
|
+
All
|
|
324
|
+
</button>
|
|
325
|
+
<button
|
|
326
|
+
onClick={() => setSourceFilter("local")}
|
|
327
|
+
className={`inline-flex items-center gap-1 text-xs px-2 py-1 rounded-full transition-colors ${
|
|
328
|
+
sourceFilter === "local"
|
|
329
|
+
? "bg-primary text-primary-foreground"
|
|
330
|
+
: "bg-muted text-muted-foreground hover:bg-muted/80"
|
|
331
|
+
}`}
|
|
332
|
+
>
|
|
333
|
+
<Server className="h-3 w-3" />
|
|
334
|
+
Local
|
|
335
|
+
</button>
|
|
336
|
+
{satellites.map((sat) => (
|
|
337
|
+
<button
|
|
338
|
+
key={sat.id}
|
|
339
|
+
onClick={() => setSourceFilter(sat.id)}
|
|
340
|
+
className={`inline-flex items-center gap-1 text-xs px-2 py-1 rounded-full transition-colors ${
|
|
341
|
+
sourceFilter === sat.id
|
|
342
|
+
? "bg-orange-500 text-white"
|
|
343
|
+
: "bg-orange-500/10 text-orange-600 hover:bg-orange-500/20"
|
|
344
|
+
}`}
|
|
345
|
+
>
|
|
346
|
+
<SatelliteIcon className="h-3 w-3" />
|
|
347
|
+
{sat.name}
|
|
348
|
+
</button>
|
|
349
|
+
))}
|
|
350
|
+
</div>
|
|
351
|
+
</div>
|
|
352
|
+
)}
|
|
359
353
|
|
|
360
354
|
{/* Charts Section */}
|
|
361
355
|
{renderCharts()}
|
|
@@ -373,12 +367,14 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
|
|
|
373
367
|
</span>
|
|
374
368
|
<div className="flex-1 h-px bg-border" />
|
|
375
369
|
</div>
|
|
370
|
+
|
|
376
371
|
<div className="rounded-md border">
|
|
377
372
|
<Table>
|
|
378
373
|
<TableHeader>
|
|
379
374
|
<TableRow>
|
|
380
375
|
<TableHead className="w-24">Status</TableHead>
|
|
381
376
|
<TableHead>Time</TableHead>
|
|
377
|
+
<TableHead>Source</TableHead>
|
|
382
378
|
</TableRow>
|
|
383
379
|
</TableHeader>
|
|
384
380
|
<TableBody>
|
|
@@ -412,6 +408,19 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
|
|
|
412
408
|
addSuffix: true,
|
|
413
409
|
})}
|
|
414
410
|
</TableCell>
|
|
411
|
+
<TableCell>
|
|
412
|
+
{run.sourceId ? (
|
|
413
|
+
<span className="inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded-full bg-orange-500/10 text-orange-600">
|
|
414
|
+
<SatelliteIcon className="h-3 w-3" />
|
|
415
|
+
{run.sourceLabel ?? "Remote"}
|
|
416
|
+
</span>
|
|
417
|
+
) : (
|
|
418
|
+
<span className="inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded-full bg-muted text-muted-foreground">
|
|
419
|
+
<Server className="h-3 w-3" />
|
|
420
|
+
{run.sourceLabel ?? "Local"}
|
|
421
|
+
</span>
|
|
422
|
+
)}
|
|
423
|
+
</TableCell>
|
|
415
424
|
</TableRow>
|
|
416
425
|
))}
|
|
417
426
|
</TableBody>
|