@checkstack/infrastructure-frontend 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 +36 -0
- package/package.json +37 -0
- package/src/components/UserMenuItems.tsx +37 -0
- package/src/index.tsx +28 -0
- package/src/pages/InfrastructureConfigPage.tsx +118 -0
- package/tsconfig.json +6 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# @checkstack/infrastructure-frontend
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 8d1ef12: ## Infrastructure Configuration Shell & Cache System
|
|
8
|
+
|
|
9
|
+
### New Packages
|
|
10
|
+
|
|
11
|
+
- **`@checkstack/cache-api`**: Core cache abstractions — `CacheProvider` interface, `createScopedCache` factory for plugin key isolation, `CachePlugin`/`CacheManager` lifecycle interfaces.
|
|
12
|
+
- **`@checkstack/cache-common`**: Shared cache types, RPC contract (`getPlugins`, `getConfiguration`, `updateConfiguration`), access rules, and plugin metadata.
|
|
13
|
+
- **`@checkstack/cache-backend`**: Cache settings RPC router — exposes plugin discovery, configuration read/write endpoints with access-gated authorization.
|
|
14
|
+
- **`@checkstack/cache-frontend`**: Cache configuration tab component for the Infrastructure Settings page.
|
|
15
|
+
- **`@checkstack/infrastructure-common`**: Infrastructure tab registry, routes, and shared types for the IDE-style configuration shell.
|
|
16
|
+
- **`@checkstack/infrastructure-frontend`**: Infrastructure Settings page with vertical tab bar, per-tab access control, and user menu integration.
|
|
17
|
+
|
|
18
|
+
### Modified Packages
|
|
19
|
+
|
|
20
|
+
- **`@checkstack/backend-api`**: Added `cachePluginRegistry` and `cacheManager` to `RpcContext` and `coreServices`.
|
|
21
|
+
- **`@checkstack/backend`**: Registered cache services in boot sequence, added cache config loading, extended dependency sorter for cache plugin ordering.
|
|
22
|
+
- **`@checkstack/queue-frontend`**: Refactored from standalone `/queue/config` route to an infrastructure tab. Queue settings now live inside the Infrastructure Settings page.
|
|
23
|
+
|
|
24
|
+
### Architecture
|
|
25
|
+
|
|
26
|
+
The former monolithic Queue Config page is replaced by a pluggable Infrastructure Settings shell (`/infrastructure/config`). Plugins register configuration tabs via `registerInfrastructureTab()` with their own access rules, icons, and components. The shell evaluates per-tab access and only renders tabs the user can see.
|
|
27
|
+
|
|
28
|
+
### Patch Changes
|
|
29
|
+
|
|
30
|
+
- Updated dependencies [8d1ef12]
|
|
31
|
+
- Updated dependencies [8d1ef12]
|
|
32
|
+
- Updated dependencies [8d1ef12]
|
|
33
|
+
- @checkstack/common@0.7.0
|
|
34
|
+
- @checkstack/infrastructure-common@0.2.0
|
|
35
|
+
- @checkstack/ui@1.6.0
|
|
36
|
+
- @checkstack/frontend-api@0.3.11
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@checkstack/infrastructure-frontend",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"checkstack": {
|
|
7
|
+
"type": "frontend"
|
|
8
|
+
},
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": "./src/index.tsx"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"dev": "tsc --watch",
|
|
18
|
+
"typecheck": "tsc --noEmit",
|
|
19
|
+
"lint": "bun run lint:code",
|
|
20
|
+
"lint:code": "eslint . --max-warnings 0"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@checkstack/common": "0.6.5",
|
|
24
|
+
"@checkstack/frontend-api": "0.3.10",
|
|
25
|
+
"@checkstack/infrastructure-common": "0.1.0",
|
|
26
|
+
"@checkstack/ui": "1.5.1",
|
|
27
|
+
"lucide-react": "^0.468.0",
|
|
28
|
+
"react": "^18.3.1",
|
|
29
|
+
"react-router-dom": "^7.1.1"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/react": "^18.3.18",
|
|
33
|
+
"typescript": "^5.7.2",
|
|
34
|
+
"@checkstack/tsconfig": "0.0.5",
|
|
35
|
+
"@checkstack/scripts": "0.1.2"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Link } from "react-router-dom";
|
|
3
|
+
import { Server } from "lucide-react";
|
|
4
|
+
import type { UserMenuItemsContext } from "@checkstack/frontend-api";
|
|
5
|
+
import { DropdownMenuItem } from "@checkstack/ui";
|
|
6
|
+
import { resolveRoute } from "@checkstack/common";
|
|
7
|
+
import {
|
|
8
|
+
infrastructureRoutes,
|
|
9
|
+
getInfrastructureTabs,
|
|
10
|
+
} from "@checkstack/infrastructure-common";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* User menu item pointing to Infrastructure Settings.
|
|
14
|
+
* Only visible if the user has read access to at least one infrastructure tab.
|
|
15
|
+
*/
|
|
16
|
+
export const InfrastructureUserMenuItems = ({
|
|
17
|
+
accessRules: userPerms,
|
|
18
|
+
}: UserMenuItemsContext) => {
|
|
19
|
+
// Check if user has access to ANY infrastructure tab's read permission
|
|
20
|
+
const tabs = getInfrastructureTabs();
|
|
21
|
+
const canReadAny = tabs.some((tab) => {
|
|
22
|
+
const qualifiedId = `${tab.pluginId}.${tab.readAccess.id}`;
|
|
23
|
+
return userPerms.includes("*") || userPerms.includes(qualifiedId);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (!canReadAny) {
|
|
27
|
+
return <React.Fragment />;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<Link to={resolveRoute(infrastructureRoutes.routes.config)}>
|
|
32
|
+
<DropdownMenuItem icon={<Server className="h-4 w-4" />}>
|
|
33
|
+
Infrastructure Settings
|
|
34
|
+
</DropdownMenuItem>
|
|
35
|
+
</Link>
|
|
36
|
+
);
|
|
37
|
+
};
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {
|
|
2
|
+
UserMenuItemsSlot,
|
|
3
|
+
createSlotExtension,
|
|
4
|
+
createFrontendPlugin,
|
|
5
|
+
} from "@checkstack/frontend-api";
|
|
6
|
+
import { InfrastructureConfigPage } from "./pages/InfrastructureConfigPage";
|
|
7
|
+
import { InfrastructureUserMenuItems } from "./components/UserMenuItems";
|
|
8
|
+
import {
|
|
9
|
+
pluginMetadata,
|
|
10
|
+
infrastructureRoutes,
|
|
11
|
+
} from "@checkstack/infrastructure-common";
|
|
12
|
+
|
|
13
|
+
export const infrastructurePlugin = createFrontendPlugin({
|
|
14
|
+
metadata: pluginMetadata,
|
|
15
|
+
routes: [
|
|
16
|
+
{
|
|
17
|
+
route: infrastructureRoutes.routes.config,
|
|
18
|
+
element: <InfrastructureConfigPage />,
|
|
19
|
+
// No accessRule here — the page handles per-tab access internally
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
extensions: [
|
|
23
|
+
createSlotExtension(UserMenuItemsSlot, {
|
|
24
|
+
id: "infrastructure.user-menu.items",
|
|
25
|
+
component: InfrastructureUserMenuItems,
|
|
26
|
+
}),
|
|
27
|
+
],
|
|
28
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { useState, useMemo } from "react";
|
|
2
|
+
import {
|
|
3
|
+
wrapInSuspense,
|
|
4
|
+
accessApiRef,
|
|
5
|
+
useApi,
|
|
6
|
+
} from "@checkstack/frontend-api";
|
|
7
|
+
import { getInfrastructureTabs } from "@checkstack/infrastructure-common";
|
|
8
|
+
import {
|
|
9
|
+
PageLayout,
|
|
10
|
+
cn,
|
|
11
|
+
} from "@checkstack/ui";
|
|
12
|
+
import { Server, ShieldOff } from "lucide-react";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Infrastructure Configuration page — IDE Editor pattern.
|
|
16
|
+
*
|
|
17
|
+
* Renders a vertical tab bar on the left with content area on the right.
|
|
18
|
+
* Each tab is registered by plugins (queue, cache, etc.) with its own access rules.
|
|
19
|
+
* The page only shows tabs the user has read access to.
|
|
20
|
+
*/
|
|
21
|
+
const InfrastructureConfigPageContent = () => {
|
|
22
|
+
const accessApi = useApi(accessApiRef);
|
|
23
|
+
const allTabs = getInfrastructureTabs();
|
|
24
|
+
|
|
25
|
+
// Check access for each tab
|
|
26
|
+
const tabAccess = allTabs.map((tab) => ({
|
|
27
|
+
tab,
|
|
28
|
+
readResult: accessApi.useAccess(tab.readAccess),
|
|
29
|
+
manageResult: accessApi.useAccess(tab.manageAccess),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
// Filter to visible tabs
|
|
33
|
+
const visibleTabs = useMemo(
|
|
34
|
+
() =>
|
|
35
|
+
tabAccess.filter(({ readResult }) => readResult.allowed),
|
|
36
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
37
|
+
[tabAccess.map(({ readResult }) => readResult.allowed).join(",")],
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const isLoading = tabAccess.some(({ readResult }) => readResult.loading);
|
|
41
|
+
const hasAnyAccess = visibleTabs.length > 0;
|
|
42
|
+
|
|
43
|
+
const [activeTabId, setActiveTabId] = useState<string>();
|
|
44
|
+
|
|
45
|
+
// Default to first visible tab
|
|
46
|
+
const effectiveActiveTabId = activeTabId ?? visibleTabs[0]?.tab.id;
|
|
47
|
+
const activeTabEntry = visibleTabs.find(
|
|
48
|
+
(t) => t.tab.id === effectiveActiveTabId,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
if (!isLoading && !hasAnyAccess) {
|
|
52
|
+
return (
|
|
53
|
+
<PageLayout
|
|
54
|
+
title="Infrastructure Settings"
|
|
55
|
+
subtitle="Configure core infrastructure services"
|
|
56
|
+
icon={Server}
|
|
57
|
+
loading={false}
|
|
58
|
+
allowed={false}
|
|
59
|
+
>
|
|
60
|
+
<div className="flex flex-col items-center justify-center gap-4 py-12 text-muted-foreground">
|
|
61
|
+
<ShieldOff className="h-12 w-12" />
|
|
62
|
+
<p className="text-lg font-medium">Access Denied</p>
|
|
63
|
+
<p className="text-sm">
|
|
64
|
+
You don't have permission to view any infrastructure settings.
|
|
65
|
+
</p>
|
|
66
|
+
</div>
|
|
67
|
+
</PageLayout>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<PageLayout
|
|
73
|
+
title="Infrastructure Settings"
|
|
74
|
+
subtitle="Configure core infrastructure services"
|
|
75
|
+
icon={Server}
|
|
76
|
+
loading={isLoading}
|
|
77
|
+
allowed={hasAnyAccess}
|
|
78
|
+
>
|
|
79
|
+
<div className="flex gap-6 min-h-[600px]">
|
|
80
|
+
{/* Tab Bar */}
|
|
81
|
+
<nav className="flex flex-col gap-1 w-52 shrink-0 border-r border-border pr-4">
|
|
82
|
+
{visibleTabs.map(({ tab }) => {
|
|
83
|
+
const Icon = tab.icon;
|
|
84
|
+
const isActive = tab.id === effectiveActiveTabId;
|
|
85
|
+
return (
|
|
86
|
+
<button
|
|
87
|
+
key={tab.id}
|
|
88
|
+
onClick={() => setActiveTabId(tab.id)}
|
|
89
|
+
className={cn(
|
|
90
|
+
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors text-left",
|
|
91
|
+
isActive
|
|
92
|
+
? "bg-accent text-accent-foreground"
|
|
93
|
+
: "text-muted-foreground hover:bg-accent/50 hover:text-accent-foreground",
|
|
94
|
+
)}
|
|
95
|
+
>
|
|
96
|
+
<Icon className="h-4 w-4 shrink-0" />
|
|
97
|
+
{tab.label}
|
|
98
|
+
</button>
|
|
99
|
+
);
|
|
100
|
+
})}
|
|
101
|
+
</nav>
|
|
102
|
+
|
|
103
|
+
{/* Tab Content */}
|
|
104
|
+
<div className="flex-1 min-w-0">
|
|
105
|
+
{activeTabEntry && (
|
|
106
|
+
<activeTabEntry.tab.component
|
|
107
|
+
canUpdate={activeTabEntry.manageResult.allowed}
|
|
108
|
+
/>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
</PageLayout>
|
|
113
|
+
);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export const InfrastructureConfigPage = wrapInSuspense(
|
|
117
|
+
InfrastructureConfigPageContent,
|
|
118
|
+
);
|