Files
openobserve/web/src/composables/useSloFormat.ts
T
Prabhat Sharmaandgithub-actions[bot] ecc1d5dbff fix(ui): clean up SLO detail and traces navigation (#13852)
## Summary

This PR combines the SLO detail cleanup and the Traces navigation fixes
into one change set.

### SLO cleanup

- remove the internal definition generation value from the SLO detail
title
- remove the unused translation key and update generated locale files
- normalize SLO health icon identifiers
- avoid rewriting unchanged locale files during translation updates

### Traces navigation

- add Spans as the first item in the Traces flyout
- route Spans, Traces, Service Graph, and Service Catalog through
/traces?tab=...
- redirect legacy Service Graph and Service Catalog paths while
preserving query state
- support same-route menu switching and retain stream, time-range, and
query context
- remove the deprecated search_mode parameter from generated and legacy
URLs
- keep Traces as the default for tabless URLs

## Validation

- focused Traces Vitest suites: 436 passed, 2 skipped
- app TypeScript check
- full frontend ESLint check
- Prettier checks
- live browser verification of Traces and Spans menu switching

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-18 17:07:57 +00:00

146 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright 2026 OpenObserve Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { raw, type TranslateFn } from "@/types/i18n";
import type { SloStatus } from "@/ts/interfaces/slo";
/** The four states a user sees, in the order the list sorts them.
*
* `no_data` is deliberately NOT a flavour of healthy or of breached. An SLO
* below the coverage floor is frozen: its alerts neither fire nor resolve,
* and showing it as either would be a lie in a direction someone acts on. */
export type SloHealth = "budget_blown" | "at_risk" | "meeting" | "no_data";
export function sloHealth(status: SloStatus | null | undefined): SloHealth {
if (!status || status.no_data) return "no_data";
const remaining = status.error_budget_remaining;
if (remaining === null) return "no_data";
if (remaining <= 0) return "budget_blown";
// Burning faster than the budget allows, but not yet out. Worth surfacing
// separately: it is the window in which someone can still act.
if ((status.burn_rate ?? 0) > 1) return "at_risk";
return "meeting";
}
export function healthTone(health: SloHealth): string {
switch (health) {
case "budget_blown":
return "error";
case "at_risk":
return "warning";
case "meeting":
return "success";
default:
return "neutral";
}
}
export function healthIcon(health: SloHealth): string {
switch (health) {
case "budget_blown":
return "local-fire-department";
case "at_risk":
return "trending-down";
case "meeting":
return "check-circle";
default:
return "help-outline";
}
}
/** An em dash, not "0" or "—0%".
*
* Used everywhere a derived figure is absent. The distinction matters: a
* brand-new SLO has measured nothing, and rendering that as a number invites
* someone to read it as a measurement. */
export const ABSENT = "—";
/** SLI to the 3 decimals the target allows (S-2). Fewer would round 99.9994%
* and 99.9% to the same string, which is the difference between meeting a
* four-nines target and missing it. */
export function formatSli(value: number | null | undefined): string {
if (value === null || value === undefined || !Number.isFinite(value)) return ABSENT;
return `${value.toFixed(3)}%`;
}
export function formatTarget(value: number): string {
// Trailing zeros trimmed: 99.900 reads as noise, 99.9 as a target.
return `${Number(value.toFixed(3))}%`;
}
/** Signed, because an overspent budget is the number people need most. */
export function formatBudget(value: number | null | undefined): string {
if (value === null || value === undefined || !Number.isFinite(value)) return ABSENT;
return `${value > 0 ? "" : ""}${value.toFixed(1)}%`;
}
export function formatBurn(value: number | null | undefined): string {
if (value === null || value === undefined || !Number.isFinite(value)) return ABSENT;
return `×${value.toFixed(1)}`;
}
export function formatCoverage(value: number | null | undefined): string {
if (value === null || value === undefined || !Number.isFinite(value)) return ABSENT;
return `${Math.round(value * 100)}%`;
}
/** `7d` / `30d` / `90d` — the only windows the backend accepts (D31). */
export function formatWindow(secs: number): string {
const days = Math.round(secs / 86400);
return `${days}d`;
}
export function formatSlice(secs: number): string {
return raw(secs === 60 ? "1 min" : `${Math.round(secs / 60)} min`);
}
/** How long until the budget is gone, at the current burn. */
export function formatTimeToExhaust(secs: number | null | undefined): string {
if (secs === null || secs === undefined || secs <= 0) return ABSENT;
const days = Math.floor(secs / 86400);
const hours = Math.floor((secs % 86400) / 3600);
if (days > 0) return `${days}d ${hours}h`;
const mins = Math.floor((secs % 3600) / 60);
return hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
}
export function sliTypeLabel(sliType: string, t: TranslateFn): string {
switch (sliType) {
case "count":
return t("slos.type.count");
case "time_slice":
return t("slos.type.timeSlice");
case "alert":
return t("slos.type.alert");
default:
// An SLI type the UI does not know about — echo the server's token.
return sliType;
}
}
/** Sort worst-budget-first, with frozen SLOs last.
*
* Frozen last on purpose: they are not the worst, they are unknown, and
* putting an unknown at the top of a list sorted by severity trains people
* to ignore the top of the list. */
export function compareByUrgency(a: SloStatus | null, b: SloStatus | null): number {
const av = a && !a.no_data ? a.error_budget_remaining : null;
const bv = b && !b.no_data ? b.error_budget_remaining : null;
if (av === null && bv === null) return 0;
if (av === null) return 1;
if (bv === null) return -1;
return av - bv;
}