@qwickapps/server 1.3.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/README.md +154 -0
  2. package/dist/core/control-panel.d.ts.map +1 -1
  3. package/dist/core/control-panel.js +30 -2
  4. package/dist/core/control-panel.js.map +1 -1
  5. package/dist/core/plugin-registry.d.ts +36 -0
  6. package/dist/core/plugin-registry.d.ts.map +1 -1
  7. package/dist/core/plugin-registry.js +26 -0
  8. package/dist/core/plugin-registry.js.map +1 -1
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/plugins/auth/adapters/index.d.ts +1 -0
  14. package/dist/plugins/auth/adapters/index.d.ts.map +1 -1
  15. package/dist/plugins/auth/adapters/index.js +1 -0
  16. package/dist/plugins/auth/adapters/index.js.map +1 -1
  17. package/dist/plugins/auth/adapters/supabase-adapter.d.ts.map +1 -1
  18. package/dist/plugins/auth/adapters/supabase-adapter.js.map +1 -1
  19. package/dist/plugins/auth/adapters/supertokens-adapter.d.ts +18 -0
  20. package/dist/plugins/auth/adapters/supertokens-adapter.d.ts.map +1 -0
  21. package/dist/plugins/auth/adapters/supertokens-adapter.js +267 -0
  22. package/dist/plugins/auth/adapters/supertokens-adapter.js.map +1 -0
  23. package/dist/plugins/auth/env-config.d.ts +88 -0
  24. package/dist/plugins/auth/env-config.d.ts.map +1 -0
  25. package/dist/plugins/auth/env-config.js +489 -0
  26. package/dist/plugins/auth/env-config.js.map +1 -0
  27. package/dist/plugins/auth/index.d.ts +3 -1
  28. package/dist/plugins/auth/index.d.ts.map +1 -1
  29. package/dist/plugins/auth/index.js +3 -0
  30. package/dist/plugins/auth/index.js.map +1 -1
  31. package/dist/plugins/auth/supertokens-adapter.test.d.ts +10 -0
  32. package/dist/plugins/auth/supertokens-adapter.test.d.ts.map +1 -0
  33. package/dist/plugins/auth/supertokens-adapter.test.js +486 -0
  34. package/dist/plugins/auth/supertokens-adapter.test.js.map +1 -0
  35. package/dist/plugins/auth/types.d.ts +70 -0
  36. package/dist/plugins/auth/types.d.ts.map +1 -1
  37. package/dist/plugins/auth/types.js.map +1 -1
  38. package/dist/plugins/cache-plugin.test.js +3 -0
  39. package/dist/plugins/cache-plugin.test.js.map +1 -1
  40. package/dist/plugins/index.d.ts +4 -2
  41. package/dist/plugins/index.d.ts.map +1 -1
  42. package/dist/plugins/index.js +3 -1
  43. package/dist/plugins/index.js.map +1 -1
  44. package/dist/plugins/postgres-plugin.test.js +3 -0
  45. package/dist/plugins/postgres-plugin.test.js.map +1 -1
  46. package/dist/plugins/preferences/__tests__/deep-merge.test.d.ts +7 -0
  47. package/dist/plugins/preferences/__tests__/deep-merge.test.d.ts.map +1 -0
  48. package/dist/plugins/preferences/__tests__/deep-merge.test.js +215 -0
  49. package/dist/plugins/preferences/__tests__/deep-merge.test.js.map +1 -0
  50. package/dist/plugins/preferences/__tests__/preferences-plugin.test.d.ts +7 -0
  51. package/dist/plugins/preferences/__tests__/preferences-plugin.test.d.ts.map +1 -0
  52. package/dist/plugins/preferences/__tests__/preferences-plugin.test.js +265 -0
  53. package/dist/plugins/preferences/__tests__/preferences-plugin.test.js.map +1 -0
  54. package/dist/plugins/preferences/index.d.ts +12 -0
  55. package/dist/plugins/preferences/index.d.ts.map +1 -0
  56. package/dist/plugins/preferences/index.js +13 -0
  57. package/dist/plugins/preferences/index.js.map +1 -0
  58. package/dist/plugins/preferences/preferences-plugin.d.ts +39 -0
  59. package/dist/plugins/preferences/preferences-plugin.d.ts.map +1 -0
  60. package/dist/plugins/preferences/preferences-plugin.js +226 -0
  61. package/dist/plugins/preferences/preferences-plugin.js.map +1 -0
  62. package/dist/plugins/preferences/stores/index.d.ts +9 -0
  63. package/dist/plugins/preferences/stores/index.d.ts.map +1 -0
  64. package/dist/plugins/preferences/stores/index.js +9 -0
  65. package/dist/plugins/preferences/stores/index.js.map +1 -0
  66. package/dist/plugins/preferences/stores/postgres-store.d.ts +41 -0
  67. package/dist/plugins/preferences/stores/postgres-store.d.ts.map +1 -0
  68. package/dist/plugins/preferences/stores/postgres-store.js +181 -0
  69. package/dist/plugins/preferences/stores/postgres-store.js.map +1 -0
  70. package/dist/plugins/preferences/types.d.ts +91 -0
  71. package/dist/plugins/preferences/types.d.ts.map +1 -0
  72. package/dist/plugins/preferences/types.js +10 -0
  73. package/dist/plugins/preferences/types.js.map +1 -0
  74. package/dist/plugins/users/__tests__/users-plugin.test.d.ts +9 -0
  75. package/dist/plugins/users/__tests__/users-plugin.test.d.ts.map +1 -0
  76. package/dist/plugins/users/__tests__/users-plugin.test.js +546 -0
  77. package/dist/plugins/users/__tests__/users-plugin.test.js.map +1 -0
  78. package/dist/plugins/users/index.d.ts +2 -2
  79. package/dist/plugins/users/index.d.ts.map +1 -1
  80. package/dist/plugins/users/index.js +1 -1
  81. package/dist/plugins/users/index.js.map +1 -1
  82. package/dist/plugins/users/types.d.ts +36 -0
  83. package/dist/plugins/users/types.d.ts.map +1 -1
  84. package/dist/plugins/users/users-plugin.d.ts +8 -2
  85. package/dist/plugins/users/users-plugin.d.ts.map +1 -1
  86. package/dist/plugins/users/users-plugin.js +122 -5
  87. package/dist/plugins/users/users-plugin.js.map +1 -1
  88. package/dist-ui/assets/{index-Bsp2ntcw.js → index-BY8OxNgO.js} +112 -112
  89. package/dist-ui/assets/index-BY8OxNgO.js.map +1 -0
  90. package/dist-ui/index.html +1 -1
  91. package/dist-ui-lib/api/controlPanelApi.d.ts +53 -7
  92. package/dist-ui-lib/dashboard/WidgetComponentRegistry.d.ts +9 -5
  93. package/dist-ui-lib/dashboard/builtInWidgets.d.ts +7 -1
  94. package/dist-ui-lib/index.js +2382 -3651
  95. package/dist-ui-lib/index.js.map +1 -1
  96. package/dist-ui-lib/pages/AuthPage.d.ts +1 -0
  97. package/dist-ui-lib/pages/PluginsPage.d.ts +1 -0
  98. package/package.json +7 -2
  99. package/src/core/control-panel.ts +33 -2
  100. package/src/core/plugin-registry.ts +63 -0
  101. package/src/index.ts +7 -0
  102. package/src/plugins/auth/adapters/index.ts +1 -0
  103. package/src/plugins/auth/adapters/supabase-adapter.ts +22 -14
  104. package/src/plugins/auth/adapters/supertokens-adapter.ts +326 -0
  105. package/src/plugins/auth/env-config.ts +572 -0
  106. package/src/plugins/auth/index.ts +9 -0
  107. package/src/plugins/auth/supertokens-adapter.test.ts +621 -0
  108. package/src/plugins/auth/types.ts +80 -0
  109. package/src/plugins/cache-plugin.test.ts +3 -0
  110. package/src/plugins/index.ts +26 -0
  111. package/src/plugins/postgres-plugin.test.ts +3 -0
  112. package/src/plugins/preferences/__tests__/deep-merge.test.ts +242 -0
  113. package/src/plugins/preferences/__tests__/preferences-plugin.test.ts +350 -0
  114. package/src/plugins/preferences/index.ts +30 -0
  115. package/src/plugins/preferences/preferences-plugin.ts +270 -0
  116. package/src/plugins/preferences/stores/index.ts +9 -0
  117. package/src/plugins/preferences/stores/postgres-store.ts +252 -0
  118. package/src/plugins/preferences/types.ts +100 -0
  119. package/src/plugins/users/__tests__/users-plugin.test.ts +690 -0
  120. package/src/plugins/users/index.ts +3 -0
  121. package/src/plugins/users/types.ts +38 -0
  122. package/src/plugins/users/users-plugin.ts +142 -5
  123. package/ui/src/App.tsx +4 -1
  124. package/ui/src/api/controlPanelApi.ts +100 -1
  125. package/ui/src/components/ControlPanelApp.tsx +3 -0
  126. package/ui/src/dashboard/PluginWidgetRenderer.tsx +13 -10
  127. package/ui/src/dashboard/WidgetComponentRegistry.tsx +13 -9
  128. package/ui/src/dashboard/builtInWidgets.tsx +8 -2
  129. package/ui/src/pages/AuthPage.tsx +259 -0
  130. package/ui/src/pages/PluginsPage.tsx +394 -0
  131. package/ui/vite.lib.config.ts +5 -0
  132. package/dist-ui/assets/index-Bsp2ntcw.js.map +0 -1
