@checkstack/maintenance-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/maintenance-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/maintenance-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/maintenance-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/maintenance-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/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">
@@ -3,20 +3,17 @@ import { Link } from "react-router-dom";
3
3
  import { Wrench } 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
  maintenanceRoutes,
9
- permissions,
9
+ maintenanceAccess,
10
10
  pluginMetadata,
11
11
  } from "@checkstack/maintenance-common";
12
12
 
13
13
  export const MaintenanceMenuItems = ({
14
- permissions: userPerms,
14
+ accessRules: userPerms,
15
15
  }: UserMenuItemsContext) => {
16
- const qualifiedId = qualifyPermissionId(
17
- pluginMetadata,
18
- permissions.maintenanceManage
19
- );
16
+ const qualifiedId = `${pluginMetadata.pluginId}.${maintenanceAccess.maintenance.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 { 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,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 { maintenanceApiRef, type MaintenanceApiClient } from "./api";
9
6
  import {
10
7
  maintenanceRoutes,
11
- MaintenanceApi,
12
8
  pluginMetadata,
13
- permissions,
9
+ maintenanceAccess,
14
10
  } from "@checkstack/maintenance-common";
15
11
  import {
16
12
  SystemDetailsTopSlot,
@@ -30,7 +26,7 @@ export default createFrontendPlugin({
30
26
  route: maintenanceRoutes.routes.config,
31
27
  element: <MaintenanceConfigPage />,
32
28
  title: "Maintenances",
33
- permission: permissions.maintenanceManage,
29
+ accessRule: maintenanceAccess.maintenance.manage,
34
30
  },
35
31
  {
36
32
  route: maintenanceRoutes.routes.systemHistory,
@@ -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,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 { 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
- import { CatalogApi, type System } from "@checkstack/catalog-common";
14
+ import { maintenanceAccess } from "@checkstack/maintenance-common";
15
+ import { CatalogApi } from "@checkstack/catalog-common";
15
16
  import {
16
17
  Card,
17
18
  CardHeader,
@@ -49,22 +50,16 @@ import { MaintenanceEditor } from "../components/MaintenanceEditor";
49
50
  import { getMaintenanceStatusBadge } from "../utils/badges";
50
51
 
51
52
  const MaintenanceConfigPageContent: React.FC = () => {
52
- const api = useApi(maintenanceApiRef);
53
- const rpcApi = useApi(rpcApiRef);
54
- const permissionApi = useApi(permissionApiRef);
53
+ const maintenanceClient = usePluginClient(MaintenanceApi);
54
+ const catalogClient = usePluginClient(CatalogApi);
55
+ const accessApi = useApi(accessApiRef);
55
56
  const [searchParams, setSearchParams] = useSearchParams();
56
-
57
- const catalogApi = useMemo(() => rpcApi.forPlugin(CatalogApi), [rpcApi]);
58
57
  const toast = useToast();
59
58
 
60
- const { allowed: canManage, loading: permissionLoading } =
61
- permissionApi.useResourcePermission("maintenance", "manage");
62
-
63
- const [maintenances, setMaintenances] = useState<MaintenanceWithSystems[]>(
64
- []
59
+ const { allowed: canManage, loading: accessLoading } = accessApi.useAccess(
60
+ maintenanceAccess.maintenance.manage
65
61
  );
66
- const [systems, setSystems] = useState<System[]>([]);
67
- const [loading, setLoading] = useState(true);
62
+
68
63
  const [statusFilter, setStatusFilter] = useState<MaintenanceStatus | "all">(
69
64
  "all"
70
65
  );
@@ -77,35 +72,26 @@ const MaintenanceConfigPageContent: React.FC = () => {
77
72
 
78
73
  // Delete confirmation state
79
74
  const [deleteId, setDeleteId] = useState<string | undefined>();
80
- const [isDeleting, setIsDeleting] = useState(false);
81
75
 
82
76
  // Complete confirmation state
83
77
  const [completeId, setCompleteId] = useState<string | undefined>();
84
- const [isCompleting, setIsCompleting] = useState(false);
85
78
 
86
- const loadData = async () => {
87
- setLoading(true);
88
- try {
89
- const [{ maintenances: maintenanceList }, { systems: systemList }] =
90
- await Promise.all([
91
- api.listMaintenances(
92
- statusFilter === "all" ? undefined : { status: statusFilter }
93
- ),
94
- catalogApi.getSystems(),
95
- ]);
96
- setMaintenances(maintenanceList);
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 maintenances with useQuery
80
+ const {
81
+ data: maintenancesData,
82
+ isLoading: maintenancesLoading,
83
+ refetch: refetchMaintenances,
84
+ } = maintenanceClient.listMaintenances.useQuery(
85
+ statusFilter === "all" ? {} : { status: statusFilter }
86
+ );
105
87
 
106
- useEffect(() => {
107
- loadData();
108
- }, [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;
109
95
 
110
96
  // Handle ?action=create URL parameter (from command palette)
111
97
  useEffect(() => {
@@ -118,6 +104,31 @@ const MaintenanceConfigPageContent: React.FC = () => {
118
104
  }
119
105
  }, [searchParams, canManage, setSearchParams]);
120
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
+
121
132
  const handleCreate = () => {
122
133
  setEditingMaintenance(undefined);
123
134
  setEditorOpen(true);
@@ -128,45 +139,19 @@ const MaintenanceConfigPageContent: 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.deleteMaintenance({ id: deleteId });
137
- toast.success("Maintenance 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 handleComplete = async () => {
147
+ const handleComplete = () => {
150
148
  if (!completeId) return;
151
-
152
- setIsCompleting(true);
153
- try {
154
- await api.closeMaintenance({ id: completeId });
155
- toast.success("Maintenance completed");
156
- loadData();
157
- } catch (error) {
158
- const message =
159
- error instanceof Error ? error.message : "Failed to complete";
160
- toast.error(message);
161
- } finally {
162
- setIsCompleting(false);
163
- setCompleteId(undefined);
164
- }
149
+ completeMutation.mutate({ id: completeId });
165
150
  };
166
151
 
167
152
  const handleSave = () => {
168
153
  setEditorOpen(false);
169
- loadData();
154
+ void refetchMaintenances();
170
155
  };
171
156
 
172
157
  const getSystemNames = (systemIds: string[]): string => {
@@ -186,7 +171,7 @@ const MaintenanceConfigPageContent: React.FC = () => {
186
171
  <PageLayout
187
172
  title="Planned Maintenances"
188
173
  subtitle="Manage scheduled maintenance windows for systems"
189
- loading={permissionLoading}
174
+ loading={accessLoading}
190
175
  allowed={canManage}
191
176
  actions={
192
177
  <Button onClick={handleCreate}>
@@ -326,7 +311,7 @@ const MaintenanceConfigPageContent: React.FC = () => {
326
311
  confirmText="Delete"
327
312
  variant="danger"
328
313
  onConfirm={handleDelete}
329
- isLoading={isDeleting}
314
+ isLoading={deleteMutation.isPending}
330
315
  />
331
316
 
332
317
  <ConfirmationModal
@@ -337,7 +322,7 @@ const MaintenanceConfigPageContent: React.FC = () => {
337
322
  confirmText="Complete"
338
323
  variant="info"
339
324
  onConfirm={handleComplete}
340
- isLoading={isCompleting}
325
+ isLoading={completeMutation.isPending}
341
326
  />
342
327
  </PageLayout>
343
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,20 +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
- permissionApiRef,
11
+ accessApiRef,
12
+ useApi,
13
13
  } from "@checkstack/frontend-api";
14
14
  import { resolveRoute } from "@checkstack/common";
15
- import { maintenanceApiRef } from "../api";
16
- import { maintenanceRoutes } from "@checkstack/maintenance-common";
17
- import type { MaintenanceDetail } from "@checkstack/maintenance-common";
15
+ import { MaintenanceApi } from "../api";
18
16
  import {
19
- catalogRoutes,
20
- CatalogApi,
21
- type System,
22
- } from "@checkstack/catalog-common";
17
+ maintenanceRoutes,
18
+ maintenanceAccess,
19
+ } from "@checkstack/maintenance-common";
20
+ import { catalogRoutes, CatalogApi } from "@checkstack/catalog-common";
23
21
  import {
24
22
  Card,
25
23
  CardHeader,
@@ -51,62 +49,55 @@ const MaintenanceDetailPageContent: React.FC = () => {
51
49
  const { maintenanceId } = useParams<{ maintenanceId: string }>();
52
50
  const navigate = useNavigate();
53
51
  const [searchParams] = useSearchParams();
54
- const api = useApi(maintenanceApiRef);
55
- const rpcApi = useApi(rpcApiRef);
56
- const permissionApi = useApi(permissionApiRef);
52
+ const maintenanceClient = usePluginClient(MaintenanceApi);
53
+ const catalogClient = usePluginClient(CatalogApi);
54
+ const accessApi = useApi(accessApiRef);
57
55
  const toast = useToast();
58
56
 
59
- const catalogApi = useMemo(() => rpcApi.forPlugin(CatalogApi), [rpcApi]);
60
-
61
- const { allowed: canManage } = permissionApi.useResourcePermission(
62
- "maintenance",
63
- "manage"
57
+ const { allowed: canManage } = accessApi.useAccess(
58
+ maintenanceAccess.maintenance.manage
64
59
  );
65
60
 
66
- const [maintenance, setMaintenance] = useState<MaintenanceDetail>();
67
- const [systems, setSystems] = useState<System[]>([]);
68
- const [loading, setLoading] = useState(true);
69
61
  const [showUpdateForm, setShowUpdateForm] = useState(false);
70
62
 
71
- const loadData = useCallback(async () => {
72
- 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
+ );
73
72
 
74
- setLoading(true);
75
- try {
76
- const [maintenanceData, { systems: systemList }] = await Promise.all([
77
- api.getMaintenance({ id: maintenanceId }),
78
- catalogApi.getSystems(),
79
- ]);
80
- setMaintenance(maintenanceData ?? undefined);
81
- setSystems(systemList);
82
- } catch (error) {
83
- console.error("Failed to load maintenance details:", error);
84
- } finally {
85
- setLoading(false);
86
- }
87
- }, [maintenanceId, api, catalogApi]);
73
+ // Fetch systems with useQuery
74
+ const { data: systemsData, isLoading: systemsLoading } =
75
+ catalogClient.getSystems.useQuery({});
88
76
 
89
- useEffect(() => {
90
- loadData();
91
- }, [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
+ });
92
92
 
93
93
  const handleUpdateSuccess = () => {
94
94
  setShowUpdateForm(false);
95
- loadData();
95
+ void refetchMaintenance();
96
96
  };
97
97
 
98
- const handleComplete = async () => {
98
+ const handleComplete = () => {
99
99
  if (!maintenanceId) return;
100
-
101
- try {
102
- await api.closeMaintenance({ id: maintenanceId });
103
- toast.success("Maintenance completed");
104
- await loadData();
105
- } catch (error) {
106
- const message =
107
- error instanceof Error ? error.message : "Failed to complete";
108
- toast.error(message);
109
- }
100
+ completeMutation.mutate({ id: maintenanceId });
110
101
  };
111
102
 
112
103
  const getSystemName = (systemId: string): string => {
@@ -180,7 +171,12 @@ const MaintenanceDetailPageContent: React.FC = () => {
180
171
  <div className="flex items-center gap-2">
181
172
  {getMaintenanceStatusBadge(maintenance.status)}
182
173
  {canComplete && (
183
- <Button variant="outline" size="sm" onClick={handleComplete}>
174
+ <Button
175
+ variant="outline"
176
+ size="sm"
177
+ onClick={handleComplete}
178
+ disabled={completeMutation.isPending}
179
+ >
184
180
  <CheckCircle2 className="h-4 w-4 mr-1" />
185
181
  Complete
186
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) {