From 8797d60be68918a8aa6e0b141337e5e9f10c6a9b Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 28 Mar 2024 22:53:29 +0800 Subject: [PATCH] refactor: support server, location and upstream editor --- .gitignore | 1 + Cargo.lock | 140 +++++++++++++++ Cargo.toml | 2 + TODO.md | 1 + src/serve/admin.rs | 22 ++- src/serve/mod.rs | 1 + src/serve/static_file.rs | 47 +++++ web/src/components/form-editor.tsx | 278 +++++++++++++++++++++++++++-- web/src/components/main-nav.tsx | 6 + web/src/pages/basic-info.tsx | 11 +- web/src/pages/location-info.tsx | 69 ++++++- web/src/pages/server-info.tsx | 81 ++++++++- web/src/pages/upstream-info.tsx | 93 +++++++++- web/src/states/config.ts | 16 ++ web/vite.config.ts | 14 ++ 15 files changed, 753 insertions(+), 29 deletions(-) create mode 100644 src/serve/static_file.rs diff --git a/.gitignore b/.gitignore index fbf69646..45877201 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ target/ /target .vscode node_moudles +dist diff --git a/Cargo.lock b/Cargo.lock index b04639bc..8ad6db11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + [[package]] name = "ahash" version = "0.8.11" @@ -451,6 +457,15 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.4.0" @@ -1099,6 +1114,40 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "include-flate" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2e11569346406931d20276cc460215ee2826e7cad43aa986999cb244dd7adb0" +dependencies = [ + "include-flate-codegen-exports", + "lazy_static", + "libflate", +] + +[[package]] +name = "include-flate-codegen" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a7d6e1419fa3129eb0802b4c99603c0d425c79fb5d76191d5a20d0ab0d664e8" +dependencies = [ + "libflate", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "include-flate-codegen-exports" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75657043ffe3d8280f1cb8aef0f505532b392ed7758e0baeac22edadcee31a03" +dependencies = [ + "include-flate-codegen", + "proc-macro-hack", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -1181,6 +1230,26 @@ version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +[[package]] +name = "libflate" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ff4ae71b685bbad2f2f391fe74f6b7659a34871c08b210fdc039e43bee07d18" +dependencies = [ + "adler32", + "crc32fast", + "libflate_lz77", +] + +[[package]] +name = "libflate_lz77" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a52d3a8bfc85f250440e4424db7d857e241a3aebbbe301f3eb606ab15c39acbf" +dependencies = [ + "rle-decode-fast", +] + [[package]] name = "libredox" version = "0.0.1" @@ -1266,6 +1335,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "miniz_oxide" version = "0.7.2" @@ -1484,6 +1563,7 @@ dependencies = [ "env_logger", "futures-util", "glob", + "hex", "hostname", "http 1.1.0", "humantime", @@ -1495,6 +1575,7 @@ dependencies = [ "pingora", "pretty_assertions", "regex", + "rust-embed", "serde", "serde_json", "snafu", @@ -1830,6 +1911,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + [[package]] name = "proc-macro2" version = "1.0.79" @@ -2024,6 +2111,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rle-decode-fast" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422" + [[package]] name = "rmp" version = "0.8.12" @@ -2046,6 +2139,42 @@ dependencies = [ "serde", ] +[[package]] +name = "rust-embed" +version = "8.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb78f46d0066053d16d4ca7b898e9343bc3530f71c61d5ad84cd404ada068745" +dependencies = [ + "include-flate", + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91ac2a3c6c0520a3fb3dd89321177c3c692937c4eb21893378219da10c44fc8" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.55", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f69089032567ffff4eada41c573fc43ff466c7db7c5688b2e7969584345581" +dependencies = [ + "mime_guess", + "sha2", + "walkdir", +] + [[package]] name = "rust_decimal" version = "1.34.3" @@ -2334,6 +2463,17 @@ dependencies = [ "rust_decimal", ] +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" diff --git a/Cargo.toml b/Cargo.toml index 49565027..87edfef7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ dirs = "5.0.1" env_logger = "0.11.3" futures-util = "0.3.30" glob = "0.3.1" +hex = "0.4.3" hostname = "0.3.1" http = "1.1.0" humantime = "2.1.0" @@ -34,6 +35,7 @@ once_cell = "1.19.0" path-absolutize = "3.1.1" pingora = { version = "0.1.0", default-features = false, features = ["lb"] } regex = "1.10.4" +rust-embed = { version = "8.3.0", features = ["mime-guess", "compression"] } serde = "1.0.197" serde_json = "1.0.114" snafu = "0.8.2" diff --git a/TODO.md b/TODO.md index aa1fc20f..d376e221 100644 --- a/TODO.md +++ b/TODO.md @@ -14,3 +14,4 @@ - [ ] start without config - [ ] static serve for admin - [ ] status:499 for client abort +- [ ] support get pingap start time diff --git a/src/serve/admin.rs b/src/serve/admin.rs index e40d13a1..cd0fdf23 100644 --- a/src/serve/admin.rs +++ b/src/serve/admin.rs @@ -1,17 +1,22 @@ +use super::static_file::StaticFile; use super::Serve; use crate::config::{self, save_config, LocationConf, ServerConf, UpstreamConf}; use crate::state::State; use crate::{cache::HttpResponse, config::PingapConf}; use async_trait::async_trait; -use http::Method; +use http::{Method, StatusCode}; use log::error; use once_cell::sync::Lazy; use pingora::proxy::Session; +use rust_embed::RustEmbed; use serde::{Deserialize, Serialize}; use substring::Substring; -pub struct AdminServe {} +#[derive(RustEmbed)] +#[folder = "dist/"] +struct AdminAsset; +pub struct AdminServe {} pub static ADMIN_SERVE: Lazy<&AdminServe> = Lazy::new(|| &AdminServe {}); #[derive(Serialize, Deserialize)] @@ -170,13 +175,20 @@ impl Serve for AdminServe { _ => self.get_config(category).await, } .unwrap_or_else(|err| { - HttpResponse::try_from_json(&ErrorResponse { + println!("{err:?}"); + let mut resp = HttpResponse::try_from_json(&ErrorResponse { message: err.to_string(), }) - .unwrap_or(HttpResponse::unknown_error()) + .unwrap_or(HttpResponse::unknown_error()); + resp.status = StatusCode::INTERNAL_SERVER_ERROR; + resp }) } else { - HttpResponse::not_found() + let mut file = path.substring(1, path.len()); + if file.is_empty() { + file = "index.html"; + } + StaticFile(AdminAsset::get(file)).into() }; ctx.response_body_size = resp.send(session).await?; Ok(true) diff --git a/src/serve/mod.rs b/src/serve/mod.rs index 643c9606..610b82c6 100644 --- a/src/serve/mod.rs +++ b/src/serve/mod.rs @@ -3,6 +3,7 @@ use async_trait::async_trait; use pingora::proxy::Session; mod admin; +mod static_file; #[async_trait] pub trait Serve { diff --git a/src/serve/static_file.rs b/src/serve/static_file.rs new file mode 100644 index 00000000..e675da08 --- /dev/null +++ b/src/serve/static_file.rs @@ -0,0 +1,47 @@ +use bytes::Bytes; +use hex::encode; +use http::{header, HeaderValue, StatusCode}; +use rust_embed::EmbeddedFile; + +use crate::cache::HttpResponse; + +pub struct StaticFile(pub Option); + +impl From for HttpResponse { + fn from(value: StaticFile) -> Self { + if value.0.is_none() { + return HttpResponse::not_found(); + } + // value 0 is some + let file = value.0.unwrap(); + // hash为基于内容生成 + let str = &encode(file.metadata.sha256_hash())[0..8]; + let mime_type = file.metadata.mimetype(); + // 长度+hash的一部分 + let entity_tag = format!(r#""{:x}-{str}""#, file.data.len()); + // 因为html对于网页是入口,避免缓存后更新不及时 + // 因此设置为0 + // 其它js,css会添加版本号,因此无影响 + let max_age = if mime_type.contains("text/html") { + 0 + } else { + 24 * 3600 + }; + + let mut headers = vec![]; + if let Ok(value) = HeaderValue::from_str(mime_type) { + headers.push((header::CONTENT_TYPE, value)); + } + if let Ok(value) = HeaderValue::from_str(&entity_tag) { + headers.push((header::ETAG, value)); + } + + HttpResponse { + status: StatusCode::OK, + body: Bytes::copy_from_slice(&file.data), + max_age: Some(max_age), + headers: Some(headers), + ..Default::default() + } + } +} diff --git a/web/src/components/form-editor.tsx b/web/src/components/form-editor.tsx index e764e61c..43b8c372 100644 --- a/web/src/components/form-editor.tsx +++ b/web/src/components/form-editor.tsx @@ -1,5 +1,4 @@ import * as React from "react"; -import CardActions from "@mui/material/CardActions"; import CardContent from "@mui/material/CardContent"; import Button from "@mui/material/Button"; import Typography from "@mui/material/Typography"; @@ -10,11 +9,28 @@ import FormControlLabel from "@mui/material/FormControlLabel"; import RadioGroup from "@mui/material/RadioGroup"; import FormLabel from "@mui/material/FormLabel"; import Radio from "@mui/material/Radio"; +import Snackbar from "@mui/material/Snackbar"; +import InputLabel from "@mui/material/InputLabel"; +import MenuItem from "@mui/material/MenuItem"; +import Select from "@mui/material/Select"; +import OutlinedInput from "@mui/material/OutlinedInput"; +import PlaylistRemoveIcon from "@mui/icons-material/PlaylistRemove"; +import IconButton from "@mui/material/IconButton"; +import AddRoadIcon from "@mui/icons-material/AddRoad"; +import Alert from "@mui/material/Alert"; +import CheckIcon from "@mui/icons-material/Check"; + +import Paper from "@mui/material/Paper"; +import { Theme, useTheme } from "@mui/material/styles"; +import { formatError } from "../helpers/util"; export enum FormItemCategory { TEXT = "text", NUMBER = "number", TEXTAREA = "textarea", + LOCATION = "location", + UPSTREAM = "upstream", + ADDRS = "addrs", CHECKBOX = "checkbox", } @@ -24,22 +40,79 @@ export interface FormItem { defaultValue: unknown; span: number; category: FormItemCategory; + minRows?: number; + options?: string[]; +} + +function getDefaultValues(items: FormItem[]) { + const data: Record = {}; + items.forEach((item) => { + data[item.id] = item.defaultValue; + }); + return data; +} + +function getStyles(name: string, selectItems: string[], theme: Theme) { + return { + fontWeight: + selectItems.indexOf(name) === -1 + ? theme.typography.fontWeightRegular + : theme.typography.fontWeightMedium, + }; } export default function FormEditor({ title, description, items, - onUpdate, + onUpsert, }: { title: string; description: string; items: FormItem[]; - onUpdate: (data: Record) => void; + onUpsert: (name: string, data: Record) => Promise; }) { - const data: Record = {}; + const theme = useTheme(); + const [data, setData] = React.useState(getDefaultValues(items)); + const defaultLocations: string[] = []; + const defaultAddrs: string[] = []; + let defaultUpstream = ""; + items.forEach((item) => { + switch (item.category) { + case FormItemCategory.LOCATION: { + const arr = item.defaultValue as string[]; + arr.forEach((lo) => { + defaultLocations.push(lo); + }); + break; + } + case FormItemCategory.UPSTREAM: { + defaultUpstream = item.defaultValue as string; + break; + } + case FormItemCategory.ADDRS: { + const arr = item.defaultValue as string[]; + arr.forEach((addr) => { + defaultAddrs.push(addr); + }); + break; + } + } + }); + + const [locations, setLocations] = React.useState(defaultLocations); + const [upstream, setUpstream] = React.useState(defaultUpstream); + const [addrs, setAddrs] = React.useState(defaultAddrs); + + const [updated, setUpdated] = React.useState(false); + const [processing, setProcessing] = React.useState(false); + const [showSuccess, setShowSuccess] = React.useState(false); + + const [showError, setShowError] = React.useState({ + open: false, + message: "", + }); const list = items.map((item) => { - data[item.id] = item.defaultValue; let formItem: JSX.Element = <>; switch (item.category) { case FormItemCategory.CHECKBOX: { @@ -76,7 +149,7 @@ export default function FormEditor({ break; } } - data[item.id] = checked; + updateValue(item.id, checked); }} > } label="Yes" /> @@ -87,17 +160,141 @@ export default function FormEditor({ ); break; } + case FormItemCategory.LOCATION: { + const options = item.options || []; + formItem = ( + + {item.label} + + + ); + break; + } + case FormItemCategory.UPSTREAM: { + const options = item.options || []; + formItem = ( + + {item.label} + + + ); + break; + } + case FormItemCategory.ADDRS: { + const list = addrs.map((addr, index) => { + return ( + + { + const value = e.target.value.trim(); + const values = addrs.slice(0); + values[index] = value; + setAddrs(values); + updateValue(item.id, values); + }} + /> + { + const values = addrs.slice(0); + values.splice(index, 1); + setAddrs(values); + updateValue(item.id, values); + }} + > + + + + ); + }); + list.push( + , + ); + formItem = {list}; + break; + } case FormItemCategory.TEXTAREA: { + let minRows = 4; + if (item.minRows) { + minRows = item.minRows; + } formItem = ( { - data[item.id] = e.target.value.trim(); + updateValue(item.id, e.target.value.trim()); }} /> ); @@ -115,14 +312,14 @@ export default function FormEditor({ switch (item.category) { case FormItemCategory.NUMBER: { if (value) { - data[item.id] = Number(value); + updateValue(item.id, Number(value)); } else { - data[item.id] = null; + updateValue(item.id, null); } break; } default: { - data[item.id] = value; + updateValue(item.id, value); break; } } @@ -138,6 +335,33 @@ export default function FormEditor({ ); }); + const updateValue = (key: string, value: unknown) => { + setShowSuccess(false); + const values = Object.assign({}, data); + if (!value && typeof value == "string") { + value = null; + } + values[key] = value; + setUpdated(true); + setData(values); + }; + const doUpsert = async () => { + if (processing) { + return; + } + setProcessing(true); + try { + await onUpsert("", data); + setShowSuccess(true); + } catch (err) { + setShowError({ + open: true, + message: formatError(err), + }); + } finally { + setProcessing(false); + } + }; return ( @@ -154,12 +378,38 @@ export default function FormEditor({
{list} + + +
- - - + {showSuccess && ( + } severity="success"> + Update success! + + )} + { + setShowError({ + open: false, + message: "", + }); + }} + message={showError.message} + />
); } diff --git a/web/src/components/main-nav.tsx b/web/src/components/main-nav.tsx index 7b6c3c76..b8e9a0ba 100644 --- a/web/src/components/main-nav.tsx +++ b/web/src/components/main-nav.tsx @@ -182,6 +182,12 @@ export default function MainNav() { { + setShowError({ + open: false, + message: "", + }); + }} message={showError.message} /> diff --git a/web/src/pages/basic-info.tsx b/web/src/pages/basic-info.tsx index b6e89659..af00766f 100644 --- a/web/src/pages/basic-info.tsx +++ b/web/src/pages/basic-info.tsx @@ -6,9 +6,10 @@ import FormEditor, { } from "../components/form-editor"; export default function BasicInfo() { - const [initialized, config] = useConfigStore((state) => [ + const [initialized, config, update] = useConfigStore((state) => [ state.initialized, state.data, + state.update, ]); if (!initialized) { return ; @@ -61,17 +62,19 @@ export default function BasicInfo() { label: "Error Template", defaultValue: config.error_template, span: 12, + minRows: 8, category: FormItemCategory.TEXTAREA, }, ]; + const onUpsert = async (name: string, data: Record) => { + return update("pingap", "basic", data); + }; return ( { - console.dir(data); - }} + onUpsert={onUpsert} /> ); } diff --git a/web/src/pages/location-info.tsx b/web/src/pages/location-info.tsx index 19f38cff..adcc8f06 100644 --- a/web/src/pages/location-info.tsx +++ b/web/src/pages/location-info.tsx @@ -1,5 +1,70 @@ -import Container from "@mui/material/Container"; +import useConfigStore from "../states/config"; +import { useParams } from "react-router-dom"; + +import Loading from "../components/loading"; +import FormEditor, { + FormItem, + FormItemCategory, +} from "../components/form-editor"; export default function LocationInfo() { - return LocationInfo; + const [initialized, config, update] = useConfigStore((state) => [ + state.initialized, + state.data, + state.update, + ]); + const { name } = useParams(); + if (!initialized) { + return ; + } + const locations = config.locations || {}; + const location = locations[name || ""]; + const upstreams = Object.keys(config.upstreams || {}); + + const arr: FormItem[] = [ + { + id: "host", + label: "Host", + defaultValue: location.host, + span: 6, + category: FormItemCategory.TEXT, + }, + { + id: "path", + label: "Path", + defaultValue: location.path, + span: 6, + category: FormItemCategory.TEXT, + }, + { + id: "upstream", + label: "Upstream", + defaultValue: location.upstream, + span: 6, + category: FormItemCategory.UPSTREAM, + options: upstreams, + }, + // proxy header + // header + { + id: "rewrite", + label: "Rewrite", + defaultValue: location.rewrite, + span: 6, + category: FormItemCategory.TEXT, + }, + ]; + + const onUpsert = async (_: string, data: Record) => { + return update("location", name || "", data); + }; + return ( + + ); } diff --git a/web/src/pages/server-info.tsx b/web/src/pages/server-info.tsx index 67973de2..685bcb8f 100644 --- a/web/src/pages/server-info.tsx +++ b/web/src/pages/server-info.tsx @@ -1,5 +1,82 @@ -import Container from "@mui/material/Container"; +import useConfigStore from "../states/config"; +import { useParams } from "react-router-dom"; + +import Loading from "../components/loading"; +import FormEditor, { + FormItem, + FormItemCategory, +} from "../components/form-editor"; export default function ServerInfo() { - return ServerInfo; + const [initialized, config, update] = useConfigStore((state) => [ + state.initialized, + state.data, + state.update, + ]); + const { name } = useParams(); + if (!initialized) { + return ; + } + const servers = config.servers || {}; + const server = servers[name || ""]; + const locations = Object.keys(config.locations || {}); + + const arr: FormItem[] = [ + { + id: "addr", + label: "Listen Address", + defaultValue: server.addr, + span: 6, + category: FormItemCategory.TEXT, + }, + { + id: "locations", + label: "Locations", + defaultValue: server.locations, + span: 6, + category: FormItemCategory.LOCATION, + options: locations, + }, + { + id: "stats_path", + label: "Stats Path", + defaultValue: server.stats_path, + span: 6, + category: FormItemCategory.TEXT, + }, + { + id: "access_log", + label: "Access Log", + defaultValue: server.access_log, + span: 12, + category: FormItemCategory.TEXT, + }, + { + id: "tls_cert", + label: "Tls Cert(base64)", + defaultValue: server.tls_cert, + span: 6, + category: FormItemCategory.TEXT, + }, + { + id: "tls_key", + label: "Tls Key(base64)", + defaultValue: server.tls_key, + span: 6, + category: FormItemCategory.TEXT, + }, + ]; + + const onUpsert = async (_: string, data: Record) => { + return update("server", name || "", data); + }; + return ( + + ); } diff --git a/web/src/pages/upstream-info.tsx b/web/src/pages/upstream-info.tsx index 815dc355..bf78dcb7 100644 --- a/web/src/pages/upstream-info.tsx +++ b/web/src/pages/upstream-info.tsx @@ -1,5 +1,94 @@ -import Container from "@mui/material/Container"; +import useConfigStore from "../states/config"; +import { useParams } from "react-router-dom"; + +import Loading from "../components/loading"; +import FormEditor, { + FormItem, + FormItemCategory, +} from "../components/form-editor"; export default function UpstreamInfo() { - return UptreamInfo; + const [initialized, config, update] = useConfigStore((state) => [ + state.initialized, + state.data, + state.update, + ]); + const { name } = useParams(); + if (!initialized) { + return ; + } + const upstreams = config.upstreams || {}; + const upstream = upstreams[name || ""]; + + const arr: FormItem[] = [ + { + id: "addrs", + label: "Upstream Addrs", + defaultValue: upstream.addrs, + span: 12, + category: FormItemCategory.ADDRS, + }, + { + id: "algo", + label: "Load balancer algorithm", + defaultValue: upstream.algo, + span: 6, + category: FormItemCategory.TEXT, + }, + { + id: "health_check", + label: "Health Check", + defaultValue: upstream.health_check, + span: 6, + category: FormItemCategory.TEXT, + }, + { + id: "connection_timeout", + label: "Connection Timeout", + defaultValue: upstream.connection_timeout, + span: 6, + category: FormItemCategory.TEXT, + }, + { + id: "total_connection_timeout", + label: "Total Connection Timeout", + defaultValue: upstream.total_connection_timeout, + span: 6, + category: FormItemCategory.TEXT, + }, + { + id: "read_timeout", + label: "Read Timeout", + defaultValue: upstream.read_timeout, + span: 6, + category: FormItemCategory.TEXT, + }, + { + id: "write_timeout", + label: "Write Timeout", + defaultValue: upstream.write_timeout, + span: 6, + category: FormItemCategory.TEXT, + }, + { + id: "idle_timeout", + label: "Idle Timeout", + defaultValue: upstream.idle_timeout, + span: 6, + category: FormItemCategory.TEXT, + }, + ]; + + const onUpsert = async (_: string, data: Record) => { + return update("upstream", name || "", data); + }; + return ( + + ); } diff --git a/web/src/states/config.ts b/web/src/states/config.ts index 677bd06d..3b4a585e 100644 --- a/web/src/states/config.ts +++ b/web/src/states/config.ts @@ -49,6 +49,11 @@ interface ConfigState { data: Config; initialized: boolean; fetch: () => Promise; + update: ( + category: string, + name: string, + data: Record, + ) => Promise; } const useConfigStore = create()((set, get) => ({ @@ -62,6 +67,17 @@ const useConfigStore = create()((set, get) => ({ }); return data; }, + update: async ( + category: string, + name: string, + updateData: Record, + ) => { + await request.post(`/configs/${category}/${name}`, updateData); + const { data } = await request.get("/configs"); + set({ + data, + }); + }, })); export default useConfigStore; diff --git a/web/vite.config.ts b/web/vite.config.ts index d5d5957b..87b84ea0 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -1,9 +1,23 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; +function manualChunks(id) { + if (id.includes("node_modules")) { + return "vendor"; + } +} + // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], + base: "./", + build: { + rollupOptions: { + output: { + manualChunks, + }, + }, + }, server: { proxy: { "/api": {