mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-17 14:34:42 +00:00
🚧 wip
This commit is contained in:
@@ -40,7 +40,12 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
import { UserType } from "@server/types/UserTypes";
|
import { UserType } from "@server/types/UserTypes";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { ChevronsUpDown, ExternalLink } from "lucide-react";
|
import {
|
||||||
|
ArrowDownIcon,
|
||||||
|
ChevronDownIcon,
|
||||||
|
ChevronsUpDown,
|
||||||
|
ExternalLink
|
||||||
|
} from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
@@ -50,7 +55,7 @@ import {
|
|||||||
formatMultiSitesSelectorLabel
|
formatMultiSitesSelectorLabel
|
||||||
} from "./multi-site-selector";
|
} from "./multi-site-selector";
|
||||||
import type { Selectedsite } from "./site-selector";
|
import type { Selectedsite } from "./site-selector";
|
||||||
import { CaretSortIcon } from "@radix-ui/react-icons";
|
|
||||||
import { MachinesSelector } from "./machines-selector";
|
import { MachinesSelector } from "./machines-selector";
|
||||||
import DomainPicker from "@app/components/DomainPicker";
|
import DomainPicker from "@app/components/DomainPicker";
|
||||||
import { SwitchInput } from "@app/components/SwitchInput";
|
import { SwitchInput } from "@app/components/SwitchInput";
|
||||||
@@ -155,7 +160,7 @@ export type InternalResourceData = {
|
|||||||
const tagSchema = z.object({ id: z.string(), text: z.string() });
|
const tagSchema = z.object({ id: z.string(), text: z.string() });
|
||||||
|
|
||||||
function buildSelectedSitesForResource(
|
function buildSelectedSitesForResource(
|
||||||
resource: InternalResourceData,
|
resource: InternalResourceData
|
||||||
): Selectedsite[] {
|
): Selectedsite[] {
|
||||||
return resource.siteIds.map((siteId, idx) => ({
|
return resource.siteIds.map((siteId, idx) => ({
|
||||||
name: resource.siteNames[idx] ?? "",
|
name: resource.siteNames[idx] ?? "",
|
||||||
@@ -608,9 +613,7 @@ export function InternalResourceForm({
|
|||||||
users: [],
|
users: [],
|
||||||
clients: []
|
clients: []
|
||||||
});
|
});
|
||||||
setSelectedSites(
|
setSelectedSites(buildSelectedSitesForResource(resource));
|
||||||
buildSelectedSitesForResource(resource)
|
|
||||||
);
|
|
||||||
setTcpPortMode(
|
setTcpPortMode(
|
||||||
getPortModeFromString(resource.tcpPortRangeString)
|
getPortModeFromString(resource.tcpPortRangeString)
|
||||||
);
|
);
|
||||||
@@ -877,7 +880,9 @@ export function InternalResourceForm({
|
|||||||
field.value ??
|
field.value ??
|
||||||
"http"
|
"http"
|
||||||
}
|
}
|
||||||
disabled={httpSectionDisabled}
|
disabled={
|
||||||
|
httpSectionDisabled
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger className="w-full">
|
||||||
@@ -918,7 +923,10 @@ export function InternalResourceForm({
|
|||||||
<Input
|
<Input
|
||||||
{...field}
|
{...field}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
disabled={isHttpMode && httpSectionDisabled}
|
disabled={
|
||||||
|
isHttpMode &&
|
||||||
|
httpSectionDisabled
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -974,7 +982,9 @@ export function InternalResourceForm({
|
|||||||
field.value ??
|
field.value ??
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
disabled={httpSectionDisabled}
|
disabled={
|
||||||
|
httpSectionDisabled
|
||||||
|
}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const raw =
|
const raw =
|
||||||
e.target
|
e.target
|
||||||
@@ -1009,7 +1019,9 @@ export function InternalResourceForm({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isHttpMode && (
|
{isHttpMode && (
|
||||||
<PaidFeaturesAlert tiers={tierMatrix.httpPrivateResources} />
|
<PaidFeaturesAlert
|
||||||
|
tiers={tierMatrix.httpPrivateResources}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isHttpMode ? (
|
{isHttpMode ? (
|
||||||
@@ -1022,55 +1034,61 @@ export function InternalResourceForm({
|
|||||||
{t(httpConfigurationDescriptionKey)}
|
{t(httpConfigurationDescriptionKey)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={httpSectionDisabled ? "pointer-events-none opacity-50" : undefined}>
|
<div
|
||||||
<DomainPicker
|
className={
|
||||||
key={
|
httpSectionDisabled
|
||||||
variant === "edit" && siteResourceId
|
? "pointer-events-none opacity-50"
|
||||||
? `http-domain-${siteResourceId}`
|
: undefined
|
||||||
: "http-domain-create"
|
|
||||||
}
|
}
|
||||||
orgId={orgId}
|
>
|
||||||
cols={2}
|
<DomainPicker
|
||||||
hideFreeDomain
|
key={
|
||||||
defaultSubdomain={
|
variant === "edit" && siteResourceId
|
||||||
httpConfigSubdomain ?? undefined
|
? `http-domain-${siteResourceId}`
|
||||||
}
|
: "http-domain-create"
|
||||||
defaultDomainId={
|
}
|
||||||
httpConfigDomainId ?? undefined
|
orgId={orgId}
|
||||||
}
|
cols={2}
|
||||||
defaultFullDomain={
|
hideFreeDomain
|
||||||
httpConfigFullDomain ?? undefined
|
defaultSubdomain={
|
||||||
}
|
httpConfigSubdomain ?? undefined
|
||||||
onDomainChange={(res) => {
|
}
|
||||||
if (res === null) {
|
defaultDomainId={
|
||||||
|
httpConfigDomainId ?? undefined
|
||||||
|
}
|
||||||
|
defaultFullDomain={
|
||||||
|
httpConfigFullDomain ?? undefined
|
||||||
|
}
|
||||||
|
onDomainChange={(res) => {
|
||||||
|
if (res === null) {
|
||||||
|
form.setValue(
|
||||||
|
"httpConfigSubdomain",
|
||||||
|
null
|
||||||
|
);
|
||||||
|
form.setValue(
|
||||||
|
"httpConfigDomainId",
|
||||||
|
null
|
||||||
|
);
|
||||||
|
form.setValue(
|
||||||
|
"httpConfigFullDomain",
|
||||||
|
null
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
form.setValue(
|
form.setValue(
|
||||||
"httpConfigSubdomain",
|
"httpConfigSubdomain",
|
||||||
null
|
res.subdomain ?? null
|
||||||
);
|
);
|
||||||
form.setValue(
|
form.setValue(
|
||||||
"httpConfigDomainId",
|
"httpConfigDomainId",
|
||||||
null
|
res.domainId
|
||||||
);
|
);
|
||||||
form.setValue(
|
form.setValue(
|
||||||
"httpConfigFullDomain",
|
"httpConfigFullDomain",
|
||||||
null
|
res.fullDomain
|
||||||
);
|
);
|
||||||
return;
|
}}
|
||||||
}
|
/>
|
||||||
form.setValue(
|
|
||||||
"httpConfigSubdomain",
|
|
||||||
res.subdomain ?? null
|
|
||||||
);
|
|
||||||
form.setValue(
|
|
||||||
"httpConfigDomainId",
|
|
||||||
res.domainId
|
|
||||||
);
|
|
||||||
form.setValue(
|
|
||||||
"httpConfigFullDomain",
|
|
||||||
res.fullDomain
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -1088,7 +1106,9 @@ export function InternalResourceForm({
|
|||||||
onCheckedChange={
|
onCheckedChange={
|
||||||
field.onChange
|
field.onChange
|
||||||
}
|
}
|
||||||
disabled={httpSectionDisabled}
|
disabled={
|
||||||
|
httpSectionDisabled
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -1511,7 +1531,7 @@ export function InternalResourceForm({
|
|||||||
role="combobox"
|
role="combobox"
|
||||||
className={cn(
|
className={cn(
|
||||||
"justify-between w-full",
|
"justify-between w-full",
|
||||||
"text-muted-foreground pl-1.5"
|
"text-muted-foreground pl-1.5 cursor-text"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -1548,7 +1568,7 @@ export function InternalResourceForm({
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<ChevronDownIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useMemo, useState } from "react";
|
|||||||
import { useDebounce } from "use-debounce";
|
import { useDebounce } from "use-debounce";
|
||||||
|
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { MultiSelectTags } from "./multi-select-tags";
|
import { MultiSelectTags } from "./multi-select/multi-select-tags";
|
||||||
|
|
||||||
export type SelectedMachine = Pick<
|
export type SelectedMachine = Pick<
|
||||||
ListClientsResponse["clients"][number],
|
ListClientsResponse["clients"][number],
|
||||||
|
|||||||
61
src/components/multi-select/multi-select-input.tsx
Normal file
61
src/components/multi-select/multi-select-input.tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger
|
||||||
|
} from "@app/components/ui/popover";
|
||||||
|
import { Button } from "@app/components/ui/button";
|
||||||
|
import { cn } from "@app/lib/cn";
|
||||||
|
import { ChevronDownIcon } from "lucide-react";
|
||||||
|
import {
|
||||||
|
type TagValue,
|
||||||
|
type MultiSelectTagsProps,
|
||||||
|
MultiSelectTags
|
||||||
|
} from "./multi-select-tags";
|
||||||
|
|
||||||
|
export interface MultiSelectInputProps<
|
||||||
|
T extends TagValue
|
||||||
|
> extends MultiSelectTagsProps<T> {
|
||||||
|
buttonText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MultiSelectInput<T extends TagValue>({
|
||||||
|
buttonText,
|
||||||
|
...props
|
||||||
|
}: MultiSelectInputProps<T>) {
|
||||||
|
return (
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"justify-between w-full",
|
||||||
|
"text-muted-foreground pl-1.5 cursor-text"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center gap-1",
|
||||||
|
"overflow-x-auto"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* {(field.value ?? []).map((client) => (
|
||||||
|
<span
|
||||||
|
key={client.clientId}
|
||||||
|
className={cn(
|
||||||
|
"bg-muted-foreground/20 font-normal text-foreground rounded-sm",
|
||||||
|
"py-1 px-1.5 text-xs"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{client.name}
|
||||||
|
</span>
|
||||||
|
))} */}
|
||||||
|
<span className="pl-1 font-normal">{buttonText}</span>
|
||||||
|
</span>
|
||||||
|
<ChevronDownIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</div>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-0">
|
||||||
|
<MultiSelectTags {...props} />
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
CommandInput,
|
CommandInput,
|
||||||
CommandItem,
|
CommandItem,
|
||||||
CommandList
|
CommandList
|
||||||
} from "./ui/command";
|
} from "../ui/command";
|
||||||
import { cn } from "@app/lib/cn";
|
import { cn } from "@app/lib/cn";
|
||||||
import { CheckIcon } from "lucide-react";
|
import { CheckIcon } from "lucide-react";
|
||||||
|
|
||||||
Reference in New Issue
Block a user