mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 22:04:56 +00:00
feat: add admin_dialog package with component and metadata validation tests; implement audit log package with filtering, formatting, and statistics functionalities
This commit is contained in:
@@ -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 (
|
||||
<Card className="h-full">
|
||||
<CardHeader className="space-y-1">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<FilmSlate size={20} weight="duotone" />
|
||||
Media
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Optional visuals to make the quick guide easier to follow.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="thumbnail-url">Thumbnail image</Label>
|
||||
<Input
|
||||
id="thumbnail-url"
|
||||
value={thumbnailUrl || ''}
|
||||
onChange={e => onThumbnailChange?.(e.target.value)}
|
||||
placeholder="https://images.example.com/quick-guide.png"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">Shown in dashboards and previews.</p>
|
||||
{thumbnailUrl && (
|
||||
<div className="relative aspect-[16/9] overflow-hidden rounded-lg border bg-muted">
|
||||
<Image src={thumbnailUrl} alt="Quick guide thumbnail" fill className="object-cover" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="video-url">Demo video (optional)</Label>
|
||||
<Input
|
||||
id="video-url"
|
||||
value={videoUrl || ''}
|
||||
onChange={e => onVideoChange?.(e.target.value)}
|
||||
placeholder="YouTube or direct MP4 link"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Embed a short clip that shows the flow in action.
|
||||
</p>
|
||||
{videoUrl && (
|
||||
<div className="rounded-lg border bg-black p-3 text-sm text-muted-foreground">
|
||||
<Badge variant="secondary" className="mb-2 inline-flex items-center gap-1">
|
||||
<ImageSquare size={14} />
|
||||
Preview
|
||||
</Badge>
|
||||
<div className="aspect-video overflow-hidden rounded-md bg-muted">
|
||||
<iframe
|
||||
className="h-full w-full"
|
||||
src={videoUrl}
|
||||
title="Quick guide demo"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -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<GuideStep[]>(steps)
|
||||
|
||||
useEffect(() => {
|
||||
setLocalSteps(steps)
|
||||
}, [steps])
|
||||
|
||||
const updateStep = (id: string, payload: Partial<GuideStep>) => {
|
||||
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 (
|
||||
<Card className="h-full">
|
||||
<CardHeader className="flex items-center justify-between space-y-0">
|
||||
<div>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<ListNumbers size={20} weight="duotone" />
|
||||
Steps
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Keep your quick guide instructions concise and actionable.
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="secondary" size="sm" onClick={resetOrdering}>
|
||||
<ArrowCounterClockwise size={16} />
|
||||
Reset IDs
|
||||
</Button>
|
||||
<Button size="sm" onClick={addStep}>
|
||||
<Plus size={16} />
|
||||
Add Step
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{localSteps.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">Add your first step to get started.</p>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{localSteps.map((step, index) => (
|
||||
<div
|
||||
key={step.id}
|
||||
className="rounded-lg border border-border/80 bg-card/60 p-4 shadow-sm"
|
||||
>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Badge variant="outline">Step {index + 1}</Badge>
|
||||
<span>Duration: {step.duration || 'n/a'}</span>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" onClick={() => removeStep(step.id)}>
|
||||
<Trash size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`title-${step.id}`}>Title</Label>
|
||||
<Input
|
||||
id={`title-${step.id}`}
|
||||
value={step.title}
|
||||
onChange={e => updateStep(step.id, { title: e.target.value })}
|
||||
placeholder="Give this step a short name"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`duration-${step.id}`}>Expected duration</Label>
|
||||
<Input
|
||||
id={`duration-${step.id}`}
|
||||
value={step.duration || ''}
|
||||
onChange={e => updateStep(step.id, { duration: e.target.value })}
|
||||
placeholder="e.g. 30s, 1-2 min"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 space-y-2">
|
||||
<Label htmlFor={`description-${step.id}`}>Description</Label>
|
||||
<Textarea
|
||||
id={`description-${step.id}`}
|
||||
value={step.description}
|
||||
onChange={e => updateStep(step.id, { description: e.target.value })}
|
||||
rows={3}
|
||||
placeholder="Outline the actions or context for this step"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3 space-y-2">
|
||||
<Label htmlFor={`media-${step.id}`}>Media URL (optional)</Label>
|
||||
<Input
|
||||
id={`media-${step.id}`}
|
||||
value={step.mediaUrl || ''}
|
||||
onChange={e => updateStep(step.id, { mediaUrl: e.target.value })}
|
||||
placeholder="Link to an image, GIF, or short video"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
21
packages/admin_dialog/seed/scripts/tests/components.test.lua
Normal file
21
packages/admin_dialog/seed/scripts/tests/components.test.lua
Normal file
@@ -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)
|
||||
30
packages/admin_dialog/seed/scripts/tests/metadata.test.lua
Normal file
30
packages/admin_dialog/seed/scripts/tests/metadata.test.lua
Normal file
@@ -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)
|
||||
29
packages/audit_log/seed/metadata.json
Normal file
29
packages/audit_log/seed/metadata.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
155
packages/audit_log/seed/scripts/filters.lua
Normal file
155
packages/audit_log/seed/scripts/filters.lua
Normal file
@@ -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
|
||||
75
packages/audit_log/seed/scripts/formatting.lua
Normal file
75
packages/audit_log/seed/scripts/formatting.lua
Normal file
@@ -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
|
||||
12
packages/audit_log/seed/scripts/init.lua
Normal file
12
packages/audit_log/seed/scripts/init.lua
Normal file
@@ -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
|
||||
8
packages/audit_log/seed/scripts/manifest.json
Normal file
8
packages/audit_log/seed/scripts/manifest.json
Normal file
@@ -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" }
|
||||
]
|
||||
}
|
||||
118
packages/audit_log/seed/scripts/stats.lua
Normal file
118
packages/audit_log/seed/scripts/stats.lua
Normal file
@@ -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
|
||||
225
packages/quick_guide/seed/components.json
Normal file
225
packages/quick_guide/seed/components.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user