@gntik-ai/templates 0.0.0-stage → 0.5.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.
Files changed (300) hide show
  1. package/dist/index.d.ts +2420 -0
  2. package/dist/index.js +14520 -0
  3. package/dist/index.js.map +1 -0
  4. package/package.json +64 -3
  5. package/src/accept-invitation/Page.test.tsx +40 -0
  6. package/src/accept-invitation/Page.tsx +153 -0
  7. package/src/accept-invitation/data.ts +22 -0
  8. package/src/accept-invitation/template.meta.ts +12 -0
  9. package/src/assistant-chat/Page.test.tsx +51 -0
  10. package/src/assistant-chat/Page.tsx +273 -0
  11. package/src/assistant-chat/data.ts +71 -0
  12. package/src/assistant-chat/template.meta.ts +12 -0
  13. package/src/audit-log/Page.test.tsx +58 -0
  14. package/src/audit-log/Page.tsx +284 -0
  15. package/src/audit-log/data.ts +133 -0
  16. package/src/audit-log/template.meta.ts +12 -0
  17. package/src/board/Lane.tsx +117 -0
  18. package/src/board/Page.test.tsx +68 -0
  19. package/src/board/Page.tsx +178 -0
  20. package/src/board/data.ts +127 -0
  21. package/src/board/template.meta.ts +12 -0
  22. package/src/board/useBoard.ts +146 -0
  23. package/src/changelog/Page.test.tsx +31 -0
  24. package/src/changelog/Page.tsx +174 -0
  25. package/src/changelog/data.ts +105 -0
  26. package/src/changelog/template.meta.ts +12 -0
  27. package/src/code-editor/Page.test.tsx +83 -0
  28. package/src/code-editor/Page.tsx +207 -0
  29. package/src/code-editor/data.ts +130 -0
  30. package/src/code-editor/template.meta.ts +12 -0
  31. package/src/contact/Page.test.tsx +33 -0
  32. package/src/contact/Page.tsx +272 -0
  33. package/src/contact/PublicFrame.tsx +84 -0
  34. package/src/contact/data.ts +129 -0
  35. package/src/contact/template.meta.ts +12 -0
  36. package/src/create-wizard/Overlay.test.tsx +68 -0
  37. package/src/create-wizard/Page.test.tsx +57 -0
  38. package/src/create-wizard/Page.tsx +246 -0
  39. package/src/create-wizard/data.ts +139 -0
  40. package/src/create-wizard/examples/Overlay.tsx +60 -0
  41. package/src/create-wizard/template.meta.ts +12 -0
  42. package/src/create-workspace/Page.test.tsx +41 -0
  43. package/src/create-workspace/Page.tsx +227 -0
  44. package/src/create-workspace/data.ts +61 -0
  45. package/src/create-workspace/template.meta.ts +12 -0
  46. package/src/data-explorer/Page.test.tsx +45 -0
  47. package/src/data-explorer/Page.tsx +247 -0
  48. package/src/data-explorer/data.ts +94 -0
  49. package/src/data-explorer/template.meta.ts +12 -0
  50. package/src/docs-article/ArticleFeedback.tsx +75 -0
  51. package/src/docs-article/Page.test.tsx +34 -0
  52. package/src/docs-article/Page.tsx +200 -0
  53. package/src/docs-article/data.ts +65 -0
  54. package/src/docs-article/template.meta.ts +12 -0
  55. package/src/evaluations/Page.test.tsx +40 -0
  56. package/src/evaluations/Page.tsx +238 -0
  57. package/src/evaluations/data.ts +194 -0
  58. package/src/evaluations/template.meta.ts +12 -0
  59. package/src/forgot-password/Page.test.tsx +199 -0
  60. package/src/forgot-password/Page.tsx +66 -0
  61. package/src/forgot-password/data.ts +5 -0
  62. package/src/forgot-password/examples/UsernameOrEmail.tsx +25 -0
  63. package/src/forgot-password/template.meta.ts +12 -0
  64. package/src/getting-started/Page.test.tsx +27 -0
  65. package/src/getting-started/Page.tsx +125 -0
  66. package/src/getting-started/data.ts +67 -0
  67. package/src/getting-started/template.meta.ts +12 -0
  68. package/src/help-home/Page.test.tsx +33 -0
  69. package/src/help-home/Page.tsx +159 -0
  70. package/src/help-home/data.ts +151 -0
  71. package/src/help-home/template.meta.ts +12 -0
  72. package/src/home-dashboard/Page.test.tsx +57 -0
  73. package/src/home-dashboard/Page.tsx +152 -0
  74. package/src/home-dashboard/data.ts +66 -0
  75. package/src/home-dashboard/template.meta.ts +12 -0
  76. package/src/incident-board/Page.test.tsx +77 -0
  77. package/src/incident-board/Page.tsx +263 -0
  78. package/src/incident-board/data.ts +220 -0
  79. package/src/incident-board/template.meta.ts +12 -0
  80. package/src/incident-console/Page.test.tsx +49 -0
  81. package/src/incident-console/Page.tsx +274 -0
  82. package/src/incident-console/data.ts +176 -0
  83. package/src/incident-console/template.meta.ts +12 -0
  84. package/src/index.ts +250 -0
  85. package/src/keyboard-shortcuts/Page.test.tsx +45 -0
  86. package/src/keyboard-shortcuts/Page.tsx +94 -0
  87. package/src/keyboard-shortcuts/ShortcutSheet.tsx +80 -0
  88. package/src/keyboard-shortcuts/data.ts +84 -0
  89. package/src/keyboard-shortcuts/template.meta.ts +12 -0
  90. package/src/landing/Page.test.tsx +27 -0
  91. package/src/landing/Page.tsx +110 -0
  92. package/src/landing/data.ts +17 -0
  93. package/src/landing/template.meta.ts +11 -0
  94. package/src/live-event-stream/Page.test.tsx +251 -0
  95. package/src/live-event-stream/Page.tsx +307 -0
  96. package/src/live-event-stream/data.ts +44 -0
  97. package/src/live-event-stream/examples/Connecting.tsx +5 -0
  98. package/src/live-event-stream/examples/Error.tsx +5 -0
  99. package/src/live-event-stream/examples/LiveEmpty.tsx +5 -0
  100. package/src/live-event-stream/examples/LiveWithEvents.tsx +6 -0
  101. package/src/live-event-stream/examples/Stopped.tsx +6 -0
  102. package/src/live-event-stream/template.meta.ts +74 -0
  103. package/src/messaging/Page.test.tsx +34 -0
  104. package/src/messaging/Page.tsx +236 -0
  105. package/src/messaging/data.ts +118 -0
  106. package/src/messaging/template.meta.ts +12 -0
  107. package/src/meta.ts +24 -0
  108. package/src/mfa/Page.test.tsx +22 -0
  109. package/src/mfa/Page.tsx +45 -0
  110. package/src/mfa/data.ts +6 -0
  111. package/src/mfa/template.meta.ts +12 -0
  112. package/src/model-providers/Page.test.tsx +58 -0
  113. package/src/model-providers/Page.tsx +198 -0
  114. package/src/model-providers/data.ts +101 -0
  115. package/src/model-providers/template.meta.ts +12 -0
  116. package/src/notification-inbox/Page.test.tsx +49 -0
  117. package/src/notification-inbox/Page.tsx +237 -0
  118. package/src/notification-inbox/data.ts +129 -0
  119. package/src/notification-inbox/template.meta.ts +12 -0
  120. package/src/pending-activation/Page.test.tsx +390 -0
  121. package/src/pending-activation/Page.tsx +145 -0
  122. package/src/pending-activation/data.ts +25 -0
  123. package/src/pending-activation/template.meta.ts +12 -0
  124. package/src/pricing/ComparisonTable.tsx +75 -0
  125. package/src/pricing/Page.test.tsx +29 -0
  126. package/src/pricing/Page.tsx +83 -0
  127. package/src/pricing/PublicFrame.tsx +84 -0
  128. package/src/pricing/data.ts +104 -0
  129. package/src/pricing/template.meta.ts +12 -0
  130. package/src/prompt-playground/Page.test.tsx +72 -0
  131. package/src/prompt-playground/Page.tsx +254 -0
  132. package/src/prompt-playground/data.ts +68 -0
  133. package/src/prompt-playground/template.meta.ts +12 -0
  134. package/src/public-hub/Page.test.tsx +176 -0
  135. package/src/public-hub/Page.tsx +125 -0
  136. package/src/public-hub/Source.test.ts +54 -0
  137. package/src/public-hub/data.ts +24 -0
  138. package/src/public-hub/examples/Dark.tsx +11 -0
  139. package/src/public-hub/examples/Full.tsx +6 -0
  140. package/src/public-hub/examples/HighContrast.tsx +11 -0
  141. package/src/public-hub/examples/Light.tsx +11 -0
  142. package/src/public-hub/examples/Loading.tsx +6 -0
  143. package/src/public-hub/examples/Minimal.tsx +5 -0
  144. package/src/public-hub/examples/MinimalLoading.tsx +5 -0
  145. package/src/public-hub/template.meta.ts +60 -0
  146. package/src/reset-password/Page.test.tsx +27 -0
  147. package/src/reset-password/Page.tsx +46 -0
  148. package/src/reset-password/data.ts +5 -0
  149. package/src/reset-password/template.meta.ts +12 -0
  150. package/src/resource-detail/Page.test.tsx +260 -0
  151. package/src/resource-detail/Page.tsx +262 -0
  152. package/src/resource-detail/data.ts +55 -0
  153. package/src/resource-detail/examples/postgres-table-detail.tsx +28 -0
  154. package/src/resource-detail/examples/realtime-configuration.tsx +26 -0
  155. package/src/resource-detail/examples/runtime-with-transition.tsx +30 -0
  156. package/src/resource-detail/examples/runtime-without-transition.tsx +33 -0
  157. package/src/resource-detail/examples/shell.ts +12 -0
  158. package/src/resource-detail/template.meta.ts +21 -0
  159. package/src/resource-form/Page.test.tsx +40 -0
  160. package/src/resource-form/Page.tsx +186 -0
  161. package/src/resource-form/data.ts +63 -0
  162. package/src/resource-form/template.meta.ts +12 -0
  163. package/src/resource-gallery/Page.test.tsx +42 -0
  164. package/src/resource-gallery/Page.tsx +125 -0
  165. package/src/resource-gallery/data.ts +44 -0
  166. package/src/resource-gallery/template.meta.ts +20 -0
  167. package/src/resource-index/Page.test.tsx +120 -0
  168. package/src/resource-index/Page.tsx +216 -0
  169. package/src/resource-index/data.ts +84 -0
  170. package/src/resource-index/template.meta.ts +21 -0
  171. package/src/resource-tree/Page.test.tsx +31 -0
  172. package/src/resource-tree/Page.tsx +232 -0
  173. package/src/resource-tree/data.ts +180 -0
  174. package/src/resource-tree/template.meta.ts +12 -0
  175. package/src/run-trace/Page.test.tsx +56 -0
  176. package/src/run-trace/Page.tsx +242 -0
  177. package/src/run-trace/data.ts +171 -0
  178. package/src/run-trace/template.meta.ts +12 -0
  179. package/src/scorecard/Page.test.tsx +38 -0
  180. package/src/scorecard/Page.tsx +160 -0
  181. package/src/scorecard/TargetsTable.tsx +92 -0
  182. package/src/scorecard/data.ts +118 -0
  183. package/src/scorecard/template.meta.ts +12 -0
  184. package/src/search-results/Page.test.tsx +43 -0
  185. package/src/search-results/Page.tsx +243 -0
  186. package/src/search-results/data.ts +130 -0
  187. package/src/search-results/template.meta.ts +12 -0
  188. package/src/settings-api-keys/Page.test.tsx +39 -0
  189. package/src/settings-api-keys/Page.tsx +171 -0
  190. package/src/settings-api-keys/data.ts +58 -0
  191. package/src/settings-api-keys/template.meta.ts +12 -0
  192. package/src/settings-appearance/Page.test.tsx +44 -0
  193. package/src/settings-appearance/Page.tsx +92 -0
  194. package/src/settings-appearance/data.ts +21 -0
  195. package/src/settings-appearance/template.meta.ts +11 -0
  196. package/src/settings-billing/Page.test.tsx +31 -0
  197. package/src/settings-billing/Page.tsx +71 -0
  198. package/src/settings-billing/data.ts +15 -0
  199. package/src/settings-billing/template.meta.ts +19 -0
  200. package/src/settings-integrations/Page.test.tsx +31 -0
  201. package/src/settings-integrations/Page.tsx +185 -0
  202. package/src/settings-integrations/data.ts +116 -0
  203. package/src/settings-integrations/template.meta.ts +11 -0
  204. package/src/settings-members/Page.test.tsx +32 -0
  205. package/src/settings-members/Page.tsx +140 -0
  206. package/src/settings-members/data.ts +28 -0
  207. package/src/settings-members/template.meta.ts +12 -0
  208. package/src/settings-notifications/Page.test.tsx +31 -0
  209. package/src/settings-notifications/Page.tsx +71 -0
  210. package/src/settings-notifications/data.ts +12 -0
  211. package/src/settings-notifications/template.meta.ts +11 -0
  212. package/src/settings-profile/Baseline.test.tsx +9 -0
  213. package/src/settings-profile/Editable.test.tsx +21 -0
  214. package/src/settings-profile/Page.test.tsx +41 -0
  215. package/src/settings-profile/Page.tsx +133 -0
  216. package/src/settings-profile/ReadOnly.test.tsx +143 -0
  217. package/src/settings-profile/ReadOnlyProfile.tsx +85 -0
  218. package/src/settings-profile/SettingsFrame.test.tsx +55 -0
  219. package/src/settings-profile/SettingsFrame.tsx +88 -0
  220. package/src/settings-profile/__tests__/dom.ts +8 -0
  221. package/src/settings-profile/data.ts +41 -0
  222. package/src/settings-profile/examples/ReadOnly.tsx +31 -0
  223. package/src/settings-profile/fixtures/editable.snap +1 -0
  224. package/src/settings-profile/settings-nav.ts +98 -0
  225. package/src/settings-profile/template.meta.ts +21 -0
  226. package/src/settings-security/MfaSetupDialog.tsx +96 -0
  227. package/src/settings-security/Page.test.tsx +44 -0
  228. package/src/settings-security/Page.tsx +158 -0
  229. package/src/settings-security/data.ts +11 -0
  230. package/src/settings-security/template.meta.ts +11 -0
  231. package/src/settings-workspace/Page.test.tsx +44 -0
  232. package/src/settings-workspace/Page.tsx +120 -0
  233. package/src/settings-workspace/data.ts +44 -0
  234. package/src/settings-workspace/template.meta.ts +19 -0
  235. package/src/shared/ConsoleShell.test.tsx +81 -0
  236. package/src/shared/ConsoleShell.tsx +98 -0
  237. package/src/shell-sidebar/Page.test.tsx +21 -0
  238. package/src/shell-sidebar/Page.tsx +85 -0
  239. package/src/shell-sidebar/data.ts +24 -0
  240. package/src/shell-sidebar/template.meta.ts +12 -0
  241. package/src/shell-topnav/Page.test.tsx +25 -0
  242. package/src/shell-topnav/Page.tsx +94 -0
  243. package/src/shell-topnav/data.ts +25 -0
  244. package/src/shell-topnav/template.meta.ts +12 -0
  245. package/src/sign-in/BrandPanel.tsx +34 -0
  246. package/src/sign-in/Page.test.tsx +41 -0
  247. package/src/sign-in/Page.tsx +73 -0
  248. package/src/sign-in/data.ts +11 -0
  249. package/src/sign-in/template.meta.ts +12 -0
  250. package/src/sign-in-card/Page.test.tsx +182 -0
  251. package/src/sign-in-card/Page.tsx +82 -0
  252. package/src/sign-in-card/data.ts +8 -0
  253. package/src/sign-in-card/examples/Username.tsx +28 -0
  254. package/src/sign-in-card/template.meta.ts +12 -0
  255. package/src/sign-up/Page.test.tsx +33 -0
  256. package/src/sign-up/Page.tsx +70 -0
  257. package/src/sign-up/data.ts +15 -0
  258. package/src/sign-up/template.meta.ts +12 -0
  259. package/src/sso-login/Page.test.tsx +41 -0
  260. package/src/sso-login/Page.tsx +151 -0
  261. package/src/sso-login/data.ts +10 -0
  262. package/src/sso-login/template.meta.ts +12 -0
  263. package/src/status-403/Page.test.tsx +23 -0
  264. package/src/status-403/Page.tsx +92 -0
  265. package/src/status-403/data.ts +13 -0
  266. package/src/status-403/template.meta.ts +12 -0
  267. package/src/status-404/Page.test.tsx +23 -0
  268. package/src/status-404/Page.tsx +60 -0
  269. package/src/status-404/PageHeading.test.tsx +52 -0
  270. package/src/status-404/data.ts +12 -0
  271. package/src/status-404/examples/PageHeader.tsx +28 -0
  272. package/src/status-404/template.meta.ts +12 -0
  273. package/src/status-500/Page.test.tsx +24 -0
  274. package/src/status-500/Page.tsx +70 -0
  275. package/src/status-500/data.ts +20 -0
  276. package/src/status-500/template.meta.ts +12 -0
  277. package/src/status-maintenance/Page.test.tsx +24 -0
  278. package/src/status-maintenance/Page.tsx +84 -0
  279. package/src/status-maintenance/data.ts +14 -0
  280. package/src/status-maintenance/template.meta.ts +12 -0
  281. package/src/styles.css +11 -0
  282. package/src/test/a11y.ts +18 -0
  283. package/src/test/setup.ts +36 -0
  284. package/src/upgrade-required/Page.test.tsx +28 -0
  285. package/src/upgrade-required/Page.tsx +79 -0
  286. package/src/upgrade-required/data.ts +19 -0
  287. package/src/upgrade-required/template.meta.ts +12 -0
  288. package/src/usage-analytics/Page.test.tsx +83 -0
  289. package/src/usage-analytics/Page.tsx +184 -0
  290. package/src/usage-analytics/data.ts +161 -0
  291. package/src/usage-analytics/template.meta.ts +21 -0
  292. package/src/verify-email/Page.test.tsx +32 -0
  293. package/src/verify-email/Page.tsx +118 -0
  294. package/src/verify-email/data.ts +7 -0
  295. package/src/verify-email/template.meta.ts +12 -0
  296. package/src/workflow-builder/Page.test.tsx +97 -0
  297. package/src/workflow-builder/Page.tsx +163 -0
  298. package/src/workflow-builder/data.ts +13 -0
  299. package/src/workflow-builder/template.meta.ts +12 -0
  300. package/README.md +0 -3
