organization page complete with quick actions sidebar

This commit is contained in:
shatfield4
2023-12-19 17:06:48 -08:00
parent db50cb436f
commit d12e548ac8
5 changed files with 311 additions and 91 deletions
+14 -20
View File
@@ -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>
);
}
+3
View File
@@ -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>
+11 -7
View File
@@ -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>
</>
);