mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
fix(ui): improve React Flow dark theme (#7553)
* fix(ui): theme provider topology in dark mode * fix(i18n): isolate topology translation keys * refactor(i18n): reuse existing topology labels --------- Co-authored-by: nguyenha935 <208228297+nguyenha935@users.noreply.github.com>
This commit is contained in:
1
changelog.d/fixes/pending-react-flow-dark-theme.md
Normal file
1
changelog.d/fixes/pending-react-flow-dark-theme.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(ui):** Theme React Flow controls correctly in dark mode, improve idle connector contrast, and localize the provider topology legend.
|
||||
@@ -27,9 +27,14 @@ export function HomeProviderTopologySection({
|
||||
enabled?: boolean;
|
||||
}) {
|
||||
const t = useTranslations("home");
|
||||
const tCommon = useTranslations("common");
|
||||
const tSettings = useTranslations("settings");
|
||||
const tAnalytics = useTranslations("analytics");
|
||||
// #4596: gate the live-WS connection so it only opens while the topology
|
||||
// section is actually shown on the home page.
|
||||
const { activeRequests: liveActiveRequests } = useLiveRequests({ enabled });
|
||||
const activeRequests = selectActiveRequests(liveActiveRequests);
|
||||
const activeProviderCount = new Set(activeRequests.map(({ provider }) => provider)).size;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
@@ -37,24 +42,27 @@ export function HomeProviderTopologySection({
|
||||
<div>
|
||||
<h2 className="text-base font-semibold">{t("providerTopology")}</h2>
|
||||
<p className="text-xs text-text-muted">
|
||||
Connected providers routing through OmniRoute in real time
|
||||
{t("activeError", { active: activeProviderCount, errors: errorProvider ? 1 : 0 })}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-[11px] text-text-muted">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="size-2 rounded-full bg-green-500" /> Active
|
||||
<span className="size-2 rounded-full bg-green-500" />
|
||||
{tCommon("active")}
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="size-2 rounded-full bg-amber-500" /> Recent
|
||||
<span className="size-2 rounded-full bg-amber-500" />
|
||||
{tSettings("recent")}
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="size-2 rounded-full bg-red-500" /> Error
|
||||
<span className="size-2 rounded-full bg-red-500" />
|
||||
{tAnalytics("modelStatusError")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<ProviderTopology
|
||||
providers={providers}
|
||||
activeRequests={selectActiveRequests(liveActiveRequests)}
|
||||
activeRequests={activeRequests}
|
||||
lastProvider={lastProvider}
|
||||
errorProvider={errorProvider}
|
||||
/>
|
||||
|
||||
@@ -482,19 +482,15 @@ select option {
|
||||
color: var(--color-text-main);
|
||||
}
|
||||
|
||||
/* ReactFlow Controls (zoom +/- buttons) dark mode theming */
|
||||
.react-flow__controls-button {
|
||||
background-color: var(--color-surface);
|
||||
border-bottom-color: var(--color-border);
|
||||
fill: var(--color-text-main);
|
||||
}
|
||||
|
||||
.react-flow__controls-button:hover {
|
||||
background-color: var(--color-bg-subtle);
|
||||
}
|
||||
|
||||
.react-flow__controls-button svg {
|
||||
fill: var(--color-text-main);
|
||||
/* React Flow loads its component stylesheet after this global file in some
|
||||
bundles, so direct background/fill declarations can lose to its shorthand
|
||||
rules. Override the supported theme variables on our canvas instead. */
|
||||
.react-flow.omniroute-flow {
|
||||
--xy-controls-button-background-color: var(--color-surface);
|
||||
--xy-controls-button-background-color-hover: var(--color-bg-subtle);
|
||||
--xy-controls-button-color: var(--color-text-main);
|
||||
--xy-controls-button-color-hover: var(--color-text-main);
|
||||
--xy-controls-button-border-color: var(--color-border);
|
||||
}
|
||||
|
||||
/* Desktop sidebar visibility must not depend on Tailwind's hidden/lg:flex cascade
|
||||
|
||||
@@ -121,6 +121,7 @@ export function FlowCanvas({
|
||||
<div ref={containerRef} className={className}>
|
||||
<ReactFlow
|
||||
key={fitKey}
|
||||
className="omniroute-flow"
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
nodeTypes={nodeTypes}
|
||||
|
||||
@@ -10,7 +10,7 @@ export const FLOW_EDGE_COLORS = {
|
||||
active: STATUS_HEX.success,
|
||||
error: STATUS_HEX.error,
|
||||
last: STATUS_HEX.warning,
|
||||
idle: "var(--color-border)",
|
||||
idle: "var(--color-text-muted)",
|
||||
} as const;
|
||||
|
||||
export interface FlowEdgeStyle {
|
||||
@@ -28,5 +28,5 @@ export function edgeStyle(active: boolean, last: boolean, error: boolean): FlowE
|
||||
if (error) return { stroke: FLOW_EDGE_COLORS.error, strokeWidth: 2, opacity: 0.85 };
|
||||
if (active) return { stroke: FLOW_EDGE_COLORS.active, strokeWidth: 2.5, opacity: 1 };
|
||||
if (last) return { stroke: FLOW_EDGE_COLORS.last, strokeWidth: 1.5, opacity: 0.6 };
|
||||
return { stroke: FLOW_EDGE_COLORS.idle, strokeWidth: 1, opacity: 0.2 };
|
||||
return { stroke: FLOW_EDGE_COLORS.idle, strokeWidth: 1, opacity: 0.3 };
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ describe("flow edgeStyles (U0 — extracted from ProviderTopology)", () => {
|
||||
assert.equal(FLOW_EDGE_COLORS.active, "#22c55e");
|
||||
assert.equal(FLOW_EDGE_COLORS.error, "#ef4444");
|
||||
assert.equal(FLOW_EDGE_COLORS.last, "#f59e0b");
|
||||
assert.equal(FLOW_EDGE_COLORS.idle, "var(--color-border)");
|
||||
assert.equal(FLOW_EDGE_COLORS.idle, "var(--color-text-muted)");
|
||||
});
|
||||
|
||||
it("styles an error edge", () => {
|
||||
@@ -37,9 +37,9 @@ describe("flow edgeStyles (U0 — extracted from ProviderTopology)", () => {
|
||||
|
||||
it("styles an idle edge", () => {
|
||||
assert.deepEqual(edgeStyle(false, false, false), {
|
||||
stroke: "var(--color-border)",
|
||||
stroke: "var(--color-text-muted)",
|
||||
strokeWidth: 1,
|
||||
opacity: 0.2,
|
||||
opacity: 0.3,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
// @vitest-environment jsdom
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import React, { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { describe, it, expect, beforeAll, beforeEach, afterEach } from "vitest";
|
||||
@@ -48,7 +50,7 @@ const edges = [{ id: "a-b", source: "a", target: "b" }];
|
||||
describe("FlowCanvas (U0 — shared ReactFlow wrapper)", () => {
|
||||
it("renders the canvas with Controls and hides the attribution", () => {
|
||||
const container = mount(<FlowCanvas nodes={nodes} edges={edges} fitKey="x" />);
|
||||
expect(container.querySelector(".react-flow")).toBeTruthy();
|
||||
expect(container.querySelector(".react-flow.omniroute-flow")).toBeTruthy();
|
||||
expect(container.querySelector(".react-flow__controls")).toBeTruthy();
|
||||
// proOptions.hideAttribution => the attribution element must not render.
|
||||
expect(container.querySelector(".react-flow__attribution")).toBeNull();
|
||||
@@ -60,4 +62,13 @@ describe("FlowCanvas (U0 — shared ReactFlow wrapper)", () => {
|
||||
);
|
||||
expect(container.querySelector(".omni-test-canvas")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("themes controls through React Flow variables so library shorthands cannot override them", () => {
|
||||
const css = readFileSync(resolve("src/app/globals.css"), "utf8");
|
||||
|
||||
expect(css).toContain(".react-flow.omniroute-flow");
|
||||
expect(css).toContain("--xy-controls-button-background-color: var(--color-surface)");
|
||||
expect(css).toContain("--xy-controls-button-color: var(--color-text-main)");
|
||||
expect(css).toContain("--xy-controls-button-border-color: var(--color-border)");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,7 +15,10 @@ vi.mock("next-intl", () => ({
|
||||
}));
|
||||
vi.mock("next/dynamic", () => ({
|
||||
default: () => (props: Record<string, unknown>) => (
|
||||
<div data-testid="provider-topology" data-providers={String((props.providers as unknown[])?.length ?? 0)} />
|
||||
<div
|
||||
data-testid="provider-topology"
|
||||
data-providers={String((props.providers as unknown[])?.length ?? 0)}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
vi.mock("@/shared/components", () => ({
|
||||
@@ -26,9 +29,8 @@ vi.mock("@/hooks/useLiveDashboard", () => ({
|
||||
useLiveRequests: () => liveRequestsMock(),
|
||||
}));
|
||||
|
||||
const { HomeProviderTopologySection } = await import(
|
||||
"../../../src/app/(dashboard)/dashboard/HomeProviderTopologySection"
|
||||
);
|
||||
const { HomeProviderTopologySection } =
|
||||
await import("../../../src/app/(dashboard)/dashboard/HomeProviderTopologySection");
|
||||
|
||||
let container: HTMLDivElement;
|
||||
let root: ReturnType<typeof createRoot>;
|
||||
@@ -63,4 +65,8 @@ it("renders the topology card and forwards providers to ProviderTopology", () =>
|
||||
const topology = container.querySelector("[data-testid='provider-topology']");
|
||||
expect(topology).not.toBeNull();
|
||||
expect(topology?.getAttribute("data-providers")).toBe("2");
|
||||
expect(container.textContent).toContain("activeError");
|
||||
expect(container.textContent).toContain("active");
|
||||
expect(container.textContent).toContain("recent");
|
||||
expect(container.textContent).toContain("modelStatusError");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user