From cfd781ee2206bbff2836d35923a2f6686eef9b08 Mon Sep 17 00:00:00 2001 From: Oliver Newland Date: Fri, 9 Oct 2026 13:39:24 -0700 Subject: [PATCH 1/8] feat(admin): Solarized dark theme and tiled home page Replace the home page sidebar with a grid of tool tiles that have descriptions and a hover effect, and show regions as a row of buttons colored to match the header banner. Switch the admin UI to a solarized dark palette, including the Mantine and Bootstrap themes and the SQL editor, and move the "MEREDITH IS #1" line into a low-contrast diagonal tiling on the page background. Move the less used tools (Snubsplain, SnQL to SQL, EAP Stats, Replacements) to the end of the list. Co-Authored-By: Claude Opus 5.5 (1M context) --- snuba/admin/dist/index.html | 64 +++++++- snuba/admin/static/body.tsx | 4 +- .../admin/static/common/components/theme.tsx | 52 ++++--- snuba/admin/static/copy_tables/index.tsx | 2 +- snuba/admin/static/data.tsx | 90 ++++++++--- snuba/admin/static/header.tsx | 15 +- snuba/admin/static/index.tsx | 29 +++- snuba/admin/static/nav.tsx | 13 +- snuba/admin/static/theme.tsx | 92 +++++++++--- snuba/admin/static/welcome/index.tsx | 142 +++++++++++++++--- 10 files changed, 407 insertions(+), 96 deletions(-) diff --git a/snuba/admin/dist/index.html b/snuba/admin/dist/index.html index e4efa0c6301..f4a33acc15c 100644 --- a/snuba/admin/dist/index.html +++ b/snuba/admin/dist/index.html @@ -1,10 +1,12 @@ - + Snuba admin + + - -
diff --git a/snuba/admin/static/body.tsx b/snuba/admin/static/body.tsx index 6c7b74bcc93..5b1213c348e 100644 --- a/snuba/admin/static/body.tsx +++ b/snuba/admin/static/body.tsx @@ -20,8 +20,8 @@ function Body(props: Props) { } const bodyStyle = { - width: "100%", - maxWidth: "calc(100% - 260px)", + flex: 1, + minWidth: 0, margin: 10, fontSize: 20, }; diff --git a/snuba/admin/static/common/components/theme.tsx b/snuba/admin/static/common/components/theme.tsx index 8fe75e4e927..c9a1e8f041f 100644 --- a/snuba/admin/static/common/components/theme.tsx +++ b/snuba/admin/static/common/components/theme.tsx @@ -1,42 +1,60 @@ import { EditorView } from "@codemirror/view"; import { HighlightStyle, syntaxHighlighting } from "@codemirror/language"; import { tags } from "@lezer/highlight"; +import { SOLARIZED } from "SnubaAdmin/theme"; -export const theme = EditorView.theme({ - "&": { - fontSize: "14px", +export const theme = EditorView.theme( + { + "&": { + fontSize: "14px", + color: SOLARIZED.BASE1, + backgroundColor: SOLARIZED.BASE03, + border: `1px solid ${SOLARIZED.BASE01}`, + borderRadius: "4px", + }, + ".cm-content": { caretColor: SOLARIZED.BASE1 }, + ".cm-cursor, .cm-dropCursor": { borderLeftColor: SOLARIZED.BASE1 }, + "&.cm-focused .cm-selectionBackground, .cm-selectionBackground, ::selection": + { backgroundColor: SOLARIZED.BASE02 }, + ".cm-activeLine": { backgroundColor: "transparent" }, + ".cm-gutters": { + backgroundColor: SOLARIZED.BASE02, + color: SOLARIZED.BASE01, + border: "none", + }, }, -}); + { dark: true }, +); export const highlighting = syntaxHighlighting( HighlightStyle.define([ { tag: [tags.standard(tags.tagName), tags.tagName], - color: "#116329", + color: SOLARIZED.GREEN, fontSize: 10, }, - { tag: [tags.comment, tags.bracket], color: "#6a737d" }, - { tag: [tags.className, tags.propertyName], color: "#6f42c1" }, + { tag: [tags.comment, tags.bracket], color: SOLARIZED.BASE01 }, + { tag: [tags.className, tags.propertyName], color: SOLARIZED.VIOLET }, { tag: [tags.variableName, tags.attributeName, tags.number, tags.operator], - color: "#005cc5", + color: SOLARIZED.BLUE, }, { tag: [tags.keyword, tags.typeName, tags.typeOperator, tags.typeName], - color: "#d73a49", + color: SOLARIZED.GREEN, }, - { tag: [tags.string, tags.meta, tags.regexp], color: "#032f62" }, - { tag: [tags.name, tags.quote], color: "#22863a" }, - { tag: [tags.heading, tags.strong], color: "#24292e", fontWeight: "bold" }, - { tag: [tags.emphasis], color: "#24292e", fontStyle: "italic" }, - { tag: [tags.deleted], color: "#b31d28", backgroundColor: "ffeef0" }, + { tag: [tags.string, tags.meta, tags.regexp], color: SOLARIZED.CYAN }, + { tag: [tags.name, tags.quote], color: SOLARIZED.YELLOW }, + { tag: [tags.heading, tags.strong], color: SOLARIZED.BASE2, fontWeight: "bold" }, + { tag: [tags.emphasis], color: SOLARIZED.BASE2, fontStyle: "italic" }, + { tag: [tags.deleted], color: SOLARIZED.RED }, { tag: [tags.atom, tags.bool, tags.special(tags.variableName)], - color: "#e36209", + color: SOLARIZED.ORANGE, }, - { tag: [tags.url, tags.escape, tags.regexp, tags.link], color: "#032f62" }, + { tag: [tags.url, tags.escape, tags.regexp, tags.link], color: SOLARIZED.CYAN }, { tag: tags.link, textDecoration: "underline" }, { tag: tags.strikethrough, textDecoration: "line-through" }, - { tag: tags.invalid, color: "#cb2431" }, + { tag: tags.invalid, color: SOLARIZED.RED }, ]), ); diff --git a/snuba/admin/static/copy_tables/index.tsx b/snuba/admin/static/copy_tables/index.tsx index 50ee032c04a..5d8cff2801a 100644 --- a/snuba/admin/static/copy_tables/index.tsx +++ b/snuba/admin/static/copy_tables/index.tsx @@ -300,7 +300,7 @@ const copyTableResultStyle = { const sectionContainerStyle = { padding: 15, - backgroundColor: COLORS.WHITE, + backgroundColor: COLORS.PANEL_BG, border: `1px solid ${COLORS.BORDER_GRAY}`, borderRadius: 5, marginTop: 0, diff --git a/snuba/admin/static/data.tsx b/snuba/admin/static/data.tsx index f66c0529b61..366c3352d70 100644 --- a/snuba/admin/static/data.tsx +++ b/snuba/admin/static/data.tsx @@ -16,87 +16,137 @@ import RpcEndpoints from "SnubaAdmin/rpc_endpoints"; import EapStats from "SnubaAdmin/eap_stats"; const NAV_ITEMS = [ - { id: "overview", display: "🤿 Snuba Admin", component: Welcome }, { - id: "auto-replacements-bypass-projects", - display: "👻 Replacements", - component: AutoReplacementsBypassProjects, - }, - { - id: "snql-to-sql", - display: "🌐 SnQL to SQL", - component: SnQLToSQL, - }, - { - id: "snuba-explain", - display: "🩺 Snubsplain", - component: SnubaExplain, + id: "overview", + display: "🤿 Snuba Admin", + description: "", + component: Welcome, }, { id: "clusters", display: "🗄️ ClickHouse Clusters", + description: + "Inspect the ClickHouse clusters, nodes and storages in this region.", component: Clusters, }, { id: "system-queries", display: "🏚️ System Queries", + description: + "Run predefined queries against ClickHouse system tables.", component: ClickhouseQueries, }, { id: "system-shell", display: "💻 System Shell", + description: + "Interactive SQL shell for querying ClickHouse system tables.", component: SystemShellPage, }, { id: "copy-tables", display: "📋 Copy Tables", + description: + "Copy table definitions from a source ClickHouse host to other hosts.", component: CopyTables, }, { id: "clickhouse-migrations", display: "🚧 ClickHouse Migrations", + description: + "Check migration status and run or reverse migrations by group.", component: ClickhouseMigrations, }, { id: "tracing", display: "🔎 ClickHouse Tracing", + description: + "Run a query with tracing enabled and inspect ClickHouse's trace logs.", component: TracingQueries, }, { id: "tracing-shell", display: "💻 Tracing Shell", + description: + "Interactive SQL shell that runs queries with ClickHouse tracing.", component: SQLShellPage, }, { id: "rpc-endpoints", display: "🔌 RPC Endpoints", + description: + "Send requests to EAP RPC endpoints and inspect the responses.", component: RpcEndpoints, }, { id: "querylog", display: "🔍 ClickHouse Querylog", + description: + "Search the Snuba querylog for what ran, who ran it and how it performed.", component: QuerylogQueries, }, - { - id: "eap-stats", - display: "📊 EAP Stats", - component: EapStats, - }, { id: "outcomes-analyzer", display: "📈 Outcomes Analyzer", + description: + "Query outcomes to see accepted, filtered and dropped events.", component: OutcomesAnalyzer, }, { id: "production-queries", display: "🔦 Production Queries", + description: + "Run SnQL queries against production data for allowed projects.", component: ProductionQueries, }, { id: "run-custom-jobs", display: "▶️ View/Run Custom Jobs", + description: + "List manual jobs, check their status and run them.", component: ViewCustomJobs, }, + { + id: "snuba-explain", + display: "🩺 Snubsplain", + description: + "Step through how Snuba processes a query, from parsing to the final SQL.", + component: SnubaExplain, + }, + { + id: "snql-to-sql", + display: "🌐 SnQL to SQL", + description: + "Translate a SnQL query into the ClickHouse SQL that Snuba would run.", + component: SnQLToSQL, + }, + { + id: "eap-stats", + display: "📊 EAP Stats", + description: + "Sample EAP data to see item and attribute coverage.", + component: EapStats, + }, + { + id: "auto-replacements-bypass-projects", + display: "👻 Replacements", + description: + "See which projects are currently bypassing the replacements pipeline.", + component: AutoReplacementsBypassProjects, + }, ]; -export { NAV_ITEMS }; +// Shell pages inherit permissions from their parent pages +const PERMISSION_OVERRIDES: Record = { + "tracing-shell": "tracing", + "system-shell": "system-queries", +}; + +function isToolAllowed(itemId: string, allowedTools: string[] | null): boolean { + const permissionId = PERMISSION_OVERRIDES[itemId] ?? itemId; + return ( + allowedTools?.includes(permissionId) || allowedTools?.includes("all") || false + ); +} + +export { NAV_ITEMS, isToolAllowed }; diff --git a/snuba/admin/static/header.tsx b/snuba/admin/static/header.tsx index 6e39d905f38..bb9cb3e7146 100644 --- a/snuba/admin/static/header.tsx +++ b/snuba/admin/static/header.tsx @@ -26,11 +26,9 @@ function Header() { return (
- Snuba admin + + Snuba admin +
{ + function onHashChange() { + try { + setActiveTab(getTab(window.location.hash)); + } catch {} + } + window.addEventListener("hashchange", onHashChange); + return () => window.removeEventListener("hashchange", onHashChange); + }, []); + function navigate(nextTab: string) { setActiveTab(nextTab); window.location.hash = nextTab; } return ( - +
-
diff --git a/snuba/admin/static/nav.tsx b/snuba/admin/static/nav.tsx index 3b089077692..1d2ed4644b1 100644 --- a/snuba/admin/static/nav.tsx +++ b/snuba/admin/static/nav.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import { COLORS } from "SnubaAdmin/theme"; -import { NAV_ITEMS } from "SnubaAdmin/data"; +import { NAV_ITEMS, isToolAllowed } from "SnubaAdmin/data"; import Client from "SnubaAdmin/api_client"; type NavProps = { @@ -29,11 +29,7 @@ function Nav(props: NavProps) {
+ + )} +
+

Tools

+
+ {tools.map((item) => { + const [icon, ...title] = item.display.split(" "); + return ( + + {icon} + {title.join(" ")} + {item.description} + + ); + })} +
+
+ + ); } +const containerStyle = { + padding: "10px 20px", +}; + +const sectionStyle = { + marginBottom: 30, +}; + +const headingStyle = { + fontSize: 14, + fontWeight: 700, + letterSpacing: "0.1em", + textTransform: "uppercase" as const, + color: COLORS.TEXT_LIGHTER, + margin: "0 0 12px 0", +}; + +const regionRowStyle = { + display: "flex", + flexWrap: "wrap" as const, + gap: 10, +}; + +// Same treatment as the region banner in the header. +const regionButtonStyle = { + color: COLORS.REGION_TEXT, + fontSize: 16, + fontWeight: 700, + letterSpacing: "0.05em", + textTransform: "uppercase" as const, + textDecoration: "none", + padding: "6px 16px", + borderRadius: 4, + whiteSpace: "nowrap" as const, +}; + +const tileGridStyle = { + display: "grid", + gridTemplateColumns: "repeat(auto-fill, minmax(260px, 1fr))", + gap: 16, +}; + +const tileStyle = { + display: "flex", + flexDirection: "column" as const, + gap: 6, + padding: 18, + borderRadius: 8, + textDecoration: "none", + color: COLORS.TEXT_DEFAULT, +}; + +const tileIconStyle = { + fontSize: 28, + lineHeight: 1, +}; + +const tileTitleStyle = { + fontSize: 18, + fontWeight: 700, +}; + +const tileDescriptionStyle = { + fontSize: 14, + lineHeight: 1.4, + color: COLORS.TEXT_LIGHTER, +}; + export default Welcome; From 94733dab00db2fe01a085e959c71a73fac65613f Mon Sep 17 00:00:00 2001 From: Oliver Newland Date: Fri, 9 Oct 2026 13:49:54 -0700 Subject: [PATCH 2/8] fix(admin): Show an error when the home page can't load tools If /tools failed or returned an unexpected body, the tool grid stayed empty with no explanation. Catch the failure and show it under the Tools heading. Co-Authored-By: Claude Opus 5.5 (1M context) --- snuba/admin/static/welcome/index.tsx | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/snuba/admin/static/welcome/index.tsx b/snuba/admin/static/welcome/index.tsx index afee763bf1c..e14adbfda27 100644 --- a/snuba/admin/static/welcome/index.tsx +++ b/snuba/admin/static/welcome/index.tsx @@ -8,14 +8,23 @@ import { regionColor } from "SnubaAdmin/utils/region_color"; function Welcome(props: { api: Client }) { const [adminRegions, setAdminRegions] = useState([]); const [allowedTools, setAllowedTools] = useState(null); + const [toolsError, setToolsError] = useState(null); useEffect(() => { props.api.getAdminRegions().then((res) => { setAdminRegions(res); }); - props.api.getAllowedTools().then((res) => { - setAllowedTools(res.tools); - }); + props.api + .getAllowedTools() + .then((res) => { + if (!Array.isArray(res?.tools)) { + throw new Error("unexpected response from /tools"); + } + setAllowedTools(res.tools); + }) + .catch((err) => { + setToolsError(err?.message ?? String(err)); + }); }, []); const tools = NAV_ITEMS.filter( @@ -50,6 +59,11 @@ function Welcome(props: { api: Client }) { )}

Tools

+ {toolsError && ( +

+ Couldn't load the tools you have access to: {toolsError} +

+ )}
{tools.map((item) => { const [icon, ...title] = item.display.split(" "); @@ -140,4 +154,9 @@ const tileDescriptionStyle = { color: COLORS.TEXT_LIGHTER, }; +const errorStyle = { + fontSize: 14, + color: COLORS.ERROR, +}; + export default Welcome; From 0864c54a839732e93ea5b1cae67c02f6c3d9480d Mon Sep 17 00:00:00 2001 From: Oliver Newland Date: Fri, 9 Oct 2026 13:52:37 -0700 Subject: [PATCH 3/8] feat(admin): Put the regions heading on the same line as its pills Co-Authored-By: Claude Opus 5.5 (1M context) --- snuba/admin/static/welcome/index.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/snuba/admin/static/welcome/index.tsx b/snuba/admin/static/welcome/index.tsx index e14adbfda27..0fa486969d2 100644 --- a/snuba/admin/static/welcome/index.tsx +++ b/snuba/admin/static/welcome/index.tsx @@ -34,8 +34,8 @@ function Welcome(props: { api: Client }) { return (
{adminRegions.length > 0 && ( -
-

Regions

+
+

Regions

{adminRegions.map((region) => ( Date: Fri, 9 Oct 2026 14:20:05 -0700 Subject: [PATCH 4/8] feat(admin): Switch to a high-contrast light theme Replace the solarized dark palette with near-black text on near-white surfaces, dropping the Mantine and Bootstrap dark-theme overrides and returning the SQL editor to its light syntax colors. Draw the slogan tiling at a steeper angle, wrapping each copy so it tiles seamlessly, and only show it on the home page. Co-Authored-By: Claude Opus 5.5 (1M context) --- snuba/admin/dist/index.html | 49 ++------- .../admin/static/common/components/theme.tsx | 56 ++++------ snuba/admin/static/header.tsx | 12 +- snuba/admin/static/index.tsx | 19 ++-- snuba/admin/static/nav.tsx | 2 +- snuba/admin/static/theme.tsx | 103 +++++++----------- 6 files changed, 86 insertions(+), 155 deletions(-) diff --git a/snuba/admin/dist/index.html b/snuba/admin/dist/index.html index f4a33acc15c..cda04b48fba 100644 --- a/snuba/admin/dist/index.html +++ b/snuba/admin/dist/index.html @@ -1,5 +1,5 @@ - + @@ -14,31 +14,6 @@ -webkit-font-smoothing: antialiased; } - /* Solarized dark for bootstrap components. Declared after bundle.css so - it takes precedence over bootstrap's own dark theme variables. */ - [data-bs-theme=dark] { - --bs-body-bg: #002b36; - --bs-body-color: #93a1a1; - --bs-emphasis-color: #eee8d5; - --bs-secondary-color: #839496; - --bs-secondary-bg: #073642; - --bs-tertiary-bg: #073642; - --bs-border-color: #586e75; - --bs-link-color: #268bd2; - --bs-link-color-rgb: 38, 139, 210; - --bs-link-hover-color: #2aa198; - --bs-link-hover-color-rgb: 42, 161, 152; - --bs-heading-color: #eee8d5; - } - - /* Default for bare form controls; :where() keeps specificity at zero so - mantine and bootstrap styles still win. */ - :where(input:not([type=checkbox]):not([type=radio]), textarea, select) { - background-color: #073642; - color: #93a1a1; - border-color: #586e75; - } - fieldset { border: none; padding: 0; @@ -47,29 +22,29 @@ /* Hack since we can't put hover/active pseudo selectors in inline styles */ .nav-link:hover { - background-color: rgba(147, 161, 161, 0.08); + background-color: #e6e6e6; } .nav-link:active { - background-color: rgba(147, 161, 161, 0.16); + background-color: #d6d6d6; } .nav-link-active:hover { - background-color: rgba(38, 139, 210, 0.22); + background-color: rgba(31, 111, 235, 0.18); } .tool-tile { - background-color: rgba(7, 54, 66, 0.85); - border: 1px solid #0e4553; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); + background-color: #ffffff; + border: 1px solid #d4d4d4; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06); transition: transform 150ms ease, box-shadow 150ms ease, border-color 150ms ease, background-color 150ms ease; } .tool-tile:hover { transform: translateY(-3px); - background-color: rgba(14, 69, 83, 0.95); - border-color: #268bd2; - box-shadow: 0 8px 20px rgba(0, 0, 0, 0.35), - 0 0 0 1px rgba(38, 139, 210, 0.4); + background-color: #ffffff; + border-color: #1f6feb; + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12), + 0 0 0 1px rgba(31, 111, 235, 0.35); } .tool-tile:active { transform: translateY(-1px); @@ -81,7 +56,7 @@ .region-button:hover { transform: translateY(-2px); filter: brightness(1.08); - box-shadow: 0 6px 14px rgba(0, 0, 0, 0.35); + box-shadow: 0 6px 14px rgba(0, 0, 0, 0.18); } diff --git a/snuba/admin/static/common/components/theme.tsx b/snuba/admin/static/common/components/theme.tsx index c9a1e8f041f..dfc05af57ff 100644 --- a/snuba/admin/static/common/components/theme.tsx +++ b/snuba/admin/static/common/components/theme.tsx @@ -1,60 +1,46 @@ import { EditorView } from "@codemirror/view"; import { HighlightStyle, syntaxHighlighting } from "@codemirror/language"; import { tags } from "@lezer/highlight"; -import { SOLARIZED } from "SnubaAdmin/theme"; +import { COLORS } from "SnubaAdmin/theme"; -export const theme = EditorView.theme( - { - "&": { - fontSize: "14px", - color: SOLARIZED.BASE1, - backgroundColor: SOLARIZED.BASE03, - border: `1px solid ${SOLARIZED.BASE01}`, - borderRadius: "4px", - }, - ".cm-content": { caretColor: SOLARIZED.BASE1 }, - ".cm-cursor, .cm-dropCursor": { borderLeftColor: SOLARIZED.BASE1 }, - "&.cm-focused .cm-selectionBackground, .cm-selectionBackground, ::selection": - { backgroundColor: SOLARIZED.BASE02 }, - ".cm-activeLine": { backgroundColor: "transparent" }, - ".cm-gutters": { - backgroundColor: SOLARIZED.BASE02, - color: SOLARIZED.BASE01, - border: "none", - }, +export const theme = EditorView.theme({ + "&": { + fontSize: "14px", + backgroundColor: COLORS.PANEL_BG, + border: `1px solid ${COLORS.BORDER_GRAY}`, + borderRadius: "4px", }, - { dark: true }, -); +}); export const highlighting = syntaxHighlighting( HighlightStyle.define([ { tag: [tags.standard(tags.tagName), tags.tagName], - color: SOLARIZED.GREEN, + color: "#116329", fontSize: 10, }, - { tag: [tags.comment, tags.bracket], color: SOLARIZED.BASE01 }, - { tag: [tags.className, tags.propertyName], color: SOLARIZED.VIOLET }, + { tag: [tags.comment, tags.bracket], color: "#6a737d" }, + { tag: [tags.className, tags.propertyName], color: "#6f42c1" }, { tag: [tags.variableName, tags.attributeName, tags.number, tags.operator], - color: SOLARIZED.BLUE, + color: "#005cc5", }, { tag: [tags.keyword, tags.typeName, tags.typeOperator, tags.typeName], - color: SOLARIZED.GREEN, + color: "#d73a49", }, - { tag: [tags.string, tags.meta, tags.regexp], color: SOLARIZED.CYAN }, - { tag: [tags.name, tags.quote], color: SOLARIZED.YELLOW }, - { tag: [tags.heading, tags.strong], color: SOLARIZED.BASE2, fontWeight: "bold" }, - { tag: [tags.emphasis], color: SOLARIZED.BASE2, fontStyle: "italic" }, - { tag: [tags.deleted], color: SOLARIZED.RED }, + { tag: [tags.string, tags.meta, tags.regexp], color: "#032f62" }, + { tag: [tags.name, tags.quote], color: "#22863a" }, + { tag: [tags.heading, tags.strong], color: "#24292e", fontWeight: "bold" }, + { tag: [tags.emphasis], color: "#24292e", fontStyle: "italic" }, + { tag: [tags.deleted], color: "#b31d28", backgroundColor: "ffeef0" }, { tag: [tags.atom, tags.bool, tags.special(tags.variableName)], - color: SOLARIZED.ORANGE, + color: "#e36209", }, - { tag: [tags.url, tags.escape, tags.regexp, tags.link], color: SOLARIZED.CYAN }, + { tag: [tags.url, tags.escape, tags.regexp, tags.link], color: "#032f62" }, { tag: tags.link, textDecoration: "underline" }, { tag: tags.strikethrough, textDecoration: "line-through" }, - { tag: tags.invalid, color: SOLARIZED.RED }, + { tag: tags.invalid, color: "#cb2431" }, ]), ); diff --git a/snuba/admin/static/header.tsx b/snuba/admin/static/header.tsx index bb9cb3e7146..9203a8fb6aa 100644 --- a/snuba/admin/static/header.tsx +++ b/snuba/admin/static/header.tsx @@ -27,7 +27,11 @@ function Header() { return (
- Snuba admin + Snuba admin
+ -
+
{activeTab !== "overview" && ( diff --git a/snuba/admin/static/nav.tsx b/snuba/admin/static/nav.tsx index 1d2ed4644b1..d11ec4bb268 100644 --- a/snuba/admin/static/nav.tsx +++ b/snuba/admin/static/nav.tsx @@ -79,7 +79,7 @@ const linkStyle = { const activeLinkStyle = { color: COLORS.TEXT_DEFAULT, - backgroundColor: "rgba(38, 139, 210, 0.15)", + backgroundColor: "rgba(31, 111, 235, 0.12)", borderLeft: `3px solid ${COLORS.SNUBA_BLUE}`, fontWeight: "bold" as const, }; diff --git a/snuba/admin/static/theme.tsx b/snuba/admin/static/theme.tsx index a65ecce49d0..3df05e9e6f8 100644 --- a/snuba/admin/static/theme.tsx +++ b/snuba/admin/static/theme.tsx @@ -1,80 +1,51 @@ -// Solarized dark (https://ethanschoonover.com/solarized/). -const SOLARIZED = { - BASE03: "#002b36", - BASE02: "#073642", - BASE01: "#586e75", - BASE00: "#657b83", - BASE0: "#839496", - BASE1: "#93a1a1", - BASE2: "#eee8d5", - BASE3: "#fdf6e3", - YELLOW: "#b58900", - ORANGE: "#cb4b16", - RED: "#dc322f", - MAGENTA: "#d33682", - VIOLET: "#6c71c4", - BLUE: "#268bd2", - CYAN: "#2aa198", - GREEN: "#859900", -}; - +// High-contrast light palette: near-black text on near-white surfaces. const COLORS = { - PAGE_BG: SOLARIZED.BASE03, - // A shade darker than the page so the header reads as a separate band. - HEADER_BG: "#001e26", - HEADER_TEXT: SOLARIZED.BASE1, - NAV_BG: SOLARIZED.BASE02, - PANEL_BG: SOLARIZED.BASE02, + PAGE_BG: "#fafafa", + HEADER_BG: "#ffffff", + HEADER_TEXT: "#111111", + NAV_BG: "#f2f2f2", + PANEL_BG: "#ffffff", REGION_TEXT: "black", - NAV_BORDER: "#0e4553", - TABLE_BORDER: SOLARIZED.BASE01, - SNUBA_BLUE: SOLARIZED.BLUE, - TEXT_DEFAULT: SOLARIZED.BASE1, - TEXT_LIGHTER: SOLARIZED.BASE0, - TEXT_INACTIVE: SOLARIZED.BASE0, - BG_LIGHT: SOLARIZED.BASE02, - RED: SOLARIZED.RED, + NAV_BORDER: "#d4d4d4", + TABLE_BORDER: "#cbcbcb", + SNUBA_BLUE: "#1f6feb", + TEXT_DEFAULT: "#111111", + TEXT_LIGHTER: "#3d3d3d", + TEXT_INACTIVE: "#2b2b2b", + BG_LIGHT: "#e3e3e3", + RED: "#c42e2b", WHITE: "#fff", - BG_GRAY_LIGHT: "#0e4553", - BG_GRAY_LIGHTER: SOLARIZED.BASE02, - BORDER_GRAY: SOLARIZED.BASE01, - BORDER_GRAY_LIGHT: SOLARIZED.BASE01, - ERROR: SOLARIZED.RED, - SUCCESS: SOLARIZED.GREEN, + BG_GRAY_LIGHT: "#f0f0f0", + BG_GRAY_LIGHTER: "#f7f7f7", + BORDER_GRAY: "#c8c8c8", + BORDER_GRAY_LIGHT: "#dddddd", + ERROR: "#c62828", + SUCCESS: "#1b5e20", }; -// Mantine's dark scheme reads text from the light end of this scale and -// backgrounds from the dark end (body is index 7, inputs index 6). -const MANTINE_DARK_SHADES: [ - string, string, string, string, string, - string, string, string, string, string, -] = [ - SOLARIZED.BASE1, - SOLARIZED.BASE0, - SOLARIZED.BASE00, - SOLARIZED.BASE01, - "#2f5560", - "#0e4553", - SOLARIZED.BASE02, - SOLARIZED.BASE03, - "#00232c", - "#001b22", -]; - -// Diagonal, staggered tiling of the slogan. The fill sits just above the page -// background so the pattern stays visible without competing with text. +// Diagonal, staggered tiling of the slogan. The fill is only a few shades off +// the page background so the pattern stays visible without competing with text. function sloganTiling(): string { - const fill = "#06343f"; + const fill = "#ececec"; + const angle = -55; + const width = 340; + const height = 340; const text = (x: number, y: number) => `MEREDITH IS #1`; + // Steep text overflows the tile, so repeat each copy one tile over in every + // direction; the overflow then lines up with the neighbouring tiles. + const wrapped = (x: number, y: number) => + [-1, 0, 1] + .flatMap((dx) => [-1, 0, 1].map((dy) => text(x + dx * width, y + dy * height))) + .join(""); const svg = - `` + - text(90, 55) + - text(270, 165) + + `` + + wrapped(width / 4, height / 4) + + wrapped((3 * width) / 4, (3 * height) / 4) + ``; return `url("data:image/svg+xml,${encodeURIComponent(svg)}")`; } -export { COLORS, SOLARIZED, MANTINE_DARK_SHADES, sloganTiling }; +export { COLORS, sloganTiling }; From 0371c102cdb376472d6e0de0d7df4644315d2ad6 Mon Sep 17 00:00:00 2001 From: Oliver Newland Date: Fri, 9 Oct 2026 14:24:18 -0700 Subject: [PATCH 5/8] fix(admin): Restore the shallower slogan tiling angle The steeper angle made the background text hard to read. Co-Authored-By: Claude Opus 5.5 (1M context) --- snuba/admin/static/theme.tsx | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/snuba/admin/static/theme.tsx b/snuba/admin/static/theme.tsx index 3df05e9e6f8..b327974d24b 100644 --- a/snuba/admin/static/theme.tsx +++ b/snuba/admin/static/theme.tsx @@ -27,23 +27,14 @@ const COLORS = { // the page background so the pattern stays visible without competing with text. function sloganTiling(): string { const fill = "#ececec"; - const angle = -55; - const width = 340; - const height = 340; const text = (x: number, y: number) => `MEREDITH IS #1`; - // Steep text overflows the tile, so repeat each copy one tile over in every - // direction; the overflow then lines up with the neighbouring tiles. - const wrapped = (x: number, y: number) => - [-1, 0, 1] - .flatMap((dx) => [-1, 0, 1].map((dy) => text(x + dx * width, y + dy * height))) - .join(""); const svg = - `` + - wrapped(width / 4, height / 4) + - wrapped((3 * width) / 4, (3 * height) / 4) + + `` + + text(90, 55) + + text(270, 165) + ``; return `url("data:image/svg+xml,${encodeURIComponent(svg)}")`; } From fc835cbffe9320dafec2f3d8527687ca2b2194bf Mon Sep 17 00:00:00 2001 From: Oliver Newland Date: Fri, 9 Oct 2026 14:35:17 -0700 Subject: [PATCH 6/8] feat(admin): Soften the table header colors Replace the saturated blue table header with a light slate blue and dark text, give tables a white body, and use a border color that shows on both the header and the body. Co-Authored-By: Claude Opus 5.5 (1M context) --- snuba/admin/static/table.tsx | 5 +++-- snuba/admin/static/theme.tsx | 7 ++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/snuba/admin/static/table.tsx b/snuba/admin/static/table.tsx index 248dcda95be..6d100c484f2 100644 --- a/snuba/admin/static/table.tsx +++ b/snuba/admin/static/table.tsx @@ -97,13 +97,14 @@ const tableStyle = { ...border, borderCollapse: "collapse" as const, width: "100%", + backgroundColor: COLORS.TABLE_BG, fontSize: 16, marginBottom: 20, }; const headerStyle = { - backgroundColor: COLORS.SNUBA_BLUE, - color: "white", + backgroundColor: COLORS.TABLE_HEADER_BG, + color: COLORS.TABLE_HEADER_TEXT, }; const thStyle = { diff --git a/snuba/admin/static/theme.tsx b/snuba/admin/static/theme.tsx index b327974d24b..e61540455d4 100644 --- a/snuba/admin/static/theme.tsx +++ b/snuba/admin/static/theme.tsx @@ -7,7 +7,12 @@ const COLORS = { PANEL_BG: "#ffffff", REGION_TEXT: "black", NAV_BORDER: "#d4d4d4", - TABLE_BORDER: "#cbcbcb", + // Slightly darker than TABLE_HEADER_BG so it shows on the header and body. + TABLE_BORDER: "#b9c6d6", + TABLE_BG: "#ffffff", + // Light slate blue, calmer than SNUBA_BLUE. + TABLE_HEADER_BG: "#cfdbea", + TABLE_HEADER_TEXT: "#111111", SNUBA_BLUE: "#1f6feb", TEXT_DEFAULT: "#111111", TEXT_LIGHTER: "#3d3d3d", From e1bc4d7376bb3d4e3e1450b336f720b2aa6ba5a0 Mon Sep 17 00:00:00 2001 From: Oliver Newland Date: Fri, 9 Oct 2026 15:12:05 -0700 Subject: [PATCH 7/8] feat(admin): Move "Clear site data" into the regions row Right-align the button on the same line as the region pills and give it the pills' type and height, outlined in a muted red so it doesn't read as another region. Co-Authored-By: Claude Opus 5.5 (1M context) --- snuba/admin/dist/index.html | 8 +++ snuba/admin/static/theme.tsx | 1 + snuba/admin/static/welcome/index.tsx | 76 +++++++++++++++++----------- 3 files changed, 55 insertions(+), 30 deletions(-) diff --git a/snuba/admin/dist/index.html b/snuba/admin/dist/index.html index cda04b48fba..04e28b1ee4a 100644 --- a/snuba/admin/dist/index.html +++ b/snuba/admin/dist/index.html @@ -50,6 +50,14 @@ transform: translateY(-1px); } + .danger-button { + background-color: #ffffff; + transition: background-color 150ms ease; + } + .danger-button:hover { + background-color: #fbeceb; + } + .region-button { transition: transform 150ms ease, filter 150ms ease, box-shadow 150ms ease; } diff --git a/snuba/admin/static/theme.tsx b/snuba/admin/static/theme.tsx index e61540455d4..83f49596e9f 100644 --- a/snuba/admin/static/theme.tsx +++ b/snuba/admin/static/theme.tsx @@ -25,6 +25,7 @@ const COLORS = { BORDER_GRAY: "#c8c8c8", BORDER_GRAY_LIGHT: "#dddddd", ERROR: "#c62828", + DANGER: "#a8322d", SUCCESS: "#1b5e20", }; diff --git a/snuba/admin/static/welcome/index.tsx b/snuba/admin/static/welcome/index.tsx index 3c61bc58b6d..df2091b432c 100644 --- a/snuba/admin/static/welcome/index.tsx +++ b/snuba/admin/static/welcome/index.tsx @@ -1,5 +1,4 @@ import React, { useEffect, useState } from "react"; -import { Button } from "@mantine/core"; import Client from "SnubaAdmin/api_client"; import { NAV_ITEMS, isToolAllowed } from "SnubaAdmin/data"; import { COLORS } from "SnubaAdmin/theme"; @@ -60,30 +59,39 @@ function Welcome(props: { api: Client }) { return (
- {adminRegions.length > 0 && ( -
-

Regions

-
- {adminRegions.map((region) => ( - - {region.is_main ? `SaaS (${region.name})` : region.name} - - ))} -
-
- )} +
+ {adminRegions.length > 0 && ( + <> +

Regions

+
+ {adminRegions.map((region) => ( + + {region.is_main ? `SaaS (${region.name})` : region.name} + + ))} +
+ + )} + +

Tools

{toolsError && ( @@ -109,11 +117,6 @@ function Welcome(props: { api: Client }) { })}
-
- -
); } @@ -161,6 +164,19 @@ const regionButtonStyle = { whiteSpace: "nowrap" as const, }; +// Same type as the region pills, but outlined so it doesn't read as a region. +const clearSiteDataStyle = { + ...regionButtonStyle, + marginLeft: "auto", + color: COLORS.DANGER, + border: `1px solid ${COLORS.DANGER}`, + // Buttons don't inherit line height, and the border adds 2px, so match the + // pills' height explicitly. + lineHeight: "inherit", + padding: "5px 16px", + cursor: "pointer", +}; + const tileGridStyle = { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(260px, 1fr))", From 3500953dd002144b6e975810b3365ae75d1d292d Mon Sep 17 00:00:00 2001 From: Oliver Newland Date: Fri, 9 Oct 2026 15:31:40 -0700 Subject: [PATCH 8/8] ref(admin): Move inline page styles into a CSS file Move the
diff --git a/snuba/admin/static/index.tsx b/snuba/admin/static/index.tsx index ba614171b3e..ce774f94a1d 100644 --- a/snuba/admin/static/index.tsx +++ b/snuba/admin/static/index.tsx @@ -12,6 +12,8 @@ import { COLORS, sloganTiling } from "SnubaAdmin/theme"; import { ShellStateProvider } from "SnubaAdmin/sql_shell/shell_context"; import 'bootstrap/dist/css/bootstrap.min.css'; +// Imported after bootstrap so these rules take precedence. +import './styles.css'; const containerStyle = { display: "flex", diff --git a/snuba/admin/static/styles.css b/snuba/admin/static/styles.css new file mode 100644 index 00000000000..00bb2b95097 --- /dev/null +++ b/snuba/admin/static/styles.css @@ -0,0 +1,58 @@ +html, body, #root {margin: 0; height: 100%} +body { + font-family: -apple-system,BlinkMacSystemFont,sans-serif; + -webkit-font-smoothing: antialiased; +} + +fieldset { + border: none; + padding: 0; + margin: 0 0 10px 0; +} + +/* Hack since we can't put hover/active pseudo selectors in inline styles */ +.nav-link:hover { + background-color: #e6e6e6; +} +.nav-link:active { + background-color: #d6d6d6; +} + +.nav-link-active:hover { + background-color: rgba(31, 111, 235, 0.18); +} + +.tool-tile { + background-color: #ffffff; + border: 1px solid #d4d4d4; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06); + transition: transform 150ms ease, box-shadow 150ms ease, + border-color 150ms ease, background-color 150ms ease; +} +.tool-tile:hover { + transform: translateY(-3px); + background-color: #ffffff; + border-color: #1f6feb; + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12), + 0 0 0 1px rgba(31, 111, 235, 0.35); +} +.tool-tile:active { + transform: translateY(-1px); +} + +.danger-button { + background-color: #ffffff; + transition: background-color 150ms ease; +} +.danger-button:hover { + background-color: #fbeceb; +} + +.region-button { + transition: transform 150ms ease, filter 150ms ease, box-shadow 150ms ease; +} +.region-button:hover { + transform: translateY(-2px); + filter: brightness(1.08); + box-shadow: 0 6px 14px rgba(0, 0, 0, 0.18); +}