Skip to content

Commit 82454de

Browse files
committed
Added initial cache UI framework
Signed-off-by: Zoe Nickson <mnickson@sidingsmedia.com>
1 parent a062f30 commit 82454de

File tree

16 files changed

+593
-17
lines changed

16 files changed

+593
-17
lines changed

package.json

Lines changed: 12 additions & 11 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",
25+
"eslint": "^8",
26+
"typescript": "^5",
2627
"vite": "^5.4.8"
2728
}
28-
}
29+
}

src/App.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import OpenInNewIcon from "@mui/icons-material/OpenInNew";
1212

1313
let navigation: Navigation = [
1414
{
15-
segment: "",
15+
segment: "overview",
1616
title: "Overview",
1717
icon: <DashboardIcon />,
1818
},
@@ -24,7 +24,7 @@ let navigation: Navigation = [
2424
icon: <DnsIcon />,
2525
children: [
2626
{
27-
segment: "rdns/cache",
27+
segment: "cache",
2828
title: "Cache",
2929
icon: <CachedIcon />,
3030
},

src/api/Api.ts

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

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: 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 interface Server {
5+
name: string;
6+
target: string;
7+
id: string;
8+
}

src/components/common/index.ts

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

0 commit comments

Comments
 (0)