@checkstack/queue-frontend 0.0.5 → 0.2.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,164 @@
1
1
  # @checkstack/queue-frontend
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 180be38: # Queue Lag Warning
8
+
9
+ Added a queue lag warning system that displays alerts when pending jobs exceed configurable thresholds.
10
+
11
+ ## Features
12
+
13
+ - **Backend Stats API**: New `getStats`, `getLagStatus`, and `updateLagThresholds` RPC endpoints
14
+ - **Signal-based Updates**: `QUEUE_LAG_CHANGED` signal for real-time frontend updates
15
+ - **Aggregated Stats**: `QueueManager.getAggregatedStats()` sums stats across all queues
16
+ - **Configurable Thresholds**: Warning (default 100) and Critical (default 500) thresholds stored in config
17
+ - **Dashboard Integration**: Queue lag alert displayed on main Dashboard (access-gated)
18
+ - **Queue Settings Page**: Lag alert and Performance Tuning guidance card with concurrency tips
19
+
20
+ ## UI Changes
21
+
22
+ - Queue lag alert banner appears on Dashboard and Queue Settings when pending jobs exceed thresholds
23
+ - New "Performance Tuning" card with concurrency settings guidance and bottleneck indicators
24
+
25
+ - 7a23261: ## TanStack Query Integration
26
+
27
+ Migrated all frontend components to use `usePluginClient` hook with TanStack Query integration, replacing the legacy `forPlugin()` pattern.
28
+
29
+ ### New Features
30
+
31
+ - **`usePluginClient` hook**: Provides type-safe access to plugin APIs with `.useQuery()` and `.useMutation()` methods
32
+ - **Automatic request deduplication**: Multiple components requesting the same data share a single network request
33
+ - **Built-in caching**: Configurable stale time and cache duration per query
34
+ - **Loading/error states**: TanStack Query provides `isLoading`, `error`, `isRefetching` states automatically
35
+ - **Background refetching**: Stale data is automatically refreshed when components mount
36
+
37
+ ### Contract Changes
38
+
39
+ All RPC contracts now require `operationType: "query"` or `operationType: "mutation"` metadata:
40
+
41
+ ```typescript
42
+ const getItems = proc()
43
+ .meta({ operationType: "query", access: [access.read] })
44
+ .output(z.array(itemSchema))
45
+ .query();
46
+
47
+ const createItem = proc()
48
+ .meta({ operationType: "mutation", access: [access.manage] })
49
+ .input(createItemSchema)
50
+ .output(itemSchema)
51
+ .mutation();
52
+ ```
53
+
54
+ ### Migration
55
+
56
+ ```typescript
57
+ // Before (forPlugin pattern)
58
+ const api = useApi(myPluginApiRef);
59
+ const [items, setItems] = useState<Item[]>([]);
60
+ useEffect(() => {
61
+ api.getItems().then(setItems);
62
+ }, [api]);
63
+
64
+ // After (usePluginClient pattern)
65
+ const client = usePluginClient(MyPluginApi);
66
+ const { data: items, isLoading } = client.getItems.useQuery({});
67
+ ```
68
+
69
+ ### Bug Fixes
70
+
71
+ - Fixed `rpc.test.ts` test setup for middleware type inference
72
+ - Fixed `SearchDialog` to use `setQuery` instead of deprecated `search` method
73
+ - Fixed null→undefined warnings in notification and queue frontends
74
+
75
+ ### Patch Changes
76
+
77
+ - Updated dependencies [180be38]
78
+ - Updated dependencies [7a23261]
79
+ - @checkstack/queue-common@0.2.0
80
+ - @checkstack/frontend-api@0.2.0
81
+ - @checkstack/common@0.3.0
82
+ - @checkstack/ui@0.2.1
83
+ - @checkstack/signal-frontend@0.0.7
84
+
85
+ ## 0.1.0
86
+
87
+ ### Minor Changes
88
+
89
+ - 9faec1f: # Unified AccessRule Terminology Refactoring
90
+
91
+ This release completes a comprehensive terminology refactoring from "permission" to "accessRule" across the entire codebase, establishing a consistent and modern access control vocabulary.
92
+
93
+ ## Changes
94
+
95
+ ### Core Infrastructure (`@checkstack/common`)
96
+
97
+ - Introduced `AccessRule` interface as the primary access control type
98
+ - Added `accessPair()` helper for creating read/manage access rule pairs
99
+ - Added `access()` builder for individual access rules
100
+ - Replaced `Permission` type with `AccessRule` throughout
101
+
102
+ ### API Changes
103
+
104
+ - `env.registerPermissions()` → `env.registerAccessRules()`
105
+ - `meta.permissions` → `meta.access` in RPC contracts
106
+ - `usePermission()` → `useAccess()` in frontend hooks
107
+ - Route `permission:` field → `accessRule:` field
108
+
109
+ ### UI Changes
110
+
111
+ - "Roles & Permissions" tab → "Roles & Access Rules"
112
+ - "You don't have permission..." → "You don't have access..."
113
+ - All permission-related UI text updated
114
+
115
+ ### Documentation & Templates
116
+
117
+ - Updated 18 documentation files with AccessRule terminology
118
+ - Updated 7 scaffolding templates with `accessPair()` pattern
119
+ - All code examples use new AccessRule API
120
+
121
+ ## Migration Guide
122
+
123
+ ### Backend Plugins
124
+
125
+ ```diff
126
+ - import { permissionList } from "./permissions";
127
+ - env.registerPermissions(permissionList);
128
+ + import { accessRules } from "./access";
129
+ + env.registerAccessRules(accessRules);
130
+ ```
131
+
132
+ ### RPC Contracts
133
+
134
+ ```diff
135
+ - .meta({ userType: "user", permissions: [permissions.read.id] })
136
+ + .meta({ userType: "user", access: [access.read] })
137
+ ```
138
+
139
+ ### Frontend Hooks
140
+
141
+ ```diff
142
+ - const canRead = accessApi.usePermission(permissions.read.id);
143
+ + const canRead = accessApi.useAccess(access.read);
144
+ ```
145
+
146
+ ### Routes
147
+
148
+ ```diff
149
+ - permission: permissions.entityRead.id,
150
+ + accessRule: access.read,
151
+ ```
152
+
153
+ ### Patch Changes
154
+
155
+ - Updated dependencies [9faec1f]
156
+ - Updated dependencies [f533141]
157
+ - @checkstack/common@0.2.0
158
+ - @checkstack/frontend-api@0.1.0
159
+ - @checkstack/queue-common@0.1.0
160
+ - @checkstack/ui@0.2.0
161
+
3
162
  ## 0.0.5
