feat: custom domain management, responsive server layout, and config editor integration

This commit is contained in:
Bot
2026-08-31 04:41:57 +08:00
parent 68f3e02e26
commit af8b05b690
7 changed files with 3159 additions and 2872 deletions
+6 -3
View File
@@ -29,12 +29,15 @@
"typescript/no-unnecessary-type-constraint": "error", "typescript/no-unnecessary-type-constraint": "error",
"typescript/no-unsafe-function-type": "error", "typescript/no-unsafe-function-type": "error",
"react/rules-of-hooks": "error", "react/rules-of-hooks": "error",
"react/exhaustive-deps": "warn",
"typescript/no-explicit-any": "off", "typescript/no-explicit-any": "off",
"react/only-export-components": "off", "react/only-export-components": "off",
// React Compiler lint stays opt-in until the compiler is adopted. "react/immutability": "off",
"react/react-compiler": "off" "react/set-state-in-effect": "off",
"react/incompatible-library": "off",
"react/refs": "off"
}, },
"overrides": [ "overrides": [
{ {
"files": [ "files": [
+3120 -2853
View File
File diff suppressed because it is too large Load Diff
+5 -6
View File
@@ -87,14 +87,13 @@ export const BatchEditNoteIcon: React.FC<BatchEditNoteIconProps> = ({
// For batch, usually we only override if they typed something. // For batch, usually we only override if they typed something.
// We can provide a checkbox or just update if not empty. // We can provide a checkbox or just update if not empty.
// Let's just update if not empty for safety. // Let's just update if not empty for safety.
let updatePayload = { ...server } const updatePayload = {
if (values.note && values.note.trim() !== "") { ...server,
updatePayload.note = values.note note: values.note && values.note.trim() !== "" ? values.note : server.note,
} public_note: values.public_note && values.public_note.trim() !== "" ? values.public_note : server.public_note,
if (values.public_note && values.public_note.trim() !== "") {
updatePayload.public_note = values.public_note
} }
// Clean up non-API fields // Clean up non-API fields
return updateServer(id, { return updateServer(id, {
name: updatePayload.name, name: updatePayload.name,
+16 -2
View File
@@ -62,7 +62,14 @@ const resources = {
} }
const getStoredLanguage = () => { const getStoredLanguage = () => {
return localStorage.getItem("language") || "en-US" try {
if (typeof window !== "undefined" && window.localStorage) {
return window.localStorage.getItem("language") || "en-US"
}
} catch {
// ignore
}
return "en-US"
} }
i18n.use(initReactI18next).init({ i18n.use(initReactI18next).init({
@@ -75,7 +82,14 @@ i18n.use(initReactI18next).init({
}) })
i18n.on("languageChanged", (lng) => { i18n.on("languageChanged", (lng) => {
localStorage.setItem("language", lng) try {
if (typeof window !== "undefined" && window.localStorage) {
window.localStorage.setItem("language", lng)
}
} catch {
// ignore
}
}) })
export default i18n export default i18n
+4 -3
View File
@@ -129,10 +129,10 @@ export default function NATPage() {
return ( return (
<div className="px-3"> <div className="px-3">
<div className="flex mt-6 mb-4"> <div className="flex flex-col sm:flex-row sm:items-center justify-between w-full gap-4 mt-6 mb-4">
<h1 className="flex-1 text-3xl font-bold tracking-tight"> {t("NATT")}</h1> <h1 className="text-3xl font-bold tracking-tight shrink-0">{t("NATT")}</h1>
<HeaderButtonGroup <HeaderButtonGroup
className="flex ml-auto self-end sm:self-auto gap-2 flex-wrap shrink-0" className="flex items-center gap-2 flex-wrap shrink-0 sm:ml-auto"
delete={{ delete={{
fn: deleteNAT, fn: deleteNAT,
id: selectedRows.map((r) => r.original.id!), id: selectedRows.map((r) => r.original.id!),
@@ -143,6 +143,7 @@ export default function NATPage() {
</HeaderButtonGroup> </HeaderButtonGroup>
</div> </div>
<Table> <Table>
<TableHeader> <TableHeader>
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
+4 -3
View File
@@ -198,10 +198,10 @@ export default function ServerPage() {
return ( return (
<div className="px-3"> <div className="px-3">
<div className="flex mt-6 mb-4"> <div className="flex flex-col sm:flex-row sm:items-center justify-between w-full gap-4 mt-6 mb-4">
<h1 className="text-3xl font-bold tracking-tight">{t("Server")}</h1> <h1 className="text-3xl font-bold tracking-tight shrink-0">{t("Server")}</h1>
<HeaderButtonGroup <HeaderButtonGroup
className="flex-2 flex ml-auto gap-2" className="flex items-center gap-2 flex-wrap shrink-0 sm:ml-auto"
delete={{ delete={{
fn: deleteServer, fn: deleteServer,
id: selectedRows.map((r) => r.original.id).filter(Boolean) as number[], id: selectedRows.map((r) => r.original.id).filter(Boolean) as number[],
@@ -254,6 +254,7 @@ export default function ServerPage() {
<InstallCommandsMenu className="shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] bg-blue-700 text-white hover:bg-blue-600 dark:hover:bg-blue-800 rounded-lg" /> <InstallCommandsMenu className="shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] bg-blue-700 text-white hover:bg-blue-600 dark:hover:bg-blue-800 rounded-lg" />
</HeaderButtonGroup> </HeaderButtonGroup>
</div> </div>
<Table> <Table>
<TableHeader> <TableHeader>
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
+3 -1
View File
@@ -69,9 +69,11 @@ const settingFormSchema = z.object({
admin_email: asOptionalField(z.string()), admin_email: asOptionalField(z.string()),
domain_expiry_notification_days: asOptionalField(z.string()), domain_expiry_notification_days: asOptionalField(z.string()),
server_expiry_notification_days: asOptionalField(z.string()), server_expiry_notification_days: asOptionalField(z.string()),
expiry_notification_group_id: z.coerce.number().int().min(0), expiry_notification_group_id: asOptionalField(z.coerce.number().int().min(0)),
enable_mcp: asOptionalField(z.boolean()), enable_mcp: asOptionalField(z.boolean()),
}) })
export default function SettingsPage() { export default function SettingsPage() {