@fleetbase/fleetops-engine
Version:
Fleet & Transport Management Extension for Fleetbase
169 lines (145 loc) • 5.45 kB
JavaScript
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { task } from 'ember-concurrency';
/**
* <Widget::KpiTile @slug="earnings" @title="..." @format="money" @showDelta={{true}}
* @showSparkline={{true}} @deltaInverse={{false}} @period="30d" />
*
* Renders a single KPI tile backed by `/fleet-ops/metrics/{slug}`. Hosts the
* format-specific value rendering, the optional period-over-period delta
* badge (with `deltaInverse` to flip semantics for inverse-good metrics like
* open_issues), and the optional sparkline line chart.
*/
export default class WidgetKpiTileComponent extends Component {
fetch;
data = null;
error = null;
get period() {
return this.args.period ?? '30d';
}
get showDelta() {
return this.args.showDelta ?? true;
}
get showSparkline() {
return this.args.showSparkline ?? true;
}
get deltaInverse() {
return this.args.deltaInverse ?? false;
}
get format() {
return this.args.format ?? this.data?.format ?? 'count';
}
get currency() {
return this.data?.currency ?? 'USD';
}
get deltaPct() {
return this.data?.delta_pct;
}
get hasDelta() {
return this.showDelta && typeof this.deltaPct === 'number';
}
get deltaStatus() {
const pct = this.deltaPct;
if (typeof pct !== 'number' || pct === 0) return 'info';
const positive = pct > 0;
const isGood = this.deltaInverse ? !positive : positive;
return isGood ? 'success' : 'danger';
}
get deltaText() {
const pct = this.deltaPct;
const sign = pct > 0 ? '+' : '';
return `${sign}${pct}%`;
}
/** Tints the card subtly based on trend direction so the dashboard pops. */
get cardAccentClass() {
const pct = this.deltaPct;
if (typeof pct !== 'number' || pct === 0) {
return 'kpi-accent-neutral';
}
const positive = pct > 0;
const isGood = this.deltaInverse ? !positive : positive;
return isGood ? 'kpi-accent-good' : 'kpi-accent-bad';
}
get valueTextClass() {
const pct = this.deltaPct;
if (typeof pct !== 'number' || pct === 0) {
return 'text-black dark:text-gray-100';
}
const positive = pct > 0;
const isGood = this.deltaInverse ? !positive : positive;
return isGood ? 'text-emerald-600 dark:text-emerald-400' : 'text-rose-600 dark:text-rose-400';
}
/** Neon palette per delta direction. Drives line + fill + value highlight. */
get accentColors() {
// Bright neon-ish trio that pops on the dark theme without being garish in light.
const good = { line: '#22c55e', fill: 'rgba(34, 197, 94, 0.22)', glow: 'rgba(34, 197, 94, 0.35)' };
const bad = { line: '#ef4444', fill: 'rgba(239, 68, 68, 0.22)', glow: 'rgba(239, 68, 68, 0.35)' };
const neutral = { line: '#3485e2', fill: 'rgba(52, 133, 226, 0.18)', glow: 'rgba(52, 133, 226, 0.30)' };
const pct = this.deltaPct;
if (typeof pct !== 'number' || pct === 0) return neutral;
const positive = pct > 0;
const isGood = this.deltaInverse ? !positive : positive;
return isGood ? good : bad;
}
get sparklineDatasets() {
const points = this.data?.sparkline?.data;
if (!points || points.length === 0) return null;
const { line, fill } = this.accentColors;
return [
{
data: points,
borderColor: line,
backgroundColor: fill,
pointRadius: 0,
borderWidth: 1.75,
tension: 0.4,
// Anchor the area fill to the chart's y=0 axis so the sparkline
// visually sits at the BOTTOM of the card regardless of the data range.
fill: 'origin',
},
];
}
get sparklineLabels() {
return this.data?.sparkline?.labels ?? [];
}
get sparklineOptions() {
return {
responsive: true,
maintainAspectRatio: false,
animation: { duration: 600, easing: 'easeOutQuart' },
plugins: {
legend: { display: false },
tooltip: { enabled: false },
},
scales: {
x: { display: false },
// Lock min to 0 so flat-zero data sits flush along the bottom edge.
// suggestedMin/Max keep the line off the very top/bottom for non-zero data.
y: { display: false, min: 0, beginAtZero: true, grace: '5%' },
},
elements: { line: { borderJoinStyle: 'round' } },
};
}
constructor() {
super(...arguments);
this.load.perform();
}
*load() {
try {
this.data = yield this.fetch.get(`fleet-ops/metrics/${this.args.slug}`, {
period: this.period,
sparkline: this.showSparkline,
compare: this.showDelta,
});
this.error = null;
} catch (e) {
this.error = e?.message ?? 'Failed to load metric';
}
}
refresh() {
this.load.perform();
}
}