From 227e818040367e83a307bb11021a88aa833993b3 Mon Sep 17 00:00:00 2001 From: Jamie Curnow Date: Thu, 2 Oct 2025 23:06:51 +1000 Subject: [PATCH] Wrap intl in span identifying translation --- frontend/biome.json | 3 +- frontend/src/api/backend/models.ts | 2 +- frontend/src/components/ErrorNotFound.tsx | 12 +- frontend/src/components/Form/AccessField.tsx | 99 +++++++ .../src/components/Form/BasicAuthField.tsx | 36 +++ .../src/components/Form/DNSProviderFields.tsx | 2 +- .../src/components/Form/DomainNamesField.tsx | 19 +- .../src/components/Form/NginxConfigField.tsx | 4 +- .../components/Form/SSLCertificateField.tsx | 4 +- .../src/components/Form/SSLOptionsFields.tsx | 12 +- frontend/src/components/Form/index.ts | 2 + frontend/src/components/HasPermission.tsx | 8 +- frontend/src/components/Loading.tsx | 7 +- frontend/src/components/LocalePicker.tsx | 25 +- frontend/src/components/SiteFooter.tsx | 4 +- frontend/src/components/SiteHeader.tsx | 12 +- frontend/src/components/SiteMenu.tsx | 12 +- .../Table/Formatter/CertificateFormatter.tsx | 8 +- .../Table/Formatter/DomainsFormatter.tsx | 4 +- .../Table/Formatter/EnabledFormatter.tsx | 12 +- .../Table/Formatter/EventFormatter.tsx | 10 +- .../Table/Formatter/RolesFormatter.tsx | 4 +- .../Table/Formatter/StatusFormatter.tsx | 12 +- .../Formatter/ValueWithDateFormatter.tsx | 6 +- frontend/src/hooks/index.ts | 1 + frontend/src/hooks/useAccessList.ts | 53 ++++ frontend/src/locale/IntlProvider.tsx | 8 +- frontend/src/locale/lang/en.json | 19 +- frontend/src/locale/src/en.json | 51 +++- frontend/src/modals/AccessListModal.tsx | 243 ++++++++++++++++++ frontend/src/modals/ChangePasswordModal.tsx | 24 +- frontend/src/modals/DeadHostModal.tsx | 19 +- frontend/src/modals/DeleteConfirmModal.tsx | 14 +- frontend/src/modals/EventDetailsModal.tsx | 8 +- frontend/src/modals/PermissionsModal.tsx | 38 +-- frontend/src/modals/ProxyHostModal.tsx | 53 ++-- frontend/src/modals/RedirectionHostModal.tsx | 39 +-- frontend/src/modals/SetPasswordModal.tsx | 31 +-- frontend/src/modals/StreamModal.tsx | 38 ++- frontend/src/modals/UserModal.tsx | 22 +- frontend/src/modals/index.ts | 1 + frontend/src/pages/Access/Empty.tsx | 26 +- frontend/src/pages/Access/Table.tsx | 78 +++--- frontend/src/pages/Access/TableWrapper.tsx | 89 +++++-- frontend/src/pages/AuditLog/Table.tsx | 4 +- frontend/src/pages/AuditLog/TableWrapper.tsx | 6 +- frontend/src/pages/Certificates/Empty.tsx | 72 ++++-- frontend/src/pages/Certificates/Table.tsx | 15 +- .../src/pages/Certificates/TableWrapper.tsx | 12 +- frontend/src/pages/Dashboard/index.tsx | 33 +-- frontend/src/pages/Login/index.tsx | 12 +- frontend/src/pages/Nginx/DeadHosts/Empty.tsx | 16 +- frontend/src/pages/Nginx/DeadHosts/Table.tsx | 17 +- .../pages/Nginx/DeadHosts/TableWrapper.tsx | 13 +- frontend/src/pages/Nginx/ProxyHosts/Empty.tsx | 16 +- frontend/src/pages/Nginx/ProxyHosts/Table.tsx | 17 +- .../pages/Nginx/ProxyHosts/TableWrapper.tsx | 13 +- .../pages/Nginx/RedirectionHosts/Empty.tsx | 16 +- .../pages/Nginx/RedirectionHosts/Table.tsx | 17 +- .../Nginx/RedirectionHosts/TableWrapper.tsx | 13 +- frontend/src/pages/Nginx/Streams/Empty.tsx | 16 +- frontend/src/pages/Nginx/Streams/Table.tsx | 19 +- .../src/pages/Nginx/Streams/TableWrapper.tsx | 12 +- frontend/src/pages/Settings/SettingTable.tsx | 6 +- frontend/src/pages/Setup/index.tsx | 18 +- frontend/src/pages/Users/Empty.tsx | 16 +- frontend/src/pages/Users/Table.tsx | 21 +- frontend/src/pages/Users/TableWrapper.tsx | 12 +- 68 files changed, 1076 insertions(+), 510 deletions(-) create mode 100644 frontend/src/components/Form/AccessField.tsx create mode 100644 frontend/src/components/Form/BasicAuthField.tsx create mode 100644 frontend/src/hooks/useAccessList.ts create mode 100644 frontend/src/modals/AccessListModal.tsx diff --git a/frontend/biome.json b/frontend/biome.json index 217c0d54..0e3d8f39 100644 --- a/frontend/biome.json +++ b/frontend/biome.json @@ -64,7 +64,8 @@ "useUniqueElementIds": "off" }, "suspicious": { - "noExplicitAny": "off" + "noExplicitAny": "off", + "noArrayIndexKey": "off" }, "performance": { "noDelete": "off" diff --git a/frontend/src/api/backend/models.ts b/frontend/src/api/backend/models.ts index 4667d445..7283a535 100644 --- a/frontend/src/api/backend/models.ts +++ b/frontend/src/api/backend/models.ts @@ -53,7 +53,7 @@ export interface AccessList { meta: Record; satisfyAny: boolean; passAuth: boolean; - proxyHostCount: number; + proxyHostCount?: number; // Expansions: owner?: User; items?: AccessListItem[]; diff --git a/frontend/src/components/ErrorNotFound.tsx b/frontend/src/components/ErrorNotFound.tsx index 5b92b6d5..8cdf816d 100644 --- a/frontend/src/components/ErrorNotFound.tsx +++ b/frontend/src/components/ErrorNotFound.tsx @@ -1,6 +1,6 @@ import { useNavigate } from "react-router-dom"; import { Button } from "src/components"; -import { intl } from "src/locale"; +import { T } from "src/locale"; export function ErrorNotFound() { const navigate = useNavigate(); @@ -8,11 +8,15 @@ export function ErrorNotFound() { return (
-

{intl.formatMessage({ id: "notfound.title" })}

-

{intl.formatMessage({ id: "notfound.text" })}

+

+ +

+

+ +

diff --git a/frontend/src/components/Form/AccessField.tsx b/frontend/src/components/Form/AccessField.tsx new file mode 100644 index 00000000..13412a77 --- /dev/null +++ b/frontend/src/components/Form/AccessField.tsx @@ -0,0 +1,99 @@ +import { IconLock, IconLockOpen2 } from "@tabler/icons-react"; +import { Field, useFormikContext } from "formik"; +import type { ReactNode } from "react"; +import Select, { type ActionMeta, components, type OptionProps } from "react-select"; +import type { AccessList } from "src/api/backend"; +import { useAccessLists } from "src/hooks"; +import { DateTimeFormat, intl, T } from "src/locale"; + +interface AccessOption { + readonly value: number; + readonly label: string; + readonly subLabel: string; + readonly icon: ReactNode; +} + +const Option = (props: OptionProps) => { + return ( + +
+
+ {props.data.icon} {props.data.label} +
+
{props.data.subLabel}
+
+
+ ); +}; + +interface Props { + id?: string; + name?: string; + label?: string; +} +export function AccessField({ name = "accessListId", label = "access.title", id = "accessListId" }: Props) { + const { isLoading, isError, error, data } = useAccessLists(); + const { setFieldValue } = useFormikContext(); + + const handleChange = (newValue: any, _actionMeta: ActionMeta) => { + setFieldValue(name, newValue?.value); + }; + + const options: AccessOption[] = + data?.map((item: AccessList) => ({ + value: item.id || 0, + label: item.name, + subLabel: intl.formatMessage( + { id: "access.subtitle" }, + { + users: item?.items?.length, + rules: item?.clients?.length, + date: item?.createdOn ? DateTimeFormat(item?.createdOn) : "N/A", + }, + ), + icon: , + })) || []; + + // Public option + options?.unshift({ + value: 0, + label: intl.formatMessage({ id: "access.public" }), + subLabel: "No basic auth required", + icon: , + }); + + return ( + + {({ field, form }: any) => ( +
+ + {isLoading ?
: null} + {isError ?
{`${error}`}
: null} + {!isLoading && !isError ? ( + +
+
+ +
+
+ + + ); +} diff --git a/frontend/src/components/Form/DNSProviderFields.tsx b/frontend/src/components/Form/DNSProviderFields.tsx index 2811c604..6f6d963d 100644 --- a/frontend/src/components/Form/DNSProviderFields.tsx +++ b/frontend/src/components/Form/DNSProviderFields.tsx @@ -10,7 +10,6 @@ interface DNSProviderOption { readonly label: string; readonly credentials: string; } - export function DNSProviderFields() { const { values, setFieldValue } = useFormikContext(); const { data: dnsProviders, isLoading } = useDnsProviders(); @@ -100,6 +99,7 @@ export function DNSProviderFields() { ); } if (!isWildcardPermitted) { - helperTexts.push(intl.formatMessage({ id: "domain-names.wildcards-not-permitted" })); + helperTexts.push(); } else if (!dnsProviderWildcardSupported) { - helperTexts.push(intl.formatMessage({ id: "domain-names.wildcards-not-supported" })); + helperTexts.push(); } return ( @@ -50,7 +51,7 @@ export function DomainNamesField({ {({ field, form }: any) => (
{form.errors[field.name]} ) : helperTexts.length ? ( - helperTexts.map((i) => ( - + helperTexts.map((i, idx) => ( + {i} )) diff --git a/frontend/src/components/Form/NginxConfigField.tsx b/frontend/src/components/Form/NginxConfigField.tsx index 2c2557d7..41064f22 100644 --- a/frontend/src/components/Form/NginxConfigField.tsx +++ b/frontend/src/components/Form/NginxConfigField.tsx @@ -1,6 +1,6 @@ import CodeEditor from "@uiw/react-textarea-code-editor"; import { Field } from "formik"; -import { intl } from "src/locale"; +import { intl, T } from "src/locale"; interface Props { id?: string; @@ -17,7 +17,7 @@ export function NginxConfigField({ {({ field }: any) => (
(
{isLoading ?
: null} {isError ?
{`${error}`}
: null} diff --git a/frontend/src/components/Form/SSLOptionsFields.tsx b/frontend/src/components/Form/SSLOptionsFields.tsx index 9b3a6c15..0a5af745 100644 --- a/frontend/src/components/Form/SSLOptionsFields.tsx +++ b/frontend/src/components/Form/SSLOptionsFields.tsx @@ -1,7 +1,7 @@ import cn from "classnames"; import { Field, useFormikContext } from "formik"; import { DNSProviderFields, DomainNamesField } from "src/components"; -import { intl } from "src/locale"; +import { T } from "src/locale"; interface Props { forHttp?: boolean; // the sslForced, http2Support, hstsEnabled, hstsSubdomains fields @@ -49,7 +49,7 @@ export function SSLOptionsFields({ forHttp = true, forceDNSForNew, requireDomain disabled={!hasCertificate} /> - {intl.formatMessage({ id: "domains.force-ssl" })} + )} @@ -67,7 +67,7 @@ export function SSLOptionsFields({ forHttp = true, forceDNSForNew, requireDomain disabled={!hasCertificate} /> - {intl.formatMessage({ id: "domains.http2-support" })} + )} @@ -87,7 +87,7 @@ export function SSLOptionsFields({ forHttp = true, forceDNSForNew, requireDomain disabled={!hasCertificate || !sslForced} /> - {intl.formatMessage({ id: "domains.hsts-enabled" })} + )} @@ -105,7 +105,7 @@ export function SSLOptionsFields({ forHttp = true, forceDNSForNew, requireDomain disabled={!hasCertificate || !hstsEnabled} /> - {intl.formatMessage({ id: "domains.hsts-subdomains" })} + )} @@ -131,7 +131,7 @@ export function SSLOptionsFields({ forHttp = true, forceDNSForNew, requireDomain onChange={(e) => handleToggleChange(e, field.name)} /> - {intl.formatMessage({ id: "domains.use-dns" })} + )} diff --git a/frontend/src/components/Form/index.ts b/frontend/src/components/Form/index.ts index 6bfb15fd..16eaa7f9 100644 --- a/frontend/src/components/Form/index.ts +++ b/frontend/src/components/Form/index.ts @@ -1,3 +1,5 @@ +export * from "./AccessField"; +export * from "./BasicAuthField"; export * from "./DNSProviderFields"; export * from "./DomainNamesField"; export * from "./NginxConfigField"; diff --git a/frontend/src/components/HasPermission.tsx b/frontend/src/components/HasPermission.tsx index c4779b9e..7c348c45 100644 --- a/frontend/src/components/HasPermission.tsx +++ b/frontend/src/components/HasPermission.tsx @@ -2,7 +2,7 @@ import type { ReactNode } from "react"; import Alert from "react-bootstrap/Alert"; import { Loading, LoadingPage } from "src/components"; import { useUser } from "src/hooks"; -import { intl } from "src/locale"; +import { T } from "src/locale"; interface Props { permission: string; @@ -64,7 +64,11 @@ function HasPermission({ return <>{children}; } - return !hideError ? {intl.formatMessage({ id: "no-permission-error" })} : null; + return !hideError ? ( + + + + ) : null; } export { HasPermission }; diff --git a/frontend/src/components/Loading.tsx b/frontend/src/components/Loading.tsx index 35a054db..14d96e6d 100644 --- a/frontend/src/components/Loading.tsx +++ b/frontend/src/components/Loading.tsx @@ -1,8 +1,9 @@ -import { intl } from "src/locale"; +import type { ReactNode } from "react"; +import { T } from "src/locale"; import styles from "./Loading.module.css"; interface Props { - label?: string; + label?: string | ReactNode; noLogo?: boolean; } export function Loading({ label, noLogo }: Props) { @@ -13,7 +14,7 @@ export function Loading({ label, noLogo }: Props) {
)} -
{label || intl.formatMessage({ id: "loading" })}
+
{label || }
diff --git a/frontend/src/components/LocalePicker.tsx b/frontend/src/components/LocalePicker.tsx index 7c3876d8..aaa1dc53 100644 --- a/frontend/src/components/LocalePicker.tsx +++ b/frontend/src/components/LocalePicker.tsx @@ -2,7 +2,7 @@ import cn from "classnames"; import { Flag } from "src/components"; import { useLocaleState } from "src/context"; import { useTheme } from "src/hooks"; -import { changeLocale, getFlagCodeForLocale, intl, localeOptions } from "src/locale"; +import { changeLocale, getFlagCodeForLocale, localeOptions, T } from "src/locale"; import styles from "./LocalePicker.module.css"; function LocalePicker() { @@ -35,34 +35,13 @@ function LocalePicker() { changeTo(item[0]); }} > - {" "} - {intl.formatMessage({ id: `locale-${item[1]}` })} + ); })}
); - - //
- // - // - // - // - // - // {localeOptions.map((item) => { - // return ( - // } - // onClick={() => changeTo(item[0])} - // key={`locale-${item[0]}`}> - // {intl.formatMessage({ id: `locale-${item[1]}` })} - // - // ); - // })} - // - // - // } export { LocalePicker }; diff --git a/frontend/src/components/SiteFooter.tsx b/frontend/src/components/SiteFooter.tsx index 57bc8b74..a1778395 100644 --- a/frontend/src/components/SiteFooter.tsx +++ b/frontend/src/components/SiteFooter.tsx @@ -1,5 +1,5 @@ import { useHealth } from "src/hooks"; -import { intl } from "src/locale"; +import { T } from "src/locale"; export function SiteFooter() { const health = useHealth(); @@ -25,7 +25,7 @@ export function SiteFooter() { className="link-secondary" rel="noopener" > - {intl.formatMessage({ id: "footer.github-fork" })} + diff --git a/frontend/src/components/SiteHeader.tsx b/frontend/src/components/SiteHeader.tsx index d60e26e5..0edb286e 100644 --- a/frontend/src/components/SiteHeader.tsx +++ b/frontend/src/components/SiteHeader.tsx @@ -3,7 +3,7 @@ import { useState } from "react"; import { LocalePicker, ThemeSwitcher } from "src/components"; import { useAuthState } from "src/context"; import { useUser } from "src/hooks"; -import { intl } from "src/locale"; +import { T } from "src/locale"; import { ChangePasswordModal, UserModal } from "src/modals"; import styles from "./SiteHeader.module.css"; @@ -66,9 +66,7 @@ export function SiteHeader() {
{currentUser?.nickname}
- {intl.formatMessage({ - id: isAdmin ? "role.admin" : "role.standard-user", - })} +
@@ -82,7 +80,7 @@ export function SiteHeader() { }} > - {intl.formatMessage({ id: "user.edit-profile" })} + - {intl.formatMessage({ id: "user.change-password" })} +
diff --git a/frontend/src/components/SiteMenu.tsx b/frontend/src/components/SiteMenu.tsx index 1ceb8145..63e12738 100644 --- a/frontend/src/components/SiteMenu.tsx +++ b/frontend/src/components/SiteMenu.tsx @@ -10,7 +10,7 @@ import { import cn from "classnames"; import React from "react"; import { HasPermission, NavLink } from "src/components"; -import { intl } from "src/locale"; +import { T } from "src/locale"; interface MenuItem { label: string; @@ -108,7 +108,9 @@ const getMenuItem = (item: MenuItem, onClick?: () => void) => { {item.icon && React.createElement(item.icon, { height: 24, width: 24 })} - {intl.formatMessage({ id: item.label })} + + + @@ -136,7 +138,9 @@ const getMenuDropown = (item: MenuItem, onClick?: () => void) => { - {intl.formatMessage({ id: item.label })} + + +
{item.items?.map((subitem, idx) => { @@ -148,7 +152,7 @@ const getMenuDropown = (item: MenuItem, onClick?: () => void) => { hideError > - {intl.formatMessage({ id: subitem.label })} + ); diff --git a/frontend/src/components/Table/Formatter/CertificateFormatter.tsx b/frontend/src/components/Table/Formatter/CertificateFormatter.tsx index daa4b471..d793dae8 100644 --- a/frontend/src/components/Table/Formatter/CertificateFormatter.tsx +++ b/frontend/src/components/Table/Formatter/CertificateFormatter.tsx @@ -1,13 +1,9 @@ import type { Certificate } from "src/api/backend"; -import { intl } from "src/locale"; +import { T } from "src/locale"; interface Props { certificate?: Certificate; } export function CertificateFormatter({ certificate }: Props) { - if (certificate) { - return intl.formatMessage({ id: "lets-encrypt" }); - } - - return intl.formatMessage({ id: "http-only" }); + return ; } diff --git a/frontend/src/components/Table/Formatter/DomainsFormatter.tsx b/frontend/src/components/Table/Formatter/DomainsFormatter.tsx index ca7735f1..4cb58bc6 100644 --- a/frontend/src/components/Table/Formatter/DomainsFormatter.tsx +++ b/frontend/src/components/Table/Formatter/DomainsFormatter.tsx @@ -1,4 +1,4 @@ -import { DateTimeFormat, intl } from "src/locale"; +import { DateTimeFormat, T } from "src/locale"; interface Props { domains: string[]; @@ -34,7 +34,7 @@ export function DomainsFormatter({ domains, createdOn }: Props) {
{createdOn ? (
- {intl.formatMessage({ id: "created-on" }, { date: DateTimeFormat(createdOn) })} +
) : null}
diff --git a/frontend/src/components/Table/Formatter/EnabledFormatter.tsx b/frontend/src/components/Table/Formatter/EnabledFormatter.tsx index ccd60489..ca29c50a 100644 --- a/frontend/src/components/Table/Formatter/EnabledFormatter.tsx +++ b/frontend/src/components/Table/Formatter/EnabledFormatter.tsx @@ -1,11 +1,13 @@ -import { intl } from "src/locale"; +import cn from "classnames"; +import { T } from "src/locale"; interface Props { enabled: boolean; } export function EnabledFormatter({ enabled }: Props) { - if (enabled) { - return {intl.formatMessage({ id: "enabled" })}; - } - return {intl.formatMessage({ id: "disabled" })}; + return ( + + + + ); } diff --git a/frontend/src/components/Table/Formatter/EventFormatter.tsx b/frontend/src/components/Table/Formatter/EventFormatter.tsx index 1dc0fe69..a296e04b 100644 --- a/frontend/src/components/Table/Formatter/EventFormatter.tsx +++ b/frontend/src/components/Table/Formatter/EventFormatter.tsx @@ -1,10 +1,6 @@ import { IconArrowsCross, IconBolt, IconBoltOff, IconDisc, IconUser } from "@tabler/icons-react"; import type { AuditLog } from "src/api/backend"; -import { DateTimeFormat, intl } from "src/locale"; - -const getEventTitle = (event: AuditLog) => ( - {intl.formatMessage({ id: `event.${event.action}-${event.objectType}` })} -); +import { DateTimeFormat, T } from "src/locale"; const getEventValue = (event: AuditLog) => { switch (event.objectType) { @@ -63,7 +59,9 @@ export function EventFormatter({ row }: Props) { return (
- {getIcon(row)} {getEventTitle(row)} — {getEventValue(row)} + {getIcon(row)} + + — {getEventValue(row)}
{DateTimeFormat(row.createdOn)}
diff --git a/frontend/src/components/Table/Formatter/RolesFormatter.tsx b/frontend/src/components/Table/Formatter/RolesFormatter.tsx index a2464863..2e025605 100644 --- a/frontend/src/components/Table/Formatter/RolesFormatter.tsx +++ b/frontend/src/components/Table/Formatter/RolesFormatter.tsx @@ -1,4 +1,4 @@ -import { intl } from "src/locale"; +import { T } from "src/locale"; interface Props { roles: string[]; @@ -12,7 +12,7 @@ export function RolesFormatter({ roles }: Props) { <> {r.map((role: string) => ( - {intl.formatMessage({ id: `role.${role}` })} + ))} diff --git a/frontend/src/components/Table/Formatter/StatusFormatter.tsx b/frontend/src/components/Table/Formatter/StatusFormatter.tsx index 3ea1f5c7..d602a8ed 100644 --- a/frontend/src/components/Table/Formatter/StatusFormatter.tsx +++ b/frontend/src/components/Table/Formatter/StatusFormatter.tsx @@ -1,11 +1,13 @@ -import { intl } from "src/locale"; +import cn from "classnames"; +import { T } from "src/locale"; interface Props { enabled: boolean; } export function StatusFormatter({ enabled }: Props) { - if (enabled) { - return {intl.formatMessage({ id: "online" })}; - } - return {intl.formatMessage({ id: "offline" })}; + return ( + + + + ); } diff --git a/frontend/src/components/Table/Formatter/ValueWithDateFormatter.tsx b/frontend/src/components/Table/Formatter/ValueWithDateFormatter.tsx index a01ab640..e4e7fb27 100644 --- a/frontend/src/components/Table/Formatter/ValueWithDateFormatter.tsx +++ b/frontend/src/components/Table/Formatter/ValueWithDateFormatter.tsx @@ -1,4 +1,4 @@ -import { DateTimeFormat, intl } from "src/locale"; +import { DateTimeFormat, T } from "src/locale"; interface Props { value: string; @@ -13,9 +13,7 @@ export function ValueWithDateFormatter({ value, createdOn, disabled }: Props) {
{createdOn ? (
- {disabled - ? intl.formatMessage({ id: "disabled" }) - : intl.formatMessage({ id: "created-on" }, { date: DateTimeFormat(createdOn) })} +
) : null}
diff --git a/frontend/src/hooks/index.ts b/frontend/src/hooks/index.ts index e4f23779..12e617c7 100644 --- a/frontend/src/hooks/index.ts +++ b/frontend/src/hooks/index.ts @@ -1,3 +1,4 @@ +export * from "./useAccessList"; export * from "./useAccessLists"; export * from "./useAuditLog"; export * from "./useAuditLogs"; diff --git a/frontend/src/hooks/useAccessList.ts b/frontend/src/hooks/useAccessList.ts new file mode 100644 index 00000000..90a16144 --- /dev/null +++ b/frontend/src/hooks/useAccessList.ts @@ -0,0 +1,53 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { type AccessList, createAccessList, getAccessList, updateAccessList } from "src/api/backend"; + +const fetchAccessList = (id: number | "new") => { + if (id === "new") { + return Promise.resolve({ + id: 0, + createdOn: "", + modifiedOn: "", + ownerUserId: 0, + name: "", + satisfyAny: false, + passAuth: false, + meta: {}, + } as AccessList); + } + return getAccessList(id, ["owner"]); +}; + +const useAccessList = (id: number | "new", options = {}) => { + return useQuery({ + queryKey: ["access-list", id], + queryFn: () => fetchAccessList(id), + staleTime: 60 * 1000, // 1 minute + ...options, + }); +}; + +const useSetAccessList = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (values: AccessList) => (values.id ? updateAccessList(values) : createAccessList(values)), + onMutate: (values: AccessList) => { + if (!values.id) { + return; + } + const previousObject = queryClient.getQueryData(["access-list", values.id]); + queryClient.setQueryData(["access-list", values.id], (old: AccessList) => ({ + ...old, + ...values, + })); + return () => queryClient.setQueryData(["access-list", values.id], previousObject); + }, + onError: (_, __, rollback: any) => rollback(), + onSuccess: async ({ id }: AccessList) => { + queryClient.invalidateQueries({ queryKey: ["access-list", id] }); + queryClient.invalidateQueries({ queryKey: ["access-list"] }); + queryClient.invalidateQueries({ queryKey: ["audit-logs"] }); + }, + }); +}; + +export { useAccessList, useSetAccessList }; diff --git a/frontend/src/locale/IntlProvider.tsx b/frontend/src/locale/IntlProvider.tsx index ee256859..a6160b2a 100644 --- a/frontend/src/locale/IntlProvider.tsx +++ b/frontend/src/locale/IntlProvider.tsx @@ -61,4 +61,10 @@ const changeLocale = (locale: string): void => { document.documentElement.lang = locale; }; -export { localeOptions, getFlagCodeForLocale, getLocale, createIntl, changeLocale, intl }; +// 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 {intl.formatMessage({ id }, data)}; +}; + +export { localeOptions, getFlagCodeForLocale, getLocale, createIntl, changeLocale, intl, T }; diff --git a/frontend/src/locale/lang/en.json b/frontend/src/locale/lang/en.json index 9ff54429..729e8924 100644 --- a/frontend/src/locale/lang/en.json +++ b/frontend/src/locale/lang/en.json @@ -3,9 +3,13 @@ "access.actions-title": "Access List #{id}", "access.add": "Add Access List", "access.auth-count": "{count} Users", + "access.edit": "Edit Access", "access.empty": "There are no Access Lists", - "access.satisfy-all": "All", - "access.satisfy-any": "Any", + "access.new": "New Access", + "access.pass-auth": "Pass Auth to Upstream", + "access.public": "Publicly Accessible", + "access.satisfy-any": "Satisfy Any", + "access.subtitle": "{users} User, {rules} Rules - Created: {date}", "access.title": "Access", "action.delete": "Delete", "action.disable": "Disable", @@ -23,6 +27,7 @@ "close": "Close", "column.access": "Access", "column.authorization": "Authorization", + "column.authorizations": "Authorizations", "column.custom-locations": "Custom Locations", "column.destination": "Destination", "column.details": "Details", @@ -35,7 +40,10 @@ "column.protocol": "Protocol", "column.provider": "Provider", "column.roles": "Roles", + "column.rules": "Rules", "column.satisfy": "Satisfy", + "column.satisfy-all": "All", + "column.satisfy-any": "Any", "column.scheme": "Scheme", "column.source": "Source", "column.ssl": "SSL", @@ -88,11 +96,11 @@ "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", "host.flags.protocols": "Protocols", - "host.flags.title": "Options", "host.flags.websockets-upgrade": "Websockets Support", "host.forward-port": "Forward Port", "host.forward-scheme": "Scheme", @@ -107,6 +115,7 @@ "notfound.action": "Take me home", "notfound.text": "We are sorry but the page you are looking for was not found", "notfound.title": "Oops… You just found an error page", + "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", @@ -140,6 +149,8 @@ "proxy-hosts.count": "{count} 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", "redirection-host.forward-domain": "Forward Domain", "redirection-host.new": "New Redirection Host", "redirection-hosts.actions-title": "Redirection Host #{id}", @@ -157,6 +168,7 @@ "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.forward-host": "Forward Host", "stream.incoming-port": "Incoming Port", "stream.new": "New Stream", @@ -184,6 +196,7 @@ "user.set-permissions": "Set Permissions for {name}", "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", diff --git a/frontend/src/locale/src/en.json b/frontend/src/locale/src/en.json index 6bc29e90..021b854e 100644 --- a/frontend/src/locale/src/en.json +++ b/frontend/src/locale/src/en.json @@ -11,14 +11,26 @@ "access.auth-count": { "defaultMessage": "{count} Users" }, + "access.edit": { + "defaultMessage": "Edit Access" + }, "access.empty": { "defaultMessage": "There are no Access Lists" }, - "access.satisfy-all": { - "defaultMessage": "All" + "access.new": { + "defaultMessage": "New Access" + }, + "access.pass-auth": { + "defaultMessage": "Pass Auth to Upstream" + }, + "access.public": { + "defaultMessage": "Publicly Accessible" }, "access.satisfy-any": { - "defaultMessage": "Any" + "defaultMessage": "Satisfy Any" + }, + "access.subtitle": { + "defaultMessage": "{users} User, {rules} Rules - Created: {date}" }, "access.title": { "defaultMessage": "Access" @@ -71,6 +83,9 @@ "column.authorization": { "defaultMessage": "Authorization" }, + "column.authorizations": { + "defaultMessage": "Authorizations" + }, "column.custom-locations": { "defaultMessage": "Custom Locations" }, @@ -107,9 +122,18 @@ "column.roles": { "defaultMessage": "Roles" }, + "column.rules": { + "defaultMessage": "Rules" + }, "column.satisfy": { "defaultMessage": "Satisfy" }, + "column.satisfy-all": { + "defaultMessage": "All" + }, + "column.satisfy-any": { + "defaultMessage": "Any" + }, "column.scheme": { "defaultMessage": "Scheme" }, @@ -266,6 +290,9 @@ "footer.github-fork": { "defaultMessage": "Fork me on Github" }, + "generic.flags.title": { + "defaultMessage": "Options" + }, "host.flags.block-exploits": { "defaultMessage": "Block Common Exploits" }, @@ -278,9 +305,6 @@ "host.flags.protocols": { "defaultMessage": "Protocols" }, - "host.flags.title": { - "defaultMessage": "Options" - }, "host.flags.websockets-upgrade": { "defaultMessage": "Websockets Support" }, @@ -323,6 +347,9 @@ "notfound.title": { "defaultMessage": "Oops… You just found an error page" }, + "notification.access-saved": { + "defaultMessage": "Access has been saved" + }, "notification.dead-host-saved": { "defaultMessage": "404 Host has been saved" }, @@ -422,6 +449,12 @@ "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.forward-domain": { "defaultMessage": "Forward Domain" }, @@ -473,6 +506,9 @@ "stream.delete.title": { "defaultMessage": "Delete Stream" }, + "stream.edit": { + "defaultMessage": "Edit Stream" + }, "stream.forward-host": { "defaultMessage": "Forward Host" }, @@ -554,6 +590,9 @@ "user.switch-light": { "defaultMessage": "Switch to Light mode" }, + "username": { + "defaultMessage": "Username" + }, "users.actions-title": { "defaultMessage": "User #{id}" }, diff --git a/frontend/src/modals/AccessListModal.tsx b/frontend/src/modals/AccessListModal.tsx new file mode 100644 index 00000000..0ae37c9b --- /dev/null +++ b/frontend/src/modals/AccessListModal.tsx @@ -0,0 +1,243 @@ +import cn from "classnames"; +import { Field, Form, Formik } from "formik"; +import { type ReactNode, useState } from "react"; +import { Alert } from "react-bootstrap"; +import Modal from "react-bootstrap/Modal"; +import { BasicAuthField, Button, Loading } from "src/components"; +import { useAccessList, useSetAccessList } from "src/hooks"; +import { intl, T } from "src/locale"; +import { validateString } from "src/modules/Validations"; +import { showSuccess } from "src/notifications"; + +interface Props { + id: number | "new"; + onClose: () => void; +} +export function AccessListModal({ id, onClose }: Props) { + const { data, isLoading, error } = useAccessList(id); + const { mutate: setAccessList } = useSetAccessList(); + const [errorMsg, setErrorMsg] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + const onSubmit = async (values: any, { setSubmitting }: any) => { + if (isSubmitting) return; + setIsSubmitting(true); + setErrorMsg(null); + + const { ...payload } = { + id: id === "new" ? undefined : id, + ...values, + }; + + setAccessList(payload, { + onError: (err: any) => setErrorMsg(), + onSuccess: () => { + showSuccess(intl.formatMessage({ id: "notification.access-saved" })); + onClose(); + }, + onSettled: () => { + setIsSubmitting(false); + setSubmitting(false); + }, + }); + }; + + const toggleClasses = "form-check-input"; + const toggleEnabled = cn(toggleClasses, "bg-cyan"); + + return ( + + {!isLoading && error && ( + + {error?.message || "Unknown error"} + + )} + {isLoading && } + {!isLoading && data && ( + + {({ setFieldValue }: any) => ( +
+ + + + + + + setErrorMsg(null)} dismissible> + {errorMsg} + +
+
+ +
+
+
+
+ + {({ field }: any) => ( +
+ + +
+ )} +
+
+

+ +

+
+
+ +
+
+ +
+
+
+
+
+ +
+
+ todo +
+
+
+
+
+ + + + +
+ )} +
+ )} +
+ ); +} diff --git a/frontend/src/modals/ChangePasswordModal.tsx b/frontend/src/modals/ChangePasswordModal.tsx index 26f20b58..a39abf38 100644 --- a/frontend/src/modals/ChangePasswordModal.tsx +++ b/frontend/src/modals/ChangePasswordModal.tsx @@ -1,10 +1,10 @@ import { Field, Form, Formik } from "formik"; -import { useState } from "react"; +import { type ReactNode, useState } from "react"; import { Alert } from "react-bootstrap"; import Modal from "react-bootstrap/Modal"; import { updateAuth } from "src/api/backend"; import { Button } from "src/components"; -import { intl } from "src/locale"; +import { intl, T } from "src/locale"; import { validateString } from "src/modules/Validations"; interface Props { @@ -12,12 +12,12 @@ interface Props { onClose: () => void; } export function ChangePasswordModal({ userId, onClose }: Props) { - const [error, setError] = useState(null); + const [error, setError] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const onSubmit = async (values: any, { setSubmitting }: any) => { if (values.new !== values.confirm) { - setError(intl.formatMessage({ id: "error.passwords-must-match" })); + setError(); setSubmitting(false); return; } @@ -30,7 +30,7 @@ export function ChangePasswordModal({ userId, onClose }: Props) { await updateAuth(userId, values.new, values.current); onClose(); } catch (err: any) { - setError(intl.formatMessage({ id: err.message })); + setError(); } setIsSubmitting(false); setSubmitting(false); @@ -51,7 +51,9 @@ export function ChangePasswordModal({ userId, onClose }: Props) { {() => (
- {intl.formatMessage({ id: "user.change-password" })} + + + setError(null)} dismissible> @@ -72,7 +74,7 @@ export function ChangePasswordModal({ userId, onClose }: Props) { {...field} /> {form.errors.name ? (
@@ -98,7 +100,7 @@ export function ChangePasswordModal({ userId, onClose }: Props) { {...field} /> {form.errors.new ? (
@@ -129,7 +131,7 @@ export function ChangePasswordModal({ userId, onClose }: Props) {
) : null}
)} @@ -138,7 +140,7 @@ export function ChangePasswordModal({ userId, onClose }: Props) {
diff --git a/frontend/src/modals/DeadHostModal.tsx b/frontend/src/modals/DeadHostModal.tsx index 1a420007..70364cc6 100644 --- a/frontend/src/modals/DeadHostModal.tsx +++ b/frontend/src/modals/DeadHostModal.tsx @@ -1,6 +1,6 @@ import { IconSettings } from "@tabler/icons-react"; import { Form, Formik } from "formik"; -import { useState } from "react"; +import { type ReactNode, useState } from "react"; import { Alert } from "react-bootstrap"; import Modal from "react-bootstrap/Modal"; import { @@ -12,7 +12,7 @@ import { SSLOptionsFields, } from "src/components"; import { useDeadHost, useSetDeadHost } from "src/hooks"; -import { intl } from "src/locale"; +import { intl, T } from "src/locale"; import { showSuccess } from "src/notifications"; interface Props { @@ -22,7 +22,7 @@ interface Props { export function DeadHostModal({ id, onClose }: Props) { const { data, isLoading, error } = useDeadHost(id); const { mutate: setDeadHost } = useSetDeadHost(); - const [errorMsg, setErrorMsg] = useState(null); + const [errorMsg, setErrorMsg] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const onSubmit = async (values: any, { setSubmitting }: any) => { @@ -36,7 +36,7 @@ export function DeadHostModal({ id, onClose }: Props) { }; setDeadHost(payload, { - onError: (err: any) => setErrorMsg(err.message), + onError: (err: any) => setErrorMsg(), onSuccess: () => { showSuccess(intl.formatMessage({ id: "notification.dead-host-saved" })); onClose(); @@ -76,14 +76,13 @@ export function DeadHostModal({ id, onClose }: Props) {
- {intl.formatMessage({ id: data?.id ? "dead-host.edit" : "dead-host.new" })} + setErrorMsg(null)} dismissible> {errorMsg} -
    @@ -95,7 +94,7 @@ export function DeadHostModal({ id, onClose }: Props) { aria-selected="true" role="tab" > - {intl.formatMessage({ id: "column.details" })} +
  • @@ -107,7 +106,7 @@ export function DeadHostModal({ id, onClose }: Props) { tabIndex={-1} role="tab" > - {intl.formatMessage({ id: "column.ssl" })} +
  • @@ -147,7 +146,7 @@ export function DeadHostModal({ id, onClose }: Props) {
  • diff --git a/frontend/src/modals/DeleteConfirmModal.tsx b/frontend/src/modals/DeleteConfirmModal.tsx index 0843261d..e0bc7e94 100644 --- a/frontend/src/modals/DeleteConfirmModal.tsx +++ b/frontend/src/modals/DeleteConfirmModal.tsx @@ -3,7 +3,7 @@ import { type ReactNode, useState } from "react"; import { Alert } from "react-bootstrap"; import Modal from "react-bootstrap/Modal"; import { Button } from "src/components"; -import { intl } from "src/locale"; +import { T } from "src/locale"; interface Props { title: string; @@ -14,7 +14,7 @@ interface Props { } export function DeleteConfirmModal({ title, children, onConfirm, onClose, invalidations }: Props) { const queryClient = useQueryClient(); - const [error, setError] = useState(null); + const [error, setError] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const onSubmit = async () => { @@ -29,7 +29,7 @@ export function DeleteConfirmModal({ title, children, onConfirm, onClose, invali queryClient.invalidateQueries({ queryKey: inv }); }); } catch (err: any) { - setError(intl.formatMessage({ id: err.message })); + setError(); } setIsSubmitting(false); }; @@ -37,7 +37,9 @@ export function DeleteConfirmModal({ title, children, onConfirm, onClose, invali return ( - {title} + + + setError(null)} dismissible> @@ -47,7 +49,7 @@ export function DeleteConfirmModal({ title, children, onConfirm, onClose, invali diff --git a/frontend/src/modals/EventDetailsModal.tsx b/frontend/src/modals/EventDetailsModal.tsx index c105c3f5..ac8c6d6c 100644 --- a/frontend/src/modals/EventDetailsModal.tsx +++ b/frontend/src/modals/EventDetailsModal.tsx @@ -2,7 +2,7 @@ import { Alert } from "react-bootstrap"; import Modal from "react-bootstrap/Modal"; import { Button, EventFormatter, GravatarFormatter, Loading } from "src/components"; import { useAuditLog } from "src/hooks"; -import { intl } from "src/locale"; +import { T } from "src/locale"; interface Props { id: number; @@ -22,7 +22,9 @@ export function EventDetailsModal({ id, onClose }: Props) { {!isLoading && data && ( <> - {intl.formatMessage({ id: "action.view-details" })} + + +
    @@ -40,7 +42,7 @@ export function EventDetailsModal({ id, onClose }: Props) { diff --git a/frontend/src/modals/PermissionsModal.tsx b/frontend/src/modals/PermissionsModal.tsx index ce3fb46f..4761e08b 100644 --- a/frontend/src/modals/PermissionsModal.tsx +++ b/frontend/src/modals/PermissionsModal.tsx @@ -1,13 +1,13 @@ import { useQueryClient } from "@tanstack/react-query"; import cn from "classnames"; import { Field, Form, Formik } from "formik"; -import { useState } from "react"; +import { type ReactNode, useState } from "react"; import { Alert } from "react-bootstrap"; import Modal from "react-bootstrap/Modal"; import { setPermissions } from "src/api/backend"; import { Button, Loading } from "src/components"; import { useUser } from "src/hooks"; -import { intl } from "src/locale"; +import { T } from "src/locale"; interface Props { userId: number; @@ -15,7 +15,7 @@ interface Props { } export function PermissionsModal({ userId, onClose }: Props) { const queryClient = useQueryClient(); - const [errorMsg, setErrorMsg] = useState(null); + const [errorMsg, setErrorMsg] = useState(null); const { data, isLoading, error } = useUser(userId); const [isSubmitting, setIsSubmitting] = useState(false); @@ -29,7 +29,7 @@ export function PermissionsModal({ userId, onClose }: Props) { queryClient.invalidateQueries({ queryKey: ["users"] }); queryClient.invalidateQueries({ queryKey: ["user"] }); } catch (err: any) { - setErrorMsg(intl.formatMessage({ id: err.message })); + setErrorMsg(); } setSubmitting(false); setIsSubmitting(false); @@ -50,7 +50,7 @@ export function PermissionsModal({ userId, onClose }: Props) { onChange={() => form.setFieldValue(field.name, "manage")} /> form.setFieldValue(field.name, "view")} /> form.setFieldValue(field.name, "hidden")} />
@@ -112,7 +112,7 @@ export function PermissionsModal({ userId, onClose }: Props) {
- {intl.formatMessage({ id: "user.set-permissions" }, { name: data?.name })} + @@ -121,7 +121,7 @@ export function PermissionsModal({ userId, onClose }: Props) {
{({ field, form }: any) => ( @@ -140,7 +140,7 @@ export function PermissionsModal({ userId, onClose }: Props) { htmlFor={`${field.name}-user`} className={cn("btn", { active: field.value === "user" })} > - {intl.formatMessage({ id: "permissions.visibility.user" })} + - {intl.formatMessage({ id: "permissions.visibility.all" })} +
)} @@ -166,7 +166,7 @@ export function PermissionsModal({ userId, onClose }: Props) { <>
{({ field, form }: any) => getPermissionButtons(field, form)} @@ -174,7 +174,7 @@ export function PermissionsModal({ userId, onClose }: Props) {
{({ field, form }: any) => getPermissionButtons(field, form)} @@ -182,7 +182,7 @@ export function PermissionsModal({ userId, onClose }: Props) {
{({ field, form }: any) => getPermissionButtons(field, form)} @@ -190,7 +190,7 @@ export function PermissionsModal({ userId, onClose }: Props) {
{({ field, form }: any) => getPermissionButtons(field, form)} @@ -198,7 +198,7 @@ export function PermissionsModal({ userId, onClose }: Props) {
{({ field, form }: any) => getPermissionButtons(field, form)} @@ -206,7 +206,7 @@ export function PermissionsModal({ userId, onClose }: Props) {
{({ field, form }: any) => getPermissionButtons(field, form)} @@ -217,7 +217,7 @@ export function PermissionsModal({ userId, onClose }: Props) { diff --git a/frontend/src/modals/ProxyHostModal.tsx b/frontend/src/modals/ProxyHostModal.tsx index c249def6..0b6774aa 100644 --- a/frontend/src/modals/ProxyHostModal.tsx +++ b/frontend/src/modals/ProxyHostModal.tsx @@ -1,10 +1,11 @@ import { IconSettings } from "@tabler/icons-react"; import cn from "classnames"; import { Field, Form, Formik } from "formik"; -import { useState } from "react"; +import { type ReactNode, useState } from "react"; import { Alert } from "react-bootstrap"; import Modal from "react-bootstrap/Modal"; import { + AccessField, Button, DomainNamesField, Loading, @@ -13,7 +14,7 @@ import { SSLOptionsFields, } from "src/components"; import { useProxyHost, useSetProxyHost } from "src/hooks"; -import { intl } from "src/locale"; +import { intl, T } from "src/locale"; import { validateNumber, validateString } from "src/modules/Validations"; import { showSuccess } from "src/notifications"; @@ -24,7 +25,7 @@ interface Props { export function ProxyHostModal({ id, onClose }: Props) { const { data, isLoading, error } = useProxyHost(id); const { mutate: setProxyHost } = useSetProxyHost(); - const [errorMsg, setErrorMsg] = useState(null); + const [errorMsg, setErrorMsg] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const onSubmit = async (values: any, { setSubmitting }: any) => { @@ -38,7 +39,7 @@ export function ProxyHostModal({ id, onClose }: Props) { }; setProxyHost(payload, { - onError: (err: any) => setErrorMsg(err.message), + onError: (err: any) => setErrorMsg(), onSuccess: () => { showSuccess(intl.formatMessage({ id: "notification.proxy-host-saved" })); onClose(); @@ -90,16 +91,13 @@ export function ProxyHostModal({ id, onClose }: Props) {
- {intl.formatMessage({ - id: data?.id ? "proxy-host.edit" : "proxy-host.new", - })} + setErrorMsg(null)} dismissible> {errorMsg} -
    @@ -111,7 +109,7 @@ export function ProxyHostModal({ id, onClose }: Props) { aria-selected="true" role="tab" > - {intl.formatMessage({ id: "column.details" })} +
  • @@ -123,7 +121,7 @@ export function ProxyHostModal({ id, onClose }: Props) { tabIndex={-1} role="tab" > - {intl.formatMessage({ id: "column.custom-locations" })} +
  • @@ -135,7 +133,7 @@ export function ProxyHostModal({ id, onClose }: Props) { tabIndex={-1} role="tab" > - {intl.formatMessage({ id: "column.ssl" })} +
  • @@ -166,9 +164,7 @@ export function ProxyHostModal({ id, onClose }: Props) { className="form-label" htmlFor="forwardScheme" > - {intl.formatMessage({ - id: "host.forward-scheme", - })} + (
+

- {intl.formatMessage({ id: "host.flags.title" })} +

diff --git a/frontend/src/modals/RedirectionHostModal.tsx b/frontend/src/modals/RedirectionHostModal.tsx index d47cbaa1..4f76e2a3 100644 --- a/frontend/src/modals/RedirectionHostModal.tsx +++ b/frontend/src/modals/RedirectionHostModal.tsx @@ -1,7 +1,7 @@ import { IconSettings } from "@tabler/icons-react"; import cn from "classnames"; import { Field, Form, Formik } from "formik"; -import { useState } from "react"; +import { type ReactNode, useState } from "react"; import { Alert } from "react-bootstrap"; import Modal from "react-bootstrap/Modal"; import { @@ -13,7 +13,7 @@ import { SSLOptionsFields, } from "src/components"; import { useRedirectionHost, useSetRedirectionHost } from "src/hooks"; -import { intl } from "src/locale"; +import { intl, T } from "src/locale"; import { validateString } from "src/modules/Validations"; import { showSuccess } from "src/notifications"; @@ -24,7 +24,7 @@ interface Props { export function RedirectionHostModal({ id, onClose }: Props) { const { data, isLoading, error } = useRedirectionHost(id); const { mutate: setRedirectionHost } = useSetRedirectionHost(); - const [errorMsg, setErrorMsg] = useState(null); + const [errorMsg, setErrorMsg] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const onSubmit = async (values: any, { setSubmitting }: any) => { @@ -38,7 +38,7 @@ export function RedirectionHostModal({ id, onClose }: Props) { }; setRedirectionHost(payload, { - onError: (err: any) => setErrorMsg(err.message), + onError: (err: any) => setErrorMsg(), onSuccess: () => { showSuccess(intl.formatMessage({ id: "notification.redirection-host-saved" })); onClose(); @@ -86,16 +86,13 @@ export function RedirectionHostModal({ id, onClose }: Props) {
- {intl.formatMessage({ - id: data?.id ? "redirection-host.edit" : "redirection-host.new", - })} + setErrorMsg(null)} dismissible> {errorMsg} -
    @@ -107,7 +104,7 @@ export function RedirectionHostModal({ id, onClose }: Props) { aria-selected="true" role="tab" > - {intl.formatMessage({ id: "column.details" })} +
  • @@ -119,7 +116,7 @@ export function RedirectionHostModal({ id, onClose }: Props) { tabIndex={-1} role="tab" > - {intl.formatMessage({ id: "column.ssl" })} +
  • @@ -150,9 +147,7 @@ export function RedirectionHostModal({ id, onClose }: Props) { className="form-label" htmlFor="forwardScheme" > - {intl.formatMessage({ - id: "host.forward-scheme", - })} +

    - {intl.formatMessage({ id: "host.flags.title" })} +

  • @@ -99,7 +99,7 @@ export function StreamModal({ id, onClose }: Props) { tabIndex={-1} role="tab" > - {intl.formatMessage({ id: "column.ssl" })} +
@@ -111,7 +111,7 @@ export function StreamModal({ id, onClose }: Props) { {({ field, form }: any) => (
- {intl.formatMessage({ - id: "stream.forward-host", - })} + - {intl.formatMessage({ - id: "host.forward-port", - })} +

- {intl.formatMessage({ id: "host.flags.protocols" })} +