@checkstack/maintenance-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/maintenance-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/maintenance-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/maintenance-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/maintenance-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/maintenance-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 { MaintenanceApi } from "@checkstack/maintenance-common";
3
- import type { InferClient } from "@checkstack/common";
4
-
5
- // MaintenanceApiClient type inferred from the client definition
6
- export type MaintenanceApiClient = InferClient<typeof MaintenanceApi>;
7
-
8
- export const maintenanceApiRef = createApiRef<MaintenanceApiClient>(
9
- "plugin.maintenance.api"
10
- );
1
+ // Re-export types for convenience
2
+ export type {
3
+ MaintenanceWithSystems,
4
+ MaintenanceDetail,
5
+ MaintenanceUpdate,
6
+ MaintenanceStatus,
7
+ } from "@checkstack/maintenance-common";
8
+ // Client definition is in @checkstack/maintenance-common - use with usePluginClient
9
+ export { MaintenanceApi } from "@checkstack/maintenance-common";
@@ -1,6 +1,6 @@
1
- import React, { useState, useEffect, useCallback } from "react";
2
- import { useApi } from "@checkstack/frontend-api";
3
- import { maintenanceApiRef } from "../api";
1
+ import React, { useState, useEffect } from "react";
2
+ import { usePluginClient } from "@checkstack/frontend-api";
3
+ import { MaintenanceApi } from "../api";
4
4
  import type {
5
5
  MaintenanceWithSystems,
6
6
  MaintenanceUpdate,
@@ -42,7 +42,7 @@ export const MaintenanceEditor: React.FC<Props> = ({
42
42
  systems,
43
43
  onSave,
44
44
  }) => {
45
- const api = useApi(maintenanceApiRef);
45
+ const maintenanceClient = usePluginClient(MaintenanceApi);
46
46
  const toast = useToast();
47
47
 
48
48
  // Maintenance fields
@@ -53,29 +53,45 @@ export const MaintenanceEditor: React.FC<Props> = ({
53
53
  const [selectedSystemIds, setSelectedSystemIds] = useState<Set<string>>(
54
54
  new Set()
55
55
  );
56
- const [saving, setSaving] = useState(false);
57
56
 
58
57
  // Status update fields
59
58
  const [updates, setUpdates] = useState<MaintenanceUpdate[]>([]);
60
- const [loadingUpdates, setLoadingUpdates] = useState(false);
61
59
  const [showUpdateForm, setShowUpdateForm] = useState(false);
62
60
 
63
- const loadMaintenanceDetails = useCallback(
64
- async (id: string) => {
65
- setLoadingUpdates(true);
66
- try {
67
- const detail = await api.getMaintenance({ id });
68
- if (detail) {
69
- setUpdates(detail.updates);
70
- }
71
- } catch (error) {
72
- console.error("Failed to load maintenance details:", error);
73
- } finally {
74
- setLoadingUpdates(false);
75
- }
61
+ // Query for maintenance details (only when editing)
62
+ const { data: maintenanceDetail, refetch: refetchDetail } =
63
+ maintenanceClient.getMaintenance.useQuery(
64
+ { id: maintenance?.id ?? "" },
65
+ { enabled: !!maintenance?.id && open }
66
+ );
67
+
68
+ // Mutations
69
+ const createMutation = maintenanceClient.createMaintenance.useMutation({
70
+ onSuccess: () => {
71
+ toast.success("Maintenance created");
72
+ onSave();
76
73
  },
77
- [api]
78
- );
74
+ onError: (error) => {
75
+ toast.error(error instanceof Error ? error.message : "Failed to save");
76
+ },
77
+ });
78
+
79
+ const updateMutation = maintenanceClient.updateMaintenance.useMutation({
80
+ onSuccess: () => {
81
+ toast.success("Maintenance updated");
82
+ onSave();
83
+ },
84
+ onError: (error) => {
85
+ toast.error(error instanceof Error ? error.message : "Failed to save");
86
+ },
87
+ });
88
+
89
+ // Sync updates from query
90
+ useEffect(() => {
91
+ if (maintenanceDetail) {
92
+ setUpdates(maintenanceDetail.updates);
93
+ }
94
+ }, [maintenanceDetail]);
79
95
 
80
96
  // Reset form when maintenance changes
81
97
  useEffect(() => {
@@ -85,8 +101,6 @@ export const MaintenanceEditor: React.FC<Props> = ({
85
101
  setStartAt(new Date(maintenance.startAt));
86
102
  setEndAt(new Date(maintenance.endAt));
87
103
  setSelectedSystemIds(new Set(maintenance.systemIds));
88
- // Load full maintenance with updates
89
- loadMaintenanceDetails(maintenance.id);
90
104
  } else {
91
105
  // Default to 1 hour from now to 2 hours from now
92
106
  const now = new Date();
@@ -100,7 +114,7 @@ export const MaintenanceEditor: React.FC<Props> = ({
100
114
  setUpdates([]);
101
115
  setShowUpdateForm(false);
102
116
  }
103
- }, [maintenance, open, loadMaintenanceDetails]);
117
+ }, [maintenance, open]);
104
118
 
105
119
  const handleSystemToggle = (systemId: string) => {
106
120
  setSelectedSystemIds((prev) => {
@@ -114,7 +128,7 @@ export const MaintenanceEditor: React.FC<Props> = ({
114
128
  });
115
129
  };
116
130
 
117
- const handleSubmit = async () => {
131
+ const handleSubmit = () => {
118
132
  if (!title.trim()) {
119
133
  toast.error("Title is required");
120
134
  return;
@@ -128,46 +142,38 @@ export const MaintenanceEditor: React.FC<Props> = ({
128
142
  return;
129
143
  }
130
144
 
131
- setSaving(true);
132
- try {
133
- if (maintenance) {
134
- await api.updateMaintenance({
135
- id: maintenance.id,
136
- title,
137
- description: description || undefined,
138
- startAt,
139
- endAt,
140
- systemIds: [...selectedSystemIds],
141
- });
142
- toast.success("Maintenance updated");
143
- } else {
144
- await api.createMaintenance({
145
- title,
146
- description,
147
- startAt,
148
- endAt,
149
- systemIds: [...selectedSystemIds],
150
- });
151
- toast.success("Maintenance created");
152
- }
153
- onSave();
154
- } catch (error) {
155
- const message = error instanceof Error ? error.message : "Failed to save";
156
- toast.error(message);
157
- } finally {
158
- setSaving(false);
145
+ if (maintenance) {
146
+ updateMutation.mutate({
147
+ id: maintenance.id,
148
+ title,
149
+ description: description || undefined,
150
+ startAt,
151
+ endAt,
152
+ systemIds: [...selectedSystemIds],
153
+ });
154
+ } else {
155
+ createMutation.mutate({
156
+ title,
157
+ description,
158
+ startAt,
159
+ endAt,
160
+ systemIds: [...selectedSystemIds],
161
+ });
159
162
  }
160
163
  };
161
164
 
162
165
  const handleUpdateSuccess = () => {
163
166
  if (maintenance) {
164
- loadMaintenanceDetails(maintenance.id);
167
+ void refetchDetail();
165
168
  }
166
169
  setShowUpdateForm(false);
167
170
  // Notify parent to refresh list (status may have changed)
168
171
  onSave();
169
172
  };
170
173
 
174
+ const saving = createMutation.isPending || updateMutation.isPending;
175
+ const loadingUpdates = false; // Now handled by useQuery
176
+
171
177
  return (
172
178
  <Dialog open={open} onOpenChange={onOpenChange}>
173
179
  <DialogContent size="xl">
@@ -1,6 +1,6 @@
1
1
  import React, { useState } from "react";
2
- import { useApi } from "@checkstack/frontend-api";
3
- import { maintenanceApiRef } from "../api";
2
+ import { usePluginClient } from "@checkstack/frontend-api";
3
+ import { MaintenanceApi } from "../api";
4
4
  import type { MaintenanceStatus } from "@checkstack/maintenance-common";
5
5
  import {
6
6
  Button,
@@ -30,37 +30,37 @@ export const MaintenanceUpdateForm: React.FC<MaintenanceUpdateFormProps> = ({
30
30
  onSuccess,
31
31
  onCancel,
32
32
  }) => {
33
- const api = useApi(maintenanceApiRef);
33
+ const maintenanceClient = usePluginClient(MaintenanceApi);
34
34
  const toast = useToast();
35
35
 
36
36
  const [message, setMessage] = useState("");
37
37
  const [statusChange, setStatusChange] = useState<MaintenanceStatus | "">("");
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
- maintenanceId,
50
- message,
51
- statusChange: statusChange || undefined,
52
- });
39
+ const addUpdateMutation = maintenanceClient.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
+ maintenanceId,
61
+ message,
62
+ statusChange: statusChange || undefined,
63
+ });
64
64
  };
65
65
 
66
66
  return (
@@ -106,9 +106,9 @@ export const MaintenanceUpdateForm: React.FC<MaintenanceUpdateFormProps> = ({
106
106
  <Button
107
107
  size="sm"
108
108
  onClick={handleSubmit}
109
- disabled={isPosting || !message.trim()}
109
+ disabled={addUpdateMutation.isPending || !message.trim()}
110
110
  >
111
- {isPosting ? (
111
+ {addUpdateMutation.isPending ? (
112
112
  <>
113
113
  <Loader2 className="h-4 w-4 mr-1 animate-spin" />
114
114
  Posting...
@@ -1,49 +1,64 @@
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 { maintenanceApiRef } from "../api";
5
+ import { MaintenanceApi } from "../api";
6
6
  import {
7
7
  MAINTENANCE_UPDATED,
8
8
  type MaintenanceWithSystems,
9
9
  } from "@checkstack/maintenance-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
 
15
+ /**
16
+ * Checks if any maintenance is currently in progress.
17
+ */
18
+ function hasActiveMaintenance(maintenances: MaintenanceWithSystems[]): boolean {
19
+ return maintenances.some((m) => m.status === "in_progress");
20
+ }
21
+
14
22
  /**
15
23
  * Displays a maintenance badge for a system when it has an active maintenance.
16
24
  * Shows nothing if no active maintenance.
25
+ *
26
+ * When rendered within SystemBadgeDataProvider, uses bulk-fetched data.
27
+ * Otherwise, falls back to individual fetch.
28
+ *
17
29
  * Listens for realtime updates via signals.
18
30
  */
19
31
  export const SystemMaintenanceBadge: React.FC<Props> = ({ system }) => {
20
- const api = useApi(maintenanceApiRef);
21
- const [hasActiveMaintenance, setHasActiveMaintenance] = useState(false);
22
-
23
- const refetch = useCallback(() => {
24
- if (!system?.id) return;
25
-
26
- api
27
- .getMaintenancesForSystem({ systemId: system.id })
28
- .then((maintenances: MaintenanceWithSystems[]) => {
29
- const active = maintenances.some((m) => m.status === "in_progress");
30
- setHasActiveMaintenance(active);
31
- })
32
- .catch(console.error);
33
- }, [system?.id, api]);
34
-
35
- // Initial fetch
36
- useEffect(() => {
37
- refetch();
38
- }, [refetch]);
39
-
40
- // Listen for realtime maintenance updates
32
+ const maintenanceClient = usePluginClient(MaintenanceApi);
33
+ const badgeData = useSystemBadgeDataOptional();
34
+
35
+ // Try to get data from provider first
36
+ const providerData = badgeData?.getSystemBadgeData(system?.id ?? "");
37
+ const providerHasActive = providerData
38
+ ? hasActiveMaintenance(providerData.maintenances)
39
+ : false;
40
+
41
+ // Query for maintenances if not using provider
42
+ const { data: maintenances, refetch } =
43
+ maintenanceClient.getMaintenancesForSystem.useQuery(
44
+ { systemId: system?.id ?? "" },
45
+ { enabled: !badgeData && !!system?.id }
46
+ );
47
+
48
+ const localHasActive = maintenances
49
+ ? hasActiveMaintenance(maintenances)
50
+ : false;
51
+
52
+ // Listen for realtime maintenance updates (only in fallback mode)
41
53
  useSignal(MAINTENANCE_UPDATED, ({ systemIds }) => {
42
- if (system?.id && systemIds.includes(system.id)) {
43
- refetch();
54
+ if (!badgeData && system?.id && systemIds.includes(system.id)) {
55
+ void refetch();
44
56
  }
45
57
  });
46
58
 
47
- if (!hasActiveMaintenance) return;
59
+ // Use provider data if available, otherwise use local state
60
+ const hasActive = badgeData ? providerHasActive : localHasActive;
61
+
62
+ if (!hasActive) return;
48
63
  return <Badge variant="warning">Under Maintenance</Badge>;
49
64
  };
@@ -1,14 +1,13 @@
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 { SystemDetailsSlot } from "@checkstack/catalog-common";
7
- import { maintenanceApiRef } from "../api";
7
+ import { MaintenanceApi } from "../api";
8
8
  import {
9
9
  maintenanceRoutes,
10
10
  MAINTENANCE_UPDATED,
11
- type MaintenanceWithSystems,
12
11
  } from "@checkstack/maintenance-common";
13
12
  import {
14
13
  Card,
@@ -29,31 +28,22 @@ type Props = SlotContext<typeof SystemDetailsSlot>;
29
28
  * Listens for realtime updates via signals.
30
29
  */
31
30
  export const SystemMaintenancePanel: React.FC<Props> = ({ system }) => {
32
- const api = useApi(maintenanceApiRef);
33
- const [maintenances, setMaintenances] = useState<MaintenanceWithSystems[]>(
34
- []
35
- );
36
- const [loading, setLoading] = useState(true);
37
-
38
- const refetch = useCallback(() => {
39
- if (!system?.id) return;
31
+ const maintenanceClient = usePluginClient(MaintenanceApi);
40
32
 
41
- api
42
- .getMaintenancesForSystem({ systemId: system.id })
43
- .then(setMaintenances)
44
- .catch(console.error)
45
- .finally(() => setLoading(false));
46
- }, [system?.id, api]);
47
-
48
- // Initial fetch
49
- useEffect(() => {
50
- refetch();
51
- }, [refetch]);
33
+ // Fetch maintenances with useQuery
34
+ const {
35
+ data: maintenances = [],
36
+ isLoading: loading,
37
+ refetch,
38
+ } = maintenanceClient.getMaintenancesForSystem.useQuery(
39
+ { systemId: system?.id ?? "" },
40
+ { enabled: !!system?.id }
41
+ );
52
42
 
53
43
  // Listen for realtime maintenance updates
54
44
  useSignal(MAINTENANCE_UPDATED, ({ systemIds }) => {
55
45
  if (system?.id && systemIds.includes(system.id)) {
56
- refetch();
46
+ void refetch();
57
47
  }
58
48
  });
59
49
 
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 { maintenanceApiRef, type MaintenanceApiClient } from "./api";
9
6
  import {
10
7
  maintenanceRoutes,
11
- MaintenanceApi,
12
8
  pluginMetadata,
13
9
  maintenanceAccess,
14
10
  } from "@checkstack/maintenance-common";
@@ -43,17 +39,8 @@ export default createFrontendPlugin({
43
39
  title: "Maintenance Details",
44
40
  },
45
41
  ],
46
- apis: [
47
- {
48
- ref: maintenanceApiRef,
49
- factory: (deps: {
50
- get: <T>(ref: ApiRef<T>) => T;
51
- }): MaintenanceApiClient => {
52
- const rpcApi = deps.get(rpcApiRef);
53
- return rpcApi.forPlugin(MaintenanceApi);
54
- },
55
- },
56
- ],
42
+ // No APIs needed - components use usePluginClient() directly
43
+ apis: [],
57
44
  extensions: [
58
45
  createSlotExtension(UserMenuItemsSlot, {
59
46
  id: "maintenance.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 { maintenanceApiRef } from "../api";
9
+ import { MaintenanceApi } from "../api";
10
10
  import type {
11
11
  MaintenanceWithSystems,
12
12
  MaintenanceStatus,
13
13
  } from "@checkstack/maintenance-common";
14
14
  import { maintenanceAccess } from "@checkstack/maintenance-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,
@@ -50,22 +50,16 @@ import { MaintenanceEditor } from "../components/MaintenanceEditor";
50
50
  import { getMaintenanceStatusBadge } from "../utils/badges";
51
51
 
52
52
  const MaintenanceConfigPageContent: React.FC = () => {
53
- const api = useApi(maintenanceApiRef);
54
- const rpcApi = useApi(rpcApiRef);
53
+ const maintenanceClient = usePluginClient(MaintenanceApi);
54
+ const catalogClient = usePluginClient(CatalogApi);
55
55
  const accessApi = useApi(accessApiRef);
56
56
  const [searchParams, setSearchParams] = useSearchParams();
57
-
58
- const catalogApi = useMemo(() => rpcApi.forPlugin(CatalogApi), [rpcApi]);
59
57
  const toast = useToast();
60
58
 
61
- const { allowed: canManage, loading: accessLoading } =
62
- accessApi.useAccess(maintenanceAccess.maintenance.manage);
63
-
64
- const [maintenances, setMaintenances] = useState<MaintenanceWithSystems[]>(
65
- []
59
+ const { allowed: canManage, loading: accessLoading } = accessApi.useAccess(
60
+ maintenanceAccess.maintenance.manage
66
61
  );
67
- const [systems, setSystems] = useState<System[]>([]);
68
- const [loading, setLoading] = useState(true);
62
+
69
63
  const [statusFilter, setStatusFilter] = useState<MaintenanceStatus | "all">(
70
64
  "all"
71
65
  );
@@ -78,35 +72,26 @@ const MaintenanceConfigPageContent: React.FC = () => {
78
72
 
79
73
  // Delete confirmation state
80
74
  const [deleteId, setDeleteId] = useState<string | undefined>();
81
- const [isDeleting, setIsDeleting] = useState(false);
82
75
 
83
76
  // Complete confirmation state
84
77
  const [completeId, setCompleteId] = useState<string | undefined>();
85
- const [isCompleting, setIsCompleting] = useState(false);
86
78
 
87
- const loadData = async () => {
88
- setLoading(true);
89
- try {
90
- const [{ maintenances: maintenanceList }, { systems: systemList }] =
91
- await Promise.all([
92
- api.listMaintenances(
93
- statusFilter === "all" ? undefined : { status: statusFilter }
94
- ),
95
- catalogApi.getSystems(),
96
- ]);
97
- setMaintenances(maintenanceList);
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 maintenances with useQuery
80
+ const {
81
+ data: maintenancesData,
82
+ isLoading: maintenancesLoading,
83
+ refetch: refetchMaintenances,
84
+ } = maintenanceClient.listMaintenances.useQuery(
85
+ statusFilter === "all" ? {} : { status: statusFilter }
86
+ );
106
87
 
107
- useEffect(() => {
108
- loadData();
109
- }, [statusFilter]);
88
+ // Fetch systems with useQuery
89
+ const { data: systemsData, isLoading: systemsLoading } =
90
+ catalogClient.getSystems.useQuery({});
91
+
92
+ const maintenances = maintenancesData?.maintenances ?? [];
93
+ const systems = systemsData?.systems ?? [];
94
+ const loading = maintenancesLoading || systemsLoading;
110
95
 
111
96
  // Handle ?action=create URL parameter (from command palette)
112
97
  useEffect(() => {
@@ -119,6 +104,31 @@ const MaintenanceConfigPageContent: React.FC = () => {
119
104
  }
120
105
  }, [searchParams, canManage, setSearchParams]);
121
106
 
107
+ // Mutations
108
+ const deleteMutation = maintenanceClient.deleteMaintenance.useMutation({
109
+ onSuccess: () => {
110
+ toast.success("Maintenance deleted");
111
+ void refetchMaintenances();
112
+ setDeleteId(undefined);
113
+ },
114
+ onError: (error) => {
115
+ toast.error(error instanceof Error ? error.message : "Failed to delete");
116
+ },
117
+ });
118
+
119
+ const completeMutation = maintenanceClient.closeMaintenance.useMutation({
120
+ onSuccess: () => {
121
+ toast.success("Maintenance completed");
122
+ void refetchMaintenances();
123
+ setCompleteId(undefined);
124
+ },
125
+ onError: (error) => {
126
+ toast.error(
127
+ error instanceof Error ? error.message : "Failed to complete"
128
+ );
129
+ },
130
+ });
131
+
122
132
  const handleCreate = () => {
123
133
  setEditingMaintenance(undefined);
124
134
  setEditorOpen(true);
@@ -129,45 +139,19 @@ const MaintenanceConfigPageContent: 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.deleteMaintenance({ id: deleteId });
138
- toast.success("Maintenance 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 handleComplete = async () => {
147
+ const handleComplete = () => {
151
148
  if (!completeId) return;
152
-
153
- setIsCompleting(true);
154
- try {
155
- await api.closeMaintenance({ id: completeId });
156
- toast.success("Maintenance completed");
157
- loadData();
158
- } catch (error) {
159
- const message =
160
- error instanceof Error ? error.message : "Failed to complete";
161
- toast.error(message);
162
- } finally {
163
- setIsCompleting(false);
164
- setCompleteId(undefined);
165
- }
149
+ completeMutation.mutate({ id: completeId });
166
150
  };
167
151
 
168
152
  const handleSave = () => {
169
153
  setEditorOpen(false);
170
- loadData();
154
+ void refetchMaintenances();
171
155
  };
172
156
 
173
157
  const getSystemNames = (systemIds: string[]): string => {
@@ -327,7 +311,7 @@ const MaintenanceConfigPageContent: React.FC = () => {
327
311
  confirmText="Delete"
328
312
  variant="danger"
329
313
  onConfirm={handleDelete}
330
- isLoading={isDeleting}
314
+ isLoading={deleteMutation.isPending}
331
315
  />
332
316
 
333
317
  <ConfirmationModal
@@ -338,7 +322,7 @@ const MaintenanceConfigPageContent: React.FC = () => {
338
322
  confirmText="Complete"
339
323
  variant="info"
340
324
  onConfirm={handleComplete}
341
- isLoading={isCompleting}
325
+ isLoading={completeMutation.isPending}
342
326
  />
343
327
  </PageLayout>
344
328
  );
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useState, useMemo, useCallback } from "react";
1
+ import React, { useState } from "react";
2
2
  import {
3
3
  useParams,
4
4
  Link,
@@ -6,23 +6,18 @@ import {
6
6
  useSearchParams,
7
7
  } from "react-router-dom";
8
8
  import {
9
- useApi,
10
- rpcApiRef,
9
+ usePluginClient,
11
10
  wrapInSuspense,
12
11
  accessApiRef,
12
+ useApi,
13
13
  } from "@checkstack/frontend-api";
14
14
  import { resolveRoute } from "@checkstack/common";
15
- import { maintenanceApiRef } from "../api";
15
+ import { MaintenanceApi } from "../api";
16
16
  import {
17
17
  maintenanceRoutes,
18
18
  maintenanceAccess,
19
19
  } from "@checkstack/maintenance-common";
20
- import type { MaintenanceDetail } from "@checkstack/maintenance-common";
21
- import {
22
- catalogRoutes,
23
- CatalogApi,
24
- type System,
25
- } from "@checkstack/catalog-common";
20
+ import { catalogRoutes, CatalogApi } from "@checkstack/catalog-common";
26
21
  import {
27
22
  Card,
28
23
  CardHeader,
@@ -54,61 +49,55 @@ const MaintenanceDetailPageContent: React.FC = () => {
54
49
  const { maintenanceId } = useParams<{ maintenanceId: string }>();
55
50
  const navigate = useNavigate();
56
51
  const [searchParams] = useSearchParams();
57
- const api = useApi(maintenanceApiRef);
58
- const rpcApi = useApi(rpcApiRef);
52
+ const maintenanceClient = usePluginClient(MaintenanceApi);
53
+ const catalogClient = usePluginClient(CatalogApi);
59
54
  const accessApi = useApi(accessApiRef);
60
55
  const toast = useToast();
61
56
 
62
- const catalogApi = useMemo(() => rpcApi.forPlugin(CatalogApi), [rpcApi]);
63
-
64
57
  const { allowed: canManage } = accessApi.useAccess(
65
58
  maintenanceAccess.maintenance.manage
66
59
  );
67
60
 
68
- const [maintenance, setMaintenance] = useState<MaintenanceDetail>();
69
- const [systems, setSystems] = useState<System[]>([]);
70
- const [loading, setLoading] = useState(true);
71
61
  const [showUpdateForm, setShowUpdateForm] = useState(false);
72
62
 
73
- const loadData = useCallback(async () => {
74
- if (!maintenanceId) return;
63
+ // Fetch maintenance with useQuery
64
+ const {
65
+ data: maintenance,
66
+ isLoading: maintenanceLoading,
67
+ refetch: refetchMaintenance,
68
+ } = maintenanceClient.getMaintenance.useQuery(
69
+ { id: maintenanceId ?? "" },
70
+ { enabled: !!maintenanceId }
71
+ );
75
72
 
76
- setLoading(true);
77
- try {
78
- const [maintenanceData, { systems: systemList }] = await Promise.all([
79
- api.getMaintenance({ id: maintenanceId }),
80
- catalogApi.getSystems(),
81
- ]);
82
- setMaintenance(maintenanceData ?? undefined);
83
- setSystems(systemList);
84
- } catch (error) {
85
- console.error("Failed to load maintenance details:", error);
86
- } finally {
87
- setLoading(false);
88
- }
89
- }, [maintenanceId, api, catalogApi]);
73
+ // Fetch systems with useQuery
74
+ const { data: systemsData, isLoading: systemsLoading } =
75
+ catalogClient.getSystems.useQuery({});
90
76
 
91
- useEffect(() => {
92
- loadData();
93
- }, [loadData]);
77
+ const systems = systemsData?.systems ?? [];
78
+ const loading = maintenanceLoading || systemsLoading;
79
+
80
+ // Complete mutation
81
+ const completeMutation = maintenanceClient.closeMaintenance.useMutation({
82
+ onSuccess: () => {
83
+ toast.success("Maintenance completed");
84
+ void refetchMaintenance();
85
+ },
86
+ onError: (error) => {
87
+ toast.error(
88
+ error instanceof Error ? error.message : "Failed to complete"
89
+ );
90
+ },
91
+ });
94
92
 
95
93
  const handleUpdateSuccess = () => {
96
94
  setShowUpdateForm(false);
97
- loadData();
95
+ void refetchMaintenance();
98
96
  };
99
97
 
100
- const handleComplete = async () => {
98
+ const handleComplete = () => {
101
99
  if (!maintenanceId) return;
102
-
103
- try {
104
- await api.closeMaintenance({ id: maintenanceId });
105
- toast.success("Maintenance completed");
106
- await loadData();
107
- } catch (error) {
108
- const message =
109
- error instanceof Error ? error.message : "Failed to complete";
110
- toast.error(message);
111
- }
100
+ completeMutation.mutate({ id: maintenanceId });
112
101
  };
113
102
 
114
103
  const getSystemName = (systemId: string): string => {
@@ -182,7 +171,12 @@ const MaintenanceDetailPageContent: React.FC = () => {
182
171
  <div className="flex items-center gap-2">
183
172
  {getMaintenanceStatusBadge(maintenance.status)}
184
173
  {canComplete && (
185
- <Button variant="outline" size="sm" onClick={handleComplete}>
174
+ <Button
175
+ variant="outline"
176
+ size="sm"
177
+ onClick={handleComplete}
178
+ disabled={completeMutation.isPending}
179
+ >
186
180
  <CheckCircle2 className="h-4 w-4 mr-1" />
187
181
  Complete
188
182
  </Button>
@@ -1,13 +1,10 @@
1
- import React, { useEffect, useState, useMemo } from "react";
1
+ import React from "react";
2
2
  import { useParams, useNavigate } from "react-router-dom";
3
- import { useApi, rpcApiRef, wrapInSuspense } from "@checkstack/frontend-api";
3
+ import { usePluginClient, wrapInSuspense } from "@checkstack/frontend-api";
4
4
  import { resolveRoute } from "@checkstack/common";
5
- import { maintenanceApiRef } from "../api";
5
+ import { MaintenanceApi } from "../api";
6
6
  import { maintenanceRoutes } from "@checkstack/maintenance-common";
7
- import type {
8
- MaintenanceWithSystems,
9
- MaintenanceStatus,
10
- } from "@checkstack/maintenance-common";
7
+ import type { MaintenanceStatus } from "@checkstack/maintenance-common";
11
8
  import { catalogRoutes, CatalogApi } from "@checkstack/catalog-common";
12
9
  import {
13
10
  Card,
@@ -32,40 +29,25 @@ import { format } from "date-fns";
32
29
  const SystemMaintenanceHistoryPageContent: React.FC = () => {
33
30
  const { systemId } = useParams<{ systemId: string }>();
34
31
  const navigate = useNavigate();
35
- const api = useApi(maintenanceApiRef);
36
- const rpcApi = useApi(rpcApiRef);
32
+ const maintenanceClient = usePluginClient(MaintenanceApi);
33
+ const catalogClient = usePluginClient(CatalogApi);
37
34
 
38
- const catalogApi = useMemo(() => rpcApi.forPlugin(CatalogApi), [rpcApi]);
39
-
40
- const [maintenances, setMaintenances] = useState<MaintenanceWithSystems[]>(
41
- []
42
- );
43
- const [systemName, setSystemName] = useState<string>("");
44
- const [loading, setLoading] = useState(true);
45
-
46
- useEffect(() => {
47
- if (!systemId) return;
35
+ // Fetch maintenances with useQuery
36
+ const { data: maintenancesData, isLoading: maintenancesLoading } =
37
+ maintenanceClient.listMaintenances.useQuery(
38
+ { systemId },
39
+ { enabled: !!systemId }
40
+ );
48
41
 
49
- const loadData = async () => {
50
- setLoading(true);
51
- try {
52
- const [{ maintenances: maintenanceList }, { systems: systemList }] =
53
- await Promise.all([
54
- api.listMaintenances({ systemId }),
55
- catalogApi.getSystems(),
56
- ]);
57
- setMaintenances(maintenanceList);
58
- const system = systemList.find((s) => s.id === systemId);
59
- setSystemName(system?.name ?? "Unknown System");
60
- } catch (error) {
61
- console.error("Failed to load maintenance history:", error);
62
- } finally {
63
- setLoading(false);
64
- }
65
- };
42
+ // Fetch systems with useQuery
43
+ const { data: systemsData, isLoading: systemsLoading } =
44
+ catalogClient.getSystems.useQuery({});
66
45
 
67
- loadData();
68
- }, [systemId, api, catalogApi]);
46
+ const maintenances = maintenancesData?.maintenances ?? [];
47
+ const systems = systemsData?.systems ?? [];
48
+ const system = systems.find((s) => s.id === systemId);
49
+ const systemName = system?.name ?? "Unknown System";
50
+ const loading = maintenancesLoading || systemsLoading;
69
51
 
70
52
  const getStatusBadge = (status: MaintenanceStatus) => {
71
53
  switch (status) {