Show loading indicator for peer detail view as groups are loading (#343)

This commit is contained in:
Eduard Gert
2024-02-26 18:02:28 +01:00
committed by GitHub
parent f74f9cf812
commit a8b66d935f
8 changed files with 192 additions and 121 deletions

View File

@@ -0,0 +1,36 @@
import Skeleton from "react-loading-skeleton";
export default function SkeletonPeerDetail() {
return (
<div className={"w-full mt-6 p-default"}>
<div className={"flex flex-wrap w-full justify-between max-w-6xl "}>
<Skeleton height={24} width={300} className={"rounded-md"} />
</div>
<div className={"flex flex-wrap w-full justify-between mt-4 max-w-6xl "}>
<Skeleton height={42} width={400} className={"rounded-md"} />
<div className={"flex gap-3"}>
<Skeleton height={42} width={80} className={"rounded-md"} />
<Skeleton height={42} width={120} className={"rounded-md"} />
</div>
</div>
<div
className={
"flex flex-wrap w-full justify-between mt-6 max-w-6xl gap-10"
}
>
<Skeleton
height={400}
width={"100%"}
className={"rounded-md"}
containerClassName={"flex-1 "}
/>
<Skeleton
height={300}
width={"100%"}
className={"rounded-md opacity-30"}
containerClassName={"flex-1 "}
/>
</div>
</div>
);
}

View File

@@ -31,7 +31,10 @@ export default function GroupBadge({
<TextWithTooltip text={group.name} maxChars={20} />
{children}
{showX && (
<XIcon size={12} className={"cursor-pointer group-hover:text-white"} />
<XIcon
size={12}
className={"cursor-pointer group-hover:text-white shrink-0"}
/>
)}
</Badge>
);

View File

@@ -12,11 +12,12 @@ const GroupContext = React.createContext(
refresh: () => void;
dropdownOptions: Group[];
setDropdownOptions: React.Dispatch<React.SetStateAction<Group[]>>;
isLoading: boolean;
},
);
export default function GroupsProvider({ children }: Props) {
const { data: groups, mutate } = useFetchApi<Group[]>("/groups");
const { data: groups, mutate, isLoading } = useFetchApi<Group[]>("/groups");
const [dropdownOptions, setDropdownOptions] = useState<Group[]>([]);
const refresh = () => {
@@ -25,7 +26,13 @@ export default function GroupsProvider({ children }: Props) {
return (
<GroupContext.Provider
value={{ groups, refresh, dropdownOptions, setDropdownOptions }}
value={{
groups,
refresh,
dropdownOptions,
setDropdownOptions,
isLoading,
}}
>
{children}
</GroupContext.Provider>

View File

@@ -1,4 +1,5 @@
import { notify } from "@components/Notification";
import SkeletonPeerDetail from "@components/skeletons/SkeletonPeerDetail";
import { useApiCall } from "@utils/api";
import React, { useMemo } from "react";
import { useSWRConfig } from "swr";
@@ -27,12 +28,13 @@ const PeerContext = React.createContext(
) => Promise<Peer>;
openSSHDialog: () => Promise<boolean>;
deletePeer: () => void;
isLoading: boolean;
},
);
export default function PeerProvider({ children, peer }: Props) {
const user = usePeerUser(peer);
const peerGroups = usePeerGroups(peer);
const { peerGroups, isLoading } = usePeerGroups(peer);
const peerRequest = useApiCall<Peer>("/peers");
const { confirm } = useDialog();
const { mutate } = useSWRConfig();
@@ -94,12 +96,22 @@ export default function PeerProvider({ children, peer }: Props) {
});
};
return (
return !isLoading ? (
<PeerContext.Provider
value={{ peer, peerGroups, user, update, openSSHDialog, deletePeer }}
value={{
peer,
peerGroups,
user,
update,
openSSHDialog,
deletePeer,
isLoading,
}}
>
{children}
</PeerContext.Provider>
) : (
<SkeletonPeerDetail />
);
}
@@ -108,9 +120,9 @@ export default function PeerProvider({ children, peer }: Props) {
* @param peer
*/
export const usePeerGroups = (peer?: Peer) => {
const { groups } = useGroups();
const { groups, isLoading } = useGroups();
return useMemo(() => {
const peerGroups = useMemo(() => {
if (!peer) return [];
const peerGroups = groups?.filter((group) => {
const foundGroup = group.peers?.find((p) => {
@@ -121,6 +133,8 @@ export const usePeerGroups = (peer?: Peer) => {
});
return peerGroups || [];
}, [groups, peer]);
return { peerGroups, isLoading };
};
/**

View File

@@ -30,7 +30,7 @@ export default function useGroupHelper({ initial = [], peer }: Props) {
}, [groups, initial]);
const [selectedGroups, setSelectedGroups] = useState<Group[]>(initialGroups);
const peerGroups = usePeerGroups(peer);
const { peerGroups } = usePeerGroups(peer);
const save = async () => {
return Promise.all(getAllGroupCalls()).then((groups) => {

View File

@@ -9,7 +9,7 @@ type Props = {
};
export default function usePeerRoutes({ peer }: Props) {
const { data: routes } = useFetchApi<Route[]>("/routes");
const peerGroups = usePeerGroups(peer);
const { peerGroups } = usePeerGroups(peer);
return useMemo(() => {
if (!routes) return undefined;

View File

@@ -1,5 +1,3 @@
"use client";
import Button from "@components/Button";
import Code from "@components/Code";
import FancyToggleSwitch from "@components/FancyToggleSwitch";
@@ -39,11 +37,12 @@ import { SetupKey } from "@/interfaces/SetupKey";
import useGroupHelper from "@/modules/groups/useGroupHelper";
type Props = {
children: React.ReactNode;
children?: React.ReactNode;
open: boolean;
setOpen: (open: boolean) => void;
};
const copyMessage = "Setup-Key was copied to your clipboard!";
export default function SetupKeyModal({ children }: Props) {
const [modal, setModal] = useState(false);
export default function SetupKeyModal({ children, open, setOpen }: Props) {
const [successModal, setSuccessModal] = useState(false);
const [setupKey, setSetupKey] = useState<SetupKey>();
const [, copy] = useCopyToClipboard(setupKey?.key);
@@ -55,15 +54,15 @@ export default function SetupKeyModal({ children }: Props) {
return (
<>
<Modal open={modal} onOpenChange={setModal} key={modal ? 1 : 0}>
<ModalTrigger asChild>{children}</ModalTrigger>
<Modal open={open} onOpenChange={setOpen} key={open ? 1 : 0}>
{children && <ModalTrigger asChild>{children}</ModalTrigger>}
<SetupKeyModalContent onSuccess={handleSuccess} />
</Modal>
<Modal
open={successModal}
onOpenChange={(open) => {
setSuccessModal(open);
setModal(open);
setOpen(open);
}}
>
<ModalContent

View File

@@ -9,7 +9,7 @@ import GetStartedTest from "@components/ui/GetStartedTest";
import { SortingState } from "@tanstack/react-table";
import { ExternalLinkIcon, PlusCircle } from "lucide-react";
import { usePathname } from "next/navigation";
import React from "react";
import React, { useState } from "react";
import { useSWRConfig } from "swr";
import SetupKeysIcon from "@/assets/icons/SetupKeysIcon";
import { useLocalStorage } from "@/hooks/useLocalStorage";
@@ -47,114 +47,126 @@ export default function SetupKeysTable({ setupKeys, isLoading }: Props) {
},
],
);
const [open, setOpen] = useState(false);
return (
<DataTable
isLoading={isLoading}
text={"Setup Keys"}
sorting={sorting}
setSorting={setSorting}
columns={SetupKeysTableColumns}
data={setupKeys}
searchPlaceholder={"Search by name, type or group..."}
columnVisibility={{
valid: false,
group_strings: false,
}}
getStartedCard={
<GetStartedTest
icon={
<SquareIcon
icon={<SetupKeysIcon className={"fill-nb-gray-200"} size={20} />}
color={"gray"}
size={"large"}
/>
}
title={"Create Setup Key"}
description={
"Add a setup key to register new machines in your network. The key links machines to your account during initial setup."
}
button={
<SetupKeyModal>
<Button variant={"primary"} className={""}>
<PlusCircle size={16} />
Create Setup Key
</Button>
</SetupKeyModal>
}
learnMore={
<>
Learn more about
<InlineLink
href={
"https://docs.netbird.io/how-to/register-machines-using-setup-keys"
<>
<SetupKeyModal open={open} setOpen={setOpen} />
<DataTable
isLoading={isLoading}
text={"Setup Keys"}
sorting={sorting}
setSorting={setSorting}
columns={SetupKeysTableColumns}
data={setupKeys}
searchPlaceholder={"Search by name, type or group..."}
columnVisibility={{
valid: false,
group_strings: false,
}}
getStartedCard={
<GetStartedTest
icon={
<SquareIcon
icon={
<SetupKeysIcon className={"fill-nb-gray-200"} size={20} />
}
target={"_blank"}
color={"gray"}
size={"large"}
/>
}
title={"Create Setup Key"}
description={
"Add a setup key to register new machines in your network. The key links machines to your account during initial setup."
}
button={
<Button
variant={"primary"}
className={""}
onClick={() => setOpen(true)}
>
Setup Keys
<ExternalLinkIcon size={12} />
</InlineLink>
</>
}
/>
}
rightSide={() => (
<>
{setupKeys && setupKeys?.length > 0 && (
<SetupKeyModal>
<Button variant={"primary"} className={"ml-auto"}>
<PlusCircle size={16} />
Create Setup Key
</Button>
</SetupKeyModal>
)}
</>
)}
>
{(table) => (
<>
<ButtonGroup disabled={setupKeys?.length == 0}>
<ButtonGroup.Button
onClick={() => {
table.setPageIndex(0);
table.getColumn("valid")?.setFilterValue(true);
}}
disabled={setupKeys?.length == 0}
variant={
table.getColumn("valid")?.getFilterValue() == true
? "tertiary"
: "secondary"
}
>
Valid
</ButtonGroup.Button>
<ButtonGroup.Button
onClick={() => {
table.setPageIndex(0);
table.getColumn("valid")?.setFilterValue("");
}}
disabled={setupKeys?.length == 0}
variant={
table.getColumn("valid")?.getFilterValue() != true
? "tertiary"
: "secondary"
}
>
All
</ButtonGroup.Button>
</ButtonGroup>
<DataTableRowsPerPage
table={table}
disabled={setupKeys?.length == 0}
}
learnMore={
<>
Learn more about
<InlineLink
href={
"https://docs.netbird.io/how-to/register-machines-using-setup-keys"
}
target={"_blank"}
>
Setup Keys
<ExternalLinkIcon size={12} />
</InlineLink>
</>
}
/>
<DataTableRefreshButton
isDisabled={setupKeys?.length == 0}
onClick={() => {
mutate("/setup-keys").then();
mutate("/groups").then();
}}
/>
</>
)}
</DataTable>
}
rightSide={() => (
<>
{setupKeys && setupKeys?.length > 0 && (
<Button
variant={"primary"}
className={"ml-auto"}
onClick={() => setOpen(true)}
>
<PlusCircle size={16} />
Create Setup Key
</Button>
)}
</>
)}
>
{(table) => (
<>
<ButtonGroup disabled={setupKeys?.length == 0}>
<ButtonGroup.Button
onClick={() => {
table.setPageIndex(0);
table.getColumn("valid")?.setFilterValue(true);
}}
disabled={setupKeys?.length == 0}
variant={
table.getColumn("valid")?.getFilterValue() == true
? "tertiary"
: "secondary"
}
>
Valid
</ButtonGroup.Button>
<ButtonGroup.Button
onClick={() => {
table.setPageIndex(0);
table.getColumn("valid")?.setFilterValue("");
}}
disabled={setupKeys?.length == 0}
variant={
table.getColumn("valid")?.getFilterValue() != true
? "tertiary"
: "secondary"
}
>
All
</ButtonGroup.Button>
</ButtonGroup>
<DataTableRowsPerPage
table={table}
disabled={setupKeys?.length == 0}
/>
<DataTableRefreshButton
isDisabled={setupKeys?.length == 0}
onClick={() => {
mutate("/setup-keys").then();
mutate("/groups").then();
}}
/>
</>
)}
</DataTable>
</>
);
}