@checkstack/incident-frontend 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,84 @@
1
1
  # @checkstack/incident-frontend
2
2
 
3
+ ## 0.3.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [4eed42d]
8
+ - @checkstack/frontend-api@0.3.0
9
+ - @checkstack/dashboard-frontend@0.3.0
10
+ - @checkstack/auth-frontend@0.3.1
11
+ - @checkstack/catalog-common@1.2.1
12
+ - @checkstack/incident-common@0.3.1
13
+ - @checkstack/ui@0.2.2
14
+
15
+ ## 0.3.0
16
+
17
+ ### Minor Changes
18
+
19
+ - 7a23261: ## TanStack Query Integration
20
+
21
+ Migrated all frontend components to use `usePluginClient` hook with TanStack Query integration, replacing the legacy `forPlugin()` pattern.
22
+
23
+ ### New Features
24
+
25
+ - **`usePluginClient` hook**: Provides type-safe access to plugin APIs with `.useQuery()` and `.useMutation()` methods
26
+ - **Automatic request deduplication**: Multiple components requesting the same data share a single network request
27
+ - **Built-in caching**: Configurable stale time and cache duration per query
28
+ - **Loading/error states**: TanStack Query provides `isLoading`, `error`, `isRefetching` states automatically
29
+ - **Background refetching**: Stale data is automatically refreshed when components mount
30
+
31
+ ### Contract Changes
32
+
33
+ All RPC contracts now require `operationType: "query"` or `operationType: "mutation"` metadata:
34
+
35
+ ```typescript
36
+ const getItems = proc()
37
+ .meta({ operationType: "query", access: [access.read] })
38
+ .output(z.array(itemSchema))
39
+ .query();
40
+
41
+ const createItem = proc()
42
+ .meta({ operationType: "mutation", access: [access.manage] })
43
+ .input(createItemSchema)
44
+ .output(itemSchema)
45
+ .mutation();
46
+ ```
47
+
48
+ ### Migration
49
+
50
+ ```typescript
51
+ // Before (forPlugin pattern)
52
+ const api = useApi(myPluginApiRef);
53
+ const [items, setItems] = useState<Item[]>([]);
54
+ useEffect(() => {
55
+ api.getItems().then(setItems);
56
+ }, [api]);
57
+
58
+ // After (usePluginClient pattern)
59
+ const client = usePluginClient(MyPluginApi);
60
+ const { data: items, isLoading } = client.getItems.useQuery({});
61
+ ```
62
+
63
+ ### Bug Fixes
64
+
65
+ - Fixed `rpc.test.ts` test setup for middleware type inference
66
+ - Fixed `SearchDialog` to use `setQuery` instead of deprecated `search` method
67
+ - Fixed null→undefined warnings in notification and queue frontends
68
+
69
+ ### Patch Changes
70
+
71
+ - Updated dependencies [180be38]
72
+ - Updated dependencies [7a23261]
73
+ - @checkstack/dashboard-frontend@0.2.0
74
+ - @checkstack/frontend-api@0.2.0
75
+ - @checkstack/common@0.3.0
76
+ - @checkstack/auth-frontend@0.3.0
77
+ - @checkstack/catalog-common@1.2.0
78
+ - @checkstack/incident-common@0.3.0
79
+ - @checkstack/ui@0.2.1
80
+ - @checkstack/signal-frontend@0.0.7
81
+
3
82
  ## 0.2.0
4
83
 
5
84
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/incident-frontend",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
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">
@@ -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,14 +1,10 @@
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";
@@ -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,18 +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
- useApi,
5
- rpcApiRef,
4
+ usePluginClient,
6
5
  accessApiRef,
6
+ useApi,
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
14
  import { incidentAccess } from "@checkstack/incident-common";
