mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-04 16:09:05 +00:00
# Design System Unification — one visual language, enforced end-to-end
Establishes **one source of truth for every visual decision**, wires the
whole app to it, and **locks it in with CI** so it can't drift back.
### Foundations
- **Design-token system ("one knob per decision").** Every colour, text
size, spacing, corner radius, and shadow is a named token defined in
exactly one place — change the token, every screen updates. Collapsed
hundreds of near-duplicate definitions and removed "fake" duplicates
that pretended to be different but weren't.
- **Shared component library (O2).** Buttons, tables, dialogs, drawers,
form fields, badges, etc. already know the correct colours, spacing, and
dark-mode behaviour; new screens compose them instead of hand-styling.
Added new primitives on this branch (`OContent`, `KpiCard`) and
standardized existing ones.
- **One page structure everywhere** (`PageLayout` / `AppPageHeader` /
`AppPage` / `OContent`) — titles, breadcrumbs, spacing, and content
edges line up identically across every screen.
- **One table implementation** — all tabular data uses the same table
(sorting, density, sticky headers, empty states).
- **Styling consolidated onto Tailwind; old systems removed** —
hand-written CSS driven to the practical minimum, the legacy **Quasar**
framework fully removed, surviving stylesheets required to justify
themselves.
- **Two-tier corner radius** — a tight **4px** for controls and a soft
**12px** for surfaces (dialogs, cards, the app shell), plus a
full-circle option — replacing five radius names that all secretly meant
the same thing.
### Detailed improvements across screens
**Page structure & alignment** — unified page shell rolled across ~40+
screens (IAM, Settings, Alerts, Dashboards, Functions, Pipelines, RUM,
Metrics, Synthetics, Billing, editors, lists); one header height; one
page-edge grid driving headers, tables, rails, and tabs; unified
side-rail widths; inset rail selection/hover pills; every tab strip on
the grid line; tightened title/subtitle spacing.
**Tables (cross-screen)** — action columns consistently centered (incl.
Organizations); unified row-number column with no "#" truncation;
non-standard tables restyled to match; loading skeletons made
geometrically identical to the loaded table; symmetric edge inset; IAM
first-column grid alignment fixed.
**Traces** — flame graph + span-detail correlation panel fixes;
correlated logs/metrics placeholder fit; sticky headers; removed a
double scrollbar + a 1px sliver; aligned table fonts/badges; polished
the service detail drawer; stream-selector + correlation alignment;
aligned loading spinners; floating overlay arrows for the
disabled-services strip.
**Logs** — field-list controls aligned (focus ring no longer clipped);
detail-drawer spacing + metrics-tab overflow fixed; Run Query toolbar
tidied; listing sizing + stray border fixed.
**Metrics** — search added to the "Type" facet rail and aligned with the
other rails; proper empty states for the explorer and facet rail.
**Dashboards** — removed inconsistent card shadows; tightened
borders/resizers/field-list spacing; aligned View Dashboard chrome to
the grid; chart colours via the token helper.
**Home** — redesigned the Usage tab; added reusable KPI stat cards so
summary strips stop drifting.
**Dark mode** — new theme-switch animation; fixed a 1px content shift on
toggle and a white flash on load; fixed AI-chat text and assorted
light/dark mismatches; completed the token-driven dark migration.
**Empty states, controls & small polish** — restored 3 broken
empty-state animations; adopted the standard empty state; added the
sliding indicator to the toggle control; fixed the Quality-page inset +
drawer badge; refreshed Search History / Schedulers; fixed the
alert-history drawer scrollbar + timeline hover; removed a stray
menu-bar border.
### Dead code & cruft removal
- **Deleted the entire legacy `utilities.css`** global stylesheet (well
over a thousand lines) — evacuated in waves, roughly half of it dead
once traced.
- **Removed Quasar entirely**, including dead `.q-*` styles and ~25
phantom `--q-*` variables that were silently breaking bits of the UI.
- Stripped hundreds of **dead CSS classes and tokens that compiled to
nothing** — e.g. 46 uses of a colour class with no backing token, a
no-op shadow class on 51 elements, dead layout/utility classes, a
leftover `card-container` block, a dead table-actions token, and a
mis-registered gradient that rendered nothing.
- Cut **unused keyframe animations (19 → 3)** and removed **unused
components** + a stray build artifact.
- Net effect: a much smaller, leaner styling surface — less to maintain,
fewer dead ends to copy.
## Safety & verification
Large but low-risk: this is overwhelmingly a **styling and structure**
change, not product behaviour or data. Guarded by the
design-token/consistency/type checks above plus the existing test suite,
and reviewed screen-by-screen.
---------
Co-authored-by: Kirtan Thakkar <kirtan@kiara.tech>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: ktx-mihir <mihir.patel@kiara.tech>
145 lines
5.5 KiB
JavaScript
145 lines
5.5 KiB
JavaScript
#!/usr/bin/env node
|
|
// Copyright 2026 OpenObserve Inc.
|
|
//
|
|
// Pre-flight before deleting any CSS class rule (O2_STYLE_MIGRATION_PLAN.md §7.3).
|
|
// Answers: "who actually uses this class?" — covering the ways a class name can
|
|
// reach the DOM and escape a naive template grep (plan risks R1 / R5):
|
|
//
|
|
// 1. static class="… foo …"
|
|
// 2. bound :class="{ foo: cond }" / :class="['foo']" / :class="x ? 'foo' : ''"
|
|
// 3. JS classList.add('foo'), `status-${x}` fragments, string literals in .ts
|
|
// 4. CSS the rules that DEFINE it (so you can see every definition site)
|
|
// 5. specs assertions on the class (these break when you delete it)
|
|
//
|
|
// Usage:
|
|
// node scripts/find-class-consumers.mjs logPage_bkcss histogram-container
|
|
// node scripts/find-class-consumers.mjs --quiet foo # exit 1 if consumers exist
|
|
//
|
|
// Verdict per class:
|
|
// NO CONSUMERS → safe to delete
|
|
// SELF-CONTAINED → only the defining file uses it → colocate
|
|
// HAS CONSUMERS → migrate consumers in the same PR
|
|
|
|
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
import { join, extname, dirname } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const SRC_DIR = join(__dirname, "..", "src");
|
|
const EXTS = new Set([".css", ".vue", ".ts", ".js", ".scss"]);
|
|
|
|
const args = process.argv.slice(2);
|
|
const quiet = args.includes("--quiet");
|
|
const names = args.filter((a) => !a.startsWith("--"));
|
|
|
|
if (names.length === 0) {
|
|
console.error("Usage: node scripts/find-class-consumers.mjs <className>...");
|
|
process.exit(2);
|
|
}
|
|
|
|
function walk(dir, files = []) {
|
|
for (const entry of readdirSync(dir)) {
|
|
if (entry === "node_modules" || entry === "dist") continue;
|
|
const full = join(dir, entry);
|
|
const stat = statSync(full);
|
|
if (stat.isDirectory()) walk(full, files);
|
|
else if (EXTS.has(extname(entry))) files.push(full);
|
|
}
|
|
return files;
|
|
}
|
|
|
|
const files = walk(SRC_DIR);
|
|
const cache = new Map();
|
|
for (const f of files) cache.set(f, readFileSync(f, "utf8").split("\n"));
|
|
|
|
const rel = (f) => f.replace(SRC_DIR + "/", "src/");
|
|
|
|
/**
|
|
* Split a .vue file into template / script / style line ranges so we can say
|
|
* *where* a hit lives — a hit in <style> is a definition, in <template> a consumer.
|
|
*/
|
|
function sectionOf(file, lines, lineIdx) {
|
|
if (extname(file) !== ".vue") return extname(file) === ".css" || extname(file) === ".scss" ? "style" : "script";
|
|
let section = "script";
|
|
for (let i = 0; i <= lineIdx; i++) {
|
|
const l = lines[i];
|
|
if (/^\s*<template[\s>]/i.test(l)) section = "template";
|
|
else if (/^\s*<script[\s>]/i.test(l)) section = "script";
|
|
else if (/^\s*<style[\s>]/i.test(l)) section = "style";
|
|
}
|
|
return section;
|
|
}
|
|
|
|
let anyConsumers = false;
|
|
|
|
for (const name of names) {
|
|
const esc = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
|
|
// A definition: `.name` appearing in a selector position inside CSS.
|
|
const defRe = new RegExp(`\\.${esc}(?![\\w-])`);
|
|
// A usage: the bare word inside a class-ish string context.
|
|
const wordRe = new RegExp(`(?<![\\w-])${esc}(?![\\w-])`);
|
|
// Dynamic construction: `status-${x}` style — flag prefixes of the name.
|
|
const parts = name.split("-");
|
|
const dynRes = [];
|
|
for (let i = 1; i < parts.length; i++) {
|
|
const prefix = parts.slice(0, i).join("-");
|
|
dynRes.push(new RegExp("[\"'`]" + prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "-\\$\\{"));
|
|
}
|
|
|
|
const definitions = [];
|
|
const consumers = [];
|
|
const specs = [];
|
|
const dynamic = [];
|
|
|
|
for (const [file, lines] of cache) {
|
|
lines.forEach((line, i) => {
|
|
const section = sectionOf(file, lines, i);
|
|
const isSpec = /\.spec\.(ts|js)$/.test(file);
|
|
|
|
if (wordRe.test(line)) {
|
|
const hit = { file: rel(file), line: i + 1, text: line.trim().slice(0, 100) };
|
|
if (section === "style" && defRe.test(line)) definitions.push(hit);
|
|
else if (isSpec) specs.push(hit);
|
|
else if (section === "template" || section === "script") consumers.push(hit);
|
|
else if (defRe.test(line)) definitions.push(hit);
|
|
}
|
|
for (const dre of dynRes) {
|
|
if (dre.test(line)) dynamic.push({ file: rel(file), line: i + 1, text: line.trim().slice(0, 100) });
|
|
}
|
|
});
|
|
}
|
|
|
|
const defFiles = new Set(definitions.map((d) => d.file));
|
|
const consumerFiles = new Set(consumers.map((c) => c.file));
|
|
const external = [...consumerFiles].filter((f) => !defFiles.has(f));
|
|
|
|
let verdict;
|
|
if (consumers.length === 0 && specs.length === 0) verdict = "NO CONSUMERS — safe to delete";
|
|
else if (external.length === 0) verdict = "SELF-CONTAINED — colocate into the defining file";
|
|
else verdict = `HAS CONSUMERS (${external.length} external file(s)) — migrate them in the same PR`;
|
|
|
|
if (verdict.startsWith("HAS")) anyConsumers = true;
|
|
|
|
if (quiet) continue;
|
|
|
|
console.log(`\n\x1b[1m.${name}\x1b[0m — ${verdict}`);
|
|
|
|
const show = (label, arr) => {
|
|
if (arr.length === 0) return;
|
|
console.log(` ${label} (${arr.length}):`);
|
|
for (const h of arr.slice(0, 12)) console.log(` ${h.file}:${h.line} ${h.text}`);
|
|
if (arr.length > 12) console.log(` ...and ${arr.length - 12} more`);
|
|
};
|
|
|
|
show("definitions", definitions);
|
|
show("consumers", consumers);
|
|
show("specs (update in-PR)", specs);
|
|
if (dynamic.length > 0) {
|
|
console.log(` \x1b[33mdynamic-construction candidates\x1b[0m (${dynamic.length}) — review manually:`);
|
|
for (const h of dynamic.slice(0, 6)) console.log(` ${h.file}:${h.line} ${h.text}`);
|
|
}
|
|
}
|
|
|
|
process.exit(quiet && anyConsumers ? 1 : 0);
|