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

@@ -0,0 +1,48 @@
import type { Table as ReactTable } from "@tanstack/react-table";
import cn from "classnames";
import type { ReactNode } from "react";
import { Button } from "src/components";
import { T } from "src/locale";
interface Props {
tableInstance: ReactTable<any>;
onNew?: () => void;
isFiltered?: boolean;
object: string;
objects: string;
color?: string;
customAddBtn?: ReactNode;
}
function EmptyData({ tableInstance, onNew, isFiltered, object, objects, color = "primary", customAddBtn }: Props) {
return (
<tr>
<td colSpan={tableInstance.getVisibleFlatColumns().length}>
<div className="text-center my-4">
{isFiltered ? (
<h2>
<T id="empty-search" />
</h2>
) : (
<>
<h2>
<T id="object.empty" tData={{ objects }} />
</h2>
<p className="text-muted">
<T id="empty-subtitle" />
</p>
{customAddBtn ? (
customAddBtn
) : (
<Button className={cn("my-3", `btn-${color}`)} onClick={onNew}>
<T id="object.add" tData={{ object }} />
</Button>
)}
</>
)}
</div>
</td>
</tr>
);
}
export { EmptyData };

View File

@@ -12,7 +12,7 @@ export function ErrorNotFound() {
<T id="notfound.title" />
</p>
<p className="empty-subtitle text-secondary">
<T id="notfound.text" />
<T id="notfound.content" />
</p>
<div className="empty-action">
<Button type="button" size="md" onClick={() => navigate("/")}>

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";

View File

@@ -25,33 +25,33 @@ const menuItems: MenuItem[] = [
{
to: "/",
icon: IconHome,
label: "dashboard.title",
label: "dashboard",
},
{
icon: IconDeviceDesktop,
label: "hosts.title",
label: "hosts",
items: [
{
to: "/nginx/proxy",
label: "proxy-hosts.title",
label: "proxy-hosts",
permission: "proxyHosts",
permissionType: "view",
},
{
to: "/nginx/redirection",
label: "redirection-hosts.title",
label: "redirection-hosts",
permission: "redirectionHosts",
permissionType: "view",
},
{
to: "/nginx/stream",
label: "streams.title",
label: "streams",
permission: "streams",
permissionType: "view",
},
{
to: "/nginx/404",
label: "dead-hosts.title",
label: "dead-hosts",
permission: "deadHosts",
permissionType: "view",
},
@@ -60,33 +60,33 @@ const menuItems: MenuItem[] = [
{
to: "/access",
icon: IconLock,
label: "access.title",
label: "access-lists",
permission: "accessLists",
permissionType: "view",
},
{
to: "/certificates",
icon: IconShield,
label: "certificates.title",
label: "certificates",
permission: "certificates",
permissionType: "view",
},
{
to: "/users",
icon: IconUser,
label: "users.title",
label: "users",
permission: "admin",
},
{
to: "/audit-log",
icon: IconBook,
label: "auditlog.title",
label: "auditlogs",
permission: "admin",
},
{
to: "/settings",
icon: IconSettings,
label: "settings.title",
label: "settings",
permission: "admin",
},
];

View File

@@ -0,0 +1,24 @@
import type { AccessList } from "src/api/backend";
import { T } from "src/locale";
import { showAccessListModal } from "src/modals";
interface Props {
access?: AccessList;
}
export function AccessListFormatter({ access }: Props) {
if (!access) {
return <T id="public" />;
}
return (
<button
type="button"
className="btn btn-action btn-sm px-1"
onClick={(e) => {
e.preventDefault();
showAccessListModal(access?.id || 0);
}}
>
{access.name}
</button>
);
}

View File

@@ -1,3 +1,4 @@
export * from "./AccessListformatter";
export * from "./CertificateFormatter";
export * from "./DomainsFormatter";
export * from "./EmailFormatter";

View File

@@ -1,4 +1,5 @@
export * from "./Button";
export * from "./EmptyData";
export * from "./ErrorNotFound";
export * from "./Flag";
export * from "./Form";