@@ -0,0 +1,259 @@
1
+ import { useState, useEffect } from 'react';
2
+ import {
3
+ Box,
4
+ Card,
5
+ CardContent,
6
+ Typography,
7
+ Table,
8
+ TableBody,
9
+ TableCell,
10
+ TableContainer,
11
+ TableHead,
12
+ TableRow,
13
+ Chip,
14
+ CircularProgress,
15
+ Alert,
16
+ IconButton,
17
+ Tooltip,
18
+ } from '@mui/material';
19
+ import CheckCircleIcon from '@mui/icons-material/CheckCircle';
20
+ import ErrorIcon from '@mui/icons-material/Error';
21
+ import BlockIcon from '@mui/icons-material/Block';
22
+ import ContentCopyIcon from '@mui/icons-material/ContentCopy';
23
+ import RefreshIcon from '@mui/icons-material/Refresh';
24
+ import { api, AuthConfigStatus } from '../api/controlPanelApi';
25
+
26
+ /**
27
+ * Get the status color for the auth state
28
+ */
29
+ function getStateColor(state: string): string {
30
+ switch (state) {
31
+ case 'enabled':
32
+ return 'var(--theme-success)';
33
+ case 'error':
34
+ return 'var(--theme-error)';
35
+ case 'disabled':
36
+ default:
37
+ return 'var(--theme-text-secondary)';
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Get the status icon for the auth state
43
+ */
44
+ function getStateIcon(state: string) {
45
+ switch (state) {
46
+ case 'enabled':
47
+ return <CheckCircleIcon sx={{ color: 'var(--theme-success)' }} />;
48
+ case 'error':
49
+ return <ErrorIcon sx={{ color: 'var(--theme-error)' }} />;
50
+ case 'disabled':
51
+ default:
52
+ return <BlockIcon sx={{ color: 'var(--theme-text-secondary)' }} />;
53
+ }
54
+ }
55
+
56
+ export function AuthPage() {
57
+ const [status, setStatus] = useState<AuthConfigStatus | null>(null);
58
+ const [loading, setLoading] = useState(true);
59
+ const [error, setError] = useState<string | null>(null);
60
+ const [copied, setCopied] = useState<string | null>(null);
61
+
62
+ const fetchStatus = async () => {
63
+ setLoading(true);
64
+ setError(null);
65
+ try {
66
+ const data = await api.getAuthConfig();
67
+ setStatus(data);
68
+ } catch (err) {
69
+ setError(err instanceof Error ? err.message : 'Failed to fetch auth status');
70
+ } finally {
71
+ setLoading(false);
72
+ }
73
+ };
74
+
75
+ useEffect(() => {
76
+ fetchStatus();
77
+ }, []);
78
+
79
+ const handleCopy = (key: string, value: string) => {
80
+ navigator.clipboard.writeText(value);
81
+ setCopied(key);
82
+ setTimeout(() => setCopied(null), 2000);
83
+ };
84
+
85
+ if (loading) {
86
+ return (
87
+ <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '50vh' }}>
88
+ <CircularProgress />
89
+ </Box>
90
+ );
91
+ }
92
+
93
+ if (error) {
94
+ return (
95
+ <Card sx={{ bgcolor: 'var(--theme-surface)', border: '1px solid var(--theme-error)' }}>
96
+ <CardContent>
97
+ <Typography color="error">{error}</Typography>
98
+ </CardContent>
99
+ </Card>
100
+ );
101
+ }
102
+
103
+ const configEntries = status?.config ? Object.entries(status.config) : [];
104
+
105
+ return (
106
+ <Box>
107
+ <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
108
+ <Typography variant="h4" sx={{ color: 'var(--theme-text-primary)' }}>
109
+ Authentication
110
+ </Typography>
111
+ <Tooltip title="Refresh">
112
+ <IconButton onClick={fetchStatus} sx={{ color: 'var(--theme-text-secondary)' }}>
113
+ <RefreshIcon />
114
+ </IconButton>
115
+ </Tooltip>
116
+ </Box>
117
+ <Typography variant="body2" sx={{ mb: 4, color: 'var(--theme-text-secondary)' }}>
118
+ Auth plugin configuration status
119
+ </Typography>
120
+
121
+ {/* Status Card */}
122
+ <Card sx={{ bgcolor: 'var(--theme-surface)', mb: 3 }}>
123
+ <CardContent>
124
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
125
+ {getStateIcon(status?.state || 'disabled')}
126
+ <Box>
127
+ <Typography variant="h6" sx={{ color: 'var(--theme-text-primary)' }}>
128
+ Status:{' '}
129
+ <Chip
130
+ label={status?.state?.toUpperCase() || 'UNKNOWN'}
131
+ size="small"
132
+ sx={{
133
+ bgcolor: `${getStateColor(status?.state || 'disabled')}20`,
134
+ color: getStateColor(status?.state || 'disabled'),
135
+ fontWeight: 600,
136
+ }}
137
+ />
138
+ </Typography>
139
+ {status?.adapter && (
140
+ <Typography variant="body2" sx={{ color: 'var(--theme-text-secondary)', mt: 0.5 }}>
141
+ Adapter: <strong>{status.adapter}</strong>
142
+ </Typography>
143
+ )}
144
+ </Box>
145
+ </Box>
146
+
147
+ {/* Error Message */}
148
+ {status?.state === 'error' && status.error && (
149
+ <Alert severity="error" sx={{ mb: 2 }}>
150
+ {status.error}
151
+ </Alert>
152
+ )}
153
+
154
+ {/* Missing Variables */}
155
+ {status?.missingVars && status.missingVars.length > 0 && (
156
+ <Alert severity="warning" sx={{ mb: 2 }}>
157
+ <Typography variant="body2" sx={{ fontWeight: 600, mb: 1 }}>
158
+ Missing environment variables:
159
+ </Typography>
160
+ <Box component="ul" sx={{ m: 0, pl: 2 }}>
161
+ {status.missingVars.map((v) => (
162
+ <li key={v}>
163
+ <code>{v}</code>
164
+ </li>
165
+ ))}
166
+ </Box>
167
+ </Alert>
168
+ )}
169
+
170
+ {/* Disabled State Info */}
171
+ {status?.state === 'disabled' && (
172
+ <Alert severity="info">
173
+ <Typography variant="body2">
174
+ Authentication is disabled. Set the <code>AUTH_ADAPTER</code> environment variable to enable.
175
+ </Typography>
176
+ <Typography variant="body2" sx={{ mt: 1 }}>
177
+ Valid options: <code>supertokens</code>, <code>auth0</code>, <code>supabase</code>, <code>basic</code>
178
+ </Typography>
179
+ </Alert>
180
+ )}
181
+ </CardContent>
182
+ </Card>
183
+
184
+ {/* Configuration Table */}
185
+ {configEntries.length > 0 && (
186
+ <Card sx={{ bgcolor: 'var(--theme-surface)' }}>
187
+ <CardContent sx={{ pb: 0 }}>
188
+ <Typography variant="h6" sx={{ color: 'var(--theme-text-primary)', mb: 2 }}>
189
+ Current Configuration
190
+ </Typography>
191
+ </CardContent>
192
+ <TableContainer>
193
+ <Table size="small">
194
+ <TableHead>
195
+ <TableRow>
196
+ <TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>
197
+ Variable
198
+ </TableCell>
199
+ <TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>
200
+ Value
201
+ </TableCell>
202
+ <TableCell
203
+ sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)', width: 60 }}
204
+ >
205
+ Actions
206
+ </TableCell>
207
+ </TableRow>
208
+ </TableHead>
209
+ <TableBody>
210
+ {configEntries.map(([key, value]) => (
211
+ <TableRow key={key}>
212
+ <TableCell sx={{ borderColor: 'var(--theme-border)' }}>
213
+ <Typography sx={{ color: 'var(--theme-text-primary)', fontFamily: 'monospace', fontSize: 13 }}>
214
+ {key}
215
+ </Typography>
216
+ </TableCell>
217
+ <TableCell sx={{ borderColor: 'var(--theme-border)' }}>
218
+ <Typography
219
+ sx={{
220
+ color: value.includes('*') ? 'var(--theme-text-secondary)' : 'var(--theme-text-primary)',
221
+ fontFamily: 'monospace',
222
+ fontSize: 13,
223
+ }}
224
+ >
225
+ {value}
226
+ </Typography>
227
+ </TableCell>
228
+ <TableCell sx={{ borderColor: 'var(--theme-border)' }}>
229
+ <Tooltip title={copied === key ? 'Copied!' : 'Copy value'}>
230
+ <IconButton
231
+ size="small"
232
+ onClick={() => handleCopy(key, value)}
233
+ sx={{ color: copied === key ? 'var(--theme-success)' : 'var(--theme-text-secondary)' }}
234
+ >
235
+ <ContentCopyIcon fontSize="small" />
236
+ </IconButton>
237
+ </Tooltip>
238
+ </TableCell>
239
+ </TableRow>
240
+ ))}
241
+ </TableBody>
242
+ </Table>
243
+ </TableContainer>
244
+ </Card>
245
+ )}
246
+
247
+ {/* No Configuration */}
248
+ {status?.state === 'enabled' && configEntries.length === 0 && (
249
+ <Card sx={{ bgcolor: 'var(--theme-surface)' }}>
250
+ <CardContent>
251
+ <Typography sx={{ color: 'var(--theme-text-secondary)', textAlign: 'center' }}>
252
+ No configuration details available
253
+ </Typography>
254
+ </CardContent>
255
+ </Card>
256
+ )}
257
+ </Box>
258
+ );
259
+ }
@@ -0,0 +1,394 @@
1
+ import { useState, useEffect } from 'react';
2
+ import {
3
+ Box,
4
+ Card,
5
+ CardContent,
6
+ Typography,
7
+ CircularProgress,
8
+ Chip,
9
+ Table,
10
+ TableBody,
11
+ TableCell,
12
+ TableContainer,
13
+ TableHead,
14
+ TableRow,
15
+ Collapse,
16
+ IconButton,
17
+ Alert,
18
+ } from '@mui/material';
19
+ import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
20
+ import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
21
+ import ExtensionIcon from '@mui/icons-material/Extension';
22
+ import CheckCircleIcon from '@mui/icons-material/CheckCircle';
23
+ import ErrorIcon from '@mui/icons-material/Error';
24
+ import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty';
25
+ import StopCircleIcon from '@mui/icons-material/StopCircle';
26
+ import { api, PluginInfo, PluginContributions } from '../api/controlPanelApi';
27
+
28
+ function getStatusIcon(status: PluginInfo['status']) {
29
+ switch (status) {
30
+ case 'active':
31
+ return <CheckCircleIcon sx={{ color: 'var(--theme-success)', fontSize: 20 }} />;
32
+ case 'error':
33
+ return <ErrorIcon sx={{ color: 'var(--theme-error)', fontSize: 20 }} />;
34
+ case 'starting':
35
+ return <HourglassEmptyIcon sx={{ color: 'var(--theme-warning)', fontSize: 20 }} />;
36
+ case 'stopped':
37
+ return <StopCircleIcon sx={{ color: 'var(--theme-text-secondary)', fontSize: 20 }} />;
38
+ default:
39
+ return null;
40
+ }
41
+ }
42
+
43
+ function getStatusColor(status: PluginInfo['status']): string {
44
+ switch (status) {
45
+ case 'active':
46
+ return 'var(--theme-success)';
47
+ case 'error':
48
+ return 'var(--theme-error)';
49
+ case 'starting':
50
+ return 'var(--theme-warning)';
51
+ case 'stopped':
52
+ return 'var(--theme-text-secondary)';
53
+ default:
54
+ return 'var(--theme-text-secondary)';
55
+ }
56
+ }
57
+
58
+ function ContributionsSummary({ counts }: { counts: PluginInfo['contributionCounts'] }) {
59
+ const parts: string[] = [];
60
+ if (counts.routes > 0) parts.push(`${counts.routes} route${counts.routes > 1 ? 's' : ''}`);
61
+ if (counts.menuItems > 0) parts.push(`${counts.menuItems} menu item${counts.menuItems > 1 ? 's' : ''}`);
62
+ if (counts.pages > 0) parts.push(`${counts.pages} page${counts.pages > 1 ? 's' : ''}`);
63
+ if (counts.widgets > 0) parts.push(`${counts.widgets} widget${counts.widgets > 1 ? 's' : ''}`);
64
+
65
+ if (parts.length === 0) {
66
+ return <Typography sx={{ color: 'var(--theme-text-secondary)', fontSize: '0.875rem' }}>No contributions</Typography>;
67
+ }
68
+
69
+ return (
70
+ <Typography sx={{ color: 'var(--theme-text-secondary)', fontSize: '0.875rem' }}>
71
+ {parts.join(', ')}
72
+ </Typography>
73
+ );
74
+ }
75
+
76
+ interface PluginRowProps {
77
+ plugin: PluginInfo;
78
+ }
79
+
80
+ function PluginRow({ plugin }: PluginRowProps) {
81
+ const [open, setOpen] = useState(false);
82
+ const [contributions, setContributions] = useState<PluginContributions | null>(null);
83
+ const [loading, setLoading] = useState(false);
84
+ const [detailError, setDetailError] = useState<string | null>(null);
85
+
86
+ const handleToggle = async () => {
87
+ if (!open && !contributions && !detailError) {
88
+ setLoading(true);
89
+ setDetailError(null);
90
+ try {
91
+ const detail = await api.getPluginDetail(plugin.id);
92
+ setContributions(detail.contributions);
93
+ } catch (err) {
94
+ console.error('Failed to load plugin details:', err);
95
+ setDetailError(err instanceof Error ? err.message : 'Failed to load details');
96
+ } finally {
97
+ setLoading(false);
98
+ }
99
+ }
100
+ setOpen(!open);
101
+ };
102
+
103
+ const hasContributions =
104
+ plugin.contributionCounts.routes > 0 ||
105
+ plugin.contributionCounts.menuItems > 0 ||
106
+ plugin.contributionCounts.pages > 0 ||
107
+ plugin.contributionCounts.widgets > 0;
108
+
109
+ return (
110
+ <>
111
+ <TableRow
112
+ sx={{
113
+ '& > *': { borderBottom: open ? 'none' : undefined },
114
+ cursor: hasContributions ? 'pointer' : 'default',
115
+ '&:hover': { bgcolor: hasContributions ? 'var(--theme-background)' : undefined },
116
+ }}
117
+ onClick={hasContributions ? handleToggle : undefined}
118
+ >
119
+ <TableCell sx={{ width: 50, borderColor: 'var(--theme-border)' }}>
120
+ {hasContributions && (
121
+ <IconButton size="small" sx={{ color: 'var(--theme-text-secondary)' }}>
122
+ {open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
123
+ </IconButton>
124
+ )}
125
+ </TableCell>
126
+ <TableCell sx={{ borderColor: 'var(--theme-border)' }}>
127
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
128
+ {getStatusIcon(plugin.status)}
129
+ <Typography sx={{ color: 'var(--theme-text-primary)', fontWeight: 500 }}>
130
+ {plugin.name}
131
+ </Typography>
132
+ </Box>
133
+ </TableCell>
134
+ <TableCell sx={{ borderColor: 'var(--theme-border)' }}>
135
+ <Typography sx={{ color: 'var(--theme-text-secondary)', fontFamily: 'monospace', fontSize: '0.875rem' }}>
136
+ {plugin.id}
137
+ </Typography>
138
+ </TableCell>
139
+ <TableCell sx={{ borderColor: 'var(--theme-border)' }}>
140
+ {plugin.version ? (
141
+ <Chip
142
+ label={`v${plugin.version}`}
143
+ size="small"
144
+ sx={{ bgcolor: 'var(--theme-background)', color: 'var(--theme-text-primary)' }}
145
+ />
146
+ ) : (
147
+ <Typography sx={{ color: 'var(--theme-text-secondary)' }}>-</Typography>
148
+ )}
149
+ </TableCell>
150
+ <TableCell sx={{ borderColor: 'var(--theme-border)' }}>
151
+ <Chip
152
+ label={plugin.status}
153
+ size="small"
154
+ sx={{
155
+ bgcolor: getStatusColor(plugin.status) + '20',
156
+ color: getStatusColor(plugin.status),
157
+ textTransform: 'capitalize',
158
+ }}
159
+ />
160
+ </TableCell>
161
+ <TableCell sx={{ borderColor: 'var(--theme-border)' }}>
162
+ <ContributionsSummary counts={plugin.contributionCounts} />
163
+ </TableCell>
164
+ </TableRow>
165
+
166
+ {/* Expanded details row */}
167
+ <TableRow>
168
+ <TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={6} sx={{ borderColor: 'var(--theme-border)' }}>
169
+ <Collapse in={open} timeout="auto" unmountOnExit>
170
+ <Box sx={{ py: 2, px: 4 }}>
171
+ {loading ? (
172
+ <Box sx={{ display: 'flex', justifyContent: 'center', py: 2 }}>
173
+ <CircularProgress size={24} />
174
+ </Box>
175
+ ) : detailError ? (
176
+ <Alert severity="error" sx={{ mb: 1 }}>
177
+ {detailError}
178
+ </Alert>
179
+ ) : contributions ? (
180
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
181
+ {/* Error message if plugin failed */}
182
+ {plugin.status === 'error' && plugin.error && (
183
+ <Alert severity="error" sx={{ mb: 1 }}>
184
+ {plugin.error}
185
+ </Alert>
186
+ )}
187
+
188
+ {/* Routes */}
189
+ {contributions.routes.length > 0 && (
190
+ <Box>
191
+ <Typography variant="subtitle2" sx={{ color: 'var(--theme-text-primary)', mb: 1 }}>
192
+ Routes ({contributions.routes.length})
193
+ </Typography>
194
+ <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
195
+ {contributions.routes.map((route, i) => (
196
+ <Chip
197
+ key={i}
198
+ label={`${route.method.toUpperCase()} ${route.path}`}
199
+ size="small"
200
+ sx={{
201
+ bgcolor: 'var(--theme-background)',
202
+ color: 'var(--theme-text-primary)',
203
+ fontFamily: 'monospace',
204
+ fontSize: '0.75rem',
205
+ }}
206
+ />
207
+ ))}
208
+ </Box>
209
+ </Box>
210
+ )}
211
+
212
+ {/* Menu Items */}
213
+ {contributions.menuItems.length > 0 && (
214
+ <Box>
215
+ <Typography variant="subtitle2" sx={{ color: 'var(--theme-text-primary)', mb: 1 }}>
216
+ Menu Items ({contributions.menuItems.length})
217
+ </Typography>
218
+ <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
219
+ {contributions.menuItems.map((item) => (
220
+ <Chip
221
+ key={item.id}
222
+ label={`${item.label} (${item.route})`}
223
+ size="small"
224
+ sx={{ bgcolor: 'var(--theme-background)', color: 'var(--theme-text-primary)' }}
225
+ />
226
+ ))}
227
+ </Box>
228
+ </Box>
229
+ )}
230
+
231
+ {/* Pages */}
232
+ {contributions.pages.length > 0 && (
233
+ <Box>
234
+ <Typography variant="subtitle2" sx={{ color: 'var(--theme-text-primary)', mb: 1 }}>
235
+ Pages ({contributions.pages.length})
236
+ </Typography>
237
+ <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
238
+ {contributions.pages.map((page) => (
239
+ <Chip
240
+ key={page.id}
241
+ label={`${page.title || page.id} (${page.route})`}
242
+ size="small"
243
+ sx={{ bgcolor: 'var(--theme-background)', color: 'var(--theme-text-primary)' }}
244
+ />
245
+ ))}
246
+ </Box>
247
+ </Box>
248
+ )}
249
+
250
+ {/* Widgets */}
251
+ {contributions.widgets.length > 0 && (
252
+ <Box>
253
+ <Typography variant="subtitle2" sx={{ color: 'var(--theme-text-primary)', mb: 1 }}>
254
+ Widgets ({contributions.widgets.length})
255
+ </Typography>
256
+ <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
257
+ {contributions.widgets.map((widget) => (
258
+ <Chip
259
+ key={widget.id}
260
+ label={widget.title}
261
+ size="small"
262
+ sx={{ bgcolor: 'var(--theme-background)', color: 'var(--theme-text-primary)' }}
263
+ />
264
+ ))}
265
+ </Box>
266
+ </Box>
267
+ )}
268
+ </Box>
269
+ ) : null}
270
+ </Box>
271
+ </Collapse>
272
+ </TableCell>
273
+ </TableRow>
274
+ </>
275
+ );
276
+ }
277
+
278
+ export function PluginsPage() {
279
+ const [plugins, setPlugins] = useState<PluginInfo[]>([]);
280
+ const [loading, setLoading] = useState(true);
281
+ const [error, setError] = useState<string | null>(null);
282
+
283
+ useEffect(() => {
284
+ const fetchPlugins = async () => {
285
+ try {
286
+ const data = await api.getPlugins();
287
+ setPlugins(data.plugins);
288
+ setError(null);
289
+ } catch (err) {
290
+ setError(err instanceof Error ? err.message : 'Failed to fetch plugins');
291
+ } finally {
292
+ setLoading(false);
293
+ }
294
+ };
295
+
296
+ fetchPlugins();
297
+ }, []);
298
+
299
+ if (loading) {
300
+ return (
301
+ <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '50vh' }}>
302
+ <CircularProgress />
303
+ </Box>
304
+ );
305
+ }
306
+
307
+ if (error) {
308
+ return (
309
+ <Card sx={{ bgcolor: 'var(--theme-surface)', border: '1px solid var(--theme-error)' }}>
310
+ <CardContent>
311
+ <Typography color="error">{error}</Typography>
312
+ </CardContent>
313
+ </Card>
314
+ );
315
+ }
316
+
317
+ // Count plugins by status
318
+ const activeCount = plugins.filter((p) => p.status === 'active').length;
319
+ const errorCount = plugins.filter((p) => p.status === 'error').length;
320
+
321
+ return (
322
+ <Box>
323
+ <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
324
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
325
+ <Typography variant="h4" sx={{ color: 'var(--theme-text-primary)' }}>
326
+ Plugins
327
+ </Typography>
328
+ <Chip
329
+ icon={<ExtensionIcon sx={{ fontSize: 16 }} />}
330
+ label={`${activeCount}/${plugins.length} active`}
331
+ size="small"
332
+ sx={{
333
+ bgcolor: activeCount === plugins.length ? 'var(--theme-success)20' : 'var(--theme-warning)20',
334
+ color: activeCount === plugins.length ? 'var(--theme-success)' : 'var(--theme-warning)',
335
+ }}
336
+ />
337
+ {errorCount > 0 && (
338
+ <Chip
339
+ icon={<ErrorIcon sx={{ fontSize: 16 }} />}
340
+ label={`${errorCount} error${errorCount > 1 ? 's' : ''}`}
341
+ size="small"
342
+ sx={{ bgcolor: 'var(--theme-error)20', color: 'var(--theme-error)' }}
343
+ />
344
+ )}
345
+ </Box>
346
+ </Box>
347
+ <Typography variant="body2" sx={{ mb: 4, color: 'var(--theme-text-secondary)' }}>
348
+ View registered plugins and their contributions to the control panel
349
+ </Typography>
350
+
351
+ {plugins.length === 0 ? (
352
+ <Card sx={{ bgcolor: 'var(--theme-surface)' }}>
353
+ <CardContent>
354
+ <Typography sx={{ color: 'var(--theme-text-secondary)', textAlign: 'center', py: 4 }}>
355
+ No plugins registered
356
+ </Typography>
357
+ </CardContent>
358
+ </Card>
359
+ ) : (
360
+ <Card sx={{ bgcolor: 'var(--theme-surface)' }}>
361
+ <TableContainer>
362
+ <Table>
363
+ <TableHead>
364
+ <TableRow>
365
+ <TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)', width: 50 }} />
366
+ <TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>
367
+ Name
368
+ </TableCell>
369
+ <TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>
370
+ ID
371
+ </TableCell>
372
+ <TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>
373
+ Version
374
+ </TableCell>
375
+ <TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>
376
+ Status
377
+ </TableCell>
378
+ <TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>
379
+ Contributions
380
+ </TableCell>
381
+ </TableRow>
382
+ </TableHead>
383
+ <TableBody>
384
+ {plugins.map((plugin) => (
385
+ <PluginRow key={plugin.id} plugin={plugin} />
386
+ ))}
387
+ </TableBody>
388
+ </Table>
389
+ </TableContainer>
390
+ </Card>
391
+ )}
392
+ </Box>
393
+ );
394
+ }
@@ -28,6 +28,11 @@ export default defineConfig({
28
28
  external: [
29
29
  'react',
30
30
  'react-dom',
31
+ 'react/jsx-runtime',
32
+ 'react/jsx-dev-runtime',
33
+ 'react-is',
34
+ 'prop-types',
35
+ 'hoist-non-react-statics',
31
36
  'react-router-dom',
32
37
  '@mui/material',
33
38
  '@mui/icons-material',