Skip to content

Commit f2b5b19

Browse files
committed
More react
- consolidated lang items - proxy host paths work
1 parent 7af01d0 commit f2b5b19

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+954
-936
lines changed

frontend/package.json

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,47 +18,47 @@
1818
"dependencies": {
1919
"@tabler/core": "^1.4.0",
2020
"@tabler/icons-react": "^3.35.0",
21-
"@tanstack/react-query": "^5.89.0",
21+
"@tanstack/react-query": "^5.90.3",
2222
"@tanstack/react-table": "^8.21.3",
2323
"@uiw/react-textarea-code-editor": "^3.1.1",
2424
"classnames": "^2.5.1",
25-
"country-flag-icons": "^1.5.20",
25+
"country-flag-icons": "^1.5.21",
2626
"date-fns": "^4.1.0",
2727
"ez-modal-react": "^1.0.5",
2828
"formik": "^2.4.6",
2929
"generate-password-browser": "^1.1.0",
3030
"humps": "^2.0.1",
3131
"query-string": "^9.3.1",
32-
"react": "^19.1.1",
32+
"react": "^19.2.0",
3333
"react-bootstrap": "^2.10.10",
34-
"react-dom": "^19.1.1",
35-
"react-intl": "^7.1.11",
36-
"react-router-dom": "^7.9.1",
34+
"react-dom": "^19.2.0",
35+
"react-intl": "^7.1.14",
36+
"react-router-dom": "^7.9.4",
3737
"react-select": "^5.10.2",
3838
"react-toastify": "^11.0.5",
3939
"rooks": "^9.3.0"
4040
},
4141
"devDependencies": {
42-
"@biomejs/biome": "^2.2.4",
43-
"@formatjs/cli": "^6.7.2",
44-
"@tanstack/react-query-devtools": "^5.89.0",
42+
"@biomejs/biome": "^2.2.6",
43+
"@formatjs/cli": "^6.7.4",
44+
"@tanstack/react-query-devtools": "^5.90.2",
4545
"@testing-library/dom": "^10.4.1",
46-
"@testing-library/jest-dom": "^6.8.0",
46+
"@testing-library/jest-dom": "^6.9.1",
4747
"@testing-library/react": "^16.3.0",
4848
"@types/country-flag-icons": "^1.2.2",
4949
"@types/humps": "^2.0.6",
50-
"@types/react": "^19.1.13",
51-
"@types/react-dom": "^19.1.9",
50+
"@types/react": "^19.2.2",
51+
"@types/react-dom": "^19.2.2",
5252
"@types/react-table": "^7.7.20",
53-
"@vitejs/plugin-react": "^5.0.3",
54-
"happy-dom": "^18.0.1",
53+
"@vitejs/plugin-react": "^5.0.4",
54+
"happy-dom": "^20.0.2",
5555
"postcss": "^8.5.6",
5656
"postcss-simple-vars": "^7.0.1",
57-
"sass": "^1.93.0",
57+
"sass": "^1.93.2",
5858
"tmp": "^0.2.5",
59-
"typescript": "5.9.2",
60-
"vite": "^7.1.6",
61-
"vite-plugin-checker": "^0.10.3",
59+
"typescript": "5.9.3",
60+
"vite": "^7.1.10",
61+
"vite-plugin-checker": "^0.11.0",
6262
"vite-tsconfig-paths": "^5.1.4",
6363
"vitest": "^3.2.4"
6464
}

frontend/src/api/backend/models.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,14 @@ export interface Certificate {
9797
redirectionHosts?: RedirectionHost[];
9898
}
9999

