mirror of
https://github.com/prometheus/prometheus.git
synced 2024-11-09 23:24:05 -08:00
add julius work
Signed-off-by: Augustin Husson <husson.augustin@gmail.com>
This commit is contained in:
parent
34d5c8dfaf
commit
c627cde8bf
|
@ -24,9 +24,12 @@
|
|||
"@mantine/dates": "^7.5.3",
|
||||
"@mantine/hooks": "^7.5.3",
|
||||
"@prometheus-io/codemirror-promql": "0.49.1",
|
||||
"@tabler/icons-react": "^2.47.0",
|
||||
"@tanstack/react-query": "^5.22.2",
|
||||
"@uiw/react-codemirror": "^4.21.22",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.22.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.55",
|
||||
|
|
50
web/ui/app/public/prometheus-logo.svg
Normal file
50
web/ui/app/public/prometheus-logo.svg
Normal file
|
@ -0,0 +1,50 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
width="115.333px"
|
||||
height="114px"
|
||||
viewBox="0 0 115.333 114"
|
||||
enable-background="new 0 0 115.333 114"
|
||||
xml:space="preserve"
|
||||
sodipodi:docname="prometheus_logo_orange.svg"
|
||||
inkscape:version="0.92.1 r15371"><metadata
|
||||
id="metadata4495"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs4493" /><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1484"
|
||||
inkscape:window-height="886"
|
||||
id="namedview4491"
|
||||
showgrid="false"
|
||||
inkscape:zoom="5.2784901"
|
||||
inkscape:cx="60.603667"
|
||||
inkscape:cy="60.329656"
|
||||
inkscape:window-x="54"
|
||||
inkscape:window-y="7"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="Layer_1" /><g
|
||||
id="Layer_2" /><path
|
||||
style="fill:#e6522c;fill-opacity:1"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4486"
|
||||
d="M 56.667,0.667 C 25.372,0.667 0,26.036 0,57.332 c 0,31.295 25.372,56.666 56.667,56.666 31.295,0 56.666,-25.371 56.666,-56.666 0,-31.296 -25.372,-56.665 -56.666,-56.665 z m 0,106.055 c -8.904,0 -16.123,-5.948 -16.123,-13.283 H 72.79 c 0,7.334 -7.219,13.283 -16.123,13.283 z M 83.297,89.04 H 30.034 V 79.382 H 83.298 V 89.04 Z M 83.106,74.411 H 30.186 C 30.01,74.208 29.83,74.008 29.66,73.802 24.208,67.182 22.924,63.726 21.677,60.204 c -0.021,-0.116 6.611,1.355 11.314,2.413 0,0 2.42,0.56 5.958,1.205 -3.397,-3.982 -5.414,-9.044 -5.414,-14.218 0,-11.359 8.712,-21.285 5.569,-29.308 3.059,0.249 6.331,6.456 6.552,16.161 3.252,-4.494 4.613,-12.701 4.613,-17.733 0,-5.21 3.433,-11.262 6.867,-11.469 -3.061,5.045 0.793,9.37 4.219,20.099 1.285,4.03 1.121,10.812 2.113,15.113 C 63.797,33.534 65.333,20.5 71,16 c -2.5,5.667 0.37,12.758 2.333,16.167 3.167,5.5 5.087,9.667 5.087,17.548 0,5.284 -1.951,10.259 -5.242,14.148 3.742,-0.702 6.326,-1.335 6.326,-1.335 l 12.152,-2.371 c 10e-4,-10e-4 -1.765,7.261 -8.55,14.254 z" /></svg>
|
After Width: | Height: | Size: 2.7 KiB |
|
@ -1,42 +1,32 @@
|
|||
#root {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
.control {
|
||||
display: block;
|
||||
padding: var(--mantine-spacing-xs) var(--mantine-spacing-md);
|
||||
border-radius: var(--mantine-radius-md);
|
||||
font-weight: 500;
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.react:hover {
|
||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||
}
|
||||
|
||||
@keyframes logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
@mixin hover {
|
||||
background-color: var(--mantine-color-gray-8);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
a:nth-of-type(2) .logo {
|
||||
animation: logo-spin infinite 20s linear;
|
||||
.link {
|
||||
display: block;
|
||||
line-height: 1;
|
||||
padding: rem(8px) rem(12px);
|
||||
border-radius: var(--mantine-radius-sm);
|
||||
text-decoration: none;
|
||||
color: var(--mantine-color-gray-0);
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
font-weight: 500;
|
||||
background-color: transparent;
|
||||
|
||||
@mixin hover {
|
||||
background-color: var(--mantine-color-gray-6);
|
||||
color: var(--mantine-color-gray-0);
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
[data-mantine-color-scheme] &[aria-current="page"] {
|
||||
background-color: var(--mantine-color-blue-filled);
|
||||
color: var(--mantine-color-white);
|
||||
}
|
||||
}
|
32
web/ui/app/src/App.module.css
Normal file
32
web/ui/app/src/App.module.css
Normal file
|
@ -0,0 +1,32 @@
|
|||
.control {
|
||||
display: block;
|
||||
padding: var(--mantine-spacing-xs) var(--mantine-spacing-md);
|
||||
border-radius: var(--mantine-radius-md);
|
||||
font-weight: 500;
|
||||
|
||||
@mixin hover {
|
||||
background-color: var(--mantine-color-gray-8);
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
display: block;
|
||||
line-height: 1;
|
||||
padding: rem(8px) rem(12px);
|
||||
border-radius: var(--mantine-radius-sm);
|
||||
text-decoration: none;
|
||||
color: var(--mantine-color-gray-0);
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
font-weight: 500;
|
||||
background-color: transparent;
|
||||
|
||||
@mixin hover {
|
||||
background-color: var(--mantine-color-gray-6);
|
||||
color: var(--mantine-color-gray-0);
|
||||
}
|
||||
|
||||
[data-mantine-color-scheme] &[aria-current="page"] {
|
||||
background-color: var(--mantine-color-blue-filled);
|
||||
color: var(--mantine-color-white);
|
||||
}
|
||||
}
|
|
@ -1,20 +1,305 @@
|
|||
import CodeMirror, { EditorView } from "@uiw/react-codemirror";
|
||||
import { PromQLExtension } from "@prometheus-io/codemirror-promql";
|
||||
import "@mantine/core/styles.css";
|
||||
import "@mantine/code-highlight/styles.css";
|
||||
import classes from "./App.module.css";
|
||||
import PrometheusLogo from "./images/prometheus-logo.svg";
|
||||
|
||||
import {
|
||||
AppShell,
|
||||
Burger,
|
||||
Button,
|
||||
Group,
|
||||
MantineProvider,
|
||||
Menu,
|
||||
Skeleton,
|
||||
Text,
|
||||
createTheme,
|
||||
rem,
|
||||
} from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import {
|
||||
IconBellFilled,
|
||||
IconChartAreaFilled,
|
||||
IconChevronDown,
|
||||
IconChevronRight,
|
||||
IconCloudDataConnection,
|
||||
IconDatabase,
|
||||
IconFileAnalytics,
|
||||
IconFlag,
|
||||
IconHeartRateMonitor,
|
||||
IconHelp,
|
||||
IconInfoCircle,
|
||||
IconServerCog,
|
||||
} from "@tabler/icons-react";
|
||||
import {
|
||||
BrowserRouter,
|
||||
NavLink,
|
||||
Navigate,
|
||||
Route,
|
||||
Routes,
|
||||
} from "react-router-dom";
|
||||
import Graph from "./pages/graph";
|
||||
import Alerts from "./pages/alerts";
|
||||
import { IconTable } from "@tabler/icons-react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
// import { ReactQueryDevtools } from "react-query/devtools";
|
||||
import Rules from "./pages/rules";
|
||||
import Targets from "./pages/targets";
|
||||
import ServiceDiscovery from "./pages/service-discovery";
|
||||
import Status from "./pages/status";
|
||||
import TSDBStatus from "./pages/tsdb-status";
|
||||
import Flags from "./pages/flags";
|
||||
import Config from "./pages/config";
|
||||
import { Suspense, useContext } from "react";
|
||||
import ErrorBoundary from "./error-boundary";
|
||||
import { ThemeSelector } from "./theme-selector";
|
||||
import { SettingsContext } from "./settings";
|
||||
import Agent from "./pages/agent";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const monitoringStatusPages = [
|
||||
{
|
||||
title: "Targets",
|
||||
path: "/targets",
|
||||
icon: <IconHeartRateMonitor style={{ width: rem(14), height: rem(14) }} />,
|
||||
element: <Targets />,
|
||||
},
|
||||
{
|
||||
title: "Rules",
|
||||
path: "/rules",
|
||||
icon: <IconTable style={{ width: rem(14), height: rem(14) }} />,
|
||||
element: <Rules />,
|
||||
},
|
||||
{
|
||||
title: "Service discovery",
|
||||
path: "/service-discovery",
|
||||
icon: (
|
||||
<IconCloudDataConnection style={{ width: rem(14), height: rem(14) }} />
|
||||
),
|
||||
element: <ServiceDiscovery />,
|
||||
},
|
||||
];
|
||||
|
||||
const serverStatusPages = [
|
||||
{
|
||||
title: "Runtime & build information",
|
||||
path: "/status",
|
||||
icon: <IconInfoCircle style={{ width: rem(14), height: rem(14) }} />,
|
||||
element: <Status />,
|
||||
},
|
||||
{
|
||||
title: "TSDB status",
|
||||
path: "/tsdb-status",
|
||||
icon: <IconDatabase style={{ width: rem(14), height: rem(14) }} />,
|
||||
element: <TSDBStatus />,
|
||||
},
|
||||
{
|
||||
title: "Command-line flags",
|
||||
path: "/flags",
|
||||
icon: <IconFlag style={{ width: rem(14), height: rem(14) }} />,
|
||||
element: <Flags />,
|
||||
},
|
||||
{
|
||||
title: "Configuration",
|
||||
path: "/config",
|
||||
icon: <IconServerCog style={{ width: rem(14), height: rem(14) }} />,
|
||||
element: <Config />,
|
||||
},
|
||||
];
|
||||
|
||||
const allStatusPages = [...monitoringStatusPages, ...serverStatusPages];
|
||||
|
||||
const theme = createTheme({
|
||||
colors: {
|
||||
"codebox-bg": [
|
||||
"#f5f5f5",
|
||||
"#e7e7e7",
|
||||
"#cdcdcd",
|
||||
"#b2b2b2",
|
||||
"#9a9a9a",
|
||||
"#8b8b8b",
|
||||
"#848484",
|
||||
"#717171",
|
||||
"#656565",
|
||||
"#575757",
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const promqlExtension = new PromQLExtension();
|
||||
function App() {
|
||||
const [opened, { toggle }] = useDisclosure();
|
||||
const { agentMode } = useContext(SettingsContext);
|
||||
|
||||
return (
|
||||
<CodeMirror
|
||||
basicSetup={false}
|
||||
value="rate(foo)"
|
||||
editable={false}
|
||||
extensions={[
|
||||
promqlExtension.asExtension(),
|
||||
EditorView.lineWrapping,
|
||||
]}
|
||||
/>
|
||||
)
|
||||
const navLinks = (
|
||||
<>
|
||||
<Button
|
||||
component={NavLink}
|
||||
to="/graph"
|
||||
className={classes.link}
|
||||
leftSection={<IconChartAreaFilled size={16} />}
|
||||
>
|
||||
Graph
|
||||
</Button>
|
||||
<Button
|
||||
component={NavLink}
|
||||
to="/alerts"
|
||||
className={classes.link}
|
||||
leftSection={<IconBellFilled size={16} />}
|
||||
>
|
||||
Alerts
|
||||
</Button>
|
||||
|
||||
<Menu shadow="md" width={230}>
|
||||
<Routes>
|
||||
{allStatusPages.map((p) => (
|
||||
<Route
|
||||
key={p.path}
|
||||
path={p.path}
|
||||
element={
|
||||
<Menu.Target>
|
||||
<Button
|
||||
component={NavLink}
|
||||
to={p.path}
|
||||
className={classes.link}
|
||||
leftSection={p.icon}
|
||||
rightSection={<IconChevronDown size={16} />}
|
||||
>
|
||||
Status <IconChevronRight size={16} /> {p.title}
|
||||
</Button>
|
||||
</Menu.Target>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<Route
|
||||
path="*"
|
||||
element={
|
||||
<Menu.Target>
|
||||
<Button
|
||||
component={NavLink}
|
||||
to="/"
|
||||
className={classes.link}
|
||||
leftSection={<IconFileAnalytics size={16} />}
|
||||
rightSection={<IconChevronDown size={16} />}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
Status
|
||||
</Button>
|
||||
</Menu.Target>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
|
||||
<Menu.Dropdown>
|
||||
<Menu.Label>Monitoring status</Menu.Label>
|
||||
{monitoringStatusPages.map((p) => (
|
||||
<Menu.Item
|
||||
key={p.path}
|
||||
component={NavLink}
|
||||
to={p.path}
|
||||
leftSection={p.icon}
|
||||
>
|
||||
{p.title}
|
||||
</Menu.Item>
|
||||
))}
|
||||
|
||||
<Menu.Divider />
|
||||
<Menu.Label>Server status</Menu.Label>
|
||||
{serverStatusPages.map((p) => (
|
||||
<Menu.Item
|
||||
key={p.path}
|
||||
component={NavLink}
|
||||
to={p.path}
|
||||
leftSection={p.icon}
|
||||
>
|
||||
{p.title}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
|
||||
<Button
|
||||
component="a"
|
||||
href="https://prometheus.io/docs/prometheus/latest/getting_started/"
|
||||
className={classes.link}
|
||||
leftSection={<IconHelp size={16} />}
|
||||
target="_blank"
|
||||
>
|
||||
Help
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<MantineProvider defaultColorScheme="auto" theme={theme}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AppShell
|
||||
header={{ height: 56 }}
|
||||
navbar={{
|
||||
width: 300,
|
||||
breakpoint: "sm",
|
||||
collapsed: { desktop: true, mobile: !opened },
|
||||
}}
|
||||
padding="md"
|
||||
>
|
||||
<AppShell.Header bg="rgb(65, 73, 81)" c="#fff">
|
||||
<Group h="100%" px="md">
|
||||
<Group style={{ flex: 1 }}>
|
||||
<Group gap={10}>
|
||||
<img src={PrometheusLogo} height={30} />
|
||||
<Text fz={20}>Prometheus{agentMode && " Agent"}</Text>
|
||||
</Group>
|
||||
<Group ml="lg" gap={12} visibleFrom="sm">
|
||||
{navLinks}
|
||||
</Group>
|
||||
</Group>
|
||||
<Burger
|
||||
opened={opened}
|
||||
onClick={toggle}
|
||||
hiddenFrom="sm"
|
||||
size="sm"
|
||||
color="gray.2"
|
||||
/>
|
||||
{<ThemeSelector />}
|
||||
</Group>
|
||||
</AppShell.Header>
|
||||
|
||||
<AppShell.Navbar py="md" px={4} bg="rgb(65, 73, 81)" c="#fff">
|
||||
{navLinks}
|
||||
</AppShell.Navbar>
|
||||
|
||||
<AppShell.Main>
|
||||
<ErrorBoundary key={location.pathname}>
|
||||
<Suspense
|
||||
fallback={Array.from(Array(10), (_, i) => (
|
||||
<Skeleton key={i} height={40} mb={15} width={1000} />
|
||||
))}
|
||||
>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<Navigate to={agentMode ? "/agent" : "/graph"} />
|
||||
}
|
||||
/>
|
||||
<Route path="/graph" element={<Graph />} />
|
||||
<Route path="/agent" element={<Agent />} />
|
||||
<Route path="/alerts" element={<Alerts />} />
|
||||
{allStatusPages.map((p) => (
|
||||
<Route key={p.path} path={p.path} element={p.element} />
|
||||
))}
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</AppShell.Main>
|
||||
</AppShell>
|
||||
{/* <ReactQueryDevtools initialIsOpen={false} /> */}
|
||||
</QueryClientProvider>
|
||||
</MantineProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
export default App
|
||||
export default App;
|
32
web/ui/app/src/api/api.ts
Normal file
32
web/ui/app/src/api/api.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
|
||||
export const API_PATH = "api/v1";
|
||||
|
||||
export type APIResponse<T> = { status: string; data: T };
|
||||
|
||||
export const useSuspenseAPIQuery = <T>(path: string) =>
|
||||
useSuspenseQuery<{ data: T }>({
|
||||
queryKey: [path],
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false,
|
||||
gcTime: 0,
|
||||
queryFn: () =>
|
||||
fetch(`/${API_PATH}/${path}`, {
|
||||
cache: "no-store",
|
||||
credentials: "same-origin",
|
||||
})
|
||||
// Introduce 3 seconds delay to simulate slow network.
|
||||
// .then(
|
||||
// (res) =>
|
||||
// new Promise<typeof res>((resolve) =>
|
||||
// setTimeout(() => resolve(res), 2000)
|
||||
// )
|
||||
// )
|
||||
.then((res) => {
|
||||
if (!res.ok) {
|
||||
throw new Error(res.statusText);
|
||||
}
|
||||
return res;
|
||||
})
|
||||
.then((res) => res.json() as Promise<APIResponse<T>>),
|
||||
});
|
51
web/ui/app/src/api/response-types/rules.ts
Normal file
51
web/ui/app/src/api/response-types/rules.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
type RuleState = "pending" | "firing" | "inactive";
|
||||
|
||||
export interface Alert {
|
||||
labels: Record<string, string>;
|
||||
state: RuleState;
|
||||
value: string;
|
||||
annotations: Record<string, string>;
|
||||
activeAt: string;
|
||||
keepFiringSince: string;
|
||||
}
|
||||
|
||||
type CommonRuleFields = {
|
||||
name: string;
|
||||
query: string;
|
||||
evaluationTime: string;
|
||||
health: string;
|
||||
lastError?: string;
|
||||
lastEvaluation: string;
|
||||
};
|
||||
|
||||
type AlertingRule = {
|
||||
type: "alerting";
|
||||
// For alerting rules, the 'labels' field is always present, even when there are no labels.
|
||||
labels: Record<string, string>;
|
||||
annotations: Record<string, string>;
|
||||
duration: number;
|
||||
keepFiringFor: number;
|
||||
state: RuleState;
|
||||
alerts: Alert[];
|
||||
} & CommonRuleFields;
|
||||
|
||||
type RecordingRule = {
|
||||
type: "recording";
|
||||
// For recording rules, the 'labels' field is only present when there are labels.
|
||||
labels?: Record<string, string>;
|
||||
} & CommonRuleFields;
|
||||
|
||||
export type Rule = AlertingRule | RecordingRule;
|
||||
|
||||
interface RuleGroup {
|
||||
name: string;
|
||||
file: string;
|
||||
interval: string;
|
||||
rules: Rule[];
|
||||
evaluationTime: string;
|
||||
lastEvaluation: string;
|
||||
}
|
||||
|
||||
export interface RulesMap {
|
||||
groups: RuleGroup[];
|
||||
}
|
20
web/ui/app/src/api/response-types/tsdb-status.ts
Normal file
20
web/ui/app/src/api/response-types/tsdb-status.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
interface Stats {
|
||||
name: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
interface HeadStats {
|
||||
numSeries: number;
|
||||
numLabelPairs: number;
|
||||
chunkCount: number;
|
||||
minTime: number;
|
||||
maxTime: number;
|
||||
}
|
||||
|
||||
export interface TSDBMap {
|
||||
headStats: HeadStats;
|
||||
seriesCountByMetricName: Stats[];
|
||||
labelValueCountByLabelName: Stats[];
|
||||
memoryInBytesByLabelName: Stats[];
|
||||
seriesCountByLabelValuePair: Stats[];
|
||||
}
|
45
web/ui/app/src/codebox.module.css
Normal file
45
web/ui/app/src/codebox.module.css
Normal file
|
@ -0,0 +1,45 @@
|
|||
.codebox {
|
||||
background-color: light-dark(
|
||||
var(--mantine-color-gray-1),
|
||||
var(--mantine-color-gray-9)
|
||||
);
|
||||
}
|
||||
|
||||
.statsBadge {
|
||||
background-color: light-dark(
|
||||
var(--mantine-color-gray-1),
|
||||
var(--mantine-color-gray-9)
|
||||
);
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-gray-5));
|
||||
}
|
||||
|
||||
.labelBadge {
|
||||
background-color: light-dark(
|
||||
var(--mantine-color-gray-1),
|
||||
var(--mantine-color-gray-9)
|
||||
);
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-gray-5));
|
||||
}
|
||||
|
||||
.healthOk {
|
||||
background-color: light-dark(
|
||||
var(--mantine-color-green-1),
|
||||
var(--mantine-color-green-9)
|
||||
);
|
||||
color: light-dark(var(--mantine-color-green-9), var(--mantine-color-green-1));
|
||||
}
|
||||
|
||||
.healthErr {
|
||||
background-color: light-dark(
|
||||
var(--mantine-color-red-1),
|
||||
var(--mantine-color-red-9)
|
||||
);
|
||||
color: light-dark(var(--mantine-color-red-9), var(--mantine-color-red-1));
|
||||
}
|
||||
|
||||
.healthUnknown {
|
||||
background-color: light-dark(
|
||||
var(--mantine-color-gray-1),
|
||||
var(--mantine-color-gray-9)
|
||||
);
|
||||
}
|
295
web/ui/app/src/codemirror/theme.ts
vendored
Normal file
295
web/ui/app/src/codemirror/theme.ts
vendored
Normal file
|
@ -0,0 +1,295 @@
|
|||
import { HighlightStyle } from "@codemirror/language";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { tags } from "@lezer/highlight";
|
||||
|
||||
export const baseTheme = EditorView.theme({
|
||||
"&.cm-editor": {
|
||||
"&.cm-focused": {
|
||||
outline: "none",
|
||||
outline_fallback: "none",
|
||||
},
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
".cm-scroller": {
|
||||
overflow: "hidden",
|
||||
fontFamily: '"DejaVu Sans Mono", monospace',
|
||||
},
|
||||
".cm-placeholder": {
|
||||
fontFamily:
|
||||
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"',
|
||||
},
|
||||
|
||||
".cm-matchingBracket": {
|
||||
fontWeight: "bold",
|
||||
outline: "1px dashed transparent",
|
||||
},
|
||||
".cm-nonmatchingBracket": { borderColor: "red" },
|
||||
|
||||
".cm-tooltip.cm-tooltip-autocomplete": {
|
||||
"& > ul": {
|
||||
maxHeight: "350px",
|
||||
fontFamily: '"DejaVu Sans Mono", monospace',
|
||||
maxWidth: "unset",
|
||||
},
|
||||
"& > ul > li": {
|
||||
padding: "2px 1em 2px 3px",
|
||||
},
|
||||
minWidth: "30%",
|
||||
},
|
||||
|
||||
".cm-completionDetail": {
|
||||
float: "right",
|
||||
color: "#999",
|
||||
},
|
||||
|
||||
".cm-tooltip.cm-completionInfo": {
|
||||
marginTop: "-11px",
|
||||
padding: "10px",
|
||||
fontFamily:
|
||||
"'Open Sans', 'Lucida Sans Unicode', 'Lucida Grande', sans-serif;",
|
||||
border: "none",
|
||||
minWidth: "250px",
|
||||
maxWidth: "min-content",
|
||||
},
|
||||
|
||||
".cm-completionInfo.cm-completionInfo-right": {
|
||||
"&:before": {
|
||||
content: "' '",
|
||||
height: "0",
|
||||
position: "absolute",
|
||||
width: "0",
|
||||
left: "-20px",
|
||||
borderWidth: "10px",
|
||||
borderStyle: "solid",
|
||||
borderColor: "transparent",
|
||||
},
|
||||
marginLeft: "12px",
|
||||
},
|
||||
".cm-completionInfo.cm-completionInfo-left": {
|
||||
"&:before": {
|
||||
content: "' '",
|
||||
height: "0",
|
||||
position: "absolute",
|
||||
width: "0",
|
||||
right: "-20px",
|
||||
borderWidth: "10px",
|
||||
borderStyle: "solid",
|
||||
borderColor: "transparent",
|
||||
},
|
||||
marginRight: "12px",
|
||||
},
|
||||
|
||||
".cm-completionMatchedText": {
|
||||
textDecoration: "none",
|
||||
fontWeight: "bold",
|
||||
},
|
||||
|
||||
".cm-selectionMatch": {
|
||||
backgroundColor: "#e6f3ff",
|
||||
},
|
||||
|
||||
".cm-diagnostic": {
|
||||
"&.cm-diagnostic-error": {
|
||||
borderLeft: "3px solid #e65013",
|
||||
},
|
||||
},
|
||||
|
||||
".cm-completionIcon": {
|
||||
boxSizing: "content-box",
|
||||
fontSize: "16px",
|
||||
lineHeight: "1",
|
||||
marginRight: "10px",
|
||||
verticalAlign: "top",
|
||||
"&:after": { content: "'\\ea88'" },
|
||||
fontFamily: "codicon",
|
||||
paddingRight: "0",
|
||||
opacity: "1",
|
||||
},
|
||||
|
||||
".cm-completionIcon-function, .cm-completionIcon-method": {
|
||||
"&:after": { content: "'\\ea8c'" },
|
||||
},
|
||||
".cm-completionIcon-class": {
|
||||
"&:after": { content: "'○'" },
|
||||
},
|
||||
".cm-completionIcon-interface": {
|
||||
"&:after": { content: "'◌'" },
|
||||
},
|
||||
".cm-completionIcon-variable": {
|
||||
"&:after": { content: "'𝑥'" },
|
||||
},
|
||||
".cm-completionIcon-constant": {
|
||||
"&:after": { content: "'\\eb5f'" },
|
||||
},
|
||||
".cm-completionIcon-type": {
|
||||
"&:after": { content: "'𝑡'" },
|
||||
},
|
||||
".cm-completionIcon-enum": {
|
||||
"&:after": { content: "'∪'" },
|
||||
},
|
||||
".cm-completionIcon-property": {
|
||||
"&:after": { content: "'□'" },
|
||||
},
|
||||
".cm-completionIcon-keyword": {
|
||||
"&:after": { content: "'\\eb62'" },
|
||||
},
|
||||
".cm-completionIcon-namespace": {
|
||||
"&:after": { content: "'▢'" },
|
||||
},
|
||||
".cm-completionIcon-text": {
|
||||
"&:after": { content: "'\\ea95'" },
|
||||
color: "#ee9d28",
|
||||
},
|
||||
});
|
||||
|
||||
export const lightTheme = EditorView.theme(
|
||||
{
|
||||
".cm-tooltip": {
|
||||
backgroundColor: "#f8f8f8",
|
||||
borderColor: "rgba(52, 79, 113, 0.2)",
|
||||
},
|
||||
|
||||
".cm-tooltip.cm-tooltip-autocomplete": {
|
||||
"& li:hover": {
|
||||
backgroundColor: "#ddd",
|
||||
},
|
||||
"& > ul > li[aria-selected]": {
|
||||
backgroundColor: "#d6ebff",
|
||||
color: "unset",
|
||||
},
|
||||
},
|
||||
|
||||
".cm-tooltip.cm-completionInfo": {
|
||||
backgroundColor: "#d6ebff",
|
||||
},
|
||||
|
||||
".cm-tooltip > .cm-completionInfo.cm-completionInfo-right": {
|
||||
"&:before": {
|
||||
borderRightColor: "#d6ebff",
|
||||
},
|
||||
},
|
||||
".cm-tooltip > .cm-completionInfo.cm-completionInfo-left": {
|
||||
"&:before": {
|
||||
borderLeftColor: "#d6ebff",
|
||||
},
|
||||
},
|
||||
|
||||
".cm-line": {
|
||||
"&::selection": {
|
||||
backgroundColor: "#add6ff",
|
||||
},
|
||||
"& > span::selection": {
|
||||
backgroundColor: "#add6ff",
|
||||
},
|
||||
},
|
||||
|
||||
".cm-matchingBracket": {
|
||||
color: "#000",
|
||||
backgroundColor: "#dedede",
|
||||
},
|
||||
|
||||
".cm-completionMatchedText": {
|
||||
color: "#0066bf",
|
||||
},
|
||||
|
||||
".cm-completionIcon": {
|
||||
color: "#007acc",
|
||||
},
|
||||
|
||||
".cm-completionIcon-constant": {
|
||||
color: "#007acc",
|
||||
},
|
||||
|
||||
".cm-completionIcon-function, .cm-completionIcon-method": {
|
||||
color: "#652d90",
|
||||
},
|
||||
|
||||
".cm-completionIcon-keyword": {
|
||||
color: "#616161",
|
||||
},
|
||||
},
|
||||
{ dark: false }
|
||||
);
|
||||
|
||||
export const darkTheme = EditorView.theme(
|
||||
{
|
||||
".cm-content": {
|
||||
caretColor: "#fff",
|
||||
},
|
||||
|
||||
".cm-tooltip.cm-completionInfo": {
|
||||
backgroundColor: "#333338",
|
||||
},
|
||||
|
||||
".cm-tooltip > .cm-completionInfo.cm-completionInfo-right": {
|
||||
"&:before": {
|
||||
borderRightColor: "#333338",
|
||||
},
|
||||
},
|
||||
".cm-tooltip > .cm-completionInfo.cm-completionInfo-left": {
|
||||
"&:before": {
|
||||
borderLeftColor: "#333338",
|
||||
},
|
||||
},
|
||||
|
||||
".cm-line": {
|
||||
"&::selection": {
|
||||
backgroundColor: "#767676",
|
||||
},
|
||||
"& > span::selection": {
|
||||
backgroundColor: "#767676",
|
||||
},
|
||||
},
|
||||
|
||||
".cm-matchingBracket, &.cm-focused .cm-matchingBracket": {
|
||||
backgroundColor: "#616161",
|
||||
},
|
||||
|
||||
".cm-completionMatchedText": {
|
||||
color: "#7dd3fc",
|
||||
},
|
||||
|
||||
".cm-completionIcon, .cm-completionIcon-constant": {
|
||||
color: "#7dd3fc",
|
||||
},
|
||||
|
||||
".cm-completionIcon-function, .cm-completionIcon-method": {
|
||||
color: "#d8b4fe",
|
||||
},
|
||||
|
||||
".cm-completionIcon-keyword": {
|
||||
color: "#cbd5e1 !important",
|
||||
},
|
||||
},
|
||||
{ dark: true }
|
||||
);
|
||||
|
||||
export const promqlHighlighter = HighlightStyle.define([
|
||||
{ tag: tags.number, color: "#09885a" },
|
||||
{ tag: tags.string, color: "#a31515" },
|
||||
{ tag: tags.keyword, color: "#008080" },
|
||||
{ tag: tags.function(tags.variableName), color: "#008080" },
|
||||
{ tag: tags.labelName, color: "#800000" },
|
||||
{ tag: tags.operator },
|
||||
{ tag: tags.modifier, color: "#008080" },
|
||||
{ tag: tags.paren },
|
||||
{ tag: tags.squareBracket },
|
||||
{ tag: tags.brace },
|
||||
{ tag: tags.invalid, color: "red" },
|
||||
{ tag: tags.comment, color: "#888", fontStyle: "italic" },
|
||||
]);
|
||||
|
||||
export const darkPromqlHighlighter = HighlightStyle.define([
|
||||
{ tag: tags.number, color: "#22c55e" },
|
||||
{ tag: tags.string, color: "#fca5a5" },
|
||||
{ tag: tags.keyword, color: "#14bfad" },
|
||||
{ tag: tags.function(tags.variableName), color: "#14bfad" },
|
||||
{ tag: tags.labelName, color: "#ff8585" },
|
||||
{ tag: tags.operator },
|
||||
{ tag: tags.modifier, color: "#14bfad" },
|
||||
{ tag: tags.paren },
|
||||
{ tag: tags.squareBracket },
|
||||
{ tag: tags.brace },
|
||||
{ tag: tags.invalid, color: "#ff3d3d" },
|
||||
{ tag: tags.comment, color: "#9ca3af", fontStyle: "italic" },
|
||||
]);
|
52
web/ui/app/src/error-boundary.tsx
Normal file
52
web/ui/app/src/error-boundary.tsx
Normal file
|
@ -0,0 +1,52 @@
|
|||
import { Alert } from "@mantine/core";
|
||||
import { IconAlertTriangle } from "@tabler/icons-react";
|
||||
import { Component, ErrorInfo, ReactNode } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
interface Props {
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
interface State {
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
class ErrorBoundary extends Component<Props, State> {
|
||||
public state: State = {
|
||||
error: null,
|
||||
};
|
||||
|
||||
public static getDerivedStateFromError(error: Error): State {
|
||||
// Update state so the next render will show the fallback UI.
|
||||
return { error };
|
||||
}
|
||||
|
||||
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||
console.error("Uncaught error:", error, errorInfo);
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (this.state.error !== null) {
|
||||
return (
|
||||
<Alert
|
||||
color="red"
|
||||
title="Error querying page data"
|
||||
icon={<IconAlertTriangle size={14} />}
|
||||
>
|
||||
<strong>Error:</strong> {this.state.error.message}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
const ResettingErrorBoundary = (props: Props) => {
|
||||
const location = useLocation();
|
||||
return (
|
||||
<ErrorBoundary key={location.pathname}>{props.children}</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResettingErrorBoundary;
|
19
web/ui/app/src/images/prometheus-logo.svg
Normal file
19
web/ui/app/src/images/prometheus-logo.svg
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="115.333px" height="114px" viewBox="0 0 115.333 114" enable-background="new 0 0 115.333 114" xml:space="preserve">
|
||||
<g id="Layer_2">
|
||||
</g>
|
||||
<g>
|
||||
<path fill="#EEEEEE" d="M56.667,0.667C25.372,0.667,0,26.036,0,57.332c0,31.295,25.372,56.666,56.667,56.666
|
||||
s56.666-25.371,56.666-56.666C113.333,26.036,87.961,0.667,56.667,0.667z M56.667,106.722c-8.904,0-16.123-5.948-16.123-13.283
|
||||
H72.79C72.79,100.773,65.571,106.722,56.667,106.722z M83.297,89.04H30.034v-9.658h53.264V89.04z M83.106,74.411h-52.92
|
||||
c-0.176-0.203-0.356-0.403-0.526-0.609c-5.452-6.62-6.736-10.076-7.983-13.598c-0.021-0.116,6.611,1.355,11.314,2.413
|
||||
c0,0,2.42,0.56,5.958,1.205c-3.397-3.982-5.414-9.044-5.414-14.218c0-11.359,8.712-21.285,5.569-29.308
|
||||
c3.059,0.249,6.331,6.456,6.552,16.161c3.252-4.494,4.613-12.701,4.613-17.733c0-5.21,3.433-11.262,6.867-11.469
|
||||
c-3.061,5.045,0.793,9.37,4.219,20.099c1.285,4.03,1.121,10.812,2.113,15.113C63.797,33.534,65.333,20.5,71,16
|
||||
c-2.5,5.667,0.37,12.758,2.333,16.167c3.167,5.5,5.087,9.667,5.087,17.548c0,5.284-1.951,10.259-5.242,14.148
|
||||
c3.742-0.702,6.326-1.335,6.326-1.335l12.152-2.371C91.657,60.156,89.891,67.418,83.106,74.411z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
118
web/ui/app/src/lib/time-format.ts
Normal file
118
web/ui/app/src/lib/time-format.ts
Normal file
|
@ -0,0 +1,118 @@
|
|||
import dayjs from "dayjs";
|
||||
import duration from "dayjs/plugin/duration";
|
||||
dayjs.extend(duration);
|
||||
import utc from "dayjs/plugin/utc";
|
||||
dayjs.extend(utc);
|
||||
|
||||
export const parseDuration = (durationStr: string): number | null => {
|
||||
if (durationStr === "") {
|
||||
return null;
|
||||
}
|
||||
if (durationStr === "0") {
|
||||
// Allow 0 without a unit.
|
||||
return 0;
|
||||
}
|
||||
|
||||
const durationRE = new RegExp(
|
||||
"^(([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?(([0-9]+)ms)?$"
|
||||
);
|
||||
const matches = durationStr.match(durationRE);
|
||||
if (!matches) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let dur = 0;
|
||||
|
||||
// Parse the match at pos `pos` in the regex and use `mult` to turn that
|
||||
// into ms, then add that value to the total parsed duration.
|
||||
const m = (pos: number, mult: number) => {
|
||||
if (matches[pos] === undefined) {
|
||||
return;
|
||||
}
|
||||
const n = parseInt(matches[pos]);
|
||||
dur += n * mult;
|
||||
};
|
||||
|
||||
m(2, 1000 * 60 * 60 * 24 * 365); // y
|
||||
m(4, 1000 * 60 * 60 * 24 * 7); // w
|
||||
m(6, 1000 * 60 * 60 * 24); // d
|
||||
m(8, 1000 * 60 * 60); // h
|
||||
m(10, 1000 * 60); // m
|
||||
m(12, 1000); // s
|
||||
m(14, 1); // ms
|
||||
|
||||
return dur;
|
||||
};
|
||||
|
||||
export const formatDuration = (d: number): string => {
|
||||
let ms = d;
|
||||
let r = "";
|
||||
if (ms === 0) {
|
||||
return "0s";
|
||||
}
|
||||
|
||||
const f = (unit: string, mult: number, exact: boolean) => {
|
||||
if (exact && ms % mult !== 0) {
|
||||
return;
|
||||
}
|
||||
const v = Math.floor(ms / mult);
|
||||
if (v > 0) {
|
||||
r += `${v}${unit}`;
|
||||
ms -= v * mult;
|
||||
}
|
||||
};
|
||||
|
||||
// Only format years and weeks if the remainder is zero, as it is often
|
||||
// easier to read 90d than 12w6d.
|
||||
f("y", 1000 * 60 * 60 * 24 * 365, true);
|
||||
f("w", 1000 * 60 * 60 * 24 * 7, true);
|
||||
|
||||
f("d", 1000 * 60 * 60 * 24, false);
|
||||
f("h", 1000 * 60 * 60, false);
|
||||
f("m", 1000 * 60, false);
|
||||
f("s", 1000, false);
|
||||
f("ms", 1, false);
|
||||
|
||||
return r;
|
||||
};
|
||||
|
||||
export function parseTime(timeText: string): number {
|
||||
return dayjs.utc(timeText).valueOf();
|
||||
}
|
||||
|
||||
export const now = (): number => dayjs().valueOf();
|
||||
|
||||
export const humanizeDuration = (milliseconds: number): string => {
|
||||
const sign = milliseconds < 0 ? "-" : "";
|
||||
const unsignedMillis = milliseconds < 0 ? -1 * milliseconds : milliseconds;
|
||||
const duration = dayjs.duration(unsignedMillis, "ms");
|
||||
const ms = Math.floor(duration.milliseconds());
|
||||
const s = Math.floor(duration.seconds());
|
||||
const m = Math.floor(duration.minutes());
|
||||
const h = Math.floor(duration.hours());
|
||||
const d = Math.floor(duration.asDays());
|
||||
if (d !== 0) {
|
||||
return `${sign}${d}d ${h}h ${m}m ${s}s`;
|
||||
}
|
||||
if (h !== 0) {
|
||||
return `${sign}${h}h ${m}m ${s}s`;
|
||||
}
|
||||
if (m !== 0) {
|
||||
return `${sign}${m}m ${s}s`;
|
||||
}
|
||||
if (s !== 0) {
|
||||
return `${sign}${s}.${ms}s`;
|
||||
}
|
||||
if (unsignedMillis > 0) {
|
||||
return `${sign}${unsignedMillis.toFixed(3)}ms`;
|
||||
}
|
||||
return "0s";
|
||||
};
|
||||
|
||||
export const formatRelative = (startStr: string, end: number): string => {
|
||||
const start = parseTime(startStr);
|
||||
if (start < 0) {
|
||||
return "Never";
|
||||
}
|
||||
return humanizeDuration(end - start) + " ago";
|
||||
};
|
27
web/ui/app/src/pages/agent.tsx
Normal file
27
web/ui/app/src/pages/agent.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { Card, Group, Text } from "@mantine/core";
|
||||
import { IconSpy } from "@tabler/icons-react";
|
||||
import { FC } from "react";
|
||||
|
||||
const Agent: FC = () => {
|
||||
return (
|
||||
<Card shadow="xs" withBorder radius="md" p="md">
|
||||
<Group wrap="nowrap" align="center" ml="xs" mb="sm" gap="xs">
|
||||
<IconSpy size={22} />
|
||||
<Text fz="xl" fw={600}>
|
||||
Prometheus Agent
|
||||
</Text>
|
||||
</Group>
|
||||
<Text p="md">
|
||||
This Prometheus instance is running in <strong>agent mode</strong>. In
|
||||
this mode, Prometheus is only used to scrape discovered targets and
|
||||
forward the scraped metrics to remote write endpoints.
|
||||
</Text>
|
||||
<Text p="md">
|
||||
Some features are not available in this mode, such as querying and
|
||||
alerting.
|
||||
</Text>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default Agent;
|
3
web/ui/app/src/pages/alerts.tsx
Normal file
3
web/ui/app/src/pages/alerts.tsx
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default function Alerts() {
|
||||
return <>Alerts page</>;
|
||||
}
|
16
web/ui/app/src/pages/config.tsx
Normal file
16
web/ui/app/src/pages/config.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { CodeHighlight } from "@mantine/code-highlight";
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
|
||||
export default function Config() {
|
||||
const {
|
||||
data: {
|
||||
data: { yaml },
|
||||
},
|
||||
} = useSuspenseQuery<{ data: { yaml: string } }>({
|
||||
queryKey: ["config"],
|
||||
queryFn: () => {
|
||||
return fetch("/api/v1/status/config").then((res) => res.json());
|
||||
},
|
||||
});
|
||||
return <CodeHighlight code={yaml} language="yaml" miw="30vw" />;
|
||||
}
|
21
web/ui/app/src/pages/flags.module.css
Normal file
21
web/ui/app/src/pages/flags.module.css
Normal file
|
@ -0,0 +1,21 @@
|
|||
.th {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.control {
|
||||
width: 100%;
|
||||
padding: var(--mantine-spacing-xs) var(--mantine-spacing-md);
|
||||
|
||||
@mixin hover {
|
||||
background-color: light-dark(
|
||||
var(--mantine-color-gray-0),
|
||||
var(--mantine-color-dark-6)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: rem(21px);
|
||||
height: rem(21px);
|
||||
border-radius: rem(21px);
|
||||
}
|
183
web/ui/app/src/pages/flags.tsx
Normal file
183
web/ui/app/src/pages/flags.tsx
Normal file
|
@ -0,0 +1,183 @@
|
|||
import { useState } from "react";
|
||||
import {
|
||||
Table,
|
||||
UnstyledButton,
|
||||
Group,
|
||||
Text,
|
||||
Center,
|
||||
TextInput,
|
||||
rem,
|
||||
keys,
|
||||
Card,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
IconSelector,
|
||||
IconChevronDown,
|
||||
IconChevronUp,
|
||||
IconSearch,
|
||||
} from "@tabler/icons-react";
|
||||
import classes from "./flags.module.css";
|
||||
import { useSuspenseAPIQuery } from "../api/api";
|
||||
|
||||
interface RowData {
|
||||
flag: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface ThProps {
|
||||
children: React.ReactNode;
|
||||
reversed: boolean;
|
||||
sorted: boolean;
|
||||
onSort(): void;
|
||||
}
|
||||
|
||||
function Th({ children, reversed, sorted, onSort }: ThProps) {
|
||||
const Icon = sorted
|
||||
? reversed
|
||||
? IconChevronUp
|
||||
: IconChevronDown
|
||||
: IconSelector;
|
||||
return (
|
||||
<Table.Th className={classes.th}>
|
||||
<UnstyledButton onClick={onSort} className={classes.control}>
|
||||
<Group justify="space-between">
|
||||
<Text fw={600} fz="sm">
|
||||
{children}
|
||||
</Text>
|
||||
<Center className={classes.icon}>
|
||||
<Icon style={{ width: rem(16), height: rem(16) }} stroke={1.5} />
|
||||
</Center>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
</Table.Th>
|
||||
);
|
||||
}
|
||||
|
||||
function filterData(data: RowData[], search: string) {
|
||||
const query = search.toLowerCase().trim();
|
||||
return data.filter((item) =>
|
||||
keys(data[0]).some((key) => item[key].toLowerCase().includes(query))
|
||||
);
|
||||
}
|
||||
|
||||
function sortData(
|
||||
data: RowData[],
|
||||
payload: { sortBy: keyof RowData | null; reversed: boolean; search: string }
|
||||
) {
|
||||
const { sortBy } = payload;
|
||||
|
||||
if (!sortBy) {
|
||||
return filterData(data, payload.search);
|
||||
}
|
||||
|
||||
return filterData(
|
||||
[...data].sort((a, b) => {
|
||||
if (payload.reversed) {
|
||||
return b[sortBy].localeCompare(a[sortBy]);
|
||||
}
|
||||
|
||||
return a[sortBy].localeCompare(b[sortBy]);
|
||||
}),
|
||||
payload.search
|
||||
);
|
||||
}
|
||||
|
||||
export default function Flags() {
|
||||
const { data } = useSuspenseAPIQuery<Record<string, string>>(`/status/flags`);
|
||||
|
||||
// const { response, error, isLoading } =
|
||||
// useFetchAPI<Record<string, string>>(`/status/flags`);
|
||||
|
||||
const flags = Object.entries(data.data).map(([flag, value]) => ({
|
||||
flag,
|
||||
value,
|
||||
}));
|
||||
|
||||
const [search, setSearch] = useState("");
|
||||
const [sortedData, setSortedData] = useState(flags);
|
||||
const [sortBy, setSortBy] = useState<keyof RowData | null>(null);
|
||||
const [reverseSortDirection, setReverseSortDirection] = useState(false);
|
||||
|
||||
const setSorting = (field: keyof RowData) => {
|
||||
const reversed = field === sortBy ? !reverseSortDirection : false;
|
||||
setReverseSortDirection(reversed);
|
||||
setSortBy(field);
|
||||
setSortedData(sortData(flags, { sortBy: field, reversed, search }));
|
||||
};
|
||||
|
||||
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { value } = event.currentTarget;
|
||||
setSearch(value);
|
||||
setSortedData(
|
||||
sortData(flags, { sortBy, reversed: reverseSortDirection, search: value })
|
||||
);
|
||||
};
|
||||
|
||||
const rows = sortedData.map((row) => (
|
||||
<Table.Tr key={row.flag}>
|
||||
<Table.Td>
|
||||
<code>--{row.flag}</code>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<code>{row.value}</code>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
));
|
||||
|
||||
return (
|
||||
<Card shadow="xs" maw={1000} withBorder>
|
||||
<TextInput
|
||||
placeholder="Filter by flag name or value"
|
||||
mb="md"
|
||||
autoFocus
|
||||
leftSection={
|
||||
<IconSearch
|
||||
style={{ width: rem(16), height: rem(16) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
}
|
||||
value={search}
|
||||
onChange={handleSearchChange}
|
||||
/>
|
||||
<Table
|
||||
horizontalSpacing="md"
|
||||
verticalSpacing="xs"
|
||||
miw={700}
|
||||
layout="fixed"
|
||||
>
|
||||
<Table.Tbody>
|
||||
<Table.Tr>
|
||||
<Th
|
||||
sorted={sortBy === "flag"}
|
||||
reversed={reverseSortDirection}
|
||||
onSort={() => setSorting("flag")}
|
||||
>
|
||||
Flag
|
||||
</Th>
|
||||
|
||||
<Th
|
||||
sorted={sortBy === "value"}
|
||||
reversed={reverseSortDirection}
|
||||
onSort={() => setSorting("value")}
|
||||
>
|
||||
Value
|
||||
</Th>
|
||||
</Table.Tr>
|
||||
</Table.Tbody>
|
||||
<Table.Tbody>
|
||||
{rows.length > 0 ? (
|
||||
rows
|
||||
) : (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={2}>
|
||||
<Text fw={500} ta="center">
|
||||
Nothing found
|
||||
</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
)}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Card>
|
||||
);
|
||||
}
|
3
web/ui/app/src/pages/graph.tsx
Normal file
3
web/ui/app/src/pages/graph.tsx
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default function Graph() {
|
||||
return <>Graph page</>;
|
||||
}
|
257
web/ui/app/src/pages/rules.tsx
Normal file
257
web/ui/app/src/pages/rules.tsx
Normal file
|
@ -0,0 +1,257 @@
|
|||
import {
|
||||
Alert,
|
||||
Badge,
|
||||
Card,
|
||||
Group,
|
||||
Table,
|
||||
Text,
|
||||
Tooltip,
|
||||
useComputedColorScheme,
|
||||
} from "@mantine/core";
|
||||
// import { useQuery } from "react-query";
|
||||
import {
|
||||
formatDuration,
|
||||
formatRelative,
|
||||
humanizeDuration,
|
||||
now,
|
||||
} from "../lib/time-format";
|
||||
import {
|
||||
IconAlertTriangle,
|
||||
IconBell,
|
||||
IconClockPause,
|
||||
IconClockPlay,
|
||||
IconDatabaseImport,
|
||||
IconHourglass,
|
||||
IconRefresh,
|
||||
IconRepeat,
|
||||
} from "@tabler/icons-react";
|
||||
import CodeMirror, { EditorView } from "@uiw/react-codemirror";
|
||||
import { useSuspenseAPIQuery } from "../api/api";
|
||||
import { RulesMap } from "../api/response-types/rules";
|
||||
import { syntaxHighlighting } from "@codemirror/language";
|
||||
import {
|
||||
baseTheme,
|
||||
darkPromqlHighlighter,
|
||||
lightTheme,
|
||||
promqlHighlighter,
|
||||
} from "../codemirror/theme";
|
||||
import { PromQLExtension } from "@prometheus-io/codemirror-promql";
|
||||
import classes from "../codebox.module.css";
|
||||
|
||||
const healthBadgeClass = (state: string) => {
|
||||
switch (state) {
|
||||
case "ok":
|
||||
return classes.healthOk;
|
||||
case "err":
|
||||
return classes.healthErr;
|
||||
case "unknown":
|
||||
return classes.healthUnknown;
|
||||
default:
|
||||
return "orange";
|
||||
}
|
||||
};
|
||||
|
||||
const promqlExtension = new PromQLExtension();
|
||||
|
||||
export default function Rules() {
|
||||
const { data } = useSuspenseAPIQuery<RulesMap>(`/rules`);
|
||||
const theme = useComputedColorScheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
{data.data.groups.map((g) => (
|
||||
<Card
|
||||
shadow="xs"
|
||||
withBorder
|
||||
radius="md"
|
||||
p="md"
|
||||
mb="md"
|
||||
key={g.name + ";" + g.file}
|
||||
>
|
||||
<Group mb="md" mt="xs" ml="xs" justify="space-between">
|
||||
<Group align="baseline">
|
||||
<Text fz="xl" fw={600} c="var(--mantine-primary-color-filled)">
|
||||
{g.name}
|
||||
</Text>
|
||||
<Text fz="sm" c="gray.6">
|
||||
{g.file}
|
||||
</Text>
|
||||
</Group>
|
||||
<Group>
|
||||
<Tooltip label="Last group evaluation" withArrow>
|
||||
<Badge
|
||||
variant="light"
|
||||
className={classes.statsBadge}
|
||||
styles={{ label: { textTransform: "none" } }}
|
||||
leftSection={<IconRefresh size={12} />}
|
||||
>
|
||||
{formatRelative(g.lastEvaluation, now())}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
<Tooltip label="Duration of last group evaluation" withArrow>
|
||||
<Badge
|
||||
variant="light"
|
||||
className={classes.statsBadge}
|
||||
styles={{ label: { textTransform: "none" } }}
|
||||
leftSection={<IconHourglass size={12} />}
|
||||
>
|
||||
{humanizeDuration(parseFloat(g.evaluationTime) * 1000)}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
<Tooltip label="Group evaluation interval" withArrow>
|
||||
<Badge
|
||||
variant="light"
|
||||
className={classes.statsBadge}
|
||||
styles={{ label: { textTransform: "none" } }}
|
||||
leftSection={<IconRepeat size={12} />}
|
||||
>
|
||||
{humanizeDuration(parseFloat(g.interval) * 1000)}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Group>
|
||||
<Table>
|
||||
<Table.Tbody>
|
||||
{g.rules.map((r) => (
|
||||
<Table.Tr key={r.name}>
|
||||
<Table.Td p="md" valign="top">
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
{r.type === "alerting" ? (
|
||||
<IconBell size={14} />
|
||||
) : (
|
||||
<IconDatabaseImport size={14} />
|
||||
)}
|
||||
<Text fz="sm" fw={600}>
|
||||
{r.name}
|
||||
</Text>
|
||||
</Group>
|
||||
<Group mt="md" gap="xs">
|
||||
<Badge className={healthBadgeClass(r.health)}>
|
||||
{r.health}
|
||||
</Badge>
|
||||
|
||||
<Group gap="xs" wrap="wrap">
|
||||
<Tooltip label="Last rule evaluation" withArrow>
|
||||
<Badge
|
||||
variant="light"
|
||||
className={classes.statsBadge}
|
||||
styles={{ label: { textTransform: "none" } }}
|
||||
leftSection={<IconRefresh size={12} />}
|
||||
>
|
||||
{formatRelative(r.lastEvaluation, now())}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
label="Duration of last rule evaluation"
|
||||
withArrow
|
||||
>
|
||||
<Badge
|
||||
variant="light"
|
||||
className={classes.statsBadge}
|
||||
styles={{ label: { textTransform: "none" } }}
|
||||
leftSection={<IconHourglass size={12} />}
|
||||
>
|
||||
{humanizeDuration(
|
||||
parseFloat(r.evaluationTime) * 1000
|
||||
)}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
<Table.Td p="md">
|
||||
<Card
|
||||
p="xs"
|
||||
className={classes.codebox}
|
||||
radius="sm"
|
||||
shadow="none"
|
||||
>
|
||||
<CodeMirror
|
||||
basicSetup={false}
|
||||
value={r.query}
|
||||
editable={false}
|
||||
extensions={[
|
||||
baseTheme,
|
||||
lightTheme,
|
||||
syntaxHighlighting(
|
||||
theme === "light"
|
||||
? promqlHighlighter
|
||||
: darkPromqlHighlighter
|
||||
),
|
||||
promqlExtension.asExtension(),
|
||||
EditorView.lineWrapping,
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{r.lastError && (
|
||||
<Alert
|
||||
color="red"
|
||||
mt="sm"
|
||||
title="Rule failed to evaluate"
|
||||
icon={<IconAlertTriangle size={14} />}
|
||||
>
|
||||
<strong>Error:</strong> {r.lastError}
|
||||
</Alert>
|
||||
)}
|
||||
{r.type === "alerting" && (
|
||||
<Group mt="md" gap="xs">
|
||||
{r.duration && (
|
||||
<Badge
|
||||
variant="light"
|
||||
styles={{ label: { textTransform: "none" } }}
|
||||
leftSection={<IconClockPause size={12} />}
|
||||
>
|
||||
for: {formatDuration(r.duration * 1000)}
|
||||
</Badge>
|
||||
)}
|
||||
{r.keepFiringFor && (
|
||||
<Badge
|
||||
variant="light"
|
||||
styles={{ label: { textTransform: "none" } }}
|
||||
leftSection={<IconClockPlay size={12} />}
|
||||
>
|
||||
keep_firing_for: {formatDuration(r.duration * 1000)}
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
)}
|
||||
{r.labels && Object.keys(r.labels).length > 0 && (
|
||||
<Group mt="md" gap="xs">
|
||||
{Object.entries(r.labels).map(([k, v]) => (
|
||||
<Badge
|
||||
variant="light"
|
||||
className={classes.labelBadge}
|
||||
styles={{ label: { textTransform: "none" } }}
|
||||
key={k}
|
||||
>
|
||||
{k}: {v}
|
||||
</Badge>
|
||||
))}
|
||||
</Group>
|
||||
)}
|
||||
{/* {Object.keys(r.annotations).length > 0 && (
|
||||
<Group mt="md" gap="xs">
|
||||
{Object.entries(r.annotations).map(([k, v]) => (
|
||||
<Badge
|
||||
variant="light"
|
||||
color="orange.9"
|
||||
styles={{ label: { textTransform: "none" } }}
|
||||
key={k}
|
||||
>
|
||||
{k}: {v}
|
||||
</Badge>
|
||||
))}
|
||||
</Group>
|
||||
)} */}
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Card>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
3
web/ui/app/src/pages/service-discovery.tsx
Normal file
3
web/ui/app/src/pages/service-discovery.tsx
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default function ServiceDiscovery() {
|
||||
return <>ServiceDiscovery page</>;
|
||||
}
|
82
web/ui/app/src/pages/status.tsx
Normal file
82
web/ui/app/src/pages/status.tsx
Normal file
|
@ -0,0 +1,82 @@
|
|||
import { Card, Group, Stack, Table, Text } from "@mantine/core";
|
||||
import { useSuspenseAPIQuery } from "../api/api";
|
||||
import { IconRun, IconWall } from "@tabler/icons-react";
|
||||
|
||||
const statusConfig: Record<
|
||||
string,
|
||||
{
|
||||
title?: string;
|
||||
formatValue?: (v: any) => string;
|
||||
}
|
||||
> = {
|
||||
startTime: {
|
||||
title: "Start time",
|
||||
formatValue: (v: string) => new Date(v).toUTCString(),
|
||||
},
|
||||
CWD: { title: "Working directory" },
|
||||
reloadConfigSuccess: {
|
||||
title: "Configuration reload",
|
||||
formatValue: (v: boolean) => (v ? "Successful" : "Unsuccessful"),
|
||||
},
|
||||
lastConfigTime: {
|
||||
title: "Last successful configuration reload",
|
||||
formatValue: (v: string) => new Date(v).toUTCString(),
|
||||
},
|
||||
corruptionCount: { title: "WAL corruptions" },
|
||||
goroutineCount: { title: "Goroutines" },
|
||||
storageRetention: { title: "Storage retention" },
|
||||
};
|
||||
|
||||
export default function Status() {
|
||||
const { data: buildinfo } =
|
||||
useSuspenseAPIQuery<Record<string, string>>(`/status/buildinfo`);
|
||||
const { data: runtimeinfo } =
|
||||
useSuspenseAPIQuery<Record<string, string>>(`/status/runtimeinfo`);
|
||||
|
||||
return (
|
||||
<Stack gap="md" maw={1000}>
|
||||
<Card shadow="xs" withBorder radius="md" p="md">
|
||||
<Group wrap="nowrap" align="center" ml="xs" mb="sm" gap="xs">
|
||||
<IconWall size={22} />
|
||||
<Text fz="xl" fw={600}>
|
||||
Build information
|
||||
</Text>
|
||||
</Group>
|
||||
<Table layout="fixed">
|
||||
<Table.Tbody>
|
||||
{Object.entries(buildinfo.data).map(([k, v]) => (
|
||||
<Table.Tr key={k}>
|
||||
<Table.Th style={{ textTransform: "capitalize" }}>{k}</Table.Th>
|
||||
<Table.Td>{v}</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Card>
|
||||
<Card shadow="xs" withBorder radius="md" p="md">
|
||||
<Group wrap="nowrap" align="center" ml="xs" mb="sm" gap="xs">
|
||||
<IconRun size={22} />
|
||||
<Text fz="xl" fw={600}>
|
||||
Runtime information
|
||||
</Text>
|
||||
</Group>
|
||||
<Table layout="fixed">
|
||||
<Table.Tbody>
|
||||
{Object.entries(runtimeinfo.data).map(([k, v]) => {
|
||||
const { title = k, formatValue = (val: string) => val } =
|
||||
statusConfig[k] || {};
|
||||
return (
|
||||
<Table.Tr key={k}>
|
||||
<Table.Th style={{ textTransform: "capitalize" }}>
|
||||
{title}
|
||||
</Table.Th>
|
||||
<Table.Td>{formatValue(v)}</Table.Td>
|
||||
</Table.Tr>
|
||||
);
|
||||
})}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Card>
|
||||
</Stack>
|
||||
);
|
||||
}
|
3
web/ui/app/src/pages/targets.tsx
Normal file
3
web/ui/app/src/pages/targets.tsx
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default function Targets() {
|
||||
return <>Targets page</>;
|
||||
}
|
101
web/ui/app/src/pages/tsdb-status.tsx
Normal file
101
web/ui/app/src/pages/tsdb-status.tsx
Normal file
|
@ -0,0 +1,101 @@
|
|||
import { Stack, Card, Group, Table, Text } from "@mantine/core";
|
||||
import { useSuspenseAPIQuery } from "../api/api";
|
||||
import { TSDBMap } from "../api/response-types/tsdb-status";
|
||||
|
||||
export default function TSDBStatus() {
|
||||
const {
|
||||
data: {
|
||||
data: {
|
||||
headStats,
|
||||
labelValueCountByLabelName,
|
||||
seriesCountByMetricName,
|
||||
memoryInBytesByLabelName,
|
||||
seriesCountByLabelValuePair,
|
||||
},
|
||||
},
|
||||
} = useSuspenseAPIQuery<TSDBMap>(`/status/tsdb`);
|
||||
|
||||
const unixToTime = (unix: number): string => {
|
||||
try {
|
||||
return `${new Date(unix).toISOString()} (${unix})`;
|
||||
} catch {
|
||||
if (numSeries === 0) {
|
||||
return "No datapoints yet";
|
||||
}
|
||||
return `Error parsing time (${unix})`;
|
||||
}
|
||||
};
|
||||
const { chunkCount, numSeries, numLabelPairs, minTime, maxTime } = headStats;
|
||||
const stats = [
|
||||
{ name: "Number of Series", value: numSeries },
|
||||
{ name: "Number of Chunks", value: chunkCount },
|
||||
{ name: "Number of Label Pairs", value: numLabelPairs },
|
||||
{ name: "Current Min Time", value: `${unixToTime(minTime)}` },
|
||||
{ name: "Current Max Time", value: `${unixToTime(maxTime)}` },
|
||||
];
|
||||
|
||||
return (
|
||||
<Stack gap="md" maw={1000}>
|
||||
{[
|
||||
{
|
||||
title: "TSDB Head Status",
|
||||
stats,
|
||||
formatAsCode: false,
|
||||
},
|
||||
{
|
||||
title: "Top 10 label names with value count",
|
||||
stats: labelValueCountByLabelName,
|
||||
formatAsCode: true,
|
||||
},
|
||||
{
|
||||
title: "Top 10 series count by metric names",
|
||||
stats: seriesCountByMetricName,
|
||||
formatAsCode: true,
|
||||
},
|
||||
{
|
||||
title: "Top 10 label names with high memory usage",
|
||||
unit: "Bytes",
|
||||
stats: memoryInBytesByLabelName,
|
||||
formatAsCode: true,
|
||||
},
|
||||
{
|
||||
title: "Top 10 series count by label value pairs",
|
||||
stats: seriesCountByLabelValuePair,
|
||||
formatAsCode: true,
|
||||
},
|
||||
].map(({ title, unit = "Count", stats, formatAsCode }) => (
|
||||
<Card shadow="xs" withBorder radius="md" p="md">
|
||||
<Group wrap="nowrap" align="center" ml="xs" mb="sm" gap="xs">
|
||||
<Text fz="xl" fw={600}>
|
||||
{title}
|
||||
</Text>
|
||||
</Group>
|
||||
<Table layout="fixed">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Name</Table.Th>
|
||||
<Table.Th>{unit}</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{stats.map(({ name, value }) => {
|
||||
return (
|
||||
<Table.Tr key={name}>
|
||||
<Table.Td
|
||||
style={{
|
||||
wordBreak: "break-all",
|
||||
}}
|
||||
>
|
||||
{formatAsCode ? <code>{name}</code> : name}
|
||||
</Table.Td>
|
||||
<Table.Td>{value}</Table.Td>
|
||||
</Table.Tr>
|
||||
);
|
||||
})}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Card>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
}
|
13
web/ui/app/src/settings.ts
Normal file
13
web/ui/app/src/settings.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { createContext } from "react";
|
||||
|
||||
export interface Settings {
|
||||
consolesLink: string | null;
|
||||
agentMode: boolean;
|
||||
ready: boolean;
|
||||
}
|
||||
|
||||
export const SettingsContext = createContext<Settings>({
|
||||
consolesLink: null,
|
||||
agentMode: false,
|
||||
ready: false,
|
||||
});
|
67
web/ui/app/src/theme-selector.tsx
Normal file
67
web/ui/app/src/theme-selector.tsx
Normal file
|
@ -0,0 +1,67 @@
|
|||
import {
|
||||
useMantineColorScheme,
|
||||
Group,
|
||||
SegmentedControl,
|
||||
rem,
|
||||
MantineColorScheme,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
IconMoonFilled,
|
||||
IconSunFilled,
|
||||
IconUserFilled,
|
||||
} from "@tabler/icons-react";
|
||||
import { FC } from "react";
|
||||
|
||||
export const ThemeSelector: FC = () => {
|
||||
const { colorScheme, setColorScheme } = useMantineColorScheme();
|
||||
const iconProps = {
|
||||
style: { width: rem(20), height: rem(20), display: "block" },
|
||||
stroke: 1.5,
|
||||
};
|
||||
|
||||
return (
|
||||
<Group>
|
||||
<SegmentedControl
|
||||
color="gray.7"
|
||||
size="xs"
|
||||
// styles={{ root: { backgroundColor: "var(--mantine-color-gray-7)" } }}
|
||||
styles={{
|
||||
root: {
|
||||
padding: 3,
|
||||
backgroundColor: "var(--mantine-color-gray-6)",
|
||||
},
|
||||
}}
|
||||
withItemsBorders={false}
|
||||
value={colorScheme}
|
||||
onChange={(v) => setColorScheme(v as MantineColorScheme)}
|
||||
data={[
|
||||
{
|
||||
value: "light",
|
||||
label: (
|
||||
<Tooltip label="Use light theme" offset={15}>
|
||||
<IconSunFilled {...iconProps} />
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: "dark",
|
||||
label: (
|
||||
<Tooltip label="Use dark theme" offset={15}>
|
||||
<IconMoonFilled {...iconProps} />
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: "auto",
|
||||
label: (
|
||||
<Tooltip label="Use browser-preferred theme" offset={15}>
|
||||
<IconUserFilled {...iconProps} />
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Group>
|
||||
);
|
||||
};
|
|
@ -4,4 +4,11 @@ import react from '@vitejs/plugin-react-swc'
|
|||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
server: {
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "http://localhost:9090",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
92
web/ui/package-lock.json
generated
92
web/ui/package-lock.json
generated
|
@ -36,9 +36,12 @@
|
|||
"@mantine/dates": "^7.5.3",
|
||||
"@mantine/hooks": "^7.5.3",
|
||||
"@prometheus-io/codemirror-promql": "0.49.1",
|
||||
"@tabler/icons-react": "^2.47.0",
|
||||
"@tanstack/react-query": "^5.22.2",
|
||||
"@uiw/react-codemirror": "^4.21.22",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.22.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.55",
|
||||
|
@ -904,6 +907,14 @@
|
|||
"resolved": "lezer-promql",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@remix-run/router": {
|
||||
"version": "1.15.1",
|
||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.1.tgz",
|
||||
"integrity": "sha512-zcU0gM3z+3iqj8UX45AmWY810l3oUmXM7uH4dt5xtzvMhRtYVhKGOmgOd1877dOPPepfCjUv57w+syamWIYe7w==",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-node-resolve": {
|
||||
"version": "15.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz",
|
||||
|
@ -1330,6 +1341,55 @@
|
|||
"integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@tabler/icons": {
|
||||
"version": "2.47.0",
|
||||
"resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-2.47.0.tgz",
|
||||
"integrity": "sha512-4w5evLh+7FUUiA1GucvGj2ReX2TvOjEr4ejXdwL/bsjoSkof6r1gQmzqI+VHrE2CpJpB3al7bCTulOkFa/RcyA==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/codecalm"
|
||||
}
|
||||
},
|
||||
"node_modules/@tabler/icons-react": {
|
||||
"version": "2.47.0",
|
||||
"resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-2.47.0.tgz",
|
||||
"integrity": "sha512-iqly2FvCF/qUbgmvS8E40rVeYY7laltc5GUjRxQj59DuX0x/6CpKHTXt86YlI2whg4czvd/c8Ce8YR08uEku0g==",
|
||||
"dependencies": {
|
||||
"@tabler/icons": "2.47.0",
|
||||
"prop-types": "^15.7.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/codecalm"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "5.22.2",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.22.2.tgz",
|
||||
"integrity": "sha512-z3PwKFUFACMUqe1eyesCIKg3Jv1mysSrYfrEW5ww5DCDUD4zlpTKBvUDaEjsfZzL3ULrFLDM9yVUxI/fega1Qg==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-query": {
|
||||
"version": "5.22.2",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.22.2.tgz",
|
||||
"integrity": "sha512-TaxJDRzJ8/NWRT4lY2jguKCrNI6MRN+67dELzPjNUlvqzTxGANlMp68l7aC7hG8Bd1uHNxHl7ihv7MT50i/43A==",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.22.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
|
||||
|
@ -3115,6 +3175,36 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "6.22.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.1.tgz",
|
||||
"integrity": "sha512-0pdoRGwLtemnJqn1K0XHUbnKiX0S4X8CgvVVmHGOWmofESj31msHo/1YiqcJWK7Wxfq2a4uvvtS01KAQyWK/CQ==",
|
||||
"dependencies": {
|
||||
"@remix-run/router": "1.15.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "6.22.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.1.tgz",
|
||||
"integrity": "sha512-iwMyyyrbL7zkKY7MRjOVRy+TMnS/OPusaFVxM2P11x9dzSzGmLsebkCvYirGq0DWB9K9hOspHYYtDz33gE5Duw==",
|
||||
"dependencies": {
|
||||
"@remix-run/router": "1.15.1",
|
||||
"react-router": "6.22.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8",
|
||||
"react-dom": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/react-style-singleton": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
|
||||
|
|
Loading…
Reference in a new issue