Initial
This commit is contained in:
138
client/src/components/dashboard/DashboardWidget.vue
Normal file
138
client/src/components/dashboard/DashboardWidget.vue
Normal 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>
|
||||
34
client/src/components/dashboard/WidgetLibraryModal.vue
Normal file
34
client/src/components/dashboard/WidgetLibraryModal.vue
Normal 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>
|
||||
Reference in New Issue
Block a user