milestone 32: task-shaped configuration, file mode as a rendering, config status api
Gates / frontend (push) Successful in 1m22s
Gates / test (push) Successful in 1m54s
Gates / test-aarch64 (push) Successful in 7m57s
Gates / package (push) Successful in 5m29s
Gates / container (push) Successful in 10s
CI / gates (push) Successful in 32m51s
Gates / frontend (push) Successful in 1m22s
Gates / test (push) Successful in 1m54s
Gates / test-aarch64 (push) Successful in 7m57s
Gates / package (push) Successful in 5m29s
Gates / container (push) Successful in 10s
CI / gates (push) Successful in 32m51s
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useId, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Link, Outlet, useNavigate } from "@tanstack/react-router";
|
||||
import * as stylex from "@stylexjs/stylex";
|
||||
@@ -7,8 +7,8 @@ import InlineError from "@/lib/InlineError";
|
||||
import { healthQuery, versionQuery } from "@/lib/queries";
|
||||
import PauseControl from "@/features/pause/PauseControl";
|
||||
import { diagnosticsBadge } from "./diagnosticsBadge";
|
||||
import ReadOnlyConfigBanner from "../features/settings/ReadOnlyConfigBanner";
|
||||
import RestartBanner from "../features/settings/RestartBanner";
|
||||
import ConfigStatusNotices from "./ConfigStatusNotices";
|
||||
import AuthorityLine from "@/features/configuration/AuthorityLine";
|
||||
import { styles as shared } from "@/ui/styles";
|
||||
import { colors } from "@/ui/tokens.stylex";
|
||||
|
||||
@@ -16,17 +16,22 @@ import { colors } from "@/ui/tokens.stylex";
|
||||
const WIDE = "@media (min-width: 768px)";
|
||||
const DARK = "@media (prefers-color-scheme: dark)";
|
||||
|
||||
/**
|
||||
* The four operational surfaces, then the configuration group. Configuration
|
||||
* is a labelled group rather than a collapsible tree: three items do not earn
|
||||
* a disclosure, and a tree would hide the authority line under it.
|
||||
*/
|
||||
const NAV_ITEMS = [
|
||||
{ to: "/overview", label: "Overview" },
|
||||
{ to: "/activity", label: "Activity" },
|
||||
{ to: "/clients", label: "Clients" },
|
||||
{ to: "/groups", label: "Groups" },
|
||||
{ to: "/blocklists", label: "Blocklists" },
|
||||
{ to: "/rules", label: "Rules" },
|
||||
{ to: "/local-dns", label: "Local DNS" },
|
||||
{ to: "/upstreams", label: "Upstreams" },
|
||||
{ to: "/diagnostics", label: "Diagnostics" },
|
||||
{ to: "/settings", label: "Settings" },
|
||||
] as const;
|
||||
|
||||
const CONFIGURATION_ITEMS = [
|
||||
{ to: "/configuration/protection", label: "Protection" },
|
||||
{ to: "/configuration/resolution", label: "Resolution" },
|
||||
{ to: "/configuration/system", label: "System" },
|
||||
] as const;
|
||||
|
||||
const styles = stylex.create({
|
||||
@@ -47,6 +52,19 @@ const styles = stylex.create({
|
||||
navLabel: {
|
||||
flex: 1,
|
||||
},
|
||||
navGroup: {
|
||||
marginTop: "1rem",
|
||||
},
|
||||
navGroupLabel: {
|
||||
paddingInline: "0.75rem",
|
||||
paddingBlock: "0.25rem",
|
||||
fontSize: "0.75rem",
|
||||
lineHeight: "1rem",
|
||||
fontWeight: 600,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.05em",
|
||||
color: colors.textMuted,
|
||||
},
|
||||
/**
|
||||
* Neutral chrome: the mark is the message, and a coloured pill here would be
|
||||
* the page's loudest element on every route. Text and shape carry it.
|
||||
@@ -163,33 +181,71 @@ const styles = stylex.create({
|
||||
},
|
||||
});
|
||||
|
||||
function NavItem({
|
||||
to,
|
||||
label,
|
||||
onNavigate,
|
||||
badge,
|
||||
}: {
|
||||
to: string;
|
||||
label: string;
|
||||
onNavigate?: () => void;
|
||||
badge?: { text: string; label: string } | null;
|
||||
}) {
|
||||
return (
|
||||
<li>
|
||||
<Link
|
||||
to={to}
|
||||
onClick={onNavigate}
|
||||
activeProps={{
|
||||
"aria-current": "page",
|
||||
className: stylex.props(styles.navActive).className,
|
||||
}}
|
||||
inactiveProps={{ className: stylex.props(styles.navIdle).className }}
|
||||
{...stylex.props(styles.navLink, shared.focusRing)}
|
||||
>
|
||||
<span {...stylex.props(styles.navLabel)}>{label}</span>
|
||||
{badge !== undefined && badge !== null && (
|
||||
<span aria-label={badge.label} {...stylex.props(styles.badge)}>
|
||||
{badge.text}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function NavLinks({ onNavigate }: { onNavigate?: () => void }) {
|
||||
const health = useQuery(healthQuery());
|
||||
const badge = diagnosticsBadge(health.data, health.isError);
|
||||
const groupHeadingId = useId();
|
||||
return (
|
||||
<ul {...stylex.props(styles.navList)}>
|
||||
{NAV_ITEMS.map((item) => (
|
||||
<li key={item.to}>
|
||||
<Link
|
||||
<>
|
||||
<ul {...stylex.props(styles.navList)}>
|
||||
{NAV_ITEMS.map((item) => (
|
||||
<NavItem
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
onClick={onNavigate}
|
||||
activeProps={{
|
||||
"aria-current": "page",
|
||||
className: stylex.props(styles.navActive).className,
|
||||
}}
|
||||
inactiveProps={{ className: stylex.props(styles.navIdle).className }}
|
||||
{...stylex.props(styles.navLink, shared.focusRing)}
|
||||
>
|
||||
<span {...stylex.props(styles.navLabel)}>{item.label}</span>
|
||||
{item.to === "/diagnostics" && badge !== null && (
|
||||
<span aria-label={badge.label} {...stylex.props(styles.badge)}>
|
||||
{badge.text}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
label={item.label}
|
||||
onNavigate={onNavigate}
|
||||
badge={item.to === "/diagnostics" ? badge : undefined}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
<div {...stylex.props(styles.navGroup)}>
|
||||
{/* A span, not a heading: the sidebar label is not a section of the
|
||||
page, and an h2 here lands in the middle of the page's own outline. */}
|
||||
<span id={groupHeadingId} {...stylex.props(styles.navGroupLabel)}>
|
||||
Configuration
|
||||
</span>
|
||||
<ul aria-labelledby={groupHeadingId} {...stylex.props(styles.navList)}>
|
||||
{CONFIGURATION_ITEMS.map((item) => (
|
||||
<NavItem key={item.to} to={item.to} label={item.label} onNavigate={onNavigate} />
|
||||
))}
|
||||
</ul>
|
||||
<AuthorityLine />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -270,8 +326,7 @@ export default function AppShell() {
|
||||
<LogoutButton />
|
||||
</div>
|
||||
</header>
|
||||
<RestartBanner />
|
||||
<ReadOnlyConfigBanner />
|
||||
<ConfigStatusNotices />
|
||||
{drawerOpen && (
|
||||
<div id="mobile-nav" {...stylex.props(styles.drawer)}>
|
||||
<nav aria-label="Main" {...stylex.props(styles.drawerNav)}>
|
||||
|
||||
Reference in New Issue
Block a user