Skip to content

Commit 1d7a4b5

Browse files
authored
Merge pull request #1 from SidingsMedia/computroniks/feat/cache-ui
Added cache UI
2 parents a062f30 + f4de313 commit 1d7a4b5

File tree

22 files changed

+1108
-540
lines changed

22 files changed

+1108
-540
lines changed

package.json

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,24 @@
66
"preview": "vite preview"
77
},
88
"dependencies": {
9-
"react": "^19",
10-
"react-dom": "^19",
11-
"@toolpad/core": "0.16.0",
12-
"@mui/material": "^7",
13-
"@mui/material-nextjs": "^7",
14-
"@mui/icons-material": "^7",
159
"@emotion/react": "^11",
1610
"@emotion/styled": "^11",
17-
"zod": "^3.24.2",
18-
"react-router": "^7"
11+
"@mui/icons-material": "^7",
12+
"@mui/material": "^7",
13+
"@mui/material-nextjs": "^7",
14+
"@mui/x-tree-view": "^8.11.2",
15+
"@toolpad/core": "0.16.0",
16+
"react": "^19",
17+
"react-dom": "^19",
18+
"react-router": "^7",
19+
"zod": "^3.24.2"
1920
},
2021
"devDependencies": {
21-
"typescript": "^5",
2222
"@types/react": "^18",
2323
"@types/react-dom": "^18",
24-
"eslint": "^8",
2524
"@vitejs/plugin-react": "^4.3.2",
26-
"vite": "^5.4.8"
25+
"eslint": "^8",
26+
"typescript": "^5",
27+
"vite": "^7.1.5"
2728
}
28-
}
29+
}

src/App.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// SPDX-FileCopyrightText: 2025 Sidings Media <contact@sidingsmedia.com>
22
// SPDX-License-Identifier: MIT
33

4-
import * as React from "react";
54
import DnsIcon from "@mui/icons-material/Dns";
65
import CachedIcon from "@mui/icons-material/Cached";
76
import DashboardIcon from "@mui/icons-material/Dashboard";
@@ -12,7 +11,7 @@ import OpenInNewIcon from "@mui/icons-material/OpenInNew";
1211

