diff --git a/eslint.config.js b/eslint.config.js index 71dc862c..afdfdd54 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -3,7 +3,8 @@ export default [ { rules: { semi: "error", - "prefer-const": "error" + "prefer-const": "error", + quotes: ["error", "double"] } } ]; diff --git a/messages/en-US.json b/messages/en-US.json index 9ecea382..8f518e02 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1089,5 +1089,40 @@ "sidebarSettings": "Settings", "sidebarAllUsers": "All Users", "sidebarIdentityProviders": "Identity Providers", - "sidebarLicense": "License" + "sidebarLicense": "License", + "enableDockerSocket": "Enable Docker Socket", + "enableDockerSocketDescription": "Enable Docker Socket discovery for populating container information, useful in resource targets.", + "enableDockerSocketLink": "Enable Docker Socket discovery for populating container information, useful in resource targets.", + "viewDockerContainers": "View Docker Containers", + "containersIn": "Containers in {siteName}", + "selectContainerDescription": "Select any container to use as a hostname for this target. Click a port to use a port.", + "containerName": "Name", + "containerImage": "Image", + "containerState": "State", + "containerNetworks": "Networks", + "containerHostnameIp": "Hostname/IP", + "containerLabels": "Labels", + "containerLabelsCount": "{count} label{s,plural,one{} other{s}}", + "containerLabelsTitle": "Container Labels", + "containerLabelEmpty": "", + "containerPorts": "Ports", + "containerPortsMore": "+{count} more", + "containerActions": "Actions", + "select": "Select", + "noContainersMatchingFilters": "No containers found matching the current filters.", + "showContainersWithoutPorts": "Show containers without ports", + "showStoppedContainers": "Show stopped containers", + "noContainersFound": "No containers found. Make sure Docker containers are running.", + "searchContainersPlaceholder": "Search across {count} containers...", + "searchResultsCount": "{count} result{s,plural,one{} other{s}}", + "filters": "Filters", + "filterOptions": "Filter Options", + "filterPorts": "Ports", + "filterStopped": "Stopped", + "clearAllFilters": "Clear all filters", + "columns": "Columns", + "toggleColumns": "Toggle Columns", + "refreshContainersList": "Refresh containers list", + "searching": "Searching...", + "noContainersFoundMatching": "No containers found matching \"{filter}\"." } \ No newline at end of file diff --git a/src/app/[orgId]/settings/access/invitations/InvitationsTable.tsx b/src/app/[orgId]/settings/access/invitations/InvitationsTable.tsx index 95fadf42..322d67fa 100644 --- a/src/app/[orgId]/settings/access/invitations/InvitationsTable.tsx +++ b/src/app/[orgId]/settings/access/invitations/InvitationsTable.tsx @@ -148,7 +148,7 @@ export default function InvitationsTable({ dialog={

- {t('inviteQuestionRemove', {email: selectedInvitation?.email})} + {t('inviteQuestionRemove', {email: selectedInvitation?.email || ""})}

{t('inviteMessageRemove')} diff --git a/src/app/[orgId]/settings/access/invitations/RegenerateInvitationForm.tsx b/src/app/[orgId]/settings/access/invitations/RegenerateInvitationForm.tsx index fbd1c4f5..a5850b60 100644 --- a/src/app/[orgId]/settings/access/invitations/RegenerateInvitationForm.tsx +++ b/src/app/[orgId]/settings/access/invitations/RegenerateInvitationForm.tsx @@ -177,7 +177,7 @@ export default function RegenerateInvitationForm({ {!inviteLink ? (

- {t('inviteQuestionRegenerate', {email: invitation?.email})} + {t('inviteQuestionRegenerate', {email: invitation?.email || ""})}

@@ -244,7 +244,7 @@ export default function UsersTable({ users: u }: UsersTableProps) { dialog={

- {t('userQuestionOrgRemove', {email: selectedUser?.email || selectedUser?.name || selectedUser?.username})} + {t('userQuestionOrgRemove', {email: selectedUser?.email || selectedUser?.name || selectedUser?.username || ""})}

diff --git a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx index d571f7b8..a0c89773 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx @@ -71,7 +71,6 @@ const TransferFormSchema = z.object({ siteId: z.number() }); -type GeneralFormValues = z.infer; type TransferFormValues = z.infer; export default function GeneralForm() { @@ -123,7 +122,7 @@ export default function GeneralForm() { return true; }, { - message: t('proxyErrorInvalidPort'), + message: t("proxyErrorInvalidPort"), path: ["proxyPort"] } ) @@ -135,11 +134,13 @@ export default function GeneralForm() { return true; }, { - message: t('subdomainErrorInvalid'), + message: t("subdomainErrorInvalid"), path: ["subdomain"] } ); + type GeneralFormValues = z.infer; + const form = useForm({ resolver: zodResolver(GeneralFormSchema), defaultValues: { @@ -176,8 +177,11 @@ export default function GeneralForm() { .catch((e) => { toast({ variant: "destructive", - title: t('domainErrorFetch'), - description: formatAxiosError(e, t('domainErrorFetchDescription')) + title: t("domainErrorFetch"), + description: formatAxiosError( + e, + t("domainErrorFetchDescription") + ) }); }); @@ -215,15 +219,18 @@ export default function GeneralForm() { .catch((e) => { toast({ variant: "destructive", - title: t('resourceErrorUpdate'), - description: formatAxiosError(e, t('resourceErrorUpdateDescription')) + title: t("resourceErrorUpdate"), + description: formatAxiosError( + e, + t("resourceErrorUpdateDescription") + ) }); }); if (res && res.status === 200) { toast({ - title: t('resourceUpdated'), - description: t('resourceUpdatedDescription') + title: t("resourceUpdated"), + description: t("resourceUpdatedDescription") }); const resource = res.data.data; @@ -251,16 +258,18 @@ export default function GeneralForm() { .catch((e) => { toast({ variant: "destructive", - title: t('resourceErrorTransfer'), - description: formatAxiosError(e, t('resourceErrorTransferDescription') + title: t("resourceErrorTransfer"), + description: formatAxiosError( + e, + t("resourceErrorTransferDescription") ) }); }); if (res && res.status === 200) { toast({ - title: t('resourceTransferred'), - description: t('resourceTransferredDescription') + title: t("resourceTransferred"), + description: t("resourceTransferredDescription") }); router.refresh(); @@ -284,10 +293,10 @@ export default function GeneralForm() { .catch((e) => { toast({ variant: "destructive", - title: t('resourceErrorToggle'), + title: t("resourceErrorToggle"), description: formatAxiosError( e, - t('resourceErrorToggleDescription') + t("resourceErrorToggleDescription") ) }); }); @@ -302,15 +311,17 @@ export default function GeneralForm() { - {t('resourceVisibilityTitle')} + + {t("resourceVisibilityTitle")} + - {t('resourceVisibilityTitleDescription')} + {t("resourceVisibilityTitleDescription")} { await toggleResourceEnabled(val); @@ -322,10 +333,10 @@ export default function GeneralForm() { - {t('resourceGeneral')} + {t("resourceGeneral")} - {t('resourceGeneralDescription')} + {t("resourceGeneralDescription")} @@ -342,7 +353,9 @@ export default function GeneralForm() { name="name" render={({ field }) => ( - {t('name')} + + {t("name")} + @@ -361,7 +374,9 @@ export default function GeneralForm() { render={({ field }) => ( - {t('domainType')} + {t( + "domainType" + )} @@ -409,7 +428,7 @@ export default function GeneralForm() { {domainType === "subdomain" ? (

- {t('subdomain')} + {t("subdomain")}
@@ -495,7 +514,9 @@ export default function GeneralForm() { render={({ field }) => ( - {t('baseDomain')} + {t( + "baseDomain" + )} - {t('saveGeneralSettings')} + {t("saveGeneralSettings")} @@ -597,10 +620,10 @@ export default function GeneralForm() { - {t('resourceTransfer')} + {t("resourceTransfer")} - {t('resourceTransferDescription')} + {t("resourceTransferDescription")} @@ -620,7 +643,7 @@ export default function GeneralForm() { render={({ field }) => ( - {t('siteDestination')} + {t("siteDestination")} - + - {t('sitesNotFound')} + {t( + "sitesNotFound" + )} {sites.map( @@ -709,7 +740,7 @@ export default function GeneralForm() { disabled={transferLoading} form="transfer-form" > - {t('resourceTransferSubmit')} + {t("resourceTransferSubmit")} diff --git a/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx index 8232d9f4..2ca6244f 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx @@ -74,7 +74,6 @@ import { CollapsibleTrigger } from "@app/components/ui/collapsible"; import { ContainersSelector } from "@app/components/ContainersSelector"; -import { FaDocker } from "react-icons/fa"; import { useTranslations } from "next-intl"; const addTargetSchema = z.object({ diff --git a/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx b/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx index 190a5745..5619911b 100644 --- a/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx +++ b/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx @@ -34,7 +34,6 @@ import { useState } from "react"; import { SwitchInput } from "@app/components/SwitchInput"; import { useTranslations } from "next-intl"; import Link from "next/link"; -import { ArrowRight } from "lucide-react"; const GeneralFormSchema = z.object({ name: z.string().nonempty("Name is required"), @@ -53,13 +52,6 @@ export default function GeneralPage() { const router = useRouter(); const t = useTranslations(); - const GeneralFormSchema = z.object({ - name: z.string().nonempty("Name is required"), - dockerSocketEnabled: z.boolean().optional() - }); - - type GeneralFormValues = z.infer; - const form = useForm({ resolver: zodResolver(GeneralFormSchema), defaultValues: { @@ -80,10 +72,10 @@ export default function GeneralPage() { .catch((e) => { toast({ variant: "destructive", - title: t('siteErrorUpdate'), + title: t("siteErrorUpdate"), description: formatAxiosError( e, - t('siteErrorUpdateDescription') + t("siteErrorUpdateDescription") ) }); }); @@ -94,8 +86,8 @@ export default function GeneralPage() { }); toast({ - title: t('siteUpdated'), - description: t('siteUpdatedDescription') + title: t("siteUpdated"), + description: t("siteUpdatedDescription") }); setLoading(false); @@ -108,10 +100,10 @@ export default function GeneralPage() { - {t('generalSettings')} + {t("generalSettings")} - {t('siteGeneralDescription')} + {t("siteGeneralDescription")} @@ -128,13 +120,13 @@ export default function GeneralPage() { name="name" render={({ field }) => ( - {t('name')} + {t("name")} - {t('siteNameDescription')} + {t("siteNameDescription")} )} @@ -148,7 +140,9 @@ export default function GeneralPage() { - Enable Docker Socket - discovery for populating - container information, - useful in resource targets. + {t( + "enableDockerSocketDescription" + )} {" "} - Docker socket path - must be provided to - Newt in order to use - this feature. + {t( + "enableDockerSocketLink" + )} @@ -194,7 +186,7 @@ export default function GeneralPage() { loading={loading} disabled={loading} > - {t('saveGeneralSettings')} + {t("saveGeneralSettings")} diff --git a/src/components/ContainersSelector.tsx b/src/components/ContainersSelector.tsx index edc6b77c..0f09fb5a 100644 --- a/src/components/ContainersSelector.tsx +++ b/src/components/ContainersSelector.tsx @@ -45,7 +45,7 @@ import { ScrollArea } from "@/components/ui/scroll-area"; import { Search, RefreshCw, Filter, Columns } from "lucide-react"; import { GetSiteResponse, Container } from "@server/routers/site"; import { useDockerSocket } from "@app/hooks/useDockerSocket"; -import { FaDocker } from "react-icons/fa"; +import { useTranslations } from "next-intl"; // Type definitions based on the JSON structure @@ -60,6 +60,8 @@ export const ContainersSelector: FC = ({ }) => { const [open, setOpen] = useState(false); + const t = useTranslations(); + const { isAvailable, containers, fetchContainers } = useDockerSocket(site); useEffect(() => { @@ -87,15 +89,16 @@ export const ContainersSelector: FC = ({ className="text-sm text-primary hover:underline cursor-pointer" onClick={() => setOpen(true)} > - View Docker Containers + {t("viewDockerContainers")} - Containers in {site.name} + + {t("containersIn", { siteName: site.name })} + - Select any container to use as a hostname for this - target. Click a port to use a port. + {t("selectContainerDescription")} @@ -109,7 +112,7 @@ export const ContainersSelector: FC = ({ - + @@ -132,6 +135,8 @@ const DockerContainersTable: FC<{ labels: false }); + const t = useTranslations(); + useEffect(() => { const timer = setTimeout(() => { setGlobalFilter(searchInput); @@ -182,14 +187,14 @@ const DockerContainersTable: FC<{ const columns: ColumnDef[] = [ { accessorKey: "name", - header: "Name", + header: t("containerName"), cell: ({ row }) => (
{row.original.name}
) }, { accessorKey: "image", - header: "Image", + header: t("containerImage"), cell: ({ row }) => (
{row.original.image} @@ -198,7 +203,7 @@ const DockerContainersTable: FC<{ }, { accessorKey: "state", - header: "State", + header: t("containerState"), cell: ({ row }) => ( { const networks = Object.keys(row.original.networks); return ( @@ -231,7 +236,7 @@ const DockerContainersTable: FC<{ }, { accessorKey: "hostname", - header: "Hostname/IP", + header: t("containerHostnameIp"), enableHiding: false, cell: ({ row }) => (
@@ -241,7 +246,7 @@ const DockerContainersTable: FC<{ }, { accessorKey: "labels", - header: "Labels", + header: t("containerLabels"), cell: ({ row }) => { const labels = row.original.labels || {}; const labelEntries = Object.entries(labels); @@ -258,15 +263,14 @@ const DockerContainersTable: FC<{ size="sm" className="h-6 px-2 text-xs hover:bg-muted" > - {labelEntries.length} label - {labelEntries.length !== 1 ? "s" : ""} + {t("containerLabelsCount", { count: labelEntries.length })}

- Container Labels + {t("containerLabelsTitle")}

{labelEntries.map(([key, value]) => ( @@ -275,7 +279,7 @@ const DockerContainersTable: FC<{ {key}
- {value || ""} + {value || t("containerLabelEmpty")}
))} @@ -289,7 +293,7 @@ const DockerContainersTable: FC<{ }, { accessorKey: "ports", - header: "Ports", + header: t("containerPorts"), enableHiding: false, cell: ({ row }) => { const ports = getExposedPorts(row.original); @@ -312,7 +316,7 @@ const DockerContainersTable: FC<{ { const ports = getExposedPorts(row.original); return ( @@ -355,7 +359,7 @@ const DockerContainersTable: FC<{ onClick={() => onContainerSelect(row.original, ports[0])} disabled={row.original.state !== "running"} > - Select + {t("select")} ); } @@ -412,8 +416,7 @@ const DockerContainersTable: FC<{ containers.length > 0 ? ( <>

- No containers found matching the current - filters. + {t("noContainersMatchingFilters")}

{hideContainersWithoutPorts && ( @@ -426,7 +429,7 @@ const DockerContainersTable: FC<{ ) } > - Show containers without ports + {t("showContainersWithoutPorts")} )} {hideStoppedContainers && ( @@ -437,15 +440,14 @@ const DockerContainersTable: FC<{ setHideStoppedContainers(false) } > - Show stopped containers + {t("showStoppedContainers")} )}
) : (

- No containers found. Make sure Docker containers - are running. + {t("noContainersFound")}

)}
@@ -461,7 +463,7 @@ const DockerContainersTable: FC<{
setSearchInput(event.target.value) @@ -471,12 +473,7 @@ const DockerContainersTable: FC<{ {searchInput && table.getFilteredRowModel().rows.length > 0 && (
- {table.getFilteredRowModel().rows.length}{" "} - result - {table.getFilteredRowModel().rows.length !== - 1 - ? "s" - : ""} + {t("searchResultsCount", { count: table.getFilteredRowModel().rows.length })}
)}
@@ -489,7 +486,7 @@ const DockerContainersTable: FC<{ className="gap-2" > - Filters + {t("filters")} {(hideContainersWithoutPorts || hideStoppedContainers) && ( @@ -502,7 +499,7 @@ const DockerContainersTable: FC<{ - Filter Options + {t("filterOptions")} - Ports + {t("filterPorts")} - Stopped + {t("filterStopped")} {(hideContainersWithoutPorts || hideStoppedContainers) && ( @@ -537,7 +534,7 @@ const DockerContainersTable: FC<{ }} className="w-full text-xs" > - Clear all filters + {t("clearAllFilters")}
@@ -553,12 +550,12 @@ const DockerContainersTable: FC<{ className="gap-2" > - Columns + {t("columns")} - Toggle Columns + {t("toggleColumns")} {table @@ -577,7 +574,7 @@ const DockerContainersTable: FC<{ } > {column.id === "hostname" - ? "Hostname/IP" + ? t("containerHostnameIp") : column.id} ); @@ -589,7 +586,7 @@ const DockerContainersTable: FC<{ variant="outline" size="icon" onClick={onRefresh} - title="Refresh containers list" + title={t("refreshContainersList")} > @@ -644,10 +641,10 @@ const DockerContainersTable: FC<{ {searchInput && !globalFilter ? (
- Searching... + {t("searching")}
) : ( - `No containers found matching "${globalFilter}".` + t("noContainersFoundMatching", { filter: globalFilter }) )}