mirror of
https://github.com/fosrl/pangolin.git
synced 2025-07-31 16:14:46 +02:00
Update create client form
This commit is contained in:
parent
bcd80e19d4
commit
875fa215c5
5 changed files with 222 additions and 72 deletions
73
package-lock.json
generated
73
package-lock.json
generated
|
@ -22,6 +22,7 @@
|
||||||
"@radix-ui/react-label": "2.1.1",
|
"@radix-ui/react-label": "2.1.1",
|
||||||
"@radix-ui/react-popover": "1.1.4",
|
"@radix-ui/react-popover": "1.1.4",
|
||||||
"@radix-ui/react-radio-group": "1.2.2",
|
"@radix-ui/react-radio-group": "1.2.2",
|
||||||
|
"@radix-ui/react-scroll-area": "1.2.3",
|
||||||
"@radix-ui/react-select": "2.1.4",
|
"@radix-ui/react-select": "2.1.4",
|
||||||
"@radix-ui/react-separator": "1.1.1",
|
"@radix-ui/react-separator": "1.1.1",
|
||||||
"@radix-ui/react-slot": "1.1.1",
|
"@radix-ui/react-slot": "1.1.1",
|
||||||
|
@ -3536,6 +3537,78 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-scroll-area": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-l7+NNBfBYYJa9tNqVcP2AGvxdE3lmE6kFTBXdvHgUaZuy+4wGCL1Cl2AfaR7RKyimj7lZURGLwFO59k4eBnDJQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/number": "1.1.0",
|
||||||
|
"@radix-ui/primitive": "1.1.1",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1",
|
||||||
|
"@radix-ui/react-context": "1.1.1",
|
||||||
|
"@radix-ui/react-direction": "1.1.0",
|
||||||
|
"@radix-ui/react-presence": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.0.2",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-primitive": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "1.1.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-slot": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-select": {
|
"node_modules/@radix-ui/react-select": {
|
||||||
"version": "2.1.4",
|
"version": "2.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.4.tgz",
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
"@radix-ui/react-label": "2.1.1",
|
"@radix-ui/react-label": "2.1.1",
|
||||||
"@radix-ui/react-popover": "1.1.4",
|
"@radix-ui/react-popover": "1.1.4",
|
||||||
"@radix-ui/react-radio-group": "1.2.2",
|
"@radix-ui/react-radio-group": "1.2.2",
|
||||||
|
"@radix-ui/react-scroll-area": "1.2.3",
|
||||||
"@radix-ui/react-select": "2.1.4",
|
"@radix-ui/react-select": "2.1.4",
|
||||||
"@radix-ui/react-separator": "1.1.1",
|
"@radix-ui/react-separator": "1.1.1",
|
||||||
"@radix-ui/react-slot": "1.1.1",
|
"@radix-ui/react-slot": "1.1.1",
|
||||||
|
|
|
@ -21,7 +21,6 @@ import { formatAxiosError } from "@app/lib/api";
|
||||||
import { createApiClient } from "@app/lib/api";
|
import { createApiClient } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { Collapsible } from "@app/components/ui/collapsible";
|
|
||||||
import { ClientRow } from "./ClientsTable";
|
import { ClientRow } from "./ClientsTable";
|
||||||
import {
|
import {
|
||||||
CreateClientBody,
|
CreateClientBody,
|
||||||
|
@ -45,6 +44,9 @@ import {
|
||||||
CommandItem,
|
CommandItem,
|
||||||
CommandList
|
CommandList
|
||||||
} from "@app/components/ui/command";
|
} from "@app/components/ui/command";
|
||||||
|
import { ScrollArea } from "@app/components/ui/scroll-area";
|
||||||
|
import { Badge } from "@app/components/ui/badge";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
|
||||||
const createClientFormSchema = z.object({
|
const createClientFormSchema = z.object({
|
||||||
name: z
|
name: z
|
||||||
|
@ -55,16 +57,19 @@ const createClientFormSchema = z.object({
|
||||||
.max(30, {
|
.max(30, {
|
||||||
message: "Name must not be longer than 30 characters."
|
message: "Name must not be longer than 30 characters."
|
||||||
}),
|
}),
|
||||||
siteId: z.coerce.number()
|
siteIds: z.array(z.number()).min(1, {
|
||||||
|
message: "Select at least one site."
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
type CreateSiteFormValues = z.infer<typeof createClientFormSchema>;
|
type CreateClientFormValues = z.infer<typeof createClientFormSchema>;
|
||||||
|
|
||||||
const defaultValues: Partial<CreateSiteFormValues> = {
|
const defaultValues: Partial<CreateClientFormValues> = {
|
||||||
name: ""
|
name: "",
|
||||||
|
siteIds: []
|
||||||
};
|
};
|
||||||
|
|
||||||
type CreateSiteFormProps = {
|
type CreateClientFormProps = {
|
||||||
onCreate?: (client: ClientRow) => void;
|
onCreate?: (client: ClientRow) => void;
|
||||||
setLoading?: (loading: boolean) => void;
|
setLoading?: (loading: boolean) => void;
|
||||||
setChecked?: (checked: boolean) => void;
|
setChecked?: (checked: boolean) => void;
|
||||||
|
@ -76,7 +81,7 @@ export default function CreateClientForm({
|
||||||
setLoading,
|
setLoading,
|
||||||
setChecked,
|
setChecked,
|
||||||
orgId
|
orgId
|
||||||
}: CreateSiteFormProps) {
|
}: CreateClientFormProps) {
|
||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
const { env } = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
|
|
||||||
|
@ -87,6 +92,7 @@ export default function CreateClientForm({
|
||||||
const [clientDefaults, setClientDefaults] =
|
const [clientDefaults, setClientDefaults] =
|
||||||
useState<PickClientDefaultsResponse | null>(null);
|
useState<PickClientDefaultsResponse | null>(null);
|
||||||
const [olmCommand, setOlmCommand] = useState<string | null>(null);
|
const [olmCommand, setOlmCommand] = useState<string | null>(null);
|
||||||
|
const [selectedSites, setSelectedSites] = useState<Array<{id: number, name: string}>>([]);
|
||||||
|
|
||||||
const handleCheckboxChange = (checked: boolean) => {
|
const handleCheckboxChange = (checked: boolean) => {
|
||||||
setIsChecked(checked);
|
setIsChecked(checked);
|
||||||
|
@ -95,11 +101,16 @@ export default function CreateClientForm({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const form = useForm<CreateSiteFormValues>({
|
const form = useForm<CreateClientFormValues>({
|
||||||
resolver: zodResolver(createClientFormSchema),
|
resolver: zodResolver(createClientFormSchema),
|
||||||
defaultValues
|
defaultValues
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Update form value when selectedSites changes
|
||||||
|
form.setValue('siteIds', selectedSites.map(site => site.id));
|
||||||
|
}, [selectedSites, form]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
|
|
||||||
|
@ -109,6 +120,7 @@ export default function CreateClientForm({
|
||||||
form.reset();
|
form.reset();
|
||||||
setChecked?.(false);
|
setChecked?.(false);
|
||||||
setClientDefaults(null);
|
setClientDefaults(null);
|
||||||
|
setSelectedSites([]);
|
||||||
|
|
||||||
const fetchSites = async () => {
|
const fetchSites = async () => {
|
||||||
const res = await api.get<AxiosResponse<ListSitesResponse>>(
|
const res = await api.get<AxiosResponse<ListSitesResponse>>(
|
||||||
|
@ -118,25 +130,19 @@ export default function CreateClientForm({
|
||||||
(s) => s.type === "newt" && s.subnet
|
(s) => s.type === "newt" && s.subnet
|
||||||
);
|
);
|
||||||
setSites(sites);
|
setSites(sites);
|
||||||
|
|
||||||
if (sites.length > 0) {
|
|
||||||
form.setValue("siteId", sites[0].siteId);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchSites();
|
fetchSites();
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const siteId = form.getValues("siteId");
|
if (selectedSites.length === 0) return;
|
||||||
|
|
||||||
if (siteId === undefined || siteId === null) return;
|
api.get(`/pick-client-defaults`)
|
||||||
|
|
||||||
api.get(`/site/${siteId}/pick-client-defaults`)
|
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: `Error fetching client defaults for site ${siteId}`,
|
title: `Error fetching client defaults`,
|
||||||
description: formatAxiosError(e)
|
description: formatAxiosError(e)
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
@ -148,17 +154,27 @@ export default function CreateClientForm({
|
||||||
setOlmCommand(olmConfig);
|
setOlmCommand(olmConfig);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [form.watch("siteId")]);
|
}, [selectedSites]);
|
||||||
|
|
||||||
async function onSubmit(data: CreateSiteFormValues) {
|
const addSite = (siteId: number, siteName: string) => {
|
||||||
|
if (!selectedSites.some(site => site.id === siteId)) {
|
||||||
|
setSelectedSites([...selectedSites, { id: siteId, name: siteName }]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeSite = (siteId: number) => {
|
||||||
|
setSelectedSites(selectedSites.filter(site => site.id !== siteId));
|
||||||
|
};
|
||||||
|
|
||||||
|
async function onSubmit(data: CreateClientFormValues) {
|
||||||
setLoading?.(true);
|
setLoading?.(true);
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
if (!clientDefaults) {
|
if (!clientDefaults) {
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: "Error creating site",
|
title: "Error creating client",
|
||||||
description: "Site defaults not found"
|
description: "Client defaults not found"
|
||||||
});
|
});
|
||||||
setLoading?.(false);
|
setLoading?.(false);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
@ -167,17 +183,17 @@ export default function CreateClientForm({
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
siteId: data.siteId,
|
siteIds: data.siteIds,
|
||||||
subnet: clientDefaults.subnet,
|
|
||||||
olmId: clientDefaults.olmId,
|
olmId: clientDefaults.olmId,
|
||||||
secret: clientDefaults.olmSecret,
|
secret: clientDefaults.olmSecret,
|
||||||
type: "olm"
|
type: "olm"
|
||||||
} as CreateClientBody;
|
} as CreateClientBody;
|
||||||
|
|
||||||
const res = await api
|
const res = await api
|
||||||
.put<
|
.put<AxiosResponse<CreateClientResponse>>(
|
||||||
AxiosResponse<CreateClientResponse>
|
`/org/${orgId}/client`,
|
||||||
>(`/site/${data.siteId}/client`, payload)
|
payload
|
||||||
|
)
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
|
@ -189,12 +205,14 @@ export default function CreateClientForm({
|
||||||
if (res && res.status === 201) {
|
if (res && res.status === 201) {
|
||||||
const data = res.data.data;
|
const data = res.data.data;
|
||||||
|
|
||||||
const site = sites.find((site) => site.siteId === data.siteId);
|
// For now we'll just use the first site for display purposes
|
||||||
|
// The actual client will be associated with all selected sites
|
||||||
|
const firstSite = sites.find((site) => site.siteId === selectedSites[0]?.id);
|
||||||
|
|
||||||
onCreate?.({
|
onCreate?.({
|
||||||
name: data.name,
|
name: data.name,
|
||||||
siteId: site!.niceId,
|
siteId: firstSite?.niceId || "",
|
||||||
siteName: site!.name,
|
siteName: firstSite?.name || "",
|
||||||
id: data.clientId,
|
id: data.clientId,
|
||||||
mbIn: "0 MB",
|
mbIn: "0 MB",
|
||||||
mbOut: "0 MB",
|
mbOut: "0 MB",
|
||||||
|
@ -213,7 +231,7 @@ export default function CreateClientForm({
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
id="create-site-form"
|
id="create-client-form"
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
|
@ -235,10 +253,10 @@ export default function CreateClientForm({
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="siteId"
|
name="siteIds"
|
||||||
render={({ field }) => (
|
render={() => (
|
||||||
<FormItem className="flex flex-col">
|
<FormItem className="flex flex-col">
|
||||||
<FormLabel>Site</FormLabel>
|
<FormLabel>Sites</FormLabel>
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
|
@ -247,61 +265,71 @@ export default function CreateClientForm({
|
||||||
role="combobox"
|
role="combobox"
|
||||||
className={cn(
|
className={cn(
|
||||||
"justify-between",
|
"justify-between",
|
||||||
!field.value &&
|
selectedSites.length === 0 &&
|
||||||
"text-muted-foreground"
|
"text-muted-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{field.value
|
{selectedSites.length > 0
|
||||||
? sites.find(
|
? `${selectedSites.length} site${selectedSites.length !== 1 ? 's' : ''} selected`
|
||||||
(site) =>
|
: "Select sites"}
|
||||||
site.siteId ===
|
|
||||||
field.value
|
|
||||||
)?.name
|
|
||||||
: "Select site"}
|
|
||||||
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="p-0">
|
<PopoverContent className="p-0 w-[300px]">
|
||||||
<Command>
|
<Command>
|
||||||
<CommandInput placeholder="Search site..." />
|
<CommandInput placeholder="Search sites..." />
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty>
|
<CommandEmpty>
|
||||||
No site found.
|
No sites found.
|
||||||
</CommandEmpty>
|
</CommandEmpty>
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{sites.map((site) => (
|
<ScrollArea className="h-[200px]">
|
||||||
<CommandItem
|
{sites.map((site) => (
|
||||||
value={`${site.siteId}:${site.name}:${site.niceId}`}
|
<CommandItem
|
||||||
key={site.siteId}
|
value={`${site.siteId}:${site.name}:${site.niceId}`}
|
||||||
onSelect={() => {
|
key={site.siteId}
|
||||||
form.setValue(
|
onSelect={() => {
|
||||||
"siteId",
|
addSite(site.siteId, site.name);
|
||||||
site.siteId
|
}}
|
||||||
);
|
>
|
||||||
}}
|
<CheckIcon
|
||||||
>
|
className={cn(
|
||||||
<CheckIcon
|
"mr-2 h-4 w-4",
|
||||||
className={cn(
|
selectedSites.some(s => s.id === site.siteId)
|
||||||
"mr-2 h-4 w-4",
|
? "opacity-100"
|
||||||
site.siteId ===
|
: "opacity-0"
|
||||||
field.value
|
)}
|
||||||
? "opacity-100"
|
/>
|
||||||
: "opacity-0"
|
{site.name}
|
||||||
)}
|
</CommandItem>
|
||||||
/>
|
))}
|
||||||
{site.name}
|
</ScrollArea>
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</Command>
|
</Command>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
||||||
|
{selectedSites.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-2 mt-2">
|
||||||
|
{selectedSites.map(site => (
|
||||||
|
<Badge key={site.id} variant="secondary">
|
||||||
|
{site.name}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => removeSite(site.id)}
|
||||||
|
className="ml-1 rounded-full outline-none focus:ring-2 focus:ring-offset-2"
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
The client will be have connectivity to this
|
The client will have connectivity to the selected sites. The sites must be configured to accept client connections.
|
||||||
site. The site must be configured to accept
|
|
||||||
client connections.
|
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
@ -342,4 +370,4 @@ export default function CreateClientForm({
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -44,7 +44,7 @@ export default function CreateClientFormModal({
|
||||||
<CredenzaHeader>
|
<CredenzaHeader>
|
||||||
<CredenzaTitle>Create Client</CredenzaTitle>
|
<CredenzaTitle>Create Client</CredenzaTitle>
|
||||||
<CredenzaDescription>
|
<CredenzaDescription>
|
||||||
Create a new client to connect to your site
|
Create a new client to connect to your sites
|
||||||
</CredenzaDescription>
|
</CredenzaDescription>
|
||||||
</CredenzaHeader>
|
</CredenzaHeader>
|
||||||
<CredenzaBody>
|
<CredenzaBody>
|
||||||
|
|
48
src/components/ui/scroll-area.tsx
Normal file
48
src/components/ui/scroll-area.tsx
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const ScrollArea = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<ScrollAreaPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn("relative overflow-hidden", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||||
|
{children}
|
||||||
|
</ScrollAreaPrimitive.Viewport>
|
||||||
|
<ScrollBar />
|
||||||
|
<ScrollAreaPrimitive.Corner />
|
||||||
|
</ScrollAreaPrimitive.Root>
|
||||||
|
))
|
||||||
|
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
||||||
|
|
||||||
|
const ScrollBar = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
|
>(({ className, orientation = "vertical", ...props }, ref) => (
|
||||||
|
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||||
|
ref={ref}
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"flex touch-none select-none transition-colors",
|
||||||
|
orientation === "vertical" &&
|
||||||
|
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
||||||
|
orientation === "horizontal" &&
|
||||||
|
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||||
|
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
|
))
|
||||||
|
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
||||||
|
|
||||||
|
export { ScrollArea, ScrollBar }
|
Loading…
Add table
Add a link
Reference in a new issue