More react

- consolidated lang items
- proxy host paths work
This commit is contained in:
Jamie Curnow
2025-10-16 18:59:19 +10:00
parent 7af01d0fc7
commit f2b5b19a83
56 changed files with 946 additions and 928 deletions

View File

@@ -48,7 +48,7 @@ export function AccessClientFields({ initialValues, name = "clients" }: Props) {
return (
<>
<p className="text-muted">
<T id="access.help.rules-order" />
<T id="access-list.help.rules-order" />
</p>
{values.map((client: AccessListClient, idx: number) => (
<div className="row mb-1" key={idx}>
@@ -101,7 +101,7 @@ export function AccessClientFields({ initialValues, name = "clients" }: Props) {
</div>
<div className="row mb-3">
<p className="text-muted">
<T id="access.help-rules-last" />
<T id="access-list.help-rules-last" />
</p>
<div className="col-11">
<div className="input-group mb-2">

View File

@@ -31,7 +31,7 @@ interface Props {
name?: string;
label?: string;
}
export function AccessField({ name = "accessListId", label = "access.title", id = "accessListId" }: Props) {
export function AccessField({ name = "accessListId", label = "access-list", id = "accessListId" }: Props) {
const { isLoading, isError, error, data } = useAccessLists(["owner", "items", "clients"]);
const { setFieldValue } = useFormikContext();
@@ -44,7 +44,7 @@ export function AccessField({ name = "accessListId", label = "access.title", id
value: item.id || 0,
label: item.name,
subLabel: intl.formatMessage(
{ id: "access.subtitle" },
{ id: "access-list.subtitle" },
{
users: item?.items?.length,
rules: item?.clients?.length,
@@ -57,7 +57,7 @@ export function AccessField({ name = "accessListId", label = "access.title", id
// Public option
options?.unshift({
value: 0,
label: intl.formatMessage({ id: "access.public" }),
label: intl.formatMessage({ id: "access-list.public" }),
subLabel: "No basic auth required",
icon: <IconLockOpen2 size={14} className="text-red" />,
});

View File

@@ -0,0 +1,3 @@
.locationCard {
border-color: light-dark(var(--tblr-gray-200), var(--tblr-gray-700)) !important;
}

View File

@@ -0,0 +1,185 @@
import { IconSettings } from "@tabler/icons-react";
import CodeEditor from "@uiw/react-textarea-code-editor";
import cn from "classnames";
import { useFormikContext } from "formik";
import { useState } from "react";
import type { ProxyLocation } from "src/api/backend";
import { intl, T } from "src/locale";
import styles from "./LocationsFields.module.css";
interface Props {
initialValues: ProxyLocation[];
name?: string;
}
export function LocationsFields({ initialValues, name = "items" }: Props) {
const [values, setValues] = useState<ProxyLocation[]>(initialValues || []);
const { setFieldValue } = useFormikContext();
const [advVisible, setAdvVisible] = useState<number[]>([]);
const blankItem: ProxyLocation = {
path: "",
advancedConfig: "",
forwardScheme: "http",
forwardHost: "",
forwardPort: 80,
};
const toggleAdvVisible = (idx: number) => {
setAdvVisible(advVisible.includes(idx) ? advVisible.filter((i) => i !== idx) : [...advVisible, idx]);
};
const handleAdd = () => {
setValues([...values, blankItem]);
};
const handleRemove = (idx: number) => {
const newValues = values.filter((_: ProxyLocation, i: number) => i !== idx);
setValues(newValues);
setFormField(newValues);
};
const handleChange = (idx: number, field: string, fieldValue: string) => {
const newValues = values.map((v: ProxyLocation, i: number) => (i === idx ? { ...v, [field]: fieldValue } : v));
setValues(newValues);
setFormField(newValues);
};
const setFormField = (newValues: ProxyLocation[]) => {
const filtered = newValues.filter((v: ProxyLocation) => v?.path?.trim() !== "");
setFieldValue(name, filtered);
};
if (values.length === 0) {
return (
<div className="text-center">
<button type="button" className="btn my-3" onClick={handleAdd}>
<T id="action.add-location" />
</button>
</div>
);
}
return (
<>
{values.map((item: ProxyLocation, idx: number) => (
<div key={idx} className={cn("card", "card-active", "mb-3", styles.locationCard)}>
<div className="card-body">
<div className="row">
<div className="col-md-10">
<div className="input-group mb-3">
<span className="input-group-text">Location</span>
<input
type="text"
className="form-control"
placeholder="/path"
autoComplete="off"
value={item.path}
onChange={(e) => handleChange(idx, "path", e.target.value)}
/>
</div>
</div>
<div className="col-md-2 text-end">
<button
type="button"
className="btn p-0"
title="Advanced"
onClick={() => toggleAdvVisible(idx)}
>
<IconSettings size={20} />
</button>
</div>
</div>
<div className="row">
<div className="col-md-3">
<div className="mb-3">
<label className="form-label" htmlFor="forwardScheme">
<T id="host.forward-scheme" />
</label>
<select
id="forwardScheme"
className="form-control"
value={item.forwardScheme}
onChange={(e) => handleChange(idx, "forwardScheme", e.target.value)}
>
<option value="http">http</option>
<option value="https">https</option>
</select>
</div>
</div>
<div className="col-md-6">
<div className="mb-3">
<label className="form-label" htmlFor="forwardHost">
<T id="proxy-host.forward-host" />
</label>
<input
id="forwardHost"
type="text"
className="form-control"
required
placeholder="eg: 10.0.0.1/path/"
value={item.forwardHost}
onChange={(e) => handleChange(idx, "forwardHost", e.target.value)}
/>
</div>
</div>
<div className="col-md-3">
<div className="mb-3">
<label className="form-label" htmlFor="forwardPort">
<T id="host.forward-port" />
</label>
<input
id="forwardPort"
type="number"
min={1}
max={65535}
className="form-control"
required
placeholder="eg: 8081"
value={item.forwardPort}
onChange={(e) => handleChange(idx, "forwardPort", e.target.value)}
/>
</div>
</div>
</div>
{advVisible.includes(idx) && (
<div className="">
<CodeEditor
language="nginx"
placeholder={intl.formatMessage({ id: "nginx-config.placeholder" })}
padding={15}
data-color-mode="dark"
minHeight={170}
indentWidth={2}
value={item.advancedConfig}
onChange={(e) => handleChange(idx, "advancedConfig", e.target.value)}
style={{
fontFamily:
"ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace",
borderRadius: "0.3rem",
minHeight: "170px",
}}
/>
</div>
)}
<div className="mt-1">
<a
href="#"
onClick={(e) => {
e.preventDefault();
handleRemove(idx);
}}
>
<T id="action.delete" />
</a>
</div>
</div>
</div>
))}
<div>
<button type="button" className="btn btn-sm" onClick={handleAdd}>
<T id="action.add-location" />
</button>
</div>
</>
);
}

View File

@@ -3,6 +3,7 @@ export * from "./AccessField";
export * from "./BasicAuthFields";
export * from "./DNSProviderFields";
export * from "./DomainNamesField";
export * from "./LocationsFields";
export * from "./NginxConfigField";
export * from "./SSLCertificateField";
export * from "./SSLOptionsFields";