mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-01-26 01:21:04 +00:00
UX changes for modals and refactoring (#380)
This commit is contained in:
@@ -1,14 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import FullScreenLoading from "@components/ui/FullScreenLoading";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { useLocalStorage } from "@hooks/useLocalStorage";
|
||||
import { useRedirect } from "@hooks/useRedirect";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
type Props = {
|
||||
url: string;
|
||||
queryParams?: string;
|
||||
};
|
||||
export default function NotFound() {
|
||||
const router = useRouter();
|
||||
useEffect(() => {
|
||||
router.push("/peers");
|
||||
});
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [tempQueryParams, setTempQueryParams] = useLocalStorage(
|
||||
"netbird-query-params",
|
||||
"",
|
||||
);
|
||||
const [queryParams, setQueryParams] = useState("");
|
||||
|
||||
return <FullScreenLoading />;
|
||||
useEffect(() => {
|
||||
setQueryParams(tempQueryParams);
|
||||
setTempQueryParams("");
|
||||
setMounted(true);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return mounted ? (
|
||||
<Redirect
|
||||
url={window?.location?.pathname || "/"}
|
||||
queryParams={queryParams}
|
||||
/>
|
||||
) : (
|
||||
<FullScreenLoading />
|
||||
);
|
||||
}
|
||||
|
||||
const Redirect = ({ url, queryParams }: Props) => {
|
||||
useRedirect(url == "/" ? "/peers" : url + (queryParams && `?${queryParams}`));
|
||||
return <FullScreenLoading />;
|
||||
};
|
||||
|
||||
@@ -16,6 +16,8 @@ export default function CircleIcon({
|
||||
return (
|
||||
<span
|
||||
style={{ width: size + "px", height: size + "px" }}
|
||||
data-cy="circle-icon"
|
||||
data-cy-status={active ? "active" : "inactive"}
|
||||
className={cn(
|
||||
"rounded-full",
|
||||
active
|
||||
|
||||
@@ -5,14 +5,16 @@ import React, { forwardRef } from "react";
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function ButtonGroup({ children, disabled }: Props) {
|
||||
function ButtonGroup({ children, disabled, className }: Props) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-lg border-[1px] dark:border-nb-gray-900 border-neutral-200 overflow-hidden flex items-center justify-center shrink-0 border-separate",
|
||||
disabled ? "opacity-100 !border-nb-gray-900/20" : "",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
@@ -21,7 +23,10 @@ function ButtonGroup({ children, disabled }: Props) {
|
||||
}
|
||||
|
||||
const ButtonGroupButton = forwardRef(
|
||||
({ ...props }: ButtonProps, ref: React.ForwardedRef<HTMLButtonElement>) => {
|
||||
(
|
||||
{ className, ...props }: ButtonProps,
|
||||
ref: React.ForwardedRef<HTMLButtonElement>,
|
||||
) => {
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
@@ -31,6 +36,7 @@ const ButtonGroupButton = forwardRef(
|
||||
className={cn(
|
||||
"first:border-l-0 last:border-r-0 border-t-0 border-b-0 h-[41px]",
|
||||
"!py-2.5 !px-4",
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -198,6 +198,7 @@ export function PeerGroupSelector({
|
||||
<CommandList className={"w-full"}>
|
||||
<div className={"relative"}>
|
||||
<CommandInput
|
||||
data-cy={"group-search-input"}
|
||||
className={cn(
|
||||
"min-h-[42px] w-full relative",
|
||||
"border-b-0 border-t-0 border-r-0 border-l-0 border-neutral-200 dark:border-nb-gray-700 items-center",
|
||||
|
||||
@@ -21,12 +21,19 @@ function SegmentedTabs({ value, onChange, children }: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
function List({ children }: { children: React.ReactNode }) {
|
||||
function List({
|
||||
children,
|
||||
className = "",
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<TabsList
|
||||
className={
|
||||
"bg-nb-gray-930/70 p-1.5 rounded-t-lg flex justify-center gap-1 border border-b-0 border-nb-gray-900"
|
||||
}
|
||||
className={cn(
|
||||
"bg-nb-gray-930/70 p-1.5 rounded-t-lg flex justify-center gap-1 border border-b-0 border-nb-gray-900",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</TabsList>
|
||||
|
||||
@@ -75,7 +75,10 @@ const ModalContent = React.forwardRef<
|
||||
<>
|
||||
{children}
|
||||
{showClose && (
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-white transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-neutral-950 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-neutral-100 data-[state=open]:text-neutral-500 dark:ring-offset-neutral-950 dark:focus:ring-neutral-300 dark:data-[state=open]:bg-neutral-800 dark:data-[state=open]:text-neutral-400">
|
||||
<DialogPrimitive.Close
|
||||
data-cy={"modal-close"}
|
||||
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-white transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-neutral-950 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-neutral-100 data-[state=open]:text-neutral-500 dark:ring-offset-neutral-950 dark:focus:ring-neutral-300 dark:data-[state=open]:bg-neutral-800 dark:data-[state=open]:text-neutral-400"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
|
||||
@@ -28,9 +28,10 @@ export function DataTableRowsPerPage<TData>({
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
disabled={disabled}
|
||||
data-cy={"rows-per-page"}
|
||||
className="w-[200px] justify-between"
|
||||
>
|
||||
<RowsIcon size={15} className={"text-nb-gray-300"} />
|
||||
<RowsIcon size={15} className={"text-nb-gray-300 shrink-0"} />
|
||||
<div>
|
||||
<span className={"text-white"}>
|
||||
{table.getState().pagination.pageSize}
|
||||
@@ -47,6 +48,7 @@ export function DataTableRowsPerPage<TData>({
|
||||
<CommandItem
|
||||
key={val}
|
||||
value={val.toString()}
|
||||
data-cy={`rows-per-page-value`}
|
||||
onSelect={(currentValue) => {
|
||||
table.setPageSize(Number(currentValue));
|
||||
setOpen(false);
|
||||
|
||||
@@ -94,6 +94,7 @@ export default function DialogProvider({ children }: Props) {
|
||||
className={"w-full"}
|
||||
size={"sm"}
|
||||
tabIndex={-1}
|
||||
data-cy={"confirmation.cancel"}
|
||||
onClick={() => fn.current && fn.current(false)}
|
||||
>
|
||||
{dialogOptions.cancelText || "Cancel"}
|
||||
@@ -109,6 +110,7 @@ export default function DialogProvider({ children }: Props) {
|
||||
}
|
||||
className={"w-full"}
|
||||
size={"sm"}
|
||||
data-cy={"confirmation.confirm"}
|
||||
onClick={() => fn.current && fn.current(true)}
|
||||
>
|
||||
{dialogOptions.confirmText || "Confirm"}
|
||||
|
||||
@@ -77,9 +77,7 @@ export default function PeerProvider({ children, peer }: Props) {
|
||||
? loginExpiration
|
||||
: peer.login_expiration_enabled,
|
||||
approval_required:
|
||||
approval_required != undefined
|
||||
? approval_required
|
||||
: peer.approval_required,
|
||||
approval_required == undefined ? undefined : approval_required,
|
||||
},
|
||||
`/${peer.id}`,
|
||||
);
|
||||
|
||||
@@ -239,12 +239,6 @@ export function AccessControlModalContent({
|
||||
|
||||
const portAndDirectionDisabled = protocol == "icmp" || protocol == "all";
|
||||
|
||||
const buttonDisabled = useMemo(() => {
|
||||
if (sourceGroups.length == 0 || destinationGroups.length == 0) return true;
|
||||
if (name.length == 0) return true;
|
||||
if (direction != "bi" && ports.length == 0) return true;
|
||||
}, [sourceGroups, destinationGroups, direction, ports, name]);
|
||||
|
||||
const [postureChecks, setPostureChecks] = useState<PostureCheck[]>([]);
|
||||
const postureChecksLoaded = useRef(false);
|
||||
|
||||
@@ -268,6 +262,16 @@ export function AccessControlModalContent({
|
||||
}
|
||||
}, [initialPostureChecks]);
|
||||
|
||||
const continuePostureChecksDisabled = useMemo(() => {
|
||||
if (sourceGroups.length == 0 || destinationGroups.length == 0) return true;
|
||||
if (direction != "bi" && ports.length == 0) return true;
|
||||
}, [sourceGroups, destinationGroups, direction, ports]);
|
||||
|
||||
const submitDisabled = useMemo(() => {
|
||||
if (name.length == 0) return true;
|
||||
if (continuePostureChecksDisabled) return true;
|
||||
}, [name, continuePostureChecksDisabled]);
|
||||
|
||||
return (
|
||||
<ModalContent maxWidthClass={"max-w-2xl"}>
|
||||
<ModalHeader
|
||||
@@ -283,14 +287,17 @@ export function AccessControlModalContent({
|
||||
color={"netbird"}
|
||||
/>
|
||||
|
||||
<Tabs defaultValue={tab} onValueChange={(v) => setTab(v)}>
|
||||
<Tabs defaultValue={tab} onValueChange={(v) => setTab(v)} value={tab}>
|
||||
<TabsList justify={"start"} className={"px-8"}>
|
||||
<TabsTrigger value={"policy"}>
|
||||
<ArrowRightLeft size={16} />
|
||||
Policy
|
||||
</TabsTrigger>
|
||||
<PostureCheckTabTrigger />
|
||||
<TabsTrigger value={"general"}>
|
||||
<PostureCheckTabTrigger disabled={continuePostureChecksDisabled} />
|
||||
<TabsTrigger
|
||||
value={"general"}
|
||||
disabled={continuePostureChecksDisabled}
|
||||
>
|
||||
<Text
|
||||
size={16}
|
||||
className={
|
||||
@@ -456,24 +463,74 @@ export function AccessControlModalContent({
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div className={"flex gap-3 w-full justify-end"}>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
</ModalClose>
|
||||
{!policy ? (
|
||||
<>
|
||||
{tab == "policy" && (
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
</ModalClose>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant={"primary"}
|
||||
disabled={buttonDisabled}
|
||||
onClick={submit}
|
||||
>
|
||||
{policy ? (
|
||||
<>Save Changes</>
|
||||
) : (
|
||||
<>
|
||||
<PlusCircle size={16} />
|
||||
Add Policy
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
{tab == "posture_checks" && (
|
||||
<Button variant={"secondary"} onClick={() => setTab("policy")}>
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{tab == "policy" && (
|
||||
<Button
|
||||
variant={"primary"}
|
||||
onClick={() => setTab("posture_checks")}
|
||||
disabled={continuePostureChecksDisabled}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{tab == "posture_checks" && (
|
||||
<Button
|
||||
variant={"primary"}
|
||||
onClick={() => setTab("general")}
|
||||
disabled={continuePostureChecksDisabled}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{tab == "general" && (
|
||||
<>
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
onClick={() => setTab("posture_checks")}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant={"primary"}
|
||||
disabled={submitDisabled}
|
||||
onClick={submit}
|
||||
>
|
||||
<PlusCircle size={16} />
|
||||
Add Policy
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
</ModalClose>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
disabled={submitDisabled}
|
||||
onClick={submit}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import Badge from "@components/Badge";
|
||||
import Button from "@components/Button";
|
||||
import {
|
||||
Modal,
|
||||
@@ -10,6 +11,7 @@ import ModalHeader from "@components/modal/ModalHeader";
|
||||
import { PeerGroupSelector } from "@components/PeerGroupSelector";
|
||||
import Separator from "@components/Separator";
|
||||
import MultipleGroups from "@components/ui/MultipleGroups";
|
||||
import { IconCirclePlus } from "@tabler/icons-react";
|
||||
import { FolderGit2 } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useMemo } from "react";
|
||||
@@ -27,6 +29,7 @@ type Props = {
|
||||
label?: string;
|
||||
description?: string;
|
||||
peer?: Peer;
|
||||
showAddGroupButton?: boolean;
|
||||
};
|
||||
|
||||
export default function GroupsRow({
|
||||
@@ -37,6 +40,7 @@ export default function GroupsRow({
|
||||
label = "Assigned Groups",
|
||||
description = "Use groups to control what this peer can access",
|
||||
peer,
|
||||
showAddGroupButton = false,
|
||||
}: Props) {
|
||||
const { groups: allGroups } = useGroups();
|
||||
const { isUser } = useLoggedInUser();
|
||||
@@ -59,7 +63,14 @@ export default function GroupsRow({
|
||||
setModal && !isUser && setModal(true);
|
||||
}}
|
||||
>
|
||||
<MultipleGroups groups={foundGroups} label={label} />
|
||||
{foundGroups?.length == 0 && showAddGroupButton ? (
|
||||
<Badge variant={"gray"} useHover={true}>
|
||||
<IconCirclePlus size={14} />
|
||||
Add Groups
|
||||
</Badge>
|
||||
) : (
|
||||
<MultipleGroups groups={foundGroups} label={label} />
|
||||
)}
|
||||
</ModalTrigger>
|
||||
<EditGroupsModal
|
||||
groups={foundGroups}
|
||||
|
||||
@@ -238,27 +238,22 @@ export function NameserverModalContent({
|
||||
return "";
|
||||
}, [name]);
|
||||
|
||||
const hasAnyError = useMemo(() => {
|
||||
return (
|
||||
const canContinueToDomains = useMemo(() => {
|
||||
return !(
|
||||
hasNSErrors ||
|
||||
nsError ||
|
||||
domainError ||
|
||||
nameservers.length == 0 ||
|
||||
hasDomainErrors ||
|
||||
groups.length == 0 ||
|
||||
nameLengthError !== "" ||
|
||||
name == ""
|
||||
groups.length == 0
|
||||
);
|
||||
}, [
|
||||
nsError,
|
||||
domainError,
|
||||
nameservers,
|
||||
groups,
|
||||
hasNSErrors,
|
||||
hasDomainErrors,
|
||||
nameLengthError,
|
||||
name,
|
||||
]);
|
||||
}, [hasNSErrors, nsError, nameservers.length, groups.length]);
|
||||
|
||||
const canContinueToGeneral = useMemo(() => {
|
||||
return !(!canContinueToDomains || domainError || hasDomainErrors);
|
||||
}, [canContinueToDomains, domainError, hasDomainErrors]);
|
||||
|
||||
const canSubmit = useMemo(() => {
|
||||
return !(!canContinueToGeneral || nameLengthError !== "" || name == "");
|
||||
}, [canContinueToGeneral, nameLengthError, name]);
|
||||
|
||||
return (
|
||||
<ModalContent maxWidthClass={"max-w-xl"}>
|
||||
@@ -269,7 +264,7 @@ export function NameserverModalContent({
|
||||
color={"netbird"}
|
||||
/>
|
||||
|
||||
<Tabs defaultValue={tab} onValueChange={(v) => setTab(v)}>
|
||||
<Tabs defaultValue={tab} onValueChange={(v) => setTab(v)} value={tab}>
|
||||
<TabsList justify={"start"} className={"px-8"}>
|
||||
<TabsTrigger value={"nameserver"}>
|
||||
<ServerIcon
|
||||
@@ -280,7 +275,7 @@ export function NameserverModalContent({
|
||||
/>
|
||||
Nameserver
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value={"domains"}>
|
||||
<TabsTrigger value={"domains"} disabled={!canContinueToDomains}>
|
||||
<GlobeIcon
|
||||
size={16}
|
||||
className={
|
||||
@@ -289,7 +284,7 @@ export function NameserverModalContent({
|
||||
/>
|
||||
Domains
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value={"general"}>
|
||||
<TabsTrigger value={"general"} disabled={!canContinueToGeneral}>
|
||||
<Text
|
||||
size={16}
|
||||
className={
|
||||
@@ -473,20 +468,77 @@ export function NameserverModalContent({
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div className={"flex gap-3 w-full justify-end"}>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
</ModalClose>
|
||||
{!isUpdate ? (
|
||||
<>
|
||||
{tab == "nameserver" && (
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
</ModalClose>
|
||||
)}
|
||||
|
||||
<Button variant={"primary"} disabled={hasAnyError} onClick={submit}>
|
||||
{isUpdate ? (
|
||||
<>Save Changes</>
|
||||
) : (
|
||||
<>
|
||||
<PlusCircle size={16} />
|
||||
Add Nameserver
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
{tab == "domains" && (
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
onClick={() => setTab("nameserver")}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{tab == "nameserver" && (
|
||||
<Button
|
||||
variant={"primary"}
|
||||
onClick={() => setTab("domains")}
|
||||
disabled={!canContinueToDomains}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{tab == "domains" && (
|
||||
<Button
|
||||
variant={"primary"}
|
||||
onClick={() => setTab("general")}
|
||||
disabled={!canContinueToGeneral}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{tab == "general" && (
|
||||
<>
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
onClick={() => setTab("domains")}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant={"primary"}
|
||||
disabled={!canSubmit}
|
||||
onClick={submit}
|
||||
>
|
||||
<PlusCircle size={16} />
|
||||
Add Nameserver
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
</ModalClose>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
disabled={!canSubmit}
|
||||
onClick={submit}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@@ -13,8 +13,7 @@ export const useHasExitNodes = (peer?: Peer) => {
|
||||
);
|
||||
return peer
|
||||
? routes?.some(
|
||||
(route) =>
|
||||
route?.peer === peer.id && route?.network.includes("0.0.0.0"),
|
||||
(route) => route?.peer === peer.id && route?.network === "0.0.0.0/0",
|
||||
) || false
|
||||
: false;
|
||||
};
|
||||
|
||||
@@ -163,7 +163,10 @@ export default function PostureCheckModal({
|
||||
Checks
|
||||
</TabsTrigger>
|
||||
|
||||
<TabsTrigger value={"general"}>
|
||||
<TabsTrigger
|
||||
value={"general"}
|
||||
disabled={!isAtLeastOneCheckEnabled}
|
||||
>
|
||||
<Text
|
||||
size={16}
|
||||
className={
|
||||
@@ -243,12 +246,23 @@ export default function PostureCheckModal({
|
||||
</div>
|
||||
<div className={"flex gap-3 w-full justify-end"}>
|
||||
<>
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
{tab == "checks" && (
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{tab == "general" && (
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
onClick={() => setTab("checks")}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{!postureCheck && tab == "checks" && (
|
||||
<Button
|
||||
|
||||
@@ -2,9 +2,13 @@ import { TabsTrigger } from "@components/Tabs";
|
||||
import { ShieldCheck } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
export const PostureCheckTabTrigger = () => {
|
||||
type Props = {
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const PostureCheckTabTrigger = ({ disabled = false }: Props) => {
|
||||
return (
|
||||
<TabsTrigger value={"posture_checks"}>
|
||||
<TabsTrigger value={"posture_checks"} disabled={disabled}>
|
||||
<ShieldCheck size={16} />
|
||||
Posture Checks
|
||||
</TabsTrigger>
|
||||
|
||||
37
src/modules/setup-keys/SetupKeyEphemeralCell.tsx
Normal file
37
src/modules/setup-keys/SetupKeyEphemeralCell.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import Badge from "@components/Badge";
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { HelpCircle } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import EmptyRow from "@/modules/common-table-rows/EmptyRow";
|
||||
|
||||
type Props = {
|
||||
ephemeral: boolean;
|
||||
};
|
||||
export default function SetupKeyEphemeralCell({ ephemeral }: Props) {
|
||||
return ephemeral ? (
|
||||
<FullTooltip
|
||||
interactive={false}
|
||||
content={
|
||||
<div className={"max-w-xs text-xs"}>
|
||||
Peers that are offline for over 10 minutes will be removed
|
||||
automatically.
|
||||
</div>
|
||||
}
|
||||
disabled={!ephemeral}
|
||||
>
|
||||
<Badge variant={"gray"}>
|
||||
<span
|
||||
className={cn(
|
||||
"h-2 w-2 rounded-full mr-0.5",
|
||||
ephemeral ? "bg-yellow-500" : "bg-nb-gray-400",
|
||||
)}
|
||||
></span>
|
||||
Ephemeral
|
||||
<HelpCircle size={12} />
|
||||
</Badge>
|
||||
</FullTooltip>
|
||||
) : (
|
||||
<EmptyRow />
|
||||
);
|
||||
}
|
||||
@@ -46,6 +46,7 @@ export default function SetupKeyGroupsCell({ setupKey }: Props) {
|
||||
}
|
||||
groups={setupKey.auto_groups || []}
|
||||
onSave={handleSave}
|
||||
showAddGroupButton={true}
|
||||
modal={modal}
|
||||
setModal={setModal}
|
||||
/>
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import Badge from "@components/Badge";
|
||||
import React from "react";
|
||||
|
||||
type Props = {
|
||||
text: string;
|
||||
};
|
||||
|
||||
export default function SetupKeyKeyCell({ text }: Props) {
|
||||
return (
|
||||
<div className={"flex"}>
|
||||
<Badge variant={"gray"} className={"text-xs font-mono"}>
|
||||
{text.substring(0, 5) + "****"}
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -87,7 +87,11 @@ export default function SetupKeyModal({ children, open, setOpen }: Props) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={"px-8 pb-6"}>
|
||||
<div
|
||||
className={"px-8 pb-6"}
|
||||
data-cy={"setup-key-copy-input"}
|
||||
data-cy-setup-key-value={setupKey?.key || ""}
|
||||
>
|
||||
<Code message={copyMessage}>
|
||||
<Code.Line>
|
||||
{setupKey?.key || "Setup key could not be created..."}
|
||||
@@ -101,6 +105,7 @@ export default function SetupKeyModal({ children, open, setOpen }: Props) {
|
||||
variant={"secondary"}
|
||||
className={"w-full"}
|
||||
tabIndex={-1}
|
||||
data-cy={"setup-key-close"}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
@@ -108,6 +113,7 @@ export default function SetupKeyModal({ children, open, setOpen }: Props) {
|
||||
<Button
|
||||
variant={"primary"}
|
||||
className={"w-full"}
|
||||
data-cy={"setup-key-copy"}
|
||||
onClick={() => copy(copyMessage)}
|
||||
>
|
||||
<CopyIcon size={14} />
|
||||
@@ -202,6 +208,7 @@ export function SetupKeyModalContent({ onSuccess }: ModalProps) {
|
||||
<Input
|
||||
placeholder={"e.g., AWS Servers"}
|
||||
value={name}
|
||||
data-cy={"setup-key-name"}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
@@ -233,6 +240,7 @@ export function SetupKeyModalContent({ onSuccess }: ModalProps) {
|
||||
disabled={!reusable}
|
||||
value={usageLimit}
|
||||
type={"number"}
|
||||
data-cy={"setup-key-usage-limit"}
|
||||
onChange={(e) => setUsageLimit(e.target.value)}
|
||||
placeholder={usageLimitPlaceholder}
|
||||
customPrefix={
|
||||
@@ -256,6 +264,7 @@ export function SetupKeyModalContent({ onSuccess }: ModalProps) {
|
||||
error={expiresInError}
|
||||
errorTooltip={true}
|
||||
type={"number"}
|
||||
data-cy={"setup-key-expire-in-days"}
|
||||
onChange={(e) => setExpiresIn(e.target.value)}
|
||||
customPrefix={
|
||||
<AlarmClock size={16} className={"text-nb-gray-300"} />
|
||||
@@ -312,7 +321,12 @@ export function SetupKeyModalContent({ onSuccess }: ModalProps) {
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
</ModalClose>
|
||||
|
||||
<Button variant={"primary"} onClick={submit} disabled={isDisabled}>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
onClick={submit}
|
||||
disabled={isDisabled}
|
||||
data-cy={"create-setup-key"}
|
||||
>
|
||||
<PlusCircle size={16} />
|
||||
Create Setup Key
|
||||
</Button>
|
||||
|
||||
@@ -3,7 +3,20 @@ import ActiveInactiveRow from "@/modules/common-table-rows/ActiveInactiveRow";
|
||||
type Props = {
|
||||
name: string;
|
||||
valid: boolean;
|
||||
secret?: string;
|
||||
};
|
||||
export default function SetupKeyNameCell({ valid, name }: Props) {
|
||||
return <ActiveInactiveRow active={valid} inactiveDot={"red"} text={name} />;
|
||||
export default function SetupKeyNameCell({ name, valid, secret }: Props) {
|
||||
return (
|
||||
<ActiveInactiveRow
|
||||
active={valid || false}
|
||||
inactiveDot={"red"}
|
||||
text={name || ""}
|
||||
>
|
||||
{secret && (
|
||||
<span className={"font-mono text-xs text-nb-gray-400 mt-1"}>
|
||||
{secret.substring(0, 5) + "****"}
|
||||
</span>
|
||||
)}
|
||||
</ActiveInactiveRow>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import Badge from "@components/Badge";
|
||||
import { IconRepeat } from "@tabler/icons-react";
|
||||
import { Repeat1 } from "lucide-react";
|
||||
|
||||
type Props = {
|
||||
reusable: boolean;
|
||||
};
|
||||
export default function SetupKeyTypeCell({ reusable }: Props) {
|
||||
return (
|
||||
<div className={"flex"}>
|
||||
<Badge className={"text-xs"} variant={"gray"}>
|
||||
{reusable ? (
|
||||
<>
|
||||
<IconRepeat size={14} className={"text-green-400"} /> Reusable
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Repeat1 size={14} /> One-off
|
||||
</>
|
||||
)}
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { MonitorSmartphoneIcon } from "lucide-react";
|
||||
import { IconRepeat } from "@tabler/icons-react";
|
||||
import { Repeat1 } from "lucide-react";
|
||||
|
||||
type Props = {
|
||||
current: number;
|
||||
@@ -7,14 +8,16 @@ type Props = {
|
||||
};
|
||||
export default function SetupKeyUsageCell({ current, limit, reusable }: Props) {
|
||||
return reusable ? (
|
||||
<div className={"flex gap-1 flex-col"}>
|
||||
<div className={"flex items-center gap-2"}>
|
||||
<MonitorSmartphoneIcon size={14} />
|
||||
{current} of {limit} Peers
|
||||
</div>
|
||||
<div></div>
|
||||
<div className={"flex items-center text-[13px] text-nb-gray-300 gap-2"}>
|
||||
<IconRepeat size={14} className={"text-green-400"} />
|
||||
<span>
|
||||
<span className={"font-medium text-nb-gray-200"}> {current} </span> of{" "}
|
||||
{limit == 0 ? <>Unlimited</> : limit} Peers
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className={"text-nb-gray-800"}>-</div>
|
||||
<div className={"flex items-center text-[13px] text-nb-gray-300 gap-2"}>
|
||||
<Repeat1 size={14} /> One-off
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,10 +3,11 @@ import ButtonGroup from "@components/ButtonGroup";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import SquareIcon from "@components/SquareIcon";
|
||||
import { DataTable } from "@components/table/DataTable";
|
||||
import DataTableHeader from "@components/table/DataTableHeader";
|
||||
import DataTableRefreshButton from "@components/table/DataTableRefreshButton";
|
||||
import { DataTableRowsPerPage } from "@components/table/DataTableRowsPerPage";
|
||||
import GetStartedTest from "@components/ui/GetStartedTest";
|
||||
import { SortingState } from "@tanstack/react-table";
|
||||
import { ColumnDef, SortingState } from "@tanstack/react-table";
|
||||
import { ExternalLinkIcon, PlusCircle } from "lucide-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import React, { useState } from "react";
|
||||
@@ -14,8 +15,96 @@ import { useSWRConfig } from "swr";
|
||||
import SetupKeysIcon from "@/assets/icons/SetupKeysIcon";
|
||||
import { useLocalStorage } from "@/hooks/useLocalStorage";
|
||||
import { SetupKey } from "@/interfaces/SetupKey";
|
||||
import ExpirationDateRow from "@/modules/common-table-rows/ExpirationDateRow";
|
||||
import LastTimeRow from "@/modules/common-table-rows/LastTimeRow";
|
||||
import SetupKeyActionCell from "@/modules/setup-keys/SetupKeyActionCell";
|
||||
import SetupKeyEphemeralCell from "@/modules/setup-keys/SetupKeyEphemeralCell";
|
||||
import SetupKeyGroupsCell from "@/modules/setup-keys/SetupKeyGroupsCell";
|
||||
import SetupKeyModal from "@/modules/setup-keys/SetupKeyModal";
|
||||
import { SetupKeysTableColumns } from "@/modules/setup-keys/SetupKeysTableColumns";
|
||||
import SetupKeyNameCell from "@/modules/setup-keys/SetupKeyNameCell";
|
||||
import SetupKeyUsageCell from "@/modules/setup-keys/SetupKeyUsageCell";
|
||||
|
||||
export const SetupKeysTableColumns: ColumnDef<SetupKey>[] = [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Name & Key</DataTableHeader>;
|
||||
},
|
||||
sortingFn: "text",
|
||||
cell: ({ row }) => (
|
||||
<SetupKeyNameCell
|
||||
name={row.original.name}
|
||||
valid={row.original.valid}
|
||||
secret={row.original.key}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "valid",
|
||||
accessorKey: "valid",
|
||||
sortingFn: "basic",
|
||||
},
|
||||
{
|
||||
accessorKey: "usage_limit",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Usage</DataTableHeader>;
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<SetupKeyUsageCell
|
||||
current={row.original.used_times}
|
||||
limit={row.original.usage_limit || 0}
|
||||
reusable={row.original.type == "reusable"}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "last_used",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Last used</DataTableHeader>;
|
||||
},
|
||||
sortingFn: "datetime",
|
||||
cell: ({ row }) => (
|
||||
<LastTimeRow date={row.original.last_used} text={"Last used on"} />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "group_strings",
|
||||
accessorKey: "group_strings",
|
||||
accessorFn: (s) => s.groups?.map((g) => g?.name || "").join(", "),
|
||||
},
|
||||
{
|
||||
accessorFn: (item) => item.auto_groups?.length,
|
||||
id: "groups",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Groups</DataTableHeader>;
|
||||
},
|
||||
cell: ({ row }) => <SetupKeyGroupsCell setupKey={row.original} />,
|
||||
},
|
||||
{
|
||||
accessorKey: "ephemeral",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Ephemeral</DataTableHeader>;
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<SetupKeyEphemeralCell ephemeral={row.original.ephemeral} />
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "expires",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Expires</DataTableHeader>;
|
||||
},
|
||||
cell: ({ row }) => <ExpirationDateRow date={row.original.expires} />,
|
||||
},
|
||||
|
||||
{
|
||||
accessorKey: "id",
|
||||
header: "",
|
||||
cell: ({ row }) => {
|
||||
return <SetupKeyActionCell setupKey={row.original} />;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
type Props = {
|
||||
setupKeys?: SetupKey[];
|
||||
@@ -33,10 +122,6 @@ export default function SetupKeysTable({ setupKeys, isLoading }: Props) {
|
||||
id: "valid",
|
||||
desc: true,
|
||||
},
|
||||
{
|
||||
id: "type",
|
||||
desc: true,
|
||||
},
|
||||
{
|
||||
id: "last_used",
|
||||
desc: true,
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import DataTableHeader from "@components/table/DataTableHeader";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import * as React from "react";
|
||||
import { SetupKey } from "@/interfaces/SetupKey";
|
||||
import ExpirationDateRow from "@/modules/common-table-rows/ExpirationDateRow";
|
||||
import LastTimeRow from "@/modules/common-table-rows/LastTimeRow";
|
||||
import SetupKeyActionCell from "@/modules/setup-keys/SetupKeyActionCell";
|
||||
import SetupKeyGroupsCell from "@/modules/setup-keys/SetupKeyGroupsCell";
|
||||
import SetupKeyKeyCell from "@/modules/setup-keys/SetupKeyKeyCell";
|
||||
import SetupKeyNameCell from "@/modules/setup-keys/SetupKeyNameCell";
|
||||
import SetupKeyTypeCell from "@/modules/setup-keys/SetupKeyTypeCell";
|
||||
|
||||
export const SetupKeysTableColumns: ColumnDef<SetupKey>[] = [
|
||||
/* {
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={table.getIsAllPageRowsSelected()}
|
||||
onCheckedChange={(value) => table.toggleAllRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
aria-label="Select row"
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
},*/
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Name</DataTableHeader>;
|
||||
},
|
||||
sortingFn: "text",
|
||||
cell: ({ row }) => (
|
||||
<SetupKeyNameCell
|
||||
valid={row.original.valid}
|
||||
name={row.original?.name || ""}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "valid",
|
||||
accessorKey: "valid",
|
||||
sortingFn: "basic",
|
||||
},
|
||||
{
|
||||
accessorKey: "type",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Reusable</DataTableHeader>;
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<SetupKeyTypeCell reusable={row.original.type === "reusable"} />
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "key",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Key</DataTableHeader>;
|
||||
},
|
||||
cell: ({ row }) => <SetupKeyKeyCell text={row.original.key} />,
|
||||
},
|
||||
{
|
||||
id: "group_strings",
|
||||
accessorKey: "group_strings",
|
||||
accessorFn: (s) => s.groups?.map((g) => g?.name || "").join(", "),
|
||||
},
|
||||
{
|
||||
accessorKey: "last_used",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Last used</DataTableHeader>;
|
||||
},
|
||||
sortingFn: "datetime",
|
||||
cell: ({ row }) => (
|
||||
<LastTimeRow date={row.original.last_used} text={"Last used on"} />
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorFn: (item) => item.auto_groups?.length,
|
||||
id: "groups",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Groups</DataTableHeader>;
|
||||
},
|
||||
cell: ({ row }) => <SetupKeyGroupsCell setupKey={row.original} />,
|
||||
},
|
||||
{
|
||||
accessorKey: "expires",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Expires</DataTableHeader>;
|
||||
},
|
||||
cell: ({ row }) => <ExpirationDateRow date={row.original.expires} />,
|
||||
},
|
||||
|
||||
{
|
||||
accessorKey: "id",
|
||||
header: "",
|
||||
cell: ({ row }) => {
|
||||
return <SetupKeyActionCell setupKey={row.original} />;
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -27,9 +27,11 @@ const loadConfig = (): Config => {
|
||||
let silentRedirectURI = "/#silent-callback";
|
||||
let tokenSource = "accessToken";
|
||||
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
if (process.env.APP_ENV === "test") {
|
||||
configJson = require("@/config/test");
|
||||
} else if (process.env.NODE_ENV === "development") {
|
||||
configJson = require("@/config/local");
|
||||
} else {
|
||||
} else if (process.env.NODE_ENV === "production") {
|
||||
configJson = require("@/config/production");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user