mirror of
https://github.com/fosrl/pangolin.git
synced 2026-06-13 19:07:18 +00:00
Merge branch 'dev' of github.com:fosrl/pangolin into dev
This commit is contained in:
@@ -1,128 +1,173 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@app/lib/cn";
|
||||
import { ChevronsUpDown, ExternalLink } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useState } from "react";
|
||||
import type { Control, FieldValues, Path } from "react-hook-form";
|
||||
import { useWatch } from "react-hook-form";
|
||||
import {
|
||||
MultiSitesSelector,
|
||||
formatMultiSitesSelectorLabel
|
||||
} from "./multi-site-selector";
|
||||
import { SitesSelector, type Selectedsite } from "./site-selector";
|
||||
import { Button } from "./ui/button";
|
||||
import {
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from "./ui/form";
|
||||
import { Input } from "./ui/input";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
|
||||
|
||||
type SingleSiteProps = {
|
||||
multiSite?: false;
|
||||
selectedSite: Selectedsite | null;
|
||||
onSiteChange: (site: Selectedsite | null) => void;
|
||||
};
|
||||
|
||||
type MultiSiteProps = {
|
||||
multiSite: true;
|
||||
selectedSites: Selectedsite[];
|
||||
onSitesChange: (sites: Selectedsite[]) => void;
|
||||
};
|
||||
|
||||
export type BrowserGatewayTargetFormProps = {
|
||||
type BaseProps<T extends FieldValues> = {
|
||||
control: Control<T>;
|
||||
orgId: string;
|
||||
destination: string;
|
||||
defaultPort: number;
|
||||
destinationPort: string;
|
||||
onDestinationChange: (v: string) => void;
|
||||
onDestinationPortChange: (v: string) => void;
|
||||
destinationField: Path<T>;
|
||||
destinationPortField: Path<T>;
|
||||
learnMoreHref?: string;
|
||||
} & (SingleSiteProps | MultiSiteProps);
|
||||
defaultPort: number;
|
||||
};
|
||||
|
||||
export function BrowserGatewayTargetForm(props: BrowserGatewayTargetFormProps) {
|
||||
type MultiSiteFormProps<T extends FieldValues> = BaseProps<T> & {
|
||||
multiSite: true;
|
||||
sitesField: Path<T>;
|
||||
};
|
||||
|
||||
type SingleSiteFormProps<T extends FieldValues> = BaseProps<T> & {
|
||||
multiSite?: false;
|
||||
siteField: Path<T>;
|
||||
};
|
||||
|
||||
export type BrowserGatewayTargetFormProps<T extends FieldValues = FieldValues> =
|
||||
| MultiSiteFormProps<T>
|
||||
| SingleSiteFormProps<T>;
|
||||
|
||||
export function BrowserGatewayTargetForm<T extends FieldValues>(
|
||||
props: BrowserGatewayTargetFormProps<T>
|
||||
) {
|
||||
const t = useTranslations();
|
||||
const [siteOpen, setSiteOpen] = useState(false);
|
||||
|
||||
const siteSelector =
|
||||
props.multiSite === true ? (
|
||||
<Popover open={siteOpen} onOpenChange={setSiteOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className="w-full justify-between font-normal"
|
||||
>
|
||||
<span className="truncate">
|
||||
{formatMultiSitesSelectorLabel(
|
||||
props.selectedSites,
|
||||
t
|
||||
)}
|
||||
</span>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0">
|
||||
<MultiSitesSelector
|
||||
orgId={props.orgId}
|
||||
selectedSites={props.selectedSites}
|
||||
onSelectionChange={props.onSitesChange}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : (
|
||||
<Popover open={siteOpen} onOpenChange={setSiteOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className="w-full justify-between font-normal"
|
||||
>
|
||||
<span className="truncate">
|
||||
{props.selectedSite?.name ?? t("siteSelect")}
|
||||
</span>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0">
|
||||
<SitesSelector
|
||||
orgId={props.orgId}
|
||||
selectedSite={props.selectedSite}
|
||||
onSelectSite={(site) => {
|
||||
props.onSiteChange(site);
|
||||
setSiteOpen(false);
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
const sitesFieldName =
|
||||
props.multiSite === true ? props.sitesField : props.siteField;
|
||||
|
||||
const watchedSites = useWatch({
|
||||
control: props.control,
|
||||
name: sitesFieldName
|
||||
});
|
||||
|
||||
const showMultiSiteDisclaimer =
|
||||
props.multiSite === true &&
|
||||
((watchedSites as Selectedsite[] | undefined)?.length ?? 0) > 1;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-semibold">
|
||||
{t("sites")}
|
||||
</label>
|
||||
{siteSelector}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-semibold">
|
||||
{t("destination")}
|
||||
</label>
|
||||
<Input
|
||||
value={props.destination}
|
||||
onChange={(e) =>
|
||||
props.onDestinationChange(e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-semibold">{t("port")}</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={props.destinationPort}
|
||||
onChange={(e) =>
|
||||
props.onDestinationPortChange(e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4 items-start">
|
||||
<FormField
|
||||
control={props.control}
|
||||
name={sitesFieldName}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("sites")}</FormLabel>
|
||||
<Popover open={siteOpen} onOpenChange={setSiteOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
"w-full justify-between font-normal",
|
||||
"aria-invalid:border-destructive aria-invalid:ring-destructive/20",
|
||||
props.multiSite === true
|
||||
? (
|
||||
field.value as Selectedsite[]
|
||||
)?.length === 0 &&
|
||||
"text-muted-foreground"
|
||||
: !field.value &&
|
||||
"text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
<span className="truncate">
|
||||
{props.multiSite === true
|
||||
? formatMultiSitesSelectorLabel(
|
||||
(field.value as Selectedsite[]) ??
|
||||
[],
|
||||
t
|
||||
)
|
||||
: ((
|
||||
field.value as Selectedsite | null
|
||||
)?.name ??
|
||||
t("siteSelect"))}
|
||||
</span>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0">
|
||||
{props.multiSite === true ? (
|
||||
<MultiSitesSelector
|
||||
orgId={props.orgId}
|
||||
selectedSites={
|
||||
(field.value as Selectedsite[]) ??
|
||||
[]
|
||||
}
|
||||
onSelectionChange={field.onChange}
|
||||
/>
|
||||
) : (
|
||||
<SitesSelector
|
||||
orgId={props.orgId}
|
||||
selectedSite={
|
||||
field.value as Selectedsite | null
|
||||
}
|
||||
onSelectSite={(site) => {
|
||||
field.onChange(site);
|
||||
setSiteOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={props.control}
|
||||
name={props.destinationField}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("destination")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} value={field.value ?? ""} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={props.control}
|
||||
name={props.destinationPortField}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("port")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={65535}
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{props.multiSite === true && props.selectedSites.length > 1 && (
|
||||
{showMultiSiteDisclaimer && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("bgTargetMultiSiteDisclaimer")}{" "}
|
||||
<a
|
||||
|
||||
@@ -408,12 +408,7 @@ export function HealthCheckCredenza(props: HealthCheckCredenzaProps) {
|
||||
? t("standaloneHcEditTitle")
|
||||
: t("standaloneHcCreateTitle");
|
||||
|
||||
const description =
|
||||
mode === "autoSave"
|
||||
? t("configureHealthCheckDescription", {
|
||||
target: (props as any).targetAddress
|
||||
})
|
||||
: t("standaloneHcDescription");
|
||||
const description = t("configureHealthCheckDescription");
|
||||
|
||||
const disableTabInputs = mode === "autoSave" && !watchedEnabled;
|
||||
const isSnmpOrIcmp = watchedMode === "snmp" || watchedMode === "icmp";
|
||||
|
||||
@@ -1813,9 +1813,9 @@ export function PrivateResourceForm({
|
||||
|
||||
{/* Mode */}
|
||||
<div className="space-y-2">
|
||||
<SettingsSubsectionTitle>
|
||||
<p className="font-semibold text-sm">
|
||||
{t("sshServerMode")}
|
||||
</SettingsSubsectionTitle>
|
||||
</p>
|
||||
<StrategySelect<"standard" | "native">
|
||||
value={sshServerMode}
|
||||
options={[
|
||||
@@ -1870,9 +1870,9 @@ export function PrivateResourceForm({
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<SettingsSubsectionTitle>
|
||||
<p className="font-semibold text-sm">
|
||||
{t("sshAuthenticationMethod")}
|
||||
</SettingsSubsectionTitle>
|
||||
</p>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="pamMode"
|
||||
@@ -1965,9 +1965,9 @@ export function PrivateResourceForm({
|
||||
{/* Daemon Location (standard + push) */}
|
||||
{showDaemonLocation && (
|
||||
<div className="space-y-2">
|
||||
<SettingsSubsectionTitle>
|
||||
<p className="font-semibold text-sm">
|
||||
{t("sshAuthDaemonLocation")}
|
||||
</SettingsSubsectionTitle>
|
||||
</p>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="authDaemonMode"
|
||||
|
||||
@@ -90,7 +90,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<span className="inline-flex items-center">
|
||||
{resource.mode!.toUpperCase()}
|
||||
{resource.ssl ? "HTTPS" : "HTTP"}
|
||||
</span>
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
|
||||
@@ -200,7 +200,7 @@ export function ResourcePoliciesTable({
|
||||
<DropdownMenuContent align="end">
|
||||
<Link
|
||||
className="block w-full"
|
||||
href={`/${policyRow.orgId}/settings/policies/resource/${policyRow.niceId}`}
|
||||
href={`/${policyRow.orgId}/settings/policies/resources/public/${policyRow.niceId}`}
|
||||
>
|
||||
<DropdownMenuItem>
|
||||
{t("viewSettings")}
|
||||
@@ -219,7 +219,7 @@ export function ResourcePoliciesTable({
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Link
|
||||
href={`/${policyRow.orgId}/settings/policies/resource/${policyRow.niceId}`}
|
||||
href={`/${policyRow.orgId}/settings/policies/resources/public/${policyRow.niceId}`}
|
||||
>
|
||||
<Button variant={"outline"}>
|
||||
{t("edit")}
|
||||
@@ -288,7 +288,7 @@ export function ResourcePoliciesTable({
|
||||
onAdd={() =>
|
||||
startNavigation(() =>
|
||||
router.push(
|
||||
`/${orgId}/settings/policies/resource/create`
|
||||
`/${orgId}/settings/policies/resources/public/create`
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ export function SettingsSubsectionHeader({
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return <div className={cn("space-y-0.5", className)}>{children}</div>;
|
||||
return <div className={cn("py-3 space-y-0.5", className)}>{children}</div>;
|
||||
}
|
||||
|
||||
export function SettingsSubsectionTitle({
|
||||
@@ -80,9 +80,7 @@ export function SettingsSubsectionTitle({
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<h3 className={cn("text-sm font-semibold", className)}>{children}</h3>
|
||||
);
|
||||
return <h3 className={cn("font-semibold", className)}>{children}</h3>;
|
||||
}
|
||||
|
||||
export function SettingsSubsectionDescription({
|
||||
|
||||
@@ -157,7 +157,7 @@ export function LabelsSelector({
|
||||
/>
|
||||
|
||||
<Select defaultValue={randomColor} name="color">
|
||||
<SelectTrigger className="w-18 [&_[data-name]]:hidden [&_[svg]]:hidden!">
|
||||
<SelectTrigger className="w-auto min-w-24">
|
||||
<SelectValue
|
||||
placeholder={t("selectColor")}
|
||||
/>
|
||||
|
||||
@@ -140,7 +140,7 @@ export function CreatePolicyForm({}: CreatePolicyFormProps) {
|
||||
if (res && res.status === 201) {
|
||||
const niceId = res.data.data.niceId;
|
||||
router.push(
|
||||
`/${org.org.orgId}/settings/policies/resource/${niceId}`
|
||||
`/${org.org.orgId}/settings/policies/resources/public/${niceId}`
|
||||
);
|
||||
toast({
|
||||
title: t("success"),
|
||||
|
||||
@@ -109,7 +109,7 @@ export function EditPolicyNameSectionForm({
|
||||
|
||||
if (payload.niceId && payload.niceId !== policy.niceId) {
|
||||
router.replace(
|
||||
`/${org.org.orgId}/settings/policies/resource/${payload.niceId}`
|
||||
`/${org.org.orgId}/settings/policies/resources/public/${payload.niceId}`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import { cn } from "@app/lib/cn";
|
||||
import type { DockerState } from "@app/lib/docker";
|
||||
import { parseHostTarget } from "@app/lib/parseHostTarget";
|
||||
import { orgQueries } from "@app/lib/queries";
|
||||
import { CaretSortIcon } from "@radix-ui/react-icons";
|
||||
import type { ListSitesResponse } from "@server/routers/site";
|
||||
import { type ListTargetsResponse } from "@server/routers/target";
|
||||
import type { ArrayElement } from "@server/types/ArrayElement";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { ContainersSelector } from "./ContainersSelector";
|
||||
import { Button } from "./ui/button";
|
||||
import { Input } from "./ui/input";
|
||||
@@ -28,23 +25,21 @@ export type LocalTarget = Omit<
|
||||
"protocol"
|
||||
>;
|
||||
|
||||
export type ResourceTargetAddressItemProps = {
|
||||
export type ResourceTargetSiteItemProps = {
|
||||
getDockerStateForSite: (siteId: number) => DockerState;
|
||||
updateTarget: (targetId: number, data: Partial<LocalTarget>) => void;
|
||||
orgId: string;
|
||||
proxyTarget: LocalTarget;
|
||||
isHttp: boolean;
|
||||
refreshContainersForSite: (siteId: number) => void;
|
||||
};
|
||||
|
||||
export function ResourceTargetAddressItem({
|
||||
export function ResourceTargetSiteItem({
|
||||
orgId,
|
||||
getDockerStateForSite,
|
||||
updateTarget,
|
||||
proxyTarget,
|
||||
isHttp,
|
||||
refreshContainersForSite
|
||||
}: ResourceTargetAddressItemProps) {
|
||||
}: ResourceTargetSiteItemProps) {
|
||||
const t = useTranslations();
|
||||
|
||||
const [selectedSite, setSelectedSite] = useState<Pick<
|
||||
@@ -76,62 +71,78 @@ export function ResourceTargetAddressItem({
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex w-full min-w-0 items-center h-9 border border-input rounded-md"
|
||||
key={proxyTarget.targetId}
|
||||
>
|
||||
{selectedSite && selectedSite.type === "newt" && (
|
||||
<ContainersSelector
|
||||
site={selectedSite}
|
||||
containers={
|
||||
getDockerStateForSite(selectedSite.siteId).containers
|
||||
}
|
||||
isAvailable={
|
||||
getDockerStateForSite(selectedSite.siteId).isAvailable
|
||||
}
|
||||
onContainerSelect={handleContainerSelectForTarget}
|
||||
onRefresh={() =>
|
||||
refreshContainersForSite(selectedSite.siteId)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
"h-9 min-w-0 flex-1 justify-between px-3 rounded-none hover:bg-transparent",
|
||||
!proxyTarget.siteId && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
<span className="truncate">
|
||||
{proxyTarget.siteId
|
||||
? selectedSite?.name
|
||||
: t("siteSelect")}
|
||||
</span>
|
||||
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0">
|
||||
<SitesSelector
|
||||
orgId={orgId}
|
||||
selectedSite={selectedSite}
|
||||
onSelectSite={(site) => {
|
||||
updateTarget(proxyTarget.targetId, {
|
||||
siteId: site.siteId,
|
||||
siteType: site.type,
|
||||
siteName: site.name
|
||||
});
|
||||
setSelectedSite(site);
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export type ResourceTargetAddressItemProps = {
|
||||
updateTarget: (targetId: number, data: Partial<LocalTarget>) => void;
|
||||
proxyTarget: LocalTarget;
|
||||
isHttp: boolean;
|
||||
};
|
||||
|
||||
export function ResourceTargetAddressItem({
|
||||
updateTarget,
|
||||
proxyTarget,
|
||||
isHttp
|
||||
}: ResourceTargetAddressItemProps) {
|
||||
return (
|
||||
<div className="flex items-center w-full" key={proxyTarget.targetId}>
|
||||
<div className="flex items-center w-full justify-start py-0 space-x-2 px-0 cursor-default border border-input rounded-md">
|
||||
{selectedSite && selectedSite.type === "newt" && (
|
||||
<ContainersSelector
|
||||
site={selectedSite}
|
||||
containers={
|
||||
getDockerStateForSite(selectedSite.siteId)
|
||||
.containers
|
||||
}
|
||||
isAvailable={
|
||||
getDockerStateForSite(selectedSite.siteId)
|
||||
.isAvailable
|
||||
}
|
||||
onContainerSelect={handleContainerSelectForTarget}
|
||||
onRefresh={() =>
|
||||
refreshContainersForSite(selectedSite.siteId)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
"w-45 justify-between text-sm border-r pr-4 rounded-none h-8 hover:bg-transparent",
|
||||
"",
|
||||
!proxyTarget.siteId && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
<span className="truncate max-w-37.5">
|
||||
{proxyTarget.siteId
|
||||
? selectedSite?.name
|
||||
: t("siteSelect")}
|
||||
</span>
|
||||
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0">
|
||||
<SitesSelector
|
||||
orgId={orgId}
|
||||
selectedSite={selectedSite}
|
||||
onSelectSite={(site) => {
|
||||
updateTarget(proxyTarget.targetId, {
|
||||
siteId: site.siteId,
|
||||
siteType: site.type,
|
||||
siteName: site.name
|
||||
});
|
||||
setSelectedSite(site);
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{isHttp && (
|
||||
<Select
|
||||
defaultValue={proxyTarget.method ?? "http"}
|
||||
@@ -142,7 +153,7 @@ export function ResourceTargetAddressItem({
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-8 px-2 w-17.5 border-none bg-transparent shadow-none data-[state=open]:bg-transparent rounded-none">
|
||||
<SelectTrigger className="h-9 w-17.5 border-none bg-transparent shadow-none data-[state=open]:bg-transparent rounded-none mr-0 pr-0">
|
||||
{proxyTarget.method || "http"}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -154,7 +165,7 @@ export function ResourceTargetAddressItem({
|
||||
)}
|
||||
|
||||
{isHttp && (
|
||||
<div className="flex items-center justify-center px-2 h-9">
|
||||
<div className="flex items-center justify-center h-9 mr-0 pl-1">
|
||||
{"://"}
|
||||
</div>
|
||||
)}
|
||||
@@ -162,7 +173,7 @@ export function ResourceTargetAddressItem({
|
||||
<Input
|
||||
defaultValue={proxyTarget.ip}
|
||||
placeholder="Host"
|
||||
className="flex-1 min-w-30 px-2 border-none placeholder-gray-400 rounded-xs"
|
||||
className="flex-1 min-w-30 border-none placeholder-gray-400 rounded-xs"
|
||||
onBlur={(e) => {
|
||||
const input = e.target.value.trim();
|
||||
const hasProtocol = /^(https?|h2c):\/\//.test(input);
|
||||
@@ -195,7 +206,7 @@ export function ResourceTargetAddressItem({
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center justify-center px-2 h-9">
|
||||
<div className="flex items-center justify-center h-9 mr-0">
|
||||
{":"}
|
||||
</div>
|
||||
<Input
|
||||
|
||||
Reference in New Issue
Block a user