@@ -0,0 +1,71 @@
1
+ import type { CitationSource, Suggestion } from '@gntik-ai/chat';
2
+ import type { BreadcrumbItem } from '@gntik-ai/ui';
3
+
4
+ /** Mock tool call the assistant runs before answering. */
5
+ export interface AssistantTool {
6
+ name: string;
7
+ args: unknown;
8
+ result: unknown;
9
+ durationMs: number;
10
+ }
11
+
12
+ export const assistantSuggestions: Suggestion[] = [
13
+ { label: 'Summarise this week’s deployments', description: 'What shipped across projects and what rolled back' },
14
+ { label: 'Which invoices are still open?', description: 'Unpaid invoices from last month' },
15
+ { label: 'Who has admin access?', description: 'Members with an admin role' },
16
+ { label: 'Why did the last build fail?', description: 'Likely cause and a fix' },
17
+ ];
18
+
19
+ export const assistantSources: CitationSource[] = [
20
+ {
21
+ title: 'Deployment history · web-app',
22
+ url: 'https://app.example.com/projects/web-app/deployments',
23
+ snippet: '14 deployments in the last 7 days, 1 rolled back.',
24
+ },
25
+ {
26
+ title: 'Rollback guide',
27
+ url: 'https://docs.example.com/deployments/rollback',
28
+ snippet: 'Roll back from the Deployments page or with the CLI.',
29
+ },
30
+ {
31
+ title: 'Health checks',
32
+ url: 'https://docs.example.com/deployments/health-checks',
33
+ snippet: 'A deployment is promoted only after every region reports healthy.',
34
+ },
35
+ ];
36
+
37
+ export const assistantTool: AssistantTool = {
38
+ name: 'list_deployments',
39
+ args: { project: 'web-app', since: '7d' },
40
+ result: { count: 14, succeeded: 13, rolledBack: 1, latest: 'v3.2.0' },
41
+ durationMs: 600,
42
+ };
43
+
44
+ export const assistantReply = `This week **web-app** shipped 14 deployments:
45
+
46
+ | Status | Count |
47
+ | --- | --- |
48
+ | Succeeded | 13 |
49
+ | Rolled back | 1 |
50
+
51
+ The rollback was \`v3.1.4\`: its health check failed in one region, so traffic stayed on the previous build.
52
+
53
+ To pin the current release from the CLI:
54
+
55
+ \`\`\`bash
56
+ deploy pin --project web-app --version v3.2.0
57
+ \`\`\`
58
+
59
+ The [rollback guide](https://docs.example.com/deployments/rollback) has the full steps.`;
60
+
61
+ export const assistantBreadcrumbs: BreadcrumbItem[] = [
62
+ { label: 'Acme Industries', href: '/overview' },
63
+ { label: 'Assistant' },
64
+ ];
65
+
66
+ /** Splits text into small chunks to imitate token streaming. */
67
+ export function chunkReply(text: string, size = 6): string[] {
68
+ const out: string[] = [];
69
+ for (let i = 0; i < text.length; i += size) out.push(text.slice(i, i + size));
70
+ return out;
71
+ }
@@ -0,0 +1,12 @@
1
+ import type { TemplateMeta } from '../meta';
2
+
3
+ export const meta: TemplateMeta = {
4
+ name: 'Assistant chat',
5
+ family: 'AI',
6
+ priority: 'P3',
7
+ status: 'experimental',
8
+ description:
9
+ 'Workspace assistant in the console: @gntik-ai/chat ChatLayout with suggestions, ChatMessage turns, a ToolCallCard, a mock-streamed markdown reply with Citations, Stop / Retry / feedback and a ChatComposer; a details panel lists the context sources. Streaming timers are cleared on Stop and unmount.',
10
+ layout: 'SidebarLayout · Page · ChatLayout',
11
+ blocks: ['app-sidebar', 'app-topbar', 'page-header'],
12
+ };
@@ -0,0 +1,58 @@
1
+ import { render, screen, within } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { expectNoAxeViolations } from '../test/a11y';
4
+ import AuditLogPage, { auditEventsToCsv, filterAuditEvents } from './Page';
5
+ import { auditEvents } from './data';
6
+
7
+ const ALL = { start: null, end: null };
8
+
9
+ describe('AuditLogPage', () => {
10
+ it('renders filters, date range and the events table', { timeout: 15000 }, async () => {
11
+ render(<AuditLogPage />);
12
+ expect(screen.getByRole('main')).toBeInTheDocument();
13
+ expect(screen.getByRole('heading', { level: 1, name: 'Audit log' })).toBeInTheDocument();
14
+ expect(screen.getByRole('search', { name: 'Filters' })).toBeInTheDocument();
15
+ const table = screen.getByRole('table');
16
+ expect(within(table).getAllByRole('row')).toHaveLength(auditEvents.length + 1);
17
+ await expectNoAxeViolations();
18
+ });
19
+
20
+ it('searches, opens a diff and exports the filtered events', { timeout: 15000 }, async () => {
21
+ const onExport = vi.fn();
22
+ render(<AuditLogPage onExport={onExport} />);
23
+ await userEvent.type(screen.getByRole('searchbox', { name: 'Search' }), 'project.');
24
+ const table = screen.getByRole('table');
25
+ expect(within(table).getAllByRole('row')).toHaveLength(3);
26
+
27
+ await userEvent.click(screen.getByRole('button', { name: 'View changes for project.update on web-app' }));
28
+ const dialog = await screen.findByRole('dialog');
29
+ expect(within(dialog).getByText(/"region": "eu-central-1"/)).toBeInTheDocument();
30
+ await userEvent.keyboard('{Escape}');
31
+
32
+ await userEvent.click(screen.getAllByRole('button', { name: 'Export CSV' })[0]!);
33
+ expect(onExport).toHaveBeenCalledWith([
34
+ expect.objectContaining({ id: 'evt_9f21' }),
35
+ expect.objectContaining({ id: 'evt_9ec0' }),
36
+ ]);
37
+ });
38
+
39
+ it('filters by field (OR within, AND across) and writes CSV', () => {
40
+ const rows = filterAuditEvents(
41
+ auditEvents,
42
+ '',
43
+ [
44
+ { field: 'result', value: 'failed' },
45
+ { field: 'result', value: 'denied' },
46
+ ],
47
+ ALL,
48
+ );
49
+ expect(rows.map((e) => e.id)).toEqual(['evt_9f0a', 'evt_9ef3']);
50
+ expect(auditEventsToCsv(rows).split('\n')[0]).toBe('time,actor,email,action,resource,target,ip,result');
51
+ });
52
+
53
+ it('takes the heading copy from props', { timeout: 15000 }, () => {
54
+ render(<AuditLogPage title="Tenant activity" description="Changes across this tenant." />);
55
+ expect(screen.getByRole('heading', { level: 1, name: 'Tenant activity' })).toBeInTheDocument();
56
+ expect(screen.getByText('Changes across this tenant.')).toBeInTheDocument();
57
+ });
58
+ });
@@ -0,0 +1,284 @@
1
+ import {
2
+ DataTable,
3
+ DateRangeFilter,
4
+ FilterBar,
5
+ NoResultsEmpty,
6
+ PageHeader,
7
+ type ActiveFilter,
8
+ type DataTableColumn,
9
+ type FilterField,
10
+ } from '@gntik-ai/blocks';
11
+ import { Download, FileDiff } from '@gntik-ai/icons';
12
+ import {
13
+ Avatar,
14
+ Button,
15
+ CodeBlock,
16
+ Drawer,
17
+ DrawerBody,
18
+ DrawerContent,
19
+ DrawerDescription,
20
+ DrawerHeader,
21
+ DrawerTitle,
22
+ Page,
23
+ StatusTag,
24
+ Timestamp,
25
+ type BreadcrumbItem,
26
+ type DateRange,
27
+ } from '@gntik-ai/ui';
28
+ import { useState } from 'react';
29
+ import { ConsoleShell, type ConsoleShellProps } from '../shared/ConsoleShell';
30
+ import { auditBreadcrumbs, auditEvents, auditFilterFields, auditResultStatuses, type AuditEvent } from './data';
31
+
32
+ export interface AuditLogProps {
33
+ /** Page heading (default "Audit log"). */
34
+ title: string;
35
+ /** Line under the heading. */
36
+ description: string;
37
+ events: AuditEvent[];
38
+ filterFields: FilterField[];
39
+ /** Initial date range; defaults to the last 7 days. */
40
+ defaultRange: DateRange;
41
+ /** Reference "today" for the date presets. */
42
+ today: Date;
43
+ /** Called with the filtered events; defaults to downloading them as CSV. */
44
+ onExport: (events: AuditEvent[]) => void;
45
+ breadcrumbs: BreadcrumbItem[];
46
+ currentHref: string;
47
+ /** ConsoleShell props (app nav, user, workspaces, sidebar footer, topbar…). */
48
+ shell: Omit<ConsoleShellProps, 'children'>;
49
+ }
50
+
51
+ const DAY = 86_400_000;
52
+ const startOfDay = (d: Date) => new Date(d.getFullYear(), d.getMonth(), d.getDate());
53
+
54
+ const fieldValue = (e: AuditEvent, field: string) =>
55
+ field === 'actor' ? e.actor.name : field === 'resource' ? e.resource : field === 'result' ? e.result : '';
56
+
57
+ /** Applies the search text, the filter chips (OR within a field, AND across fields) and the date range. */
58
+ export function filterAuditEvents(
59
+ events: readonly AuditEvent[],
60
+ query: string,
61
+ filters: readonly ActiveFilter[],
62
+ range: DateRange,
63
+ ) {
64
+ const q = query.trim().toLowerCase();
65
+ const fields = [...new Set(filters.map((f) => f.field))];
66
+ const from = range.start ? startOfDay(range.start).getTime() : -Infinity;
67
+ const to = range.end ? startOfDay(range.end).getTime() + DAY : Infinity;
68
+ return events.filter((e) => {
69
+ if (q && ![e.actor.name, e.actor.email, e.action, e.target].some((v) => v.toLowerCase().includes(q))) return false;
70
+ if (!fields.every((field) => filters.some((f) => f.field === field && f.value === fieldValue(e, field))))
71
+ return false;
72
+ const t = e.at.getTime();
73
+ return t >= from && t < to;
74
+ });
75
+ }
76
+
77
+ const csvCell = (v: string) => (/[",\n]/.test(v) ? `"${v.replace(/"/g, '""')}"` : v);
78
+
79
+ /** CSV of the events (time, actor, action, resource, target, ip, result). */
80
+ export function auditEventsToCsv(events: readonly AuditEvent[]) {
81
+ const head = ['time', 'actor', 'email', 'action', 'resource', 'target', 'ip', 'result'];
82
+ const rows = events.map((e) => [
83
+ e.at.toISOString(),
84
+ e.actor.name,
85
+ e.actor.email,
86
+ e.action,
87
+ e.resource,
88
+ e.target,
89
+ e.ip,
90
+ e.result,
91
+ ]);
92
+ return [head, ...rows].map((r) => r.map(csvCell).join(',')).join('\n');
93
+ }
94
+
95
+ function downloadCsv(events: AuditEvent[]) {
96
+ if (typeof URL.createObjectURL !== 'function') return;
97
+ const url = URL.createObjectURL(new Blob([auditEventsToCsv(events)], { type: 'text/csv' }));
98
+ const a = document.createElement('a');
99
+ a.href = url;
100
+ a.download = 'audit-log.csv';
101
+ a.click();
102
+ URL.revokeObjectURL(url);
103
+ }
104
+
105
+ /** Audit log: ConsoleShell + Page with FilterBar + DateRangeFilter, an events DataTable and a diff drawer. */
106
+ export default function AuditLogPage(props: Partial<AuditLogProps>) {
107
+ const {
108
+ events = auditEvents,
109
+ filterFields = auditFilterFields,
110
+ defaultRange,
111
+ today,
112
+ onExport = downloadCsv,
113
+ breadcrumbs = auditBreadcrumbs,
114
+ currentHref = '/activity',
115
+ title = 'Audit log',
116
+ description = 'Every change made in this workspace: who did it, when, from where and what changed.',
117
+ shell,
118
+ } = props;
119
+ const [query, setQuery] = useState('');
120
+ const [filters, setFilters] = useState<ActiveFilter[]>([]);
121
+ const [range, setRange] = useState<DateRange>(() => {
122
+ if (defaultRange) return defaultRange;
123
+ const t = today ?? new Date();
124
+ return { start: new Date(t.getTime() - 6 * DAY), end: t };
125
+ });
126
+ const [diffEvent, setDiffEvent] = useState<AuditEvent | null>(null);
127
+
128
+ const rows = filterAuditEvents(events, query, filters, range);
129
+
130
+ const columns: DataTableColumn<AuditEvent>[] = [
131
+ {
132
+ id: 'at',
133
+ header: 'Time',
134
+ accessor: (e) => e.at,
135
+ cell: (e) => <Timestamp value={e.at} format="absolute" className="font-mono text-[12px]" />,
136
+ sortable: true,
137
+ hideable: false,
138
+ headClassName: 'w-44',
139
+ },
140
+ {
141
+ id: 'actor',
142
+ header: 'Actor',
143
+ accessor: (e) => e.actor.name,
144
+ cell: (e) => (
145
+ <span className="flex items-center gap-2">
146
+ <Avatar name={e.actor.name} size="xs" />
147
+ <span className="min-w-0">
148
+ <span className="block truncate font-medium">{e.actor.name}</span>
149
+ <span className="block truncate text-[11.5px] text-muted-foreground">{e.actor.email}</span>
150
+ </span>
151
+ </span>
152
+ ),
153
+ sortable: true,
154
+ },
155
+ { id: 'action', header: 'Action', accessor: (e) => e.action, sortable: true, className: 'font-mono text-[12px]' },
156
+ {
157
+ id: 'target',
158
+ header: 'Target',
159
+ accessor: (e) => e.target,
160
+ cell: (e) => <span className="font-medium">{e.target}</span>,
161
+ },
162
+ {
163
+ id: 'ip',
164
+ header: 'IP address',
165
+ accessor: (e) => e.ip,
166
+ defaultHidden: true,
167
+ className: 'font-mono text-[12px] text-muted-foreground',
168
+ },
169
+ {
170
+ id: 'result',
171
+ header: 'Result',
172
+ accessor: (e) => e.result,
173
+ cell: (e) => <StatusTag status={e.result} statuses={auditResultStatuses} />,
174
+ sortable: true,
175
+ },
176
+ {
177
+ id: 'changes',
178
+ header: 'Changes',
179
+ align: 'right',
180
+ cell: (e) =>
181
+ e.diff ? (
182
+ <Button
183
+ size="sm"
184
+ variant="ghost"
185
+ icon={FileDiff}
186
+ aria-label={`View changes for ${e.action} on ${e.target}`}
187
+ onClick={() => setDiffEvent(e)}
188
+ >
189
+ Diff
190
+ </Button>
191
+ ) : (
192
+ <span className="text-[12px] text-muted-foreground">—</span>
193
+ ),
194
+ },
195
+ ];
196
+
197
+ return (
198
+ <ConsoleShell breadcrumbs={breadcrumbs} currentHref={currentHref} {...shell}>
199
+ <Page
200
+ width="wide"
201
+ header={
202
+ <PageHeader
203
+ breadcrumbs={null}
204
+ title={title}
205
+ description={description}
206
+ status=""
207
+ meta={[]}
208
+ tabs={null}
209
+ actions={[
210
+ {
211
+ label: 'Export CSV',
212
+ icon: Download,
213
+ variant: 'secondary',
214
+ disabled: rows.length === 0,
215
+ onClick: () => onExport(rows),
216
+ },
217
+ ]}
218
+ />
219
+ }
220
+ >
221
+ <div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
222
+ <FilterBar
223
+ className="min-w-0 flex-1"
224
+ fields={filterFields}
225
+ views={[]}
226
+ query={query}
227
+ onQueryChange={setQuery}
228
+ filters={filters}
229
+ onFiltersChange={setFilters}
230
+ placeholder="Search actor, action or target…"
231
+ resultCount={rows.length}
232
+ />
233
+ <DateRangeFilter value={range} onValueChange={setRange} today={today} label="Event date" />
234
+ </div>
235
+ <DataTable<AuditEvent>
236
+ caption="Audit events"
237
+ rows={rows}
238
+ columns={columns}
239
+ getRowLabel={(e) => `${e.action} on ${e.target}`}
240
+ selectable={false}
241
+ defaultSort={{ columnId: 'at', direction: 'descending' }}
242
+ defaultDensity="compact"
243
+ emptyState={
244
+ <NoResultsEmpty
245
+ entity="events"
246
+ query={query}
247
+ filterCount={filters.length}
248
+ onClearFilters={() => setFilters([])}
249
+ onClearSearch={() => setQuery('')}
250
+ />
251
+ }
252
+ />
253
+ </Page>
254
+ <Drawer open={diffEvent !== null} onOpenChange={(open) => !open && setDiffEvent(null)}>
255
+ <DrawerContent size="lg">
256
+ {diffEvent && (
257
+ <>
258
+ <DrawerHeader>
259
+ <DrawerTitle>
260
+ <span className="font-mono">{diffEvent.action}</span> · {diffEvent.target}
261
+ </DrawerTitle>
262
+ <DrawerDescription>
263
+ By {diffEvent.actor.name} from <span className="font-mono">{diffEvent.ip}</span> ·{' '}
264
+ <Timestamp value={diffEvent.at} format="absolute" />
265
+ </DrawerDescription>
266
+ </DrawerHeader>
267
+ <DrawerBody>
268
+ <CodeBlock
269
+ code={diffEvent.diff ?? ''}
270
+ language="diff"
271
+ filename={`${diffEvent.id}.diff`}
272
+ label={`Changes for ${diffEvent.action}`}
273
+ highlightLines={(diffEvent.diff ?? '')
274
+ .split('\n')
275
+ .flatMap((l, i) => (l.startsWith('+') ? [i + 1] : []))}
276
+ />
277
+ </DrawerBody>
278
+ </>
279
+ )}
280
+ </DrawerContent>
281
+ </Drawer>
282
+ </ConsoleShell>
283
+ );
284
+ }
@@ -0,0 +1,133 @@
1
+ import type { FilterField } from '@gntik-ai/blocks';
2
+ import type { BreadcrumbItem, StatusDefinition } from '@gntik-ai/ui';
3
+
4
+ export type AuditResult = 'succeeded' | 'failed' | 'denied';
5
+
6
+ export interface AuditEvent {
7
+ id: string;
8
+ at: Date;
9
+ actor: { name: string; email: string };
10
+ /** Dotted action key, e.g. "project.update". */
11
+ action: string;
12
+ /** Kind of resource touched (filter field). */
13
+ resource: string;
14
+ /** The resource instance, e.g. "web-app". */
15
+ target: string;
16
+ ip: string;
17
+ result: AuditResult;
18
+ /** Unified diff of the change; absent for events without a payload change. */
19
+ diff?: string;
20
+ }
21
+
22
+ export const auditResultStatuses: Record<string, StatusDefinition> = {
23
+ denied: { label: 'Denied', tone: 'warning' },
24
+ };
25
+
26
+ const MIN = 60_000;
27
+ const HOUR = 60 * MIN;
28
+ const NOW = Date.now();
29
+
30
+ const dana = { name: 'Dana Whitfield', email: 'dana@example.com' };
31
+ const sam = { name: 'Sam Patel', email: 'sam@example.com' };
32
+ const lee = { name: 'Lee Chen', email: 'lee@example.com' };
33
+ const bot = { name: 'Deploy bot', email: 'bot@example.com' };
34
+
35
+ /** Neutral audit trail, newest first, all within the last 6 days. */
36
+ export const auditEvents: AuditEvent[] = [
37
+ {
38
+ id: 'evt_9f21',
39
+ at: new Date(NOW - 6 * MIN),
40
+ actor: dana,
41
+ action: 'project.update',
42
+ resource: 'Project',
43
+ target: 'web-app',
44
+ ip: '203.0.113.24',
45
+ result: 'succeeded',
46
+ diff: ' {\n "name": "web-app",\n- "region": "eu-west-1",\n+ "region": "eu-central-1",\n- "autoscale": false\n+ "autoscale": true\n }',
47
+ },
48
+ {
49
+ id: 'evt_9f1c',
50
+ at: new Date(NOW - 48 * MIN),
51
+ actor: sam,
52
+ action: 'member.role.update',
53
+ resource: 'Member',
54
+ target: 'lee@example.com',
55
+ ip: '198.51.100.7',
56
+ result: 'succeeded',
57
+ diff: ' {\n "member": "lee@example.com",\n- "role": "viewer"\n+ "role": "developer"\n }',
58
+ },
59
+ {
60
+ id: 'evt_9f0a',
61
+ at: new Date(NOW - 3 * HOUR),
62
+ actor: lee,
63
+ action: 'billing.plan.update',
64
+ resource: 'Billing',
65
+ target: 'Acme Industries',
66
+ ip: '192.0.2.81',
67
+ result: 'denied',
68
+ },
69
+ {
70
+ id: 'evt_9ef3',
71
+ at: new Date(NOW - 7 * HOUR),
72
+ actor: bot,
73
+ action: 'deployment.create',
74
+ resource: 'Deployment',
75
+ target: 'orders-api build 212',
76
+ ip: '10.0.4.12',
77
+ result: 'failed',
78
+ },
79
+ {
80
+ id: 'evt_9ee8',
81
+ at: new Date(NOW - 26 * HOUR),
82
+ actor: dana,
83
+ action: 'api_key.create',
84
+ resource: 'API key',
85
+ target: 'ci-pipeline',
86
+ ip: '203.0.113.24',
87
+ result: 'succeeded',
88
+ diff: '+{\n+ "name": "ci-pipeline",\n+ "scopes": ["deployments:write"],\n+ "expiresIn": "90d"\n+}',
89
+ },
90
+ {
91
+ id: 'evt_9ed1',
92
+ at: new Date(NOW - 2 * 24 * HOUR),
93
+ actor: sam,
94
+ action: 'member.invite',
95
+ resource: 'Member',
96
+ target: 'riley@example.com',
97
+ ip: '198.51.100.7',
98
+ result: 'succeeded',
99
+ },
100
+ {
101
+ id: 'evt_9ec0',
102
+ at: new Date(NOW - 3 * 24 * HOUR),
103
+ actor: dana,
104
+ action: 'project.delete',
105
+ resource: 'Project',
106
+ target: 'legacy-site',
107
+ ip: '203.0.113.24',
108
+ result: 'succeeded',
109
+ diff: '-{\n- "name": "legacy-site",\n- "region": "us-east-1"\n-}',
110
+ },
111
+ {
112
+ id: 'evt_9eb4',
113
+ at: new Date(NOW - 5 * 24 * HOUR),
114
+ actor: lee,
115
+ action: 'api_key.revoke',
116
+ resource: 'API key',
117
+ target: 'old-ci',
118
+ ip: '192.0.2.81',
119
+ result: 'succeeded',
120
+ },
121
+ ];
122
+
123
+ export const auditFilterFields: FilterField[] = [
124
+ { id: 'actor', label: 'Actor', options: [dana.name, sam.name, lee.name, bot.name] },
125
+ { id: 'resource', label: 'Resource', options: ['Project', 'Member', 'Deployment', 'API key', 'Billing'] },
126
+ { id: 'result', label: 'Result', options: ['succeeded', 'failed', 'denied'] },
127
+ ];
128
+
129
+ export const auditBreadcrumbs: BreadcrumbItem[] = [
130
+ { label: 'Acme Industries', href: '/overview' },
131
+ { label: 'Settings', href: '/settings' },
132
+ { label: 'Audit log' },
133
+ ];
@@ -0,0 +1,12 @@
1
+ import type { TemplateMeta } from '../meta';
2
+
3
+ export const meta: TemplateMeta = {
4
+ name: 'Audit log',
5
+ family: 'Work',
6
+ priority: 'P2',
7
+ status: 'beta',
8
+ description:
9
+ 'Workspace audit trail: FilterBar (search, actor / resource / result filters) and DateRangeFilter over a DataTable of events with actor, action, target and result; a drawer shows the change diff (CodeBlock); export to CSV.',
10
+ layout: 'SidebarLayout · Page',
11
+ blocks: ['app-sidebar', 'app-topbar', 'page-header', 'filter-bar', 'date-range-filter', 'data-table', 'empty-states'],
12
+ };
@@ -0,0 +1,117 @@
1
+ import { CalendarClock, GripVertical } from '@gntik-ai/icons';
2
+ import { Avatar, Badge, cn, Timestamp } from '@gntik-ai/ui';
3
+ import type { DragEvent, KeyboardEvent } from 'react';
4
+ import { priorityLabels, priorityTones, type BoardCard, type BoardColumn } from './data';
5
+
6
+ export interface LaneProps {
7
+ column: BoardColumn;
8
+ cards: BoardCard[];
9
+ headingId: string;
10
+ instructionsId: string;
11
+ grabbedId: string | null;
12
+ draggingId: string | null;
13
+ isDropTarget: boolean;
14
+ register: (id: string) => (el: HTMLButtonElement | null) => void;
15
+ onOpen: (id: string) => void;
16
+ onCardKeyDown: (id: string) => (e: KeyboardEvent<HTMLButtonElement>) => void;
17
+ onCardKeyUp: (e: KeyboardEvent<HTMLButtonElement>) => void;
18
+ onDragStart: (id: string) => (e: DragEvent<HTMLElement>) => void;
19
+ onDragEnd: () => void;
20
+ onDragOver: (e: DragEvent<HTMLElement>) => void;
21
+ onDrop: (e: DragEvent<HTMLElement>) => void;
22
+ }
23
+
24
+ /** One board column: heading with count and a list of draggable card buttons. */
25
+ export function Lane({
26
+ column,
27
+ cards,
28
+ headingId,
29
+ instructionsId,
30
+ grabbedId,
31
+ draggingId,
32
+ isDropTarget,
33
+ register,
34
+ onOpen,
35
+ onCardKeyDown,
36
+ onCardKeyUp,
37
+ onDragStart,
38
+ onDragEnd,
39
+ onDragOver,
40
+ onDrop,
41
+ }: LaneProps) {
42
+ return (
43
+ <section
44
+ aria-labelledby={headingId}
45
+ onDragOver={onDragOver}
46
+ onDrop={onDrop}
47
+ data-drop-target={isDropTarget || undefined}
48
+ className="flex w-72 shrink-0 flex-col rounded-xl border border-border bg-secondary/40 data-[drop-target]:border-primary/60 data-[drop-target]:bg-accent/40"
49
+ >
50
+ <h2 id={headingId} className="flex items-center gap-2 px-3.5 pt-3 pb-2 text-[13px] font-semibold text-foreground">
51
+ {column.title}
52
+ <span className="font-mono text-[11px] font-normal text-muted-foreground">
53
+ {cards.length}
54
+ <span className="sr-only"> cards</span>
55
+ </span>
56
+ </h2>
57
+ <ul className="flex min-h-24 flex-1 flex-col gap-2 px-2 pb-2">
58
+ {cards.map((card, index) => (
59
+ <li key={card.id} data-card-index={index}>
60
+ <button
61
+ ref={register(card.id)}
62
+ type="button"
63
+ draggable
64
+ aria-describedby={instructionsId}
65
+ aria-pressed={grabbedId === card.id}
66
+ onClick={() => onOpen(card.id)}
67
+ onKeyDown={onCardKeyDown(card.id)}
68
+ onKeyUp={onCardKeyUp}
69
+ onDragStart={onDragStart(card.id)}
70
+ onDragEnd={onDragEnd}
71
+ className={cn(
72
+ 'group flex w-full cursor-grab flex-col gap-2 rounded-lg border border-border bg-card p-3 text-start shadow-sm transition-colors motion-reduce:transition-none hover:border-primary/40 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus-ring',
73
+ 'aria-pressed:border-primary aria-pressed:bg-accent',
74
+ draggingId === card.id && 'opacity-50',
75
+ )}
76
+ >
77
+ <span className="flex items-start gap-2">
78
+ <span className="min-w-0 flex-1 text-[13px] leading-snug font-medium text-foreground">
79
+ {card.title}
80
+ </span>
81
+ <GripVertical size={14} aria-hidden className="mt-0.5 shrink-0 text-muted-foreground" />
82
+ </span>
83
+ <span className="flex flex-wrap items-center gap-1.5">
84
+ <span className="font-mono text-[11px] text-muted-foreground">{card.id}</span>
85
+ <Badge size="sm" tone={priorityTones[card.priority]}>
86
+ {priorityLabels[card.priority]}
87
+ </Badge>
88
+ {card.labels.map((l) => (
89
+ <Badge key={l} size="sm" variant="outline">
90
+ {l}
91
+ </Badge>
92
+ ))}
93
+ </span>
94
+ <span className="flex items-center justify-between gap-2 text-[11.5px] text-muted-foreground">
95
+ <span className="inline-flex items-center gap-1">
96
+ {card.due && (
97
+ <>
98
+ <CalendarClock size={12} aria-hidden />
99
+ <span className="sr-only">Due </span>
100
+ <Timestamp
101
+ value={card.due}
102
+ format="absolute"
103
+ tooltip={false}
104
+ dateOptions={{ month: 'short', day: 'numeric' }}
105
+ />
106
+ </>
107
+ )}
108
+ </span>
109
+ <Avatar size="xs" name={card.assignee} />
110
+ </span>
111
+ </button>
112
+ </li>
113
+ ))}
114
+ </ul>
115
+ </section>
116
+ );
117
+ }