100+
export interface ProxyLocation {
101+
path: string;
102+
advancedConfig: string;
103+
forwardScheme: string;
104+
forwardHost: string;
105+
forwardPort: number;
106+
}
107+
100108
export interface ProxyHost {
101109
id: number;
102110
createdOn: string;
@@ -116,7 +124,7 @@ export interface ProxyHost {
116124
allowWebsocketUpgrade: boolean;
117125
http2Support: boolean;
118126
enabled: boolean;
119-
locations?: string[]; // todo: string or object?
127+
locations?: ProxyLocation[];
120128
hstsEnabled: boolean;
121129
hstsSubdomains: boolean;
122130
// Expansions:
Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
import type { Table as ReactTable } from "@tanstack/react-table";
2+
import cn from "classnames";
3+
import type { ReactNode } from "react";
24
import { Button } from "src/components";
35
import { T } from "src/locale";
46

57
interface Props {
68
tableInstance: ReactTable<any>;
79
onNew?: () => void;
810
isFiltered?: boolean;
11+
object: string;
12+
objects: string;
13+
color?: string;
14+
customAddBtn?: ReactNode;
915
}
10-
export default function Empty({ tableInstance, onNew, isFiltered }: Props) {
16+
function EmptyData({ tableInstance, onNew, isFiltered, object, objects, color = "primary", customAddBtn }: Props) {
1117
return (
1218
<tr>
1319
<td colSpan={tableInstance.getVisibleFlatColumns().length}>
@@ -19,18 +25,24 @@ export default function Empty({ tableInstance, onNew, isFiltered }: Props) {
1925
) : (
2026
<>
2127
<h2>
22-
<T id="access.empty" />
28+
<T id="object.empty" tData={{ objects }} />
2329
</h2>
2430
<p className="text-muted">
2531
<T id="empty-subtitle" />
2632
</p>
27-
<Button className="btn-cyan my-3" onClick={onNew}>
28-
<T id="access.add" />
29-
</Button>
33+
{customAddBtn ? (
34+
customAddBtn
35+
) : (
36+
<Button className={cn("my-3", `btn-${color}`)} onClick={onNew}>
37+
<T id="object.add" tData={{ object }} />
38+
</Button>
39+
)}
3040
</>
3141
)}
3242
</div>
3343
</td>
3444
</tr>
3545
);
3646
}
47+
48+
export { EmptyData };

frontend/src/components/ErrorNotFound.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export function ErrorNotFound() {
1212
<T id="notfound.title" />
1313
</p>
1414
<p className="empty-subtitle text-secondary">
15-
<T id="notfound.text" />
15+
<T id="notfound.content" />
1616
</p>
1717
<div className="empty-action">
1818
<Button type="button" size="md" onClick={() => navigate("/")}>

frontend/src/components/Form/AccessClientFields.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export function AccessClientFields({ initialValues, name = "clients" }: Props) {
4848
return (
4949
<>
5050
<p className="text-muted">
51-
<T id="access.help.rules-order" />
51+
<T id="access-list.help.rules-order" />
5252
</p>
5353
{values.map((client: AccessListClient, idx: number) => (
5454
<div className="row mb-1" key={idx}>
@@ -101,7 +101,7 @@ export function AccessClientFields({ initialValues, name = "clients" }: Props) {
101101
</div>
102102
<div className="row mb-3">
103103
<p className="text-muted">
104-
<T id="access.help-rules-last" />
104+
<T id="access-list.help-rules-last" />
105105
</p>
106106
<div className="col-11">
107107
<div className="input-group mb-2">

frontend/src/components/Form/AccessField.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ interface Props {
3131
name?: string;
3232
label?: string;
3333
}
34-
export function AccessField({ name = "accessListId", label = "access.title", id = "accessListId" }: Props) {
34+
export function AccessField({ name = "accessListId", label = "access-list", id = "accessListId" }: Props) {
3535
const { isLoading, isError, error, data } = useAccessLists(["owner", "items", "clients"]);
3636
const { setFieldValue } = useFormikContext();
3737

@@ -44,7 +44,7 @@ export function AccessField({ name = "accessListId", label = "access.title", id
4444
value: item.id || 0,
4545
label: item.name,
4646
subLabel: intl.formatMessage(
47-
{ id: "access.subtitle" },
47+
{ id: "access-list.subtitle" },
4848
{
4949
users: item?.items?.length,
5050
rules: item?.clients?.length,
@@ -57,7 +57,7 @@ export function AccessField({ name = "accessListId", label = "access.title", id
5757
// Public option
5858
options?.unshift({
5959
value: 0,
60-
label: intl.formatMessage({ id: "access.public" }),
60+
label: intl.formatMessage({ id: "access-list.public" }),
6161
subLabel: "No basic auth required",
6262
icon: <IconLockOpen2 size={14} className="text-red" />,
6363
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.locationCard {
2+
border-color: light-dark(var(--tblr-gray-200), var(--tblr-gray-700)) !important;
3+
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import { IconSettings } from "@tabler/icons-react";
2+
import CodeEditor from "@uiw/react-textarea-code-editor";
3+
import cn from "classnames";
4+
import { useFormikContext } from "formik";
5+
import { useState } from "react";
6+
import type { ProxyLocation } from "src/api/backend";
7+
import { intl, T } from "src/locale";
8+
import styles from "./LocationsFields.module.css";
9+
10+
interface Props {
11+
initialValues: ProxyLocation[];
12+
name?: string;
13+
}
14+
export function LocationsFields({ initialValues, name = "items" }: Props) {
15+
const [values, setValues] = useState<ProxyLocation[]>(initialValues || []);
16+
const { setFieldValue } = useFormikContext();
17+
const [advVisible, setAdvVisible] = useState<number[]>([]);
18+
19+
const blankItem: ProxyLocation = {
20+
path: "",
21+
advancedConfig: "",
22+
forwardScheme: "http",
23+
forwardHost: "",
24+
forwardPort: 80,
25+
};
26+
27+
const toggleAdvVisible = (idx: number) => {
28+
setAdvVisible(advVisible.includes(idx) ? advVisible.filter((i) => i !== idx) : [...advVisible, idx]);
29+
};
30+
31+
const handleAdd = () => {
32+
setValues([...values, blankItem]);
33+
};
34+
35+
const handleRemove = (idx: number) => {
36+
const newValues = values.filter((_: ProxyLocation, i: number) => i !== idx);
37+
setValues(newValues);
38+
setFormField(newValues);
39+
};
40+
41+
const handleChange = (idx: number, field: string, fieldValue: string) => {
42+
const newValues = values.map((v: ProxyLocation, i: number) => (i === idx ? { ...v, [field]: fieldValue } : v));
43+
setValues(newValues);
44+
setFormField(newValues);
45+
};
46+
47+
const setFormField = (newValues: ProxyLocation[]) => {
48+
const filtered = newValues.filter((v: ProxyLocation) => v?.path?.trim() !== "");
49+
setFieldValue(name, filtered);
50+
};
51+
52+
if (values.length === 0) {
53+
return (
54+
<div className="text-center">
55+
<button type="button" className="btn my-3" onClick={handleAdd}>
56+
<T id="action.add-location" />
57+
</button>
58+
</div>
59+
);
60+
}
61+
62+
return (
63+
<>
64+
{values.map((item: ProxyLocation, idx: number) => (
65+
<div key={idx} className={cn("card", "card-active", "mb-3", styles.locationCard)}>
66+
<div className="card-body">
67+
<div className="row">
68+
<div className="col-md-10">
69+
<div className="input-group mb-3">
70+
<span className="input-group-text">Location</span>
71+
<input
72+
type="text"
73+
className="form-control"
74+
placeholder="/path"
75+
autoComplete="off"
76+
value={item.path}
77+
onChange={(e) => handleChange(idx, "path", e.target.value)}
78+
/>
79+
</div>
80+
</div>
81+
<div className="col-md-2 text-end">
82+
<button
83+
type="button"
84+
className="btn p-0"
85+
title="Advanced"
86+
onClick={() => toggleAdvVisible(idx)}
87+
>
88+
<IconSettings size={20} />
89+
</button>
90+
</div>
91+
</div>
92+
<div className="row">
93+
<div className="col-md-3">
94+
<div className="mb-3">
95+
<label className="form-label" htmlFor="forwardScheme">
96+
<T id="host.forward-scheme" />
97+
</label>
98+
<select
99+
id="forwardScheme"
100+
className="form-control"
101+
value={item.forwardScheme}
102+
onChange={(e) => handleChange(idx, "forwardScheme", e.target.value)}
103+
>
104+
<option value="http">http</option>
105+
<option value="https">https</option>
106+
</select>
107+
</div>
108+
</div>
109+
<div className="col-md-6">
110+
<div className="mb-3">
111+
<label className="form-label" htmlFor="forwardHost">
112+
<T id="proxy-host.forward-host" />
113+
</label>
114+
<input
115+
id="forwardHost"
116+
type="text"
117+
className="form-control"
118+
required
119+
placeholder="eg: 10.0.0.1/path/"
120+
value={item.forwardHost}
121+
onChange={(e) => handleChange(idx, "forwardHost", e.target.value)}
122+
/>
123+
</div>
124+
</div>
125+
<div className="col-md-3">
126+
<div className="mb-3">
127+
<label className="form-label" htmlFor="forwardPort">
128+
<T id="host.forward-port" />
129+
</label>
130+
<input
131+
id="forwardPort"
132+
type="number"
133+
min={1}
134+
max={65535}
135+
className="form-control"
136+
required
137+
placeholder="eg: 8081"
138+
value={item.forwardPort}
139+
onChange={(e) => handleChange(idx, "forwardPort", e.target.value)}
140+
/>
141+
</div>
142+
</div>
143+
</div>
144+
{advVisible.includes(idx) && (
145+
<div className="">
146+
<CodeEditor
147+
language="nginx"
148+
placeholder={intl.formatMessage({ id: "nginx-config.placeholder" })}
149+
padding={15}
150+
data-color-mode="dark"
151+
minHeight={170}
152+
indentWidth={2}
153+
value={item.advancedConfig}
154+
onChange={(e) => handleChange(idx, "advancedConfig", e.target.value)}
155+
style={{
156+
fontFamily:
157+
"ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace",
158+
borderRadius: "0.3rem",
159+
minHeight: "170px",
160+
}}
161+
/>
162+
</div>
163+
)}
164+
<div className="mt-1">
165+
<a
166+
href="#"
167+
onClick={(e) => {
168+
e.preventDefault();
169+
handleRemove(idx);
170+
}}
171+
>
172+
<T id="action.delete" />
173+
</a>
174+
</div>
175+
</div>
176+
</div>
177+
))}
178+
<div>
179+
<button type="button" className="btn btn-sm" onClick={handleAdd}>
180+
<T id="action.add-location" />
181+
</button>
182+
</div>
183+
</>
184+
);
185+
}

frontend/src/components/Form/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export * from "./AccessField";
33
export * from "./BasicAuthFields";
44
export * from "./DNSProviderFields";
55
export * from "./DomainNamesField";
6+
export * from "./LocationsFields";
67
export * from "./NginxConfigField";
78
export * from "./SSLCertificateField";
89
export * from "./SSLOptionsFields";

0 commit comments

Comments
 (0)