@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 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.1.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
- import { createApiRef } from "@checkstack/frontend-api";
2
- import { IncidentApi } from "@checkstack/incident-common";
3
- import type { InferClient } from "@checkstack/common";
4
-
5
- // IncidentApiClient type inferred from the client definition
6
- export type IncidentApiClient = InferClient<typeof IncidentApi>;
7
-
8
- export const incidentApiRef = createApiRef<IncidentApiClient>(
9
- "plugin.incident.api"
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, useCallback } from "react";
2
- import { useApi } from "@checkstack/frontend-api";
3
- import { incidentApiRef } from "../api";
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 api = useApi(incidentApiRef);
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, setLoadingUpdates] = useState(false);
63
+ const [loadingUpdates, _setLoadingUpdates] = useState(false);
65
64
  const [showUpdateForm, setShowUpdateForm] = useState(false);
66
65
 
67
- const loadIncidentDetails = useCallback(
68
- async (id: string) => {
69
- setLoadingUpdates(true);
70
- try {
71
- const detail = await api.getIncident({ id });
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
- [api]
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, loadIncidentDetails]);
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 = async () => {
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
- setSaving(true);
126
- try {
127
- if (incident) {
128
- await api.updateIncident({
129
- id: incident.id,
130
- title,
131
- description: description || undefined,
132
- severity,
133
- systemIds: [...selectedSystemIds],
134
- });
135
- toast.success("Incident updated");
136
- } else {
137
- await api.createIncident({
138
- title,
139
- description,
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
- loadIncidentDetails(incident.id);
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 { qualifyPermissionId, resolveRoute } from "@checkstack/common";
6
+ import { resolveRoute } from "@checkstack/common";
7
7
  import {
8
8
  incidentRoutes,
9
- permissions,
9
+ incidentAccess,
10
10
  pluginMetadata,
11
11
  } from "@checkstack/incident-common";
12
12
 
13
13
  export const IncidentMenuItems = ({
14
- permissions: userPerms,
14
+ accessRules: userPerms,
15
15
  }: UserMenuItemsContext) => {
16
- const qualifiedId = qualifyPermissionId(
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 { useApi } from "@checkstack/frontend-api";
3
- import { incidentApiRef } from "../api";
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 api = useApi(incidentApiRef);
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 handleSubmit = async () => {
41
- if (!message.trim()) {
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
- } catch (error) {
58
- const errorMessage =
59
- error instanceof Error ? error.message : "Failed to post update";
60
- toast.error(errorMessage);
61
- } finally {
62
- setIsPosting(false);
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={isPosting || !message.trim()}
110
+ disabled={addUpdateMutation.isPending || !message.trim()}
111
111
  >
112
- {isPosting ? (
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, { useEffect, useState, useCallback } from "react";
2
- import { useApi, type SlotContext } from "@checkstack/frontend-api";
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 { incidentApiRef } from "../api";
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 api = useApi(incidentApiRef);
23
- const [activeIncident, setActiveIncident] = useState<
24
- IncidentWithSystems | undefined
25
- >();
43
+ const incidentClient = usePluginClient(IncidentApi);
44
+ const badgeData = useSystemBadgeDataOptional();
26
45
 
27
- const refetch = useCallback(() => {
28
- if (!system?.id) return;
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
- api
31
- .getIncidentsForSystem({ systemId: system.id })
32
- .then((incidents: IncidentWithSystems[]) => {
33
- // Get the most severe active incident
34
- const sorted = [...incidents].toSorted((a, b) => {
35
- return (
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
- // Initial fetch
47
- useEffect(() => {
48
- refetch();
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, { useEffect, useState, useCallback } from "react";
1
+ import React from "react";
2
2
  import { Link } from "react-router-dom";
3
- import { useApi, type SlotContext } from "@checkstack/frontend-api";
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 { incidentApiRef } from "../api";
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 api = useApi(incidentApiRef);
80
- const [incidents, setIncidents] = useState<IncidentWithSystems[]>([]);
81
- const [loading, setLoading] = useState(true);
79
+ const incidentClient = usePluginClient(IncidentApi);
82
80
 
83
- const refetch = useCallback(() => {
84
- if (!system?.id) return;
85
-
86
- api
87
- .getIncidentsForSystem({ systemId: system.id })
88
- .then(setIncidents)
89
- .catch(console.error)
90
- .finally(() => setLoading(false));
91
- }, [system?.id, api]);
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
- permissions,
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
- permission: permissions.incidentManage,
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
- apis: [
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, useMemo } from "react";
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 { incidentApiRef } from "../api";
9
+ import { IncidentApi } from "../api";
10
10
  import type {
11
11
  IncidentWithSystems,
12
12
  IncidentStatus,
13
13
  } from "@checkstack/incident-common";
14
- import { CatalogApi, type System } from "@checkstack/catalog-common";
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 api = useApi(incidentApiRef);
52
- const rpcApi = useApi(rpcApiRef);
53
- const permissionApi = useApi(permissionApiRef);
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: permissionLoading } =
60
- permissionApi.useResourcePermission("incident", "manage");
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
- const loadData = async () => {
85
- setLoading(true);
86
- try {
87
- const [{ incidents: incidentList }, { systems: systemList }] =
88
- await Promise.all([
89
- api.listIncidents(
90
- statusFilter === "all"
91
- ? { includeResolved: showResolved }
92
- : { status: statusFilter, includeResolved: showResolved }
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
- useEffect(() => {
107
- loadData();
108
- }, [statusFilter, showResolved]);
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 = async () => {
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 = async () => {
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
- loadData();
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={permissionLoading}
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={isDeleting}
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={isResolving}
367
+ isLoading={resolveMutation.isPending}
383
368
  />
384
369
  </PageLayout>
385
370
  );
@@ -1,20 +1,20 @@
1
- import React, { useEffect, useState, useCallback, useMemo } from "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 { incidentApiRef } from "../api";
11
+ import { IncidentApi } from "../api";
12
12
  import {
13
13
  incidentRoutes,
14
14
  INCIDENT_UPDATED,
15
- type IncidentDetail,
15
+ incidentAccess,
16
16
  } from "@checkstack/incident-common";
17
- import { CatalogApi, type System } from "@checkstack/catalog-common";
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 api = useApi(incidentApiRef);
52
- const rpcApi = useApi(rpcApiRef);
53
- const permissionApi = useApi(permissionApiRef);
51
+ const incidentClient = usePluginClient(IncidentApi);
52
+ const catalogClient = usePluginClient(CatalogApi);
53
+ const accessApi = useApi(accessApiRef);
54
54
  const toast = useToast();
55
55
 
56
- const catalogApi = useMemo(() => rpcApi.forPlugin(CatalogApi), [rpcApi]);
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
- const loadData = useCallback(async () => {
69
- if (!incidentId) return;
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
- try {
72
- const [incidentData, { systems: systemList }] = await Promise.all([
73
- api.getIncident({ id: incidentId }),
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
- useEffect(() => {
86
- loadData();
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
- loadData();
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
- loadData();
99
+ void refetchIncident();
99
100
  };
100
101
 
101
- const handleResolve = async () => {
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 variant="outline" size="sm" onClick={handleResolve}>
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, { useEffect, useState, useMemo } from "react";
1
+ import React from "react";
2
2
  import { useParams, Link } from "react-router-dom";
3
- import { useApi, rpcApiRef, wrapInSuspense } from "@checkstack/frontend-api";
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 { incidentApiRef } from "../api";
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 api = useApi(incidentApiRef);
34
- const rpcApi = useApi(rpcApiRef);
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
- const loadData = async () => {
43
- if (!systemId) return;
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
- setLoading(true);
46
- try {
47
- const [{ incidents: incidentList }, { systems: systemList }] =
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
- useEffect(() => {
63
- loadData();
64
- }, [systemId]);
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
- loadData();
53
+ void refetchIncidents();
70
54
  }
71
55
  });
72
56