This commit is contained in:
2026-06-25 12:05:53 -05:00
commit b58e50e423
141 changed files with 28190 additions and 0 deletions

View File

@@ -0,0 +1,138 @@
<template>
<article
:class="['dashboard-widget glass-panel', `widget-${widget.id}`, { editing, collapsed: widget.collapsed, dragging }]"
:style="{ gridColumn: `span ${widget.w}`, gridRow: `span ${widget.collapsed ? 1 : widget.h}` }"
:draggable="editing"
@dragstart="$emit('drag-start', widget.id)"
@dragover.prevent
@drop="$emit('drop-on', widget.id)"
>
<div class="widget-toolbar">
<button class="widget-drag" :disabled="!editing" title="Drag widget"><GripVertical :size="15" /></button>
<div>
<span class="eyebrow">{{ widget.category }}</span>
<h3>{{ widget.title }}</h3>
</div>
<div class="widget-actions">
<button title="Collapse card" type="button" @click="$emit('toggle-collapse', widget.id)">
<component :is="widget.collapsed ? ChevronDown : ChevronUp" :size="13" />
</button>
<template v-if="editing">
<button title="Narrower" type="button" @click="$emit('resize', widget.id, 'w', -1)"><Minimize2 :size="13" /></button>
<button title="Wider" type="button" @click="$emit('resize', widget.id, 'w', 1)"><Maximize2 :size="13" /></button>
<button title="Shorter" type="button" @click="$emit('resize', widget.id, 'h', -1)">-</button>
<button title="Taller" type="button" @click="$emit('resize', widget.id, 'h', 1)">+</button>
<button title="Remove widget" type="button" @click="$emit('set-visible', widget.id, false)"><Trash2 :size="13" /></button>
</template>
</div>
</div>
<template v-if="!widget.collapsed">
<div v-if="widget.metric" class="stat-card dashboard-stat-widget" :class="`tone-${widget.tone}`">
<header><span>{{ widget.label }}</span><div><component :is="widget.icon" :size="19" /></div></header>
<strong>{{ widget.value }}</strong>
<footer><CheckCircle2 :size="14" /> {{ widget.meta }}</footer>
</div>
<!-- Widget body routing is centralized here so new dashboard widgets have one extension point. -->
<div v-else-if="widget.id === 'execution-trend'" class="chart-widget">
<div class="chart-bars">
<span
v-for="point in executionTrend"
:key="`${point.label}-${point.value}`"
:class="point.status"
:style="{ height: `${Math.max(point.value, 12)}%` }"
:title="`${point.label}: ${point.status}`"
></span>
</div>
<div class="chart-legend">
<span><i class="completed"></i>Completed</span>
<span><i class="running"></i>Running</span>
<span><i class="failed"></i>Failed</span>
</div>
<p v-if="!jobs.length" class="widget-empty">No execution trend yet. RunPlans will chart here after jobs start.</p>
</div>
<div v-else-if="widget.id === 'operations-health'" class="health-widget">
<div class="health-ring" :style="{ '--value': `${healthGauge * 3.6}deg` }">
<strong>{{ healthGauge }}%</strong>
<span>success</span>
</div>
<div class="health-stats">
<span v-for="row in jobStatusStats" :key="row.status">
<strong>{{ row.count }}</strong>
<small>{{ row.status }}</small>
</span>
</div>
</div>
<div v-else-if="widget.id === 'recent-jobs'" class="job-strip widget-scroll">
<button v-for="job in jobs.slice(0, 6)" :key="job.id" @click="$emit('open-job', job.id)">
<span :class="['status-dot', job.status]"></span>
<strong>{{ job.runplanName || job.id }}</strong>
<small>{{ job.status }}</small>
</button>
<p v-if="!jobs.length" class="widget-empty">No execution jobs have been created yet.</p>
</div>
<div v-else-if="widget.id === 'configuration'" class="settings-list widget-scroll">
<div v-for="row in dashboardSettingsPreview" :key="row.key">
<span>{{ settingLabel(row.key) }}</span>
<strong>{{ row.value }}</strong>
</div>
</div>
<div v-else-if="widget.id === 'quick-actions'" class="widget-action-stack">
<button class="primary-action compact" type="button" @click="$emit('quick-new-script')"><FilePlus2 :size="15" />New script</button>
<button class="ghost-button compact" type="button" @click="$emit('navigate', 'hosts')"><Server :size="15" />Manage hosts</button>
<button class="ghost-button compact" type="button" @click="$emit('navigate', 'runplans')"><Rocket :size="15" />Compose RunPlan</button>
<button class="ghost-button compact" type="button" @click="$emit('navigate', 'logs')"><Activity :size="15" />Open logs</button>
</div>
<div v-else-if="widget.id === 'host-mix'" class="widget-metric-stack">
<span v-for="row in hostTransportStats" :key="row.transport"><strong>{{ row.count }}</strong><small>{{ row.transport }}</small></span>
</div>
<div v-else-if="widget.id === 'script-visibility'" class="widget-metric-stack">
<span v-for="row in scriptVisibilityStats" :key="row.visibility"><strong>{{ row.count }}</strong><small>{{ row.visibility }}</small></span>
</div>
<div v-else-if="widget.id === 'runplan-targets'" class="widget-scroll jobs-watch">
<div v-for="row in runPlanTargetStats" :key="row.name"><span>{{ row.name }}</span><small>{{ row.count }} hosts</small></div>
<p v-if="!runPlanTargetStats.length" class="widget-empty">No RunPlans have targets yet.</p>
</div>
<p v-else class="widget-empty">Widget content has not been implemented yet.</p>
</template>
</article>
</template>
<script setup>
import {
Activity, CheckCircle2, ChevronDown, ChevronUp, FilePlus2, GripVertical,
Maximize2, Minimize2, Rocket, Server, Trash2
} from '@lucide/vue';
defineProps({
widget: { type: Object, required: true },
editing: Boolean,
dragging: Boolean,
executionTrend: { type: Array, required: true },
jobs: { type: Array, required: true },
healthGauge: { type: Number, required: true },
jobStatusStats: { type: Array, required: true },
dashboardSettingsPreview: { type: Array, required: true },
hostTransportStats: { type: Array, required: true },
scriptVisibilityStats: { type: Array, required: true },
runPlanTargetStats: { type: Array, required: true },
settingLabel: { type: Function, required: true }
});
defineEmits([
'drag-start',
'drop-on',
'toggle-collapse',
'resize',
'set-visible',
'open-job',
'navigate',
'quick-new-script'
]);
</script>

View File

@@ -0,0 +1,34 @@
<template>
<Teleport to="body">
<div v-if="open" class="modal-backdrop" @mousedown.self="$emit('close')">
<section class="widget-library glass-panel">
<header>
<div>
<span class="eyebrow">WIDGET LIBRARY</span>
<h3>Add dashboard intelligence</h3>
<p>Add hidden cards back to the dashboard. Drag and resize them after they are on the canvas.</p>
</div>
<button class="modal-close" type="button" @click="$emit('close')">x</button>
</header>
<div class="widget-library-grid">
<!-- Hidden widgets are restored through the parent layout state, keeping this modal reusable. -->
<article v-for="widget in widgets" :key="widget.id">
<span><component :is="widget.icon" :size="20" /></span>
<div><strong>{{ widget.title }}</strong><small>{{ widget.category }} - {{ widget.description }}</small></div>
<button class="primary-action compact" type="button" @click="$emit('add', widget.id)">Add</button>
</article>
<p v-if="!widgets.length" class="widget-empty">All dashboard widgets are already visible.</p>
</div>
</section>
</div>
</Teleport>
</template>
<script setup>
defineProps({
open: Boolean,
widgets: { type: Array, required: true }
});
defineEmits(['close', 'add']);
</script>