diff --git a/frontends/nextjs/src/components/misc/data/quick-guide/MediaPane.tsx b/frontends/nextjs/src/components/misc/data/quick-guide/MediaPane.tsx deleted file mode 100644 index d7aa77c5c..000000000 --- a/frontends/nextjs/src/components/misc/data/quick-guide/MediaPane.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { FilmSlate, ImageSquare } from '@phosphor-icons/react' -import Image from 'next/image' - -import { - Badge, - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, - Input, - Label, -} from '@/components/ui' - -interface MediaPaneProps { - thumbnailUrl?: string - videoUrl?: string - onThumbnailChange?: (value: string) => void - onVideoChange?: (value: string) => void -} - -export function MediaPane({ - thumbnailUrl, - videoUrl, - onThumbnailChange, - onVideoChange, -}: MediaPaneProps) { - return ( - - - - - Media - - - Optional visuals to make the quick guide easier to follow. - - - - - Thumbnail image - onThumbnailChange?.(e.target.value)} - placeholder="https://images.example.com/quick-guide.png" - /> - Shown in dashboards and previews. - {thumbnailUrl && ( - - - - )} - - - - Demo video (optional) - onVideoChange?.(e.target.value)} - placeholder="YouTube or direct MP4 link" - /> - - Embed a short clip that shows the flow in action. - - {videoUrl && ( - - - - Preview - - - - - - )} - - - - ) -} diff --git a/frontends/nextjs/src/components/misc/data/quick-guide/StepsEditor.tsx b/frontends/nextjs/src/components/misc/data/quick-guide/StepsEditor.tsx deleted file mode 100644 index c7d38641a..000000000 --- a/frontends/nextjs/src/components/misc/data/quick-guide/StepsEditor.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import { ArrowCounterClockwise, ListNumbers, Plus, Trash } from '@phosphor-icons/react' -import { useEffect, useState } from 'react' - -import { - Badge, - Button, - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, - Input, - Label, - Textarea, -} from '@/components/ui' - -export interface GuideStep { - id: string - title: string - description: string - mediaUrl?: string - duration?: string -} - -interface StepsEditorProps { - steps: GuideStep[] - onChange?: (steps: GuideStep[]) => void -} - -export function StepsEditor({ steps, onChange }: StepsEditorProps) { - const [localSteps, setLocalSteps] = useState(steps) - - useEffect(() => { - setLocalSteps(steps) - }, [steps]) - - const updateStep = (id: string, payload: Partial) => { - const nextSteps = localSteps.map(step => (step.id === id ? { ...step, ...payload } : step)) - setLocalSteps(nextSteps) - onChange?.(nextSteps) - } - - const removeStep = (id: string) => { - const nextSteps = localSteps.filter(step => step.id !== id) - setLocalSteps(nextSteps) - onChange?.(nextSteps) - } - - const addStep = () => { - const newStep: GuideStep = { - id: crypto.randomUUID(), - title: 'New step', - description: 'Describe what happens in this step.', - duration: '1-2 min', - } - - const nextSteps = [...localSteps, newStep] - setLocalSteps(nextSteps) - onChange?.(nextSteps) - } - - const resetOrdering = () => { - const nextSteps = localSteps.map((step, index) => ({ ...step, id: `step_${index + 1}` })) - setLocalSteps(nextSteps) - onChange?.(nextSteps) - } - - return ( - - - - - - Steps - - - Keep your quick guide instructions concise and actionable. - - - - - - Reset IDs - - - - Add Step - - - - - {localSteps.length === 0 ? ( - Add your first step to get started. - ) : ( - - {localSteps.map((step, index) => ( - - - - Step {index + 1} - Duration: {step.duration || 'n/a'} - - removeStep(step.id)}> - - - - - - Title - updateStep(step.id, { title: e.target.value })} - placeholder="Give this step a short name" - /> - - - Expected duration - updateStep(step.id, { duration: e.target.value })} - placeholder="e.g. 30s, 1-2 min" - /> - - - - Description - updateStep(step.id, { description: e.target.value })} - rows={3} - placeholder="Outline the actions or context for this step" - /> - - - Media URL (optional) - updateStep(step.id, { mediaUrl: e.target.value })} - placeholder="Link to an image, GIF, or short video" - /> - - - ))} - - )} - - - ) -} diff --git a/packages/admin_dialog/seed/scripts/tests/components.test.lua b/packages/admin_dialog/seed/scripts/tests/components.test.lua new file mode 100644 index 000000000..051c382d2 --- /dev/null +++ b/packages/admin_dialog/seed/scripts/tests/components.test.lua @@ -0,0 +1,21 @@ +-- Component validation tests for admin_dialog package +-- Uses lua_test framework + +describe("Admin Dialog Components", function() + local components = load_cases("components.json") + + it("should be a valid array", function() + expect(components).toBeType("table") + end) + + it("should have valid component structure if components exist", function() + if #components > 0 then + for _, component in ipairs(components) do + expect(component.id).toBeTruthy() + expect(component.type).toBeTruthy() + expect(component.id).toBeType("string") + expect(component.type).toBeType("string") + end + end + end) +end) diff --git a/packages/admin_dialog/seed/scripts/tests/metadata.test.lua b/packages/admin_dialog/seed/scripts/tests/metadata.test.lua new file mode 100644 index 000000000..6368b1d9a --- /dev/null +++ b/packages/admin_dialog/seed/scripts/tests/metadata.test.lua @@ -0,0 +1,30 @@ +-- Metadata validation tests for admin_dialog package +-- Uses lua_test framework + +describe("Admin Dialog Package Metadata", function() + local metadata = load_cases("metadata.json") + + it("should have valid package structure", function() + expect(metadata.packageId).toBe("admin_dialog") + expect(metadata.name).toBe("Admin Dialog") + expect(metadata.version).toBeTruthy() + expect(metadata.description).toBeTruthy() + end) + + it("should have correct package ID format", function() + expect(metadata.packageId).toMatch("^[a-z_]+$") + end) + + it("should have semantic version", function() + expect(metadata.version).toMatch("^%d+%.%d+%.%d+$") + end) + + it("should have exports defined", function() + expect(metadata.exports).toBeTruthy() + expect(metadata.exports.components).toBeType("table") + end) + + it("should have dependencies array", function() + expect(metadata.dependencies).toBeType("table") + end) +end) diff --git a/packages/audit_log/seed/metadata.json b/packages/audit_log/seed/metadata.json new file mode 100644 index 000000000..5a90fb9eb --- /dev/null +++ b/packages/audit_log/seed/metadata.json @@ -0,0 +1,29 @@ +{ + "packageId": "audit_log", + "name": "Audit Log", + "version": "1.0.0", + "description": "Security audit log viewer and stats dashboard", + "icon": "static_content/icon.svg", + "author": "MetaBuilder", + "category": "admin", + "dependencies": [], + "exports": { + "components": [ + "AuditLogViewer", + "AuditStatsCard", + "LogTable", + "LogFilters" + ], + "scripts": [ + "init", + "stats", + "filters", + "formatting" + ] + }, + "minLevel": 3, + "bindings": { + "dbal": true, + "browser": false + } +} diff --git a/packages/audit_log/seed/scripts/filters.lua b/packages/audit_log/seed/scripts/filters.lua new file mode 100644 index 000000000..ce889e493 --- /dev/null +++ b/packages/audit_log/seed/scripts/filters.lua @@ -0,0 +1,155 @@ +-- Audit log filtering logic +local M = {} + +-- Filter logs by operation type +function M.filterByOperation(logs, operation) + if not operation or operation == "" then + return logs + end + + local result = {} + for _, log in ipairs(logs or {}) do + if log.operation == operation then + result[#result + 1] = log + end + end + return result +end + +-- Filter logs by resource type +function M.filterByResource(logs, resource) + if not resource or resource == "" then + return logs + end + + local result = {} + for _, log in ipairs(logs or {}) do + if log.resource == resource then + result[#result + 1] = log + end + end + return result +end + +-- Filter logs by success status +function M.filterBySuccess(logs, success) + if success == nil then + return logs + end + + local result = {} + for _, log in ipairs(logs or {}) do + if log.success == success then + result[#result + 1] = log + end + end + return result +end + +-- Filter logs by username +function M.filterByUsername(logs, username) + if not username or username == "" then + return logs + end + + local result = {} + local lowerUsername = string.lower(username) + for _, log in ipairs(logs or {}) do + if log.username and string.match(string.lower(log.username), lowerUsername) then + result[#result + 1] = log + end + end + return result +end + +-- Filter logs by date range (timestamps in milliseconds) +function M.filterByDateRange(logs, startTime, endTime) + local result = {} + for _, log in ipairs(logs or {}) do + local ts = log.timestamp + local include = true + if startTime and ts < startTime then + include = false + end + if endTime and ts > endTime then + include = false + end + if include then + result[#result + 1] = log + end + end + return result +end + +-- Apply multiple filters +function M.applyFilters(logs, filters) + filters = filters or {} + local result = logs + + if filters.operation then + result = M.filterByOperation(result, filters.operation) + end + + if filters.resource then + result = M.filterByResource(result, filters.resource) + end + + if filters.success ~= nil then + result = M.filterBySuccess(result, filters.success) + end + + if filters.username then + result = M.filterByUsername(result, filters.username) + end + + if filters.startTime or filters.endTime then + result = M.filterByDateRange(result, filters.startTime, filters.endTime) + end + + return result +end + +-- Get unique values for filter dropdowns +function M.getFilterOptions(logs) + local operations = {} + local resources = {} + local usernames = {} + + for _, log in ipairs(logs or {}) do + if log.operation then + operations[log.operation] = true + end + if log.resource then + resources[log.resource] = true + end + if log.username then + usernames[log.username] = true + end + end + + local opList = {} + for op in pairs(operations) do + opList[#opList + 1] = op + end + table.sort(opList) + + local resList = {} + for res in pairs(resources) do + resList[#resList + 1] = res + end + table.sort(resList) + + local userList = {} + for user in pairs(usernames) do + userList[#userList + 1] = user + end + table.sort(userList) + + return { + operations = opList, + resources = resList, + usernames = userList + } +end + +return M diff --git a/packages/audit_log/seed/scripts/formatting.lua b/packages/audit_log/seed/scripts/formatting.lua new file mode 100644 index 000000000..dd6199810 --- /dev/null +++ b/packages/audit_log/seed/scripts/formatting.lua @@ -0,0 +1,75 @@ +-- Formatting helpers for audit log display +local M = {} + +-- Operation type to color mapping +M.operationColors = { + CREATE = "bg-green-500", + READ = "bg-blue-500", + UPDATE = "bg-yellow-500", + DELETE = "bg-red-500" +} + +-- Resource type to icon mapping +M.resourceIcons = { + user = "User", + credential = "ShieldCheck", + default = "ChartLine" +} + +-- Get color class for operation +function M.getOperationColor(operation) + return M.operationColors[operation] or "bg-gray-500" +end + +-- Get icon name for resource +function M.getResourceIcon(resource) + return M.resourceIcons[resource] or M.resourceIcons.default +end + +-- Format timestamp for display +function M.formatTimestamp(timestamp) + if not timestamp then + return "Unknown" + end + + -- Assuming timestamp is in milliseconds + local seconds = math.floor(timestamp / 1000) + return os.date("%Y-%m-%d %H:%M:%S", seconds) +end + +-- Format a log entry for display +function M.formatLogEntry(log) + return { + id = log.id, + operation = log.operation, + operationColor = M.getOperationColor(log.operation), + resource = log.resource, + resourceId = log.resourceId, + resourceIcon = M.getResourceIcon(log.resource), + username = log.username, + timestamp = M.formatTimestamp(log.timestamp), + ipAddress = log.ipAddress, + success = log.success, + errorMessage = log.errorMessage, + rowClass = log.success and "bg-card" or "bg-destructive/5 border-destructive/20" + } +end + +-- Format all logs for display +function M.formatAllLogs(logs) + local result = {} + for i, log in ipairs(logs or {}) do + result[i] = M.formatLogEntry(log) + end + return result +end + +-- Get status badge text +function M.getStatusBadge(log) + if log.success then + return nil + end + return "Failed" +end + +return M diff --git a/packages/audit_log/seed/scripts/init.lua b/packages/audit_log/seed/scripts/init.lua new file mode 100644 index 000000000..992f31181 --- /dev/null +++ b/packages/audit_log/seed/scripts/init.lua @@ -0,0 +1,12 @@ +-- Audit log package initialization +local M = {} + +M.name = "audit_log" +M.version = "1.0.0" + +function M.init() + log("Audit log package initialized") + return true +end + +return M diff --git a/packages/audit_log/seed/scripts/manifest.json b/packages/audit_log/seed/scripts/manifest.json new file mode 100644 index 000000000..ee55f56d3 --- /dev/null +++ b/packages/audit_log/seed/scripts/manifest.json @@ -0,0 +1,8 @@ +{ + "scripts": [ + { "file": "init.lua", "name": "init", "category": "lifecycle", "description": "Package initialization" }, + { "file": "stats.lua", "name": "stats", "category": "analytics", "description": "Statistics calculation" }, + { "file": "filters.lua", "name": "filters", "category": "data", "description": "Log filtering" }, + { "file": "formatting.lua", "name": "formatting", "category": "ui", "description": "Display formatting" } + ] +} diff --git a/packages/audit_log/seed/scripts/stats.lua b/packages/audit_log/seed/scripts/stats.lua new file mode 100644 index 000000000..bd93a0bd3 --- /dev/null +++ b/packages/audit_log/seed/scripts/stats.lua @@ -0,0 +1,118 @@ +-- Statistics calculation for audit logs +local M = {} + +-- Calculate stats from a list of logs +function M.calculateStats(logs) + local stats = { + total = 0, + successful = 0, + failed = 0, + rateLimit = 0 + } + + if not logs then + return stats + end + + stats.total = #logs + + for _, log in ipairs(logs) do + if log.success then + stats.successful = stats.successful + 1 + else + stats.failed = stats.failed + 1 + end + + if log.errorMessage and string.match(log.errorMessage, "Rate limit") then + stats.rateLimit = stats.rateLimit + 1 + end + end + + return stats +end + +-- Get stats by operation type +function M.getStatsByOperation(logs) + local byOperation = {} + + if not logs then + return byOperation + end + + for _, log in ipairs(logs) do + local op = log.operation or "UNKNOWN" + if not byOperation[op] then + byOperation[op] = { total = 0, successful = 0, failed = 0 } + end + byOperation[op].total = byOperation[op].total + 1 + if log.success then + byOperation[op].successful = byOperation[op].successful + 1 + else + byOperation[op].failed = byOperation[op].failed + 1 + end + end + + return byOperation +end + +-- Get stats by resource type +function M.getStatsByResource(logs) + local byResource = {} + + if not logs then + return byResource + end + + for _, log in ipairs(logs) do + local res = log.resource or "unknown" + if not byResource[res] then + byResource[res] = { total = 0, successful = 0, failed = 0 } + end + byResource[res].total = byResource[res].total + 1 + if log.success then + byResource[res].successful = byResource[res].successful + 1 + else + byResource[res].failed = byResource[res].failed + 1 + end + end + + return byResource +end + +-- Prepare stats for display +function M.prepareStatsDisplay(logs) + local stats = M.calculateStats(logs) + + return { + cards = { + { + title = "Total Operations", + value = stats.total, + icon = "ChartLine", + color = "default" + }, + { + title = "Successful", + value = stats.successful, + icon = "ShieldCheck", + color = "green" + }, + { + title = "Failed", + value = stats.failed, + icon = "Warning", + color = "red" + }, + { + title = "Rate Limited", + value = stats.rateLimit, + icon = "Clock", + color = "yellow" + } + }, + byOperation = M.getStatsByOperation(logs), + byResource = M.getStatsByResource(logs) + } +end + +return M diff --git a/packages/quick_guide/seed/components.json b/packages/quick_guide/seed/components.json new file mode 100644 index 000000000..ea52f149c --- /dev/null +++ b/packages/quick_guide/seed/components.json @@ -0,0 +1,225 @@ +[ + { + "id": "steps_editor", + "type": "steps_editor", + "name": "Steps Editor", + "description": "Editor for quick guide steps with add, edit, remove, and reorder", + "props": { + "steps": [] + }, + "layout": { + "type": "Card", + "props": { "className": "h-full" }, + "children": [ + { + "type": "CardHeader", + "props": { "className": "flex items-center justify-between space-y-0" }, + "children": [ + { + "type": "Box", + "children": [ + { + "type": "CardTitle", + "props": { "className": "flex items-center gap-2" }, + "children": [ + { "type": "Icon", "props": { "name": "ListNumbers", "size": 20 } }, + { "type": "Text", "props": { "text": "Steps" } } + ] + }, + { + "type": "CardDescription", + "props": { "text": "Keep your quick guide instructions concise and actionable." } + } + ] + }, + { + "type": "Box", + "props": { "className": "flex items-center gap-2" }, + "children": [ + { + "type": "Button", + "props": { "variant": "secondary", "size": "sm" }, + "events": { "onClick": "steps.resetOrdering" }, + "children": [ + { "type": "Icon", "props": { "name": "ArrowCounterClockwise", "size": 16 } }, + { "type": "Text", "props": { "text": "Reset IDs" } } + ] + }, + { + "type": "Button", + "props": { "size": "sm" }, + "events": { "onClick": "steps.addStep" }, + "children": [ + { "type": "Icon", "props": { "name": "Plus", "size": 16 } }, + { "type": "Text", "props": { "text": "Add Step" } } + ] + } + ] + } + ] + }, + { + "type": "CardContent", + "props": { "className": "space-y-4" }, + "children": [ + { + "type": "List", + "props": { "dataSource": "steps", "emptyText": "Add your first step to get started." }, + "itemTemplate": { + "type": "Box", + "props": { "className": "rounded-lg border border-border/80 bg-card/60 p-4 shadow-sm" }, + "children": [ + { + "type": "Box", + "props": { "className": "mb-3 flex items-center justify-between" }, + "children": [ + { + "type": "Box", + "props": { "className": "flex items-center gap-2 text-sm text-muted-foreground" }, + "children": [ + { "type": "Badge", "props": { "variant": "outline", "text": "Step {{index}}" } }, + { "type": "Text", "props": { "text": "Duration: {{item.duration}}" } } + ] + }, + { + "type": "Button", + "props": { "variant": "ghost", "size": "icon" }, + "events": { "onClick": "steps.removeStep" } + } + ] + }, + { + "type": "Grid", + "props": { "cols": 2, "gap": 3 }, + "children": [ + { + "type": "FormField", + "props": { "name": "title", "label": "Title", "value": "{{item.title}}", "placeholder": "Give this step a short name" } + }, + { + "type": "FormField", + "props": { "name": "duration", "label": "Expected duration", "value": "{{item.duration}}", "placeholder": "e.g. 30s, 1-2 min" } + } + ] + }, + { + "type": "FormField", + "props": { "name": "description", "label": "Description", "type": "textarea", "value": "{{item.description}}", "rows": 3, "placeholder": "Outline the actions or context for this step" } + }, + { + "type": "FormField", + "props": { "name": "mediaUrl", "label": "Media URL (optional)", "value": "{{item.mediaUrl}}", "placeholder": "Link to an image, GIF, or short video" } + } + ] + } + } + ] + } + ] + }, + "scripts": { + "addStep": "steps.addStep", + "removeStep": "steps.removeStep", + "updateStep": "steps.updateStep", + "resetOrdering": "steps.resetOrdering" + } + }, + { + "id": "media_pane", + "type": "media_pane", + "name": "Media Pane", + "description": "Media management pane for thumbnail and video URLs", + "props": { + "thumbnailUrl": "", + "videoUrl": "" + }, + "layout": { + "type": "Card", + "props": { "className": "h-full" }, + "children": [ + { + "type": "CardHeader", + "props": { "className": "space-y-1" }, + "children": [ + { + "type": "CardTitle", + "props": { "className": "flex items-center gap-2" }, + "children": [ + { "type": "Icon", "props": { "name": "FilmSlate", "size": 20 } }, + { "type": "Text", "props": { "text": "Media" } } + ] + }, + { + "type": "CardDescription", + "props": { "text": "Optional visuals to make the quick guide easier to follow." } + } + ] + }, + { + "type": "CardContent", + "props": { "className": "space-y-4" }, + "children": [ + { + "type": "Box", + "props": { "className": "space-y-2" }, + "children": [ + { + "type": "FormField", + "props": { "name": "thumbnailUrl", "label": "Thumbnail image", "value": "{{thumbnailUrl}}", "placeholder": "https://images.example.com/quick-guide.png", "helperText": "Shown in dashboards and previews." } + }, + { + "type": "ConditionalRender", + "props": { "condition": "{{thumbnailUrl}}" }, + "children": [ + { + "type": "Box", + "props": { "className": "relative aspect-[16/9] overflow-hidden rounded-lg border bg-muted" }, + "children": [ + { "type": "Image", "props": { "src": "{{thumbnailUrl}}", "alt": "Quick guide thumbnail", "fill": true, "className": "object-cover" } } + ] + } + ] + } + ] + }, + { + "type": "Box", + "props": { "className": "space-y-2" }, + "children": [ + { + "type": "FormField", + "props": { "name": "videoUrl", "label": "Demo video (optional)", "value": "{{videoUrl}}", "placeholder": "YouTube or direct MP4 link", "helperText": "Embed a short clip that shows the flow in action." } + }, + { + "type": "ConditionalRender", + "props": { "condition": "{{videoUrl}}" }, + "children": [ + { + "type": "Box", + "props": { "className": "rounded-lg border bg-black p-3" }, + "children": [ + { "type": "Badge", "props": { "variant": "secondary", "text": "Preview" } }, + { + "type": "Box", + "props": { "className": "aspect-video overflow-hidden rounded-md bg-muted mt-2" }, + "children": [ + { "type": "Iframe", "props": { "src": "{{videoUrl}}", "className": "h-full w-full" } } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + }, + "scripts": { + "init": "media.prepareMediaState", + "onThumbnailChange": "media.handleThumbnailChange", + "onVideoChange": "media.handleVideoChange" + } + } +]
Shown in dashboards and previews.
- Embed a short clip that shows the flow in action. -
Add your first step to get started.