1312
let navigation: Navigation = [
1413
{
15-
segment: "",
14+
segment: "overview",
1615
title: "Overview",
1716
icon: <DashboardIcon />,
1817
},
@@ -24,7 +23,7 @@ let navigation: Navigation = [
2423
icon: <DnsIcon />,
2524
children: [
2625
{
27-
segment: "rdns/cache",
26+
segment: "cache",
2827
title: "Cache",
2928
icon: <CachedIcon />,
3029
},
@@ -34,6 +33,7 @@ let navigation: Navigation = [
3433
{ kind: "header", title: "External" },
3534
];
3635

36+
//@ts-expect-error
3737
for (const link of JSON.parse(import.meta.env.VITE_EXTERNAL_SIDEBAR_LINKS)) {
3838
navigation.push({
3939
title: link.title,

src/api/Api.ts

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
// SPDX-FileCopyrightText: 2025 Sidings Media <contact@sidingsmedia.com>
2+
// SPDX-License-Identifier: MIT
3+
4+
import { useNotifications } from "@toolpad/core/useNotifications";
5+
import { ApiError } from "./ApiError";
6+
import type { Error } from "./responses/common";
7+
import { NOTIFICATION_DISPLAY_TIME } from "./constants";
8+
9+
export type DecodeAs = "json" | "text" | "blob";
10+
11+
export class Api {
12+
private baseUrl: string =
13+
// @ts-expect-error
14+
(import.meta.env.VITE_API_URL as string | undefined) ??
15+
"http://localhost:8080/v1/";
16+
17+
/**
18+
* Make a GET request to the API
19+
* @param path Endpoint path to call
20+
* @param options Standard fetch() options
21+
* @returns API response
22+
*/
23+
public async get<T>(path: string, options?: RequestInit): Promise<T> {
24+
const completeOptions = options ?? {};
25+
return this.request(path, "get", completeOptions);
26+
}
27+
28+
/**
29+
* Make a POST request to the API
30+
* @param path Endpoint path to call
31+
* @param options Standard fetch() options
32+
* @returns API response
33+
*/
34+
public async post<T>(path: string, options: RequestInit): Promise<T> {
35+
return this.request(path, "post", options);
36+
}
37+
38+
/**
39+
* Make a PUT request to the API
40+
* @param path Endpoint path to call
41+
* @param options Standard fetch() options
42+
* @returns API response
43+
*/
44+
public async put<T>(path: string, options: RequestInit): Promise<T> {
45+
return this.request(path, "put", options);
46+
}
47+
48+
/**
49+
* Make a PATCH request to the API
50+
* @param path Endpoint path to call
51+
* @param options Standard fetch() options
52+
* @returns API response
53+
*/
54+
public async patch<T>(path: string, options: RequestInit): Promise<T> {
55+
return this.request(path, "patch", options);
56+
}
57+
58+
/**
59+
* Make a DELETE request to the API
60+
* @param path Endpoint path to call
61+
* @param options Standard fetch() options
62+
* @returns API response
63+
*/
64+
public async delete<T>(path: string, options?: RequestInit): Promise<T> {
65+
const completeOptions = options ?? {};
66+
return this.request(path, "delete", completeOptions);
67+
}
68+
69+
/**
70+
* Make request to API. Generally one of the other methods (e.g.
71+
* get(), post() and so on) should be used rather than this one. Only
72+
* use this one if you need more flexibility (e.g. return a blob).
73+
*
74+
* @param path Endpoint path
75+
* @param method HTTP method
76+
* @param options Standard fetch() options
77+
* @param baseUrl Optional alternate base url to use
78+
* @param [decodeAs="json"] How should the response be decoded? Defaults
79+
* to `json`.
80+
* @param [contentType="application/json"] Content type to use.
81+
* `Defaults to application/json`
82+
* @returns API response
83+
*/
84+
public async request<T>(
85+
path: string,
86+
method: "get" | "post" | "put" | "patch" | "delete",
87+
options: RequestInit,
88+
baseUrl?: string,
89+
decodeAs: DecodeAs = "json",
90+
contentType = "application/json",
91+
): Promise<T> {
92+
const opts = {
93+
...options,
94+
method,
95+
};
96+
97+
opts.headers = {
98+
// eslint-disable-next-line @typescript-eslint/no-misused-spread
99+
...opts.headers,
100+
"Content-type": contentType,
101+
...this.headers(),
102+
};
103+
104+
let url: URL;
105+
if (baseUrl === undefined) {
106+
// Handle when base URL is set to relative path
107+
if (!this.baseUrl.startsWith("http")) {
108+
this.baseUrl = new URL(this.baseUrl, window.location.origin).toString();
109+
}
110+
url = new URL(path, this.baseUrl);
111+
} else {
112+
url = new URL(path, baseUrl);
113+
}
114+
115+
console.log(`Sending request: ${method.toUpperCase()} ${url.toString()}`);
116+
const response = await fetch(url.toString(), opts);
117+
if (!response.ok) {
118+
const data: Error = (await response.json()) as Error;
119+
throw new ApiError(data.message, data);
120+
}
121+
122+
if (response.status === 204) {
123+
return undefined as T;
124+
}
125+
126+
switch (decodeAs) {
127+
case "json":
128+
return response.json() as Promise<T>;
129+
case "text":
130+
return response.text() as Promise<T>;
131+
case "blob":
132+
return response.blob() as T;
133+
}
134+
}
135+
136+
/**
137+
* Get the required headers for requests
138+
* @returns No headers needed
139+
*/
140+
private headers(): Record<string, string> {
141+
return {};
142+
}
143+
144+
/**
145+
* Create a URL using the configured base URL.
146+
*
147+
* @param path Path to append to base URL
148+
* @returns Correctly formatted URL
149+
*/
150+
public constructUrl(path: string): URL {
151+
// Handle when base URL is set to relative path
152+
if (!this.baseUrl.startsWith("http")) {
153+
this.baseUrl = new URL(this.baseUrl, window.location.origin).toString();
154+
}
155+
return new URL(path, this.baseUrl);
156+
}
157+
158+
public handleError(
159+
e: unknown,
160+
notifications: ReturnType<typeof useNotifications>,
161+
): void {
162+
if (e instanceof ApiError) {
163+
notifications.show(e.error.message, {
164+
severity: "error",
165+
autoHideDuration: NOTIFICATION_DISPLAY_TIME,
166+
});
167+
} else {
168+
notifications.show(`Network error: ${e}`, {
169+
severity: "error",
170+
autoHideDuration: NOTIFICATION_DISPLAY_TIME,
171+
});
172+
}
173+
}
174+
}

src/api/ApiError.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// SPDX-FileCopyrightText: 2025 Sidings Media <contact@sidingsmedia.com>
2+
// SPDX-License-Identifier: MIT
3+
4+
import type { Error as ErrorResponse } from "./responses/common";
5+
6+
export class ApiError extends Error {
7+
public error: ErrorResponse;
8+
9+
public constructor(message: string, error: ErrorResponse) {
10+
super(message);
11+
Object.setPrototypeOf(this, ApiError.prototype);
12+
13+
this.error = error;
14+
}
15+
}

src/api/constants.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// SPDX-FileCopyrightText: 2025 Sidings Media <contact@sidingsmedia.com>
2+
// SPDX-License-Identifier: MIT
3+
4+
export const NOTIFICATION_DISPLAY_TIME = 5000;

src/api/responses/common.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// SPDX-FileCopyrightText: 2025 Sidings Media <contact@sidingsmedia.com>
2+
// SPDX-License-Identifier: MIT
3+
4+
export interface Error {
5+
code: number;
6+
message: string;
7+
}
8+
9+
export interface List<T> {
10+
results: T[];
11+
}

src/api/responses/rdns.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// SPDX-FileCopyrightText: 2025 Sidings Media <contact@sidingsmedia.com>
2+
// SPDX-License-Identifier: MIT
3+
4+
export interface Server {
5+
name: string;
6+
target: string;
7+
id: string;
8+
}
9+
10+
export interface CachedResult {
11+
id: string;
12+
data: Record<string, any>;
13+
ttl: string;
14+
}
15+
16+
export interface CacheEntry {
17+
name: string;
18+
type: string;
19+
cachedResults: CachedResult[];
20+
}
21+
22+
export interface CacheResponse {
23+
zones: string[];
24+
entries: CacheEntry[];
25+
}

src/components/common/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// SPDX-FileCopyrightText: 2025 Sidings Media <contact@sidingsmedia.com>
2+
// SPDX-License-Identifier: MIT
3+
4+
export { SearchBar } from "./search_bar";
5+
export type { SearchBarProps } from "./search_bar";
6+
7+
export { ServerSelect } from "./server_select";
8+
export type { ServerSelectProps, ServerSelectOption } from "./server_select";
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// SPDX-FileCopyrightText: 2025 Sidings Media <contact@sidingsmedia.com>
2+
// SPDX-License-Identifier: MIT
3+
4+
import type { SxProps, Theme } from "@mui/material";
5+
6+
import Box from "@mui/material/Box";
7+
import FormControl from "@mui/material/FormControl";
8+
import IconButton from "@mui/material/IconButton";
9+
import InputAdornment from "@mui/material/InputAdornment";
10+
import InputLabel from "@mui/material/InputLabel";
11+
import OutlinedInput from "@mui/material/OutlinedInput";
12+
import React from "react";
13+
import SearchIcon from "@mui/icons-material/Search";
14+
15+
export type SearchBarProps = {
16+
readonly placeholder?: string;
17+
readonly ariaLabel?: string;
18+
readonly sx?: SxProps<Theme>;
19+
readonly loading?: boolean;
20+
readonly onSubmit?: (val: string) => void;
21+
};
22+
23+
export function SearchBar(props: SearchBarProps) {
24+
const [query, setQuery] = React.useState("");
25+
26+
return (
27+
<Box
28+
component="form"
29+
onSubmit={(event) => {
30+
event.preventDefault();
31+
if (props.onSubmit) {
32+
props.onSubmit(query);
33+
}
34+
}}
35+
>
36+
<FormControl sx={props.sx}>
37+
<InputLabel htmlFor="search-bar">
38+
{props.placeholder ?? "Search"}
39+
</InputLabel>
40+
<OutlinedInput
41+
id="search-bar"
42+
type="text"
43+
value={query}
44+
onChange={(event) => setQuery(event.target.value)}
45+
endAdornment={
46+
<InputAdornment position="end">
47+
<IconButton
48+
loading={props.loading}
49+
aria-label={props.ariaLabel ?? "search"}
50+
edge="end"
51+
type="submit"
52+
>
53+
<SearchIcon />
54+
</IconButton>
55+
</InputAdornment>
56+
}
57+
label={props.placeholder ?? "Search"}
58+
></OutlinedInput>
59+
</FormControl>
60+
</Box>
61+
);
62+
}

0 commit comments

Comments
 (0)