15
- import { CatalogApi, type System } from "@checkstack/catalog-common";
15
+ import { CatalogApi } from "@checkstack/catalog-common";
16
16
  import {
17
17
  Card,
18
18
  CardHeader,
@@ -49,20 +49,16 @@ import { formatDistanceToNow } from "date-fns";
49
49
  import { IncidentEditor } from "../components/IncidentEditor";
50
50
 
51
51
  const IncidentConfigPageContent: React.FC = () => {
52
- const api = useApi(incidentApiRef);
53
- const rpcApi = useApi(rpcApiRef);
52
+ const incidentClient = usePluginClient(IncidentApi);
53
+ const catalogClient = usePluginClient(CatalogApi);
54
54
  const accessApi = useApi(accessApiRef);
55
55
  const [searchParams, setSearchParams] = useSearchParams();
56
-
57
- const catalogApi = useMemo(() => rpcApi.forPlugin(CatalogApi), [rpcApi]);
58
56
  const toast = useToast();
59
57
 
60
- const { allowed: canManage, loading: accessLoading } =
61
- accessApi.useAccess(incidentAccess.incident.manage);
58
+ const { allowed: canManage, loading: accessLoading } = accessApi.useAccess(
59
+ incidentAccess.incident.manage
60
+ );
62
61
 
63
- const [incidents, setIncidents] = useState<IncidentWithSystems[]>([]);
64
- const [systems, setSystems] = useState<System[]>([]);
65
- const [loading, setLoading] = useState(true);
66
62
  const [statusFilter, setStatusFilter] = useState<IncidentStatus | "all">(
67
63
  "all"
68
64
  );
@@ -76,37 +72,28 @@ const IncidentConfigPageContent: React.FC = () => {
76
72
 
77
73
  // Delete confirmation state
78
74
  const [deleteId, setDeleteId] = useState<string | undefined>();
79
- const [isDeleting, setIsDeleting] = useState(false);
80
75
 
81
76
  // Resolve confirmation state
82
77
  const [resolveId, setResolveId] = useState<string | undefined>();
83
- const [isResolving, setIsResolving] = useState(false);
84
78
 
85
- const loadData = async () => {
86
- setLoading(true);
87
- try {
88
- const [{ incidents: incidentList }, { systems: systemList }] =
89
- await Promise.all([
90
- api.listIncidents(
91
- statusFilter === "all"
92
- ? { includeResolved: showResolved }
93
- : { status: statusFilter, includeResolved: showResolved }
94
- ),
95
- catalogApi.getSystems(),
96
- ]);
97
- setIncidents(incidentList);
98
- setSystems(systemList);
99
- } catch (error) {
100
- const message = error instanceof Error ? error.message : "Failed to load";
101
- toast.error(message);
102
- } finally {
103
- setLoading(false);
104
- }
105
- };
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
+ );
106
89
 
107
- useEffect(() => {
108
- loadData();
109
- }, [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;
110
97
 
111
98
  // Handle ?action=create URL parameter (from command palette)
112
99
  useEffect(() => {
@@ -119,6 +106,29 @@ const IncidentConfigPageContent: React.FC = () => {
119
106
  }
120
107
  }, [searchParams, canManage, setSearchParams]);
121
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
+
122
132
  const handleCreate = () => {
123
133
  setEditingIncident(undefined);
124
134
  setEditorOpen(true);
@@ -129,45 +139,19 @@ const IncidentConfigPageContent: React.FC = () => {
129
139
  setEditorOpen(true);
130
140
  };
131
141
 
132
- const handleDelete = async () => {
142
+ const handleDelete = () => {
133
143
  if (!deleteId) return;
134
-
135
- setIsDeleting(true);
136
- try {
137
- await api.deleteIncident({ id: deleteId });
138
- toast.success("Incident deleted");
139
- loadData();
140
- } catch (error) {
141
- const message =
142
- error instanceof Error ? error.message : "Failed to delete";
143
- toast.error(message);
144
- } finally {
145
- setIsDeleting(false);
146
- setDeleteId(undefined);
147
- }
144
+ deleteMutation.mutate({ id: deleteId });
148
145
  };
149
146
 
150
- const handleResolve = async () => {
147
+ const handleResolve = () => {
151
148
  if (!resolveId) return;
152
-
153
- setIsResolving(true);
154
- try {
155
- await api.resolveIncident({ id: resolveId });
156
- toast.success("Incident resolved");
157
- loadData();
158
- } catch (error) {
159
- const message =
160
- error instanceof Error ? error.message : "Failed to resolve";
161
- toast.error(message);
162
- } finally {
163
- setIsResolving(false);
164
- setResolveId(undefined);
165
- }
149
+ resolveMutation.mutate({ id: resolveId });
166
150
  };
167
151
 
168
152
  const handleSave = () => {
169
153
  setEditorOpen(false);
170
- loadData();
154
+ void refetchIncidents();
171
155
  };
172
156
 
173
157
  const getStatusBadge = (status: IncidentStatus) => {
@@ -369,7 +353,7 @@ const IncidentConfigPageContent: React.FC = () => {
369
353
  confirmText="Delete"
370
354
  variant="danger"
371
355
  onConfirm={handleDelete}
372
- isLoading={isDeleting}
356
+ isLoading={deleteMutation.isPending}
373
357
  />
374
358
 
375
359
  <ConfirmationModal
@@ -380,7 +364,7 @@ const IncidentConfigPageContent: React.FC = () => {
380
364
  confirmText="Resolve"
381
365
  variant="info"
382
366
  onConfirm={handleResolve}
383
- isLoading={isResolving}
367
+ isLoading={resolveMutation.isPending}
384
368
  />
385
369
  </PageLayout>
386
370
  );
@@ -1,21 +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
- useApi,
5
- rpcApiRef,
4
+ usePluginClient,
6
5
  accessApiRef,
6
+ useApi,
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,
16
15
  incidentAccess,
17
16
  } from "@checkstack/incident-common";
18
- import { CatalogApi, type System } from "@checkstack/catalog-common";
17
+ import { CatalogApi } from "@checkstack/catalog-common";
19
18
  import {
20
19
  Card,
21
20
  CardHeader,
@@ -49,67 +48,60 @@ const IncidentDetailPageContent: React.FC = () => {
49
48
  const { incidentId } = useParams<{ incidentId: string }>();
50
49
  const navigate = useNavigate();
51
50
  const [searchParams] = useSearchParams();
52
- const api = useApi(incidentApiRef);
53
- const rpcApi = useApi(rpcApiRef);
51
+ const incidentClient = usePluginClient(IncidentApi);
52
+ const catalogClient = usePluginClient(CatalogApi);
54
53
  const accessApi = useApi(accessApiRef);
55
54
  const toast = useToast();
56
55
 
57
- const catalogApi = useMemo(() => rpcApi.forPlugin(CatalogApi), [rpcApi]);
58
-
59
56
  const { allowed: canManage } = accessApi.useAccess(
60
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