4
163
 
5
164
  ### Patch Changes
package/package.json CHANGED
@@ -1,9 +1,14 @@
1
1
  {
2
2
  "name": "@checkstack/queue-frontend",
3
- "version": "0.0.5",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./src/index.tsx"
10
+ }
11
+ },
7
12
  "scripts": {
8
13
  "build": "tsc",
9
14
  "dev": "tsc --watch",
@@ -15,6 +20,7 @@
15
20
  "@checkstack/common": "workspace:*",
16
21
  "@checkstack/frontend-api": "workspace:*",
17
22
  "@checkstack/queue-common": "workspace:*",
23
+ "@checkstack/signal-frontend": "workspace:*",
18
24
  "@checkstack/ui": "workspace:*",
19
25
  "ajv": "^8.17.1",
20
26
  "ajv-formats": "^3.0.1",
package/src/api.ts CHANGED
@@ -1,15 +1,8 @@
1
- import { createApiRef } from "@checkstack/frontend-api";
2
- import { QueueApi } from "@checkstack/queue-common";
3
- import type { InferClient } from "@checkstack/common";
4
-
5
1
  // Re-export types for convenience
6
2
  export type {
7
3
  QueuePluginDto,
8
4
  QueueConfigurationDto,
9
5
  UpdateQueueConfiguration,
10
6
  } from "@checkstack/queue-common";
11
-
12
- // QueueApiClient type inferred from the client definition
13
- export type QueueApiClient = InferClient<typeof QueueApi>;
14
-
15
- export const queueApiRef = createApiRef<QueueApiClient>("queue-api");
7
+ // Client definition is in @checkstack/queue-common - use with usePluginClient
8
+ export { QueueApi } from "@checkstack/queue-common";
@@ -0,0 +1,95 @@
1
+ import React from "react";
2
+ import {
3
+ useApi,
4
+ accessApiRef,
5
+ usePluginClient,
6
+ } from "@checkstack/frontend-api";
7
+ import { useSignal } from "@checkstack/signal-frontend";
8
+ import {
9
+ QueueApi,
10
+ QUEUE_LAG_CHANGED,
11
+ queueAccess,
12
+ type LagSeverity,
13
+ } from "@checkstack/queue-common";
14
+ import { Alert, AlertTitle, AlertDescription } from "@checkstack/ui";
15
+ import { AlertTriangle, AlertCircle } from "lucide-react";
16
+
17
+ interface QueueLagAlertProps {
18
+ /** Only show if user has queue settings access */
19
+ requireAccess?: boolean;
20
+ }
21
+
22
+ /**
23
+ * Displays a warning alert when queue is lagging (high pending jobs).
24
+ * Uses signal for real-time updates + initial fetch via useQuery.
25
+ */
26
+ export const QueueLagAlert: React.FC<QueueLagAlertProps> = ({
27
+ requireAccess = true,
28
+ }) => {
29
+ const accessApi = useApi(accessApiRef);
30
+ const queueClient = usePluginClient(QueueApi);
31
+
32
+ // Check access if required
33
+ const { allowed, loading: accessLoading } = accessApi.useAccess(
34
+ queueAccess.settings.read
35
+ );
36
+
37
+ // Fetch lag status via useQuery
38
+ const { data: lagStatus, isLoading } = queueClient.getLagStatus.useQuery(
39
+ undefined,
40
+ {
41
+ enabled: !requireAccess || allowed,
42
+ staleTime: 30_000, // Cache for 30 seconds
43
+ }
44
+ );
45
+
46
+ // State for real-time updates via signal
47
+ const [signalData, setSignalData] = React.useState<
48
+ | {
49
+ pending: number;
50
+ severity: LagSeverity;
51
+ }
52
+ | undefined
53
+ >();
54
+
55
+ // Listen for real-time lag updates
56
+ useSignal(QUEUE_LAG_CHANGED, (payload) => {
57
+ setSignalData({ pending: payload.pending, severity: payload.severity });
58
+ });
59
+
60
+ // Use signal data if available, otherwise use query data
61
+ const pending = signalData?.pending ?? lagStatus?.pending ?? 0;
62
+ const severity = signalData?.severity ?? lagStatus?.severity ?? "none";
63
+
64
+ // Don't render if loading, no access, or no lag
65
+ if (isLoading || accessLoading) {
66
+ return;
67
+ }
68
+
69
+ if (requireAccess && !allowed) {
70
+ return;
71
+ }
72
+
73
+ if (severity === "none") {
74
+ return;
75
+ }
76
+
77
+ const variant = severity === "critical" ? "error" : "warning";
78
+ const Icon = severity === "critical" ? AlertCircle : AlertTriangle;
79
+ const title =
80
+ severity === "critical" ? "Queue backlog critical" : "Queue building up";
81
+ const description =
82
+ severity === "critical"
83
+ ? `${pending} jobs pending. Consider scaling or reducing load.`
84
+ : `${pending} jobs pending. Some jobs may be delayed.`;
85
+
86
+ return (
87
+ <Alert variant={variant} className="mb-4">
88
+ <Icon className="h-5 w-5" />
89
+ <div>
90
+ <AlertTitle>{title}</AlertTitle>
91
+ <AlertDescription>{description}</AlertDescription>
92
+ </div>
93
+ </Alert>
94
+ );
95
+ };
@@ -3,20 +3,17 @@ import { Link } from "react-router-dom";
3
3
  import { ListOrdered } 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
  queueRoutes,
9
- permissions,
9
+ queueAccess,
10
10
  pluginMetadata,
11
11
  } from "@checkstack/queue-common";
