mirror of
https://github.com/Mintplex-Labs/vector-admin.git
synced 2026-07-19 21:23:38 -04:00
organization page complete with quick actions sidebar
This commit is contained in:
@@ -14,10 +14,17 @@ export default function Header(props: {
|
||||
sidebarOpen: string | boolean | undefined;
|
||||
setSidebarOpen: (arg0: boolean) => void;
|
||||
extendedItems?: any;
|
||||
quickActions: boolean;
|
||||
}) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
if (!props.entity) return null;
|
||||
const { entity, property, nameProp, extendedItems = <></> } = props;
|
||||
const {
|
||||
entity,
|
||||
property,
|
||||
nameProp,
|
||||
extendedItems = <></>,
|
||||
quickActions = false,
|
||||
} = props;
|
||||
|
||||
const handleCopy = () => {
|
||||
window.navigator.clipboard.writeText(entity[property]);
|
||||
@@ -124,25 +131,12 @@ export default function Header(props: {
|
||||
// </div>
|
||||
// </div>
|
||||
// </header>
|
||||
<header className="mr-26 flex h-[76px] w-full rounded-t-xl bg-main">
|
||||
<div className="flex w-full justify-between p-4">
|
||||
{/* <div className="w-fit rounded-xl border-2 border-white/20 px-5 py-2 text-sky-400">
|
||||
Organization {'>'} Workspace 1 {'>'} Document 1
|
||||
</div>
|
||||
<div className="flex gap-x-3">
|
||||
<button className="inline-flex h-11 w-[74px] flex-col items-center justify-center gap-2.5 rounded-lg bg-white bg-opacity-10 px-5 py-2.5 transition-all duration-300 hover:bg-opacity-5">
|
||||
<div className="h-[25.53px] w-11 text-center font-['Satoshi'] text-base font-bold text-white">
|
||||
Clone
|
||||
</div>
|
||||
</button>
|
||||
<button className="inline-flex h-11 w-[74px] flex-col items-center justify-center gap-2.5 rounded-lg border border-white border-opacity-20 px-3.5 py-2.5 transition-all duration-300 hover:bg-red-500">
|
||||
<div className="h-[25.53px] w-[59px] text-center font-['Satoshi'] text-base font-bold text-white">
|
||||
Delete
|
||||
</div>
|
||||
</button>
|
||||
</div> */}
|
||||
{extendedItems}
|
||||
</div>
|
||||
<header
|
||||
className={`${
|
||||
quickActions ? 'mr-[235px]' : 'mr-[104px]'
|
||||
} flex h-[76px] w-full rounded-t-xl bg-main`}
|
||||
>
|
||||
<div className="flex w-full justify-between p-4">{extendedItems}</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ interface DefaultLayoutProps {
|
||||
children: ReactNode;
|
||||
hasMoreWorkspaces?: boolean;
|
||||
loadMoreWorkspaces?: VoidFunction;
|
||||
hasQuickActions?: boolean;
|
||||
}
|
||||
|
||||
const AppLayout = ({
|
||||
@@ -26,6 +27,7 @@ const AppLayout = ({
|
||||
children,
|
||||
hasMoreWorkspaces,
|
||||
loadMoreWorkspaces,
|
||||
hasQuickActions = false,
|
||||
}: DefaultLayoutProps) => {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
|
||||
@@ -52,6 +54,7 @@ const AppLayout = ({
|
||||
sidebarOpen={sidebarOpen}
|
||||
setSidebarOpen={setSidebarOpen}
|
||||
extendedItems={headerExtendedItems}
|
||||
quickActions={hasQuickActions}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
import {
|
||||
CaretDown,
|
||||
Key,
|
||||
MagnifyingGlass,
|
||||
Plus,
|
||||
SpinnerGap,
|
||||
Toolbox,
|
||||
User,
|
||||
} from '@phosphor-icons/react';
|
||||
import CreateWorkspaceModal from '../WorkspacesList/CreateWorkspaceModal';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { NavLink, useParams } from 'react-router-dom';
|
||||
import paths from '../../../utils/paths';
|
||||
import truncate from 'truncate';
|
||||
import InfiniteScroll from 'react-infinite-scroll-component';
|
||||
import { debounce } from 'lodash';
|
||||
import Organization from '../../../models/organization';
|
||||
|
||||
const debouncedSearch = debounce(
|
||||
async (searchTerm, setResults, setIsSearching, slug) => {
|
||||
if (!slug) return;
|
||||
setIsSearching(true);
|
||||
|
||||
const { workspacesResults = [] } = await Organization.searchWorkspaces(
|
||||
slug,
|
||||
1, // Page 1
|
||||
30, // 30 results per page
|
||||
searchTerm
|
||||
);
|
||||
|
||||
setResults(workspacesResults);
|
||||
setIsSearching(false);
|
||||
},
|
||||
500
|
||||
);
|
||||
|
||||
export default function QuickActionsSidebar({
|
||||
knownConnector,
|
||||
organization,
|
||||
workspaces,
|
||||
totalWorkspaces = 0,
|
||||
loadMoreWorkspaces,
|
||||
hasMoreWorkspaces,
|
||||
}: {
|
||||
knownConnector: any;
|
||||
organization: any;
|
||||
workspaces: any[];
|
||||
totalWorkspaces?: number;
|
||||
loadMoreWorkspaces?: VoidFunction;
|
||||
hasMoreWorkspaces: boolean;
|
||||
}) {
|
||||
const { slug } = useParams();
|
||||
const [quickActionsOpen, setQuickActionsOpen] = useState(true);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [searchResults, setSearchResults] = useState([]);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
|
||||
const renderWorkspaceItem = (workspace: any) => (
|
||||
<WorkspaceItem key={workspace.id} workspace={workspace} slug={slug} />
|
||||
);
|
||||
|
||||
const loadMoreWorkspacesAndScrollToBottom = async () => {
|
||||
loadMoreWorkspaces?.();
|
||||
const organizationList = document.getElementById('organization-list');
|
||||
if (organizationList) {
|
||||
organizationList.scrollTop = organizationList.scrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (searchTerm !== '') {
|
||||
setIsSearching(true);
|
||||
debouncedSearch(searchTerm, setSearchResults, setIsSearching, slug);
|
||||
} else {
|
||||
setSearchResults(workspaces);
|
||||
setIsSearching(false);
|
||||
}
|
||||
}, [searchTerm, slug]);
|
||||
|
||||
return (
|
||||
<div className="-mt-14 w-[217px]">
|
||||
<button
|
||||
onClick={() => setQuickActionsOpen(!quickActionsOpen)}
|
||||
className="w-full text-white/80"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="font-['Plus Jakarta Sans'] whitespace-nowrap text-xs font-semibold uppercase leading-tight tracking-wide text-white text-opacity-80">
|
||||
quick actions
|
||||
</div>
|
||||
<div
|
||||
className={`${
|
||||
quickActionsOpen ? '' : 'rotate-180'
|
||||
} transition-all duration-300`}
|
||||
>
|
||||
<CaretDown size={18} weight="bold" />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div
|
||||
className={`${
|
||||
quickActionsOpen
|
||||
? 'slide-down mb-4 mt-4 transition-all duration-300'
|
||||
: 'slide-up'
|
||||
}`}
|
||||
style={{
|
||||
animationDuration: '0.15s',
|
||||
}}
|
||||
>
|
||||
<NavLink to={paths.toolsHome(organization)}>
|
||||
<div className="mt-5 flex items-center gap-x-2 text-white hover:cursor-pointer hover:text-sky-400 hover:underline">
|
||||
<Toolbox size={18} weight="bold" />
|
||||
<div className="text-sm font-medium">Tools</div>
|
||||
</div>
|
||||
</NavLink>
|
||||
|
||||
<NavLink to={paths.users()}>
|
||||
<div className="mt-5 flex items-center gap-x-2 text-white hover:cursor-pointer hover:text-sky-400 hover:underline">
|
||||
<User size={18} weight="bold" />
|
||||
<div className="text-sm font-medium">Add User</div>
|
||||
</div>
|
||||
</NavLink>
|
||||
|
||||
<NavLink to={paths.settings()}>
|
||||
<div className="mt-5 flex items-center gap-x-2 text-white hover:cursor-pointer hover:text-sky-400 hover:underline">
|
||||
<Key size={18} weight="bold" />
|
||||
<div className="text-sm font-medium">OpenAI Key</div>
|
||||
</div>
|
||||
</NavLink>
|
||||
|
||||
<NavLink to={paths.jobs(organization)}>
|
||||
<div className="mt-5 flex items-center gap-x-2 text-white hover:cursor-pointer hover:text-sky-400 hover:underline">
|
||||
<SpinnerGap size={18} weight="bold" />
|
||||
<div className="text-sm font-medium">Background Jobs</div>
|
||||
</div>
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="mb-4 mt-8"
|
||||
style={{
|
||||
animationDuration: '0.15s',
|
||||
}}
|
||||
>
|
||||
<div className="mb-3.5 flex items-center justify-between">
|
||||
<div className="flex w-full items-center gap-x-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="12"
|
||||
viewBox="0 0 16 12"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M7.20098 8.16845H13.601C14.0426 8.16845 14.401 8.52685 14.401 8.96845C14.401 9.41005 14.0426 9.76845 13.601 9.76845H7.20098C6.75938 9.76845 6.40098 9.41005 6.40098 8.96845C6.40098 8.52685 6.75938 8.16845 7.20098 8.16845ZM7.20098 2.56845H13.601C14.0426 2.56845 14.401 2.92685 14.401 3.36845C14.401 3.81005 14.0426 4.16845 13.601 4.16845H7.20098C6.75938 4.16845 6.40098 3.81005 6.40098 3.36845C6.40098 2.92685 6.75938 2.56845 7.20098 2.56845ZM4.80098 4.16845C4.80098 5.05245 5.51698 5.76845 6.40098 5.76845H14.401C15.285 5.76845 16.001 5.05245 16.001 4.16845V2.56845C16.001 1.68445 15.285 0.96845 14.401 0.96845H6.40098C5.51698 0.96845 4.80098 1.68445 4.80098 2.56845H1.60098L1.60098 0.854024H0.000976562V8.16767C0.000976562 9.05167 0.717782 9.76845 1.60178 9.76845H1.77617H4.80098C4.80098 10.6525 5.51698 11.3685 6.40098 11.3685H14.401C15.285 11.3685 16.001 10.6525 16.001 9.76845V8.16845C16.001 7.28445 15.285 6.56845 14.401 6.56845H6.40098C5.51698 6.56845 4.80098 7.28445 4.80098 8.16845H2.39778C1.95778 8.16845 1.60098 7.81158 1.60098 7.37158V4.16845H4.80098Z"
|
||||
fill="rgb(56 189 248)"
|
||||
/>
|
||||
</svg>
|
||||
<div className="text-xs font-medium uppercase tracking-widest text-sky-400">
|
||||
Workspaces
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
document.getElementById('workspace-creation-modal')?.showModal();
|
||||
}}
|
||||
>
|
||||
<Plus className="text-sky-400" size={17} weight="bold" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center rounded-full bg-main-2 p-2">
|
||||
<MagnifyingGlass
|
||||
className="mx-1 h-4 w-4 text-white/60"
|
||||
weight="bold"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search"
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="border-none bg-transparent text-sm text-white/60 placeholder-white/60 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isSearching ? (
|
||||
<LoadingWorkspaceItem />
|
||||
) : searchTerm !== '' && searchResults.length > 0 ? (
|
||||
<div className="mt-2 max-h-[180px] overflow-y-auto">
|
||||
{searchResults.map((workspace, idx) => (
|
||||
<WorkspaceItem key={idx} workspace={workspace} slug={slug} />
|
||||
))}
|
||||
</div>
|
||||
) : searchTerm !== '' && searchResults.length === 0 ? (
|
||||
<div className="mt-2">
|
||||
<div className="flex w-full items-center justify-center rounded-sm text-xs text-white/60">
|
||||
<p className="p-1">No results found.</p>
|
||||
</div>
|
||||
</div>
|
||||
) : workspaces.length > 0 ? (
|
||||
<div className="mt-2">
|
||||
<InfiniteScroll
|
||||
dataLength={workspaces.length}
|
||||
scrollableTarget="organization-list"
|
||||
height={workspaces.length > 5 ? 180 : workspaces.length * 50}
|
||||
next={loadMoreWorkspacesAndScrollToBottom}
|
||||
hasMore={hasMoreWorkspaces}
|
||||
loader={<LoadingWorkspaceItem />}
|
||||
>
|
||||
{workspaces.map(renderWorkspaceItem)}
|
||||
</InfiniteScroll>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-2">
|
||||
<div className="flex w-48 items-center justify-center rounded-sm text-xs text-white/60">
|
||||
<p className="p-1">
|
||||
No workspaces,{' '}
|
||||
<button
|
||||
onClick={() => {
|
||||
document
|
||||
.getElementById('workspace-creation-modal')
|
||||
?.showModal();
|
||||
}}
|
||||
className="italic underline hover:cursor-pointer"
|
||||
>
|
||||
create
|
||||
</button>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<CreateWorkspaceModal organization={organization} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function WorkspaceItem({ workspace, slug }: any) {
|
||||
return (
|
||||
<div className="mt-2">
|
||||
<NavLink
|
||||
to={paths.workspace(slug, workspace.slug)}
|
||||
className="inline-flex h-9 w-full items-center justify-start gap-3.5 rounded-lg border border-sky-400 p-2.5"
|
||||
>
|
||||
<div className="h-[22.43px] w-24 whitespace-nowrap font-['Satoshi'] text-base font-medium leading-snug text-sky-400">
|
||||
{truncate(workspace.name, 15)}
|
||||
</div>
|
||||
</NavLink>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LoadingWorkspaceItem() {
|
||||
return (
|
||||
<div className="mt-2">
|
||||
<div className="flex w-full animate-pulse items-center justify-center rounded-sm text-xs text-white/60">
|
||||
<p className="p-1">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,16 @@
|
||||
import { memo, useState, useEffect } from 'react';
|
||||
import PreLoader from '../../../components/Preloader';
|
||||
import { humanFileSize, nFormatter } from '../../../utils/numbers';
|
||||
import moment from 'moment';
|
||||
import Organization from '../../../models/organization';
|
||||
import pluralize from 'pluralize';
|
||||
import Workspace from '../../../models/workspace';
|
||||
|
||||
const Statistics = ({ organization }: { organization: any }) => {
|
||||
const Statistics = ({
|
||||
organization,
|
||||
workspaces,
|
||||
}: {
|
||||
organization: any;
|
||||
workspaces: any;
|
||||
}) => {
|
||||
const [documents, setDocuments] = useState({
|
||||
status: 'loading',
|
||||
value: 0,
|
||||
@@ -18,6 +23,10 @@ const Statistics = ({ organization }: { organization: any }) => {
|
||||
status: 'loading',
|
||||
value: 0,
|
||||
});
|
||||
const [dimensions, setDimensions] = useState({
|
||||
status: 'loading',
|
||||
value: '-',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
async function collectStats() {
|
||||
@@ -32,70 +41,17 @@ const Statistics = ({ organization }: { organization: any }) => {
|
||||
Organization.stats(organization.slug, 'cache-size').then((json) => {
|
||||
setCache({ status: 'complete', value: json.value });
|
||||
});
|
||||
Workspace.stats(organization.slug, workspaces[0].slug, 'dimensions').then(
|
||||
(json) => {
|
||||
setDimensions({ status: 'complete', value: json.value });
|
||||
}
|
||||
);
|
||||
}
|
||||
collectStats();
|
||||
}, [organization?.slug]);
|
||||
}, [organization?.slug, workspaces[0]?.slug]);
|
||||
|
||||
return (
|
||||
// <div className="col-span-12 rounded-md border border-stroke bg-white p-7.5 shadow-default dark:border-strokedark dark:bg-boxdark">
|
||||
// <div className="grid grid-cols-1 gap-5 sm:grid-cols-2 xl:grid-cols-4 xl:gap-0">
|
||||
// <div className="flex items-center justify-center gap-2 border-b border-stroke pb-5 dark:border-strokedark xl:border-b-0 xl:border-r xl:pb-0">
|
||||
// {documents.status === 'loading' ? (
|
||||
// <PreLoader />
|
||||
// ) : (
|
||||
// <div className="flex flex-col items-center">
|
||||
// <h4 className="mb-0.5 text-xl font-bold text-black dark:text-white md:text-title-lg">
|
||||
// {nFormatter(documents.value)}
|
||||
// </h4>
|
||||
// <p className="text-sm font-medium">
|
||||
// {pluralize('Document', documents.value)}
|
||||
// </p>
|
||||
// </div>
|
||||
// )}
|
||||
// </div>
|
||||
|
||||
// <div className="flex items-center justify-center gap-2 border-b border-stroke pb-5 dark:border-strokedark xl:border-b-0 xl:border-r xl:pb-0">
|
||||
// {vectors.status === 'loading' ? (
|
||||
// <PreLoader />
|
||||
// ) : (
|
||||
// <div className="flex flex-col items-center">
|
||||
// <h4 className="mb-0.5 text-xl font-bold text-black dark:text-white md:text-title-lg">
|
||||
// {nFormatter(vectors.value)}
|
||||
// </h4>
|
||||
// <p className="text-sm font-medium">
|
||||
// {pluralize('Vector', vectors.value)}
|
||||
// </p>
|
||||
// </div>
|
||||
// )}
|
||||
// </div>
|
||||
|
||||
// <div className="flex items-center justify-center gap-2 border-b border-stroke pb-5 dark:border-strokedark sm:border-b-0 sm:pb-0 xl:border-r">
|
||||
// {cache.status === 'loading' ? (
|
||||
// <PreLoader />
|
||||
// ) : (
|
||||
// <div className="flex flex-col items-center">
|
||||
// <h4 className="mb-0.5 text-xl font-bold text-black dark:text-white md:text-title-lg">
|
||||
// {humanFileSize(cache.value)}
|
||||
// </h4>
|
||||
// <p className="text-sm font-medium">Vector Cache (MB)</p>
|
||||
// </div>
|
||||
// )}
|
||||
// </div>
|
||||
|
||||
// <div className="flex items-center justify-center gap-2">
|
||||
// <div className="flex flex-col items-center">
|
||||
// <h4 className="mb-0.5 text-xl font-bold text-black dark:text-white md:text-title-lg">
|
||||
// {organization?.lastUpdated
|
||||
// ? moment(organization.lastUpdated).fromNow()
|
||||
// : moment(organization.createdAt).fromNow()}
|
||||
// </h4>
|
||||
// <p className="text-sm font-medium">Last Modified</p>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
|
||||
<div className="-mt-4 flex w-full items-center">
|
||||
<div className="-mt-6 flex w-full items-center pr-7">
|
||||
<div className=" ml-4 flex items-center gap-x-6">
|
||||
<div className="flex items-center gap-x-1">
|
||||
<span className="font-['Plus Jakarta Sans'] text-sm font-bold uppercase leading-[18px] tracking-wide text-white">
|
||||
@@ -125,7 +81,8 @@ const Statistics = ({ organization }: { organization: any }) => {
|
||||
</span>
|
||||
</span>
|
||||
<span className="font-jetbrains uppercase text-white">
|
||||
Dimensions: <span className=" font-jetbrainsbold">{10}</span>
|
||||
Dimensions:{' '}
|
||||
<span className=" font-jetbrainsbold">{dimensions.value}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,9 +5,8 @@ import DefaultLayout from '../../layout/DefaultLayout';
|
||||
import User from '../../models/user';
|
||||
import paths from '../../utils/paths';
|
||||
import AppLayout from '../../layout/AppLayout';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { NavLink, useParams } from 'react-router-dom';
|
||||
import Statistics from './Statistics';
|
||||
import WorkspacesList from './WorkspacesList';
|
||||
import DocumentsList from './DocumentsList';
|
||||
import Organization from '../../models/organization';
|
||||
import ApiKeyCard from './ApiKey';
|
||||
@@ -21,6 +20,8 @@ import qDrantLogo from '../../images/vectordbs/qdrant.png';
|
||||
import WeaviateLogo from '../../images/vectordbs/weaviate.png';
|
||||
import { GearSix } from '@phosphor-icons/react';
|
||||
import { titleCase } from 'title-case';
|
||||
import CreateWorkspaceModal from './WorkspacesList/CreateWorkspaceModal';
|
||||
import QuickActionsSidebar from './QuickActionSidebar';
|
||||
|
||||
export default function Dashboard() {
|
||||
const { slug } = useParams();
|
||||
@@ -114,6 +115,7 @@ export default function Dashboard() {
|
||||
deleteWorkspace={() => {}}
|
||||
/>
|
||||
}
|
||||
hasQuickActions={true}
|
||||
>
|
||||
{!!organization && (
|
||||
<div className="mb-4 grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-6 xl:grid-cols-4 2xl:gap-7.5">
|
||||
@@ -134,7 +136,7 @@ export default function Dashboard() {
|
||||
{/* <ApiKeyCard organization={organization} /> */}
|
||||
</div>
|
||||
)}
|
||||
<Statistics organization={organization} />
|
||||
<Statistics organization={organization} workspaces={workspaces} />
|
||||
<div className="mt-4 flex w-full">
|
||||
<div className="mr-6.5 w-full">
|
||||
<DocumentsList
|
||||
@@ -143,11 +145,12 @@ export default function Dashboard() {
|
||||
workspaces={workspaces}
|
||||
/>
|
||||
</div>
|
||||
<WorkspacesList
|
||||
<QuickActionsSidebar
|
||||
knownConnector={connector}
|
||||
organization={organization}
|
||||
workspaces={workspaces}
|
||||
totalWorkspaces={totalWorkspaces}
|
||||
loadMoreWorkspaces={fetchWorkspaces}
|
||||
/>
|
||||
</div>
|
||||
</AppLayout>
|
||||
@@ -208,12 +211,13 @@ function OrganizationHeader({
|
||||
Sync
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => alert('Settings clicked.')}
|
||||
|
||||
<NavLink
|
||||
className="flex h-11 w-11 items-center justify-center rounded-lg border-2 border-white border-opacity-20 text-white transition-all duration-300 hover:bg-opacity-5"
|
||||
to={paths.organizationSettings(organization)}
|
||||
>
|
||||
<GearSix size={28} />
|
||||
</button>
|
||||
</NavLink>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user