mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-05 12:10:52 +02:00
many minor visual improvements
This commit is contained in:
+6
-2
@@ -101,6 +101,8 @@
|
|||||||
"sitesTableViewPrivateResources": "View Private Resources",
|
"sitesTableViewPrivateResources": "View Private Resources",
|
||||||
"siteInstallNewt": "Install Site",
|
"siteInstallNewt": "Install Site",
|
||||||
"siteInstallNewtDescription": "Install the site connector for your system",
|
"siteInstallNewtDescription": "Install the site connector for your system",
|
||||||
|
"siteInstallKubernetesDocsDescription": "For more and up to date Kubernetes installation information, see <docsLink>docs.pangolin.net/manage/sites/install-kubernetes</docsLink>.",
|
||||||
|
"siteInstallAdvantechDocsDescription": "For Advantech modem installation instructions, see <docsLink>docs.pangolin.net/manage/sites/install-advantech</docsLink>.",
|
||||||
"WgConfiguration": "WireGuard Configuration",
|
"WgConfiguration": "WireGuard Configuration",
|
||||||
"WgConfigurationDescription": "Use the following configuration to connect to the network",
|
"WgConfigurationDescription": "Use the following configuration to connect to the network",
|
||||||
"operatingSystem": "Operating System",
|
"operatingSystem": "Operating System",
|
||||||
@@ -1220,8 +1222,10 @@
|
|||||||
"addLabels": "Add labels",
|
"addLabels": "Add labels",
|
||||||
"siteLabelsTab": "Labels",
|
"siteLabelsTab": "Labels",
|
||||||
"siteLabelsDescription": "Manage labels associated with this site.",
|
"siteLabelsDescription": "Manage labels associated with this site.",
|
||||||
"labelsNotFound": "Labels not found",
|
"labelsNotFound": "No labels found.",
|
||||||
|
"labelsEmptyCreateHint": "Start typing above to create a label.",
|
||||||
"labelSearch": "Search labels",
|
"labelSearch": "Search labels",
|
||||||
|
"labelSearchOrCreate": "Search or create a label",
|
||||||
"accessLabelFilterCount": "{count, plural, one {# label} other {# labels}}",
|
"accessLabelFilterCount": "{count, plural, one {# label} other {# labels}}",
|
||||||
"labelOverflowCount": "+{count, plural, one {# label} other {# labels}}",
|
"labelOverflowCount": "+{count, plural, one {# label} other {# labels}}",
|
||||||
"accessLabelFilterClear": "Clear label filters",
|
"accessLabelFilterClear": "Clear label filters",
|
||||||
@@ -2073,7 +2077,7 @@
|
|||||||
"sshDaemonDisclaimer": "Ensure your target host is properly configured to run the auth daemon before completing this setup, or provisioning will fail.",
|
"sshDaemonDisclaimer": "Ensure your target host is properly configured to run the auth daemon before completing this setup, or provisioning will fail.",
|
||||||
"sshDaemonPort": "Daemon Port",
|
"sshDaemonPort": "Daemon Port",
|
||||||
"sshServerDestination": "Server Destination",
|
"sshServerDestination": "Server Destination",
|
||||||
"sshServerDestinationDescription": "Configure the destination and port of the SSH server",
|
"sshServerDestinationDescription": "Configure the destination of the SSH server",
|
||||||
"destination": "Destination",
|
"destination": "Destination",
|
||||||
"bgTargetMultiSiteDisclaimer": "Selecting multiple sites enables resilient routing and failover for high availability.",
|
"bgTargetMultiSiteDisclaimer": "Selecting multiple sites enables resilient routing and failover for high availability.",
|
||||||
"roleAllowSsh": "Allow SSH",
|
"roleAllowSsh": "Allow SSH",
|
||||||
|
|||||||
@@ -334,19 +334,15 @@ export function ProxyResourceTargetsForm({
|
|||||||
{row.original.siteType === "newt" ? (
|
{row.original.siteType === "newt" ? (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="flex items-center gap-2 w-full text-left cursor-pointer"
|
className="flex items-center space-x-2 w-full text-left cursor-pointer"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
openHealthCheckDialog(row.original)
|
openHealthCheckDialog(row.original)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`flex items-center gap-2 ${status === "healthy" ? "text-green-500" : status === "unhealthy" ? "text-destructive" : "text-neutral-500"}`}
|
className={`w-2 h-2 rounded-full ${status === "healthy" ? "bg-green-500" : status === "unhealthy" ? "bg-destructive" : "bg-neutral-500"}`}
|
||||||
>
|
></div>
|
||||||
<div
|
<span>{getStatusText(status)}</span>
|
||||||
className={`w-2 h-2 rounded-full ${status === "healthy" ? "bg-green-500" : status === "unhealthy" ? "bg-destructive" : "bg-neutral-500"}`}
|
|
||||||
></div>
|
|
||||||
{getStatusText(status)}
|
|
||||||
</div>
|
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<span>-</span>
|
<span>-</span>
|
||||||
@@ -535,7 +531,7 @@ export function ProxyResourceTargetsForm({
|
|||||||
accessorKey: "enabled",
|
accessorKey: "enabled",
|
||||||
header: () => <span className="p-3">{t("enabled")}</span>,
|
header: () => <span className="p-3">{t("enabled")}</span>,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="flex items-center justify-center w-full">
|
<div className="flex items-center w-full">
|
||||||
<Switch
|
<Switch
|
||||||
defaultChecked={row.original.enabled}
|
defaultChecked={row.original.enabled}
|
||||||
onCheckedChange={(val) =>
|
onCheckedChange={(val) =>
|
||||||
@@ -554,9 +550,8 @@ export function ProxyResourceTargetsForm({
|
|||||||
|
|
||||||
const actionsColumn: ColumnDef<LocalTarget> = {
|
const actionsColumn: ColumnDef<LocalTarget> = {
|
||||||
id: "actions",
|
id: "actions",
|
||||||
header: () => <span className="p-3">{t("actions")}</span>,
|
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="flex items-center w-full">
|
<div className="flex items-center justify-end w-full">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => removeTarget(row.original.targetId)}
|
onClick={() => removeTarget(row.original.targetId)}
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ import {
|
|||||||
SettingsSectionDescription,
|
SettingsSectionDescription,
|
||||||
SettingsSectionForm,
|
SettingsSectionForm,
|
||||||
SettingsSectionHeader,
|
SettingsSectionHeader,
|
||||||
SettingsSectionTitle
|
SettingsSectionTitle,
|
||||||
|
SettingsSubsectionDescription,
|
||||||
|
SettingsSubsectionHeader,
|
||||||
|
SettingsSubsectionTitle
|
||||||
} from "@app/components/Settings";
|
} from "@app/components/Settings";
|
||||||
import { StrategySelect, StrategyOption } from "@app/components/StrategySelect";
|
import { StrategySelect, StrategyOption } from "@app/components/StrategySelect";
|
||||||
import { BrowserGatewayTargetForm } from "@app/components/BrowserGatewayTargetForm";
|
import { BrowserGatewayTargetForm } from "@app/components/BrowserGatewayTargetForm";
|
||||||
@@ -386,9 +389,9 @@ function SshServerForm({
|
|||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
<SettingsSectionForm variant="half">
|
<SettingsSectionForm variant="half">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<p className="text-sm font-semibold">
|
<SettingsSubsectionTitle>
|
||||||
{t("sshServerMode")}
|
{t("sshServerMode")}
|
||||||
</p>
|
</SettingsSubsectionTitle>
|
||||||
<Badge variant="secondary">
|
<Badge variant="secondary">
|
||||||
{sshServerMode == "standard"
|
{sshServerMode == "standard"
|
||||||
? t("sshServerModeStandard")
|
? t("sshServerModeStandard")
|
||||||
@@ -397,9 +400,9 @@ function SshServerForm({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<p className="text-sm font-semibold">
|
<SettingsSubsectionTitle>
|
||||||
{t("sshAuthenticationMethod")}
|
{t("sshAuthenticationMethod")}
|
||||||
</p>
|
</SettingsSubsectionTitle>
|
||||||
<StrategySelect<"passthrough" | "push">
|
<StrategySelect<"passthrough" | "push">
|
||||||
value={pamMode}
|
value={pamMode}
|
||||||
options={authMethodOptions}
|
options={authMethodOptions}
|
||||||
@@ -410,9 +413,9 @@ function SshServerForm({
|
|||||||
|
|
||||||
{showDaemonLocation && (
|
{showDaemonLocation && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<p className="text-sm font-semibold">
|
<SettingsSubsectionTitle>
|
||||||
{t("sshAuthDaemonLocation")}
|
{t("sshAuthDaemonLocation")}
|
||||||
</p>
|
</SettingsSubsectionTitle>
|
||||||
<StrategySelect<"site" | "remote">
|
<StrategySelect<"site" | "remote">
|
||||||
value={standardDaemonLocation}
|
value={standardDaemonLocation}
|
||||||
options={daemonLocationOptions}
|
options={daemonLocationOptions}
|
||||||
@@ -460,14 +463,14 @@ function SshServerForm({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<SettingsSubsectionHeader>
|
||||||
<h2 className="text-1xl font-semibold tracking-tight flex items-center gap-2">
|
<SettingsSubsectionTitle>
|
||||||
{t("sshServerDestination")}
|
{t("sshServerDestination")}
|
||||||
</h2>
|
</SettingsSubsectionTitle>
|
||||||
<p className="text-sm text-muted-foreground">
|
<SettingsSubsectionDescription>
|
||||||
{t("sshServerDestinationDescription")}
|
{t("sshServerDestinationDescription")}
|
||||||
</p>
|
</SettingsSubsectionDescription>
|
||||||
</div>
|
</SettingsSubsectionHeader>
|
||||||
{isNative ? (
|
{isNative ? (
|
||||||
<Popover
|
<Popover
|
||||||
open={nativeSiteOpen}
|
open={nativeSiteOpen}
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ import {
|
|||||||
SettingsSectionDescription,
|
SettingsSectionDescription,
|
||||||
SettingsSectionForm,
|
SettingsSectionForm,
|
||||||
SettingsSectionHeader,
|
SettingsSectionHeader,
|
||||||
SettingsSectionTitle
|
SettingsSectionTitle,
|
||||||
|
SettingsSubsectionDescription,
|
||||||
|
SettingsSubsectionHeader,
|
||||||
|
SettingsSubsectionTitle
|
||||||
} from "@app/components/Settings";
|
} from "@app/components/Settings";
|
||||||
import HeaderTitle from "@app/components/SettingsSectionTitle";
|
import HeaderTitle from "@app/components/SettingsSectionTitle";
|
||||||
import {
|
import {
|
||||||
@@ -711,7 +714,7 @@ export default function Page() {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="space-y-4"
|
className="grid gap-4 grid-cols-1 md:grid-cols-2 items-start"
|
||||||
id="base-resource-form"
|
id="base-resource-form"
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
@@ -794,7 +797,7 @@ export default function Page() {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="space-y-4"
|
className="grid gap-4 grid-cols-1 md:grid-cols-2 items-start"
|
||||||
id="tcp-udp-settings-form"
|
id="tcp-udp-settings-form"
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
@@ -879,10 +882,10 @@ export default function Page() {
|
|||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
<SettingsSectionForm variant="half">
|
<SettingsSectionForm variant="half">
|
||||||
{/* Mode */}
|
{/* Mode */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-2">
|
||||||
<p className="text-sm font-semibold">
|
<SettingsSubsectionTitle>
|
||||||
{t("sshServerMode")}
|
{t("sshServerMode")}
|
||||||
</p>
|
</SettingsSubsectionTitle>
|
||||||
<StrategySelect<
|
<StrategySelect<
|
||||||
"standard" | "native"
|
"standard" | "native"
|
||||||
>
|
>
|
||||||
@@ -893,12 +896,12 @@ export default function Page() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-2">
|
||||||
<p className="text-sm font-semibold">
|
<SettingsSubsectionTitle>
|
||||||
{t(
|
{t(
|
||||||
"sshAuthenticationMethod"
|
"sshAuthenticationMethod"
|
||||||
)}
|
)}
|
||||||
</p>
|
</SettingsSubsectionTitle>
|
||||||
<StrategySelect<
|
<StrategySelect<
|
||||||
"passthrough" | "push"
|
"passthrough" | "push"
|
||||||
>
|
>
|
||||||
@@ -913,12 +916,12 @@ export default function Page() {
|
|||||||
|
|
||||||
{/* Daemon Location (standard + push) */}
|
{/* Daemon Location (standard + push) */}
|
||||||
{showDaemonLocation && (
|
{showDaemonLocation && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-2">
|
||||||
<p className="text-sm font-semibold">
|
<SettingsSubsectionTitle>
|
||||||
{t(
|
{t(
|
||||||
"sshAuthDaemonLocation"
|
"sshAuthDaemonLocation"
|
||||||
)}
|
)}
|
||||||
</p>
|
</SettingsSubsectionTitle>
|
||||||
<StrategySelect<
|
<StrategySelect<
|
||||||
"site" | "remote"
|
"site" | "remote"
|
||||||
>
|
>
|
||||||
@@ -953,49 +956,55 @@ export default function Page() {
|
|||||||
{/* Daemon Port (standard + push + remote) */}
|
{/* Daemon Port (standard + push + remote) */}
|
||||||
{showDaemonPort && (
|
{showDaemonPort && (
|
||||||
<Form {...sshDaemonPortForm}>
|
<Form {...sshDaemonPortForm}>
|
||||||
<FormField
|
<div className="w-full md:w-1/2">
|
||||||
control={
|
<FormField
|
||||||
sshDaemonPortForm.control
|
control={
|
||||||
}
|
sshDaemonPortForm.control
|
||||||
name="authDaemonPort"
|
}
|
||||||
render={({ field }) => (
|
name="authDaemonPort"
|
||||||
<FormItem>
|
render={({
|
||||||
<FormLabel>
|
field
|
||||||
{t(
|
}) => (
|
||||||
"sshDaemonPort"
|
<FormItem>
|
||||||
)}
|
<FormLabel>
|
||||||
</FormLabel>
|
{t(
|
||||||
<FormControl>
|
"sshDaemonPort"
|
||||||
<Input
|
)}
|
||||||
type="number"
|
</FormLabel>
|
||||||
min={1}
|
<FormControl>
|
||||||
max={
|
<Input
|
||||||
65535
|
type="number"
|
||||||
}
|
min={
|
||||||
{...field}
|
1
|
||||||
/>
|
}
|
||||||
</FormControl>
|
max={
|
||||||
<FormMessage />
|
65535
|
||||||
</FormItem>
|
}
|
||||||
)}
|
{...field}
|
||||||
/>
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Server Destination */}
|
{/* Server Destination */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<SettingsSubsectionHeader>
|
||||||
<h2 className="text-sm font-semibold">
|
<SettingsSubsectionTitle>
|
||||||
{t(
|
{t(
|
||||||
"sshServerDestination"
|
"sshServerDestination"
|
||||||
)}
|
)}
|
||||||
</h2>
|
</SettingsSubsectionTitle>
|
||||||
<p className="text-sm text-muted-foreground">
|
<SettingsSubsectionDescription>
|
||||||
{t(
|
{t(
|
||||||
"sshServerDestinationDescription"
|
"sshServerDestinationDescription"
|
||||||
)}
|
)}
|
||||||
</p>
|
</SettingsSubsectionDescription>
|
||||||
</div>
|
</SettingsSubsectionHeader>
|
||||||
{isNative ? (
|
{isNative ? (
|
||||||
<Popover
|
<Popover
|
||||||
open={nativeSiteOpen}
|
open={nativeSiteOpen}
|
||||||
@@ -1007,7 +1016,7 @@ export default function Page() {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
className="w-full max-w-xs justify-between font-normal"
|
className="w-full md:w-1/2 justify-between font-normal"
|
||||||
>
|
>
|
||||||
<span className="truncate">
|
<span className="truncate">
|
||||||
{nativeSelectedSite?.name ??
|
{nativeSelectedSite?.name ??
|
||||||
|
|||||||
@@ -256,7 +256,7 @@ export default function GeneralPage() {
|
|||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<div className="flex items-center gap-3">
|
<div className="">
|
||||||
<SwitchInput
|
<SwitchInput
|
||||||
id="auto-update-enabled"
|
id="auto-update-enabled"
|
||||||
label={t(
|
label={t(
|
||||||
@@ -285,7 +285,7 @@ export default function GeneralPage() {
|
|||||||
type="button"
|
type="button"
|
||||||
variant="link"
|
variant="link"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-auto p-0 pb-2 text-xs"
|
className="text-sm text-muted-foreground underline px-0"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
form.setValue(
|
form.setValue(
|
||||||
"autoUpdateOverrideOrg",
|
"autoUpdateOverrideOrg",
|
||||||
|
|||||||
@@ -243,10 +243,8 @@ export default function Page() {
|
|||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
form.setValue("autoProvision", checked);
|
form.setValue("autoProvision", checked);
|
||||||
}}
|
}}
|
||||||
|
description={t("idpAutoProvisionConfigureAfterCreate")}
|
||||||
/>
|
/>
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{t("idpAutoProvisionConfigureAfterCreate")}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|||||||
+2
-2
@@ -22,7 +22,7 @@
|
|||||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||||
--destructive: oklch(0.577 0.245 27.325);
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
--destructive-foreground: oklch(0.985 0 0);
|
--destructive-foreground: oklch(0.985 0 0);
|
||||||
--border: oklch(0.88 0.004 286.32);
|
--border: oklch(0.92 0.004 286.32);
|
||||||
--input: oklch(0.88 0.004 286.32);
|
--input: oklch(0.88 0.004 286.32);
|
||||||
--ring: oklch(0.705 0.213 47.604);
|
--ring: oklch(0.705 0.213 47.604);
|
||||||
--chart-1: oklch(0.646 0.222 41.116);
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
--accent-foreground: oklch(0.985 0 0);
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
--destructive: oklch(0.5382 0.1949 22.216);
|
--destructive: oklch(0.5382 0.1949 22.216);
|
||||||
--destructive-foreground: oklch(0.985 0 0);
|
--destructive-foreground: oklch(0.985 0 0);
|
||||||
--border: oklch(1 0 0 / 15%);
|
--border: oklch(1 0 0 / 8%);
|
||||||
--input: oklch(1 0 0 / 18%);
|
--input: oklch(1 0 0 / 18%);
|
||||||
--ring: oklch(0.646 0.222 41.116);
|
--ring: oklch(0.646 0.222 41.116);
|
||||||
--chart-1: oklch(0.488 0.243 264.376);
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
|
|||||||
@@ -105,7 +105,6 @@ export function BrowserGatewayTargetForm(props: BrowserGatewayTargetFormProps) {
|
|||||||
{t("destination")}
|
{t("destination")}
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
placeholder="192.168.1.1"
|
|
||||||
value={props.destination}
|
value={props.destination}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
props.onDestinationChange(e.target.value)
|
props.onDestinationChange(e.target.value)
|
||||||
@@ -116,7 +115,6 @@ export function BrowserGatewayTargetForm(props: BrowserGatewayTargetFormProps) {
|
|||||||
<label className="text-sm font-semibold">{t("port")}</label>
|
<label className="text-sm font-semibold">{t("port")}</label>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
placeholder={props.defaultPort.toString()}
|
|
||||||
value={props.destinationPort}
|
value={props.destinationPort}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
props.onDestinationPortChange(e.target.value)
|
props.onDestinationPortChange(e.target.value)
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ const CredenzaContent = ({ className, children, ...props }: CredenzaProps) => {
|
|||||||
return (
|
return (
|
||||||
<CredenzaContent
|
<CredenzaContent
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex min-h-0 max-h-[100dvh] flex-col overflow-y-auto md:top-[clamp(1.5rem,12vh,200px)] md:max-h-[calc(100vh-clamp(3rem,24vh,400px))] md:translate-y-0",
|
"flex min-h-0 max-h-[100dvh] flex-col overflow-y-auto md:top-[clamp(1.5rem,12vh,200px)] md:max-h-[calc(100dvh-clamp(1.5rem,12vh,200px)-1.5rem)] md:translate-y-0 md:overflow-hidden",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
SettingsSubsectionDescription,
|
||||||
|
SettingsSubsectionHeader,
|
||||||
|
SettingsSubsectionTitle
|
||||||
|
} from "@app/components/Settings";
|
||||||
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
||||||
import {
|
import {
|
||||||
OptionSelect,
|
OptionSelect,
|
||||||
@@ -1807,10 +1812,10 @@ export function PrivateResourceForm({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Mode */}
|
{/* Mode */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-2">
|
||||||
<p className="text-sm font-semibold">
|
<SettingsSubsectionTitle>
|
||||||
{t("sshServerMode")}
|
{t("sshServerMode")}
|
||||||
</p>
|
</SettingsSubsectionTitle>
|
||||||
<StrategySelect<"standard" | "native">
|
<StrategySelect<"standard" | "native">
|
||||||
value={sshServerMode}
|
value={sshServerMode}
|
||||||
options={[
|
options={[
|
||||||
@@ -1864,10 +1869,10 @@ export function PrivateResourceForm({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-2">
|
||||||
<p className="text-sm font-semibold">
|
<SettingsSubsectionTitle>
|
||||||
{t("sshAuthenticationMethod")}
|
{t("sshAuthenticationMethod")}
|
||||||
</p>
|
</SettingsSubsectionTitle>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="pamMode"
|
name="pamMode"
|
||||||
@@ -1959,10 +1964,10 @@ export function PrivateResourceForm({
|
|||||||
|
|
||||||
{/* Daemon Location (standard + push) */}
|
{/* Daemon Location (standard + push) */}
|
||||||
{showDaemonLocation && (
|
{showDaemonLocation && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-2">
|
||||||
<p className="text-sm font-semibold">
|
<SettingsSubsectionTitle>
|
||||||
{t("sshAuthDaemonLocation")}
|
{t("sshAuthDaemonLocation")}
|
||||||
</p>
|
</SettingsSubsectionTitle>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="authDaemonMode"
|
name="authDaemonMode"
|
||||||
@@ -2062,51 +2067,57 @@ export function PrivateResourceForm({
|
|||||||
|
|
||||||
{/* Daemon Port (standard + push + remote) */}
|
{/* Daemon Port (standard + push + remote) */}
|
||||||
{showDaemonPort && (
|
{showDaemonPort && (
|
||||||
<FormField
|
<div className="w-full md:w-1/2">
|
||||||
control={form.control}
|
<FormField
|
||||||
name="authDaemonPort"
|
control={form.control}
|
||||||
render={({ field }) => (
|
name="authDaemonPort"
|
||||||
<FormItem>
|
render={({ field }) => (
|
||||||
<FormLabel>
|
<FormItem>
|
||||||
{t("sshDaemonPort")}
|
<FormLabel>
|
||||||
</FormLabel>
|
{t("sshDaemonPort")}
|
||||||
<FormControl>
|
</FormLabel>
|
||||||
<Input
|
<FormControl>
|
||||||
type="number"
|
<Input
|
||||||
min={1}
|
type="number"
|
||||||
max={65535}
|
min={1}
|
||||||
placeholder="22123"
|
max={65535}
|
||||||
disabled={
|
placeholder="22123"
|
||||||
sshSectionDisabled
|
disabled={
|
||||||
}
|
sshSectionDisabled
|
||||||
value={field.value ?? ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (sshSectionDisabled)
|
|
||||||
return;
|
|
||||||
const v =
|
|
||||||
e.target.value;
|
|
||||||
if (v === "") {
|
|
||||||
field.onChange(
|
|
||||||
null
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
const num = parseInt(
|
value={
|
||||||
v,
|
field.value ?? ""
|
||||||
10
|
}
|
||||||
);
|
onChange={(e) => {
|
||||||
field.onChange(
|
if (
|
||||||
Number.isNaN(num)
|
sshSectionDisabled
|
||||||
? null
|
)
|
||||||
: num
|
return;
|
||||||
);
|
const v =
|
||||||
}}
|
e.target.value;
|
||||||
/>
|
if (v === "") {
|
||||||
</FormControl>
|
field.onChange(
|
||||||
<FormMessage />
|
null
|
||||||
</FormItem>
|
);
|
||||||
)}
|
return;
|
||||||
/>
|
}
|
||||||
|
const num =
|
||||||
|
parseInt(v, 10);
|
||||||
|
field.onChange(
|
||||||
|
Number.isNaN(
|
||||||
|
num
|
||||||
|
)
|
||||||
|
? null
|
||||||
|
: num
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -823,7 +823,7 @@ function TargetStatusCell({
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="flex items-center gap-2 h-8 px-2 font-normal"
|
className="flex items-center gap-2 h-8 px-0 font-normal"
|
||||||
>
|
>
|
||||||
<StatusIcon status={overallStatus} />
|
<StatusIcon status={overallStatus} />
|
||||||
<span className="text-sm">
|
<span className="text-sm">
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ export default function SetResourceHeaderAuthForm({
|
|||||||
label={t(
|
label={t(
|
||||||
"headerAuthCompatibility"
|
"headerAuthCompatibility"
|
||||||
)}
|
)}
|
||||||
info={t(
|
description={t(
|
||||||
"headerAuthCompatibilityInfo"
|
"headerAuthCompatibilityInfo"
|
||||||
)}
|
)}
|
||||||
checked={field.value}
|
checked={field.value}
|
||||||
|
|||||||
@@ -63,6 +63,42 @@ export function SettingsSectionDescription({
|
|||||||
return <p className="text-muted-foreground text-sm">{children}</p>;
|
return <p className="text-muted-foreground text-sm">{children}</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function SettingsSubsectionHeader({
|
||||||
|
children,
|
||||||
|
className
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
return <div className={cn("space-y-0.5", className)}>{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingsSubsectionTitle({
|
||||||
|
children,
|
||||||
|
className
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<h3 className={cn("text-sm font-semibold", className)}>{children}</h3>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingsSubsectionDescription({
|
||||||
|
children,
|
||||||
|
className
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<p className={cn("text-sm text-muted-foreground", className)}>
|
||||||
|
{children}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function SettingsSectionBody({
|
export function SettingsSectionBody({
|
||||||
children
|
children
|
||||||
}: {
|
}: {
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ export function SwitchInput({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="flex flex-col space-y-2">
|
||||||
<div className="flex items-center space-x-2 mb-2">
|
<div className="flex items-center space-x-2">
|
||||||
{label && (
|
{label && (
|
||||||
<Label
|
<Label
|
||||||
htmlFor={id}
|
htmlFor={id}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type { CreateOrEditLabelResponse } from "@server/routers/labels/types";
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import type { AxiosResponse } from "axios";
|
import type { AxiosResponse } from "axios";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useActionState, useMemo, useState } from "react";
|
import { useActionState, useMemo, useRef, useState } from "react";
|
||||||
import { useDebounce } from "use-debounce";
|
import { useDebounce } from "use-debounce";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
import { Checkbox } from "./ui/checkbox";
|
import { Checkbox } from "./ui/checkbox";
|
||||||
@@ -88,6 +88,11 @@ export function LabelsSelector({
|
|||||||
colorValues[Math.floor(Math.random() * colorValues.length)];
|
colorValues[Math.floor(Math.random() * colorValues.length)];
|
||||||
|
|
||||||
const [, action, isPending] = useActionState(createLabel, null);
|
const [, action, isPending] = useActionState(createLabel, null);
|
||||||
|
const createFormRef = useRef<HTMLFormElement>(null);
|
||||||
|
|
||||||
|
const trimmedQuery = labelSearchQuery.trim();
|
||||||
|
const canCreateLabel =
|
||||||
|
trimmedQuery.length > 0 && labelsShown.length === 0 && !isPending;
|
||||||
|
|
||||||
async function createLabel(_: any, formData: FormData) {
|
async function createLabel(_: any, formData: FormData) {
|
||||||
const name = formData.get("name")?.toString();
|
const name = formData.get("name")?.toString();
|
||||||
@@ -120,21 +125,28 @@ export function LabelsSelector({
|
|||||||
return (
|
return (
|
||||||
<Command shouldFilter={false}>
|
<Command shouldFilter={false}>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder={t("labelSearch")}
|
placeholder={t("labelSearchOrCreate")}
|
||||||
value={labelSearchQuery}
|
value={labelSearchQuery}
|
||||||
onValueChange={setlabelsSearchQuery}
|
onValueChange={setlabelsSearchQuery}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" && canCreateLabel) {
|
||||||
|
e.preventDefault();
|
||||||
|
createFormRef.current?.requestSubmit();
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty className="px-3 break-all wrap-anywhere text-wrap">
|
<CommandEmpty className="px-3 py-6 text-center text-wrap">
|
||||||
{labelSearchQuery.trim().length > 0 ? (
|
{labelSearchQuery.trim().length > 0 ? (
|
||||||
<div className="flex flex-col gap-2 items-center">
|
<div className="flex flex-col gap-2 items-center">
|
||||||
<span className="max-w-34">
|
<span className="max-w-34 break-words">
|
||||||
{t("createNewLabel", {
|
{t("createNewLabel", {
|
||||||
label: labelSearchQuery.trim()
|
label: labelSearchQuery.trim()
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
|
ref={createFormRef}
|
||||||
action={action}
|
action={action}
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
>
|
>
|
||||||
@@ -159,14 +171,17 @@ export function LabelsSelector({
|
|||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="size-4 rounded-full bg-(--color) flex-none"
|
className="size-2 rounded-full bg-(--color) flex-none"
|
||||||
style={{
|
style={{
|
||||||
// @ts-expect-error css color
|
// @ts-expect-error css color
|
||||||
"--color": value
|
"--color": value
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span data-name>
|
<span data-name>
|
||||||
{color}
|
{color
|
||||||
|
.charAt(0)
|
||||||
|
.toUpperCase() +
|
||||||
|
color.slice(1)}
|
||||||
</span>
|
</span>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
)
|
)
|
||||||
@@ -176,7 +191,6 @@ export function LabelsSelector({
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
|
||||||
loading={isPending}
|
loading={isPending}
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
@@ -185,7 +199,14 @@ export function LabelsSelector({
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
t("labelsNotFound")
|
<div className="flex flex-col gap-1 items-center">
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{t("labelsNotFound")}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm">
|
||||||
|
{t("labelsEmptyCreateHint")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</CommandEmpty>
|
</CommandEmpty>
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
FaLinux,
|
FaLinux,
|
||||||
FaWindows
|
FaWindows
|
||||||
} from "react-icons/fa";
|
} from "react-icons/fa";
|
||||||
|
import { ExternalLink } from "lucide-react";
|
||||||
import { SiKubernetes, SiNixos } from "react-icons/si";
|
import { SiKubernetes, SiNixos } from "react-icons/si";
|
||||||
|
|
||||||
export type CommandItem = string | { title: string; command: string };
|
export type CommandItem = string | { title: string; command: string };
|
||||||
@@ -333,31 +334,36 @@ WantedBy=default.target`
|
|||||||
<p className="font-semibold mb-3">{t("commands")}</p>
|
<p className="font-semibold mb-3">{t("commands")}</p>
|
||||||
{platform === "kubernetes" && (
|
{platform === "kubernetes" && (
|
||||||
<p className="text-sm text-muted-foreground mb-3">
|
<p className="text-sm text-muted-foreground mb-3">
|
||||||
For more and up to date Kubernetes installation
|
{t.rich("siteInstallKubernetesDocsDescription", {
|
||||||
information, see{" "}
|
docsLink: (chunks) => (
|
||||||
<a
|
<a
|
||||||
href="https://docs.pangolin.net/manage/sites/install-kubernetes"
|
href="https://docs.pangolin.net/manage/sites/install-kubernetes"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="underline"
|
className="text-primary hover:underline inline-flex items-center gap-1"
|
||||||
>
|
>
|
||||||
docs.pangolin.net/manage/sites/install-kubernetes
|
{chunks}
|
||||||
</a>
|
<ExternalLink className="size-3.5 shrink-0" />
|
||||||
.
|
</a>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{platform === "advantech" && (
|
{platform === "advantech" && (
|
||||||
<p className="text-sm text-muted-foreground mb-3">
|
<p className="text-sm text-muted-foreground mb-3">
|
||||||
For Advantech modem installation instructions, see{" "}
|
{t.rich("siteInstallAdvantechDocsDescription", {
|
||||||
<a
|
docsLink: (chunks) => (
|
||||||
href="https://docs.pangolin.net/manage/sites/install-advantech"
|
<a
|
||||||
target="_blank"
|
href="https://docs.pangolin.net/manage/sites/install-advantech"
|
||||||
rel="noreferrer"
|
target="_blank"
|
||||||
className="underline"
|
rel="noopener noreferrer"
|
||||||
>
|
className="text-primary hover:underline inline-flex items-center gap-1"
|
||||||
docs.pangolin.net/manage/sites/install-advantech
|
>
|
||||||
</a>
|
{chunks}
|
||||||
.
|
<ExternalLink className="size-3.5 shrink-0" />
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<div className="mt-2 space-y-3">
|
<div className="mt-2 space-y-3">
|
||||||
|
|||||||
@@ -385,7 +385,7 @@ export function CreatePolicyAuthMethodsSectionForm({
|
|||||||
label={t(
|
label={t(
|
||||||
"headerAuthCompatibility"
|
"headerAuthCompatibility"
|
||||||
)}
|
)}
|
||||||
info={t(
|
description={t(
|
||||||
"headerAuthCompatibilityInfo"
|
"headerAuthCompatibilityInfo"
|
||||||
)}
|
)}
|
||||||
checked={field.value}
|
checked={field.value}
|
||||||
@@ -426,7 +426,10 @@ export function CreatePolicyAuthMethodsSectionForm({
|
|||||||
{/* Password row */}
|
{/* Password row */}
|
||||||
<div className="flex items-center justify-between border rounded-md p-2 mb-4">
|
<div className="flex items-center justify-between border rounded-md p-2 mb-4">
|
||||||
<div
|
<div
|
||||||
className={cn("flex items-center text-sm space-x-2", password && "text-green-500")}
|
className={cn(
|
||||||
|
"flex items-center text-sm space-x-2",
|
||||||
|
password && "text-green-500"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<Key size="14" />
|
<Key size="14" />
|
||||||
<span>
|
<span>
|
||||||
@@ -456,7 +459,10 @@ export function CreatePolicyAuthMethodsSectionForm({
|
|||||||
{/* Pincode row */}
|
{/* Pincode row */}
|
||||||
<div className="flex items-center justify-between border rounded-md p-2">
|
<div className="flex items-center justify-between border rounded-md p-2">
|
||||||
<div
|
<div
|
||||||
className={cn("flex items-center space-x-2 text-sm", pincode && "text-green-500")}
|
className={cn(
|
||||||
|
"flex items-center space-x-2 text-sm",
|
||||||
|
pincode && "text-green-500"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<Binary size="14" />
|
<Binary size="14" />
|
||||||
<span>
|
<span>
|
||||||
@@ -484,7 +490,10 @@ export function CreatePolicyAuthMethodsSectionForm({
|
|||||||
{/* Header auth row */}
|
{/* Header auth row */}
|
||||||
<div className="flex items-center justify-between border rounded-md p-2">
|
<div className="flex items-center justify-between border rounded-md p-2">
|
||||||
<div
|
<div
|
||||||
className={cn("flex items-center space-x-2 text-sm", headerAuth && "text-green-500")}
|
className={cn(
|
||||||
|
"flex items-center space-x-2 text-sm",
|
||||||
|
headerAuth && "text-green-500"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<Bot size="14" />
|
<Bot size="14" />
|
||||||
<span>
|
<span>
|
||||||
|
|||||||
@@ -491,7 +491,7 @@ export function EditPolicyAuthMethodsSectionForm({
|
|||||||
label={t(
|
label={t(
|
||||||
"headerAuthCompatibility"
|
"headerAuthCompatibility"
|
||||||
)}
|
)}
|
||||||
info={t(
|
description={t(
|
||||||
"headerAuthCompatibilityInfo"
|
"headerAuthCompatibilityInfo"
|
||||||
)}
|
)}
|
||||||
checked={field.value}
|
checked={field.value}
|
||||||
|
|||||||
@@ -670,7 +670,7 @@ export function PolicyAuthMethodsSection({
|
|||||||
label={t(
|
label={t(
|
||||||
"headerAuthCompatibility"
|
"headerAuthCompatibility"
|
||||||
)}
|
)}
|
||||||
info={t(
|
description={t(
|
||||||
"headerAuthCompatibilityInfo"
|
"headerAuthCompatibilityInfo"
|
||||||
)}
|
)}
|
||||||
checked={field.value}
|
checked={field.value}
|
||||||
|
|||||||
Reference in New Issue
Block a user