Skip to content

Commit 6785931

Browse files
committed
Settings polish
1 parent c08b1be commit 6785931

File tree

9 files changed

+391
-118
lines changed

9 files changed

+391
-118
lines changed

frontend/src/api/backend/models.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -196,10 +196,10 @@ export interface Stream {
196196

197197
export interface Setting {
198198
id: string;
199-
name: string;
200-
description: string;
199+
name?: string;
200+
description?: string;
201201
value: string;
202-
meta: Record<string, any>;
202+
meta?: Record<string, any>;
203203
}
204204

205205
export interface DNSProvider {

frontend/src/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export * from "./useProxyHost";
1313
export * from "./useProxyHosts";
1414
export * from "./useRedirectionHost";
1515
export * from "./useRedirectionHosts";
16+
export * from "./useSetting";
1617
export * from "./useStream";
1718
export * from "./useStreams";
1819
export * from "./useTheme";

frontend/src/hooks/useSetting.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
2+
import { getSetting, type Setting, updateSetting } from "src/api/backend";
3+
4+
const fetchSetting = (id: string) => {
5+
return getSetting(id);
6+
};
7+
8+
const useSetting = (id: string, options = {}) => {
9+
return useQuery<Setting, Error>({
10+
queryKey: ["setting", id],
11+
queryFn: () => fetchSetting(id),
12+
staleTime: 60 * 1000, // 1 minute
13+
...options,
14+
});
15+
};
16+
17+
const useSetSetting = () => {
18+
const queryClient = useQueryClient();
19+
return useMutation({
20+
mutationFn: (values: Setting) => updateSetting(values),
21+
onMutate: (values: Setting) => {
22+
if (!values.id) {
23+
return;
24+
}
25+
const previousObject = queryClient.getQueryData(["setting", values.id]);
26+
queryClient.setQueryData(["setting", values.id], (old: Setting) => ({
27+
...old,
28+
...values,
29+
}));
30+
return () => queryClient.setQueryData(["setting", values.id], previousObject);
31+
},
32+
onError: (_, __, rollback: any) => rollback(),
33+
onSuccess: async ({ id }: Setting) => {
34+
queryClient.invalidateQueries({ queryKey: ["setting", id] });
35+
queryClient.invalidateQueries({ queryKey: ["audit-logs"] });
36+
},
37+
});
38+
};
39+
40+
export { useSetting, useSetSetting };

frontend/src/locale/lang/en.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,16 @@
168168
"role.admin": "Administrator",
169169
"role.standard-user": "Standard User",
170170
"save": "Save",
171+
"setting": "Setting",
171172
"settings": "Settings",
173+
"settings.default-site": "Default Site",
174+
"settings.default-site.404": "404 Page",
175+
"settings.default-site.444": "No Response (444)",
176+
"settings.default-site.congratulations": "Congratulations Page",
177+
"settings.default-site.description": "What to show when Nginx is hit with an unknown Host",
178+
"settings.default-site.html": "Custom HTML",
179+
"settings.default-site.html.placeholder": "<!-- Enter your custom HTML content here -->",
180+
"settings.default-site.redirect": "Redirect",
172181
"setup.preamble": "Get started by creating your admin account.",
173182
"setup.title": "Welcome!",
174183
"sign-in": "Sign in",

frontend/src/locale/src/en.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,9 +506,36 @@
506506
"save": {
507507
"defaultMessage": "Save"
508508
},
509+
"setting": {
510+
"defaultMessage": "Setting"
511+
},
509512
"settings": {
510513
"defaultMessage": "Settings"
511514
},
515+
"settings.default-site": {
516+
"defaultMessage": "Default Site"
517+
},
518+
"settings.default-site.404": {
519+
"defaultMessage": "404 Page"
520+
},
521+
"settings.default-site.444": {
522+
"defaultMessage": "No Response (444)"
523+
},
524+
"settings.default-site.congratulations": {
525+
"defaultMessage": "Congratulations Page"
526+
},
527+
"settings.default-site.description": {
528+
"defaultMessage": "What to show when Nginx is hit with an unknown Host"
529+
},
530+
"settings.default-site.html": {
531+
"defaultMessage": "Custom HTML"
532+
},
533+
"settings.default-site.html.placeholder": {
534+
"defaultMessage": "<!-- Enter your custom HTML content here -->"
535+
},
536+
"settings.default-site.redirect": {
537+
"defaultMessage": "Redirect"
538+
},
512539
"setup.preamble": {
513540
"defaultMessage": "Get started by creating your admin account."
514541
},
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
import CodeEditor from "@uiw/react-textarea-code-editor";
2+
import { Field, Form, Formik } from "formik";
3+
import { type ReactNode, useState } from "react";
4+
import { Alert } from "react-bootstrap";
5+
import { Button, Loading } from "src/components";
6+
import { useSetSetting, useSetting } from "src/hooks";
7+
import { intl, T } from "src/locale";
8+
import { validateString } from "src/modules/Validations";
9+
import { showObjectSuccess } from "src/notifications";
10+
11+
export default function DefaultSite() {
12+
const { data, isLoading, error } = useSetting("default-site");
13+
const { mutate: setSetting } = useSetSetting();
14+
const [errorMsg, setErrorMsg] = useState<ReactNode | null>(null);
15+
const [isSubmitting, setIsSubmitting] = useState(false);
16+
17+
const onSubmit = async (values: any, { setSubmitting }: any) => {
18+
if (isSubmitting) return;
19+
setIsSubmitting(true);
20+
setErrorMsg(null);
21+
22+
const payload = {
23+
id: "default-site",
24+
value: values.value,
25+
meta: {
26+
redirect: values.redirect,
27+
html: values.html,
28+
},
29+
};
30+
31+
setSetting(payload, {
32+
onError: (err: any) => setErrorMsg(<T id={err.message} />),
33+
onSuccess: () => {
34+
showObjectSuccess("setting", "saved");
35+
},
36+
onSettled: () => {
37+
setIsSubmitting(false);
38+
setSubmitting(false);
39+
},
40+
});
41+
};
42+
43+
if (!isLoading && error) {
44+
return (
45+
<div className="card-body">
46+
<div className="mb-3">
47+
<Alert variant="danger" show>
48+
{error.message}
49+
</Alert>
50+
</div>
51+
</div>
52+
);
53+
}
54+
55+
if (isLoading) {
56+
return (
57+
<div className="card-body">
58+
<div className="mb-3">
59+
<Loading noLogo />
60+
</div>
61+
</div>
62+
);
63+
}
64+
65+
return (
66+
<Formik
67+
initialValues={
68+
{
69+
value: data?.value || "congratulations",
70+
redirect: data?.meta?.redirect || "",
71+
html: data?.meta?.html || "",
72+
} as any
73+
}
74+
onSubmit={onSubmit}
75+
>
76+
{({ values }) => (
77+
<Form>
78+
<div className="card-body">
79+
<Alert variant="danger" show={!!errorMsg} onClose={() => setErrorMsg(null)} dismissible>
80+
{errorMsg}
81+
</Alert>
82+
<Field name="value">
83+
{({ field, form }: any) => (
84+
<div className="mb-3">
85+
<label className="form-label" htmlFor="setting-host-unknown">
86+
<T id="settings.default-site.description" />
87+
</label>
88+
<div className="form-selectgroup form-selectgroup-boxes d-flex flex-column">
89+
<label className="form-selectgroup-item flex-fill">
90+
<input
91+
type="radio"
92+
name={field.name}
93+
value="congratulations"
94+
className="form-selectgroup-input"
95+
checked={field.value === "congratulations"}
96+
onChange={(e) => form.setFieldValue(field.name, e.target.value)}
97+
/>
98+
<div className="form-selectgroup-label d-flex align-items-center p-3">
99+
<div className="me-3">
100+
<span className="form-selectgroup-check" />
101+
</div>
102+
<div>
103+
<T id="settings.default-site.congratulations" />
104+
</div>
105+
</div>
106+
</label>
107+
<label className="form-selectgroup-item flex-fill">
108+
<input
109+
type="radio"
110+
name={field.name}
111+
value="404"
112+
className="form-selectgroup-input"
113+
checked={field.value === "404"}
114+
onChange={(e) => form.setFieldValue(field.name, e.target.value)}
115+
/>
116+
<div className="form-selectgroup-label d-flex align-items-center p-3">
117+
<div className="me-3">
118+
<span className="form-selectgroup-check" />
119+
</div>
120+
<div>
121+
<T id="settings.default-site.404" />
122+
</div>
123+
</div>
124+
</label>
125+
<label className="form-selectgroup-item flex-fill">
126+
<input
127+
type="radio"
128+
name={field.name}
129+
value="444"
130+
className="form-selectgroup-input"
131+
checked={field.value === "444"}
132+
onChange={(e) => form.setFieldValue(field.name, e.target.value)}
133+
/>
134+
<div className="form-selectgroup-label d-flex align-items-center p-3">
135+
<div className="me-3">
136+
<span className="form-selectgroup-check" />
137+
</div>
138+
<div>
139+
<T id="settings.default-site.444" />
140+
</div>
141+
</div>
142+
</label>
143+
<label className="form-selectgroup-item flex-fill">
144+
<input
145+
type="radio"
146+
name={field.name}
147+
value="redirect"
148+
className="form-selectgroup-input"
149+
checked={field.value === "redirect"}
150+
onChange={(e) => form.setFieldValue(field.name, e.target.value)}
151+
/>
152+
<div className="form-selectgroup-label d-flex align-items-center p-3">
153+
<div className="me-3">
154+
<span className="form-selectgroup-check" />
155+
</div>
156+
<div>
157+
<T id="settings.default-site.redirect" />
158+
</div>
159+
</div>
160+
</label>
161+
<label className="form-selectgroup-item flex-fill">
162+
<input
163+
type="radio"
164+
name={field.name}
165+
value="html"
166+
className="form-selectgroup-input"
167+
checked={field.value === "html"}
168+
onChange={(e) => form.setFieldValue(field.name, e.target.value)}
169+
/>
170+
<div className="form-selectgroup-label d-flex align-items-center p-3">
171+
<div className="me-3">
172+
<span className="form-selectgroup-check" />
173+
</div>
174+
<div>
175+
<T id="settings.default-site.redirect" />
176+
</div>
177+
</div>
178+
</label>
179+
</div>
180+
</div>
181+
)}
182+
</Field>
183+
{values.value === "redirect" && (
184+
<Field name="redirect" validate={validateString(1, 255)}>
185+
{({ field, form }: any) => (
186+
<div className="mt-5 mb-3">
187+
<label className="form-label" htmlFor="setting-host-unknown">
188+
<T id="settings.default-site.redirect" />
189+
</label>
190+
<div>
191+
<input
192+
id="redirect"
193+
type="text"
194+
placeholder="https://"
195+
required
196+
autoComplete="off"
197+
className="form-control"
198+
{...field}
199+
/>
200+
{form.errors.redirect ? (
201+
<div className="invalid-feedback">
202+
{form.errors.redirect && form.touched.redirect
203+
? form.errors.redirect
204+
: null}
205+
</div>
206+
) : null}
207+
</div>
208+
</div>
209+
)}
210+
</Field>
211+
)}
212+
{values.value === "html" && (
213+
<Field name="html" validate={validateString(1)}>
214+
{({ field, form }: any) => (
215+
<div className="mt-5 mb-3">
216+
<label className="form-label" htmlFor="setting-host-unknown">
217+
<T id="settings.default-site.html" />
218+
</label>
219+
<div>
220+
<CodeEditor
221+
// Believe it or not, 'html' sucks yet 'php' renders the html
222+
// content much nicer.
223+
language="php"
224+
placeholder={intl.formatMessage({
225+
id: "settings.default-site.html.placeholder",
226+
})}
227+
padding={15}
228+
data-color-mode="dark"
229+
minHeight={300}
230+
indentWidth={2}
231+
style={{
232+
fontFamily:
233+
"ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace",
234+
borderRadius: "0.3rem",
235+
minHeight: "300px",
236+
backgroundColor: "var(--tblr-bg-surface-dark)",
237+
}}
238+
{...field}
239+
/>
240+
{form.errors.html ? (
241+
<div className="invalid-feedback">
242+
{form.errors.html && form.touched.html ? form.errors.html : null}
243+
</div>
244+
) : null}
245+
</div>
246+
</div>
247+
)}
248+
</Field>
249+
)}
250+
</div>
251+
<div className="card-footer bg-transparent mt-auto">
252+
<div className="btn-list justify-content-end">
253+
<Button
254+
type="submit"
255+
actionType="primary"
256+
className="ms-auto bg-teal"
257+
data-bs-dismiss="modal"
258+
isLoading={isSubmitting}
259+
disabled={isSubmitting}
260+
>
261+
<T id="save" />
262+
</Button>
263+
</div>
264+
</div>
265+
</Form>
266+
)}
267+
</Formik>
268+
);
269+
}

0 commit comments

Comments
 (0)