@checkstack/incident-frontend 0.1.0 → 0.3.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 +148 -0
- package/package.json +2 -1
- package/src/api.ts +9 -10
- package/src/components/IncidentEditor.tsx +58 -52
- package/src/components/IncidentMenuItems.tsx +4 -7
- package/src/components/IncidentUpdateForm.tsx +25 -25
- package/src/components/SystemIncidentBadge.tsx +46 -31
- package/src/components/SystemIncidentPanel.tsx +14 -21
- package/src/index.tsx +4 -15
- package/src/pages/IncidentConfigPage.tsx +60 -75
- package/src/pages/IncidentDetailPage.tsx +46 -49
- package/src/pages/SystemIncidentHistoryPage.tsx +23 -39
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,153 @@
|
|
|
1
1
|
# @checkstack/incident-frontend
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 7a23261: ## TanStack Query Integration
|
|
8
|
+
|
|
9
|
+
Migrated all frontend components to use `usePluginClient` hook with TanStack Query integration, replacing the legacy `forPlugin()` pattern.
|
|
10
|
+
|
|
11
|
+
### New Features
|
|
12
|
+
|
|
13
|
+
- **`usePluginClient` hook**: Provides type-safe access to plugin APIs with `.useQuery()` and `.useMutation()` methods
|
|
14
|
+
- **Automatic request deduplication**: Multiple components requesting the same data share a single network request
|
|
15
|
+
- **Built-in caching**: Configurable stale time and cache duration per query
|
|
16
|
+
- **Loading/error states**: TanStack Query provides `isLoading`, `error`, `isRefetching` states automatically
|
|
17
|
+
- **Background refetching**: Stale data is automatically refreshed when components mount
|
|
18
|
+
|
|
19
|
+
### Contract Changes
|
|
20
|
+
|
|
21
|
+
All RPC contracts now require `operationType: "query"` or `operationType: "mutation"` metadata:
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
const getItems = proc()
|
|
25
|
+
.meta({ operationType: "query", access: [access.read] })
|
|
26
|
+
.output(z.array(itemSchema))
|
|
27
|
+
.query();
|
|
28
|
+
|
|
29
|
+
const createItem = proc()
|
|
30
|
+
.meta({ operationType: "mutation", access: [access.manage] })
|
|
31
|
+
.input(createItemSchema)
|
|
32
|
+
.output(itemSchema)
|
|
33
|
+
.mutation();
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Migration
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
// Before (forPlugin pattern)
|
|
40
|
+
const api = useApi(myPluginApiRef);
|
|
41
|
+
const [items, setItems] = useState<Item[]>([]);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
api.getItems().then(setItems);
|
|
44
|
+
}, [api]);
|
|
45
|
+
|
|
46
|
+
// After (usePluginClient pattern)
|
|
47
|
+
const client = usePluginClient(MyPluginApi);
|
|
48
|
+
const { data: items, isLoading } = client.getItems.useQuery({});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Bug Fixes
|
|
52
|
+
|
|
53
|
+
- Fixed `rpc.test.ts` test setup for middleware type inference
|
|
54
|
+
- Fixed `SearchDialog` to use `setQuery` instead of deprecated `search` method
|
|
55
|
+
- Fixed null→undefined warnings in notification and queue frontends
|
|
56
|
+
|
|
57
|
+
### Patch Changes
|
|
58
|
+
|
|
59
|
+
- Updated dependencies [180be38]
|
|
60
|
+
- Updated dependencies [7a23261]
|
|
61
|
+
- @checkstack/dashboard-frontend@0.2.0
|
|
62
|
+
- @checkstack/frontend-api@0.2.0
|
|
63
|
+
- @checkstack/common@0.3.0
|
|
64
|
+
- @checkstack/auth-frontend@0.3.0
|
|
65
|
+
- @checkstack/catalog-common@1.2.0
|
|
66
|
+
- @checkstack/incident-common@0.3.0
|
|
67
|
+
- @checkstack/ui@0.2.1
|
|
68
|
+
- @checkstack/signal-frontend@0.0.7
|
|
69
|
+
|
|
70
|
+
## 0.2.0
|
|
71
|
+
|
|
72
|
+
### Minor Changes
|
|
73
|
+
|
|
74
|
+
- 9faec1f: # Unified AccessRule Terminology Refactoring
|
|
75
|
+
|
|
76
|
+
This release completes a comprehensive terminology refactoring from "permission" to "accessRule" across the entire codebase, establishing a consistent and modern access control vocabulary.
|
|
77
|
+
|
|
78
|
+
## Changes
|
|
79
|
+
|
|
80
|
+
### Core Infrastructure (`@checkstack/common`)
|
|
81
|
+
|
|
82
|
+
- Introduced `AccessRule` interface as the primary access control type
|
|
83
|
+
- Added `accessPair()` helper for creating read/manage access rule pairs
|
|
84
|
+
- Added `access()` builder for individual access rules
|
|
85
|
+
- Replaced `Permission` type with `AccessRule` throughout
|
|
86
|
+
|
|
87
|
+
### API Changes
|
|
88
|
+
|
|
89
|
+
- `env.registerPermissions()` → `env.registerAccessRules()`
|
|
90
|
+
- `meta.permissions` → `meta.access` in RPC contracts
|
|
91
|
+
- `usePermission()` → `useAccess()` in frontend hooks
|
|
92
|
+
- Route `permission:` field → `accessRule:` field
|
|
93
|
+
|
|
94
|
+
### UI Changes
|
|
95
|
+
|
|
96
|
+
- "Roles & Permissions" tab → "Roles & Access Rules"
|
|
97
|
+
- "You don't have permission..." → "You don't have access..."
|
|
98
|
+
- All permission-related UI text updated
|
|
99
|
+
|
|
100
|
+
### Documentation & Templates
|
|
101
|
+
|
|
102
|
+
- Updated 18 documentation files with AccessRule terminology
|
|
103
|
+
- Updated 7 scaffolding templates with `accessPair()` pattern
|
|
104
|
+
- All code examples use new AccessRule API
|
|
105
|
+
|
|
106
|
+
## Migration Guide
|
|
107
|
+
|
|
108
|
+
### Backend Plugins
|
|
109
|
+
|
|
110
|
+
```diff
|
|
111
|
+
- import { permissionList } from "./permissions";
|
|
112
|
+
- env.registerPermissions(permissionList);
|
|
113
|
+
+ import { accessRules } from "./access";
|
|
114
|
+
+ env.registerAccessRules(accessRules);
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### RPC Contracts
|
|
118
|
+
|
|
119
|
+
```diff
|
|
120
|
+
- .meta({ userType: "user", permissions: [permissions.read.id] })
|
|
121
|
+
+ .meta({ userType: "user", access: [access.read] })
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Frontend Hooks
|
|
125
|
+
|
|
126
|
+
```diff
|
|
127
|
+
- const canRead = accessApi.usePermission(permissions.read.id);
|
|
128
|
+
+ const canRead = accessApi.useAccess(access.read);
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Routes
|
|
132
|
+
|
|
133
|
+
```diff
|
|
134
|
+
- permission: permissions.entityRead.id,
|
|
135
|
+
+ accessRule: access.read,
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Patch Changes
|
|
139
|
+
|
|
140
|
+
- Updated dependencies [9faec1f]
|
|
141
|
+
- Updated dependencies [95eeec7]
|
|
142
|
+
- Updated dependencies [f533141]
|
|
143
|
+
- @checkstack/auth-frontend@0.2.0
|
|
144
|
+
- @checkstack/catalog-common@1.1.0
|
|
145
|
+
- @checkstack/common@0.2.0
|
|
146
|
+
- @checkstack/frontend-api@0.1.0
|
|
147
|
+
- @checkstack/incident-common@0.2.0
|
|
148
|
+
- @checkstack/ui@0.2.0
|
|
149
|
+
- @checkstack/signal-frontend@0.0.6
|
|
150
|
+
|
|
3
151
|
## 0.1.0
|
|
4
152
|
|
|
5
153
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/incident-frontend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.tsx",
|
|
6
6
|
"scripts": {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"@checkstack/auth-frontend": "workspace:*",
|
|
13
13
|
"@checkstack/catalog-common": "workspace:*",
|
|
14
14
|
"@checkstack/common": "workspace:*",
|
|
15
|
+
"@checkstack/dashboard-frontend": "workspace:*",
|
|
15
16
|
"@checkstack/frontend-api": "workspace:*",
|
|
16
17
|
"@checkstack/incident-common": "workspace:*",
|
|
17
18
|
"@checkstack/signal-frontend": "workspace:*",
|
package/src/api.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
);
|
|
1
|
+
// Re-export types for convenience
|
|
2
|
+
export type {
|
|
3
|
+
IncidentWithSystems,
|
|
4
|
+
IncidentDetail,
|
|
5
|
+
IncidentUpdate,
|
|
6
|
+
IncidentStatus,
|
|
7
|
+
} from "@checkstack/incident-common";
|
|
8
|
+
// Client definition is in @checkstack/incident-common - use with usePluginClient
|
|
9
|
+
export { IncidentApi } from "@checkstack/incident-common";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import React, { useState, useEffect
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { usePluginClient } from "@checkstack/frontend-api";
|
|
3
|
+
import { IncidentApi } from "../api";
|
|
4
4
|
import type {
|
|
5
5
|
IncidentWithSystems,
|
|
6
6
|
IncidentSeverity,
|
|
@@ -47,7 +47,7 @@ export const IncidentEditor: React.FC<Props> = ({
|
|
|
47
47
|
systems,
|
|
48
48
|
onSave,
|
|
49
49
|
}) => {
|
|
50
|
-
const
|
|
50
|
+
const incidentClient = usePluginClient(IncidentApi);
|
|
51
51
|
const toast = useToast();
|
|
52
52
|
|
|
53
53
|
// Incident fields
|
|
@@ -57,29 +57,46 @@ export const IncidentEditor: React.FC<Props> = ({
|
|
|
57
57
|
const [selectedSystemIds, setSelectedSystemIds] = useState<Set<string>>(
|
|
58
58
|
new Set()
|
|
59
59
|
);
|
|
60
|
-
const [saving, setSaving] = useState(false);
|
|
61
60
|
|
|
62
61
|
// Status update fields
|
|
63
62
|
const [updates, setUpdates] = useState<IncidentUpdate[]>([]);
|
|
64
|
-
const [loadingUpdates,
|
|
63
|
+
const [loadingUpdates, _setLoadingUpdates] = useState(false);
|
|
65
64
|
const [showUpdateForm, setShowUpdateForm] = useState(false);
|
|
66
65
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (detail) {
|
|
73
|
-
setUpdates(detail.updates);
|
|
74
|
-
}
|
|
75
|
-
} catch (error) {
|
|
76
|
-
console.error("Failed to load incident details:", error);
|
|
77
|
-
} finally {
|
|
78
|
-
setLoadingUpdates(false);
|
|
79
|
-
}
|
|
66
|
+
// Mutations
|
|
67
|
+
const createMutation = incidentClient.createIncident.useMutation({
|
|
68
|
+
onSuccess: () => {
|
|
69
|
+
toast.success("Incident created");
|
|
70
|
+
onSave();
|
|
80
71
|
},
|
|
81
|
-
|
|
82
|
-
|
|
72
|
+
onError: (error) => {
|
|
73
|
+
toast.error(error instanceof Error ? error.message : "Failed to save");
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const updateMutation = incidentClient.updateIncident.useMutation({
|
|
78
|
+
onSuccess: () => {
|
|
79
|
+
toast.success("Incident updated");
|
|
80
|
+
onSave();
|
|
81
|
+
},
|
|
82
|
+
onError: (error) => {
|
|
83
|
+
toast.error(error instanceof Error ? error.message : "Failed to save");
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Query for incident details (only when editing)
|
|
88
|
+
const { data: incidentDetail, refetch: refetchDetail } =
|
|
89
|
+
incidentClient.getIncident.useQuery(
|
|
90
|
+
{ id: incident?.id ?? "" },
|
|
91
|
+
{ enabled: !!incident?.id && open }
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// Sync updates from query
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (incidentDetail) {
|
|
97
|
+
setUpdates(incidentDetail.updates);
|
|
98
|
+
}
|
|
99
|
+
}, [incidentDetail]);
|
|
83
100
|
|
|
84
101
|
// Reset form when incident changes
|
|
85
102
|
useEffect(() => {
|
|
@@ -88,8 +105,6 @@ export const IncidentEditor: React.FC<Props> = ({
|
|
|
88
105
|
setDescription(incident.description ?? "");
|
|
89
106
|
setSeverity(incident.severity);
|
|
90
107
|
setSelectedSystemIds(new Set(incident.systemIds));
|
|
91
|
-
// Load full incident with updates
|
|
92
|
-
loadIncidentDetails(incident.id);
|
|
93
108
|
} else {
|
|
94
109
|
setTitle("");
|
|
95
110
|
setDescription("");
|
|
@@ -98,7 +113,7 @@ export const IncidentEditor: React.FC<Props> = ({
|
|
|
98
113
|
setUpdates([]);
|
|
99
114
|
setShowUpdateForm(false);
|
|
100
115
|
}
|
|
101
|
-
}, [incident, open
|
|
116
|
+
}, [incident, open]);
|
|
102
117
|
|
|
103
118
|
const handleSystemToggle = (systemId: string) => {
|
|
104
119
|
setSelectedSystemIds((prev) => {
|
|
@@ -112,7 +127,7 @@ export const IncidentEditor: React.FC<Props> = ({
|
|
|
112
127
|
});
|
|
113
128
|
};
|
|
114
129
|
|
|
115
|
-
const handleSubmit =
|
|
130
|
+
const handleSubmit = () => {
|
|
116
131
|
if (!title.trim()) {
|
|
117
132
|
toast.error("Title is required");
|
|
118
133
|
return;
|
|
@@ -122,44 +137,35 @@ export const IncidentEditor: React.FC<Props> = ({
|
|
|
122
137
|
return;
|
|
123
138
|
}
|
|
124
139
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
severity,
|
|
141
|
-
systemIds: [...selectedSystemIds],
|
|
142
|
-
});
|
|
143
|
-
toast.success("Incident created");
|
|
144
|
-
}
|
|
145
|
-
onSave();
|
|
146
|
-
} catch (error) {
|
|
147
|
-
const message = error instanceof Error ? error.message : "Failed to save";
|
|
148
|
-
toast.error(message);
|
|
149
|
-
} finally {
|
|
150
|
-
setSaving(false);
|
|
140
|
+
if (incident) {
|
|
141
|
+
updateMutation.mutate({
|
|
142
|
+
id: incident.id,
|
|
143
|
+
title,
|
|
144
|
+
description: description || undefined,
|
|
145
|
+
severity,
|
|
146
|
+
systemIds: [...selectedSystemIds],
|
|
147
|
+
});
|
|
148
|
+
} else {
|
|
149
|
+
createMutation.mutate({
|
|
150
|
+
title,
|
|
151
|
+
description,
|
|
152
|
+
severity,
|
|
153
|
+
systemIds: [...selectedSystemIds],
|
|
154
|
+
});
|
|
151
155
|
}
|
|
152
156
|
};
|
|
153
157
|
|
|
154
158
|
const handleUpdateSuccess = () => {
|
|
155
159
|
if (incident) {
|
|
156
|
-
|
|
160
|
+
void refetchDetail();
|
|
157
161
|
}
|
|
158
162
|
setShowUpdateForm(false);
|
|
159
163
|
// Notify parent to refresh list (status may have changed)
|
|
160
164
|
onSave();
|
|
161
165
|
};
|
|
162
166
|
|
|
167
|
+
const saving = createMutation.isPending || updateMutation.isPending;
|
|
168
|
+
|
|
163
169
|
return (
|
|
164
170
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
165
171
|
<DialogContent size="xl">
|
|
@@ -3,20 +3,17 @@ import { Link } from "react-router-dom";
|
|
|
3
3
|
import { AlertTriangle } from "lucide-react";
|
|
4
4
|
import type { UserMenuItemsContext } from "@checkstack/frontend-api";
|
|
5
5
|
import { DropdownMenuItem } from "@checkstack/ui";
|
|
6
|
-
import {
|
|
6
|
+
import { resolveRoute } from "@checkstack/common";
|
|
7
7
|
import {
|
|
8
8
|
incidentRoutes,
|
|
9
|
-
|
|
9
|
+
incidentAccess,
|
|
10
10
|
pluginMetadata,
|
|
11
11
|
} from "@checkstack/incident-common";
|
|
12
12
|
|
|
13
13
|
export const IncidentMenuItems = ({
|
|
14
|
-
|
|
14
|
+
accessRules: userPerms,
|
|
15
15
|
}: UserMenuItemsContext) => {
|
|
16
|
-
const qualifiedId =
|
|
17
|
-
pluginMetadata,
|
|
18
|
-
permissions.incidentManage
|
|
19
|
-
);
|
|
16
|
+
const qualifiedId = `${pluginMetadata.pluginId}.${incidentAccess.incident.manage.id}`;
|
|
20
17
|
const canManage = userPerms.includes("*") || userPerms.includes(qualifiedId);
|
|
21
18
|
|
|
22
19
|
if (!canManage) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useState } from "react";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { usePluginClient } from "@checkstack/frontend-api";
|
|
3
|
+
import { IncidentApi } from "../api";
|
|
4
4
|
import type { IncidentStatus } from "@checkstack/incident-common";
|
|
5
5
|
import {
|
|
6
6
|
Button,
|
|
@@ -30,37 +30,37 @@ export const IncidentUpdateForm: React.FC<IncidentUpdateFormProps> = ({
|
|
|
30
30
|
onSuccess,
|
|
31
31
|
onCancel,
|
|
32
32
|
}) => {
|
|
33
|
-
const
|
|
33
|
+
const incidentClient = usePluginClient(IncidentApi);
|
|
34
34
|
const toast = useToast();
|
|
35
35
|
|
|
36
36
|
const [message, setMessage] = useState("");
|
|
37
37
|
const [statusChange, setStatusChange] = useState<IncidentStatus | "">("");
|
|
38
|
-
const [isPosting, setIsPosting] = useState(false);
|
|
39
38
|
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
toast.error("Update message is required");
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
setIsPosting(true);
|
|
47
|
-
try {
|
|
48
|
-
await api.addUpdate({
|
|
49
|
-
incidentId,
|
|
50
|
-
message,
|
|
51
|
-
statusChange: statusChange || undefined,
|
|
52
|
-
});
|
|
39
|
+
const addUpdateMutation = incidentClient.addUpdate.useMutation({
|
|
40
|
+
onSuccess: () => {
|
|
53
41
|
toast.success("Update posted");
|
|
54
42
|
setMessage("");
|
|
55
43
|
setStatusChange("");
|
|
56
44
|
onSuccess();
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
45
|
+
},
|
|
46
|
+
onError: (error) => {
|
|
47
|
+
toast.error(
|
|
48
|
+
error instanceof Error ? error.message : "Failed to post update"
|
|
49
|
+
);
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const handleSubmit = () => {
|
|
54
|
+
if (!message.trim()) {
|
|
55
|
+
toast.error("Update message is required");
|
|
56
|
+
return;
|
|
63
57
|
}
|
|
58
|
+
|
|
59
|
+
addUpdateMutation.mutate({
|
|
60
|
+
incidentId,
|
|
61
|
+
message,
|
|
62
|
+
statusChange: statusChange || undefined,
|
|
63
|
+
});
|
|
64
64
|
};
|
|
65
65
|
|
|
66
66
|
return (
|
|
@@ -107,9 +107,9 @@ export const IncidentUpdateForm: React.FC<IncidentUpdateFormProps> = ({
|
|
|
107
107
|
<Button
|
|
108
108
|
size="sm"
|
|
109
109
|
onClick={handleSubmit}
|
|
110
|
-
disabled={
|
|
110
|
+
disabled={addUpdateMutation.isPending || !message.trim()}
|
|
111
111
|
>
|
|
112
|
-
{
|
|
112
|
+
{addUpdateMutation.isPending ? (
|
|
113
113
|
<>
|
|
114
114
|
<Loader2 className="h-4 w-4 mr-1 animate-spin" />
|
|
115
115
|
Posting...
|
|
@@ -1,60 +1,75 @@
|
|
|
1
|
-
import React
|
|
2
|
-
import {
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { usePluginClient, type SlotContext } from "@checkstack/frontend-api";
|
|
3
3
|
import { useSignal } from "@checkstack/signal-frontend";
|
|
4
4
|
import { SystemStateBadgesSlot } from "@checkstack/catalog-common";
|
|
5
|
-
import {
|
|
5
|
+
import { IncidentApi } from "../api";
|
|
6
6
|
import {
|
|
7
7
|
INCIDENT_UPDATED,
|
|
8
8
|
type IncidentWithSystems,
|
|
9
9
|
} from "@checkstack/incident-common";
|
|
10
10
|
import { Badge } from "@checkstack/ui";
|
|
11
|
+
import { useSystemBadgeDataOptional } from "@checkstack/dashboard-frontend";
|
|
11
12
|
|
|
12
13
|
type Props = SlotContext<typeof SystemStateBadgesSlot>;
|
|
13
14
|
|
|
14
15
|
const SEVERITY_WEIGHTS = { critical: 3, major: 2, minor: 1 } as const;
|
|
15
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Finds the most severe incident from a list.
|
|
19
|
+
*/
|
|
20
|
+
function getMostSevereIncident(
|
|
21
|
+
incidents: IncidentWithSystems[]
|
|
22
|
+
): IncidentWithSystems | undefined {
|
|
23
|
+
if (incidents.length === 0) return undefined;
|
|
24
|
+
const sorted = [...incidents].toSorted((a, b) => {
|
|
25
|
+
return (
|
|
26
|
+
(SEVERITY_WEIGHTS[b.severity as keyof typeof SEVERITY_WEIGHTS] || 0) -
|
|
27
|
+
(SEVERITY_WEIGHTS[a.severity as keyof typeof SEVERITY_WEIGHTS] || 0)
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
return sorted[0];
|
|
31
|
+
}
|
|
32
|
+
|
|
16
33
|
/**
|
|
17
34
|
* Displays an incident badge for a system when it has an active incident.
|
|
18
35
|
* Shows nothing if no active incidents.
|
|
36
|
+
*
|
|
37
|
+
* When rendered within SystemBadgeDataProvider, uses bulk-fetched data.
|
|
38
|
+
* Otherwise, falls back to individual fetch.
|
|
39
|
+
*
|
|
19
40
|
* Listens for realtime updates via signals.
|
|
20
41
|
*/
|
|
21
42
|
export const SystemIncidentBadge: React.FC<Props> = ({ system }) => {
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
IncidentWithSystems | undefined
|
|
25
|
-
>();
|
|
43
|
+
const incidentClient = usePluginClient(IncidentApi);
|
|
44
|
+
const badgeData = useSystemBadgeDataOptional();
|
|
26
45
|
|
|
27
|
-
|
|
28
|
-
|
|
46
|
+
// Try to get data from provider first
|
|
47
|
+
const providerData = badgeData?.getSystemBadgeData(system?.id ?? "");
|
|
48
|
+
const providerIncident = providerData
|
|
49
|
+
? getMostSevereIncident(providerData.incidents)
|
|
50
|
+
: undefined;
|
|
29
51
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
(SEVERITY_WEIGHTS[b.severity as keyof typeof SEVERITY_WEIGHTS] ||
|
|
37
|
-
0) -
|
|
38
|
-
(SEVERITY_WEIGHTS[a.severity as keyof typeof SEVERITY_WEIGHTS] || 0)
|
|
39
|
-
);
|
|
40
|
-
});
|
|
41
|
-
setActiveIncident(sorted[0]);
|
|
42
|
-
})
|
|
43
|
-
.catch(console.error);
|
|
44
|
-
}, [system?.id, api]);
|
|
52
|
+
// Query for incidents if not using provider
|
|
53
|
+
const { data: incidents, refetch } =
|
|
54
|
+
incidentClient.getIncidentsForSystem.useQuery(
|
|
55
|
+
{ systemId: system?.id ?? "" },
|
|
56
|
+
{ enabled: !badgeData && !!system?.id }
|
|
57
|
+
);
|
|
45
58
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}, [refetch]);
|
|
59
|
+
const localIncident = incidents
|
|
60
|
+
? getMostSevereIncident(incidents)
|
|
61
|
+
: undefined;
|
|
50
62
|
|
|
51
|
-
// Listen for realtime incident updates
|
|
63
|
+
// Listen for realtime incident updates (only in fallback mode)
|
|
52
64
|
useSignal(INCIDENT_UPDATED, ({ systemIds }) => {
|
|
53
|
-
if (system?.id && systemIds.includes(system.id)) {
|
|
54
|
-
refetch();
|
|
65
|
+
if (!badgeData && system?.id && systemIds.includes(system.id)) {
|
|
66
|
+
void refetch();
|
|
55
67
|
}
|
|
56
68
|
});
|
|
57
69
|
|
|
70
|
+
// Use provider data if available, otherwise use local state
|
|
71
|
+
const activeIncident = badgeData ? providerIncident : localIncident;
|
|
72
|
+
|
|
58
73
|
if (!activeIncident) return;
|
|
59
74
|
|
|
60
75
|
const variant =
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import React
|
|
1
|
+
import React from "react";
|
|
2
2
|
import { Link } from "react-router-dom";
|
|
3
|
-
import {
|
|
3
|
+
import { usePluginClient, type SlotContext } from "@checkstack/frontend-api";
|
|
4
4
|
import { useSignal } from "@checkstack/signal-frontend";
|
|
5
5
|
import { resolveRoute } from "@checkstack/common";
|
|
6
6
|
import { SystemDetailsTopSlot } from "@checkstack/catalog-common";
|
|
7
|
-
import {
|
|
7
|
+
import { IncidentApi } from "../api";
|
|
8
8
|
import {
|
|
9
9
|
incidentRoutes,
|
|
10
10
|
INCIDENT_UPDATED,
|
|
@@ -76,29 +76,22 @@ function findMostSevereIncident(
|
|
|
76
76
|
* Listens for realtime updates via signals.
|
|
77
77
|
*/
|
|
78
78
|
export const SystemIncidentPanel: React.FC<Props> = ({ system }) => {
|
|
79
|
-
const
|
|
80
|
-
const [incidents, setIncidents] = useState<IncidentWithSystems[]>([]);
|
|
81
|
-
const [loading, setLoading] = useState(true);
|
|
79
|
+
const incidentClient = usePluginClient(IncidentApi);
|
|
82
80
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
// Initial fetch
|
|
94
|
-
useEffect(() => {
|
|
95
|
-
refetch();
|
|
96
|
-
}, [refetch]);
|
|
81
|
+
// Fetch incidents with useQuery
|
|
82
|
+
const {
|
|
83
|
+
data: incidents = [],
|
|
84
|
+
isLoading: loading,
|
|
85
|
+
refetch,
|
|
86
|
+
} = incidentClient.getIncidentsForSystem.useQuery(
|
|
87
|
+
{ systemId: system?.id ?? "" },
|
|
88
|
+
{ enabled: !!system?.id }
|
|
89
|
+
);
|
|
97
90
|
|
|
98
91
|
// Listen for realtime incident updates
|
|
99
92
|
useSignal(INCIDENT_UPDATED, ({ systemIds }) => {
|
|
100
93
|
if (system?.id && systemIds.includes(system.id)) {
|
|
101
|
-
refetch();
|
|
94
|
+
void refetch();
|
|
102
95
|
}
|
|
103
96
|
});
|
|
104
97
|
|
package/src/index.tsx
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createFrontendPlugin,
|
|
3
3
|
createSlotExtension,
|
|
4
|
-
rpcApiRef,
|
|
5
|
-
type ApiRef,
|
|
6
4
|
UserMenuItemsSlot,
|
|
7
5
|
} from "@checkstack/frontend-api";
|
|
8
|
-
import { incidentApiRef, type IncidentApiClient } from "./api";
|
|
9
6
|
import {
|
|
10
7
|
incidentRoutes,
|
|
11
|
-
IncidentApi,
|
|
12
8
|
pluginMetadata,
|
|
13
|
-
|
|
9
|
+
incidentAccess,
|
|
14
10
|
} from "@checkstack/incident-common";
|
|
15
11
|
import {
|
|
16
12
|
SystemDetailsTopSlot,
|
|
@@ -30,7 +26,7 @@ export default createFrontendPlugin({
|
|
|
30
26
|
route: incidentRoutes.routes.config,
|
|
31
27
|
element: <IncidentConfigPage />,
|
|
32
28
|
title: "Incidents",
|
|
33
|
-
|
|
29
|
+
accessRule: incidentAccess.incident.manage,
|
|
34
30
|
},
|
|
35
31
|
{
|
|
36
32
|
route: incidentRoutes.routes.detail,
|
|
@@ -43,15 +39,8 @@ export default createFrontendPlugin({
|
|
|
43
39
|
title: "System Incident History",
|
|
44
40
|
},
|
|
45
41
|
],
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
ref: incidentApiRef,
|
|
49
|
-
factory: (deps: { get: <T>(ref: ApiRef<T>) => T }): IncidentApiClient => {
|
|
50
|
-
const rpcApi = deps.get(rpcApiRef);
|
|
51
|
-
return rpcApi.forPlugin(IncidentApi);
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
],
|
|
42
|
+
// No APIs needed - components use usePluginClient() directly
|
|
43
|
+
apis: [],
|
|
55
44
|
extensions: [
|
|
56
45
|
createSlotExtension(UserMenuItemsSlot, {
|
|
57
46
|
id: "incident.user-menu.items",
|
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import React, { useEffect, useState
|
|
1
|
+
import React, { useEffect, useState } from "react";
|
|
2
2
|
import { useSearchParams } from "react-router-dom";
|
|
3
3
|
import {
|
|
4
|
+
usePluginClient,
|
|
5
|
+
accessApiRef,
|
|
4
6
|
useApi,
|
|
5
|
-
rpcApiRef,
|
|
6
|
-
permissionApiRef,
|
|
7
7
|
wrapInSuspense,
|
|
8
8
|
} from "@checkstack/frontend-api";
|
|
9
|
-
import {
|
|
9
|
+
import { IncidentApi } from "../api";
|
|
10
10
|
import type {
|
|
11
11
|
IncidentWithSystems,
|
|
12
12
|
IncidentStatus,
|
|
13
13
|
} from "@checkstack/incident-common";
|
|
14
|
-
import {
|
|
14
|
+
import { incidentAccess } from "@checkstack/incident-common";
|
|
15
|
+
import { CatalogApi } from "@checkstack/catalog-common";
|
|
15
16
|
import {
|
|
16
17
|
Card,
|
|
17
18
|
CardHeader,
|
|
@@ -48,20 +49,16 @@ import { formatDistanceToNow } from "date-fns";
|
|
|
48
49
|
import { IncidentEditor } from "../components/IncidentEditor";
|
|
49
50
|
|
|
50
51
|
const IncidentConfigPageContent: React.FC = () => {
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const
|
|
52
|
+
const incidentClient = usePluginClient(IncidentApi);
|
|
53
|
+
const catalogClient = usePluginClient(CatalogApi);
|
|
54
|
+
const accessApi = useApi(accessApiRef);
|
|
54
55
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
55
|
-
|
|
56
|
-
const catalogApi = useMemo(() => rpcApi.forPlugin(CatalogApi), [rpcApi]);
|
|
57
56
|
const toast = useToast();
|
|
58
57
|
|
|
59
|
-
const { allowed: canManage, loading:
|
|
60
|
-
|
|
58
|
+
const { allowed: canManage, loading: accessLoading } = accessApi.useAccess(
|
|
59
|
+
incidentAccess.incident.manage
|
|
60
|
+
);
|
|
61
61
|
|
|
62
|
-
const [incidents, setIncidents] = useState<IncidentWithSystems[]>([]);
|
|
63
|
-
const [systems, setSystems] = useState<System[]>([]);
|
|
64
|
-
const [loading, setLoading] = useState(true);
|
|
65
62
|
const [statusFilter, setStatusFilter] = useState<IncidentStatus | "all">(
|
|
66
63
|
"all"
|
|
67
64
|
);
|
|
@@ -75,37 +72,28 @@ const IncidentConfigPageContent: React.FC = () => {
|
|
|
75
72
|
|
|
76
73
|
// Delete confirmation state
|
|
77
74
|
const [deleteId, setDeleteId] = useState<string | undefined>();
|
|
78
|
-
const [isDeleting, setIsDeleting] = useState(false);
|
|
79
75
|
|
|
80
76
|
// Resolve confirmation state
|
|
81
77
|
const [resolveId, setResolveId] = useState<string | undefined>();
|
|
82
|
-
const [isResolving, setIsResolving] = useState(false);
|
|
83
78
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
catalogApi.getSystems(),
|
|
95
|
-
]);
|
|
96
|
-
setIncidents(incidentList);
|
|
97
|
-
setSystems(systemList);
|
|
98
|
-
} catch (error) {
|
|
99
|
-
const message = error instanceof Error ? error.message : "Failed to load";
|
|
100
|
-
toast.error(message);
|
|
101
|
-
} finally {
|
|
102
|
-
setLoading(false);
|
|
103
|
-
}
|
|
104
|
-
};
|
|
79
|
+
// Fetch incidents with useQuery
|
|
80
|
+
const {
|
|
81
|
+
data: incidentsData,
|
|
82
|
+
isLoading: incidentsLoading,
|
|
83
|
+
refetch: refetchIncidents,
|
|
84
|
+
} = incidentClient.listIncidents.useQuery(
|
|
85
|
+
statusFilter === "all"
|
|
86
|
+
? { includeResolved: showResolved }
|
|
87
|
+
: { status: statusFilter, includeResolved: showResolved }
|
|
88
|
+
);
|
|
105
89
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
90
|
+
// Fetch systems with useQuery
|
|
91
|
+
const { data: systemsData, isLoading: systemsLoading } =
|
|
92
|
+
catalogClient.getSystems.useQuery({});
|
|
93
|
+
|
|
94
|
+
const incidents = incidentsData?.incidents ?? [];
|
|
95
|
+
const systems = systemsData?.systems ?? [];
|
|
96
|
+
const loading = incidentsLoading || systemsLoading;
|
|
109
97
|
|
|
110
98
|
// Handle ?action=create URL parameter (from command palette)
|
|
111
99
|
useEffect(() => {
|
|
@@ -118,6 +106,29 @@ const IncidentConfigPageContent: React.FC = () => {
|
|
|
118
106
|
}
|
|
119
107
|
}, [searchParams, canManage, setSearchParams]);
|
|
120
108
|
|
|
109
|
+
// Mutations
|
|
110
|
+
const deleteMutation = incidentClient.deleteIncident.useMutation({
|
|
111
|
+
onSuccess: () => {
|
|
112
|
+
toast.success("Incident deleted");
|
|
113
|
+
void refetchIncidents();
|
|
114
|
+
setDeleteId(undefined);
|
|
115
|
+
},
|
|
116
|
+
onError: (error) => {
|
|
117
|
+
toast.error(error instanceof Error ? error.message : "Failed to delete");
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const resolveMutation = incidentClient.resolveIncident.useMutation({
|
|
122
|
+
onSuccess: () => {
|
|
123
|
+
toast.success("Incident resolved");
|
|
124
|
+
void refetchIncidents();
|
|
125
|
+
setResolveId(undefined);
|
|
126
|
+
},
|
|
127
|
+
onError: (error) => {
|
|
128
|
+
toast.error(error instanceof Error ? error.message : "Failed to resolve");
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
121
132
|
const handleCreate = () => {
|
|
122
133
|
setEditingIncident(undefined);
|
|
123
134
|
setEditorOpen(true);
|
|
@@ -128,45 +139,19 @@ const IncidentConfigPageContent: React.FC = () => {
|
|
|
128
139
|
setEditorOpen(true);
|
|
129
140
|
};
|
|
130
141
|
|
|
131
|
-
const handleDelete =
|
|
142
|
+
const handleDelete = () => {
|
|
132
143
|
if (!deleteId) return;
|
|
133
|
-
|
|
134
|
-
setIsDeleting(true);
|
|
135
|
-
try {
|
|
136
|
-
await api.deleteIncident({ id: deleteId });
|
|
137
|
-
toast.success("Incident deleted");
|
|
138
|
-
loadData();
|
|
139
|
-
} catch (error) {
|
|
140
|
-
const message =
|
|
141
|
-
error instanceof Error ? error.message : "Failed to delete";
|
|
142
|
-
toast.error(message);
|
|
143
|
-
} finally {
|
|
144
|
-
setIsDeleting(false);
|
|
145
|
-
setDeleteId(undefined);
|
|
146
|
-
}
|
|
144
|
+
deleteMutation.mutate({ id: deleteId });
|
|
147
145
|
};
|
|
148
146
|
|
|
149
|
-
const handleResolve =
|
|
147
|
+
const handleResolve = () => {
|
|
150
148
|
if (!resolveId) return;
|
|
151
|
-
|
|
152
|
-
setIsResolving(true);
|
|
153
|
-
try {
|
|
154
|
-
await api.resolveIncident({ id: resolveId });
|
|
155
|
-
toast.success("Incident resolved");
|
|
156
|
-
loadData();
|
|
157
|
-
} catch (error) {
|
|
158
|
-
const message =
|
|
159
|
-
error instanceof Error ? error.message : "Failed to resolve";
|
|
160
|
-
toast.error(message);
|
|
161
|
-
} finally {
|
|
162
|
-
setIsResolving(false);
|
|
163
|
-
setResolveId(undefined);
|
|
164
|
-
}
|
|
149
|
+
resolveMutation.mutate({ id: resolveId });
|
|
165
150
|
};
|
|
166
151
|
|
|
167
152
|
const handleSave = () => {
|
|
168
153
|
setEditorOpen(false);
|
|
169
|
-
|
|
154
|
+
void refetchIncidents();
|
|
170
155
|
};
|
|
171
156
|
|
|
172
157
|
const getStatusBadge = (status: IncidentStatus) => {
|
|
@@ -220,7 +205,7 @@ const IncidentConfigPageContent: React.FC = () => {
|
|
|
220
205
|
<PageLayout
|
|
221
206
|
title="Incident Management"
|
|
222
207
|
subtitle="Track and manage incidents affecting your systems"
|
|
223
|
-
loading={
|
|
208
|
+
loading={accessLoading}
|
|
224
209
|
allowed={canManage}
|
|
225
210
|
actions={
|
|
226
211
|
<Button onClick={handleCreate}>
|
|
@@ -368,7 +353,7 @@ const IncidentConfigPageContent: React.FC = () => {
|
|
|
368
353
|
confirmText="Delete"
|
|
369
354
|
variant="danger"
|
|
370
355
|
onConfirm={handleDelete}
|
|
371
|
-
isLoading={
|
|
356
|
+
isLoading={deleteMutation.isPending}
|
|
372
357
|
/>
|
|
373
358
|
|
|
374
359
|
<ConfirmationModal
|
|
@@ -379,7 +364,7 @@ const IncidentConfigPageContent: React.FC = () => {
|
|
|
379
364
|
confirmText="Resolve"
|
|
380
365
|
variant="info"
|
|
381
366
|
onConfirm={handleResolve}
|
|
382
|
-
isLoading={
|
|
367
|
+
isLoading={resolveMutation.isPending}
|
|
383
368
|
/>
|
|
384
369
|
</PageLayout>
|
|
385
370
|
);
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { useState } from "react";
|
|
2
2
|
import { useParams, useNavigate, useSearchParams } from "react-router-dom";
|
|
3
3
|
import {
|
|
4
|
+
usePluginClient,
|
|
5
|
+
accessApiRef,
|
|
4
6
|
useApi,
|
|
5
|
-
rpcApiRef,
|
|
6
|
-
permissionApiRef,
|
|
7
7
|
wrapInSuspense,
|
|
8
8
|
} from "@checkstack/frontend-api";
|
|
9
9
|
import { useSignal } from "@checkstack/signal-frontend";
|
|
10
10
|
import { resolveRoute } from "@checkstack/common";
|
|
11
|
-
import {
|
|
11
|
+
import { IncidentApi } from "../api";
|
|
12
12
|
import {
|
|
13
13
|
incidentRoutes,
|
|
14
14
|
INCIDENT_UPDATED,
|
|
15
|
-
|
|
15
|
+
incidentAccess,
|
|
16
16
|
} from "@checkstack/incident-common";
|
|
17
|
-
import { CatalogApi
|
|
17
|
+
import { CatalogApi } from "@checkstack/catalog-common";
|
|
18
18
|
import {
|
|
19
19
|
Card,
|
|
20
20
|
CardHeader,
|
|
@@ -48,68 +48,60 @@ const IncidentDetailPageContent: React.FC = () => {
|
|
|
48
48
|
const { incidentId } = useParams<{ incidentId: string }>();
|
|
49
49
|
const navigate = useNavigate();
|
|
50
50
|
const [searchParams] = useSearchParams();
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const
|
|
51
|
+
const incidentClient = usePluginClient(IncidentApi);
|
|
52
|
+
const catalogClient = usePluginClient(CatalogApi);
|
|
53
|
+
const accessApi = useApi(accessApiRef);
|
|
54
54
|
const toast = useToast();
|
|
55
55
|
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
const { allowed: canManage } = permissionApi.useResourcePermission(
|
|
59
|
-
"incident",
|
|
60
|
-
"manage"
|
|
56
|
+
const { allowed: canManage } = accessApi.useAccess(
|
|
57
|
+
incidentAccess.incident.manage
|
|
61
58
|
);
|
|
62
59
|
|
|
63
|
-
const [incident, setIncident] = useState<IncidentDetail | undefined>();
|
|
64
|
-
const [systems, setSystems] = useState<System[]>([]);
|
|
65
|
-
const [loading, setLoading] = useState(true);
|
|
66
60
|
const [showUpdateForm, setShowUpdateForm] = useState(false);
|
|
67
61
|
|
|
68
|
-
|
|
69
|
-
|
|
62
|
+
// Fetch incident with useQuery
|
|
63
|
+
const {
|
|
64
|
+
data: incident,
|
|
65
|
+
isLoading: incidentLoading,
|
|
66
|
+
refetch: refetchIncident,
|
|
67
|
+
} = incidentClient.getIncident.useQuery(
|
|
68
|
+
{ id: incidentId ?? "" },
|
|
69
|
+
{ enabled: !!incidentId }
|
|
70
|
+
);
|
|
70
71
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
catalogApi.getSystems(),
|
|
75
|
-
]);
|
|
76
|
-
setIncident(incidentData ?? undefined);
|
|
77
|
-
setSystems(systemList);
|
|
78
|
-
} catch (error) {
|
|
79
|
-
console.error("Failed to load incident:", error);
|
|
80
|
-
} finally {
|
|
81
|
-
setLoading(false);
|
|
82
|
-
}
|
|
83
|
-
}, [incidentId, api, catalogApi]);
|
|
72
|
+
// Fetch systems with useQuery
|
|
73
|
+
const { data: systemsData, isLoading: systemsLoading } =
|
|
74
|
+
catalogClient.getSystems.useQuery({});
|
|
84
75
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}, [loadData]);
|
|
76
|
+
const systems = systemsData?.systems ?? [];
|
|
77
|
+
const loading = incidentLoading || systemsLoading;
|
|
88
78
|
|
|
89
79
|
// Listen for realtime updates
|
|
90
80
|
useSignal(INCIDENT_UPDATED, ({ incidentId: updatedId }) => {
|
|
91
81
|
if (incidentId === updatedId) {
|
|
92
|
-
|
|
82
|
+
void refetchIncident();
|
|
93
83
|
}
|
|
94
84
|
});
|
|
95
85
|
|
|
86
|
+
// Resolve mutation
|
|
87
|
+
const resolveMutation = incidentClient.resolveIncident.useMutation({
|
|
88
|
+
onSuccess: () => {
|
|
89
|
+
toast.success("Incident resolved");
|
|
90
|
+
void refetchIncident();
|
|
91
|
+
},
|
|
92
|
+
onError: (error) => {
|
|
93
|
+
toast.error(error instanceof Error ? error.message : "Failed to resolve");
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
96
97
|
const handleUpdateSuccess = () => {
|
|
97
98
|
setShowUpdateForm(false);
|
|
98
|
-
|
|
99
|
+
void refetchIncident();
|
|
99
100
|
};
|
|
100
101
|
|
|
101
|
-
const handleResolve =
|
|
102
|
+
const handleResolve = () => {
|
|
102
103
|
if (!incidentId) return;
|
|
103
|
-
|
|
104
|
-
try {
|
|
105
|
-
await api.resolveIncident({ id: incidentId });
|
|
106
|
-
toast.success("Incident resolved");
|
|
107
|
-
await loadData();
|
|
108
|
-
} catch (error) {
|
|
109
|
-
const message =
|
|
110
|
-
error instanceof Error ? error.message : "Failed to resolve";
|
|
111
|
-
toast.error(message);
|
|
112
|
-
}
|
|
104
|
+
resolveMutation.mutate({ id: incidentId });
|
|
113
105
|
};
|
|
114
106
|
|
|
115
107
|
const getSystemName = (systemId: string): string => {
|
|
@@ -177,7 +169,12 @@ const IncidentDetailPageContent: React.FC = () => {
|
|
|
177
169
|
{getIncidentSeverityBadge(incident.severity)}
|
|
178
170
|
{getIncidentStatusBadge(incident.status)}
|
|
179
171
|
{canResolve && (
|
|
180
|
-
<Button
|
|
172
|
+
<Button
|
|
173
|
+
variant="outline"
|
|
174
|
+
size="sm"
|
|
175
|
+
onClick={handleResolve}
|
|
176
|
+
disabled={resolveMutation.isPending}
|
|
177
|
+
>
|
|
181
178
|
<CheckCircle2 className="h-4 w-4 mr-1" />
|
|
182
179
|
Resolve
|
|
183
180
|
</Button>
|
|
@@ -1,20 +1,15 @@
|
|
|
1
|
-
import React
|
|
1
|
+
import React from "react";
|
|
2
2
|
import { useParams, Link } from "react-router-dom";
|
|
3
|
-
import {
|
|
3
|
+
import { usePluginClient, wrapInSuspense } from "@checkstack/frontend-api";
|
|
4
4
|
import { useSignal } from "@checkstack/signal-frontend";
|
|
5
5
|
import { resolveRoute } from "@checkstack/common";
|
|
6
|
-
import {
|
|
6
|
+
import { IncidentApi } from "../api";
|
|
7
7
|
import {
|
|
8
8
|
incidentRoutes,
|
|
9
9
|
INCIDENT_UPDATED,
|
|
10
|
-
type IncidentWithSystems,
|
|
11
10
|
type IncidentStatus,
|
|
12
11
|
} from "@checkstack/incident-common";
|
|
13
|
-
import {
|
|
14
|
-
CatalogApi,
|
|
15
|
-
type System,
|
|
16
|
-
catalogRoutes,
|
|
17
|
-
} from "@checkstack/catalog-common";
|
|
12
|
+
import { CatalogApi, catalogRoutes } from "@checkstack/catalog-common";
|
|
18
13
|
import {
|
|
19
14
|
Card,
|
|
20
15
|
CardHeader,
|
|
@@ -30,43 +25,32 @@ import { formatDistanceToNow } from "date-fns";
|
|
|
30
25
|
|
|
31
26
|
const SystemIncidentHistoryPageContent: React.FC = () => {
|
|
32
27
|
const { systemId } = useParams<{ systemId: string }>();
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
const catalogApi = useMemo(() => rpcApi.forPlugin(CatalogApi), [rpcApi]);
|
|
37
|
-
|
|
38
|
-
const [incidents, setIncidents] = useState<IncidentWithSystems[]>([]);
|
|
39
|
-
const [system, setSystem] = useState<System | undefined>();
|
|
40
|
-
const [loading, setLoading] = useState(true);
|
|
28
|
+
const incidentClient = usePluginClient(IncidentApi);
|
|
29
|
+
const catalogClient = usePluginClient(CatalogApi);
|
|
41
30
|
|
|
42
|
-
|
|
43
|
-
|
|
31
|
+
// Fetch incidents with useQuery
|
|
32
|
+
const {
|
|
33
|
+
data: incidentsData,
|
|
34
|
+
isLoading: incidentsLoading,
|
|
35
|
+
refetch: refetchIncidents,
|
|
36
|
+
} = incidentClient.listIncidents.useQuery(
|
|
37
|
+
{ systemId, includeResolved: true },
|
|
38
|
+
{ enabled: !!systemId }
|
|
39
|
+
);
|
|
44
40
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
await Promise.all([
|
|
49
|
-
api.listIncidents({ systemId, includeResolved: true }),
|
|
50
|
-
catalogApi.getSystems(),
|
|
51
|
-
]);
|
|
52
|
-
const systemData = systemList.find((s) => s.id === systemId);
|
|
53
|
-
setIncidents(incidentList);
|
|
54
|
-
setSystem(systemData);
|
|
55
|
-
} catch (error) {
|
|
56
|
-
console.error("Failed to load incidents:", error);
|
|
57
|
-
} finally {
|
|
58
|
-
setLoading(false);
|
|
59
|
-
}
|
|
60
|
-
};
|
|
41
|
+
// Fetch systems with useQuery
|
|
42
|
+
const { data: systemsData, isLoading: systemsLoading } =
|
|
43
|
+
catalogClient.getSystems.useQuery({});
|
|
61
44
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
45
|
+
const incidents = incidentsData?.incidents ?? [];
|
|
46
|
+
const systems = systemsData?.systems ?? [];
|
|
47
|
+
const system = systems.find((s) => s.id === systemId);
|
|
48
|
+
const loading = incidentsLoading || systemsLoading;
|
|
65
49
|
|
|
66
50
|
// Listen for realtime updates
|
|
67
51
|
useSignal(INCIDENT_UPDATED, ({ systemIds }) => {
|
|
68
52
|
if (systemId && systemIds.includes(systemId)) {
|
|
69
|
-
|
|
53
|
+
void refetchIncidents();
|
|
70
54
|
}
|
|
71
55
|
});
|
|
72
56
|
|