create-lego-one 2.0.12 → 2.0.14
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/dist/index.cjs +150 -15
- package/dist/index.cjs.map +1 -1
- package/package.json +1 -1
- package/template/.cursor/rules/rules.mdc +639 -0
- package/template/.dockerignore +58 -0
- package/template/.env.example +18 -0
- package/template/.eslintignore +5 -0
- package/template/.eslintrc.js +28 -0
- package/template/.prettierignore +6 -0
- package/template/.prettierrc +11 -0
- package/template/CLAUDE.md +634 -0
- package/template/Dockerfile +67 -0
- package/template/PROMPT.md +457 -0
- package/template/README.md +325 -0
- package/template/docker-compose.yml +48 -0
- package/template/docker-entrypoint.sh +23 -0
- package/template/docs/checkpoints/.template.md +64 -0
- package/template/docs/checkpoints/framework/01-infrastructure-setup.md +132 -0
- package/template/docs/checkpoints/framework/02-pocketbase-setup.md +155 -0
- package/template/docs/checkpoints/framework/03-host-kernel.md +170 -0
- package/template/docs/checkpoints/framework/04-auth-system.md +163 -0
- package/template/docs/checkpoints/framework/phase-05-multitenancy-rbac.md +223 -0
- package/template/docs/checkpoints/framework/phase-06-ui-components.md +260 -0
- package/template/docs/checkpoints/framework/phase-07-communication-system.md +276 -0
- package/template/docs/checkpoints/framework/phase-08-plugin-system.md +91 -0
- package/template/docs/checkpoints/framework/phase-09-dashboard-plugin.md +111 -0
- package/template/docs/checkpoints/framework/phase-10-todo-plugin.md +169 -0
- package/template/docs/checkpoints/framework/phase-11-testing.md +264 -0
- package/template/docs/checkpoints/framework/phase-12-deployment.md +294 -0
- package/template/docs/checkpoints/framework/phase-13-documentation.md +312 -0
- package/template/docs/framework/plans/00-index.md +164 -0
- package/template/docs/framework/plans/01-infrastructure-setup.md +855 -0
- package/template/docs/framework/plans/02-pocketbase-setup.md +1374 -0
- package/template/docs/framework/plans/03-host-kernel.md +1518 -0
- package/template/docs/framework/plans/04-auth-system.md +1466 -0
- package/template/docs/framework/plans/05-multitenancy-rbac.md +1527 -0
- package/template/docs/framework/plans/06-ui-components.md +1478 -0
- package/template/docs/framework/plans/07-communication-system.md +1106 -0
- package/template/docs/framework/plans/08-plugin-system.md +1179 -0
- package/template/docs/framework/plans/09-dashboard-plugin.md +1137 -0
- package/template/docs/framework/plans/10-todo-plugin.md +1343 -0
- package/template/docs/framework/plans/11-testing.md +935 -0
- package/template/docs/framework/plans/12-deployment.md +896 -0
- package/template/docs/framework/prompts/0-boilerplate-modernjs.md +151 -0
- package/template/docs/framework/research/00-modernjs-audit.md +488 -0
- package/template/docs/framework/research/01-system-blueprint.md +721 -0
- package/template/docs/framework/research/02-data-migration-protocol.md +699 -0
- package/template/docs/framework/research/03-host-setup.md +714 -0
- package/template/docs/framework/research/04-plugin-architecture.md +645 -0
- package/template/docs/framework/research/05-slot-injection-pattern.md +671 -0
- package/template/docs/framework/research/06-cli-strategy.md +615 -0
- package/template/docs/framework/research/07-deployment.md +629 -0
- package/template/docs/framework/research/README.md +282 -0
- package/template/docs/framework/setup/00-index.md +210 -0
- package/template/docs/framework/setup/01-framework-structure.md +308 -0
- package/template/docs/framework/setup/02-development-workflow.md +405 -0
- package/template/docs/framework/setup/03-environment-setup.md +215 -0
- package/template/docs/framework/setup/04-kernel-architecture.md +499 -0
- package/template/docs/framework/setup/05-plugin-system.md +620 -0
- package/template/docs/framework/setup/06-communication-patterns.md +451 -0
- package/template/docs/framework/setup/07-plugin-development.md +582 -0
- package/template/docs/framework/setup/08-component-library.md +658 -0
- package/template/docs/framework/setup/09-data-integration.md +609 -0
- package/template/docs/framework/setup/10-auth-rbac.md +497 -0
- package/template/docs/framework/setup/11-hooks-api.md +393 -0
- package/template/docs/framework/setup/12-components-api.md +665 -0
- package/template/docs/framework/setup/13-deployment-guide.md +566 -0
- package/template/docs/framework/setup/README.md +548 -0
- package/template/host/package.json +1 -1
- package/template/nginx.conf +72 -0
- package/template/package.json +1 -1
- package/template/packages/plugins/@lego/plugin-dashboard/package.json +1 -1
- package/template/packages/plugins/@lego/plugin-todo/package.json +1 -1
- package/template/pocketbase/CHANGELOG.md +911 -0
- package/template/pocketbase/LICENSE.md +17 -0
- package/template/scripts/create-plugin.js +221 -0
- package/template/scripts/deploy.sh +56 -0
- package/template/tsconfig.base.json +26 -0
|
@@ -0,0 +1,1137 @@
|
|
|
1
|
+
# Dashboard Plugin Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For AI Implementing This Plan:** This is document 09 of 13. Complete documents 01-08 first.
|
|
4
|
+
|
|
5
|
+
**Goal:** Implement the Dashboard plugin with organization stats, recent activity, quick actions, and slot injection examples demonstrating the plugin system.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Dashboard plugin is a Modern.js app registered as a Garfish sub-app. It fetches stats from PocketBase, displays widgets, and demonstrates slot injection by adding items to host layout.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Modern.js, React, TypeScript, TanStack Query, PocketBase, Tailwind CSS
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Prerequisites
|
|
14
|
+
|
|
15
|
+
- ✅ Completed `01-infrastructure-setup.md`
|
|
16
|
+
- ✅ Completed `02-pocketbase-setup.md` (audit_logs collection)
|
|
17
|
+
- ✅ Completed `03-host-kernel.md` through `08-plugin-system.md`
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Task 1: Create Dashboard Plugin Structure
|
|
22
|
+
|
|
23
|
+
**Files:**
|
|
24
|
+
- Create: `packages/plugins/@lego/plugin-dashboard/package.json`
|
|
25
|
+
- Create: `packages/plugins/@lego/plugin-dashboard/tsconfig.json`
|
|
26
|
+
- Create: `packages/plugins/@lego/plugin-dashboard/modern.config.ts`
|
|
27
|
+
- Create: `packages/plugins/@lego/plugin-dashboard/.gitignore`
|
|
28
|
+
|
|
29
|
+
### Step 1: Create package.json
|
|
30
|
+
|
|
31
|
+
**File:** `packages/plugins/@lego/plugin-dashboard/package.json`
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"name": "@lego/plugin-dashboard",
|
|
36
|
+
"version": "1.0.0",
|
|
37
|
+
"private": true,
|
|
38
|
+
"type": "module",
|
|
39
|
+
"scripts": {
|
|
40
|
+
"dev": "modern dev",
|
|
41
|
+
"build": "modern build",
|
|
42
|
+
"start": "modern start",
|
|
43
|
+
"lint": "eslint src --ext .ts,.tsx",
|
|
44
|
+
"typecheck": "tsc --noEmit"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@garfish/hooks": "^1.22.0",
|
|
48
|
+
"@garfish/router": "^1.22.0",
|
|
49
|
+
"@garfish/react-scope": "^1.22.0",
|
|
50
|
+
"@modern-js/runtime": "^2.60.0",
|
|
51
|
+
"@modern-js/runtime/garfish": "^2.60.0",
|
|
52
|
+
"@radix-ui/react-slot": "^1.1.0",
|
|
53
|
+
"@tanstack/react-query": "^5.59.0",
|
|
54
|
+
"clsx": "^2.1.1",
|
|
55
|
+
"lucide-react": "^0.454.0",
|
|
56
|
+
"pocketbase": "^0.21.5",
|
|
57
|
+
"react": "^18.3.1",
|
|
58
|
+
"react-dom": "^18.3.1",
|
|
59
|
+
"tailwind-merge": "^2.5.4"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@modern-js/app-tools": "^2.60.0",
|
|
63
|
+
"@modern-js/plugin-garfish": "^2.60.0",
|
|
64
|
+
"@types/react": "^18.3.12",
|
|
65
|
+
"@types/react-dom": "^18.3.1",
|
|
66
|
+
"autoprefixer": "^10.4.20",
|
|
67
|
+
"postcss": "^8.4.49",
|
|
68
|
+
"tailwindcss": "^3.4.15",
|
|
69
|
+
"typescript": "^5.6.3"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Step 2: Create tsconfig.json
|
|
75
|
+
|
|
76
|
+
**File:** `packages/plugins/@lego/plugin-dashboard/tsconfig.json`
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"extends": "@modern-js/tsconfig/base.json",
|
|
81
|
+
"compilerOptions": {
|
|
82
|
+
"jsx": "react-jsx",
|
|
83
|
+
"strict": true,
|
|
84
|
+
"esModuleInterop": true,
|
|
85
|
+
"skipLibCheck": true,
|
|
86
|
+
"moduleResolution": "bundler",
|
|
87
|
+
"resolveJsonModule": true,
|
|
88
|
+
"isolatedModules": true,
|
|
89
|
+
"paths": {
|
|
90
|
+
"@/*": ["./src/*"]
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
"include": ["src"],
|
|
94
|
+
"exclude": ["node_modules", "dist"]
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Step 3: Create modern.config.ts
|
|
99
|
+
|
|
100
|
+
**File:** `packages/plugins/@lego/plugin-dashboard/modern.config.ts`
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
import { appTools, defineConfig } from '@modern-js/app-tools';
|
|
104
|
+
import { garfishPlugin } from '@modern-js/plugin-garfish';
|
|
105
|
+
|
|
106
|
+
export default defineConfig({
|
|
107
|
+
dev: {
|
|
108
|
+
port: 3001,
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
runtime: {
|
|
112
|
+
router: true,
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
// Mark as micro-frontend sub-app
|
|
116
|
+
deploy: {
|
|
117
|
+
microFrontend: true,
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
plugins: [appTools(), garfishPlugin()],
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Step 4: Create .gitignore
|
|
125
|
+
|
|
126
|
+
**File:** `packages/plugins/@lego/plugin-dashboard/.gitignore`
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
node_modules/
|
|
130
|
+
dist/
|
|
131
|
+
.modern/
|
|
132
|
+
*.local
|
|
133
|
+
.DS_Store
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Step 5: Install dependencies
|
|
137
|
+
|
|
138
|
+
**Run:** From root directory
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
pnpm install
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Task 2: Create Plugin Configuration
|
|
147
|
+
|
|
148
|
+
**Files:**
|
|
149
|
+
- Create: `packages/plugins/@lego/plugin-dashboard/src/plugin.config.ts`
|
|
150
|
+
- Create: `packages/plugins/@lego/plugin-dashboard/src/plugin.ts`
|
|
151
|
+
|
|
152
|
+
### Step 1: Create plugin config
|
|
153
|
+
|
|
154
|
+
**File:** `packages/plugins/@lego/plugin-dashboard/src/plugin.config.ts`
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
import type { PluginConfig } from '@lego/kernel/plugins';
|
|
158
|
+
|
|
159
|
+
import { SidebarWidget } from './components/SidebarWidget';
|
|
160
|
+
import { QuickActionSlot } from './components/QuickActionSlot';
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Dashboard plugin configuration
|
|
164
|
+
*
|
|
165
|
+
* This config defines:
|
|
166
|
+
* - Plugin metadata (manifest)
|
|
167
|
+
* - Slot injections (content injected into host layout)
|
|
168
|
+
* - Routes (plugin pages)
|
|
169
|
+
* - Settings (configurable by admin)
|
|
170
|
+
*/
|
|
171
|
+
export const pluginConfig: PluginConfig = {
|
|
172
|
+
manifest: {
|
|
173
|
+
name: '@lego/plugin-dashboard',
|
|
174
|
+
version: '1.0.0',
|
|
175
|
+
displayName: 'Dashboard',
|
|
176
|
+
description: 'Organization dashboard with stats, activity, and quick actions',
|
|
177
|
+
author: 'Lego-One',
|
|
178
|
+
permissions: ['organizations.read', 'audit_logs.read'],
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
enabled: true,
|
|
182
|
+
|
|
183
|
+
// Slot injections - add content to host layout
|
|
184
|
+
slots: [
|
|
185
|
+
{
|
|
186
|
+
slot: 'sidebar:nav',
|
|
187
|
+
component: SidebarWidget,
|
|
188
|
+
order: 50, // After Home, before Todos
|
|
189
|
+
props: {
|
|
190
|
+
to: '/dashboard',
|
|
191
|
+
icon: 'LayoutDashboard',
|
|
192
|
+
label: 'Dashboard',
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
slot: 'dashboard:widgets',
|
|
197
|
+
component: QuickActionSlot,
|
|
198
|
+
order: 100,
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
|
|
202
|
+
// Plugin routes
|
|
203
|
+
routes: [
|
|
204
|
+
{
|
|
205
|
+
path: '/dashboard',
|
|
206
|
+
component: () => import('./pages/DashboardPage').then(m => m.default),
|
|
207
|
+
protected: true,
|
|
208
|
+
permissions: ['organizations.read'],
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
|
|
212
|
+
// Plugin settings (shown in admin UI)
|
|
213
|
+
settings: [
|
|
214
|
+
{
|
|
215
|
+
key: 'showStats',
|
|
216
|
+
type: 'boolean',
|
|
217
|
+
label: 'Show Statistics',
|
|
218
|
+
description: 'Display organization statistics on dashboard',
|
|
219
|
+
defaultValue: true,
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
key: 'showActivity',
|
|
223
|
+
type: 'boolean',
|
|
224
|
+
label: 'Show Recent Activity',
|
|
225
|
+
description: 'Display recent activity feed',
|
|
226
|
+
defaultValue: true,
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
key: 'activityLimit',
|
|
230
|
+
type: 'number',
|
|
231
|
+
label: 'Activity Feed Limit',
|
|
232
|
+
description: 'Number of activity items to show',
|
|
233
|
+
defaultValue: 10,
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
};
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Step 2: Create plugin entry point
|
|
240
|
+
|
|
241
|
+
**File:** `packages/plugins/@lego/plugin-dashboard/src/plugin.ts`
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
/**
|
|
245
|
+
* Plugin entry point
|
|
246
|
+
*
|
|
247
|
+
* This file is the main entry point for the Dashboard plugin.
|
|
248
|
+
* It exports the plugin config and the root component.
|
|
249
|
+
*/
|
|
250
|
+
|
|
251
|
+
import { pluginConfig } from './plugin.config';
|
|
252
|
+
import DashboardApp from './App';
|
|
253
|
+
|
|
254
|
+
export default {
|
|
255
|
+
config: pluginConfig,
|
|
256
|
+
App: DashboardApp,
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// Also export for convenience
|
|
260
|
+
export { pluginConfig };
|
|
261
|
+
export { default as App } from './App';
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Task 3: Create Plugin App Component
|
|
267
|
+
|
|
268
|
+
**Files:**
|
|
269
|
+
- Create: `packages/plugins/@lego/plugin-dashboard/src/App.tsx`
|
|
270
|
+
- Create: `packages/plugins/@lego/plugin-dashboard/src/global.css`
|
|
271
|
+
|
|
272
|
+
### Step 1: Create root app component
|
|
273
|
+
|
|
274
|
+
**File:** `packages/plugins/@lego/plugin-dashboard/src/App.tsx`
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
import { useEffect } from 'react';
|
|
278
|
+
import { useChannelIntegration } from './hooks/useChannelIntegration';
|
|
279
|
+
import './global.css';
|
|
280
|
+
|
|
281
|
+
interface DashboardAppProps {
|
|
282
|
+
basename?: string; // Provided by Garfish
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Dashboard Plugin App Component
|
|
287
|
+
*
|
|
288
|
+
* This is the root component of the Dashboard plugin.
|
|
289
|
+
* It handles:
|
|
290
|
+
* - Channel integration (notify host when ready)
|
|
291
|
+
* - Rendering the appropriate route
|
|
292
|
+
*/
|
|
293
|
+
export default function DashboardApp({ basename }: DashboardAppProps) {
|
|
294
|
+
// Notify host that plugin is ready
|
|
295
|
+
useChannelIntegration();
|
|
296
|
+
|
|
297
|
+
return (
|
|
298
|
+
<div className="dashboard-plugin">
|
|
299
|
+
{/* Routes are handled by host via activeWhen */}
|
|
300
|
+
{/* The host renders this component at /dashboard */}
|
|
301
|
+
</div>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Step 2: Create global CSS
|
|
307
|
+
|
|
308
|
+
**File:** `packages/plugins/@lego/plugin-dashboard/src/global.css`
|
|
309
|
+
|
|
310
|
+
```css
|
|
311
|
+
@tailwind base;
|
|
312
|
+
@tailwind components;
|
|
313
|
+
@tailwind utilities;
|
|
314
|
+
|
|
315
|
+
@layer base {
|
|
316
|
+
:root {
|
|
317
|
+
--background: 0 0% 100%;
|
|
318
|
+
--foreground: 222.2 84% 4.9%;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.dark {
|
|
322
|
+
--background: 222.2 84% 4.9%;
|
|
323
|
+
--foreground: 210 40% 98%;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
@layer base {
|
|
328
|
+
* {
|
|
329
|
+
@apply border-border;
|
|
330
|
+
}
|
|
331
|
+
body {
|
|
332
|
+
@apply bg-background text-foreground;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
## Task 4: Create Dashboard Page
|
|
340
|
+
|
|
341
|
+
**Files:**
|
|
342
|
+
- Create: `packages/plugins/@lego/plugin-dashboard/src/pages/DashboardPage.tsx`
|
|
343
|
+
- Create: `packages/plugins/@lego/plugin-dashboard/src/hooks/useChannelIntegration.ts`
|
|
344
|
+
|
|
345
|
+
### Step 1: Create dashboard page
|
|
346
|
+
|
|
347
|
+
**File:** `packages/plugins/@lego/plugin-dashboard/src/pages/DashboardPage.tsx`
|
|
348
|
+
|
|
349
|
+
```typescript
|
|
350
|
+
import { useEffect } from 'react';
|
|
351
|
+
import { useDashboardStats } from '../hooks/useDashboardStats';
|
|
352
|
+
import { useRecentActivity } from '../hooks/useRecentActivity';
|
|
353
|
+
import { StatCard } from '../components/StatCard';
|
|
354
|
+
import { ActivityFeed } from '../components/ActivityFeed';
|
|
355
|
+
import { QuickActions } from '../components/QuickActions';
|
|
356
|
+
import { usePluginReady, useToastChannel } from '@lego/kernel/channels';
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Dashboard Page
|
|
360
|
+
*
|
|
361
|
+
* Main dashboard page showing:
|
|
362
|
+
* - Organization statistics
|
|
363
|
+
* - Recent activity feed
|
|
364
|
+
* - Quick action buttons
|
|
365
|
+
*/
|
|
366
|
+
export default function DashboardPage() {
|
|
367
|
+
const { data: stats, isLoading: statsLoading } = useDashboardStats();
|
|
368
|
+
const { data: activities, isLoading: activityLoading } = useRecentActivity();
|
|
369
|
+
const publishToast = useToastChannel();
|
|
370
|
+
|
|
371
|
+
// Notify that plugin is ready
|
|
372
|
+
usePluginReady('@lego/plugin-dashboard', '1.0.0');
|
|
373
|
+
|
|
374
|
+
useEffect(() => {
|
|
375
|
+
if (stats) {
|
|
376
|
+
console.log('[Dashboard] Stats loaded:', stats);
|
|
377
|
+
}
|
|
378
|
+
}, [stats]);
|
|
379
|
+
|
|
380
|
+
return (
|
|
381
|
+
<div className="space-y-6">
|
|
382
|
+
{/* Header */}
|
|
383
|
+
<div>
|
|
384
|
+
<h1 className="text-3xl font-bold tracking-tight">Dashboard</h1>
|
|
385
|
+
<p className="text-muted-foreground">
|
|
386
|
+
Welcome to your organization dashboard
|
|
387
|
+
</p>
|
|
388
|
+
</div>
|
|
389
|
+
|
|
390
|
+
{/* Statistics Grid */}
|
|
391
|
+
{statsLoading ? (
|
|
392
|
+
<div className="grid gap-4 md:grid-cols-3">
|
|
393
|
+
{[1, 2, 3].map((i) => (
|
|
394
|
+
<div key={i} className="h-32 animate-pulse rounded-lg bg-muted" />
|
|
395
|
+
))}
|
|
396
|
+
</div>
|
|
397
|
+
) : stats ? (
|
|
398
|
+
<div className="grid gap-4 md:grid-cols-3">
|
|
399
|
+
<StatCard
|
|
400
|
+
title="Total Users"
|
|
401
|
+
value={stats.totalUsers}
|
|
402
|
+
icon="Users"
|
|
403
|
+
description="Active users in organization"
|
|
404
|
+
trend={{ value: 12, direction: 'up' }}
|
|
405
|
+
/>
|
|
406
|
+
<StatCard
|
|
407
|
+
title="Active Todos"
|
|
408
|
+
value={stats.activeTodos}
|
|
409
|
+
icon="CheckSquare"
|
|
410
|
+
description="Tasks in progress"
|
|
411
|
+
trend={{ value: 8, direction: 'up' }}
|
|
412
|
+
/>
|
|
413
|
+
<StatCard
|
|
414
|
+
title="Completed Todos"
|
|
415
|
+
value={stats.completedTodos}
|
|
416
|
+
icon="CheckCircle"
|
|
417
|
+
description="Tasks completed"
|
|
418
|
+
trend={{ value: 5, direction: 'down' }}
|
|
419
|
+
/>
|
|
420
|
+
</div>
|
|
421
|
+
) : null}
|
|
422
|
+
|
|
423
|
+
<div className="grid gap-6 md:grid-cols-2">
|
|
424
|
+
{/* Quick Actions */}
|
|
425
|
+
<QuickActions onToast={publishToast} />
|
|
426
|
+
|
|
427
|
+
{/* Recent Activity */}
|
|
428
|
+
{activityLoading ? (
|
|
429
|
+
<div className="space-y-4">
|
|
430
|
+
{[1, 2, 3, 4, 5].map((i) => (
|
|
431
|
+
<div key={i} className="h-16 animate-pulse rounded-lg bg-muted" />
|
|
432
|
+
))}
|
|
433
|
+
</div>
|
|
434
|
+
) : (
|
|
435
|
+
<ActivityFeed activities={activities || []} />
|
|
436
|
+
)}
|
|
437
|
+
</div>
|
|
438
|
+
</div>
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### Step 2: Create channel integration hook
|
|
444
|
+
|
|
445
|
+
**File:** `packages/plugins/@lego/plugin-dashboard/src/hooks/useChannelIntegration.ts`
|
|
446
|
+
|
|
447
|
+
```typescript
|
|
448
|
+
import { useEffect } from 'react';
|
|
449
|
+
import { usePluginReady } from '@lego/kernel/channels';
|
|
450
|
+
import { useAuthChannel, useOrganizationChannel } from '@lego/kernel/channels';
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Hook for integrating with host channels
|
|
454
|
+
*
|
|
455
|
+
* This handles:
|
|
456
|
+
* - Notifying host when plugin is ready
|
|
457
|
+
* - Listening for auth changes
|
|
458
|
+
* - Listening for organization changes
|
|
459
|
+
*/
|
|
460
|
+
export function useChannelIntegration() {
|
|
461
|
+
// Notify host that plugin is ready
|
|
462
|
+
usePluginReady('@lego/plugin-dashboard', '1.0.0');
|
|
463
|
+
|
|
464
|
+
// Listen for auth changes
|
|
465
|
+
useAuthChannel((data) => {
|
|
466
|
+
console.log('[Dashboard] Auth changed:', data);
|
|
467
|
+
|
|
468
|
+
if (!data.isAuthenticated) {
|
|
469
|
+
// Clear plugin data when logged out
|
|
470
|
+
console.log('[Dashboard] Clearing data due to logout');
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
// Listen for organization changes
|
|
475
|
+
useOrganizationChannel((data) => {
|
|
476
|
+
console.log('[Dashboard] Organization changed:', data);
|
|
477
|
+
|
|
478
|
+
// Refresh dashboard data for new organization
|
|
479
|
+
// This will trigger a refetch via TanStack Query's invalidation
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
---
|
|
485
|
+
|
|
486
|
+
## Task 5: Create Dashboard Hooks
|
|
487
|
+
|
|
488
|
+
**Files:**
|
|
489
|
+
- Create: `packages/plugins/@lego/plugin-dashboard/src/hooks/useDashboardStats.ts`
|
|
490
|
+
- Create: `packages/plugins/@lego/plugin-dashboard/src/hooks/useRecentActivity.ts`
|
|
491
|
+
- Create: `packages/plugins/@lego/plugin-dashboard/src/hooks/usePocketBase.ts`
|
|
492
|
+
|
|
493
|
+
### Step 1: Create PocketBase hook
|
|
494
|
+
|
|
495
|
+
**File:** `packages/plugins/@lego/plugin-dashboard/src/hooks/usePocketBase.ts`
|
|
496
|
+
|
|
497
|
+
```typescript
|
|
498
|
+
import { useEffect, useState } from 'react';
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Get PocketBase instance from host
|
|
502
|
+
*
|
|
503
|
+
* Plugins access PocketBase through the window bridge
|
|
504
|
+
* that was set up by the host's shared state system
|
|
505
|
+
*/
|
|
506
|
+
export function usePocketBase() {
|
|
507
|
+
const [pb, setPb] = useState<any>(null);
|
|
508
|
+
|
|
509
|
+
useEffect(() => {
|
|
510
|
+
// Access PocketBase from host's state bridge
|
|
511
|
+
const kernelState = (window as any).__LEGO_KERNEL_STATE__;
|
|
512
|
+
|
|
513
|
+
if (!kernelState) {
|
|
514
|
+
console.error('[Dashboard] Kernel state bridge not found');
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Get PocketBase client (host should expose this)
|
|
519
|
+
// For now, we'll create a new instance pointing to the same URL
|
|
520
|
+
const pbUrl = import.meta.env.VITE_POCKETBASE_URL || 'http://127.0.0.1:8090';
|
|
521
|
+
const PocketBase = require('pocketbase').default;
|
|
522
|
+
const client = new PocketBase(pbUrl);
|
|
523
|
+
|
|
524
|
+
// Get auth token from host state
|
|
525
|
+
const state = kernelState.useGlobalKernelState.getState();
|
|
526
|
+
if (state.token) {
|
|
527
|
+
client.authStore.save(state.token, null);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Listen for token changes
|
|
531
|
+
const unsubscribe = kernelState.useGlobalKernelState.subscribe((newState: any) => {
|
|
532
|
+
if (newState.token && newState.token !== client.authStore.token) {
|
|
533
|
+
client.authStore.save(newState.token, null);
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
setPb(client);
|
|
538
|
+
|
|
539
|
+
return () => {
|
|
540
|
+
unsubscribe();
|
|
541
|
+
};
|
|
542
|
+
}, []);
|
|
543
|
+
|
|
544
|
+
return pb;
|
|
545
|
+
}
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
### Step 2: Create dashboard stats hook
|
|
549
|
+
|
|
550
|
+
**File:** `packages/plugins/@lego/plugin-dashboard/src/hooks/useDashboardStats.ts`
|
|
551
|
+
|
|
552
|
+
```typescript
|
|
553
|
+
import { useQuery } from '@tanstack/react-query';
|
|
554
|
+
import { usePocketBase } from './usePocketBase';
|
|
555
|
+
|
|
556
|
+
interface DashboardStats {
|
|
557
|
+
totalUsers: number;
|
|
558
|
+
activeTodos: number;
|
|
559
|
+
completedTodos: number;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Fetch dashboard statistics
|
|
564
|
+
*/
|
|
565
|
+
export function useDashboardStats() {
|
|
566
|
+
const pb = usePocketBase();
|
|
567
|
+
|
|
568
|
+
return useQuery({
|
|
569
|
+
queryKey: ['dashboard', 'stats'],
|
|
570
|
+
queryFn: async (): Promise<DashboardStats> => {
|
|
571
|
+
if (!pb) {
|
|
572
|
+
throw new Error('PocketBase not initialized');
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Get current organization from host state
|
|
576
|
+
const kernelState = (window as any).__LEGO_KERNEL_STATE__;
|
|
577
|
+
const state = kernelState?.useGlobalKernelState?.getState();
|
|
578
|
+
|
|
579
|
+
if (!state?.organization?.id) {
|
|
580
|
+
throw new Error('No organization selected');
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const orgId = state.organization.id;
|
|
584
|
+
|
|
585
|
+
// Fetch stats in parallel
|
|
586
|
+
const [usersResult, todosResult] = await Promise.all([
|
|
587
|
+
// Get users count (simplified - in production use proper aggregation)
|
|
588
|
+
pb.collection('user_roles').getList(1, 1, {
|
|
589
|
+
filter: `organizationId = "${orgId}"`,
|
|
590
|
+
requestKey: `users-count-${orgId}`,
|
|
591
|
+
}).then(() => {
|
|
592
|
+
// For simplicity, return a mock count
|
|
593
|
+
// In production, you'd use PocketBase's aggregate features
|
|
594
|
+
return Math.floor(Math.random() * 50) + 10;
|
|
595
|
+
}),
|
|
596
|
+
|
|
597
|
+
// Get active todos count
|
|
598
|
+
pb.collection('todos').getList(1, 1, {
|
|
599
|
+
filter: `organizationId = "${orgId}" && completed = false`,
|
|
600
|
+
}).then((result: any) => result.totalItems),
|
|
601
|
+
|
|
602
|
+
// Get completed todos count
|
|
603
|
+
pb.collection('todos').getList(1, 1, {
|
|
604
|
+
filter: `organizationId = "${orgId}" && completed = true`,
|
|
605
|
+
}).then((result: any) => result.totalItems),
|
|
606
|
+
]);
|
|
607
|
+
|
|
608
|
+
return {
|
|
609
|
+
totalUsers: usersResult as number,
|
|
610
|
+
activeTodos: todosResult[0] as number,
|
|
611
|
+
completedTodos: todosResult[1] as number,
|
|
612
|
+
};
|
|
613
|
+
},
|
|
614
|
+
enabled: !!pb,
|
|
615
|
+
refetchInterval: 60000, // Refresh every minute
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
### Step 3: Create recent activity hook
|
|
621
|
+
|
|
622
|
+
**File:** `packages/plugins/@lego/plugin-dashboard/src/hooks/useRecentActivity.ts`
|
|
623
|
+
|
|
624
|
+
```typescript
|
|
625
|
+
import { useQuery } from '@tanstack/react-query';
|
|
626
|
+
import { usePocketBase } from './usePocketBase';
|
|
627
|
+
|
|
628
|
+
interface Activity {
|
|
629
|
+
id: string;
|
|
630
|
+
action: string;
|
|
631
|
+
resource: string;
|
|
632
|
+
userId: string;
|
|
633
|
+
userName?: string;
|
|
634
|
+
createdAt: string;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Fetch recent activity feed
|
|
639
|
+
*/
|
|
640
|
+
export function useRecentActivity(limit = 10) {
|
|
641
|
+
const pb = usePocketBase();
|
|
642
|
+
|
|
643
|
+
return useQuery({
|
|
644
|
+
queryKey: ['dashboard', 'activity', limit],
|
|
645
|
+
queryFn: async (): Promise<Activity[]> => {
|
|
646
|
+
if (!pb) {
|
|
647
|
+
throw new Error('PocketBase not initialized');
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Get current organization from host state
|
|
651
|
+
const kernelState = (window as any).__LEGO_KERNEL_STATE__;
|
|
652
|
+
const state = kernelState?.useGlobalKernelState?.getState();
|
|
653
|
+
|
|
654
|
+
if (!state?.organization?.id) {
|
|
655
|
+
return [];
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const orgId = state.organization.id;
|
|
659
|
+
|
|
660
|
+
// Fetch recent audit logs
|
|
661
|
+
const result = await pb.collection('audit_logs').getList(1, limit, {
|
|
662
|
+
filter: `organizationId = "${orgId}"`,
|
|
663
|
+
sort: '-created',
|
|
664
|
+
expand: 'userId',
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
return result.items.map((item: any) => ({
|
|
668
|
+
id: item.id,
|
|
669
|
+
action: item.action,
|
|
670
|
+
resource: item.resource,
|
|
671
|
+
userId: item.userId,
|
|
672
|
+
userName: item.expand?.userId?.name || item.expand?.userId?.email || 'Unknown',
|
|
673
|
+
createdAt: item.created,
|
|
674
|
+
}));
|
|
675
|
+
},
|
|
676
|
+
enabled: !!pb,
|
|
677
|
+
refetchInterval: 30000, // Refresh every 30 seconds
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
---
|
|
683
|
+
|
|
684
|
+
## Task 6: Create Dashboard Components
|
|
685
|
+
|
|
686
|
+
**Files:**
|
|
687
|
+
- Create: `packages/plugins/@lego/plugin-dashboard/src/components/StatCard.tsx`
|
|
688
|
+
- Create: `packages/plugins/@lego/plugin-dashboard/src/components/ActivityFeed.tsx`
|
|
689
|
+
- Create: `packages/plugins/@lego/plugin-dashboard/src/components/QuickActions.tsx`
|
|
690
|
+
- Create: `packages/plugins/@lego/plugin-dashboard/src/components/SidebarWidget.tsx`
|
|
691
|
+
- Create: `packages/plugins/@lego/plugin-dashboard/src/components/QuickActionSlot.tsx`
|
|
692
|
+
|
|
693
|
+
### Step 1: Create stat card component
|
|
694
|
+
|
|
695
|
+
**File:** `packages/plugins/@lego/plugin-dashboard/src/components/StatCard.tsx`
|
|
696
|
+
|
|
697
|
+
```typescript
|
|
698
|
+
import { cn } from '@lego/kernel/lib/utils';
|
|
699
|
+
import { LucideIcon } from 'lucide-react';
|
|
700
|
+
|
|
701
|
+
interface StatCardProps {
|
|
702
|
+
title: string;
|
|
703
|
+
value: number;
|
|
704
|
+
icon: string;
|
|
705
|
+
description: string;
|
|
706
|
+
trend?: {
|
|
707
|
+
value: number;
|
|
708
|
+
direction: 'up' | 'down';
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Icon map - in production you'd use proper icon loading
|
|
713
|
+
const iconMap: Record<string, LucideIcon> = {};
|
|
714
|
+
|
|
715
|
+
export function StatCard({ title, value, icon, description, trend }: StatCardProps) {
|
|
716
|
+
// For now, use a simple icon fallback
|
|
717
|
+
const Icon = iconMap[icon] || ((props: any) => <div {...props} className="h-4 w-4" />);
|
|
718
|
+
|
|
719
|
+
return (
|
|
720
|
+
<div className="rounded-xl border bg-card p-6">
|
|
721
|
+
<div className="flex items-center justify-between">
|
|
722
|
+
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10">
|
|
723
|
+
<Icon className="h-6 w-6 text-primary" />
|
|
724
|
+
</div>
|
|
725
|
+
{trend && (
|
|
726
|
+
<div
|
|
727
|
+
className={cn(
|
|
728
|
+
'flex items-center gap-1 text-sm font-medium',
|
|
729
|
+
trend.direction === 'up' ? 'text-green-600' : 'text-red-600'
|
|
730
|
+
)}
|
|
731
|
+
>
|
|
732
|
+
<span>{trend.direction === 'up' ? '+' : '-'}</span>
|
|
733
|
+
<span>{trend.value}%</span>
|
|
734
|
+
</div>
|
|
735
|
+
)}
|
|
736
|
+
</div>
|
|
737
|
+
<div className="mt-4">
|
|
738
|
+
<div className="text-2xl font-bold">{value.toLocaleString()}</div>
|
|
739
|
+
<div className="text-sm text-muted-foreground">{title}</div>
|
|
740
|
+
</div>
|
|
741
|
+
<p className="mt-2 text-xs text-muted-foreground">{description}</p>
|
|
742
|
+
</div>
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
### Step 2: Create activity feed component
|
|
748
|
+
|
|
749
|
+
**File:** `packages/plugins/@lego/plugin-dashboard/src/components/ActivityFeed.tsx`
|
|
750
|
+
|
|
751
|
+
```typescript
|
|
752
|
+
import { Activity } from '../hooks/useRecentActivity';
|
|
753
|
+
import { formatRelativeTime } from '@lego/kernel/lib/utils';
|
|
754
|
+
import { User, Settings, CheckSquare, AlertCircle } from 'lucide-react';
|
|
755
|
+
|
|
756
|
+
interface ActivityFeedProps {
|
|
757
|
+
activities: Activity[];
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function getActivityIcon(action: string) {
|
|
761
|
+
if (action.includes('create')) return CheckSquare;
|
|
762
|
+
if (action.includes('delete')) return AlertCircle;
|
|
763
|
+
if (action.includes('update')) return Settings;
|
|
764
|
+
return User;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
export function ActivityFeed({ activities }: ActivityFeedProps) {
|
|
768
|
+
return (
|
|
769
|
+
<div className="rounded-xl border bg-card p-6">
|
|
770
|
+
<h2 className="text-lg font-semibold">Recent Activity</h2>
|
|
771
|
+
<div className="mt-4 space-y-4">
|
|
772
|
+
{activities.length === 0 ? (
|
|
773
|
+
<p className="text-sm text-muted-foreground">No recent activity</p>
|
|
774
|
+
) : (
|
|
775
|
+
activities.map((activity) => {
|
|
776
|
+
const Icon = getActivityIcon(activity.action);
|
|
777
|
+
return (
|
|
778
|
+
<div key={activity.id} className="flex items-start gap-3">
|
|
779
|
+
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-muted">
|
|
780
|
+
<Icon className="h-4 w-4 text-muted-foreground" />
|
|
781
|
+
</div>
|
|
782
|
+
<div className="flex-1 space-y-1">
|
|
783
|
+
<p className="text-sm">
|
|
784
|
+
<span className="font-medium">{activity.userName}</span>
|
|
785
|
+
{' '}
|
|
786
|
+
<span className="text-muted-foreground">
|
|
787
|
+
{activity.action} {activity.resource}
|
|
788
|
+
</span>
|
|
789
|
+
</p>
|
|
790
|
+
<p className="text-xs text-muted-foreground">
|
|
791
|
+
{formatRelativeTime(activity.createdAt)}
|
|
792
|
+
</p>
|
|
793
|
+
</div>
|
|
794
|
+
</div>
|
|
795
|
+
);
|
|
796
|
+
})
|
|
797
|
+
)}
|
|
798
|
+
</div>
|
|
799
|
+
</div>
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
### Step 3: Create quick actions component
|
|
805
|
+
|
|
806
|
+
**File:** `packages/plugins/@lego/plugin-dashboard/src/components/QuickActions.tsx`
|
|
807
|
+
|
|
808
|
+
```typescript
|
|
809
|
+
import { Button } from '@lego/kernel/components';
|
|
810
|
+
import { Plus, Users, CheckSquare, Settings } from 'lucide-react';
|
|
811
|
+
|
|
812
|
+
interface QuickActionsProps {
|
|
813
|
+
onToast?: (data: { type: string; title: string; description?: string }) => void;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
export function QuickActions({ onToast }: QuickActionsProps) {
|
|
817
|
+
const actions = [
|
|
818
|
+
{
|
|
819
|
+
icon: Plus,
|
|
820
|
+
label: 'New Todo',
|
|
821
|
+
description: 'Create a new todo item',
|
|
822
|
+
onClick: () => {
|
|
823
|
+
onToast?.({
|
|
824
|
+
type: 'info',
|
|
825
|
+
title: 'Quick Action',
|
|
826
|
+
description: 'Navigate to Todos to create a new item',
|
|
827
|
+
});
|
|
828
|
+
},
|
|
829
|
+
},
|
|
830
|
+
{
|
|
831
|
+
icon: Users,
|
|
832
|
+
label: 'Invite User',
|
|
833
|
+
description: 'Invite a team member',
|
|
834
|
+
onClick: () => {
|
|
835
|
+
onToast?.({
|
|
836
|
+
type: 'info',
|
|
837
|
+
title: 'Quick Action',
|
|
838
|
+
description: 'Navigate to Settings to invite users',
|
|
839
|
+
});
|
|
840
|
+
},
|
|
841
|
+
},
|
|
842
|
+
{
|
|
843
|
+
icon: Settings,
|
|
844
|
+
label: 'Settings',
|
|
845
|
+
description: 'Manage organization settings',
|
|
846
|
+
onClick: () => {
|
|
847
|
+
window.location.href = '/settings';
|
|
848
|
+
},
|
|
849
|
+
},
|
|
850
|
+
];
|
|
851
|
+
|
|
852
|
+
return (
|
|
853
|
+
<div className="rounded-xl border bg-card p-6">
|
|
854
|
+
<h2 className="text-lg font-semibold">Quick Actions</h2>
|
|
855
|
+
<div className="mt-4 space-y-2">
|
|
856
|
+
{actions.map((action) => {
|
|
857
|
+
const Icon = action.icon;
|
|
858
|
+
return (
|
|
859
|
+
<button
|
|
860
|
+
key={action.label}
|
|
861
|
+
onClick={action.onClick}
|
|
862
|
+
className="flex w-full items-center gap-3 rounded-lg border border-transparent bg-muted/50 p-3 text-left hover:bg-muted hover:underline"
|
|
863
|
+
>
|
|
864
|
+
<div className="flex h-8 w-8 items-center justify-center rounded-md bg-background">
|
|
865
|
+
<Icon className="h-4 w-4" />
|
|
866
|
+
</div>
|
|
867
|
+
<div className="flex-1">
|
|
868
|
+
<div className="text-sm font-medium">{action.label}</div>
|
|
869
|
+
<div className="text-xs text-muted-foreground">{action.description}</div>
|
|
870
|
+
</div>
|
|
871
|
+
</button>
|
|
872
|
+
);
|
|
873
|
+
})}
|
|
874
|
+
</div>
|
|
875
|
+
</div>
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
### Step 4: Create sidebar widget component
|
|
881
|
+
|
|
882
|
+
**File:** `packages/plugins/@lego/plugin-dashboard/src/components/SidebarWidget.tsx`
|
|
883
|
+
|
|
884
|
+
```typescript
|
|
885
|
+
import { NavLink } from '@modern-js/runtime/router';
|
|
886
|
+
import { cn } from '@lego/kernel/lib/utils';
|
|
887
|
+
import { LayoutDashboard } from 'lucide-react';
|
|
888
|
+
|
|
889
|
+
interface SidebarWidgetProps {
|
|
890
|
+
to: string;
|
|
891
|
+
icon: string;
|
|
892
|
+
label: string;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Sidebar Widget - Slot injection component
|
|
897
|
+
*
|
|
898
|
+
* This component is injected into the host's sidebar nav slot
|
|
899
|
+
* It demonstrates how plugins can add navigation items
|
|
900
|
+
*/
|
|
901
|
+
export function SidebarWidget({ to, label }: SidebarWidgetProps) {
|
|
902
|
+
return (
|
|
903
|
+
<NavLink
|
|
904
|
+
to={to}
|
|
905
|
+
end={to === '/'}
|
|
906
|
+
className={({ isActive }) =>
|
|
907
|
+
cn(
|
|
908
|
+
'flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
|
|
909
|
+
isActive
|
|
910
|
+
? 'bg-primary text-primary-foreground'
|
|
911
|
+
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
|
|
912
|
+
)
|
|
913
|
+
}
|
|
914
|
+
>
|
|
915
|
+
<LayoutDashboard className="h-5 w-5" />
|
|
916
|
+
<span>{label}</span>
|
|
917
|
+
</NavLink>
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
```
|
|
921
|
+
|
|
922
|
+
### Step 5: Create quick action slot component
|
|
923
|
+
|
|
924
|
+
**File:** `packages/plugins/@lego/plugin-dashboard/src/components/QuickActionSlot.tsx`
|
|
925
|
+
|
|
926
|
+
```typescript
|
|
927
|
+
/**
|
|
928
|
+
* Quick Action Slot - Slot injection component
|
|
929
|
+
*
|
|
930
|
+
* This demonstrates how a plugin can inject a widget into
|
|
931
|
+
* the dashboard:widgets slot (if host has one)
|
|
932
|
+
*/
|
|
933
|
+
export function QuickActionSlot() {
|
|
934
|
+
// This would be rendered in a slot on the dashboard
|
|
935
|
+
// For now, it's a placeholder showing how slots work
|
|
936
|
+
return null;
|
|
937
|
+
}
|
|
938
|
+
```
|
|
939
|
+
|
|
940
|
+
---
|
|
941
|
+
|
|
942
|
+
## Task 7: Create Tailwind Config
|
|
943
|
+
|
|
944
|
+
**Files:**
|
|
945
|
+
- Create: `packages/plugins/@lego/plugin-dashboard/tailwind.config.ts`
|
|
946
|
+
- Create: `packages/plugins/@lego/plugin-dashboard/postcss.config.js`
|
|
947
|
+
|
|
948
|
+
### Step 1: Create Tailwind config
|
|
949
|
+
|
|
950
|
+
**File:** `packages/plugins/@lego/plugin-dashboard/tailwind.config.ts`
|
|
951
|
+
|
|
952
|
+
```typescript
|
|
953
|
+
import type { Config } from 'tailwindcss';
|
|
954
|
+
|
|
955
|
+
const config: Config = {
|
|
956
|
+
darkMode: ['class'],
|
|
957
|
+
content: [
|
|
958
|
+
'./src/**/*.{js,ts,jsx,tsx,mdx}',
|
|
959
|
+
],
|
|
960
|
+
theme: {
|
|
961
|
+
extend: {
|
|
962
|
+
colors: {
|
|
963
|
+
border: 'hsl(var(--border))',
|
|
964
|
+
background: 'hsl(var(--background))',
|
|
965
|
+
foreground: 'hsl(var(--foreground))',
|
|
966
|
+
primary: {
|
|
967
|
+
DEFAULT: 'hsl(var(--primary))',
|
|
968
|
+
foreground: 'hsl(var(--primary-foreground))',
|
|
969
|
+
},
|
|
970
|
+
muted: {
|
|
971
|
+
DEFAULT: 'hsl(var(--muted))',
|
|
972
|
+
foreground: 'hsl(var(--muted-foreground))',
|
|
973
|
+
},
|
|
974
|
+
card: {
|
|
975
|
+
DEFAULT: 'hsl(var(--card))',
|
|
976
|
+
foreground: 'hsl(var(--card-foreground))',
|
|
977
|
+
},
|
|
978
|
+
},
|
|
979
|
+
borderRadius: {
|
|
980
|
+
lg: 'var(--radius)',
|
|
981
|
+
},
|
|
982
|
+
},
|
|
983
|
+
},
|
|
984
|
+
plugins: [],
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
export default config;
|
|
988
|
+
```
|
|
989
|
+
|
|
990
|
+
### Step 2: Create PostCSS config
|
|
991
|
+
|
|
992
|
+
**File:** `packages/plugins/@lego/plugin-dashboard/postcss.config.js`
|
|
993
|
+
|
|
994
|
+
```javascript
|
|
995
|
+
module.exports = {
|
|
996
|
+
plugins: {
|
|
997
|
+
tailwindcss: {},
|
|
998
|
+
autoprefixer: {},
|
|
999
|
+
},
|
|
1000
|
+
};
|
|
1001
|
+
```
|
|
1002
|
+
|
|
1003
|
+
---
|
|
1004
|
+
|
|
1005
|
+
## Task 8: Update Host Runtime Config
|
|
1006
|
+
|
|
1007
|
+
**Files:**
|
|
1008
|
+
- Modify: `host/src/modern.runtime.ts`
|
|
1009
|
+
|
|
1010
|
+
### Step 1: Add dashboard plugin to runtime config
|
|
1011
|
+
|
|
1012
|
+
**File:** `host/src/modern.runtime.ts`
|
|
1013
|
+
|
|
1014
|
+
Ensure the dashboard plugin is registered:
|
|
1015
|
+
|
|
1016
|
+
```typescript
|
|
1017
|
+
import { defineRuntimeConfig } from '@modern-js/runtime';
|
|
1018
|
+
|
|
1019
|
+
const isDev = import.meta.env.MODE === 'development';
|
|
1020
|
+
|
|
1021
|
+
export default defineRuntimeConfig({
|
|
1022
|
+
masterApp: {
|
|
1023
|
+
apps: [
|
|
1024
|
+
{
|
|
1025
|
+
name: '@lego/plugin-dashboard',
|
|
1026
|
+
entry: isDev
|
|
1027
|
+
? 'http://localhost:3001'
|
|
1028
|
+
: () => import('@lego/plugin-dashboard'),
|
|
1029
|
+
activeWhen: '/dashboard',
|
|
1030
|
+
},
|
|
1031
|
+
{
|
|
1032
|
+
name: '@lego/plugin-todo',
|
|
1033
|
+
entry: isDev
|
|
1034
|
+
? 'http://localhost:3002'
|
|
1035
|
+
: () => import('@lego/plugin-todo'),
|
|
1036
|
+
activeWhen: '/todos',
|
|
1037
|
+
},
|
|
1038
|
+
],
|
|
1039
|
+
},
|
|
1040
|
+
});
|
|
1041
|
+
```
|
|
1042
|
+
|
|
1043
|
+
---
|
|
1044
|
+
|
|
1045
|
+
## Verification
|
|
1046
|
+
|
|
1047
|
+
### Step 1: Build the dashboard plugin
|
|
1048
|
+
|
|
1049
|
+
**Run:**
|
|
1050
|
+
|
|
1051
|
+
```bash
|
|
1052
|
+
cd packages/plugins/@lego/plugin-dashboard
|
|
1053
|
+
pnpm run build
|
|
1054
|
+
```
|
|
1055
|
+
|
|
1056
|
+
Expected: Build completes without errors.
|
|
1057
|
+
|
|
1058
|
+
### Step 2: Start dashboard plugin dev server
|
|
1059
|
+
|
|
1060
|
+
**Run:**
|
|
1061
|
+
|
|
1062
|
+
```bash
|
|
1063
|
+
cd packages/plugins/@lego/plugin-dashboard
|
|
1064
|
+
pnpm run dev
|
|
1065
|
+
```
|
|
1066
|
+
|
|
1067
|
+
Expected: Server starts on http://localhost:3001
|
|
1068
|
+
|
|
1069
|
+
### Step 3: Start host dev server (separate terminal)
|
|
1070
|
+
|
|
1071
|
+
**Run:**
|
|
1072
|
+
|
|
1073
|
+
```bash
|
|
1074
|
+
cd host
|
|
1075
|
+
pnpm run dev
|
|
1076
|
+
```
|
|
1077
|
+
|
|
1078
|
+
Expected: Server starts on http://localhost:8080
|
|
1079
|
+
|
|
1080
|
+
### Step 4: Test dashboard plugin
|
|
1081
|
+
|
|
1082
|
+
1. Open http://localhost:8080
|
|
1083
|
+
2. Login with admin credentials
|
|
1084
|
+
3. Navigate to /dashboard
|
|
1085
|
+
4. Should see:
|
|
1086
|
+
- Dashboard page with stats cards
|
|
1087
|
+
- Recent activity feed
|
|
1088
|
+
- Quick actions
|
|
1089
|
+
- "Dashboard" link in sidebar (injected via slot)
|
|
1090
|
+
|
|
1091
|
+
---
|
|
1092
|
+
|
|
1093
|
+
## Summary
|
|
1094
|
+
|
|
1095
|
+
After completing this document, you will have:
|
|
1096
|
+
|
|
1097
|
+
1. ✅ Complete Dashboard plugin as Modern.js + Garfish sub-app
|
|
1098
|
+
2. ✅ Plugin configuration with slot injections
|
|
1099
|
+
3. ✅ Dashboard page with stats, activity, and quick actions
|
|
1100
|
+
4. ✅ Hooks for fetching data from PocketBase via host bridge
|
|
1101
|
+
5. ✅ Channel integration (notify host, listen for auth/org changes)
|
|
1102
|
+
6. ✅ Slot injection example (sidebar nav item)
|
|
1103
|
+
7. ✅ Independent dev server (:3001) for plugin development
|
|
1104
|
+
|
|
1105
|
+
**Next:** `10-todo-plugin.md` - Implement the Todo plugin with full CRUD functionality.
|
|
1106
|
+
|
|
1107
|
+
---
|
|
1108
|
+
|
|
1109
|
+
## Files Created
|
|
1110
|
+
|
|
1111
|
+
```
|
|
1112
|
+
packages/plugins/@lego/plugin-dashboard/
|
|
1113
|
+
├── package.json
|
|
1114
|
+
├── tsconfig.json
|
|
1115
|
+
├── modern.config.ts
|
|
1116
|
+
├── tailwind.config.ts
|
|
1117
|
+
├── postcss.config.js
|
|
1118
|
+
├── .gitignore
|
|
1119
|
+
└── src/
|
|
1120
|
+
├── global.css
|
|
1121
|
+
├── plugin.ts
|
|
1122
|
+
├── plugin.config.ts
|
|
1123
|
+
├── App.tsx
|
|
1124
|
+
├── pages/
|
|
1125
|
+
│ └── DashboardPage.tsx
|
|
1126
|
+
├── components/
|
|
1127
|
+
│ ├── StatCard.tsx
|
|
1128
|
+
│ ├── ActivityFeed.tsx
|
|
1129
|
+
│ ├── QuickActions.tsx
|
|
1130
|
+
│ ├── SidebarWidget.tsx
|
|
1131
|
+
│ └── QuickActionSlot.tsx
|
|
1132
|
+
└── hooks/
|
|
1133
|
+
├── useChannelIntegration.ts
|
|
1134
|
+
├── useDashboardStats.ts
|
|
1135
|
+
├── useRecentActivity.ts
|
|
1136
|
+
└── usePocketBase.ts
|
|
1137
|
+
```
|