From a6da3ee839f483c7724776fd2c14f3325014a856 Mon Sep 17 00:00:00 2001 From: Eduard Gert Date: Wed, 8 May 2024 11:23:38 +0200 Subject: [PATCH] UX changes for modals and refactoring --- next.config.js | 3 + src/app/not-found.tsx | 38 +++++- src/assets/icons/CircleIcon.tsx | 2 + src/components/ButtonGroup.tsx | 10 +- src/components/PeerGroupSelector.tsx | 1 + src/components/SegmentedTabs.tsx | 15 ++- src/components/modal/Modal.tsx | 5 +- src/components/table/DataTableRowsPerPage.tsx | 4 +- src/contexts/DialogProvider.tsx | 2 + src/contexts/PeerProvider.tsx | 4 +- .../access-control/AccessControlModal.tsx | 111 ++++++++++++---- src/modules/common-table-rows/GroupsRow.tsx | 13 +- .../dns-nameservers/NameserverModal.tsx | 120 +++++++++++++----- src/modules/exit-node/useHasExitNodes.tsx | 3 +- .../modal/PostureCheckModal.tsx | 28 +++- .../ui/PostureCheckTabTrigger.tsx | 8 +- .../setup-keys/SetupKeyEphemeralCell.tsx | 37 ++++++ src/modules/setup-keys/SetupKeyGroupsCell.tsx | 1 + src/modules/setup-keys/SetupKeyKeyCell.tsx | 16 --- src/modules/setup-keys/SetupKeyModal.tsx | 18 ++- src/modules/setup-keys/SetupKeyNameCell.tsx | 17 ++- src/modules/setup-keys/SetupKeyTypeCell.tsx | 24 ---- src/modules/setup-keys/SetupKeyUsageCell.tsx | 19 +-- src/modules/setup-keys/SetupKeysTable.tsx | 97 +++++++++++++- .../setup-keys/SetupKeysTableColumns.tsx | 107 ---------------- src/utils/config.ts | 6 +- tsconfig.json | 3 + 27 files changed, 455 insertions(+), 257 deletions(-) create mode 100644 src/modules/setup-keys/SetupKeyEphemeralCell.tsx delete mode 100644 src/modules/setup-keys/SetupKeyKeyCell.tsx delete mode 100644 src/modules/setup-keys/SetupKeyTypeCell.tsx delete mode 100644 src/modules/setup-keys/SetupKeysTableColumns.tsx diff --git a/next.config.js b/next.config.js index 13c4fd1d..b270871a 100644 --- a/next.config.js +++ b/next.config.js @@ -5,6 +5,9 @@ const nextConfig = { unoptimized: true, }, reactStrictMode: false, + env: { + APP_ENV: process.env.APP_ENV || "production", + }, }; module.exports = nextConfig; diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index ef291c07..58f7f98e 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -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(); + const [mounted, setMounted] = useState(false); + const [tempQueryParams, setTempQueryParams] = useLocalStorage( + "netbird-query-params", + "", + ); + const [queryParams, setQueryParams] = useState(""); + useEffect(() => { - router.push("/peers"); - }); + setQueryParams(tempQueryParams); + setTempQueryParams(""); + setMounted(true); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); - return ; + return mounted ? ( + + ) : ( + + ); } + +const Redirect = ({ url, queryParams }: Props) => { + useRedirect(url == "/" ? "/peers" : url + (queryParams && `?${queryParams}`)); + return ; +}; diff --git a/src/assets/icons/CircleIcon.tsx b/src/assets/icons/CircleIcon.tsx index c852b51d..8301cc2c 100644 --- a/src/assets/icons/CircleIcon.tsx +++ b/src/assets/icons/CircleIcon.tsx @@ -16,6 +16,8 @@ export default function CircleIcon({ return ( {children} @@ -21,7 +23,10 @@ function ButtonGroup({ children, disabled }: Props) { } const ButtonGroupButton = forwardRef( - ({ ...props }: ButtonProps, ref: React.ForwardedRef) => { + ( + { className, ...props }: ButtonProps, + ref: React.ForwardedRef, + ) => { return ( - - - + {!policy ? ( + <> + {tab == "policy" && ( + + + + )} + + {tab == "posture_checks" && ( + + )} + + {tab == "policy" && ( + + )} + + {tab == "posture_checks" && ( + + )} + + {tab == "general" && ( + <> + + + + + )} + + ) : ( + <> + + + + + + )} diff --git a/src/modules/common-table-rows/GroupsRow.tsx b/src/modules/common-table-rows/GroupsRow.tsx index 18fd70bc..0a8fc8f0 100644 --- a/src/modules/common-table-rows/GroupsRow.tsx +++ b/src/modules/common-table-rows/GroupsRow.tsx @@ -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); }} > - + {foundGroups?.length == 0 && showAddGroupButton ? ( + + + Add Groups + + ) : ( + + )} { - 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 ( @@ -269,7 +264,7 @@ export function NameserverModalContent({ color={"netbird"} /> - setTab(v)}> + setTab(v)} value={tab}> Nameserver - + Domains - +
- - - - - + {!isUpdate ? ( + <> + {tab == "nameserver" && ( + + + + )} + + {tab == "domains" && ( + + )} + + {tab == "nameserver" && ( + + )} + + {tab == "domains" && ( + + )} + + {tab == "general" && ( + <> + + + + + )} + + ) : ( + <> + + + + + + )}
diff --git a/src/modules/exit-node/useHasExitNodes.tsx b/src/modules/exit-node/useHasExitNodes.tsx index 288fab2c..e99b967d 100644 --- a/src/modules/exit-node/useHasExitNodes.tsx +++ b/src/modules/exit-node/useHasExitNodes.tsx @@ -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; }; diff --git a/src/modules/posture-checks/modal/PostureCheckModal.tsx b/src/modules/posture-checks/modal/PostureCheckModal.tsx index a0db0d84..2459427e 100644 --- a/src/modules/posture-checks/modal/PostureCheckModal.tsx +++ b/src/modules/posture-checks/modal/PostureCheckModal.tsx @@ -163,7 +163,10 @@ export default function PostureCheckModal({ Checks - +
<> - + {tab == "checks" && ( + + )} + + {tab == "general" && ( + + )} {!postureCheck && tab == "checks" && (
+ } + disabled={!ephemeral} + > + + + Ephemeral + + + + ) : ( + + ); +} diff --git a/src/modules/setup-keys/SetupKeyGroupsCell.tsx b/src/modules/setup-keys/SetupKeyGroupsCell.tsx index 8b53e350..f862be1c 100644 --- a/src/modules/setup-keys/SetupKeyGroupsCell.tsx +++ b/src/modules/setup-keys/SetupKeyGroupsCell.tsx @@ -46,6 +46,7 @@ export default function SetupKeyGroupsCell({ setupKey }: Props) { } groups={setupKey.auto_groups || []} onSave={handleSave} + showAddGroupButton={true} modal={modal} setModal={setModal} /> diff --git a/src/modules/setup-keys/SetupKeyKeyCell.tsx b/src/modules/setup-keys/SetupKeyKeyCell.tsx deleted file mode 100644 index bb9fbd1d..00000000 --- a/src/modules/setup-keys/SetupKeyKeyCell.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import Badge from "@components/Badge"; -import React from "react"; - -type Props = { - text: string; -}; - -export default function SetupKeyKeyCell({ text }: Props) { - return ( -
- - {text.substring(0, 5) + "****"} - -
- ); -} diff --git a/src/modules/setup-keys/SetupKeyModal.tsx b/src/modules/setup-keys/SetupKeyModal.tsx index c5ebd44b..06a4d5b2 100644 --- a/src/modules/setup-keys/SetupKeyModal.tsx +++ b/src/modules/setup-keys/SetupKeyModal.tsx @@ -87,7 +87,11 @@ export default function SetupKeyModal({ children, open, setOpen }: Props) { -
+
{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 @@ -108,6 +113,7 @@ export default function SetupKeyModal({ children, open, setOpen }: Props) {
@@ -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={ @@ -312,7 +321,12 @@ export function SetupKeyModalContent({ onSuccess }: ModalProps) { - diff --git a/src/modules/setup-keys/SetupKeyNameCell.tsx b/src/modules/setup-keys/SetupKeyNameCell.tsx index 918f2c94..71eabb30 100644 --- a/src/modules/setup-keys/SetupKeyNameCell.tsx +++ b/src/modules/setup-keys/SetupKeyNameCell.tsx @@ -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 ; +export default function SetupKeyNameCell({ name, valid, secret }: Props) { + return ( + + {secret && ( + + {secret.substring(0, 5) + "****"} + + )} + + ); } diff --git a/src/modules/setup-keys/SetupKeyTypeCell.tsx b/src/modules/setup-keys/SetupKeyTypeCell.tsx deleted file mode 100644 index 63ef685b..00000000 --- a/src/modules/setup-keys/SetupKeyTypeCell.tsx +++ /dev/null @@ -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 ( -
- - {reusable ? ( - <> - Reusable - - ) : ( - <> - One-off - - )} - -
- ); -} diff --git a/src/modules/setup-keys/SetupKeyUsageCell.tsx b/src/modules/setup-keys/SetupKeyUsageCell.tsx index 10bca29a..ca48b27c 100644 --- a/src/modules/setup-keys/SetupKeyUsageCell.tsx +++ b/src/modules/setup-keys/SetupKeyUsageCell.tsx @@ -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 ? ( -
-
- - {current} of {limit} Peers -
-
+
+ + + {current} of{" "} + {limit == 0 ? <>Unlimited : limit} Peers +
) : ( -
-
+
+ One-off +
); } diff --git a/src/modules/setup-keys/SetupKeysTable.tsx b/src/modules/setup-keys/SetupKeysTable.tsx index 785484e1..f04ce55c 100644 --- a/src/modules/setup-keys/SetupKeysTable.tsx +++ b/src/modules/setup-keys/SetupKeysTable.tsx @@ -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[] = [ + { + accessorKey: "name", + header: ({ column }) => { + return Name & Key; + }, + sortingFn: "text", + cell: ({ row }) => ( + + ), + }, + { + id: "valid", + accessorKey: "valid", + sortingFn: "basic", + }, + { + accessorKey: "usage_limit", + header: ({ column }) => { + return Usage; + }, + cell: ({ row }) => ( + + ), + }, + { + accessorKey: "last_used", + header: ({ column }) => { + return Last used; + }, + sortingFn: "datetime", + cell: ({ row }) => ( + + ), + }, + { + 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 Groups; + }, + cell: ({ row }) => , + }, + { + accessorKey: "ephemeral", + header: ({ column }) => { + return Ephemeral; + }, + cell: ({ row }) => ( + + ), + }, + { + accessorKey: "expires", + header: ({ column }) => { + return Expires; + }, + cell: ({ row }) => , + }, + + { + accessorKey: "id", + header: "", + cell: ({ row }) => { + return ; + }, + }, +]; 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, diff --git a/src/modules/setup-keys/SetupKeysTableColumns.tsx b/src/modules/setup-keys/SetupKeysTableColumns.tsx deleted file mode 100644 index ae60a4dd..00000000 --- a/src/modules/setup-keys/SetupKeysTableColumns.tsx +++ /dev/null @@ -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[] = [ - /* { - id: "select", - header: ({ table }) => ( - table.toggleAllRowsSelected(!!value)} - aria-label="Select all" - /> - ), - cell: ({ row }) => ( - row.toggleSelected(!!value)} - aria-label="Select row" - /> - ), - enableSorting: false, - enableHiding: false, - },*/ - { - accessorKey: "name", - header: ({ column }) => { - return Name; - }, - sortingFn: "text", - cell: ({ row }) => ( - - ), - }, - { - id: "valid", - accessorKey: "valid", - sortingFn: "basic", - }, - { - accessorKey: "type", - header: ({ column }) => { - return Reusable; - }, - cell: ({ row }) => ( - - ), - }, - { - accessorKey: "key", - header: ({ column }) => { - return Key; - }, - cell: ({ row }) => , - }, - { - id: "group_strings", - accessorKey: "group_strings", - accessorFn: (s) => s.groups?.map((g) => g?.name || "").join(", "), - }, - { - accessorKey: "last_used", - header: ({ column }) => { - return Last used; - }, - sortingFn: "datetime", - cell: ({ row }) => ( - - ), - }, - { - accessorFn: (item) => item.auto_groups?.length, - id: "groups", - header: ({ column }) => { - return Groups; - }, - cell: ({ row }) => , - }, - { - accessorKey: "expires", - header: ({ column }) => { - return Expires; - }, - cell: ({ row }) => , - }, - - { - accessorKey: "id", - header: "", - cell: ({ row }) => { - return ; - }, - }, -]; diff --git a/src/utils/config.ts b/src/utils/config.ts index 19c0fdbe..bbe05cee 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -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"); } diff --git a/tsconfig.json b/tsconfig.json index e945d0f6..c1e99ba9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -34,6 +34,9 @@ "@/config/local": [ "./.local-config.json" ], + "@/config/test": [ + "./.test-config.json" + ], "@components/*": [ "./src/components/*" ],