You've already forked nginx-proxy-manager
							
							
				mirror of
				https://github.com/NginxProxyManager/nginx-proxy-manager.git
				synced 2025-11-04 04:11:42 +03:00 
			
		
		
		
	More react
- consolidated lang items - proxy host paths work
This commit is contained in:
		@@ -18,47 +18,47 @@
 | 
			
		||||
	"dependencies": {
 | 
			
		||||
		"@tabler/core": "^1.4.0",
 | 
			
		||||
		"@tabler/icons-react": "^3.35.0",
 | 
			
		||||
		"@tanstack/react-query": "^5.89.0",
 | 
			
		||||
		"@tanstack/react-query": "^5.90.3",
 | 
			
		||||
		"@tanstack/react-table": "^8.21.3",
 | 
			
		||||
		"@uiw/react-textarea-code-editor": "^3.1.1",
 | 
			
		||||
		"classnames": "^2.5.1",
 | 
			
		||||
		"country-flag-icons": "^1.5.20",
 | 
			
		||||
		"country-flag-icons": "^1.5.21",
 | 
			
		||||
		"date-fns": "^4.1.0",
 | 
			
		||||
		"ez-modal-react": "^1.0.5",
 | 
			
		||||
		"formik": "^2.4.6",
 | 
			
		||||
		"generate-password-browser": "^1.1.0",
 | 
			
		||||
		"humps": "^2.0.1",
 | 
			
		||||
		"query-string": "^9.3.1",
 | 
			
		||||
		"react": "^19.1.1",
 | 
			
		||||
		"react": "^19.2.0",
 | 
			
		||||
		"react-bootstrap": "^2.10.10",
 | 
			
		||||
		"react-dom": "^19.1.1",
 | 
			
		||||
		"react-intl": "^7.1.11",
 | 
			
		||||
		"react-router-dom": "^7.9.1",
 | 
			
		||||
		"react-dom": "^19.2.0",
 | 
			
		||||
		"react-intl": "^7.1.14",
 | 
			
		||||
		"react-router-dom": "^7.9.4",
 | 
			
		||||
		"react-select": "^5.10.2",
 | 
			
		||||
		"react-toastify": "^11.0.5",
 | 
			
		||||
		"rooks": "^9.3.0"
 | 
			
		||||
	},
 | 
			
		||||
	"devDependencies": {
 | 
			
		||||
		"@biomejs/biome": "^2.2.4",
 | 
			
		||||
		"@formatjs/cli": "^6.7.2",
 | 
			
		||||
		"@tanstack/react-query-devtools": "^5.89.0",
 | 
			
		||||
		"@biomejs/biome": "^2.2.6",
 | 
			
		||||
		"@formatjs/cli": "^6.7.4",
 | 
			
		||||
		"@tanstack/react-query-devtools": "^5.90.2",
 | 
			
		||||
		"@testing-library/dom": "^10.4.1",
 | 
			
		||||
		"@testing-library/jest-dom": "^6.8.0",
 | 
			
		||||
		"@testing-library/jest-dom": "^6.9.1",
 | 
			
		||||
		"@testing-library/react": "^16.3.0",
 | 
			
		||||
		"@types/country-flag-icons": "^1.2.2",
 | 
			
		||||
		"@types/humps": "^2.0.6",
 | 
			
		||||
		"@types/react": "^19.1.13",
 | 
			
		||||
		"@types/react-dom": "^19.1.9",
 | 
			
		||||
		"@types/react": "^19.2.2",
 | 
			
		||||
		"@types/react-dom": "^19.2.2",
 | 
			
		||||
		"@types/react-table": "^7.7.20",
 | 
			
		||||
		"@vitejs/plugin-react": "^5.0.3",
 | 
			
		||||
		"happy-dom": "^18.0.1",
 | 
			
		||||
		"@vitejs/plugin-react": "^5.0.4",
 | 
			
		||||
		"happy-dom": "^20.0.2",
 | 
			
		||||
		"postcss": "^8.5.6",
 | 
			
		||||
		"postcss-simple-vars": "^7.0.1",
 | 
			
		||||
		"sass": "^1.93.0",
 | 
			
		||||
		"sass": "^1.93.2",
 | 
			
		||||
		"tmp": "^0.2.5",
 | 
			
		||||
		"typescript": "5.9.2",
 | 
			
		||||
		"vite": "^7.1.6",
 | 
			
		||||
		"vite-plugin-checker": "^0.10.3",
 | 
			
		||||
		"typescript": "5.9.3",
 | 
			
		||||
		"vite": "^7.1.10",
 | 
			
		||||
		"vite-plugin-checker": "^0.11.0",
 | 
			
		||||
		"vite-tsconfig-paths": "^5.1.4",
 | 
			
		||||
		"vitest": "^3.2.4"
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -97,6 +97,14 @@ export interface Certificate {
 | 
			
		||||
	redirectionHosts?: RedirectionHost[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ProxyLocation {
 | 
			
		||||
	path: string;
 | 
			
		||||
	advancedConfig: string;
 | 
			
		||||
	forwardScheme: string;
 | 
			
		||||
	forwardHost: string;
 | 
			
		||||
	forwardPort: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ProxyHost {
 | 
			
		||||
	id: number;
 | 
			
		||||
	createdOn: string;
 | 
			
		||||
@@ -116,7 +124,7 @@ export interface ProxyHost {
 | 
			
		||||
	allowWebsocketUpgrade: boolean;
 | 
			
		||||
	http2Support: boolean;
 | 
			
		||||
	enabled: boolean;
 | 
			
		||||
	locations?: string[]; // todo: string or object?
 | 
			
		||||
	locations?: ProxyLocation[];
 | 
			
		||||
	hstsEnabled: boolean;
 | 
			
		||||
	hstsSubdomains: boolean;
 | 
			
		||||
	// Expansions:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,6 @@
 | 
			
		||||
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";
 | 
			
		||||
 | 
			
		||||
@@ -6,8 +8,12 @@ interface Props {
 | 
			
		||||
	tableInstance: ReactTable<any>;
 | 
			
		||||
	onNew?: () => void;
 | 
			
		||||
	isFiltered?: boolean;
 | 
			
		||||
	object: string;
 | 
			
		||||
	objects: string;
 | 
			
		||||
	color?: string;
 | 
			
		||||
	customAddBtn?: ReactNode;
 | 
			
		||||
}
 | 
			
		||||
export default function Empty({ tableInstance, onNew, isFiltered }: Props) {
 | 
			
		||||
function EmptyData({ tableInstance, onNew, isFiltered, object, objects, color = "primary", customAddBtn }: Props) {
 | 
			
		||||
	return (
 | 
			
		||||
		<tr>
 | 
			
		||||
			<td colSpan={tableInstance.getVisibleFlatColumns().length}>
 | 
			
		||||
@@ -19,14 +25,18 @@ export default function Empty({ tableInstance, onNew, isFiltered }: Props) {
 | 
			
		||||
					) : (
 | 
			
		||||
						<>
 | 
			
		||||
							<h2>
 | 
			
		||||
								<T id="access.empty" />
 | 
			
		||||
								<T id="object.empty" tData={{ objects }} />
 | 
			
		||||
							</h2>
 | 
			
		||||
							<p className="text-muted">
 | 
			
		||||
								<T id="empty-subtitle" />
 | 
			
		||||
							</p>
 | 
			
		||||
							<Button className="btn-cyan my-3" onClick={onNew}>
 | 
			
		||||
								<T id="access.add" />
 | 
			
		||||
							{customAddBtn ? (
 | 
			
		||||
								customAddBtn
 | 
			
		||||
							) : (
 | 
			
		||||
								<Button className={cn("my-3", `btn-${color}`)} onClick={onNew}>
 | 
			
		||||
									<T id="object.add" tData={{ object }} />
 | 
			
		||||
								</Button>
 | 
			
		||||
							)}
 | 
			
		||||
						</>
 | 
			
		||||
					)}
 | 
			
		||||
				</div>
 | 
			
		||||
@@ -34,3 +44,5 @@ export default function Empty({ tableInstance, onNew, isFiltered }: Props) {
 | 
			
		||||
		</tr>
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { EmptyData };
 | 
			
		||||
@@ -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("/")}>
 | 
			
		||||
 
 | 
			
		||||
@@ -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">
 | 
			
		||||
 
 | 
			
		||||
@@ -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" />,
 | 
			
		||||
	});
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								frontend/src/components/Form/LocationsFields.module.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								frontend/src/components/Form/LocationsFields.module.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
.locationCard {
 | 
			
		||||
	border-color: light-dark(var(--tblr-gray-200), var(--tblr-gray-700)) !important;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										185
									
								
								frontend/src/components/Form/LocationsFields.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										185
									
								
								frontend/src/components/Form/LocationsFields.tsx
									
									
									
									
									
										Normal 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>
 | 
			
		||||
		</>
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
@@ -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";
 | 
			
		||||
 
 | 
			
		||||
@@ -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",
 | 
			
		||||
	},
 | 
			
		||||
];
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
export * from "./AccessListformatter";
 | 
			
		||||
export * from "./CertificateFormatter";
 | 
			
		||||
export * from "./DomainsFormatter";
 | 
			
		||||
export * from "./EmailFormatter";
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
export * from "./Button";
 | 
			
		||||
export * from "./EmptyData";
 | 
			
		||||
export * from "./ErrorNotFound";
 | 
			
		||||
export * from "./Flag";
 | 
			
		||||
export * from "./Form";
 | 
			
		||||
 
 | 
			
		||||
@@ -52,6 +52,7 @@ const useSetAccessList = () => {
 | 
			
		||||
			queryClient.invalidateQueries({ queryKey: ["access-list", id] });
 | 
			
		||||
			queryClient.invalidateQueries({ queryKey: ["access-lists"] });
 | 
			
		||||
			queryClient.invalidateQueries({ queryKey: ["audit-logs"] });
 | 
			
		||||
			queryClient.invalidateQueries({ queryKey: ["proxy-hosts"] });
 | 
			
		||||
		},
 | 
			
		||||
	});
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -63,8 +63,33 @@ const changeLocale = (locale: string): void => {
 | 
			
		||||
 | 
			
		||||
// This is a translation component that wraps the translation in a span with a data
 | 
			
		||||
// attribute so devs can inspect the element to see the translation ID
 | 
			
		||||
const T = ({ id, data }: { id: string; data?: any }) => {
 | 
			
		||||
	return <span data-translation-id={id}>{intl.formatMessage({ id }, data)}</span>;
 | 
			
		||||
const T = ({
 | 
			
		||||
	id,
 | 
			
		||||
	data,
 | 
			
		||||
	tData,
 | 
			
		||||
}: {
 | 
			
		||||
	id: string;
 | 
			
		||||
	data?: Record<string, string | number | undefined>;
 | 
			
		||||
	tData?: Record<string, string>;
 | 
			
		||||
}) => {
 | 
			
		||||
	const translatedData: Record<string, string> = {};
 | 
			
		||||
	if (tData) {
 | 
			
		||||
		// iterate over tData and translate each value
 | 
			
		||||
		Object.entries(tData).forEach(([key, value]) => {
 | 
			
		||||
			translatedData[key] = intl.formatMessage({ id: value });
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
	return (
 | 
			
		||||
		<span data-translation-id={id}>
 | 
			
		||||
			{intl.formatMessage(
 | 
			
		||||
				{ id },
 | 
			
		||||
				{
 | 
			
		||||
					...data,
 | 
			
		||||
					...translatedData,
 | 
			
		||||
				},
 | 
			
		||||
			)}
 | 
			
		||||
		</span>
 | 
			
		||||
	);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export { localeOptions, getFlagCodeForLocale, getLocale, createIntl, changeLocale, intl, T };
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,3 @@
 | 
			
		||||
{
 | 
			
		||||
  "dashboard.title": "Armaturenbrett"
 | 
			
		||||
  "dashboard": "Armaturenbrett"
 | 
			
		||||
}
 | 
			
		||||
@@ -1,33 +1,28 @@
 | 
			
		||||
{
 | 
			
		||||
  "access.access-count": "{count} {count, plural, one {Rule} other {Rules}}",
 | 
			
		||||
  "access.actions-title": "Access List #{id}",
 | 
			
		||||
  "access.add": "Add Access List",
 | 
			
		||||
  "access.auth-count": "{count} {count, plural, one {User} other {Users}}",
 | 
			
		||||
  "access.edit": "Edit Access",
 | 
			
		||||
  "access.empty": "There are no Access Lists",
 | 
			
		||||
  "access.help-rules-last": "When at least 1 rule exists, this deny all rule will be added last",
 | 
			
		||||
  "access.help.rules-order": "Note that the allow and deny directives will be applied in the order they are defined.",
 | 
			
		||||
  "access.new": "New Access",
 | 
			
		||||
  "access.pass-auth": "Pass Auth to Upstream",
 | 
			
		||||
  "access.public": "Publicly Accessible",
 | 
			
		||||
  "access.satisfy-any": "Satisfy Any",
 | 
			
		||||
  "access.subtitle": "{users} {users, plural, one {User} other {Users}}, {rules} {rules, plural, one {Rule} other {Rules}} - Created: {date}",
 | 
			
		||||
  "access.title": "Access",
 | 
			
		||||
  "access-list": "Access List",
 | 
			
		||||
  "access-list.access-count": "{count} {count, plural, one {Rule} other {Rules}}",
 | 
			
		||||
  "access-list.auth-count": "{count} {count, plural, one {User} other {Users}}",
 | 
			
		||||
  "access-list.help-rules-last": "When at least 1 rule exists, this deny all rule will be added last",
 | 
			
		||||
  "access-list.help.rules-order": "Note that the allow and deny directives will be applied in the order they are defined.",
 | 
			
		||||
  "access-list.pass-auth": "Pass Auth to Upstream",
 | 
			
		||||
  "access-list.public": "Publicly Accessible",
 | 
			
		||||
  "access-list.satisfy-any": "Satisfy Any",
 | 
			
		||||
  "access-list.subtitle": "{users} {users, plural, one {User} other {Users}}, {rules} {rules, plural, one {Rule} other {Rules}} - Created: {date}",
 | 
			
		||||
  "access-lists": "Access Lists",
 | 
			
		||||
  "action.add": "Add",
 | 
			
		||||
  "action.add-location": "Add Location",
 | 
			
		||||
  "action.close": "Close",
 | 
			
		||||
  "action.delete": "Delete",
 | 
			
		||||
  "action.disable": "Disable",
 | 
			
		||||
  "action.edit": "Edit",
 | 
			
		||||
  "action.enable": "Enable",
 | 
			
		||||
  "action.permissions": "Permissions",
 | 
			
		||||
  "action.view-details": "View Details",
 | 
			
		||||
  "auditlog.title": "Audit Log",
 | 
			
		||||
  "auditlogs": "Audit Logs",
 | 
			
		||||
  "cancel": "Cancel",
 | 
			
		||||
  "certificates.actions-title": "Certificate #{id}",
 | 
			
		||||
  "certificates.add": "Add Certificate",
 | 
			
		||||
  "certificate": "Certificate",
 | 
			
		||||
  "certificates": "Certificates",
 | 
			
		||||
  "certificates.custom": "Custom Certificate",
 | 
			
		||||
  "certificates.empty": "There are no Certificates",
 | 
			
		||||
  "certificates.title": "SSL Certificates",
 | 
			
		||||
  "close": "Close",
 | 
			
		||||
  "column.access": "Access",
 | 
			
		||||
  "column.authorization": "Authorization",
 | 
			
		||||
  "column.authorizations": "Authorizations",
 | 
			
		||||
@@ -52,16 +47,10 @@
 | 
			
		||||
  "column.ssl": "SSL",
 | 
			
		||||
  "column.status": "Status",
 | 
			
		||||
  "created-on": "Created: {date}",
 | 
			
		||||
  "dashboard.title": "Dashboard",
 | 
			
		||||
  "dead-host.delete.content": "Are you sure you want to delete this 404 host?",
 | 
			
		||||
  "dead-host.delete.title": "Delete 404 Host",
 | 
			
		||||
  "dead-host.edit": "Edit 404 Host",
 | 
			
		||||
  "dead-host.new": "New 404 Host",
 | 
			
		||||
  "dead-hosts.actions-title": "404 Host #{id}",
 | 
			
		||||
  "dead-hosts.add": "Add 404 Host",
 | 
			
		||||
  "dashboard": "Dashboard",
 | 
			
		||||
  "dead-host": "404 Host",
 | 
			
		||||
  "dead-hosts": "404 Hosts",
 | 
			
		||||
  "dead-hosts.count": "{count} {count, plural, one {404 Host} other {404 Hosts}}",
 | 
			
		||||
  "dead-hosts.empty": "There are no 404 Hosts",
 | 
			
		||||
  "dead-hosts.title": "404 Hosts",
 | 
			
		||||
  "disabled": "Disabled",
 | 
			
		||||
  "domain-names": "Domain Names",
 | 
			
		||||
  "domain-names.max": "{count} domain names maximum",
 | 
			
		||||
@@ -102,7 +91,6 @@
 | 
			
		||||
  "event.updated-redirection-host": "Updated Redirection Host",
 | 
			
		||||
  "event.updated-user": "Updated User",
 | 
			
		||||
  "footer.github-fork": "Fork me on Github",
 | 
			
		||||
  "generic.flags.title": "Options",
 | 
			
		||||
  "host.flags.block-exploits": "Block Common Exploits",
 | 
			
		||||
  "host.flags.cache-assets": "Cache Assets",
 | 
			
		||||
  "host.flags.preserve-path": "Preserve Path",
 | 
			
		||||
@@ -110,7 +98,7 @@
 | 
			
		||||
  "host.flags.websockets-upgrade": "Websockets Support",
 | 
			
		||||
  "host.forward-port": "Forward Port",
 | 
			
		||||
  "host.forward-scheme": "Scheme",
 | 
			
		||||
  "hosts.title": "Hosts",
 | 
			
		||||
  "hosts": "Hosts",
 | 
			
		||||
  "http-only": "HTTP Only",
 | 
			
		||||
  "lets-encrypt": "Let's Encrypt",
 | 
			
		||||
  "loading": "Loading…",
 | 
			
		||||
@@ -119,27 +107,23 @@
 | 
			
		||||
  "nginx-config.placeholder": "# Enter your custom Nginx configuration here at your own risk!",
 | 
			
		||||
  "no-permission-error": "You do not have access to view this.",
 | 
			
		||||
  "notfound.action": "Take me home",
 | 
			
		||||
  "notfound.text": "We are sorry but the page you are looking for was not found",
 | 
			
		||||
  "notfound.content": "We are sorry but the page you are looking for was not found",
 | 
			
		||||
  "notfound.title": "Oops… You just found an error page",
 | 
			
		||||
  "notification.access-deleted": "Access has been deleted",
 | 
			
		||||
  "notification.access-saved": "Access has been saved",
 | 
			
		||||
  "notification.dead-host-saved": "404 Host has been saved",
 | 
			
		||||
  "notification.error": "Error",
 | 
			
		||||
  "notification.host-deleted": "Host has been deleted",
 | 
			
		||||
  "notification.host-disabled": "Host has been disabled",
 | 
			
		||||
  "notification.host-enabled": "Host has been enabled",
 | 
			
		||||
  "notification.proxy-host-saved": "Proxy Host has been saved",
 | 
			
		||||
  "notification.redirection-host-saved": "Redirection Host has been saved",
 | 
			
		||||
  "notification.stream-deleted": "Stream has been deleted",
 | 
			
		||||
  "notification.stream-disabled": "Stream has been disabled",
 | 
			
		||||
  "notification.stream-enabled": "Stream has been enabled",
 | 
			
		||||
  "notification.object-deleted": "{object} has been deleted",
 | 
			
		||||
  "notification.object-disabled": "{object} has been disabled",
 | 
			
		||||
  "notification.object-enabled": "{object} has been enabled",
 | 
			
		||||
  "notification.object-saved": "{object} has been saved",
 | 
			
		||||
  "notification.success": "Success",
 | 
			
		||||
  "notification.user-deleted": "User has been deleted",
 | 
			
		||||
  "notification.user-disabled": "User has been disabled",
 | 
			
		||||
  "notification.user-enabled": "User has been enabled",
 | 
			
		||||
  "notification.user-saved": "User has been saved",
 | 
			
		||||
  "object.actions-title": "{object} #{id}",
 | 
			
		||||
  "object.add": "Add {object}",
 | 
			
		||||
  "object.delete": "Delete {object}",
 | 
			
		||||
  "object.delete.content": "Are you sure you want to delete this {object}?",
 | 
			
		||||
  "object.edit": "Edit {object}",
 | 
			
		||||
  "object.empty": "There are no {objects}",
 | 
			
		||||
  "offline": "Offline",
 | 
			
		||||
  "online": "Online",
 | 
			
		||||
  "options": "Options",
 | 
			
		||||
  "password": "Password",
 | 
			
		||||
  "password.generate": "Generate random password",
 | 
			
		||||
  "password.hide": "Hide Password",
 | 
			
		||||
@@ -150,55 +134,37 @@
 | 
			
		||||
  "permissions.visibility.all": "All Items",
 | 
			
		||||
  "permissions.visibility.title": "Item Visibility",
 | 
			
		||||
  "permissions.visibility.user": "Created Items Only",
 | 
			
		||||
  "proxy-host.edit": "Edit Proxy Host",
 | 
			
		||||
  "proxy-host": "Proxy Host",
 | 
			
		||||
  "proxy-host.forward-host": "Forward Hostname / IP",
 | 
			
		||||
  "proxy-host.new": "New Proxy Host",
 | 
			
		||||
  "proxy-hosts.actions-title": "Proxy Host #{id}",
 | 
			
		||||
  "proxy-hosts.add": "Add Proxy Host",
 | 
			
		||||
  "proxy-hosts": "Proxy Hosts",
 | 
			
		||||
  "proxy-hosts.count": "{count} {count, plural, one {Proxy Host} other {Proxy Hosts}}",
 | 
			
		||||
  "proxy-hosts.empty": "There are no Proxy Hosts",
 | 
			
		||||
  "proxy-hosts.title": "Proxy Hosts",
 | 
			
		||||
  "redirection-host.delete.content": "Are you sure you want to delete this Redirection host?",
 | 
			
		||||
  "redirection-host.delete.title": "Delete Redirection Host",
 | 
			
		||||
  "public": "Public",
 | 
			
		||||
  "redirection-host": "Redirection Host",
 | 
			
		||||
  "redirection-host.forward-domain": "Forward Domain",
 | 
			
		||||
  "redirection-host.new": "New Redirection Host",
 | 
			
		||||
  "redirection-hosts.actions-title": "Redirection Host #{id}",
 | 
			
		||||
  "redirection-hosts.add": "Add Redirection Host",
 | 
			
		||||
  "redirection-hosts": "Redirection Hosts",
 | 
			
		||||
  "redirection-hosts.count": "{count} {count, plural, one {Redirection Host} other {Redirection Hosts}}",
 | 
			
		||||
  "redirection-hosts.empty": "There are no Redirection Hosts",
 | 
			
		||||
  "redirection-hosts.title": "Redirection Hosts",
 | 
			
		||||
  "role.admin": "Administrator",
 | 
			
		||||
  "role.standard-user": "Standard User",
 | 
			
		||||
  "save": "Save",
 | 
			
		||||
  "settings.title": "Settings",
 | 
			
		||||
  "settings": "Settings",
 | 
			
		||||
  "setup.preamble": "Get started by creating your admin account.",
 | 
			
		||||
  "setup.title": "Welcome!",
 | 
			
		||||
  "sign-in": "Sign in",
 | 
			
		||||
  "ssl-certificate": "SSL Certificate",
 | 
			
		||||
  "stream.delete.content": "Are you sure you want to delete this Stream?",
 | 
			
		||||
  "stream.delete.title": "Delete Stream",
 | 
			
		||||
  "stream.edit": "Edit Stream",
 | 
			
		||||
  "stream": "Stream",
 | 
			
		||||
  "stream.forward-host": "Forward Host",
 | 
			
		||||
  "stream.incoming-port": "Incoming Port",
 | 
			
		||||
  "stream.new": "New Stream",
 | 
			
		||||
  "streams.actions-title": "Stream #{id}",
 | 
			
		||||
  "streams.add": "Add Stream",
 | 
			
		||||
  "streams": "Streams",
 | 
			
		||||
  "streams.count": "{count} {count, plural, one {Stream} other {Streams}}",
 | 
			
		||||
  "streams.empty": "There are no Streams",
 | 
			
		||||
  "streams.tcp": "TCP",
 | 
			
		||||
  "streams.title": "Streams",
 | 
			
		||||
  "streams.udp": "UDP",
 | 
			
		||||
  "user": "User",
 | 
			
		||||
  "user.change-password": "Change Password",
 | 
			
		||||
  "user.confirm-password": "Confirm Password",
 | 
			
		||||
  "user.current-password": "Current Password",
 | 
			
		||||
  "user.delete.content": "Are you sure you want to delete this user?",
 | 
			
		||||
  "user.delete.title": "Delete User",
 | 
			
		||||
  "user.edit": "Edit User",
 | 
			
		||||
  "user.edit-profile": "Edit Profile",
 | 
			
		||||
  "user.flags.title": "Properties",
 | 
			
		||||
  "user.full-name": "Full Name",
 | 
			
		||||
  "user.logout": "Logout",
 | 
			
		||||
  "user.new": "New User",
 | 
			
		||||
  "user.new-password": "New Password",
 | 
			
		||||
  "user.nickname": "Nickname",
 | 
			
		||||
  "user.set-password": "Set Password",
 | 
			
		||||
@@ -206,8 +172,5 @@
 | 
			
		||||
  "user.switch-dark": "Switch to Dark mode",
 | 
			
		||||
  "user.switch-light": "Switch to Light mode",
 | 
			
		||||
  "username": "Username",
 | 
			
		||||
  "users.actions-title": "User #{id}",
 | 
			
		||||
  "users.add": "Add User",
 | 
			
		||||
  "users.empty": "There are no Users",
 | 
			
		||||
  "users.title": "Users"
 | 
			
		||||
  "users": "Users"
 | 
			
		||||
}
 | 
			
		||||
@@ -1,3 +1,3 @@
 | 
			
		||||
{
 | 
			
		||||
  "dashboard.title": "داشبورد"
 | 
			
		||||
  "dashboard": "داشبورد"
 | 
			
		||||
}
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
	"dashboard.title": {
 | 
			
		||||
	"dashboard": {
 | 
			
		||||
		"defaultMessage": "Armaturenbrett"
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,49 +1,43 @@
 | 
			
		||||
{
 | 
			
		||||
	"access.access-count": {
 | 
			
		||||
	"access-list": {
 | 
			
		||||
		"defaultMessage": "Access List"
 | 
			
		||||
	},
 | 
			
		||||
	"access-list.access-count": {
 | 
			
		||||
		"defaultMessage": "{count} {count, plural, one {Rule} other {Rules}}"
 | 
			
		||||
	},
 | 
			
		||||
	"access.actions-title": {
 | 
			
		||||
		"defaultMessage": "Access List #{id}"
 | 
			
		||||
	},
 | 
			
		||||
	"access.add": {
 | 
			
		||||
		"defaultMessage": "Add Access List"
 | 
			
		||||
	},
 | 
			
		||||
	"access.auth-count": {
 | 
			
		||||
	"access-list.auth-count": {
 | 
			
		||||
		"defaultMessage": "{count} {count, plural, one {User} other {Users}}"
 | 
			
		||||
	},
 | 
			
		||||
	"access.edit": {
 | 
			
		||||
		"defaultMessage": "Edit Access"
 | 
			
		||||
	},
 | 
			
		||||
	"access.empty": {
 | 
			
		||||
		"defaultMessage": "There are no Access Lists"
 | 
			
		||||
	},
 | 
			
		||||
	"access.help-rules-last": {
 | 
			
		||||
	"access-list.help-rules-last": {
 | 
			
		||||
		"defaultMessage": "When at least 1 rule exists, this deny all rule will be added last"
 | 
			
		||||
	},
 | 
			
		||||
	"access.help.rules-order": {
 | 
			
		||||
	"access-list.help.rules-order": {
 | 
			
		||||
		"defaultMessage": "Note that the allow and deny directives will be applied in the order they are defined."
 | 
			
		||||
	},
 | 
			
		||||
	"access.new": {
 | 
			
		||||
		"defaultMessage": "New Access"
 | 
			
		||||
	},
 | 
			
		||||
	"access.pass-auth": {
 | 
			
		||||
	"access-list.pass-auth": {
 | 
			
		||||
		"defaultMessage": "Pass Auth to Upstream"
 | 
			
		||||
	},
 | 
			
		||||
	"access.public": {
 | 
			
		||||
	"access-list.public": {
 | 
			
		||||
		"defaultMessage": "Publicly Accessible"
 | 
			
		||||
	},
 | 
			
		||||
	"access.satisfy-any": {
 | 
			
		||||
	"access-list.satisfy-any": {
 | 
			
		||||
		"defaultMessage": "Satisfy Any"
 | 
			
		||||
	},
 | 
			
		||||
	"access.subtitle": {
 | 
			
		||||
	"access-list.subtitle": {
 | 
			
		||||
		"defaultMessage": "{users} {users, plural, one {User} other {Users}}, {rules} {rules, plural, one {Rule} other {Rules}} - Created: {date}"
 | 
			
		||||
	},
 | 
			
		||||
	"access.title": {
 | 
			
		||||
		"defaultMessage": "Access"
 | 
			
		||||
	"access-lists": {
 | 
			
		||||
		"defaultMessage": "Access Lists"
 | 
			
		||||
	},
 | 
			
		||||
	"action.add": {
 | 
			
		||||
		"defaultMessage": "Add"
 | 
			
		||||
	},
 | 
			
		||||
	"action.add-location": {
 | 
			
		||||
		"defaultMessage": "Add Location"
 | 
			
		||||
	},
 | 
			
		||||
	"action.close": {
 | 
			
		||||
		"defaultMessage": "Close"
 | 
			
		||||
	},
 | 
			
		||||
	"action.delete": {
 | 
			
		||||
		"defaultMessage": "Delete"
 | 
			
		||||
	},
 | 
			
		||||
@@ -62,30 +56,21 @@
 | 
			
		||||
	"action.view-details": {
 | 
			
		||||
		"defaultMessage": "View Details"
 | 
			
		||||
	},
 | 
			
		||||
	"auditlog.title": {
 | 
			
		||||
		"defaultMessage": "Audit Log"
 | 
			
		||||
	"auditlogs": {
 | 
			
		||||
		"defaultMessage": "Audit Logs"
 | 
			
		||||
	},
 | 
			
		||||
	"cancel": {
 | 
			
		||||
		"defaultMessage": "Cancel"
 | 
			
		||||
	},
 | 
			
		||||
	"certificates.actions-title": {
 | 
			
		||||
		"defaultMessage": "Certificate #{id}"
 | 
			
		||||
	"certificate": {
 | 
			
		||||
		"defaultMessage": "Certificate"
 | 
			
		||||
	},
 | 
			
		||||
	"certificates.add": {
 | 
			
		||||
		"defaultMessage": "Add Certificate"
 | 
			
		||||
	"certificates": {
 | 
			
		||||
		"defaultMessage": "Certificates"
 | 
			
		||||
	},
 | 
			
		||||
	"certificates.custom": {
 | 
			
		||||
		"defaultMessage": "Custom Certificate"
 | 
			
		||||
	},
 | 
			
		||||
	"certificates.empty": {
 | 
			
		||||
		"defaultMessage": "There are no Certificates"
 | 
			
		||||
	},
 | 
			
		||||
	"certificates.title": {
 | 
			
		||||
		"defaultMessage": "SSL Certificates"
 | 
			
		||||
	},
 | 
			
		||||
	"close": {
 | 
			
		||||
		"defaultMessage": "Close"
 | 
			
		||||
	},
 | 
			
		||||
	"column.access": {
 | 
			
		||||
		"defaultMessage": "Access"
 | 
			
		||||
	},
 | 
			
		||||
@@ -158,36 +143,18 @@
 | 
			
		||||
	"created-on": {
 | 
			
		||||
		"defaultMessage": "Created: {date}"
 | 
			
		||||
	},
 | 
			
		||||
	"dashboard.title": {
 | 
			
		||||
	"dashboard": {
 | 
			
		||||
		"defaultMessage": "Dashboard"
 | 
			
		||||
	},
 | 
			
		||||
	"dead-host.delete.content": {
 | 
			
		||||
		"defaultMessage": "Are you sure you want to delete this 404 host?"
 | 
			
		||||
	"dead-host": {
 | 
			
		||||
		"defaultMessage": "404 Host"
 | 
			
		||||
	},
 | 
			
		||||
	"dead-host.delete.title": {
 | 
			
		||||
		"defaultMessage": "Delete 404 Host"
 | 
			
		||||
	},
 | 
			
		||||
	"dead-host.edit": {
 | 
			
		||||
		"defaultMessage": "Edit 404 Host"
 | 
			
		||||
	},
 | 
			
		||||
	"dead-host.new": {
 | 
			
		||||
		"defaultMessage": "New 404 Host"
 | 
			
		||||
	},
 | 
			
		||||
	"dead-hosts.actions-title": {
 | 
			
		||||
		"defaultMessage": "404 Host #{id}"
 | 
			
		||||
	},
 | 
			
		||||
	"dead-hosts.add": {
 | 
			
		||||
		"defaultMessage": "Add 404 Host"
 | 
			
		||||
	"dead-hosts": {
 | 
			
		||||
		"defaultMessage": "404 Hosts"
 | 
			
		||||
	},
 | 
			
		||||
	"dead-hosts.count": {
 | 
			
		||||
		"defaultMessage": "{count} {count, plural, one {404 Host} other {404 Hosts}}"
 | 
			
		||||
	},
 | 
			
		||||
	"dead-hosts.empty": {
 | 
			
		||||
		"defaultMessage": "There are no 404 Hosts"
 | 
			
		||||
	},
 | 
			
		||||
	"dead-hosts.title": {
 | 
			
		||||
		"defaultMessage": "404 Hosts"
 | 
			
		||||
	},
 | 
			
		||||
	"disabled": {
 | 
			
		||||
		"defaultMessage": "Disabled"
 | 
			
		||||
	},
 | 
			
		||||
@@ -308,9 +275,6 @@
 | 
			
		||||
	"footer.github-fork": {
 | 
			
		||||
		"defaultMessage": "Fork me on Github"
 | 
			
		||||
	},
 | 
			
		||||
	"generic.flags.title": {
 | 
			
		||||
		"defaultMessage": "Options"
 | 
			
		||||
	},
 | 
			
		||||
	"host.flags.block-exploits": {
 | 
			
		||||
		"defaultMessage": "Block Common Exploits"
 | 
			
		||||
	},
 | 
			
		||||
@@ -332,7 +296,7 @@
 | 
			
		||||
	"host.forward-scheme": {
 | 
			
		||||
		"defaultMessage": "Scheme"
 | 
			
		||||
	},
 | 
			
		||||
	"hosts.title": {
 | 
			
		||||
	"hosts": {
 | 
			
		||||
		"defaultMessage": "Hosts"
 | 
			
		||||
	},
 | 
			
		||||
	"http-only": {
 | 
			
		||||
@@ -359,62 +323,47 @@
 | 
			
		||||
	"notfound.action": {
 | 
			
		||||
		"defaultMessage": "Take me home"
 | 
			
		||||
	},
 | 
			
		||||
	"notfound.text": {
 | 
			
		||||
	"notfound.content": {
 | 
			
		||||
		"defaultMessage": "We are sorry but the page you are looking for was not found"
 | 
			
		||||
	},
 | 
			
		||||
	"notfound.title": {
 | 
			
		||||
		"defaultMessage": "Oops… You just found an error page"
 | 
			
		||||
	},
 | 
			
		||||
	"notification.access-deleted": {
 | 
			
		||||
		"defaultMessage": "Access has been deleted"
 | 
			
		||||
	},
 | 
			
		||||
	"notification.access-saved": {
 | 
			
		||||
		"defaultMessage": "Access has been saved"
 | 
			
		||||
	},
 | 
			
		||||
	"notification.dead-host-saved": {
 | 
			
		||||
		"defaultMessage": "404 Host has been saved"
 | 
			
		||||
	},
 | 
			
		||||
	"notification.error": {
 | 
			
		||||
		"defaultMessage": "Error"
 | 
			
		||||
	},
 | 
			
		||||
	"notification.host-deleted": {
 | 
			
		||||
		"defaultMessage": "Host has been deleted"
 | 
			
		||||
	"notification.object-deleted": {
 | 
			
		||||
		"defaultMessage": "{object} has been deleted"
 | 
			
		||||
	},
 | 
			
		||||
	"notification.host-disabled": {
 | 
			
		||||
		"defaultMessage": "Host has been disabled"
 | 
			
		||||
	"notification.object-disabled": {
 | 
			
		||||
		"defaultMessage": "{object} has been disabled"
 | 
			
		||||
	},
 | 
			
		||||
	"notification.host-enabled": {
 | 
			
		||||
		"defaultMessage": "Host has been enabled"
 | 
			
		||||
	"notification.object-enabled": {
 | 
			
		||||
		"defaultMessage": "{object} has been enabled"
 | 
			
		||||
	},
 | 
			
		||||
	"notification.proxy-host-saved": {
 | 
			
		||||
		"defaultMessage": "Proxy Host has been saved"
 | 
			
		||||
	},
 | 
			
		||||
	"notification.redirection-host-saved": {
 | 
			
		||||
		"defaultMessage": "Redirection Host has been saved"
 | 
			
		||||
	},
 | 
			
		||||
	"notification.stream-deleted": {
 | 
			
		||||
		"defaultMessage": "Stream has been deleted"
 | 
			
		||||
	},
 | 
			
		||||
	"notification.stream-disabled": {
 | 
			
		||||
		"defaultMessage": "Stream has been disabled"
 | 
			
		||||
	},
 | 
			
		||||
	"notification.stream-enabled": {
 | 
			
		||||
		"defaultMessage": "Stream has been enabled"
 | 
			
		||||
	"notification.object-saved": {
 | 
			
		||||
		"defaultMessage": "{object} has been saved"
 | 
			
		||||
	},
 | 
			
		||||
	"notification.success": {
 | 
			
		||||
		"defaultMessage": "Success"
 | 
			
		||||
	},
 | 
			
		||||
	"notification.user-deleted": {
 | 
			
		||||
		"defaultMessage": "User has been deleted"
 | 
			
		||||
	"object.actions-title": {
 | 
			
		||||
		"defaultMessage": "{object} #{id}"
 | 
			
		||||
	},
 | 
			
		||||
	"notification.user-disabled": {
 | 
			
		||||
		"defaultMessage": "User has been disabled"
 | 
			
		||||
	"object.add": {
 | 
			
		||||
		"defaultMessage": "Add {object}"
 | 
			
		||||
	},
 | 
			
		||||
	"notification.user-enabled": {
 | 
			
		||||
		"defaultMessage": "User has been enabled"
 | 
			
		||||
	"object.delete": {
 | 
			
		||||
		"defaultMessage": "Delete {object}"
 | 
			
		||||
	},
 | 
			
		||||
	"notification.user-saved": {
 | 
			
		||||
		"defaultMessage": "User has been saved"
 | 
			
		||||
	"object.delete.content": {
 | 
			
		||||
		"defaultMessage": "Are you sure you want to delete this {object}?"
 | 
			
		||||
	},
 | 
			
		||||
	"object.edit": {
 | 
			
		||||
		"defaultMessage": "Edit {object}"
 | 
			
		||||
	},
 | 
			
		||||
	"object.empty": {
 | 
			
		||||
		"defaultMessage": "There are no {objects}"
 | 
			
		||||
	},
 | 
			
		||||
	"offline": {
 | 
			
		||||
		"defaultMessage": "Offline"
 | 
			
		||||
@@ -422,6 +371,9 @@
 | 
			
		||||
	"online": {
 | 
			
		||||
		"defaultMessage": "Online"
 | 
			
		||||
	},
 | 
			
		||||
	"options": {
 | 
			
		||||
		"defaultMessage": "Options"
 | 
			
		||||
	},
 | 
			
		||||
	"password": {
 | 
			
		||||
		"defaultMessage": "Password"
 | 
			
		||||
	},
 | 
			
		||||
@@ -452,57 +404,33 @@
 | 
			
		||||
	"permissions.visibility.user": {
 | 
			
		||||
		"defaultMessage": "Created Items Only"
 | 
			
		||||
	},
 | 
			
		||||
	"proxy-host.edit": {
 | 
			
		||||
		"defaultMessage": "Edit Proxy Host"
 | 
			
		||||
	"proxy-host": {
 | 
			
		||||
		"defaultMessage": "Proxy Host"
 | 
			
		||||
	},
 | 
			
		||||
	"proxy-host.forward-host": {
 | 
			
		||||
		"defaultMessage": "Forward Hostname / IP"
 | 
			
		||||
	},
 | 
			
		||||
	"proxy-host.new": {
 | 
			
		||||
		"defaultMessage": "New Proxy Host"
 | 
			
		||||
	},
 | 
			
		||||
	"proxy-hosts.actions-title": {
 | 
			
		||||
		"defaultMessage": "Proxy Host #{id}"
 | 
			
		||||
	},
 | 
			
		||||
	"proxy-hosts.add": {
 | 
			
		||||
		"defaultMessage": "Add Proxy Host"
 | 
			
		||||
	"proxy-hosts": {
 | 
			
		||||
		"defaultMessage": "Proxy Hosts"
 | 
			
		||||
	},
 | 
			
		||||
	"proxy-hosts.count": {
 | 
			
		||||
		"defaultMessage": "{count} {count, plural, one {Proxy Host} other {Proxy Hosts}}"
 | 
			
		||||
	},
 | 
			
		||||
	"proxy-hosts.empty": {
 | 
			
		||||
		"defaultMessage": "There are no Proxy Hosts"
 | 
			
		||||
	"public": {
 | 
			
		||||
		"defaultMessage": "Public"
 | 
			
		||||
	},
 | 
			
		||||
	"proxy-hosts.title": {
 | 
			
		||||
		"defaultMessage": "Proxy Hosts"
 | 
			
		||||
	},
 | 
			
		||||
	"redirection-host.delete.content": {
 | 
			
		||||
		"defaultMessage": "Are you sure you want to delete this Redirection host?"
 | 
			
		||||
	},
 | 
			
		||||
	"redirection-host.delete.title": {
 | 
			
		||||
		"defaultMessage": "Delete Redirection Host"
 | 
			
		||||
	"redirection-host": {
 | 
			
		||||
		"defaultMessage": "Redirection Host"
 | 
			
		||||
	},
 | 
			
		||||
	"redirection-host.forward-domain": {
 | 
			
		||||
		"defaultMessage": "Forward Domain"
 | 
			
		||||
	},
 | 
			
		||||
	"redirection-host.new": {
 | 
			
		||||
		"defaultMessage": "New Redirection Host"
 | 
			
		||||
	},
 | 
			
		||||
	"redirection-hosts.actions-title": {
 | 
			
		||||
		"defaultMessage": "Redirection Host #{id}"
 | 
			
		||||
	},
 | 
			
		||||
	"redirection-hosts.add": {
 | 
			
		||||
		"defaultMessage": "Add Redirection Host"
 | 
			
		||||
	"redirection-hosts": {
 | 
			
		||||
		"defaultMessage": "Redirection Hosts"
 | 
			
		||||
	},
 | 
			
		||||
	"redirection-hosts.count": {
 | 
			
		||||
		"defaultMessage": "{count} {count, plural, one {Redirection Host} other {Redirection Hosts}}"
 | 
			
		||||
	},
 | 
			
		||||
	"redirection-hosts.empty": {
 | 
			
		||||
		"defaultMessage": "There are no Redirection Hosts"
 | 
			
		||||
	},
 | 
			
		||||
	"redirection-hosts.title": {
 | 
			
		||||
		"defaultMessage": "Redirection Hosts"
 | 
			
		||||
	},
 | 
			
		||||
	"role.admin": {
 | 
			
		||||
		"defaultMessage": "Administrator"
 | 
			
		||||
	},
 | 
			
		||||
@@ -512,7 +440,7 @@
 | 
			
		||||
	"save": {
 | 
			
		||||
		"defaultMessage": "Save"
 | 
			
		||||
	},
 | 
			
		||||
	"settings.title": {
 | 
			
		||||
	"settings": {
 | 
			
		||||
		"defaultMessage": "Settings"
 | 
			
		||||
	},
 | 
			
		||||
	"setup.preamble": {
 | 
			
		||||
@@ -527,14 +455,8 @@
 | 
			
		||||
	"ssl-certificate": {
 | 
			
		||||
		"defaultMessage": "SSL Certificate"
 | 
			
		||||
	},
 | 
			
		||||
	"stream.delete.content": {
 | 
			
		||||
		"defaultMessage": "Are you sure you want to delete this Stream?"
 | 
			
		||||
	},
 | 
			
		||||
	"stream.delete.title": {
 | 
			
		||||
		"defaultMessage": "Delete Stream"
 | 
			
		||||
	},
 | 
			
		||||
	"stream.edit": {
 | 
			
		||||
		"defaultMessage": "Edit Stream"
 | 
			
		||||
	"stream": {
 | 
			
		||||
		"defaultMessage": "Stream"
 | 
			
		||||
	},
 | 
			
		||||
	"stream.forward-host": {
 | 
			
		||||
		"defaultMessage": "Forward Host"
 | 
			
		||||
@@ -542,30 +464,21 @@
 | 
			
		||||
	"stream.incoming-port": {
 | 
			
		||||
		"defaultMessage": "Incoming Port"
 | 
			
		||||
	},
 | 
			
		||||
	"stream.new": {
 | 
			
		||||
		"defaultMessage": "New Stream"
 | 
			
		||||
	},
 | 
			
		||||
	"streams.actions-title": {
 | 
			
		||||
		"defaultMessage": "Stream #{id}"
 | 
			
		||||
	},
 | 
			
		||||
	"streams.add": {
 | 
			
		||||
		"defaultMessage": "Add Stream"
 | 
			
		||||
	"streams": {
 | 
			
		||||
		"defaultMessage": "Streams"
 | 
			
		||||
	},
 | 
			
		||||
	"streams.count": {
 | 
			
		||||
		"defaultMessage": "{count} {count, plural, one {Stream} other {Streams}}"
 | 
			
		||||
	},
 | 
			
		||||
	"streams.empty": {
 | 
			
		||||
		"defaultMessage": "There are no Streams"
 | 
			
		||||
	},
 | 
			
		||||
	"streams.tcp": {
 | 
			
		||||
		"defaultMessage": "TCP"
 | 
			
		||||
	},
 | 
			
		||||
	"streams.title": {
 | 
			
		||||
		"defaultMessage": "Streams"
 | 
			
		||||
	},
 | 
			
		||||
	"streams.udp": {
 | 
			
		||||
		"defaultMessage": "UDP"
 | 
			
		||||
	},
 | 
			
		||||
	"user": {
 | 
			
		||||
		"defaultMessage": "User"
 | 
			
		||||
	},
 | 
			
		||||
	"user.change-password": {
 | 
			
		||||
		"defaultMessage": "Change Password"
 | 
			
		||||
	},
 | 
			
		||||
@@ -575,30 +488,15 @@
 | 
			
		||||
	"user.current-password": {
 | 
			
		||||
		"defaultMessage": "Current Password"
 | 
			
		||||
	},
 | 
			
		||||
	"user.delete.content": {
 | 
			
		||||
		"defaultMessage": "Are you sure you want to delete this user?"
 | 
			
		||||
	},
 | 
			
		||||
	"user.delete.title": {
 | 
			
		||||
		"defaultMessage": "Delete User"
 | 
			
		||||
	},
 | 
			
		||||
	"user.edit": {
 | 
			
		||||
		"defaultMessage": "Edit User"
 | 
			
		||||
	},
 | 
			
		||||
	"user.edit-profile": {
 | 
			
		||||
		"defaultMessage": "Edit Profile"
 | 
			
		||||
	},
 | 
			
		||||
	"user.flags.title": {
 | 
			
		||||
		"defaultMessage": "Properties"
 | 
			
		||||
	},
 | 
			
		||||
	"user.full-name": {
 | 
			
		||||
		"defaultMessage": "Full Name"
 | 
			
		||||
	},
 | 
			
		||||
	"user.logout": {
 | 
			
		||||
		"defaultMessage": "Logout"
 | 
			
		||||
	},
 | 
			
		||||
	"user.new": {
 | 
			
		||||
		"defaultMessage": "New User"
 | 
			
		||||
	},
 | 
			
		||||
	"user.new-password": {
 | 
			
		||||
		"defaultMessage": "New Password"
 | 
			
		||||
	},
 | 
			
		||||
@@ -620,16 +518,7 @@
 | 
			
		||||
	"username": {
 | 
			
		||||
		"defaultMessage": "Username"
 | 
			
		||||
	},
 | 
			
		||||
	"users.actions-title": {
 | 
			
		||||
		"defaultMessage": "User #{id}"
 | 
			
		||||
	},
 | 
			
		||||
	"users.add": {
 | 
			
		||||
		"defaultMessage": "Add User"
 | 
			
		||||
	},
 | 
			
		||||
	"users.empty": {
 | 
			
		||||
		"defaultMessage": "There are no Users"
 | 
			
		||||
	},
 | 
			
		||||
	"users.title": {
 | 
			
		||||
	"users": {
 | 
			
		||||
		"defaultMessage": "Users"
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
	"dashboard.title": {
 | 
			
		||||
	"dashboard": {
 | 
			
		||||
		"defaultMessage": "داشبورد"
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ import { AccessClientFields, BasicAuthFields, Button, Loading } from "src/compon
 | 
			
		||||
import { useAccessList, useSetAccessList } from "src/hooks";
 | 
			
		||||
import { intl, T } from "src/locale";
 | 
			
		||||
import { validateString } from "src/modules/Validations";
 | 
			
		||||
import { showSuccess } from "src/notifications";
 | 
			
		||||
import { showObjectSuccess } from "src/notifications";
 | 
			
		||||
 | 
			
		||||
const showAccessListModal = (id: number | "new") => {
 | 
			
		||||
	EasyModal.show(AccessListModal, { id });
 | 
			
		||||
@@ -72,7 +72,7 @@ const AccessListModal = EasyModal.create(({ id, visible, remove }: Props) => {
 | 
			
		||||
		setAccessList(payload, {
 | 
			
		||||
			onError: (err: any) => setErrorMsg(<T id={err.message} />),
 | 
			
		||||
			onSuccess: () => {
 | 
			
		||||
				showSuccess(intl.formatMessage({ id: "notification.access-saved" }));
 | 
			
		||||
				showObjectSuccess("access-list", "saved");
 | 
			
		||||
				remove();
 | 
			
		||||
			},
 | 
			
		||||
			onSettled: () => {
 | 
			
		||||
@@ -110,7 +110,7 @@ const AccessListModal = EasyModal.create(({ id, visible, remove }: Props) => {
 | 
			
		||||
						<Form>
 | 
			
		||||
							<Modal.Header closeButton>
 | 
			
		||||
								<Modal.Title>
 | 
			
		||||
									<T id={data?.id ? "access.edit" : "access.new"} />
 | 
			
		||||
									<T id={data?.id ? "object.edit" : "object.add"} tData={{ object: "access-list" }} />
 | 
			
		||||
								</Modal.Title>
 | 
			
		||||
							</Modal.Header>
 | 
			
		||||
							<Modal.Body className="p-0">
 | 
			
		||||
@@ -186,13 +186,13 @@ const AccessListModal = EasyModal.create(({ id, visible, remove }: Props) => {
 | 
			
		||||
												</Field>
 | 
			
		||||
												<div className="my-3">
 | 
			
		||||
													<h3 className="py-2">
 | 
			
		||||
														<T id="generic.flags.title" />
 | 
			
		||||
														<T id="options" />
 | 
			
		||||
													</h3>
 | 
			
		||||
													<div className="divide-y">
 | 
			
		||||
														<div>
 | 
			
		||||
															<label className="row" htmlFor="satisfyAny">
 | 
			
		||||
																<span className="col">
 | 
			
		||||
																	<T id="access.satisfy-any" />
 | 
			
		||||
																	<T id="access-list.satisfy-any" />
 | 
			
		||||
																</span>
 | 
			
		||||
																<span className="col-auto">
 | 
			
		||||
																	<Field name="satisfyAny" type="checkbox">
 | 
			
		||||
@@ -224,7 +224,7 @@ const AccessListModal = EasyModal.create(({ id, visible, remove }: Props) => {
 | 
			
		||||
														<div>
 | 
			
		||||
															<label className="row" htmlFor="passAuth">
 | 
			
		||||
																<span className="col">
 | 
			
		||||
																	<T id="access.pass-auth" />
 | 
			
		||||
																	<T id="access-list.pass-auth" />
 | 
			
		||||
																</span>
 | 
			
		||||
																<span className="col-auto">
 | 
			
		||||
																	<Field name="passAuth" type="checkbox">
 | 
			
		||||
 
 | 
			
		||||
@@ -13,8 +13,8 @@ import {
 | 
			
		||||
	SSLOptionsFields,
 | 
			
		||||
} from "src/components";
 | 
			
		||||
import { useDeadHost, useSetDeadHost } from "src/hooks";
 | 
			
		||||
import { intl, T } from "src/locale";
 | 
			
		||||
import { showSuccess } from "src/notifications";
 | 
			
		||||
import { T } from "src/locale";
 | 
			
		||||
import { showObjectSuccess } from "src/notifications";
 | 
			
		||||
 | 
			
		||||
const showDeadHostModal = (id: number | "new") => {
 | 
			
		||||
	EasyModal.show(DeadHostModal, { id });
 | 
			
		||||
@@ -42,7 +42,7 @@ const DeadHostModal = EasyModal.create(({ id, visible, remove }: Props) => {
 | 
			
		||||
		setDeadHost(payload, {
 | 
			
		||||
			onError: (err: any) => setErrorMsg(<T id={err.message} />),
 | 
			
		||||
			onSuccess: () => {
 | 
			
		||||
				showSuccess(intl.formatMessage({ id: "notification.dead-host-saved" }));
 | 
			
		||||
				showObjectSuccess("dead-host", "saved");
 | 
			
		||||
				remove();
 | 
			
		||||
			},
 | 
			
		||||
			onSettled: () => {
 | 
			
		||||
@@ -80,7 +80,7 @@ const DeadHostModal = EasyModal.create(({ id, visible, remove }: Props) => {
 | 
			
		||||
						<Form>
 | 
			
		||||
							<Modal.Header closeButton>
 | 
			
		||||
								<Modal.Title>
 | 
			
		||||
									<T id={data?.id ? "dead-host.edit" : "dead-host.new"} />
 | 
			
		||||
									<T id={data?.id ? "object.edit" : "object.add"} tData={{ object: "dead-host" }} />
 | 
			
		||||
								</Modal.Title>
 | 
			
		||||
							</Modal.Header>
 | 
			
		||||
							<Modal.Body className="p-0">
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,8 @@ import { Button } from "src/components";
 | 
			
		||||
import { T } from "src/locale";
 | 
			
		||||
 | 
			
		||||
interface ShowProps {
 | 
			
		||||
	title: string;
 | 
			
		||||
	title?: ReactNode;
 | 
			
		||||
	tTitle?: string;
 | 
			
		||||
	children: ReactNode;
 | 
			
		||||
	onConfirm: () => Promise<void> | void;
 | 
			
		||||
	invalidations?: any[];
 | 
			
		||||
@@ -19,7 +20,8 @@ const showDeleteConfirmModal = (props: ShowProps) => {
 | 
			
		||||
	EasyModal.show(DeleteConfirmModal, props);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const DeleteConfirmModal = EasyModal.create(({ title, children, onConfirm, invalidations, visible, remove }: Props) => {
 | 
			
		||||
const DeleteConfirmModal = EasyModal.create(
 | 
			
		||||
	({ title, tTitle, children, onConfirm, invalidations, visible, remove }: Props) => {
 | 
			
		||||
		const queryClient = useQueryClient();
 | 
			
		||||
		const [error, setError] = useState<ReactNode | null>(null);
 | 
			
		||||
		const [isSubmitting, setIsSubmitting] = useState(false);
 | 
			
		||||
@@ -44,9 +46,7 @@ const DeleteConfirmModal = EasyModal.create(({ title, children, onConfirm, inval
 | 
			
		||||
		return (
 | 
			
		||||
			<Modal show={visible} onHide={remove}>
 | 
			
		||||
				<Modal.Header closeButton>
 | 
			
		||||
				<Modal.Title>
 | 
			
		||||
					<T id={title} />
 | 
			
		||||
				</Modal.Title>
 | 
			
		||||
					<Modal.Title>{tTitle ? <T id={tTitle} /> : title ? title : null}</Modal.Title>
 | 
			
		||||
				</Modal.Header>
 | 
			
		||||
				<Modal.Body>
 | 
			
		||||
					<Alert variant="danger" show={!!error} onClose={() => setError(null)} dismissible>
 | 
			
		||||
@@ -72,6 +72,7 @@ const DeleteConfirmModal = EasyModal.create(({ title, children, onConfirm, inval
 | 
			
		||||
				</Modal.Footer>
 | 
			
		||||
			</Modal>
 | 
			
		||||
		);
 | 
			
		||||
});
 | 
			
		||||
	},
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export { showDeleteConfirmModal };
 | 
			
		||||
 
 | 
			
		||||
@@ -46,7 +46,7 @@ const EventDetailsModal = EasyModal.create(({ id, visible, remove }: Props) => {
 | 
			
		||||
					</Modal.Body>
 | 
			
		||||
					<Modal.Footer>
 | 
			
		||||
						<Button data-bs-dismiss="modal" onClick={remove}>
 | 
			
		||||
							<T id="close" />
 | 
			
		||||
							<T id="action.close" />
 | 
			
		||||
						</Button>
 | 
			
		||||
					</Modal.Footer>
 | 
			
		||||
				</>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								frontend/src/modals/PermissionsModal.module.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								frontend/src/modals/PermissionsModal.module.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
.active {
 | 
			
		||||
    border-color: var(--tblr-orange) !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -9,6 +9,7 @@ import { setPermissions } from "src/api/backend";
 | 
			
		||||
import { Button, Loading } from "src/components";
 | 
			
		||||
import { useUser } from "src/hooks";
 | 
			
		||||
import { T } from "src/locale";
 | 
			
		||||
import styles from "./PermissionsModal.module.css";
 | 
			
		||||
 | 
			
		||||
const showPermissionsModal = (id: number) => {
 | 
			
		||||
	EasyModal.show(PermissionsModal, { id });
 | 
			
		||||
@@ -39,7 +40,18 @@ const PermissionsModal = EasyModal.create(({ id, visible, remove }: Props) => {
 | 
			
		||||
		setIsSubmitting(false);
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const getClasses = (active: boolean) => {
 | 
			
		||||
		return cn("btn", active ? styles.active : null, {
 | 
			
		||||
			active,
 | 
			
		||||
			"bg-orange-lt": active,
 | 
			
		||||
		});
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const getPermissionButtons = (field: any, form: any) => {
 | 
			
		||||
		const isManage = field.value === "manage";
 | 
			
		||||
		const isView = field.value === "view";
 | 
			
		||||
		const isHidden = field.value === "hidden";
 | 
			
		||||
 | 
			
		||||
		return (
 | 
			
		||||
			<div>
 | 
			
		||||
				<div className="btn-group w-100" role="group">
 | 
			
		||||
@@ -53,7 +65,7 @@ const PermissionsModal = EasyModal.create(({ id, visible, remove }: Props) => {
 | 
			
		||||
						checked={field.value === "manage"}
 | 
			
		||||
						onChange={() => form.setFieldValue(field.name, "manage")}
 | 
			
		||||
					/>
 | 
			
		||||
					<label htmlFor={`${field.name}-manage`} className={cn("btn", { active: field.value === "manage" })}>
 | 
			
		||||
					<label htmlFor={`${field.name}-manage`} className={getClasses(isManage)}>
 | 
			
		||||
						<T id="permissions.manage" />
 | 
			
		||||
					</label>
 | 
			
		||||
					<input
 | 
			
		||||
@@ -66,7 +78,7 @@ const PermissionsModal = EasyModal.create(({ id, visible, remove }: Props) => {
 | 
			
		||||
						checked={field.value === "view"}
 | 
			
		||||
						onChange={() => form.setFieldValue(field.name, "view")}
 | 
			
		||||
					/>
 | 
			
		||||
					<label htmlFor={`${field.name}-view`} className={cn("btn", { active: field.value === "view" })}>
 | 
			
		||||
					<label htmlFor={`${field.name}-view`} className={getClasses(isView)}>
 | 
			
		||||
						<T id="permissions.view" />
 | 
			
		||||
					</label>
 | 
			
		||||
					<input
 | 
			
		||||
@@ -79,7 +91,7 @@ const PermissionsModal = EasyModal.create(({ id, visible, remove }: Props) => {
 | 
			
		||||
						checked={field.value === "hidden"}
 | 
			
		||||
						onChange={() => form.setFieldValue(field.name, "hidden")}
 | 
			
		||||
					/>
 | 
			
		||||
					<label htmlFor={`${field.name}-hidden`} className={cn("btn", { active: field.value === "hidden" })}>
 | 
			
		||||
					<label htmlFor={`${field.name}-hidden`} className={getClasses(isHidden)}>
 | 
			
		||||
						<T id="permissions.hidden" />
 | 
			
		||||
					</label>
 | 
			
		||||
				</div>
 | 
			
		||||
@@ -142,7 +154,7 @@ const PermissionsModal = EasyModal.create(({ id, visible, remove }: Props) => {
 | 
			
		||||
												/>
 | 
			
		||||
												<label
 | 
			
		||||
													htmlFor={`${field.name}-user`}
 | 
			
		||||
													className={cn("btn", { active: field.value === "user" })}
 | 
			
		||||
													className={getClasses(field.value === "user")}
 | 
			
		||||
												>
 | 
			
		||||
													<T id="permissions.visibility.user" />
 | 
			
		||||
												</label>
 | 
			
		||||
@@ -158,7 +170,7 @@ const PermissionsModal = EasyModal.create(({ id, visible, remove }: Props) => {
 | 
			
		||||
												/>
 | 
			
		||||
												<label
 | 
			
		||||
													htmlFor={`${field.name}-all`}
 | 
			
		||||
													className={cn("btn", { active: field.value === "all" })}
 | 
			
		||||
													className={getClasses(field.value === "all")}
 | 
			
		||||
												>
 | 
			
		||||
													<T id="permissions.visibility.all" />
 | 
			
		||||
												</label>
 | 
			
		||||
@@ -170,7 +182,7 @@ const PermissionsModal = EasyModal.create(({ id, visible, remove }: Props) => {
 | 
			
		||||
									<>
 | 
			
		||||
										<div className="mb-3">
 | 
			
		||||
											<label htmlFor="ignored" className="form-label">
 | 
			
		||||
												<T id="proxy-hosts.title" />
 | 
			
		||||
												<T id="proxy-hosts" />
 | 
			
		||||
											</label>
 | 
			
		||||
											<Field name="proxyHosts">
 | 
			
		||||
												{({ field, form }: any) => getPermissionButtons(field, form)}
 | 
			
		||||
@@ -178,7 +190,7 @@ const PermissionsModal = EasyModal.create(({ id, visible, remove }: Props) => {
 | 
			
		||||
										</div>
 | 
			
		||||
										<div className="mb-3">
 | 
			
		||||
											<label htmlFor="ignored" className="form-label">
 | 
			
		||||
												<T id="redirection-hosts.title" />
 | 
			
		||||
												<T id="redirection-hosts" />
 | 
			
		||||
											</label>
 | 
			
		||||
											<Field name="redirectionHosts">
 | 
			
		||||
												{({ field, form }: any) => getPermissionButtons(field, form)}
 | 
			
		||||
@@ -186,7 +198,7 @@ const PermissionsModal = EasyModal.create(({ id, visible, remove }: Props) => {
 | 
			
		||||
										</div>
 | 
			
		||||
										<div className="mb-3">
 | 
			
		||||
											<label htmlFor="ignored" className="form-label">
 | 
			
		||||
												<T id="dead-hosts.title" />
 | 
			
		||||
												<T id="dead-hosts" />
 | 
			
		||||
											</label>
 | 
			
		||||
											<Field name="deadHosts">
 | 
			
		||||
												{({ field, form }: any) => getPermissionButtons(field, form)}
 | 
			
		||||
@@ -194,7 +206,7 @@ const PermissionsModal = EasyModal.create(({ id, visible, remove }: Props) => {
 | 
			
		||||
										</div>
 | 
			
		||||
										<div className="mb-3">
 | 
			
		||||
											<label htmlFor="ignored" className="form-label">
 | 
			
		||||
												<T id="streams.title" />
 | 
			
		||||
												<T id="streams" />
 | 
			
		||||
											</label>
 | 
			
		||||
											<Field name="streams">
 | 
			
		||||
												{({ field, form }: any) => getPermissionButtons(field, form)}
 | 
			
		||||
@@ -202,7 +214,7 @@ const PermissionsModal = EasyModal.create(({ id, visible, remove }: Props) => {
 | 
			
		||||
										</div>
 | 
			
		||||
										<div className="mb-3">
 | 
			
		||||
											<label htmlFor="ignored" className="form-label">
 | 
			
		||||
												<T id="access.title" />
 | 
			
		||||
												<T id="access-lists" />
 | 
			
		||||
											</label>
 | 
			
		||||
											<Field name="accessLists">
 | 
			
		||||
												{({ field, form }: any) => getPermissionButtons(field, form)}
 | 
			
		||||
@@ -210,7 +222,7 @@ const PermissionsModal = EasyModal.create(({ id, visible, remove }: Props) => {
 | 
			
		||||
										</div>
 | 
			
		||||
										<div className="mb-3">
 | 
			
		||||
											<label htmlFor="ignored" className="form-label">
 | 
			
		||||
												<T id="certificates.title" />
 | 
			
		||||
												<T id="certificates" />
 | 
			
		||||
											</label>
 | 
			
		||||
											<Field name="certificates">
 | 
			
		||||
												{({ field, form }: any) => getPermissionButtons(field, form)}
 | 
			
		||||
@@ -225,8 +237,7 @@ const PermissionsModal = EasyModal.create(({ id, visible, remove }: Props) => {
 | 
			
		||||
								</Button>
 | 
			
		||||
								<Button
 | 
			
		||||
									type="submit"
 | 
			
		||||
									actionType="primary"
 | 
			
		||||
									className="ms-auto"
 | 
			
		||||
									className="ms-auto btn-orange"
 | 
			
		||||
									data-bs-dismiss="modal"
 | 
			
		||||
									isLoading={isSubmitting}
 | 
			
		||||
									disabled={isSubmitting}
 | 
			
		||||
 
 | 
			
		||||
@@ -10,14 +10,15 @@ import {
 | 
			
		||||
	Button,
 | 
			
		||||
	DomainNamesField,
 | 
			
		||||
	Loading,
 | 
			
		||||
	LocationsFields,
 | 
			
		||||
	NginxConfigField,
 | 
			
		||||
	SSLCertificateField,
 | 
			
		||||
	SSLOptionsFields,
 | 
			
		||||
} from "src/components";
 | 
			
		||||
import { useProxyHost, useSetProxyHost } from "src/hooks";
 | 
			
		||||
import { intl, T } from "src/locale";
 | 
			
		||||
import { T } from "src/locale";
 | 
			
		||||
import { validateNumber, validateString } from "src/modules/Validations";
 | 
			
		||||
import { showSuccess } from "src/notifications";
 | 
			
		||||
import { showObjectSuccess } from "src/notifications";
 | 
			
		||||
 | 
			
		||||
const showProxyHostModal = (id: number | "new") => {
 | 
			
		||||
	EasyModal.show(ProxyHostModal, { id });
 | 
			
		||||
@@ -45,7 +46,7 @@ const ProxyHostModal = EasyModal.create(({ id, visible, remove }: Props) => {
 | 
			
		||||
		setProxyHost(payload, {
 | 
			
		||||
			onError: (err: any) => setErrorMsg(<T id={err.message} />),
 | 
			
		||||
			onSuccess: () => {
 | 
			
		||||
				showSuccess(intl.formatMessage({ id: "notification.proxy-host-saved" }));
 | 
			
		||||
				showObjectSuccess("proxy-host", "saved");
 | 
			
		||||
				remove();
 | 
			
		||||
			},
 | 
			
		||||
			onSettled: () => {
 | 
			
		||||
@@ -95,7 +96,7 @@ const ProxyHostModal = EasyModal.create(({ id, visible, remove }: Props) => {
 | 
			
		||||
						<Form>
 | 
			
		||||
							<Modal.Header closeButton>
 | 
			
		||||
								<Modal.Title>
 | 
			
		||||
									<T id={data?.id ? "proxy-host.edit" : "proxy-host.new"} />
 | 
			
		||||
									<T id={data?.id ? "object.edit" : "object.add"} tData={{ object: "proxy-host" }} />
 | 
			
		||||
								</Modal.Title>
 | 
			
		||||
							</Modal.Header>
 | 
			
		||||
							<Modal.Body className="p-0">
 | 
			
		||||
@@ -251,7 +252,7 @@ const ProxyHostModal = EasyModal.create(({ id, visible, remove }: Props) => {
 | 
			
		||||
												<AccessField />
 | 
			
		||||
												<div className="my-3">
 | 
			
		||||
													<h4 className="py-2">
 | 
			
		||||
														<T id="generic.flags.title" />
 | 
			
		||||
														<T id="options" />
 | 
			
		||||
													</h4>
 | 
			
		||||
													<div className="divide-y">
 | 
			
		||||
														<div>
 | 
			
		||||
@@ -327,7 +328,7 @@ const ProxyHostModal = EasyModal.create(({ id, visible, remove }: Props) => {
 | 
			
		||||
												</div>
 | 
			
		||||
											</div>
 | 
			
		||||
											<div className="tab-pane" id="tab-locations" role="tabpanel">
 | 
			
		||||
												locations TODO
 | 
			
		||||
												<LocationsFields initialValues={data?.locations || []} />
 | 
			
		||||
											</div>
 | 
			
		||||
											<div className="tab-pane" id="tab-ssl" role="tabpanel">
 | 
			
		||||
												<SSLCertificateField
 | 
			
		||||
 
 | 
			
		||||
@@ -14,9 +14,9 @@ import {
 | 
			
		||||
	SSLOptionsFields,
 | 
			
		||||
} from "src/components";
 | 
			
		||||
import { useRedirectionHost, useSetRedirectionHost } from "src/hooks";
 | 
			
		||||
import { intl, T } from "src/locale";
 | 
			
		||||
import { T } from "src/locale";
 | 
			
		||||
import { validateString } from "src/modules/Validations";
 | 
			
		||||
import { showSuccess } from "src/notifications";
 | 
			
		||||
import { showObjectSuccess } from "src/notifications";
 | 
			
		||||
 | 
			
		||||
const showRedirectionHostModal = (id: number | "new") => {
 | 
			
		||||
	EasyModal.show(RedirectionHostModal, { id });
 | 
			
		||||
@@ -44,7 +44,7 @@ const RedirectionHostModal = EasyModal.create(({ id, visible, remove }: Props) =
 | 
			
		||||
		setRedirectionHost(payload, {
 | 
			
		||||
			onError: (err: any) => setErrorMsg(<T id={err.message} />),
 | 
			
		||||
			onSuccess: () => {
 | 
			
		||||
				showSuccess(intl.formatMessage({ id: "notification.redirection-host-saved" }));
 | 
			
		||||
				showObjectSuccess("redirection-host", "saved");
 | 
			
		||||
				remove();
 | 
			
		||||
			},
 | 
			
		||||
			onSettled: () => {
 | 
			
		||||
@@ -90,7 +90,10 @@ const RedirectionHostModal = EasyModal.create(({ id, visible, remove }: Props) =
 | 
			
		||||
						<Form>
 | 
			
		||||
							<Modal.Header closeButton>
 | 
			
		||||
								<Modal.Title>
 | 
			
		||||
									<T id={data?.id ? "redirection-host.edit" : "redirection-host.new"} />
 | 
			
		||||
									<T
 | 
			
		||||
										id={data?.id ? "object.edit" : "object.add"}
 | 
			
		||||
										tData={{ object: "redirection-host" }}
 | 
			
		||||
									/>
 | 
			
		||||
								</Modal.Title>
 | 
			
		||||
							</Modal.Header>
 | 
			
		||||
							<Modal.Body className="p-0">
 | 
			
		||||
@@ -211,7 +214,7 @@ const RedirectionHostModal = EasyModal.create(({ id, visible, remove }: Props) =
 | 
			
		||||
												</div>
 | 
			
		||||
												<div className="my-3">
 | 
			
		||||
													<h4 className="py-2">
 | 
			
		||||
														<T id="generic.flags.title" />
 | 
			
		||||
														<T id="options" />
 | 
			
		||||
													</h4>
 | 
			
		||||
													<div className="divide-y">
 | 
			
		||||
														<div>
 | 
			
		||||
 
 | 
			
		||||
@@ -5,9 +5,9 @@ import { Alert } from "react-bootstrap";
 | 
			
		||||
import Modal from "react-bootstrap/Modal";
 | 
			
		||||
import { Button, Loading, SSLCertificateField, SSLOptionsFields } from "src/components";
 | 
			
		||||
import { useSetStream, useStream } from "src/hooks";
 | 
			
		||||
import { intl, T } from "src/locale";
 | 
			
		||||
import { T } from "src/locale";
 | 
			
		||||
import { validateNumber, validateString } from "src/modules/Validations";
 | 
			
		||||
import { showSuccess } from "src/notifications";
 | 
			
		||||
import { showObjectSuccess } from "src/notifications";
 | 
			
		||||
 | 
			
		||||
const showStreamModal = (id: number | "new") => {
 | 
			
		||||
	EasyModal.show(StreamModal, { id });
 | 
			
		||||
@@ -35,7 +35,7 @@ const StreamModal = EasyModal.create(({ id, visible, remove }: Props) => {
 | 
			
		||||
		setStream(payload, {
 | 
			
		||||
			onError: (err: any) => setErrorMsg(<T id={err.message} />),
 | 
			
		||||
			onSuccess: () => {
 | 
			
		||||
				showSuccess(intl.formatMessage({ id: "notification.stream-saved" }));
 | 
			
		||||
				showObjectSuccess("stream", "saved");
 | 
			
		||||
				remove();
 | 
			
		||||
			},
 | 
			
		||||
			onSettled: () => {
 | 
			
		||||
@@ -72,7 +72,7 @@ const StreamModal = EasyModal.create(({ id, visible, remove }: Props) => {
 | 
			
		||||
						<Form>
 | 
			
		||||
							<Modal.Header closeButton>
 | 
			
		||||
								<Modal.Title>
 | 
			
		||||
									<T id={data?.id ? "stream.edit" : "stream.new"} />
 | 
			
		||||
									<T id={data?.id ? "object.edit" : "object.add"} tData={{ object: "stream" }} />
 | 
			
		||||
								</Modal.Title>
 | 
			
		||||
							</Modal.Header>
 | 
			
		||||
							<Modal.Body className="p-0">
 | 
			
		||||
@@ -180,7 +180,7 @@ const StreamModal = EasyModal.create(({ id, visible, remove }: Props) => {
 | 
			
		||||
																		className="form-label"
 | 
			
		||||
																		htmlFor="forwardingPort"
 | 
			
		||||
																	>
 | 
			
		||||
																		<T id="stream.forward-port" />
 | 
			
		||||
																		<T id="host.forward-port" />
 | 
			
		||||
																	</label>
 | 
			
		||||
																	<input
 | 
			
		||||
																		id="forwardingPort"
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ import { Button, Loading } from "src/components";
 | 
			
		||||
import { useSetUser, useUser } from "src/hooks";
 | 
			
		||||
import { intl, T } from "src/locale";
 | 
			
		||||
import { validateEmail, validateString } from "src/modules/Validations";
 | 
			
		||||
import { showSuccess } from "src/notifications";
 | 
			
		||||
import { showObjectSuccess } from "src/notifications";
 | 
			
		||||
 | 
			
		||||
const showUserModal = (id: number | "me" | "new") => {
 | 
			
		||||
	EasyModal.show(UserModal, { id });
 | 
			
		||||
@@ -48,7 +48,7 @@ const UserModal = EasyModal.create(({ id, visible, remove }: Props) => {
 | 
			
		||||
		setUser(payload, {
 | 
			
		||||
			onError: (err: any) => setErrorMsg(err.message),
 | 
			
		||||
			onSuccess: () => {
 | 
			
		||||
				showSuccess(intl.formatMessage({ id: "notification.user-saved" }));
 | 
			
		||||
				showObjectSuccess("user", "saved");
 | 
			
		||||
				remove();
 | 
			
		||||
			},
 | 
			
		||||
			onSettled: () => {
 | 
			
		||||
@@ -83,7 +83,7 @@ const UserModal = EasyModal.create(({ id, visible, remove }: Props) => {
 | 
			
		||||
						<Form>
 | 
			
		||||
							<Modal.Header closeButton>
 | 
			
		||||
								<Modal.Title>
 | 
			
		||||
									<T id={data?.id ? "user.edit" : "user.new"} />
 | 
			
		||||
									<T id={data?.id ? "object.edit" : "object.add"} tData={{ object: "user" }} />
 | 
			
		||||
								</Modal.Title>
 | 
			
		||||
							</Modal.Header>
 | 
			
		||||
							<Modal.Body>
 | 
			
		||||
@@ -172,7 +172,7 @@ const UserModal = EasyModal.create(({ id, visible, remove }: Props) => {
 | 
			
		||||
								{currentUser && data && currentUser?.id !== data?.id ? (
 | 
			
		||||
									<div className="my-3">
 | 
			
		||||
										<h4 className="py-2">
 | 
			
		||||
											<T id="user.flags.title" />
 | 
			
		||||
											<T id="options" />
 | 
			
		||||
										</h4>
 | 
			
		||||
										<div className="divide-y">
 | 
			
		||||
											<div>
 | 
			
		||||
@@ -227,8 +227,7 @@ const UserModal = EasyModal.create(({ id, visible, remove }: Props) => {
 | 
			
		||||
								</Button>
 | 
			
		||||
								<Button
 | 
			
		||||
									type="submit"
 | 
			
		||||
									actionType="primary"
 | 
			
		||||
									className="ms-auto"
 | 
			
		||||
									className="ms-auto btn-orange"
 | 
			
		||||
									data-bs-dismiss="modal"
 | 
			
		||||
									isLoading={isSubmitting}
 | 
			
		||||
									disabled={isSubmitting}
 | 
			
		||||
 
 | 
			
		||||
@@ -24,4 +24,15 @@ const showError = (message: string) => {
 | 
			
		||||
	});
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export { showSuccess, showError };
 | 
			
		||||
const showObjectSuccess = (obj: string, action: string) => {
 | 
			
		||||
	showSuccess(
 | 
			
		||||
		intl.formatMessage(
 | 
			
		||||
			{
 | 
			
		||||
				id: `notification.object-${action}`,
 | 
			
		||||
			},
 | 
			
		||||
			{ object: intl.formatMessage({ id: obj }) },
 | 
			
		||||
		),
 | 
			
		||||
	);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export { showSuccess, showError, showObjectSuccess };
 | 
			
		||||
 
 | 
			
		||||
@@ -2,10 +2,9 @@ import { IconDotsVertical, IconEdit, IconTrash } from "@tabler/icons-react";
 | 
			
		||||
import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table";
 | 
			
		||||
import { useMemo } from "react";
 | 
			
		||||
import type { AccessList } from "src/api/backend";
 | 
			
		||||
import { GravatarFormatter, ValueWithDateFormatter } from "src/components";
 | 
			
		||||
import { EmptyData, GravatarFormatter, ValueWithDateFormatter } from "src/components";
 | 
			
		||||
import { TableLayout } from "src/components/Table/TableLayout";
 | 
			
		||||
import { intl, T } from "src/locale";
 | 
			
		||||
import Empty from "./Empty";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
	data: AccessList[];
 | 
			
		||||
@@ -36,12 +35,12 @@ export default function Table({ data, isFetching, isFiltered, onEdit, onDelete,
 | 
			
		||||
			columnHelper.accessor((row: any) => row.items, {
 | 
			
		||||
				id: "items",
 | 
			
		||||
				header: intl.formatMessage({ id: "column.authorization" }),
 | 
			
		||||
				cell: (info: any) => <T id="access.auth-count" data={{ count: info.getValue().length }} />,
 | 
			
		||||
				cell: (info: any) => <T id="access-list.auth-count" data={{ count: info.getValue().length }} />,
 | 
			
		||||
			}),
 | 
			
		||||
			columnHelper.accessor((row: any) => row.clients, {
 | 
			
		||||
				id: "clients",
 | 
			
		||||
				header: intl.formatMessage({ id: "column.access" }),
 | 
			
		||||
				cell: (info: any) => <T id="access.access-count" data={{ count: info.getValue().length }} />,
 | 
			
		||||
				cell: (info: any) => <T id="access-list.access-count" data={{ count: info.getValue().length }} />,
 | 
			
		||||
			}),
 | 
			
		||||
			columnHelper.accessor((row: any) => row.satisfyAny, {
 | 
			
		||||
				id: "satisfyAny",
 | 
			
		||||
@@ -50,7 +49,7 @@ export default function Table({ data, isFetching, isFiltered, onEdit, onDelete,
 | 
			
		||||
			}),
 | 
			
		||||
			columnHelper.accessor((row: any) => row.proxyHostCount, {
 | 
			
		||||
				id: "proxyHostCount",
 | 
			
		||||
				header: intl.formatMessage({ id: "proxy-hosts.title" }),
 | 
			
		||||
				header: intl.formatMessage({ id: "proxy-hosts" }),
 | 
			
		||||
				cell: (info: any) => <T id="proxy-hosts.count" data={{ count: info.getValue() }} />,
 | 
			
		||||
			}),
 | 
			
		||||
			columnHelper.display({
 | 
			
		||||
@@ -68,7 +67,11 @@ export default function Table({ data, isFetching, isFiltered, onEdit, onDelete,
 | 
			
		||||
							</button>
 | 
			
		||||
							<div className="dropdown-menu dropdown-menu-end">
 | 
			
		||||
								<span className="dropdown-header">
 | 
			
		||||
									<T id="access.actions-title" data={{ id: info.row.original.id }} />
 | 
			
		||||
									<T
 | 
			
		||||
										id="object.actions-title"
 | 
			
		||||
										tData={{ object: "access-list" }}
 | 
			
		||||
										data={{ id: info.row.original.id }}
 | 
			
		||||
									/>
 | 
			
		||||
								</span>
 | 
			
		||||
								<a
 | 
			
		||||
									className="dropdown-item"
 | 
			
		||||
@@ -119,7 +122,16 @@ export default function Table({ data, isFetching, isFiltered, onEdit, onDelete,
 | 
			
		||||
	return (
 | 
			
		||||
		<TableLayout
 | 
			
		||||
			tableInstance={tableInstance}
 | 
			
		||||
			emptyState={<Empty tableInstance={tableInstance} onNew={onNew} isFiltered={isFiltered} />}
 | 
			
		||||
			emptyState={
 | 
			
		||||
				<EmptyData
 | 
			
		||||
					object="access-list"
 | 
			
		||||
					objects="access-lists"
 | 
			
		||||
					tableInstance={tableInstance}
 | 
			
		||||
					onNew={onNew}
 | 
			
		||||
					isFiltered={isFiltered}
 | 
			
		||||
					color="cyan"
 | 
			
		||||
				/>
 | 
			
		||||
			}
 | 
			
		||||
		/>
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,9 +4,9 @@ import Alert from "react-bootstrap/Alert";
 | 
			
		||||
import { deleteAccessList } from "src/api/backend";
 | 
			
		||||
import { Button, LoadingPage } from "src/components";
 | 
			
		||||
import { useAccessLists } from "src/hooks";
 | 
			
		||||
import { intl, T } from "src/locale";
 | 
			
		||||
import { T } from "src/locale";
 | 
			
		||||
import { showAccessListModal, showDeleteConfirmModal } from "src/modals";
 | 
			
		||||
import { showSuccess } from "src/notifications";
 | 
			
		||||
import { showObjectSuccess } from "src/notifications";
 | 
			
		||||
import Table from "./Table";
 | 
			
		||||
 | 
			
		||||
export default function TableWrapper() {
 | 
			
		||||
@@ -23,7 +23,7 @@ export default function TableWrapper() {
 | 
			
		||||
 | 
			
		||||
	const handleDelete = async (id: number) => {
 | 
			
		||||
		await deleteAccessList(id);
 | 
			
		||||
		showSuccess(intl.formatMessage({ id: "notification.access-deleted" }));
 | 
			
		||||
		showObjectSuccess("access-list", "deleted");
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	let filtered = null;
 | 
			
		||||
@@ -44,7 +44,7 @@ export default function TableWrapper() {
 | 
			
		||||
					<div className="row w-full">
 | 
			
		||||
						<div className="col">
 | 
			
		||||
							<h2 className="mt-1 mb-0">
 | 
			
		||||
								<T id="access.title" />
 | 
			
		||||
								<T id="access-lists" />
 | 
			
		||||
							</h2>
 | 
			
		||||
						</div>
 | 
			
		||||
						{data?.length ? (
 | 
			
		||||
@@ -63,7 +63,7 @@ export default function TableWrapper() {
 | 
			
		||||
										/>
 | 
			
		||||
									</div>
 | 
			
		||||
									<Button size="sm" className="btn-cyan" onClick={() => showAccessListModal("new")}>
 | 
			
		||||
										<T id="access.add" />
 | 
			
		||||
										<T id="object.add" tData={{ object: "access-list" }} />
 | 
			
		||||
									</Button>
 | 
			
		||||
								</div>
 | 
			
		||||
							</div>
 | 
			
		||||
@@ -80,7 +80,7 @@ export default function TableWrapper() {
 | 
			
		||||
							title: "access.delete.title",
 | 
			
		||||
							onConfirm: () => handleDelete(id),
 | 
			
		||||
							invalidations: [["access-lists"], ["access-list", id]],
 | 
			
		||||
							children: <T id="access.delete.content" />,
 | 
			
		||||
							children: <T id="object.delete.content" tData={{ object: "access-list" }} />,
 | 
			
		||||
						})
 | 
			
		||||
					}
 | 
			
		||||
					onNew={() => showAccessListModal("new")}
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ export default function TableWrapper() {
 | 
			
		||||
					<div className="row w-full">
 | 
			
		||||
						<div className="col">
 | 
			
		||||
							<h2 className="mt-1 mb-0">
 | 
			
		||||
								<T id="auditlog.title" />
 | 
			
		||||
								<T id="auditlogs" />
 | 
			
		||||
							</h2>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,64 +0,0 @@
 | 
			
		||||
import type { Table as ReactTable } from "@tanstack/react-table";
 | 
			
		||||
import { T } from "src/locale";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
	tableInstance: ReactTable<any>;
 | 
			
		||||
	onNew?: () => void;
 | 
			
		||||
	onNewCustom?: () => void;
 | 
			
		||||
	isFiltered?: boolean;
 | 
			
		||||
}
 | 
			
		||||
export default function Empty({ tableInstance, onNew, onNewCustom, isFiltered }: Props) {
 | 
			
		||||
	return (
 | 
			
		||||
		<tr>
 | 
			
		||||
			<td colSpan={tableInstance.getVisibleFlatColumns().length}>
 | 
			
		||||
				<div className="text-center my-4">
 | 
			
		||||
					{isFiltered ? (
 | 
			
		||||
						<h2>
 | 
			
		||||
							<T id="empty-search" />
 | 
			
		||||
						</h2>
 | 
			
		||||
					) : (
 | 
			
		||||
						<>
 | 
			
		||||
							<h2>
 | 
			
		||||
								<T id="certificates.empty" />
 | 
			
		||||
							</h2>
 | 
			
		||||
							<p className="text-muted">
 | 
			
		||||
								<T id="empty-subtitle" />
 | 
			
		||||
							</p>
 | 
			
		||||
							<div className="dropdown">
 | 
			
		||||
								<button
 | 
			
		||||
									type="button"
 | 
			
		||||
									className="btn dropdown-toggle btn-pink my-3"
 | 
			
		||||
									data-bs-toggle="dropdown"
 | 
			
		||||
								>
 | 
			
		||||
									<T id="certificates.add" />
 | 
			
		||||
								</button>
 | 
			
		||||
								<div className="dropdown-menu">
 | 
			
		||||
									<a
 | 
			
		||||
										className="dropdown-item"
 | 
			
		||||
										href="#"
 | 
			
		||||
										onClick={(e) => {
 | 
			
		||||
											e.preventDefault();
 | 
			
		||||
											onNew?.();
 | 
			
		||||
										}}
 | 
			
		||||
									>
 | 
			
		||||
										<T id="lets-encrypt" />
 | 
			
		||||
									</a>
 | 
			
		||||
									<a
 | 
			
		||||
										className="dropdown-item"
 | 
			
		||||
										href="#"
 | 
			
		||||
										onClick={(e) => {
 | 
			
		||||
											e.preventDefault();
 | 
			
		||||
											onNewCustom?.();
 | 
			
		||||
										}}
 | 
			
		||||
									>
 | 
			
		||||
										<T id="certificates.custom" />
 | 
			
		||||
									</a>
 | 
			
		||||
								</div>
 | 
			
		||||
							</div>
 | 
			
		||||
						</>
 | 
			
		||||
					)}
 | 
			
		||||
				</div>
 | 
			
		||||
			</td>
 | 
			
		||||
		</tr>
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
@@ -2,10 +2,9 @@ import { IconDotsVertical, IconEdit, IconPower, IconTrash } from "@tabler/icons-
 | 
			
		||||
import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table";
 | 
			
		||||
import { useMemo } from "react";
 | 
			
		||||
import type { Certificate } from "src/api/backend";
 | 
			
		||||
import { DomainsFormatter, GravatarFormatter } from "src/components";
 | 
			
		||||
import { DomainsFormatter, EmptyData, GravatarFormatter } from "src/components";
 | 
			
		||||
import { TableLayout } from "src/components/Table/TableLayout";
 | 
			
		||||
import { intl, T } from "src/locale";
 | 
			
		||||
import Empty from "./Empty";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
	data: Certificate[];
 | 
			
		||||
@@ -69,7 +68,11 @@ export default function Table({ data, isFetching }: Props) {
 | 
			
		||||
							</button>
 | 
			
		||||
							<div className="dropdown-menu dropdown-menu-end">
 | 
			
		||||
								<span className="dropdown-header">
 | 
			
		||||
									<T id="certificates.actions-title" data={{ id: info.row.original.id }} />
 | 
			
		||||
									<T
 | 
			
		||||
										id="object.actions-title"
 | 
			
		||||
										tData={{ object: "certificate" }}
 | 
			
		||||
										data={{ id: info.row.original.id }}
 | 
			
		||||
									/>
 | 
			
		||||
								</span>
 | 
			
		||||
								<a className="dropdown-item" href="#">
 | 
			
		||||
									<IconEdit size={16} />
 | 
			
		||||
@@ -107,5 +110,50 @@ export default function Table({ data, isFetching }: Props) {
 | 
			
		||||
		enableSortingRemoval: false,
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	return <TableLayout tableInstance={tableInstance} emptyState={<Empty tableInstance={tableInstance} />} />;
 | 
			
		||||
	const customAddBtn = (
 | 
			
		||||
		<div className="dropdown">
 | 
			
		||||
			<button type="button" className="btn dropdown-toggle btn-pink my-3" data-bs-toggle="dropdown">
 | 
			
		||||
				<T id="object.add" tData={{ object: "certificate" }} />
 | 
			
		||||
			</button>
 | 
			
		||||
			<div className="dropdown-menu">
 | 
			
		||||
				<a
 | 
			
		||||
					className="dropdown-item"
 | 
			
		||||
					href="#"
 | 
			
		||||
					onClick={(e) => {
 | 
			
		||||
						e.preventDefault();
 | 
			
		||||
						// onNew();
 | 
			
		||||
					}}
 | 
			
		||||
				>
 | 
			
		||||
					<T id="lets-encrypt" />
 | 
			
		||||
				</a>
 | 
			
		||||
				<a
 | 
			
		||||
					className="dropdown-item"
 | 
			
		||||
					href="#"
 | 
			
		||||
					onClick={(e) => {
 | 
			
		||||
						e.preventDefault();
 | 
			
		||||
						// onNewCustom();
 | 
			
		||||
					}}
 | 
			
		||||
				>
 | 
			
		||||
					<T id="certificates.custom" />
 | 
			
		||||
				</a>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	return (
 | 
			
		||||
		<TableLayout
 | 
			
		||||
			tableInstance={tableInstance}
 | 
			
		||||
			emptyState={
 | 
			
		||||
				<EmptyData
 | 
			
		||||
					object="certificate"
 | 
			
		||||
					objects="certificates"
 | 
			
		||||
					tableInstance={tableInstance}
 | 
			
		||||
					// onNew={onNew}
 | 
			
		||||
					// isFiltered={isFiltered}
 | 
			
		||||
					color="pink"
 | 
			
		||||
					customAddBtn={customAddBtn}
 | 
			
		||||
				/>
 | 
			
		||||
			}
 | 
			
		||||
		/>
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -29,7 +29,7 @@ export default function TableWrapper() {
 | 
			
		||||
					<div className="row w-full">
 | 
			
		||||
						<div className="col">
 | 
			
		||||
							<h2 className="mt-1 mb-0">
 | 
			
		||||
								<T id="certificates.title" />
 | 
			
		||||
								<T id="certificates" />
 | 
			
		||||
							</h2>
 | 
			
		||||
						</div>
 | 
			
		||||
						<div className="col-md-auto col-sm-12">
 | 
			
		||||
@@ -51,7 +51,7 @@ export default function TableWrapper() {
 | 
			
		||||
										className="btn btn-sm dropdown-toggle btn-pink mt-1"
 | 
			
		||||
										data-bs-toggle="dropdown"
 | 
			
		||||
									>
 | 
			
		||||
										<T id="certificates.add" />
 | 
			
		||||
										<T id="object.add" tData={{ object: "certificate" }} />
 | 
			
		||||
									</button>
 | 
			
		||||
									<div className="dropdown-menu">
 | 
			
		||||
										<a className="dropdown-item" href="#">
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ const Dashboard = () => {
 | 
			
		||||
	return (
 | 
			
		||||
		<div>
 | 
			
		||||
			<h2>
 | 
			
		||||
				<T id="dashboard.title" />
 | 
			
		||||
				<T id="dashboard" />
 | 
			
		||||
			</h2>
 | 
			
		||||
			<div className="row row-deck row-cards">
 | 
			
		||||
				<div className="col-12 my-4">
 | 
			
		||||
@@ -127,6 +127,7 @@ const Dashboard = () => {
 | 
			
		||||
- use syntax highligting for audit logs json output
 | 
			
		||||
- double check output of access field selection on proxy host dialog, after access lists are completed
 | 
			
		||||
- proxy host custom locations dialog
 | 
			
		||||
- check permissions in all places
 | 
			
		||||
 | 
			
		||||
More for api, then implement here:
 | 
			
		||||
- Properly implement refresh tokens
 | 
			
		||||
 
 | 
			
		||||
@@ -1,36 +0,0 @@
 | 
			
		||||
import type { Table as ReactTable } from "@tanstack/react-table";
 | 
			
		||||
import { Button } from "src/components";
 | 
			
		||||
import { T } from "src/locale";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
	tableInstance: ReactTable<any>;
 | 
			
		||||
	onNew?: () => void;
 | 
			
		||||
	isFiltered?: boolean;
 | 
			
		||||
}
 | 
			
		||||
export default function Empty({ tableInstance, onNew, isFiltered }: Props) {
 | 
			
		||||
	return (
 | 
			
		||||
		<tr>
 | 
			
		||||
			<td colSpan={tableInstance.getVisibleFlatColumns().length}>
 | 
			
		||||
				<div className="text-center my-4">
 | 
			
		||||
					{isFiltered ? (
 | 
			
		||||
						<h2>
 | 
			
		||||
							<T id="empty-search" />
 | 
			
		||||
						</h2>
 | 
			
		||||
					) : (
 | 
			
		||||
						<>
 | 
			
		||||
							<h2>
 | 
			
		||||
								<T id="dead-hosts.empty" />
 | 
			
		||||
							</h2>
 | 
			
		||||
							<p className="text-muted">
 | 
			
		||||
								<T id="empty-subtitle" />
 | 
			
		||||
							</p>
 | 
			
		||||
							<Button className="btn-red my-3" onClick={onNew}>
 | 
			
		||||
								<T id="dead-hosts.add" />
 | 
			
		||||
							</Button>
 | 
			
		||||
						</>
 | 
			
		||||
					)}
 | 
			
		||||
				</div>
 | 
			
		||||
			</td>
 | 
			
		||||
		</tr>
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
@@ -2,10 +2,9 @@ import { IconDotsVertical, IconEdit, IconPower, IconTrash } from "@tabler/icons-
 | 
			
		||||
import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table";
 | 
			
		||||
import { useMemo } from "react";
 | 
			
		||||
import type { DeadHost } from "src/api/backend";
 | 
			
		||||
import { CertificateFormatter, DomainsFormatter, GravatarFormatter, StatusFormatter } from "src/components";
 | 
			
		||||
import { CertificateFormatter, DomainsFormatter, EmptyData, GravatarFormatter, StatusFormatter } from "src/components";
 | 
			
		||||
import { TableLayout } from "src/components/Table/TableLayout";
 | 
			
		||||
import { intl, T } from "src/locale";
 | 
			
		||||
import Empty from "./Empty";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
	data: DeadHost[];
 | 
			
		||||
@@ -67,7 +66,11 @@ export default function Table({ data, isFetching, onEdit, onDelete, onDisableTog
 | 
			
		||||
							</button>
 | 
			
		||||
							<div className="dropdown-menu dropdown-menu-end">
 | 
			
		||||
								<span className="dropdown-header">
 | 
			
		||||
									<T id="dead-hosts.actions-title" data={{ id: info.row.original.id }} />
 | 
			
		||||
									<T
 | 
			
		||||
										id="object.actions-title"
 | 
			
		||||
										tData={{ object: "dead-host" }}
 | 
			
		||||
										data={{ id: info.row.original.id }}
 | 
			
		||||
									/>
 | 
			
		||||
								</span>
 | 
			
		||||
								<a
 | 
			
		||||
									className="dropdown-item"
 | 
			
		||||
@@ -129,7 +132,16 @@ export default function Table({ data, isFetching, onEdit, onDelete, onDisableTog
 | 
			
		||||
	return (
 | 
			
		||||
		<TableLayout
 | 
			
		||||
			tableInstance={tableInstance}
 | 
			
		||||
			emptyState={<Empty tableInstance={tableInstance} onNew={onNew} isFiltered={isFiltered} />}
 | 
			
		||||
			emptyState={
 | 
			
		||||
				<EmptyData
 | 
			
		||||
					object="dead-host"
 | 
			
		||||
					objects="dead-hosts"
 | 
			
		||||
					tableInstance={tableInstance}
 | 
			
		||||
					onNew={onNew}
 | 
			
		||||
					isFiltered={isFiltered}
 | 
			
		||||
					color="red"
 | 
			
		||||
				/>
 | 
			
		||||
			}
 | 
			
		||||
		/>
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,9 +5,9 @@ import Alert from "react-bootstrap/Alert";
 | 
			
		||||
import { deleteDeadHost, toggleDeadHost } from "src/api/backend";
 | 
			
		||||
import { Button, LoadingPage } from "src/components";
 | 
			
		||||
import { useDeadHosts } from "src/hooks";
 | 
			
		||||
import { intl, T } from "src/locale";
 | 
			
		||||
import { T } from "src/locale";
 | 
			
		||||
import { showDeadHostModal, showDeleteConfirmModal } from "src/modals";
 | 
			
		||||
import { showSuccess } from "src/notifications";
 | 
			
		||||
import { showObjectSuccess } from "src/notifications";
 | 
			
		||||
import Table from "./Table";
 | 
			
		||||
 | 
			
		||||
export default function TableWrapper() {
 | 
			
		||||
@@ -25,14 +25,14 @@ export default function TableWrapper() {
 | 
			
		||||
 | 
			
		||||
	const handleDelete = async (id: number) => {
 | 
			
		||||
		await deleteDeadHost(id);
 | 
			
		||||
		showSuccess(intl.formatMessage({ id: "notification.host-deleted" }));
 | 
			
		||||
		showObjectSuccess("dead-host", "deleted");
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const handleDisableToggle = async (id: number, enabled: boolean) => {
 | 
			
		||||
		await toggleDeadHost(id, enabled);
 | 
			
		||||
		queryClient.invalidateQueries({ queryKey: ["dead-hosts"] });
 | 
			
		||||
		queryClient.invalidateQueries({ queryKey: ["dead-host", id] });
 | 
			
		||||
		showSuccess(intl.formatMessage({ id: enabled ? "notification.host-enabled" : "notification.host-disabled" }));
 | 
			
		||||
		showObjectSuccess("dead-host", enabled ? "enabled" : "disabled");
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	let filtered = null;
 | 
			
		||||
@@ -53,7 +53,7 @@ export default function TableWrapper() {
 | 
			
		||||
					<div className="row w-full">
 | 
			
		||||
						<div className="col">
 | 
			
		||||
							<h2 className="mt-1 mb-0">
 | 
			
		||||
								<T id="dead-hosts.title" />
 | 
			
		||||
								<T id="dead-hosts" />
 | 
			
		||||
							</h2>
 | 
			
		||||
						</div>
 | 
			
		||||
						{data?.length ? (
 | 
			
		||||
@@ -72,7 +72,7 @@ export default function TableWrapper() {
 | 
			
		||||
										/>
 | 
			
		||||
									</div>
 | 
			
		||||
									<Button size="sm" className="btn-red" onClick={() => showDeadHostModal("new")}>
 | 
			
		||||
										<T id="dead-hosts.add" />
 | 
			
		||||
										<T id="object.add" tData={{ object: "dead-host" }} />
 | 
			
		||||
									</Button>
 | 
			
		||||
								</div>
 | 
			
		||||
							</div>
 | 
			
		||||
@@ -86,10 +86,10 @@ export default function TableWrapper() {
 | 
			
		||||
					onEdit={(id: number) => showDeadHostModal(id)}
 | 
			
		||||
					onDelete={(id: number) =>
 | 
			
		||||
						showDeleteConfirmModal({
 | 
			
		||||
							title: "dead-host.delete.title",
 | 
			
		||||
							title: <T id="object.delete" tData={{ object: "dead-host" }} />,
 | 
			
		||||
							onConfirm: () => handleDelete(id),
 | 
			
		||||
							invalidations: [["dead-hosts"], ["dead-host", id]],
 | 
			
		||||
							children: <T id="dead-host.delete.content" />,
 | 
			
		||||
							children: <T id="object.delete.content" tData={{ object: "dead-host" }} />,
 | 
			
		||||
						})
 | 
			
		||||
					}
 | 
			
		||||
					onDisableToggle={handleDisableToggle}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,36 +0,0 @@
 | 
			
		||||
import type { Table as ReactTable } from "@tanstack/react-table";
 | 
			
		||||
import { Button } from "src/components";
 | 
			
		||||
import { T } from "src/locale";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
	tableInstance: ReactTable<any>;
 | 
			
		||||
	onNew?: () => void;
 | 
			
		||||
	isFiltered?: boolean;
 | 
			
		||||
}
 | 
			
		||||
export default function Empty({ tableInstance, onNew, isFiltered }: Props) {
 | 
			
		||||
	return (
 | 
			
		||||
		<tr>
 | 
			
		||||
			<td colSpan={tableInstance.getVisibleFlatColumns().length}>
 | 
			
		||||
				<div className="text-center my-4">
 | 
			
		||||
					{isFiltered ? (
 | 
			
		||||
						<h2>
 | 
			
		||||
							<T id="empty-search" />
 | 
			
		||||
						</h2>
 | 
			
		||||
					) : (
 | 
			
		||||
						<>
 | 
			
		||||
							<h2>
 | 
			
		||||
								<T id="proxy-hosts.empty" />
 | 
			
		||||
							</h2>
 | 
			
		||||
							<p className="text-muted">
 | 
			
		||||
								<T id="empty-subtitle" />
 | 
			
		||||
							</p>
 | 
			
		||||
							<Button className="btn-lime my-3" onClick={onNew}>
 | 
			
		||||
								<T id="proxy-hosts.add" />
 | 
			
		||||
							</Button>
 | 
			
		||||
						</>
 | 
			
		||||
					)}
 | 
			
		||||
				</div>
 | 
			
		||||
			</td>
 | 
			
		||||
		</tr>
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
@@ -2,10 +2,16 @@ import { IconDotsVertical, IconEdit, IconPower, IconTrash } from "@tabler/icons-
 | 
			
		||||
import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table";
 | 
			
		||||
import { useMemo } from "react";
 | 
			
		||||
import type { ProxyHost } from "src/api/backend";
 | 
			
		||||
import { CertificateFormatter, DomainsFormatter, GravatarFormatter, StatusFormatter } from "src/components";
 | 
			
		||||
import {
 | 
			
		||||
	AccessListFormatter,
 | 
			
		||||
	CertificateFormatter,
 | 
			
		||||
	DomainsFormatter,
 | 
			
		||||
	EmptyData,
 | 
			
		||||
	GravatarFormatter,
 | 
			
		||||
	StatusFormatter,
 | 
			
		||||
} from "src/components";
 | 
			
		||||
import { TableLayout } from "src/components/Table/TableLayout";
 | 
			
		||||
import { intl, T } from "src/locale";
 | 
			
		||||
import Empty from "./Empty";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
	data: ProxyHost[];
 | 
			
		||||
@@ -53,12 +59,11 @@ export default function Table({ data, isFetching, onEdit, onDelete, onDisableTog
 | 
			
		||||
					return <CertificateFormatter certificate={info.getValue()} />;
 | 
			
		||||
				},
 | 
			
		||||
			}),
 | 
			
		||||
			// TODO: formatter for access list
 | 
			
		||||
			columnHelper.accessor((row: any) => row.access, {
 | 
			
		||||
			columnHelper.accessor((row: any) => row.accessList, {
 | 
			
		||||
				id: "accessList",
 | 
			
		||||
				header: intl.formatMessage({ id: "column.access" }),
 | 
			
		||||
				cell: (info: any) => {
 | 
			
		||||
					return info.getValue();
 | 
			
		||||
					return <AccessListFormatter access={info.getValue()} />;
 | 
			
		||||
				},
 | 
			
		||||
			}),
 | 
			
		||||
			columnHelper.accessor((row: any) => row.enabled, {
 | 
			
		||||
@@ -83,7 +88,11 @@ export default function Table({ data, isFetching, onEdit, onDelete, onDisableTog
 | 
			
		||||
							</button>
 | 
			
		||||
							<div className="dropdown-menu dropdown-menu-end">
 | 
			
		||||
								<span className="dropdown-header">
 | 
			
		||||
									<T id="proxy-hosts.actions-title" data={{ id: info.row.original.id }} />
 | 
			
		||||
									<T
 | 
			
		||||
										id="object.actions-title"
 | 
			
		||||
										tData={{ object: "proxy-host" }}
 | 
			
		||||
										data={{ id: info.row.original.id }}
 | 
			
		||||
									/>
 | 
			
		||||
								</span>
 | 
			
		||||
								<a
 | 
			
		||||
									className="dropdown-item"
 | 
			
		||||
@@ -145,7 +154,16 @@ export default function Table({ data, isFetching, onEdit, onDelete, onDisableTog
 | 
			
		||||
	return (
 | 
			
		||||
		<TableLayout
 | 
			
		||||
			tableInstance={tableInstance}
 | 
			
		||||
			emptyState={<Empty tableInstance={tableInstance} onNew={onNew} isFiltered={isFiltered} />}
 | 
			
		||||
			emptyState={
 | 
			
		||||
				<EmptyData
 | 
			
		||||
					object="proxy-host"
 | 
			
		||||
					objects="proxy-hosts"
 | 
			
		||||
					tableInstance={tableInstance}
 | 
			
		||||
					onNew={onNew}
 | 
			
		||||
					isFiltered={isFiltered}
 | 
			
		||||
					color="lime"
 | 
			
		||||
				/>
 | 
			
		||||
			}
 | 
			
		||||
		/>
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,9 +5,9 @@ import Alert from "react-bootstrap/Alert";
 | 
			
		||||
import { deleteProxyHost, toggleProxyHost } from "src/api/backend";
 | 
			
		||||
import { Button, LoadingPage } from "src/components";
 | 
			
		||||
import { useProxyHosts } from "src/hooks";
 | 
			
		||||
import { intl, T } from "src/locale";
 | 
			
		||||
import { T } from "src/locale";
 | 
			
		||||
import { showDeleteConfirmModal, showProxyHostModal } from "src/modals";
 | 
			
		||||
import { showSuccess } from "src/notifications";
 | 
			
		||||
import { showObjectSuccess } from "src/notifications";
 | 
			
		||||
import Table from "./Table";
 | 
			
		||||
 | 
			
		||||
export default function TableWrapper() {
 | 
			
		||||
@@ -25,14 +25,14 @@ export default function TableWrapper() {
 | 
			
		||||
 | 
			
		||||
	const handleDelete = async (id: number) => {
 | 
			
		||||
		await deleteProxyHost(id);
 | 
			
		||||
		showSuccess(intl.formatMessage({ id: "notification.host-deleted" }));
 | 
			
		||||
		showObjectSuccess("proxy-host", "deleted");
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const handleDisableToggle = async (id: number, enabled: boolean) => {
 | 
			
		||||
		await toggleProxyHost(id, enabled);
 | 
			
		||||
		queryClient.invalidateQueries({ queryKey: ["proxy-hosts"] });
 | 
			
		||||
		queryClient.invalidateQueries({ queryKey: ["proxy-host", id] });
 | 
			
		||||
		showSuccess(intl.formatMessage({ id: enabled ? "notification.host-enabled" : "notification.host-disabled" }));
 | 
			
		||||
		showObjectSuccess("proxy-host", enabled ? "enabled" : "disabled");
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	let filtered = null;
 | 
			
		||||
@@ -57,7 +57,7 @@ export default function TableWrapper() {
 | 
			
		||||
					<div className="row w-full">
 | 
			
		||||
						<div className="col">
 | 
			
		||||
							<h2 className="mt-1 mb-0">
 | 
			
		||||
								<T id="proxy-hosts.title" />
 | 
			
		||||
								<T id="proxy-hosts" />
 | 
			
		||||
							</h2>
 | 
			
		||||
						</div>
 | 
			
		||||
						{data?.length ? (
 | 
			
		||||
@@ -76,7 +76,7 @@ export default function TableWrapper() {
 | 
			
		||||
										/>
 | 
			
		||||
									</div>
 | 
			
		||||
									<Button size="sm" className="btn-lime" onClick={() => showProxyHostModal("new")}>
 | 
			
		||||
										<T id="proxy-hosts.add" />
 | 
			
		||||
										<T id="object.add" tData={{ object: "proxy-host" }} />
 | 
			
		||||
									</Button>
 | 
			
		||||
								</div>
 | 
			
		||||
							</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,36 +0,0 @@
 | 
			
		||||
import type { Table as ReactTable } from "@tanstack/react-table";
 | 
			
		||||
import { Button } from "src/components";
 | 
			
		||||
import { T } from "src/locale";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
	tableInstance: ReactTable<any>;
 | 
			
		||||
	onNew?: () => void;
 | 
			
		||||
	isFiltered?: boolean;
 | 
			
		||||
}
 | 
			
		||||
export default function Empty({ tableInstance, onNew, isFiltered }: Props) {
 | 
			
		||||
	return (
 | 
			
		||||
		<tr>
 | 
			
		||||
			<td colSpan={tableInstance.getVisibleFlatColumns().length}>
 | 
			
		||||
				<div className="text-center my-4">
 | 
			
		||||
					{isFiltered ? (
 | 
			
		||||
						<h2>
 | 
			
		||||
							<T id="empty-search" />
 | 
			
		||||
						</h2>
 | 
			
		||||
					) : (
 | 
			
		||||
						<>
 | 
			
		||||
							<h2>
 | 
			
		||||
								<T id="redirection-hosts.empty" />
 | 
			
		||||
							</h2>
 | 
			
		||||
							<p className="text-muted">
 | 
			
		||||
								<T id="empty-subtitle" />
 | 
			
		||||
							</p>
 | 
			
		||||
							<Button className="btn-yellow my-3" onClick={onNew}>
 | 
			
		||||
								<T id="redirection-hosts.add" />
 | 
			
		||||
							</Button>
 | 
			
		||||
						</>
 | 
			
		||||
					)}
 | 
			
		||||
				</div>
 | 
			
		||||
			</td>
 | 
			
		||||
		</tr>
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
@@ -2,10 +2,9 @@ import { IconDotsVertical, IconEdit, IconPower, IconTrash } from "@tabler/icons-
 | 
			
		||||
import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table";
 | 
			
		||||
import { useMemo } from "react";
 | 
			
		||||
import type { RedirectionHost } from "src/api/backend";
 | 
			
		||||
import { CertificateFormatter, DomainsFormatter, GravatarFormatter, StatusFormatter } from "src/components";
 | 
			
		||||
import { CertificateFormatter, DomainsFormatter, EmptyData, GravatarFormatter, StatusFormatter } from "src/components";
 | 
			
		||||
import { TableLayout } from "src/components/Table/TableLayout";
 | 
			
		||||
import { intl, T } from "src/locale";
 | 
			
		||||
import Empty from "./Empty";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
	data: RedirectionHost[];
 | 
			
		||||
@@ -88,7 +87,11 @@ export default function Table({ data, isFetching, onEdit, onDelete, onDisableTog
 | 
			
		||||
							</button>
 | 
			
		||||
							<div className="dropdown-menu dropdown-menu-end">
 | 
			
		||||
								<span className="dropdown-header">
 | 
			
		||||
									<T id="redirection-hosts.actions-title" data={{ id: info.row.original.id }} />
 | 
			
		||||
									<T
 | 
			
		||||
										id="object.actions-title"
 | 
			
		||||
										tData={{ object: "redirection-host" }}
 | 
			
		||||
										data={{ id: info.row.original.id }}
 | 
			
		||||
									/>
 | 
			
		||||
								</span>
 | 
			
		||||
								<a
 | 
			
		||||
									className="dropdown-item"
 | 
			
		||||
@@ -150,7 +153,16 @@ export default function Table({ data, isFetching, onEdit, onDelete, onDisableTog
 | 
			
		||||
	return (
 | 
			
		||||
		<TableLayout
 | 
			
		||||
			tableInstance={tableInstance}
 | 
			
		||||
			emptyState={<Empty tableInstance={tableInstance} onNew={onNew} isFiltered={isFiltered} />}
 | 
			
		||||
			emptyState={
 | 
			
		||||
				<EmptyData
 | 
			
		||||
					object="redirection-host"
 | 
			
		||||
					objects="redirection-hosts"
 | 
			
		||||
					tableInstance={tableInstance}
 | 
			
		||||
					onNew={onNew}
 | 
			
		||||
					isFiltered={isFiltered}
 | 
			
		||||
					color="yellow"
 | 
			
		||||
				/>
 | 
			
		||||
			}
 | 
			
		||||
		/>
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,9 +5,9 @@ import Alert from "react-bootstrap/Alert";
 | 
			
		||||
import { deleteRedirectionHost, toggleRedirectionHost } from "src/api/backend";
 | 
			
		||||
import { Button, LoadingPage } from "src/components";
 | 
			
		||||
import { useRedirectionHosts } from "src/hooks";
 | 
			
		||||
import { intl, T } from "src/locale";
 | 
			
		||||
import { T } from "src/locale";
 | 
			
		||||
import { showDeleteConfirmModal, showRedirectionHostModal } from "src/modals";
 | 
			
		||||
import { showSuccess } from "src/notifications";
 | 
			
		||||
import { showObjectSuccess } from "src/notifications";
 | 
			
		||||
import Table from "./Table";
 | 
			
		||||
 | 
			
		||||
export default function TableWrapper() {
 | 
			
		||||
@@ -25,14 +25,14 @@ export default function TableWrapper() {
 | 
			
		||||
 | 
			
		||||
	const handleDelete = async (id: number) => {
 | 
			
		||||
		await deleteRedirectionHost(id);
 | 
			
		||||
		showSuccess(intl.formatMessage({ id: "notification.host-deleted" }));
 | 
			
		||||
		showObjectSuccess("redirection-host", "deleted");
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const handleDisableToggle = async (id: number, enabled: boolean) => {
 | 
			
		||||
		await toggleRedirectionHost(id, enabled);
 | 
			
		||||
		queryClient.invalidateQueries({ queryKey: ["redirection-hosts"] });
 | 
			
		||||
		queryClient.invalidateQueries({ queryKey: ["redirection-host", id] });
 | 
			
		||||
		showSuccess(intl.formatMessage({ id: enabled ? "notification.host-enabled" : "notification.host-disabled" }));
 | 
			
		||||
		showObjectSuccess("redirection-host", enabled ? "enabled" : "disabled");
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	let filtered = null;
 | 
			
		||||
@@ -56,7 +56,7 @@ export default function TableWrapper() {
 | 
			
		||||
					<div className="row w-full">
 | 
			
		||||
						<div className="col">
 | 
			
		||||
							<h2 className="mt-1 mb-0">
 | 
			
		||||
								<T id="redirection-hosts.title" />
 | 
			
		||||
								<T id="redirection-hosts" />
 | 
			
		||||
							</h2>
 | 
			
		||||
						</div>
 | 
			
		||||
						{data?.length ? (
 | 
			
		||||
@@ -79,7 +79,7 @@ export default function TableWrapper() {
 | 
			
		||||
										className="btn-yellow"
 | 
			
		||||
										onClick={() => showRedirectionHostModal("new")}
 | 
			
		||||
									>
 | 
			
		||||
										<T id="redirection-hosts.add" />
 | 
			
		||||
										<T id="object.add" tData={{ object: "redirection-host" }} />
 | 
			
		||||
									</Button>
 | 
			
		||||
								</div>
 | 
			
		||||
							</div>
 | 
			
		||||
@@ -93,10 +93,10 @@ export default function TableWrapper() {
 | 
			
		||||
					onEdit={(id: number) => showRedirectionHostModal(id)}
 | 
			
		||||
					onDelete={(id: number) =>
 | 
			
		||||
						showDeleteConfirmModal({
 | 
			
		||||
							title: "redirection-host.delete.title",
 | 
			
		||||
							title: <T id="object.delete" tData={{ object: "redirection-host" }} />,
 | 
			
		||||
							onConfirm: () => handleDelete(id),
 | 
			
		||||
							invalidations: [["redirection-hosts"], ["redirection-host", id]],
 | 
			
		||||
							children: <T id="redirection-host.delete.content" />,
 | 
			
		||||
							children: <T id="object.delete.content" tData={{ object: "redirection-host" }} />,
 | 
			
		||||
						})
 | 
			
		||||
					}
 | 
			
		||||
					onDisableToggle={handleDisableToggle}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,36 +0,0 @@
 | 
			
		||||
import type { Table as ReactTable } from "@tanstack/react-table";
 | 
			
		||||
import { Button } from "src/components";
 | 
			
		||||
import { T } from "src/locale";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
	tableInstance: ReactTable<any>;
 | 
			
		||||
	onNew?: () => void;
 | 
			
		||||
	isFiltered?: boolean;
 | 
			
		||||
}
 | 
			
		||||
export default function Empty({ tableInstance, onNew, isFiltered }: Props) {
 | 
			
		||||
	return (
 | 
			
		||||
		<tr>
 | 
			
		||||
			<td colSpan={tableInstance.getVisibleFlatColumns().length}>
 | 
			
		||||
				<div className="text-center my-4">
 | 
			
		||||
					{isFiltered ? (
 | 
			
		||||
						<h2>
 | 
			
		||||
							<T id="empty-search" />
 | 
			
		||||
						</h2>
 | 
			
		||||
					) : (
 | 
			
		||||
						<>
 | 
			
		||||
							<h2>
 | 
			
		||||
								<T id="streams.empty" />
 | 
			
		||||
							</h2>
 | 
			
		||||
							<p className="text-muted">
 | 
			
		||||
								<T id="empty-subtitle" />
 | 
			
		||||
							</p>
 | 
			
		||||
							<Button className="btn-blue my-3" onClick={onNew}>
 | 
			
		||||
								<T id="streams.add" />
 | 
			
		||||
							</Button>
 | 
			
		||||
						</>
 | 
			
		||||
					)}
 | 
			
		||||
				</div>
 | 
			
		||||
			</td>
 | 
			
		||||
		</tr>
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
@@ -2,10 +2,15 @@ import { IconDotsVertical, IconEdit, IconPower, IconTrash } from "@tabler/icons-
 | 
			
		||||
import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table";
 | 
			
		||||
import { useMemo } from "react";
 | 
			
		||||
import type { Stream } from "src/api/backend";
 | 
			
		||||
import { CertificateFormatter, GravatarFormatter, StatusFormatter, ValueWithDateFormatter } from "src/components";
 | 
			
		||||
import {
 | 
			
		||||
	CertificateFormatter,
 | 
			
		||||
	EmptyData,
 | 
			
		||||
	GravatarFormatter,
 | 
			
		||||
	StatusFormatter,
 | 
			
		||||
	ValueWithDateFormatter,
 | 
			
		||||
} from "src/components";
 | 
			
		||||
import { TableLayout } from "src/components/Table/TableLayout";
 | 
			
		||||
import { intl, T } from "src/locale";
 | 
			
		||||
import Empty from "./Empty";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
	data: Stream[];
 | 
			
		||||
@@ -96,7 +101,11 @@ export default function Table({ data, isFetching, isFiltered, onEdit, onDelete,
 | 
			
		||||
							</button>
 | 
			
		||||
							<div className="dropdown-menu dropdown-menu-end">
 | 
			
		||||
								<span className="dropdown-header">
 | 
			
		||||
									<T id="streams.actions-title" data={{ id: info.row.original.id }} />
 | 
			
		||||
									<T
 | 
			
		||||
										id="object.actions-title"
 | 
			
		||||
										tData={{ object: "stream" }}
 | 
			
		||||
										data={{ id: info.row.original.id }}
 | 
			
		||||
									/>
 | 
			
		||||
								</span>
 | 
			
		||||
								<a
 | 
			
		||||
									className="dropdown-item"
 | 
			
		||||
@@ -158,7 +167,16 @@ export default function Table({ data, isFetching, isFiltered, onEdit, onDelete,
 | 
			
		||||
	return (
 | 
			
		||||
		<TableLayout
 | 
			
		||||
			tableInstance={tableInstance}
 | 
			
		||||
			emptyState={<Empty tableInstance={tableInstance} onNew={onNew} isFiltered={isFiltered} />}
 | 
			
		||||
			emptyState={
 | 
			
		||||
				<EmptyData
 | 
			
		||||
					object="stream"
 | 
			
		||||
					objects="streams"
 | 
			
		||||
					tableInstance={tableInstance}
 | 
			
		||||
					onNew={onNew}
 | 
			
		||||
					isFiltered={isFiltered}
 | 
			
		||||
					color="blue"
 | 
			
		||||
				/>
 | 
			
		||||
			}
 | 
			
		||||
		/>
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,9 +5,9 @@ import Alert from "react-bootstrap/Alert";
 | 
			
		||||
import { deleteStream, toggleStream } from "src/api/backend";
 | 
			
		||||
import { Button, LoadingPage } from "src/components";
 | 
			
		||||
import { useStreams } from "src/hooks";
 | 
			
		||||
import { intl, T } from "src/locale";
 | 
			
		||||
import { T } from "src/locale";
 | 
			
		||||
import { showDeleteConfirmModal, showStreamModal } from "src/modals";
 | 
			
		||||
import { showSuccess } from "src/notifications";
 | 
			
		||||
import { showObjectSuccess } from "src/notifications";
 | 
			
		||||
import Table from "./Table";
 | 
			
		||||
 | 
			
		||||
export default function TableWrapper() {
 | 
			
		||||
@@ -26,16 +26,14 @@ export default function TableWrapper() {
 | 
			
		||||
 | 
			
		||||
	const handleDelete = async (id: number) => {
 | 
			
		||||
		await deleteStream(id);
 | 
			
		||||
		showSuccess(intl.formatMessage({ id: "notification.stream-deleted" }));
 | 
			
		||||
		showObjectSuccess("stream", "deleted");
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const handleDisableToggle = async (id: number, enabled: boolean) => {
 | 
			
		||||
		await toggleStream(id, enabled);
 | 
			
		||||
		queryClient.invalidateQueries({ queryKey: ["streams"] });
 | 
			
		||||
		queryClient.invalidateQueries({ queryKey: ["stream", id] });
 | 
			
		||||
		showSuccess(
 | 
			
		||||
			intl.formatMessage({ id: enabled ? "notification.stream-enabled" : "notification.stream-disabled" }),
 | 
			
		||||
		);
 | 
			
		||||
		showObjectSuccess("stream", enabled ? "enabled" : "disabled");
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	let filtered = null;
 | 
			
		||||
@@ -60,7 +58,7 @@ export default function TableWrapper() {
 | 
			
		||||
					<div className="row w-full">
 | 
			
		||||
						<div className="col">
 | 
			
		||||
							<h2 className="mt-1 mb-0">
 | 
			
		||||
								<T id="streams.title" />
 | 
			
		||||
								<T id="streams" />
 | 
			
		||||
							</h2>
 | 
			
		||||
						</div>
 | 
			
		||||
						{data?.length ? (
 | 
			
		||||
@@ -79,7 +77,7 @@ export default function TableWrapper() {
 | 
			
		||||
										/>
 | 
			
		||||
									</div>
 | 
			
		||||
									<Button size="sm" className="btn-blue" onClick={() => showStreamModal("new")}>
 | 
			
		||||
										<T id="streams.add" />
 | 
			
		||||
										<T id="object.add" tData={{ object: "stream" }} />
 | 
			
		||||
									</Button>
 | 
			
		||||
								</div>
 | 
			
		||||
							</div>
 | 
			
		||||
@@ -93,10 +91,10 @@ export default function TableWrapper() {
 | 
			
		||||
					onEdit={(id: number) => showStreamModal(id)}
 | 
			
		||||
					onDelete={(id: number) =>
 | 
			
		||||
						showDeleteConfirmModal({
 | 
			
		||||
							title: "stream.delete.title",
 | 
			
		||||
							title: <T id="object.delete" tData={{ object: "stream" }} />,
 | 
			
		||||
							onConfirm: () => handleDelete(id),
 | 
			
		||||
							invalidations: [["streams"], ["stream", id]],
 | 
			
		||||
							children: <T id="stream.delete.content" />,
 | 
			
		||||
							children: <T id="object.delete.content" tData={{ object: "stream" }} />,
 | 
			
		||||
						})
 | 
			
		||||
					}
 | 
			
		||||
					onDisableToggle={handleDisableToggle}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ export default function SettingTable() {
 | 
			
		||||
				<div className="card-header">
 | 
			
		||||
					<div className="row w-full">
 | 
			
		||||
						<h2 className="mt-1 mb-0">
 | 
			
		||||
							<T id="settings.title" />
 | 
			
		||||
							<T id="settings" />
 | 
			
		||||
						</h2>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,36 +0,0 @@
 | 
			
		||||
import type { Table as ReactTable } from "@tanstack/react-table";
 | 
			
		||||
import { Button } from "src/components";
 | 
			
		||||
import { T } from "src/locale";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
	tableInstance: ReactTable<any>;
 | 
			
		||||
	onNewUser?: () => void;
 | 
			
		||||
	isFiltered?: boolean;
 | 
			
		||||
}
 | 
			
		||||
export default function Empty({ tableInstance, onNewUser, isFiltered }: Props) {
 | 
			
		||||
	return (
 | 
			
		||||
		<tr>
 | 
			
		||||
			<td colSpan={tableInstance.getVisibleFlatColumns().length}>
 | 
			
		||||
				<div className="text-center my-4">
 | 
			
		||||
					{isFiltered ? (
 | 
			
		||||
						<h2>
 | 
			
		||||
							<T id="empty-search" />
 | 
			
		||||
						</h2>
 | 
			
		||||
					) : (
 | 
			
		||||
						<>
 | 
			
		||||
							<h2>
 | 
			
		||||
								<T id="users.empty" />
 | 
			
		||||
							</h2>
 | 
			
		||||
							<p className="text-muted">
 | 
			
		||||
								<T id="empty-subtitle" />
 | 
			
		||||
							</p>
 | 
			
		||||
							<Button className="btn-orange my-3" onClick={onNewUser}>
 | 
			
		||||
								<T id="users.add" />
 | 
			
		||||
							</Button>
 | 
			
		||||
						</>
 | 
			
		||||
					)}
 | 
			
		||||
				</div>
 | 
			
		||||
			</td>
 | 
			
		||||
		</tr>
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
@@ -4,6 +4,7 @@ import { useMemo } from "react";
 | 
			
		||||
import type { User } from "src/api/backend";
 | 
			
		||||
import {
 | 
			
		||||
	EmailFormatter,
 | 
			
		||||
	EmptyData,
 | 
			
		||||
	EnabledFormatter,
 | 
			
		||||
	GravatarFormatter,
 | 
			
		||||
	RolesFormatter,
 | 
			
		||||
@@ -11,7 +12,6 @@ import {
 | 
			
		||||
} from "src/components";
 | 
			
		||||
import { TableLayout } from "src/components/Table/TableLayout";
 | 
			
		||||
import { intl, T } from "src/locale";
 | 
			
		||||
import Empty from "./Empty";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
	data: User[];
 | 
			
		||||
@@ -101,7 +101,11 @@ export default function Table({
 | 
			
		||||
							</button>
 | 
			
		||||
							<div className="dropdown-menu dropdown-menu-end">
 | 
			
		||||
								<span className="dropdown-header">
 | 
			
		||||
									<T id="users.actions-title" data={{ id: info.row.original.id }} />
 | 
			
		||||
									<T
 | 
			
		||||
										id="object.actions-title"
 | 
			
		||||
										tData={{ object: "user" }}
 | 
			
		||||
										data={{ id: info.row.original.id }}
 | 
			
		||||
									/>
 | 
			
		||||
								</span>
 | 
			
		||||
								<a
 | 
			
		||||
									className="dropdown-item"
 | 
			
		||||
@@ -112,7 +116,7 @@ export default function Table({
 | 
			
		||||
									}}
 | 
			
		||||
								>
 | 
			
		||||
									<IconEdit size={16} />
 | 
			
		||||
									<T id="user.edit" />
 | 
			
		||||
									<T id="action.edit" />
 | 
			
		||||
								</a>
 | 
			
		||||
								{currentUserId !== info.row.original.id ? (
 | 
			
		||||
									<>
 | 
			
		||||
@@ -189,7 +193,16 @@ export default function Table({
 | 
			
		||||
	return (
 | 
			
		||||
		<TableLayout
 | 
			
		||||
			tableInstance={tableInstance}
 | 
			
		||||
			emptyState={<Empty tableInstance={tableInstance} onNewUser={onNewUser} isFiltered={isFiltered} />}
 | 
			
		||||
			emptyState={
 | 
			
		||||
				<EmptyData
 | 
			
		||||
					object="user"
 | 
			
		||||
					objects="users"
 | 
			
		||||
					tableInstance={tableInstance}
 | 
			
		||||
					onNew={onNewUser}
 | 
			
		||||
					isFiltered={isFiltered}
 | 
			
		||||
					color="orange"
 | 
			
		||||
				/>
 | 
			
		||||
			}
 | 
			
		||||
		/>
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,9 +5,9 @@ import Alert from "react-bootstrap/Alert";
 | 
			
		||||
import { deleteUser, toggleUser } from "src/api/backend";
 | 
			
		||||
import { Button, LoadingPage } from "src/components";
 | 
			
		||||
import { useUser, useUsers } from "src/hooks";
 | 
			
		||||
import { intl, T } from "src/locale";
 | 
			
		||||
import { T } from "src/locale";
 | 
			
		||||
import { showDeleteConfirmModal, showPermissionsModal, showSetPasswordModal, showUserModal } from "src/modals";
 | 
			
		||||
import { showSuccess } from "src/notifications";
 | 
			
		||||
import { showObjectSuccess } from "src/notifications";
 | 
			
		||||
import Table from "./Table";
 | 
			
		||||
 | 
			
		||||
export default function TableWrapper() {
 | 
			
		||||
@@ -26,14 +26,14 @@ export default function TableWrapper() {
 | 
			
		||||
 | 
			
		||||
	const handleDelete = async (id: number) => {
 | 
			
		||||
		await deleteUser(id);
 | 
			
		||||
		showSuccess(intl.formatMessage({ id: "notification.user-deleted" }));
 | 
			
		||||
		showObjectSuccess("user", "deleted");
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const handleDisableToggle = async (id: number, enabled: boolean) => {
 | 
			
		||||
		await toggleUser(id, enabled);
 | 
			
		||||
		queryClient.invalidateQueries({ queryKey: ["users"] });
 | 
			
		||||
		queryClient.invalidateQueries({ queryKey: ["user", id] });
 | 
			
		||||
		showSuccess(intl.formatMessage({ id: enabled ? "notification.user-enabled" : "notification.user-disabled" }));
 | 
			
		||||
		showObjectSuccess("user", enabled ? "enabled" : "disabled");
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	let filtered = null;
 | 
			
		||||
@@ -58,7 +58,7 @@ export default function TableWrapper() {
 | 
			
		||||
					<div className="row w-full">
 | 
			
		||||
						<div className="col">
 | 
			
		||||
							<h2 className="mt-1 mb-0">
 | 
			
		||||
								<T id="users.title" />
 | 
			
		||||
								<T id="users" />
 | 
			
		||||
							</h2>
 | 
			
		||||
						</div>
 | 
			
		||||
						{data?.length ? (
 | 
			
		||||
@@ -78,7 +78,7 @@ export default function TableWrapper() {
 | 
			
		||||
									</div>
 | 
			
		||||
 | 
			
		||||
									<Button size="sm" className="btn-orange" onClick={() => showUserModal("new")}>
 | 
			
		||||
										<T id="users.add" />
 | 
			
		||||
										<T id="object.add" tData={{ object: "user" }} />
 | 
			
		||||
									</Button>
 | 
			
		||||
								</div>
 | 
			
		||||
							</div>
 | 
			
		||||
@@ -95,10 +95,10 @@ export default function TableWrapper() {
 | 
			
		||||
					onSetPassword={(id: number) => showSetPasswordModal(id)}
 | 
			
		||||
					onDeleteUser={(id: number) =>
 | 
			
		||||
						showDeleteConfirmModal({
 | 
			
		||||
							title: "user.delete.title",
 | 
			
		||||
							title: <T id="object.delete" tData={{ object: "user" }} />,
 | 
			
		||||
							onConfirm: () => handleDelete(id),
 | 
			
		||||
							invalidations: [["users"], ["user", id]],
 | 
			
		||||
							children: <T id="user.delete.content" />,
 | 
			
		||||
							children: <T id="object.delete.content" tData={{ object: "user" }} />,
 | 
			
		||||
						})
 | 
			
		||||
					}
 | 
			
		||||
					onDisableToggle={handleDisableToggle}
 | 
			
		||||
 
 | 
			
		||||
@@ -203,59 +203,59 @@
 | 
			
		||||
    "@babel/helper-string-parser" "^7.27.1"
 | 
			
		||||
    "@babel/helper-validator-identifier" "^7.27.1"
 | 
			
		||||
 | 
			
		||||
"@biomejs/biome@^2.2.4":
 | 
			
		||||
  version "2.2.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@biomejs/biome/-/biome-2.2.4.tgz#184e4b83f89bd0d4151682a5aa3840df37748e17"
 | 
			
		||||
  integrity sha512-TBHU5bUy/Ok6m8c0y3pZiuO/BZoY/OcGxoLlrfQof5s8ISVwbVBdFINPQZyFfKwil8XibYWb7JMwnT8wT4WVPg==
 | 
			
		||||
"@biomejs/biome@^2.2.6":
 | 
			
		||||
  version "2.2.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@biomejs/biome/-/biome-2.2.6.tgz#76d8afbdd609a5dbace84bc982ae974a24b70b62"
 | 
			
		||||
  integrity sha512-yKTCNGhek0rL5OEW1jbLeZX8LHaM8yk7+3JRGv08my+gkpmtb5dDE+54r2ZjZx0ediFEn1pYBOJSmOdDP9xtFw==
 | 
			
		||||
  optionalDependencies:
 | 
			
		||||
    "@biomejs/cli-darwin-arm64" "2.2.4"
 | 
			
		||||
    "@biomejs/cli-darwin-x64" "2.2.4"
 | 
			
		||||
    "@biomejs/cli-linux-arm64" "2.2.4"
 | 
			
		||||
    "@biomejs/cli-linux-arm64-musl" "2.2.4"
 | 
			
		||||
    "@biomejs/cli-linux-x64" "2.2.4"
 | 
			
		||||
    "@biomejs/cli-linux-x64-musl" "2.2.4"
 | 
			
		||||
    "@biomejs/cli-win32-arm64" "2.2.4"
 | 
			
		||||
    "@biomejs/cli-win32-x64" "2.2.4"
 | 
			
		||||
    "@biomejs/cli-darwin-arm64" "2.2.6"
 | 
			
		||||
    "@biomejs/cli-darwin-x64" "2.2.6"
 | 
			
		||||
    "@biomejs/cli-linux-arm64" "2.2.6"
 | 
			
		||||
    "@biomejs/cli-linux-arm64-musl" "2.2.6"
 | 
			
		||||
    "@biomejs/cli-linux-x64" "2.2.6"
 | 
			
		||||
    "@biomejs/cli-linux-x64-musl" "2.2.6"
 | 
			
		||||
    "@biomejs/cli-win32-arm64" "2.2.6"
 | 
			
		||||
    "@biomejs/cli-win32-x64" "2.2.6"
 | 
			
		||||
 | 
			
		||||
"@biomejs/cli-darwin-arm64@2.2.4":
 | 
			
		||||
  version "2.2.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.2.4.tgz#9b50620c93501e370b7e6d5a8445f117f9946a0c"
 | 
			
		||||
  integrity sha512-RJe2uiyaloN4hne4d2+qVj3d3gFJFbmrr5PYtkkjei1O9c+BjGXgpUPVbi8Pl8syumhzJjFsSIYkcLt2VlVLMA==
 | 
			
		||||
"@biomejs/cli-darwin-arm64@2.2.6":
 | 
			
		||||
  version "2.2.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.2.6.tgz#b232a7d92c0b28884c1cf99d4348e1a52f95931f"
 | 
			
		||||
  integrity sha512-UZPmn3M45CjTYulgcrFJFZv7YmK3pTxTJDrFYlNElT2FNnkkX4fsxjExTSMeWKQYoZjvekpH5cvrYZZlWu3yfA==
 | 
			
		||||
 | 
			
		||||
"@biomejs/cli-darwin-x64@2.2.4":
 | 
			
		||||
  version "2.2.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.2.4.tgz#343620c884fc8141155d114430e80e4eacfddc9e"
 | 
			
		||||
  integrity sha512-cFsdB4ePanVWfTnPVaUX+yr8qV8ifxjBKMkZwN7gKb20qXPxd/PmwqUH8mY5wnM9+U0QwM76CxFyBRJhC9tQwg==
 | 
			
		||||
"@biomejs/cli-darwin-x64@2.2.6":
 | 
			
		||||
  version "2.2.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.2.6.tgz#3a2d1582037735f8ed5243aa6d3cc4f4082c5f42"
 | 
			
		||||
  integrity sha512-HOUIquhHVgh/jvxyClpwlpl/oeMqntlteL89YqjuFDiZ091P0vhHccwz+8muu3nTyHWM5FQslt+4Jdcd67+xWQ==
 | 
			
		||||
 | 
			
		||||
"@biomejs/cli-linux-arm64-musl@2.2.4":
 | 
			
		||||
  version "2.2.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.2.4.tgz#cabcdadce2bc88b697f4063374224266c6f8b6e5"
 | 
			
		||||
  integrity sha512-7TNPkMQEWfjvJDaZRSkDCPT/2r5ESFPKx+TEev+I2BXDGIjfCZk2+b88FOhnJNHtksbOZv8ZWnxrA5gyTYhSsQ==
 | 
			
		||||
"@biomejs/cli-linux-arm64-musl@2.2.6":
 | 
			
		||||
  version "2.2.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.2.6.tgz#2bfe0859c55c840c0648f2b307efccca2527d655"
 | 
			
		||||
  integrity sha512-TjCenQq3N6g1C+5UT3jE1bIiJb5MWQvulpUngTIpFsL4StVAUXucWD0SL9MCW89Tm6awWfeXBbZBAhJwjyFbRQ==
 | 
			
		||||
 | 
			
		||||
"@biomejs/cli-linux-arm64@2.2.4":
 | 
			
		||||
  version "2.2.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.2.4.tgz#55620f8f088145e62e1158eb85c568554d0c8673"
 | 
			
		||||
  integrity sha512-M/Iz48p4NAzMXOuH+tsn5BvG/Jb07KOMTdSVwJpicmhN309BeEyRyQX+n1XDF0JVSlu28+hiTQ2L4rZPvu7nMw==
 | 
			
		||||
"@biomejs/cli-linux-arm64@2.2.6":
 | 
			
		||||
  version "2.2.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.2.6.tgz#aa1817296d84bda6ea972a54b139d1664580c190"
 | 
			
		||||
  integrity sha512-BpGtuMJGN+o8pQjvYsUKZ+4JEErxdSmcRD/JG3mXoWc6zrcA7OkuyGFN1mDggO0Q1n7qXxo/PcupHk8gzijt5g==
 | 
			
		||||
 | 
			
		||||
"@biomejs/cli-linux-x64-musl@2.2.4":
 | 
			
		||||
  version "2.2.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.2.4.tgz#6bfaea72505afdbda66a66c998d2d169a8b55f90"
 | 
			
		||||
  integrity sha512-m41nFDS0ksXK2gwXL6W6yZTYPMH0LughqbsxInSKetoH6morVj43szqKx79Iudkp8WRT5SxSh7qVb8KCUiewGg==
 | 
			
		||||
"@biomejs/cli-linux-x64-musl@2.2.6":
 | 
			
		||||
  version "2.2.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.2.6.tgz#b65f9191420e3384e287b21a8b1769795ee2c4c1"
 | 
			
		||||
  integrity sha512-1ZcBux8zVM3JhWN2ZCPaYf0+ogxXG316uaoXJdgoPZcdK/rmRcRY7PqHdAos2ExzvjIdvhQp72UcveI98hgOog==
 | 
			
		||||
 | 
			
		||||
"@biomejs/cli-linux-x64@2.2.4":
 | 
			
		||||
  version "2.2.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64/-/cli-linux-x64-2.2.4.tgz#8c1ed61dcafb8a5939346c714ec122651f57e1db"
 | 
			
		||||
  integrity sha512-orr3nnf2Dpb2ssl6aihQtvcKtLySLta4E2UcXdp7+RTa7mfJjBgIsbS0B9GC8gVu0hjOu021aU8b3/I1tn+pVQ==
 | 
			
		||||
"@biomejs/cli-linux-x64@2.2.6":
 | 
			
		||||
  version "2.2.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64/-/cli-linux-x64-2.2.6.tgz#5c5f07264bed31a436db60577c77ea9f64a62d2b"
 | 
			
		||||
  integrity sha512-1HaM/dpI/1Z68zp8ZdT6EiBq+/O/z97a2AiHMl+VAdv5/ELckFt9EvRb8hDHpk8hUMoz03gXkC7VPXOVtU7faA==
 | 
			
		||||
 | 
			
		||||
"@biomejs/cli-win32-arm64@2.2.4":
 | 
			
		||||
  version "2.2.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.2.4.tgz#b2528f6c436e753d6083d7779f0662e08786cedb"
 | 
			
		||||
  integrity sha512-NXnfTeKHDFUWfxAefa57DiGmu9VyKi0cDqFpdI+1hJWQjGJhJutHPX0b5m+eXvTKOaf+brU+P0JrQAZMb5yYaQ==
 | 
			
		||||
"@biomejs/cli-win32-arm64@2.2.6":
 | 
			
		||||
  version "2.2.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.2.6.tgz#a47ef2d6bec694df088ba70ffd6e89839c9c2304"
 | 
			
		||||
  integrity sha512-h3A88G8PGM1ryTeZyLlSdfC/gz3e95EJw9BZmA6Po412DRqwqPBa2Y9U+4ZSGUAXCsnSQE00jLV8Pyrh0d+jQw==
 | 
			
		||||
 | 
			
		||||
"@biomejs/cli-win32-x64@2.2.4":
 | 
			
		||||
  version "2.2.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-x64/-/cli-win32-x64-2.2.4.tgz#c8e21413120fe073fa49b78fdd987022941ff66f"
 | 
			
		||||
  integrity sha512-3Y4V4zVRarVh/B/eSHczR4LYoSVyv3Dfuvm3cWs5w/HScccS0+Wt/lHOcDTRYeHjQmMYVC3rIRWqyN2EI52+zg==
 | 
			
		||||
"@biomejs/cli-win32-x64@2.2.6":
 | 
			
		||||
  version "2.2.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-x64/-/cli-win32-x64-2.2.6.tgz#cec00e860d9f20d820a48db4a6c7ab3b35c83ac6"
 | 
			
		||||
  integrity sha512-yx0CqeOhPjYQ5ZXgPfu8QYkgBhVJyvWe36as7jRuPrKPO5ylVDfwVtPQ+K/mooNTADW0IhxOZm3aPu16dP8yNQ==
 | 
			
		||||
 | 
			
		||||
"@emotion/babel-plugin@^11.13.5":
 | 
			
		||||
  version "11.13.5"
 | 
			
		||||
@@ -495,18 +495,18 @@
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.10.tgz#a2a1e3812d14525f725d011a73eceb41fef5bc1c"
 | 
			
		||||
  integrity sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==
 | 
			
		||||
 | 
			
		||||
"@formatjs/cli@^6.7.2":
 | 
			
		||||
  version "6.7.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@formatjs/cli/-/cli-6.7.2.tgz#8d1a3225dee98cadbc5331b1971f1b79dead4253"
 | 
			
		||||
  integrity sha512-714/ifbtq7CmOtcLVGjYZp5EX0vKclQt3XZ+4Oiz0lhrHWU1ObFTtX9sLS9dADGta1pgDtS4+A3YpjWMY26Spg==
 | 
			
		||||
"@formatjs/cli@^6.7.4":
 | 
			
		||||
  version "6.7.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@formatjs/cli/-/cli-6.7.4.tgz#21fdab4e215763034682abf148b64b4f8d9d4a5c"
 | 
			
		||||
  integrity sha512-k6uqdeZDAjDd7iKKQ8yFYizFpbi5Y9H9NkV+hoIhmxaMSGvWRnRusQJaIQ+2rI14MH6knW6fx7tnO15C+ijDiw==
 | 
			
		||||
 | 
			
		||||
"@formatjs/ecma402-abstract@2.3.4":
 | 
			
		||||
  version "2.3.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.4.tgz#e90c5a846ba2b33d92bc400fdd709da588280fbc"
 | 
			
		||||
  integrity sha512-qrycXDeaORzIqNhBOx0btnhpD1c+/qFIHAN9znofuMJX6QBwtbrmlpWfD4oiUUD2vJUOIYFA/gYtg2KAMGG7sA==
 | 
			
		||||
"@formatjs/ecma402-abstract@2.3.6":
 | 
			
		||||
  version "2.3.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz#d6ca9d3579054fe1e1a0a0b5e872e0d64922e4e1"
 | 
			
		||||
  integrity sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@formatjs/fast-memoize" "2.2.7"
 | 
			
		||||
    "@formatjs/intl-localematcher" "0.6.1"
 | 
			
		||||
    "@formatjs/intl-localematcher" "0.6.2"
 | 
			
		||||
    decimal.js "^10.4.3"
 | 
			
		||||
    tslib "^2.8.0"
 | 
			
		||||
 | 
			
		||||
@@ -517,39 +517,39 @@
 | 
			
		||||
  dependencies:
 | 
			
		||||
    tslib "^2.8.0"
 | 
			
		||||
 | 
			
		||||
"@formatjs/icu-messageformat-parser@2.11.2":
 | 
			
		||||
  version "2.11.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.2.tgz#85aea211bea40aa81ee1d44ac7accc3cf5500a73"
 | 
			
		||||
  integrity sha512-AfiMi5NOSo2TQImsYAg8UYddsNJ/vUEv/HaNqiFjnI3ZFfWihUtD5QtuX6kHl8+H+d3qvnE/3HZrfzgdWpsLNA==
 | 
			
		||||
"@formatjs/icu-messageformat-parser@2.11.4":
 | 
			
		||||
  version "2.11.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.4.tgz#63bd2cd82d08ae2bef55adeeb86486df68826f32"
 | 
			
		||||
  integrity sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@formatjs/ecma402-abstract" "2.3.4"
 | 
			
		||||
    "@formatjs/icu-skeleton-parser" "1.8.14"
 | 
			
		||||
    "@formatjs/ecma402-abstract" "2.3.6"
 | 
			
		||||
    "@formatjs/icu-skeleton-parser" "1.8.16"
 | 
			
		||||
    tslib "^2.8.0"
 | 
			
		||||
 | 
			
		||||
"@formatjs/icu-skeleton-parser@1.8.14":
 | 
			
		||||
  version "1.8.14"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.14.tgz#b9581d00363908efb29817fdffc32b79f41dabe5"
 | 
			
		||||
  integrity sha512-i4q4V4qslThK4Ig8SxyD76cp3+QJ3sAqr7f6q9VVfeGtxG9OhiAk3y9XF6Q41OymsKzsGQ6OQQoJNY4/lI8TcQ==
 | 
			
		||||
"@formatjs/icu-skeleton-parser@1.8.16":
 | 
			
		||||
  version "1.8.16"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.16.tgz#13f81f6845c7cf6599623006aacaf7d6b4ad2970"
 | 
			
		||||
  integrity sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@formatjs/ecma402-abstract" "2.3.4"
 | 
			
		||||
    "@formatjs/ecma402-abstract" "2.3.6"
 | 
			
		||||
    tslib "^2.8.0"
 | 
			
		||||
 | 
			
		||||
"@formatjs/intl-localematcher@0.6.1":
 | 
			
		||||
  version "0.6.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.6.1.tgz#25dc30675320bf65a9d7f73876fc1e4064c0e299"
 | 
			
		||||
  integrity sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==
 | 
			
		||||
"@formatjs/intl-localematcher@0.6.2":
 | 
			
		||||
  version "0.6.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.6.2.tgz#e9ebe0b4082d7d48e5b2d753579fb7ece4eaefea"
 | 
			
		||||
  integrity sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    tslib "^2.8.0"
 | 
			
		||||
 | 
			
		||||
"@formatjs/intl@3.1.6":
 | 
			
		||||
  version "3.1.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@formatjs/intl/-/intl-3.1.6.tgz#4c7fec6f082628cfa80871fbe7f9bc2644300e3b"
 | 
			
		||||
  integrity sha512-tDkXnA4qpIFcDWac8CyVJq6oW8DR7W44QDUBsfXWIIJD/FYYen0QoH46W7XsVMFfPOVKkvbufjboZrrWbEfmww==
 | 
			
		||||
"@formatjs/intl@3.1.8":
 | 
			
		||||
  version "3.1.8"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@formatjs/intl/-/intl-3.1.8.tgz#17f4a5721f32cd077ab04949be558f9f767c39f5"
 | 
			
		||||
  integrity sha512-LWXgwI5zTMatvR8w8kCNh/priDTOF/ZssokMBHJ7ZWXFoYLVOYo0EJERD9Eajv+xsfQO1QkuAt77KWQ1OI4mOQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@formatjs/ecma402-abstract" "2.3.4"
 | 
			
		||||
    "@formatjs/ecma402-abstract" "2.3.6"
 | 
			
		||||
    "@formatjs/fast-memoize" "2.2.7"
 | 
			
		||||
    "@formatjs/icu-messageformat-parser" "2.11.2"
 | 
			
		||||
    intl-messageformat "10.7.16"
 | 
			
		||||
    "@formatjs/icu-messageformat-parser" "2.11.4"
 | 
			
		||||
    intl-messageformat "10.7.18"
 | 
			
		||||
    tslib "^2.8.0"
 | 
			
		||||
 | 
			
		||||
"@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.5":
 | 
			
		||||
@@ -716,10 +716,10 @@
 | 
			
		||||
    uncontrollable "^8.0.4"
 | 
			
		||||
    warning "^4.0.3"
 | 
			
		||||
 | 
			
		||||
"@rolldown/pluginutils@1.0.0-beta.35":
 | 
			
		||||
  version "1.0.0-beta.35"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.35.tgz#1a477e7742b154b67519d40e4fc17485de338e7a"
 | 
			
		||||
  integrity sha512-slYrCpoxJUqzFDDNlvrOYRazQUNRvWPjXA17dAOISY3rDMxX6k8K4cj2H+hEYMHF81HO3uNd5rHVigAWRM5dSg==
 | 
			
		||||
"@rolldown/pluginutils@1.0.0-beta.38":
 | 
			
		||||
  version "1.0.0-beta.38"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.38.tgz#95253608c4629eb2a5f3d656009ac9ba031eb292"
 | 
			
		||||
  integrity sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw==
 | 
			
		||||
 | 
			
		||||
"@rollup/rollup-android-arm-eabi@4.50.0":
 | 
			
		||||
  version "4.50.0"
 | 
			
		||||
@@ -853,29 +853,29 @@
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@tabler/icons/-/icons-3.35.0.tgz#6f35247e41baba2a1b0f4dff048bb1335d6c1075"
 | 
			
		||||
  integrity sha512-yYXe+gJ56xlZFiXwV9zVoe3FWCGuZ/D7/G4ZIlDtGxSx5CGQK110wrnT29gUj52kEZoxqF7oURTk97GQxELOFQ==
 | 
			
		||||
 | 
			
		||||
"@tanstack/query-core@5.89.0":
 | 
			
		||||
  version "5.89.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.89.0.tgz#67862fa9aa036942b1906bc51385115a2bbec45a"
 | 
			
		||||
  integrity sha512-joFV1MuPhSLsKfTzwjmPDrp8ENfZ9N23ymFu07nLfn3JCkSHy0CFgsyhHTJOmWaumC/WiNIKM0EJyduCF/Ih/Q==
 | 
			
		||||
"@tanstack/query-core@5.90.3":
 | 
			
		||||
  version "5.90.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.90.3.tgz#461422437d30aad0a7618122c5e7568095d79cfb"
 | 
			
		||||
  integrity sha512-HtPOnCwmx4dd35PfXU8jjkhwYrsHfuqgC8RCJIwWglmhIUIlzPP0ZcEkDAc+UtAWCiLm7T8rxeEfHZlz3hYMCA==
 | 
			
		||||
 | 
			
		||||
"@tanstack/query-devtools@5.87.3":
 | 
			
		||||
  version "5.87.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.87.3.tgz#0cfff30823f837d6b9ead08f8d1b16459203fdb2"
 | 
			
		||||
  integrity sha512-LkzxzSr2HS1ALHTgDmJH5eGAVsSQiuwz//VhFW5OqNk0OQ+Fsqba0Tsf+NzWRtXYvpgUqwQr4b2zdFZwxHcGvg==
 | 
			
		||||
"@tanstack/query-devtools@5.90.1":
 | 
			
		||||
  version "5.90.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.90.1.tgz#c57a739a5293f4960a4a6b6fb4b7e4a56e6bd932"
 | 
			
		||||
  integrity sha512-GtINOPjPUH0OegJExZ70UahT9ykmAhmtNVcmtdnOZbxLwT7R5OmRztR5Ahe3/Cu7LArEmR6/588tAycuaWb1xQ==
 | 
			
		||||
 | 
			
		||||
"@tanstack/react-query-devtools@^5.89.0":
 | 
			
		||||
  version "5.89.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.89.0.tgz#ca9cc274cbec7c1479ab67335282a06b4566d164"
 | 
			
		||||
  integrity sha512-Syc4UjZeIJCkXCRGyQcWwlnv89JNb98MMg/DAkFCV3rwOcknj98+nG3Nm6xLXM6ne9sK6RZeDJMPLKZUh6NUGA==
 | 
			
		||||
"@tanstack/react-query-devtools@^5.90.2":
 | 
			
		||||
  version "5.90.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.90.2.tgz#248d6ae89ace7dc2da816fa95cdc2b4f63c9e4d2"
 | 
			
		||||
  integrity sha512-vAXJzZuBXtCQtrY3F/yUNJCV4obT/A/n81kb3+YqLbro5Z2+phdAbceO+deU3ywPw8B42oyJlp4FhO0SoivDFQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@tanstack/query-devtools" "5.87.3"
 | 
			
		||||
    "@tanstack/query-devtools" "5.90.1"
 | 
			
		||||
 | 
			
		||||
"@tanstack/react-query@^5.89.0":
 | 
			
		||||
  version "5.89.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.89.0.tgz#ae295ced60d1aee555337a572ad4045e2af3d00a"
 | 
			
		||||
  integrity sha512-SXbtWSTSRXyBOe80mszPxpEbaN4XPRUp/i0EfQK1uyj3KCk/c8FuPJNIRwzOVe/OU3rzxrYtiNabsAmk1l714A==
 | 
			
		||||
"@tanstack/react-query@^5.90.3":
 | 
			
		||||
  version "5.90.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.90.3.tgz#670ed97948c5d4e3c075049f8a01e84d51e0bdc4"
 | 
			
		||||
  integrity sha512-i/LRL6DtuhG6bjGzavIMIVuKKPWx2AnEBIsBfuMm3YoHne0a20nWmsatOCBcVSaT0/8/5YFjNkebHAPLVUSi0Q==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@tanstack/query-core" "5.89.0"
 | 
			
		||||
    "@tanstack/query-core" "5.90.3"
 | 
			
		||||
 | 
			
		||||
"@tanstack/react-table@^8.21.3":
 | 
			
		||||
  version "8.21.3"
 | 
			
		||||
@@ -903,10 +903,10 @@
 | 
			
		||||
    picocolors "1.1.1"
 | 
			
		||||
    pretty-format "^27.0.2"
 | 
			
		||||
 | 
			
		||||
"@testing-library/jest-dom@^6.8.0":
 | 
			
		||||
  version "6.8.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.8.0.tgz#697db9424f0d21d8216f1958fa0b1b69b5f43923"
 | 
			
		||||
  integrity sha512-WgXcWzVM6idy5JaftTVC8Vs83NKRmGJz4Hqs4oyOuO2J4r/y79vvKZsb+CaGyCSEbUPI6OsewfPd0G1A0/TUZQ==
 | 
			
		||||
"@testing-library/jest-dom@^6.9.1":
 | 
			
		||||
  version "6.9.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz#7613a04e146dd2976d24ddf019730d57a89d56c2"
 | 
			
		||||
  integrity sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@adobe/css-tools" "^4.4.0"
 | 
			
		||||
    aria-query "^5.0.0"
 | 
			
		||||
@@ -1037,10 +1037,10 @@
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.15.tgz#e6e5a86d602beaca71ce5163fadf5f95d70931c7"
 | 
			
		||||
  integrity sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==
 | 
			
		||||
 | 
			
		||||
"@types/react-dom@^19.1.9":
 | 
			
		||||
  version "19.1.9"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.1.9.tgz#5ab695fce1e804184767932365ae6569c11b4b4b"
 | 
			
		||||
  integrity sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==
 | 
			
		||||
"@types/react-dom@^19.2.2":
 | 
			
		||||
  version "19.2.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.2.2.tgz#a4cc874797b7ddc9cb180ef0d5dc23f596fc2332"
 | 
			
		||||
  integrity sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==
 | 
			
		||||
 | 
			
		||||
"@types/react-table@^7.7.20":
 | 
			
		||||
  version "7.7.20"
 | 
			
		||||
@@ -1061,10 +1061,10 @@
 | 
			
		||||
  dependencies:
 | 
			
		||||
    csstype "^3.0.2"
 | 
			
		||||
 | 
			
		||||
"@types/react@^19.1.13":
 | 
			
		||||
  version "19.1.13"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.13.tgz#fc650ffa680d739a25a530f5d7ebe00cdd771883"
 | 
			
		||||
  integrity sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==
 | 
			
		||||
"@types/react@^19.2.2":
 | 
			
		||||
  version "19.2.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/react/-/react-19.2.2.tgz#ba123a75d4c2a51158697160a4ea2ff70aa6bf36"
 | 
			
		||||
  integrity sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    csstype "^3.0.2"
 | 
			
		||||
 | 
			
		||||
@@ -1102,15 +1102,15 @@
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8"
 | 
			
		||||
  integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==
 | 
			
		||||
 | 
			
		||||
"@vitejs/plugin-react@^5.0.3":
 | 
			
		||||
  version "5.0.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-5.0.3.tgz#182ea45406d89e55b4e35c92a4a8c2c8388726c8"
 | 
			
		||||
  integrity sha512-PFVHhosKkofGH0Yzrw1BipSedTH68BFF8ZWy1kfUpCtJcouXXY0+racG8sExw7hw0HoX36813ga5o3LTWZ4FUg==
 | 
			
		||||
"@vitejs/plugin-react@^5.0.4":
 | 
			
		||||
  version "5.0.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-5.0.4.tgz#d642058e89c5b712655c8cbd13482f5813519602"
 | 
			
		||||
  integrity sha512-La0KD0vGkVkSk6K+piWDKRUyg8Rl5iAIKRMH0vMJI0Eg47bq1eOxmoObAaQG37WMW9MSyk7Cs8EIWwJC1PtzKA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@babel/core" "^7.28.4"
 | 
			
		||||
    "@babel/plugin-transform-react-jsx-self" "^7.27.1"
 | 
			
		||||
    "@babel/plugin-transform-react-jsx-source" "^7.27.1"
 | 
			
		||||
    "@rolldown/pluginutils" "1.0.0-beta.35"
 | 
			
		||||
    "@rolldown/pluginutils" "1.0.0-beta.38"
 | 
			
		||||
    "@types/babel__core" "^7.20.5"
 | 
			
		||||
    react-refresh "^0.17.0"
 | 
			
		||||
 | 
			
		||||
@@ -1180,11 +1180,6 @@ ansi-regex@^5.0.1:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
 | 
			
		||||
  integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
 | 
			
		||||
 | 
			
		||||
ansi-regex@^6.0.1:
 | 
			
		||||
  version "6.2.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.2.0.tgz#2f302e7550431b1b7762705fffb52cf1ffa20447"
 | 
			
		||||
  integrity sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==
 | 
			
		||||
 | 
			
		||||
ansi-styles@^5.0.0:
 | 
			
		||||
  version "5.2.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b"
 | 
			
		||||
@@ -1360,10 +1355,10 @@ cosmiconfig@^7.0.0:
 | 
			
		||||
    path-type "^4.0.0"
 | 
			
		||||
    yaml "^1.10.0"
 | 
			
		||||
 | 
			
		||||
country-flag-icons@^1.5.20:
 | 
			
		||||
  version "1.5.20"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/country-flag-icons/-/country-flag-icons-1.5.20.tgz#a373b13f2b2899a2ef85a4b6fb6a80b8f38f9a01"
 | 
			
		||||
  integrity sha512-ldchBFYlw2dXXGXO6q/xCYaFiiK/eg5qSctBRDzUTDvHqAdIMOVTj1sp0OWVo7g6h+RURTjlu+5BhtGwIbe9tg==
 | 
			
		||||
country-flag-icons@^1.5.21:
 | 
			
		||||
  version "1.5.21"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/country-flag-icons/-/country-flag-icons-1.5.21.tgz#f7502b144c523f5a6ad8d29de780c90a5f9a131e"
 | 
			
		||||
  integrity sha512-0KmU4oeiyAM+F+atzK99ghQDQJKxEY3tiDhnRraVFL4o65rZgrmrx7xKi0b+hxcVpcEpuUbu+KCC6TKTZQTDcA==
 | 
			
		||||
 | 
			
		||||
css.escape@^1.5.1:
 | 
			
		||||
  version "1.5.1"
 | 
			
		||||
@@ -1604,10 +1599,10 @@ globrex@^0.1.2:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098"
 | 
			
		||||
  integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==
 | 
			
		||||
 | 
			
		||||
happy-dom@^18.0.1:
 | 
			
		||||
  version "18.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/happy-dom/-/happy-dom-18.0.1.tgz#8f8a2199462842deb675f54e405af55f3b674540"
 | 
			
		||||
  integrity sha512-qn+rKOW7KWpVTtgIUi6RVmTBZJSe2k0Db0vh1f7CWrWclkkc7/Q+FrOfkZIb2eiErLyqu5AXEzE7XthO9JVxRA==
 | 
			
		||||
happy-dom@^20.0.2:
 | 
			
		||||
  version "20.0.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/happy-dom/-/happy-dom-20.0.2.tgz#da676a1e3d0742aff8ba4789db90445842538808"
 | 
			
		||||
  integrity sha512-pYOyu624+6HDbY+qkjILpQGnpvZOusItCk+rvF5/V+6NkcgTKnbOldpIy22tBnxoaLtlM9nXgoqAcW29/B7CIw==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@types/node" "^20.0.0"
 | 
			
		||||
    "@types/whatwg-mimetype" "^3.0.2"
 | 
			
		||||
@@ -1753,14 +1748,14 @@ indent-string@^4.0.0:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
 | 
			
		||||
  integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
 | 
			
		||||
 | 
			
		||||
intl-messageformat@10.7.16:
 | 
			
		||||
  version "10.7.16"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-10.7.16.tgz#d909f9f9f4ab857fbe681d559b958dd4dd9f665a"
 | 
			
		||||
  integrity sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==
 | 
			
		||||
intl-messageformat@10.7.18:
 | 
			
		||||
  version "10.7.18"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-10.7.18.tgz#51a6f387afbca9b0f881b2ec081566db8c540b0d"
 | 
			
		||||
  integrity sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@formatjs/ecma402-abstract" "2.3.4"
 | 
			
		||||
    "@formatjs/ecma402-abstract" "2.3.6"
 | 
			
		||||
    "@formatjs/fast-memoize" "2.2.7"
 | 
			
		||||
    "@formatjs/icu-messageformat-parser" "2.11.2"
 | 
			
		||||
    "@formatjs/icu-messageformat-parser" "2.11.4"
 | 
			
		||||
    tslib "^2.8.0"
 | 
			
		||||
 | 
			
		||||
invariant@^2.2.4:
 | 
			
		||||
@@ -2180,30 +2175,30 @@ react-bootstrap@^2.10.10:
 | 
			
		||||
    uncontrollable "^7.2.1"
 | 
			
		||||
    warning "^4.0.3"
 | 
			
		||||
 | 
			
		||||
react-dom@^19.1.1:
 | 
			
		||||
  version "19.1.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.1.1.tgz#2daa9ff7f3ae384aeb30e76d5ee38c046dc89893"
 | 
			
		||||
  integrity sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==
 | 
			
		||||
react-dom@^19.2.0:
 | 
			
		||||
  version "19.2.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.0.tgz#00ed1e959c365e9a9d48f8918377465466ec3af8"
 | 
			
		||||
  integrity sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    scheduler "^0.26.0"
 | 
			
		||||
    scheduler "^0.27.0"
 | 
			
		||||
 | 
			
		||||
react-fast-compare@^2.0.1:
 | 
			
		||||
  version "2.0.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
 | 
			
		||||
  integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
 | 
			
		||||
 | 
			
		||||
react-intl@^7.1.11:
 | 
			
		||||
  version "7.1.11"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-7.1.11.tgz#6155602c46621ad9b67dd31750d00908e1e0b516"
 | 
			
		||||
  integrity sha512-tnVoRCWvW5Ie2ikYSdPF7z3+880yCe/9xPmitFeRPw3RYDcCfR4m8ZYa4MBq19W4adt9Z+PQA4FaMBCJ7E+HCQ==
 | 
			
		||||
react-intl@^7.1.14:
 | 
			
		||||
  version "7.1.14"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-7.1.14.tgz#5500f76b0bb35694a7bec418b1adf1533dad6752"
 | 
			
		||||
  integrity sha512-VE/0Wi/lHJlBC7APQpCzLUdIt3GB5B0GZrRW8Q+ACbkHI4j+Wwgg9J1TniN6zmLHmPH5gxXcMy+fkSPfw5p1WQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@formatjs/ecma402-abstract" "2.3.4"
 | 
			
		||||
    "@formatjs/icu-messageformat-parser" "2.11.2"
 | 
			
		||||
    "@formatjs/intl" "3.1.6"
 | 
			
		||||
    "@formatjs/ecma402-abstract" "2.3.6"
 | 
			
		||||
    "@formatjs/icu-messageformat-parser" "2.11.4"
 | 
			
		||||
    "@formatjs/intl" "3.1.8"
 | 
			
		||||
    "@types/hoist-non-react-statics" "^3.3.1"
 | 
			
		||||
    "@types/react" "16 || 17 || 18 || 19"
 | 
			
		||||
    hoist-non-react-statics "^3.3.2"
 | 
			
		||||
    intl-messageformat "10.7.16"
 | 
			
		||||
    intl-messageformat "10.7.18"
 | 
			
		||||
    tslib "^2.8.0"
 | 
			
		||||
 | 
			
		||||
react-is@^16.13.1, react-is@^16.3.2, react-is@^16.7.0:
 | 
			
		||||
@@ -2226,17 +2221,17 @@ react-refresh@^0.17.0:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.17.0.tgz#b7e579c3657f23d04eccbe4ad2e58a8ed51e7e53"
 | 
			
		||||
  integrity sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==
 | 
			
		||||
 | 
			
		||||
react-router-dom@^7.9.1:
 | 
			
		||||
  version "7.9.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-7.9.1.tgz#48044923701773da6362f9003ec46f308f293f15"
 | 
			
		||||
  integrity sha512-U9WBQssBE9B1vmRjo9qTM7YRzfZ3lUxESIZnsf4VjR/lXYz9MHjvOxHzr/aUm4efpktbVOrF09rL/y4VHa8RMw==
 | 
			
		||||
react-router-dom@^7.9.4:
 | 
			
		||||
  version "7.9.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-7.9.4.tgz#37d35b4b7f730b37434f2b7e95121ef557a6b538"
 | 
			
		||||
  integrity sha512-f30P6bIkmYvnHHa5Gcu65deIXoA2+r3Eb6PJIAddvsT9aGlchMatJ51GgpU470aSqRRbFX22T70yQNUGuW3DfA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    react-router "7.9.1"
 | 
			
		||||
    react-router "7.9.4"
 | 
			
		||||
 | 
			
		||||
react-router@7.9.1:
 | 
			
		||||
  version "7.9.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.9.1.tgz#b227410c31f24dd416c939ca5d0f8d5c8a1404d4"
 | 
			
		||||
  integrity sha512-pfAByjcTpX55mqSDGwGnY9vDCpxqBLASg0BMNAuMmpSGESo/TaOUG6BllhAtAkCGx8Rnohik/XtaqiYUJtgW2g==
 | 
			
		||||
react-router@7.9.4:
 | 
			
		||||
  version "7.9.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.9.4.tgz#2c4249e5d0a6bb8b8f6bf0ede8f5077e4ff8024f"
 | 
			
		||||
  integrity sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    cookie "^1.0.1"
 | 
			
		||||
    set-cookie-parser "^2.6.0"
 | 
			
		||||
@@ -2273,10 +2268,10 @@ react-transition-group@^4.3.0, react-transition-group@^4.4.5:
 | 
			
		||||
    loose-envify "^1.4.0"
 | 
			
		||||
    prop-types "^15.6.2"
 | 
			
		||||
 | 
			
		||||
react@^19.1.1:
 | 
			
		||||
  version "19.1.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/react/-/react-19.1.1.tgz#06d9149ec5e083a67f9a1e39ce97b06a03b644af"
 | 
			
		||||
  integrity sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==
 | 
			
		||||
react@^19.2.0:
 | 
			
		||||
  version "19.2.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/react/-/react-19.2.0.tgz#d33dd1721698f4376ae57a54098cb47fc75d93a5"
 | 
			
		||||
  integrity sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==
 | 
			
		||||
 | 
			
		||||
readdirp@^4.0.1:
 | 
			
		||||
  version "4.1.2"
 | 
			
		||||
@@ -2400,10 +2395,10 @@ safe-buffer@^5.1.0:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
 | 
			
		||||
  integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
 | 
			
		||||
 | 
			
		||||
sass@^1.93.0:
 | 
			
		||||
  version "1.93.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/sass/-/sass-1.93.0.tgz#8252f61405be295f4755d1ed5df48bf118587aa5"
 | 
			
		||||
  integrity sha512-CQi5/AzCwiubU3dSqRDJ93RfOfg/hhpW1l6wCIvolmehfwgCI35R/0QDs1+R+Ygrl8jFawwwIojE2w47/mf94A==
 | 
			
		||||
sass@^1.93.2:
 | 
			
		||||
  version "1.93.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/sass/-/sass-1.93.2.tgz#e97d225d60f59a3b3dbb6d2ae3c1b955fd1f2cd1"
 | 
			
		||||
  integrity sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    chokidar "^4.0.0"
 | 
			
		||||
    immutable "^5.0.2"
 | 
			
		||||
@@ -2411,10 +2406,10 @@ sass@^1.93.0:
 | 
			
		||||
  optionalDependencies:
 | 
			
		||||
    "@parcel/watcher" "^2.4.1"
 | 
			
		||||
 | 
			
		||||
scheduler@^0.26.0:
 | 
			
		||||
  version "0.26.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.26.0.tgz#4ce8a8c2a2095f13ea11bf9a445be50c555d6337"
 | 
			
		||||
  integrity sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==
 | 
			
		||||
scheduler@^0.27.0:
 | 
			
		||||
  version "0.27.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.27.0.tgz#0c4ef82d67d1e5c1e359e8fc76d3a87f045fe5bd"
 | 
			
		||||
  integrity sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==
 | 
			
		||||
 | 
			
		||||
semver@^6.3.1:
 | 
			
		||||
  version "6.3.1"
 | 
			
		||||
@@ -2469,13 +2464,6 @@ stringify-entities@^4.0.0:
 | 
			
		||||
    character-entities-html4 "^2.0.0"
 | 
			
		||||
    character-entities-legacy "^3.0.0"
 | 
			
		||||
 | 
			
		||||
strip-ansi@^7.1.0:
 | 
			
		||||
  version "7.1.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
 | 
			
		||||
  integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    ansi-regex "^6.0.1"
 | 
			
		||||
 | 
			
		||||
strip-indent@^3.0.0:
 | 
			
		||||
  version "3.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001"
 | 
			
		||||
@@ -2583,10 +2571,10 @@ tslib@^2.0.0, tslib@^2.8.0:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
 | 
			
		||||
  integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
 | 
			
		||||
 | 
			
		||||
typescript@5.9.2:
 | 
			
		||||
  version "5.9.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.2.tgz#d93450cddec5154a2d5cabe3b8102b83316fb2a6"
 | 
			
		||||
  integrity sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==
 | 
			
		||||
typescript@5.9.3:
 | 
			
		||||
  version "5.9.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f"
 | 
			
		||||
  integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==
 | 
			
		||||
 | 
			
		||||
uncontrollable@^7.2.1:
 | 
			
		||||
  version "7.2.1"
 | 
			
		||||
@@ -2726,17 +2714,16 @@ vite-node@3.2.4:
 | 
			
		||||
    pathe "^2.0.3"
 | 
			
		||||
    vite "^5.0.0 || ^6.0.0 || ^7.0.0-0"
 | 
			
		||||
 | 
			
		||||
vite-plugin-checker@^0.10.3:
 | 
			
		||||
  version "0.10.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/vite-plugin-checker/-/vite-plugin-checker-0.10.3.tgz#ddc609aebe098b23aea685daba5548bea75adf80"
 | 
			
		||||
  integrity sha512-f4sekUcDPF+T+GdbbE8idb1i2YplBAoH+SfRS0e/WRBWb2rYb1Jf5Pimll0Rj+3JgIYWwG2K5LtBPCXxoibkLg==
 | 
			
		||||
vite-plugin-checker@^0.11.0:
 | 
			
		||||
  version "0.11.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/vite-plugin-checker/-/vite-plugin-checker-0.11.0.tgz#063ce180c3751b790a6472e19c5d1a352b215070"
 | 
			
		||||
  integrity sha512-iUdO9Pl9UIBRPAragwi3as/BXXTtRu4G12L3CMrjx+WVTd9g/MsqNakreib9M/2YRVkhZYiTEwdH2j4Dm0w7lw==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@babel/code-frame" "^7.27.1"
 | 
			
		||||
    chokidar "^4.0.3"
 | 
			
		||||
    npm-run-path "^6.0.0"
 | 
			
		||||
    picocolors "^1.1.1"
 | 
			
		||||
    picomatch "^4.0.3"
 | 
			
		||||
    strip-ansi "^7.1.0"
 | 
			
		||||
    tiny-invariant "^1.3.3"
 | 
			
		||||
    tinyglobby "^0.2.14"
 | 
			
		||||
    vscode-uri "^3.1.0"
 | 
			
		||||
@@ -2764,10 +2751,10 @@ vite-tsconfig-paths@^5.1.4:
 | 
			
		||||
  optionalDependencies:
 | 
			
		||||
    fsevents "~2.3.3"
 | 
			
		||||
 | 
			
		||||
vite@^7.1.6:
 | 
			
		||||
  version "7.1.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/vite/-/vite-7.1.6.tgz#336806d29983135677f498a05efb0fd46c5eef2d"
 | 
			
		||||
  integrity sha512-SRYIB8t/isTwNn8vMB3MR6E+EQZM/WG1aKmmIUCfDXfVvKfc20ZpamngWHKzAmmu9ppsgxsg4b2I7c90JZudIQ==
 | 
			
		||||
vite@^7.1.10:
 | 
			
		||||
  version "7.1.10"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/vite/-/vite-7.1.10.tgz#47c9970f3b0fe9057bfbcfeff8cd370edd7bd41b"
 | 
			
		||||
  integrity sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    esbuild "^0.25.0"
 | 
			
		||||
    fdir "^6.5.0"
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user