@checkstack/healthcheck-frontend 0.0.2
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 +135 -0
- package/package.json +36 -0
- package/src/api.ts +17 -0
- package/src/auto-charts/AutoChartGrid.tsx +383 -0
- package/src/auto-charts/extension.tsx +27 -0
- package/src/auto-charts/index.ts +12 -0
- package/src/auto-charts/schema-parser.ts +121 -0
- package/src/auto-charts/useStrategySchemas.ts +62 -0
- package/src/components/AggregatedDataBanner.tsx +24 -0
- package/src/components/HealthCheckDiagram.tsx +88 -0
- package/src/components/HealthCheckEditor.tsx +136 -0
- package/src/components/HealthCheckHistory.tsx +79 -0
- package/src/components/HealthCheckLatencyChart.tsx +168 -0
- package/src/components/HealthCheckList.tsx +84 -0
- package/src/components/HealthCheckMenuItems.tsx +33 -0
- package/src/components/HealthCheckRunsTable.tsx +187 -0
- package/src/components/HealthCheckSparkline.tsx +46 -0
- package/src/components/HealthCheckStatusTimeline.tsx +190 -0
- package/src/components/HealthCheckSystemOverview.tsx +380 -0
- package/src/components/SystemHealthBadge.tsx +46 -0
- package/src/components/SystemHealthCheckAssignment.tsx +869 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useHealthCheckData.ts +257 -0
- package/src/index.tsx +99 -0
- package/src/pages/HealthCheckConfigPage.tsx +164 -0
- package/src/pages/HealthCheckHistoryDetailPage.tsx +100 -0
- package/src/pages/HealthCheckHistoryPage.tsx +67 -0
- package/src/slots.tsx +185 -0
- package/tsconfig.json +6 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useHealthCheckData } from "./useHealthCheckData";
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { useEffect, useState, useMemo } from "react";
|
|
2
|
+
import { useApi, permissionApiRef } from "@checkstack/frontend-api";
|
|
3
|
+
import { healthCheckApiRef } from "../api";
|
|
4
|
+
import {
|
|
5
|
+
permissions,
|
|
6
|
+
DEFAULT_RETENTION_CONFIG,
|
|
7
|
+
type RetentionConfig,
|
|
8
|
+
} from "@checkstack/healthcheck-common";
|
|
9
|
+
import type {
|
|
10
|
+
HealthCheckDiagramSlotContext,
|
|
11
|
+
TypedHealthCheckRun,
|
|
12
|
+
TypedAggregatedBucket,
|
|
13
|
+
} from "../slots";
|
|
14
|
+
|
|
15
|
+
interface UseHealthCheckDataProps {
|
|
16
|
+
systemId: string;
|
|
17
|
+
configurationId: string;
|
|
18
|
+
strategyId: string;
|
|
19
|
+
dateRange: {
|
|
20
|
+
startDate: Date;
|
|
21
|
+
endDate: Date;
|
|
22
|
+
};
|
|
23
|
+
/** Pagination for raw data mode */
|
|
24
|
+
limit?: number;
|
|
25
|
+
offset?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface UseHealthCheckDataResult {
|
|
29
|
+
/** The context to pass to HealthCheckDiagramSlot */
|
|
30
|
+
context: HealthCheckDiagramSlotContext | undefined;
|
|
31
|
+
/** Whether data is currently loading */
|
|
32
|
+
loading: boolean;
|
|
33
|
+
/** Whether aggregated data mode is active */
|
|
34
|
+
isAggregated: boolean;
|
|
35
|
+
/** The resolved retention config */
|
|
36
|
+
retentionConfig: RetentionConfig;
|
|
37
|
+
/** Whether user has permission to view detailed data */
|
|
38
|
+
hasPermission: boolean;
|
|
39
|
+
/** Whether permission is still loading */
|
|
40
|
+
permissionLoading: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Hook that handles fetching health check data for visualization.
|
|
45
|
+
* Automatically determines whether to use raw or aggregated data based on:
|
|
46
|
+
* - The selected date range
|
|
47
|
+
* - The configured rawRetentionDays for the assignment
|
|
48
|
+
*
|
|
49
|
+
* Returns a ready-to-use context for HealthCheckDiagramSlot.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```tsx
|
|
53
|
+
* const { context, loading, hasPermission } = useHealthCheckData({
|
|
54
|
+
* systemId,
|
|
55
|
+
* configurationId,
|
|
56
|
+
* strategyId,
|
|
57
|
+
* dateRange: { startDate, endDate },
|
|
58
|
+
* });
|
|
59
|
+
*
|
|
60
|
+
* if (!hasPermission) return <NoPermissionMessage />;
|
|
61
|
+
* if (loading) return <LoadingSpinner />;
|
|
62
|
+
* if (!context) return null;
|
|
63
|
+
*
|
|
64
|
+
* return <ExtensionSlot slot={HealthCheckDiagramSlot} context={context} />;
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export function useHealthCheckData({
|
|
68
|
+
systemId,
|
|
69
|
+
configurationId,
|
|
70
|
+
strategyId,
|
|
71
|
+
dateRange,
|
|
72
|
+
limit = 100,
|
|
73
|
+
offset = 0,
|
|
74
|
+
}: UseHealthCheckDataProps): UseHealthCheckDataResult {
|
|
75
|
+
const api = useApi(healthCheckApiRef);
|
|
76
|
+
const permissionApi = useApi(permissionApiRef);
|
|
77
|
+
|
|
78
|
+
// Permission state
|
|
79
|
+
const { allowed: hasPermission, loading: permissionLoading } =
|
|
80
|
+
permissionApi.usePermission(permissions.healthCheckDetailsRead.id);
|
|
81
|
+
|
|
82
|
+
// Retention config state
|
|
83
|
+
const [retentionConfig, setRetentionConfig] = useState<RetentionConfig>(
|
|
84
|
+
DEFAULT_RETENTION_CONFIG
|
|
85
|
+
);
|
|
86
|
+
const [retentionLoading, setRetentionLoading] = useState(true);
|
|
87
|
+
|
|
88
|
+
// Raw data state
|
|
89
|
+
const [rawRuns, setRawRuns] = useState<
|
|
90
|
+
TypedHealthCheckRun<Record<string, unknown>>[]
|
|
91
|
+
>([]);
|
|
92
|
+
const [rawLoading, setRawLoading] = useState(false);
|
|
93
|
+
|
|
94
|
+
// Aggregated data state
|
|
95
|
+
const [aggregatedBuckets, setAggregatedBuckets] = useState<
|
|
96
|
+
TypedAggregatedBucket<Record<string, unknown>>[]
|
|
97
|
+
>([]);
|
|
98
|
+
const [aggregatedLoading, setAggregatedLoading] = useState(false);
|
|
99
|
+
|
|
100
|
+
// Calculate date range in days
|
|
101
|
+
const dateRangeDays = useMemo(() => {
|
|
102
|
+
return Math.ceil(
|
|
103
|
+
(dateRange.endDate.getTime() - dateRange.startDate.getTime()) /
|
|
104
|
+
(1000 * 60 * 60 * 24)
|
|
105
|
+
);
|
|
106
|
+
}, [dateRange.startDate, dateRange.endDate]);
|
|
107
|
+
|
|
108
|
+
// Determine if we should use aggregated data
|
|
109
|
+
const isAggregated = dateRangeDays > retentionConfig.rawRetentionDays;
|
|
110
|
+
|
|
111
|
+
// Fetch retention config on mount
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
setRetentionLoading(true);
|
|
114
|
+
api
|
|
115
|
+
.getRetentionConfig({ systemId, configurationId })
|
|
116
|
+
.then((response) =>
|
|
117
|
+
setRetentionConfig(response.retentionConfig ?? DEFAULT_RETENTION_CONFIG)
|
|
118
|
+
)
|
|
119
|
+
.catch(() => {
|
|
120
|
+
// Fall back to default on error
|
|
121
|
+
setRetentionConfig(DEFAULT_RETENTION_CONFIG);
|
|
122
|
+
})
|
|
123
|
+
.finally(() => setRetentionLoading(false));
|
|
124
|
+
}, [api, systemId, configurationId]);
|
|
125
|
+
|
|
126
|
+
// Fetch raw data when in raw mode
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
if (!hasPermission || permissionLoading || retentionLoading || isAggregated)
|
|
129
|
+
return;
|
|
130
|
+
|
|
131
|
+
setRawLoading(true);
|
|
132
|
+
api
|
|
133
|
+
.getDetailedHistory({
|
|
134
|
+
systemId,
|
|
135
|
+
configurationId,
|
|
136
|
+
startDate: dateRange.startDate,
|
|
137
|
+
endDate: dateRange.endDate,
|
|
138
|
+
limit,
|
|
139
|
+
offset,
|
|
140
|
+
})
|
|
141
|
+
.then((response) => {
|
|
142
|
+
setRawRuns(
|
|
143
|
+
response.runs.map((r) => ({
|
|
144
|
+
id: r.id,
|
|
145
|
+
configurationId,
|
|
146
|
+
systemId,
|
|
147
|
+
status: r.status,
|
|
148
|
+
timestamp: r.timestamp,
|
|
149
|
+
latencyMs: r.latencyMs,
|
|
150
|
+
result: r.result,
|
|
151
|
+
}))
|
|
152
|
+
);
|
|
153
|
+
})
|
|
154
|
+
.finally(() => setRawLoading(false));
|
|
155
|
+
}, [
|
|
156
|
+
api,
|
|
157
|
+
systemId,
|
|
158
|
+
configurationId,
|
|
159
|
+
hasPermission,
|
|
160
|
+
permissionLoading,
|
|
161
|
+
retentionLoading,
|
|
162
|
+
isAggregated,
|
|
163
|
+
dateRange.startDate,
|
|
164
|
+
dateRange.endDate,
|
|
165
|
+
limit,
|
|
166
|
+
offset,
|
|
167
|
+
]);
|
|
168
|
+
|
|
169
|
+
// Fetch aggregated data when in aggregated mode
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
if (
|
|
172
|
+
!hasPermission ||
|
|
173
|
+
permissionLoading ||
|
|
174
|
+
retentionLoading ||
|
|
175
|
+
!isAggregated
|
|
176
|
+
)
|
|
177
|
+
return;
|
|
178
|
+
|
|
179
|
+
setAggregatedLoading(true);
|
|
180
|
+
// Use daily buckets for ranges > 30 days, hourly otherwise
|
|
181
|
+
const bucketSize = dateRangeDays > 30 ? "daily" : "hourly";
|
|
182
|
+
// Use detailed endpoint to get aggregatedResult since we have permission
|
|
183
|
+
api
|
|
184
|
+
.getDetailedAggregatedHistory({
|
|
185
|
+
systemId,
|
|
186
|
+
configurationId,
|
|
187
|
+
startDate: dateRange.startDate,
|
|
188
|
+
endDate: dateRange.endDate,
|
|
189
|
+
bucketSize,
|
|
190
|
+
})
|
|
191
|
+
.then((response) => {
|
|
192
|
+
setAggregatedBuckets(
|
|
193
|
+
response.buckets as TypedAggregatedBucket<Record<string, unknown>>[]
|
|
194
|
+
);
|
|
195
|
+
})
|
|
196
|
+
.finally(() => setAggregatedLoading(false));
|
|
197
|
+
}, [
|
|
198
|
+
api,
|
|
199
|
+
systemId,
|
|
200
|
+
configurationId,
|
|
201
|
+
hasPermission,
|
|
202
|
+
permissionLoading,
|
|
203
|
+
retentionLoading,
|
|
204
|
+
isAggregated,
|
|
205
|
+
dateRangeDays,
|
|
206
|
+
dateRange.startDate,
|
|
207
|
+
dateRange.endDate,
|
|
208
|
+
]);
|
|
209
|
+
|
|
210
|
+
const context = useMemo((): HealthCheckDiagramSlotContext | undefined => {
|
|
211
|
+
if (!hasPermission || permissionLoading || retentionLoading) {
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (isAggregated) {
|
|
216
|
+
return {
|
|
217
|
+
type: "aggregated",
|
|
218
|
+
systemId,
|
|
219
|
+
configurationId,
|
|
220
|
+
strategyId,
|
|
221
|
+
buckets: aggregatedBuckets,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
type: "raw",
|
|
227
|
+
systemId,
|
|
228
|
+
configurationId,
|
|
229
|
+
strategyId,
|
|
230
|
+
runs: rawRuns,
|
|
231
|
+
};
|
|
232
|
+
}, [
|
|
233
|
+
hasPermission,
|
|
234
|
+
permissionLoading,
|
|
235
|
+
retentionLoading,
|
|
236
|
+
isAggregated,
|
|
237
|
+
systemId,
|
|
238
|
+
configurationId,
|
|
239
|
+
strategyId,
|
|
240
|
+
rawRuns,
|
|
241
|
+
aggregatedBuckets,
|
|
242
|
+
]);
|
|
243
|
+
|
|
244
|
+
const loading =
|
|
245
|
+
permissionLoading ||
|
|
246
|
+
retentionLoading ||
|
|
247
|
+
(isAggregated ? aggregatedLoading : rawLoading);
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
context,
|
|
251
|
+
loading,
|
|
252
|
+
isAggregated,
|
|
253
|
+
retentionConfig,
|
|
254
|
+
hasPermission,
|
|
255
|
+
permissionLoading,
|
|
256
|
+
};
|
|
257
|
+
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createFrontendPlugin,
|
|
3
|
+
createSlotExtension,
|
|
4
|
+
rpcApiRef,
|
|
5
|
+
type ApiRef,
|
|
6
|
+
UserMenuItemsSlot,
|
|
7
|
+
} from "@checkstack/frontend-api";
|
|
8
|
+
import { healthCheckApiRef, type HealthCheckApiClient } from "./api";
|
|
9
|
+
import { HealthCheckConfigPage } from "./pages/HealthCheckConfigPage";
|
|
10
|
+
import { HealthCheckHistoryPage } from "./pages/HealthCheckHistoryPage";
|
|
11
|
+
import { HealthCheckHistoryDetailPage } from "./pages/HealthCheckHistoryDetailPage";
|
|
12
|
+
import { HealthCheckMenuItems } from "./components/HealthCheckMenuItems";
|
|
13
|
+
import { HealthCheckSystemOverview } from "./components/HealthCheckSystemOverview";
|
|
14
|
+
import { SystemHealthCheckAssignment } from "./components/SystemHealthCheckAssignment";
|
|
15
|
+
import { SystemHealthBadge } from "./components/SystemHealthBadge";
|
|
16
|
+
import { permissions } from "@checkstack/healthcheck-common";
|
|
17
|
+
import { autoChartExtension } from "./auto-charts";
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
SystemDetailsSlot,
|
|
21
|
+
CatalogSystemActionsSlot,
|
|
22
|
+
SystemStateBadgesSlot,
|
|
23
|
+
} from "@checkstack/catalog-common";
|
|
24
|
+
import {
|
|
25
|
+
healthcheckRoutes,
|
|
26
|
+
HealthCheckApi,
|
|
27
|
+
pluginMetadata,
|
|
28
|
+
} from "@checkstack/healthcheck-common";
|
|
29
|
+
|
|
30
|
+
// Export slot definitions for other plugins to use
|
|
31
|
+
export {
|
|
32
|
+
HealthCheckDiagramSlot,
|
|
33
|
+
createStrategyDiagramExtension,
|
|
34
|
+
createDiagramExtensionFactory,
|
|
35
|
+
type HealthCheckDiagramSlotContext,
|
|
36
|
+
type RawDiagramContext,
|
|
37
|
+
type AggregatedDiagramContext,
|
|
38
|
+
type TypedHealthCheckRun,
|
|
39
|
+
type TypedAggregatedBucket,
|
|
40
|
+
} from "./slots";
|
|
41
|
+
|
|
42
|
+
// Export hooks for reusable data fetching
|
|
43
|
+
export { useHealthCheckData } from "./hooks";
|
|
44
|
+
|
|
45
|
+
export default createFrontendPlugin({
|
|
46
|
+
metadata: pluginMetadata,
|
|
47
|
+
routes: [
|
|
48
|
+
{
|
|
49
|
+
route: healthcheckRoutes.routes.config,
|
|
50
|
+
element: <HealthCheckConfigPage />,
|
|
51
|
+
title: "Health Checks",
|
|
52
|
+
permission: permissions.healthCheckManage,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
route: healthcheckRoutes.routes.history,
|
|
56
|
+
element: <HealthCheckHistoryPage />,
|
|
57
|
+
title: "Health Check History",
|
|
58
|
+
permission: permissions.healthCheckRead,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
route: healthcheckRoutes.routes.historyDetail,
|
|
62
|
+
element: <HealthCheckHistoryDetailPage />,
|
|
63
|
+
title: "Health Check Detail",
|
|
64
|
+
permission: permissions.healthCheckDetailsRead,
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
apis: [
|
|
68
|
+
{
|
|
69
|
+
ref: healthCheckApiRef,
|
|
70
|
+
factory: (deps: {
|
|
71
|
+
get: <T>(ref: ApiRef<T>) => T;
|
|
72
|
+
}): HealthCheckApiClient => {
|
|
73
|
+
const rpcApi = deps.get(rpcApiRef);
|
|
74
|
+
// HealthCheckApiClient is just the RPC contract - return it directly
|
|
75
|
+
return rpcApi.forPlugin(HealthCheckApi);
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
extensions: [
|
|
80
|
+
createSlotExtension(UserMenuItemsSlot, {
|
|
81
|
+
id: "healthcheck.user-menu.items",
|
|
82
|
+
component: HealthCheckMenuItems,
|
|
83
|
+
}),
|
|
84
|
+
createSlotExtension(SystemStateBadgesSlot, {
|
|
85
|
+
id: "healthcheck.system-health-badge",
|
|
86
|
+
component: SystemHealthBadge,
|
|
87
|
+
}),
|
|
88
|
+
createSlotExtension(SystemDetailsSlot, {
|
|
89
|
+
id: "healthcheck.system-details.overview",
|
|
90
|
+
component: HealthCheckSystemOverview,
|
|
91
|
+
}),
|
|
92
|
+
createSlotExtension(CatalogSystemActionsSlot, {
|
|
93
|
+
id: "healthcheck.catalog.system-actions",
|
|
94
|
+
component: SystemHealthCheckAssignment,
|
|
95
|
+
}),
|
|
96
|
+
// Auto-generated charts based on schema metadata
|
|
97
|
+
autoChartExtension,
|
|
98
|
+
],
|
|
99
|
+
});
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { useSearchParams } from "react-router-dom";
|
|
3
|
+
import {
|
|
4
|
+
useApi,
|
|
5
|
+
wrapInSuspense,
|
|
6
|
+
permissionApiRef,
|
|
7
|
+
} from "@checkstack/frontend-api";
|
|
8
|
+
import { healthCheckApiRef } from "../api";
|
|
9
|
+
import {
|
|
10
|
+
HealthCheckConfiguration,
|
|
11
|
+
HealthCheckStrategyDto,
|
|
12
|
+
CreateHealthCheckConfiguration,
|
|
13
|
+
healthcheckRoutes,
|
|
14
|
+
} from "@checkstack/healthcheck-common";
|
|
15
|
+
import { HealthCheckList } from "../components/HealthCheckList";
|
|
16
|
+
import { HealthCheckEditor } from "../components/HealthCheckEditor";
|
|
17
|
+
import { Button, ConfirmationModal, PageLayout } from "@checkstack/ui";
|
|
18
|
+
import { Plus, History } from "lucide-react";
|
|
19
|
+
import { Link } from "react-router-dom";
|
|
20
|
+
import { resolveRoute } from "@checkstack/common";
|
|
21
|
+
|
|
22
|
+
const HealthCheckConfigPageContent = () => {
|
|
23
|
+
const api = useApi(healthCheckApiRef);
|
|
24
|
+
const permissionApi = useApi(permissionApiRef);
|
|
25
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
26
|
+
const { allowed: canRead, loading: permissionLoading } =
|
|
27
|
+
permissionApi.useResourcePermission("healthcheck", "read");
|
|
28
|
+
const { allowed: canManage } = permissionApi.useResourcePermission(
|
|
29
|
+
"healthcheck",
|
|
30
|
+
"manage"
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const [configurations, setConfigurations] = useState<
|
|
34
|
+
HealthCheckConfiguration[]
|
|
35
|
+
>([]);
|
|
36
|
+
const [strategies, setStrategies] = useState<HealthCheckStrategyDto[]>([]);
|
|
37
|
+
const [isEditorOpen, setIsEditorOpen] = useState(false);
|
|
38
|
+
const [editingConfig, setEditingConfig] = useState<
|
|
39
|
+
HealthCheckConfiguration | undefined
|
|
40
|
+
>();
|
|
41
|
+
|
|
42
|
+
// Delete modal state
|
|
43
|
+
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
|
44
|
+
const [idToDelete, setIdToDelete] = useState<string | undefined>();
|
|
45
|
+
const [isDeleting, setIsDeleting] = useState(false);
|
|
46
|
+
|
|
47
|
+
const fetchData = async () => {
|
|
48
|
+
const [configs, strats] = await Promise.all([
|
|
49
|
+
api.getConfigurations(),
|
|
50
|
+
api.getStrategies(),
|
|
51
|
+
]);
|
|
52
|
+
setConfigurations(configs);
|
|
53
|
+
setStrategies(strats);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
fetchData();
|
|
58
|
+
}, [api]);
|
|
59
|
+
|
|
60
|
+
// Handle ?action=create URL parameter (from command palette)
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (searchParams.get("action") === "create" && canManage) {
|
|
63
|
+
setEditingConfig(undefined);
|
|
64
|
+
setIsEditorOpen(true);
|
|
65
|
+
// Clear the URL param after opening
|
|
66
|
+
searchParams.delete("action");
|
|
67
|
+
setSearchParams(searchParams, { replace: true });
|
|
68
|
+
}
|
|
69
|
+
}, [searchParams, canManage, setSearchParams]);
|
|
70
|
+
|
|
71
|
+
const handleCreate = () => {
|
|
72
|
+
setEditingConfig(undefined);
|
|
73
|
+
setIsEditorOpen(true);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const handleEdit = (config: HealthCheckConfiguration) => {
|
|
77
|
+
setEditingConfig(config);
|
|
78
|
+
setIsEditorOpen(true);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const handleDelete = (id: string) => {
|
|
82
|
+
setIdToDelete(id);
|
|
83
|
+
setIsDeleteModalOpen(true);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const confirmDelete = async () => {
|
|
87
|
+
if (!idToDelete) return;
|
|
88
|
+
setIsDeleting(true);
|
|
89
|
+
try {
|
|
90
|
+
await api.deleteConfiguration(idToDelete);
|
|
91
|
+
await fetchData();
|
|
92
|
+
} finally {
|
|
93
|
+
setIsDeleting(false);
|
|
94
|
+
setIsDeleteModalOpen(false);
|
|
95
|
+
setIdToDelete(undefined);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const handleSave = async (data: CreateHealthCheckConfiguration) => {
|
|
100
|
+
await (editingConfig
|
|
101
|
+
? api.updateConfiguration({ id: editingConfig.id, body: data })
|
|
102
|
+
: api.createConfiguration(data));
|
|
103
|
+
setIsEditorOpen(false);
|
|
104
|
+
await fetchData();
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const handleEditorClose = () => {
|
|
108
|
+
setIsEditorOpen(false);
|
|
109
|
+
setEditingConfig(undefined);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<PageLayout
|
|
114
|
+
title="Health Checks"
|
|
115
|
+
subtitle="Manage health check configurations"
|
|
116
|
+
loading={permissionLoading}
|
|
117
|
+
allowed={canRead}
|
|
118
|
+
actions={
|
|
119
|
+
<div className="flex gap-2">
|
|
120
|
+
{canManage && (
|
|
121
|
+
<Button variant="outline" asChild>
|
|
122
|
+
<Link to={resolveRoute(healthcheckRoutes.routes.history)}>
|
|
123
|
+
<History className="mr-2 h-4 w-4" /> View History
|
|
124
|
+
</Link>
|
|
125
|
+
</Button>
|
|
126
|
+
)}
|
|
127
|
+
<Button onClick={handleCreate}>
|
|
128
|
+
<Plus className="mr-2 h-4 w-4" /> Create Check
|
|
129
|
+
</Button>
|
|
130
|
+
</div>
|
|
131
|
+
}
|
|
132
|
+
>
|
|
133
|
+
<HealthCheckList
|
|
134
|
+
configurations={configurations}
|
|
135
|
+
strategies={strategies}
|
|
136
|
+
onEdit={handleEdit}
|
|
137
|
+
onDelete={handleDelete}
|
|
138
|
+
/>
|
|
139
|
+
|
|
140
|
+
<HealthCheckEditor
|
|
141
|
+
open={isEditorOpen}
|
|
142
|
+
strategies={strategies}
|
|
143
|
+
initialData={editingConfig}
|
|
144
|
+
onSave={handleSave}
|
|
145
|
+
onCancel={handleEditorClose}
|
|
146
|
+
/>
|
|
147
|
+
|
|
148
|
+
<ConfirmationModal
|
|
149
|
+
isOpen={isDeleteModalOpen}
|
|
150
|
+
onClose={() => setIsDeleteModalOpen(false)}
|
|
151
|
+
onConfirm={confirmDelete}
|
|
152
|
+
title="Delete Health Check"
|
|
153
|
+
message="Are you sure you want to delete this health check configuration? This action cannot be undone."
|
|
154
|
+
confirmText="Delete"
|
|
155
|
+
variant="danger"
|
|
156
|
+
isLoading={isDeleting}
|
|
157
|
+
/>
|
|
158
|
+
</PageLayout>
|
|
159
|
+
);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export const HealthCheckConfigPage = wrapInSuspense(
|
|
163
|
+
HealthCheckConfigPageContent
|
|
164
|
+
);
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
useApi,
|
|
4
|
+
wrapInSuspense,
|
|
5
|
+
permissionApiRef,
|
|
6
|
+
} from "@checkstack/frontend-api";
|
|
7
|
+
import { healthCheckApiRef } from "../api";
|
|
8
|
+
import { healthcheckRoutes } from "@checkstack/healthcheck-common";
|
|
9
|
+
import { resolveRoute } from "@checkstack/common";
|
|
10
|
+
import {
|
|
11
|
+
PageLayout,
|
|
12
|
+
usePagination,
|
|
13
|
+
Card,
|
|
14
|
+
CardHeader,
|
|
15
|
+
CardTitle,
|
|
16
|
+
CardContent,
|
|
17
|
+
BackLink,
|
|
18
|
+
DateRangeFilter,
|
|
19
|
+
getDefaultDateRange,
|
|
20
|
+
type DateRange,
|
|
21
|
+
} from "@checkstack/ui";
|
|
22
|
+
import { useParams } from "react-router-dom";
|
|
23
|
+
import {
|
|
24
|
+
HealthCheckRunsTable,
|
|
25
|
+
type HealthCheckRunDetailed,
|
|
26
|
+
} from "../components/HealthCheckRunsTable";
|
|
27
|
+
|
|
28
|
+
const HealthCheckHistoryDetailPageContent = () => {
|
|
29
|
+
const { systemId, configurationId } = useParams<{
|
|
30
|
+
systemId: string;
|
|
31
|
+
configurationId: string;
|
|
32
|
+
}>();
|
|
33
|
+
|
|
34
|
+
const api = useApi(healthCheckApiRef);
|
|
35
|
+
const permissionApi = useApi(permissionApiRef);
|
|
36
|
+
const { allowed: canManage, loading: permissionLoading } =
|
|
37
|
+
permissionApi.useResourcePermission("healthcheck", "manage");
|
|
38
|
+
|
|
39
|
+
const [dateRange, setDateRange] = useState<DateRange>(getDefaultDateRange);
|
|
40
|
+
|
|
41
|
+
const {
|
|
42
|
+
items: runs,
|
|
43
|
+
loading,
|
|
44
|
+
pagination,
|
|
45
|
+
} = usePagination({
|
|
46
|
+
fetchFn: (params) =>
|
|
47
|
+
api.getDetailedHistory({
|
|
48
|
+
systemId,
|
|
49
|
+
configurationId,
|
|
50
|
+
startDate: params.startDate,
|
|
51
|
+
endDate: params.endDate,
|
|
52
|
+
limit: params.limit,
|
|
53
|
+
offset: params.offset,
|
|
54
|
+
}),
|
|
55
|
+
getItems: (response) => response.runs as HealthCheckRunDetailed[],
|
|
56
|
+
getTotal: (response) => response.total,
|
|
57
|
+
defaultLimit: 20,
|
|
58
|
+
extraParams: { startDate: dateRange.startDate, endDate: dateRange.endDate },
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<PageLayout
|
|
63
|
+
title="Health Check Run History"
|
|
64
|
+
subtitle={`System: ${systemId} • Configuration: ${configurationId?.slice(
|
|
65
|
+
0,
|
|
66
|
+
8
|
|
67
|
+
)}...`}
|
|
68
|
+
loading={permissionLoading}
|
|
69
|
+
allowed={canManage}
|
|
70
|
+
actions={
|
|
71
|
+
<BackLink to={resolveRoute(healthcheckRoutes.routes.history)}>
|
|
72
|
+
Back to All History
|
|
73
|
+
</BackLink>
|
|
74
|
+
}
|
|
75
|
+
>
|
|
76
|
+
<Card>
|
|
77
|
+
<CardHeader>
|
|
78
|
+
<CardTitle>Run History</CardTitle>
|
|
79
|
+
</CardHeader>
|
|
80
|
+
<CardContent>
|
|
81
|
+
<DateRangeFilter
|
|
82
|
+
value={dateRange}
|
|
83
|
+
onChange={setDateRange}
|
|
84
|
+
className="mb-4"
|
|
85
|
+
/>
|
|
86
|
+
<HealthCheckRunsTable
|
|
87
|
+
runs={runs}
|
|
88
|
+
loading={loading}
|
|
89
|
+
emptyMessage="No health check runs found for this configuration."
|
|
90
|
+
pagination={pagination}
|
|
91
|
+
/>
|
|
92
|
+
</CardContent>
|
|
93
|
+
</Card>
|
|
94
|
+
</PageLayout>
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const HealthCheckHistoryDetailPage = wrapInSuspense(
|
|
99
|
+
HealthCheckHistoryDetailPageContent
|
|
100
|
+
);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useApi,
|
|
3
|
+
wrapInSuspense,
|
|
4
|
+
permissionApiRef,
|
|
5
|
+
} from "@checkstack/frontend-api";
|
|
6
|
+
import { healthCheckApiRef } from "../api";
|
|
7
|
+
import {
|
|
8
|
+
PageLayout,
|
|
9
|
+
usePagination,
|
|
10
|
+
Card,
|
|
11
|
+
CardHeader,
|
|
12
|
+
CardTitle,
|
|
13
|
+
CardContent,
|
|
14
|
+
} from "@checkstack/ui";
|
|
15
|
+
import {
|
|
16
|
+
HealthCheckRunsTable,
|
|
17
|
+
type HealthCheckRunDetailed,
|
|
18
|
+
} from "../components/HealthCheckRunsTable";
|
|
19
|
+
|
|
20
|
+
const HealthCheckHistoryPageContent = () => {
|
|
21
|
+
const api = useApi(healthCheckApiRef);
|
|
22
|
+
const permissionApi = useApi(permissionApiRef);
|
|
23
|
+
const { allowed: canManage, loading: permissionLoading } =
|
|
24
|
+
permissionApi.useResourcePermission("healthcheck", "manage");
|
|
25
|
+
|
|
26
|
+
const {
|
|
27
|
+
items: runs,
|
|
28
|
+
loading,
|
|
29
|
+
pagination,
|
|
30
|
+
} = usePagination({
|
|
31
|
+
fetchFn: (params: { limit: number; offset: number }) =>
|
|
32
|
+
api.getDetailedHistory({
|
|
33
|
+
limit: params.limit,
|
|
34
|
+
offset: params.offset,
|
|
35
|
+
}),
|
|
36
|
+
getItems: (response) => response.runs as HealthCheckRunDetailed[],
|
|
37
|
+
getTotal: (response) => response.total,
|
|
38
|
+
defaultLimit: 20,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<PageLayout
|
|
43
|
+
title="Health Check History"
|
|
44
|
+
subtitle="Detailed run history with full result data"
|
|
45
|
+
loading={permissionLoading}
|
|
46
|
+
allowed={canManage}
|
|
47
|
+
>
|
|
48
|
+
<Card>
|
|
49
|
+
<CardHeader>
|
|
50
|
+
<CardTitle>Run History</CardTitle>
|
|
51
|
+
</CardHeader>
|
|
52
|
+
<CardContent>
|
|
53
|
+
<HealthCheckRunsTable
|
|
54
|
+
runs={runs}
|
|
55
|
+
loading={loading}
|
|
56
|
+
showFilterColumns
|
|
57
|
+
pagination={pagination}
|
|
58
|
+
/>
|
|
59
|
+
</CardContent>
|
|
60
|
+
</Card>
|
|
61
|
+
</PageLayout>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const HealthCheckHistoryPage = wrapInSuspense(
|
|
66
|
+
HealthCheckHistoryPageContent
|
|
67
|
+
);
|