many minor visual improvements

This commit is contained in:
miloschwartz
2026-06-04 22:25:03 -07:00
parent 2adb7b64cb
commit add9b8dfb0
20 changed files with 998 additions and 869 deletions
+6 -2
View File
@@ -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",
+1 -3
View File
@@ -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
View File
@@ -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)
+1 -1
View File
@@ -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
+64 -53
View File
@@ -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>
)} )}
+1 -1
View File
@@ -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">
+1 -1
View File
@@ -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}
+36
View File
@@ -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
}: { }: {
+2 -2
View File
@@ -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}
+29 -8
View File
@@ -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>
+27 -21
View File
@@ -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}