12
12
 
13
13
  export const QueueUserMenuItems = ({
14
- permissions: userPerms,
14
+ accessRules: userPerms,
15
15
  }: UserMenuItemsContext) => {
16
- const qualifiedId = qualifyPermissionId(
17
- pluginMetadata,
18
- permissions.queueRead
19
- );
16
+ const qualifiedId = `${pluginMetadata.pluginId}.${queueAccess.settings.read.id}`;
20
17
  const canRead = userPerms.includes("*") || userPerms.includes(qualifiedId);
21
18
 
22
19
  if (!canRead) {
package/src/index.tsx CHANGED
@@ -1,36 +1,23 @@
1
1
  import {
2
- rpcApiRef,
3
- ApiRef,
4
2
  UserMenuItemsSlot,
5
3
  createSlotExtension,
6
4
  createFrontendPlugin,
7
5
  } from "@checkstack/frontend-api";
8
- import { queueApiRef, type QueueApiClient } from "./api";
9
6
  import { QueueConfigPage } from "./pages/QueueConfigPage";
10
7
  import { QueueUserMenuItems } from "./components/UserMenuItems";
11
8
  import {
12
9
  queueRoutes,
13
- QueueApi,
14
10
  pluginMetadata,
15
- permissions,
11
+ queueAccess,
16
12
  } from "@checkstack/queue-common";
17
13
 
18
14
  export const queuePlugin = createFrontendPlugin({
19
15
  metadata: pluginMetadata,
20
- apis: [
21
- {
22
- ref: queueApiRef,
23
- factory: (deps: { get: <T>(ref: ApiRef<T>) => T }): QueueApiClient => {
24
- const rpcApi = deps.get(rpcApiRef);
25
- return rpcApi.forPlugin(QueueApi);
26
- },
27
- },
28
- ],
29
16
  routes: [
30
17
  {
31
18
  route: queueRoutes.routes.config,
32
19
  element: <QueueConfigPage />,
33
- permission: permissions.queueRead,
20
+ accessRule: queueAccess.settings.read,
34
21
  },
35
22
  ],
36
23
  extensions: [
@@ -42,3 +29,4 @@ export const queuePlugin = createFrontendPlugin({
42
29
  });
43
30
 
44
31
  export * from "./api";
32
+ export { QueueLagAlert } from "./components/QueueLagAlert";
@@ -1,11 +1,15 @@
1
1
  import { useEffect, useState } from "react";
2
2
  import {
3
- useApi,
4
3
  wrapInSuspense,
5
- permissionApiRef,
4
+ accessApiRef,
5
+ useApi,
6
+ usePluginClient,
6
7
  } from "@checkstack/frontend-api";
7
- import { queueApiRef } from "../api";
8
- import { QueuePluginDto } from "@checkstack/queue-common";
8
+ import {
9
+ QueuePluginDto,
10
+ queueAccess,
11
+ QueueApi,
12
+ } from "@checkstack/queue-common";
9
13
  import {
10
14
  Button,
11
15
  Alert,
@@ -20,105 +24,173 @@ import {
20
24
  CardTitle,
21
25
  useToast,
22
26
  } from "@checkstack/ui";
23
- import { AlertTriangle, Save } from "lucide-react";
27
+ import { AlertTriangle, Save, Info, Gauge, Activity } from "lucide-react";
28
+ import { QueueLagAlert } from "../components/QueueLagAlert";
24
29
 
25
30
  const QueueConfigPageContent = () => {
26
- const api = useApi(queueApiRef);
27
- const permissionApi = useApi(permissionApiRef);
31
+ const queueClient = usePluginClient(QueueApi);
32
+ const accessApi = useApi(accessApiRef);
28
33
  const toast = useToast();
29
- const { allowed: canRead, loading: permissionLoading } =
30
- permissionApi.useResourcePermission("queue", "read");
31
- const { allowed: canUpdate } = permissionApi.useManagePermission("queue");
34
+ const { allowed: canRead, loading: accessLoading } = accessApi.useAccess(
35
+ queueAccess.settings.read
36
+ );
37
+ const { allowed: canUpdate } = accessApi.useAccess(
38
+ queueAccess.settings.manage
39
+ );
40
+
41
+ // Fetch plugins and configuration
42
+ const { data: pluginsList } = queueClient.getPlugins.useQuery();
43
+ const { data: configuration, refetch: refetchConfig } =
44
+ queueClient.getConfiguration.useQuery();
45
+ const updateConfigMutation = queueClient.updateConfiguration.useMutation();
32
46
 
33
- const [plugins, setPlugins] = useState<QueuePluginDto[]>([]);
34
47
  const [selectedPluginId, setSelectedPluginId] = useState<string>("");
35
48
  const [config, setConfig] = useState<Record<string, unknown>>({});
36
- const [isSaving, setIsSaving] = useState(false);
37
49
 
50
+ // Sync state with fetched data
38
51
  useEffect(() => {
39
- const fetchData = async () => {
40
- const [pluginsList, configuration] = await Promise.all([
41
- api.getPlugins(),
42
- api.getConfiguration(),
43
- ]);
44
- setPlugins(pluginsList);
52
+ if (configuration) {
45
53
  setSelectedPluginId(configuration.pluginId);
46
54
  setConfig(configuration.config);
47
- };
48
- fetchData();
49
- }, [api]);
55
+ }
56
+ }, [configuration]);
50
57
 
51
58
  const handleSave = async () => {
52
59
  if (!selectedPluginId) return;
53
- setIsSaving(true);
54
60
  try {
55
- await api.updateConfiguration({
61
+ await updateConfigMutation.mutateAsync({
56
62
  pluginId: selectedPluginId,
57
63
  config,
58
64
  });
59
65
  toast.success("Configuration saved successfully!");
66
+ refetchConfig();
60
67
  } catch (error) {
61
68
  const message = error instanceof Error ? error.message : String(error);
62
69
  toast.error(`Failed to save configuration: ${message}`);
63
- } finally {
64
- setIsSaving(false);
65
70
  }
66
71
  };
67
72
 
68
73
  const isMemoryQueue = selectedPluginId === "memory";
74
+ const plugins: QueuePluginDto[] = pluginsList ?? [];
75
+ const isSaving = updateConfigMutation.isPending;
69
76
 
70
77
  return (
71
78
  <PageLayout
72
79
  title="Queue Settings"
73
80
  subtitle="Configure the queue system for background jobs"
74
- loading={permissionLoading}
81
+ loading={accessLoading}
75
82
  allowed={canRead}
76
83
  maxWidth="3xl"
77
84
  >
78
- <Card>
79
- <CardHeader>
80
- <CardTitle>Queue Configuration</CardTitle>
81
- <p className="text-sm text-muted-foreground">
82
- Select and configure the queue plugin
83
- </p>
84
- </CardHeader>
85
- <CardContent className="space-y-6">
86
- {isMemoryQueue && (
87
- <Alert variant="warning">
88
- <AlertTriangle className="h-5 w-5" />
89
- <div>
90
- <AlertTitle>In-Memory Queue Warning</AlertTitle>
91
- <AlertDescription>
92
- The in-memory queue is suitable for development and
93
- single-instance deployments only. It will not scale across
94
- multiple instances and jobs will be lost on restart. For
95
- production environments with multiple instances, consider
96
- using a persistent queue implementation.
97
- </AlertDescription>
85
+ <QueueLagAlert requireAccess={false} />
86
+ <div className="space-y-6">
87
+ <Card>
88
+ <CardHeader>
89
+ <CardTitle>Queue Configuration</CardTitle>
90
+ <p className="text-sm text-muted-foreground">
91
+ Select and configure the queue plugin
92
+ </p>
93
+ </CardHeader>
94
+ <CardContent className="space-y-6">
95
+ {isMemoryQueue && (
96
+ <Alert variant="warning">
97
+ <AlertTriangle className="h-5 w-5" />
98
+ <div>
99
+ <AlertTitle>In-Memory Queue Warning</AlertTitle>
100
+ <AlertDescription>
101
+ The in-memory queue is suitable for development and
102
+ single-instance deployments only. It will not scale across
103
+ multiple instances and jobs will be lost on restart. For
104
+ production environments with multiple instances, consider
105
+ using a persistent queue implementation.
106
+ </AlertDescription>
107
+ </div>
108
+ </Alert>
109
+ )}
110
+
111
+ <PluginConfigForm
112
+ label="Queue Plugin"
113
+ plugins={plugins}
114
+ selectedPluginId={selectedPluginId}
115
+ onPluginChange={(value) => {
116
+ setSelectedPluginId(value);
117
+ setConfig({});
118
+ }}
119
+ config={config}
120
+ onConfigChange={setConfig}
121
+ disabled={!canUpdate}
122
+ />
123
+ </CardContent>
124
+ <CardFooter className="flex justify-end gap-2">
125
+ <Button onClick={handleSave} disabled={!canUpdate || isSaving}>
126
+ <Save className="mr-2 h-4 w-4" />
127
+ {isSaving ? "Saving..." : "Save Configuration"}
128
+ </Button>
129
+ </CardFooter>
130
+ </Card>
131
+
132
+ {/* Performance Guidance */}
133
+ <Card>
134
+ <CardHeader>
135
+ <CardTitle className="flex items-center gap-2">
136
+ <Info className="h-5 w-5" />
137
+ Performance Tuning
138
+ </CardTitle>
139
+ </CardHeader>
140
+ <CardContent className="space-y-4">
141
+ <div className="grid gap-4 md:grid-cols-2">
142
+ <div className="space-y-2">
143
+ <h4 className="flex items-center gap-2 font-medium">
144
+ <Gauge className="h-4 w-4" />
145
+ Concurrency Settings
146
+ </h4>
147
+ <ul className="text-sm text-muted-foreground space-y-1 list-disc list-inside">
148
+ <li>
149
+ <strong>Default (10)</strong>: Conservative, safe for most
150
+ workloads
151
+ </li>
152
+ <li>
153
+ <strong>Moderate (25-50)</strong>: Good for I/O-bound health
154
+ checks
155
+ </li>
156
+ <li>
157
+ <strong>Aggressive (100)</strong>: Maximum, monitor resource
158
+ usage
159
+ </li>
160
+ </ul>
161
+ <p className="text-xs text-muted-foreground mt-2">
162
+ Formula: throughput ≈ concurrency / avg_job_duration
163
+ </p>
98
164
  </div>
99
- </Alert>
100
- )}
101
165
 
102
- <PluginConfigForm
103
- label="Queue Plugin"
104
- plugins={plugins}
105
- selectedPluginId={selectedPluginId}
106
- onPluginChange={(value) => {
107
- setSelectedPluginId(value);
108
- setConfig({});
109
- }}
110
- config={config}
111
- onConfigChange={setConfig}
112
- disabled={!canUpdate}
113
- />
114
- </CardContent>
115
- <CardFooter className="flex justify-end gap-2">
116
- <Button onClick={handleSave} disabled={!canUpdate || isSaving}>
117
- <Save className="mr-2 h-4 w-4" />
118
- {isSaving ? "Saving..." : "Save Configuration"}
119
- </Button>
120
- </CardFooter>
121
- </Card>
166
+ <div className="space-y-2">
167
+ <h4 className="flex items-center gap-2 font-medium">
168
+ <Activity className="h-4 w-4" />
169
+ Bottleneck Indicators
170
+ </h4>
171
+ <ul className="text-sm text-muted-foreground space-y-1 list-disc list-inside">
172
+ <li>
173
+ <strong>Jobs queueing</strong>: Increase concurrency or
174
+ scale horizontally
175
+ </li>
176
+ <li>
177
+ <strong>High CPU (&gt;70%)</strong>: Scale horizontally,
178
+ don't increase concurrency
179
+ </li>
180
+ <li>
181
+ <strong>DB connection errors</strong>: Reduce concurrency or
182
+ increase pool
183
+ </li>
184
+ <li>
185
+ <strong>Rate limit errors</strong>: Reduce concurrency for
186
+ external checks
187
+ </li>
188
+ </ul>
189
+ </div>
190
+ </div>
191
+ </CardContent>
192
+ </Card>
193
+ </div>
122
194
  </PageLayout>
123
195
  );
124
196
  };