mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-01-26 01:21:04 +00:00
Show loading indicator for peer detail view as groups are loading (#343)
This commit is contained in:
36
src/components/skeletons/SkeletonPeerDetail.tsx
Normal file
36
src/components/skeletons/SkeletonPeerDetail.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,7 +47,12 @@ export default function SetupKeysTable({ setupKeys, isLoading }: Props) {
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SetupKeyModal open={open} setOpen={setOpen} />
|
||||
<DataTable
|
||||
isLoading={isLoading}
|
||||
text={"Setup Keys"}
|
||||
@@ -64,7 +69,9 @@ export default function SetupKeysTable({ setupKeys, isLoading }: Props) {
|
||||
<GetStartedTest
|
||||
icon={
|
||||
<SquareIcon
|
||||
icon={<SetupKeysIcon className={"fill-nb-gray-200"} size={20} />}
|
||||
icon={
|
||||
<SetupKeysIcon className={"fill-nb-gray-200"} size={20} />
|
||||
}
|
||||
color={"gray"}
|
||||
size={"large"}
|
||||
/>
|
||||
@@ -74,12 +81,14 @@ export default function SetupKeysTable({ setupKeys, isLoading }: Props) {
|
||||
"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={""}>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
className={""}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<PlusCircle size={16} />
|
||||
Create Setup Key
|
||||
</Button>
|
||||
</SetupKeyModal>
|
||||
}
|
||||
learnMore={
|
||||
<>
|
||||
@@ -100,12 +109,14 @@ export default function SetupKeysTable({ setupKeys, isLoading }: Props) {
|
||||
rightSide={() => (
|
||||
<>
|
||||
{setupKeys && setupKeys?.length > 0 && (
|
||||
<SetupKeyModal>
|
||||
<Button variant={"primary"} className={"ml-auto"}>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
className={"ml-auto"}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<PlusCircle size={16} />
|
||||
Create Setup Key
|
||||
</Button>
|
||||
</SetupKeyModal>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
@@ -156,5 +167,6 @@ export default function SetupKeysTable({ setupKeys, isLoading }: Props) {
|
||||
</>
|
||||
)}
|
||||
</DataTable>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user