Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Data source / vaults navigation #6745

Merged
merged 6 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion front/components/navigation/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
BookOpenIcon,
ChatBubbleLeftRightIcon,
CloudArrowLeftRightIcon,
Cog6ToothIcon,
Expand All @@ -22,7 +23,11 @@ import { UsersIcon } from "@heroicons/react/20/solid";
* ones for the topNavigation (same across the whole app) and for the subNavigation which appears in
* some section of the app in the AppLayout navigation panel.
*/
export type TopNavigationId = "conversations" | "assistants" | "admin";
export type TopNavigationId =
| "conversations"
| "assistants"
| "admin"
| "data_sources";

export type SubNavigationConversationsId =
| "conversation"
Expand Down Expand Up @@ -118,6 +123,19 @@ export const getTopNavigationTabs = (owner: WorkspaceType) => {
sizing: "expand",
});
}

if (owner.flags.includes("data_vaults_feature")) {
nav.push({
id: "data_sources",
label: "Data sources",
icon: BookOpenIcon,
href: `/w/${owner.sId}/data-sources/vaults`,
isCurrent: (currentRoute: string) =>
currentRoute.startsWith("/w/[wId]/data-sources/vaults/"),
sizing: "expand",
});
}

if (isAdmin(owner)) {
nav.push({
id: "settings",
Expand Down
35 changes: 35 additions & 0 deletions front/components/vaults/VaultLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { SubscriptionType, WorkspaceType } from "@dust-tt/types";
import type React from "react";

import RootLayout from "@app/components/app/RootLayout";
import AppLayout from "@app/components/sparkle/AppLayout";
import VaultSideBarMenu from "@app/components/vaults/VaultSideBarMenu";

export interface VaultLayoutProps {
gaTrackingId: string;
owner: WorkspaceType;
subscription: SubscriptionType;
}

export function VaultLayout({
children,
pageProps,
}: {
children: React.ReactNode;
pageProps: VaultLayoutProps;
}) {
const { gaTrackingId, owner, subscription } = pageProps;

return (
<RootLayout>
<AppLayout
subscription={subscription}
owner={owner}
gaTrackingId={gaTrackingId}
navChildren={<VaultSideBarMenu owner={owner} />}
>
{children}
</AppLayout>
</RootLayout>
);
}
291 changes: 291 additions & 0 deletions front/components/vaults/VaultSideBarMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
import {
CloudArrowLeftRightIcon,
CommandLineIcon,
FolderIcon,
GlobeAltIcon,
Item,
LockIcon,
PlanetIcon,
Tree,
} from "@dust-tt/sparkle";
import type {
DataSourceOrViewInfo,
LightWorkspaceType,
VaultType,
} from "@dust-tt/types";
import { assertNever, DATA_SOURCE_OR_VIEW_CATEGORIES } from "@dust-tt/types";
import { groupBy } from "lodash";
import { useRouter } from "next/router";
import type { ReactElement } from "react";
import { Fragment, useState } from "react";

import { CONNECTOR_CONFIGURATIONS } from "@app/lib/connector_providers";
import {
useVaultDataSourceOrViews,
useVaultInfo,
useVaults,
} from "@app/lib/swr";

interface VaultSideBarMenuProps {
owner: LightWorkspaceType;
}

const VAULTS_SORT_ORDER = ["system", "global", "regular"];

export default function VaultSideBarMenu({ owner }: VaultSideBarMenuProps) {
const { vaults, isVaultsLoading } = useVaults({ workspaceId: owner.sId });

if (!vaults || isVaultsLoading) {
return <></>;
}

// Group by kind and sort.
const groupedVaults = groupBy(vaults, (vault) => vault.kind);
const sortedGroupedVaults = VAULTS_SORT_ORDER.map(
(kind) => groupedVaults[kind] || []
);

return (
<div className="flex flex-col px-3">
<Item.List>
{sortedGroupedVaults.map((vaults, index) => {
if (vaults.length === 0) {
return null;
}

const [vault] = vaults;
const sectionLabel = getSectionLabel(vault);

return (
<Fragment key={`vault-section-${index}`}>
<Item.SectionHeader label={sectionLabel} key={vault.sId} />
{renderVaultItems(vaults, owner)}
</Fragment>
);
})}
</Item.List>
</div>
);
}

// Function to render vault items.
const renderVaultItems = (vaults: VaultType[], owner: LightWorkspaceType) =>
vaults.map((vault) => (
<Fragment key={`vault-${vault.sId}`}>
{vault.kind === "system" ? (
<SystemVaultMenu />
) : (
<VaultMenuItem owner={owner} vault={vault} />
)}
</Fragment>
));

const getSectionLabel = (vault: VaultType) => {
switch (vault.kind) {
case "global":
return "SHARED";

case "regular":
return "PRIVATE";

case "system":
return "SYSTEM";

default:
assertNever(vault.kind);
}
};

const RootItemIconWrapper = (
IconComponent: React.ComponentType<React.SVGProps<SVGSVGElement>>
) => {
return <IconComponent className="text-brand" />;
};

const SubItemIconItemWrapper = (
IconComponent: React.ComponentType<React.SVGProps<SVGSVGElement>>
) => {
return <IconComponent className="text-element-700" />;
};

// System vault.

const SystemVaultMenu = () => {
// TODO(Groups UI) Implement system vault menu.
return <></>;
};

// Global + regular vaults.

const VaultMenuItem = ({
owner,
vault,
}: {
owner: LightWorkspaceType;
vault: VaultType;
}) => {
const router = useRouter();

const vaultPath = `/w/${owner.sId}/data-sources/vaults/${vault.sId}`;
const isAncestorToCurrentPage = router.asPath.includes(vaultPath);

// Unfold the vault if it's an ancestor of the current page.
const [isExpanded, setIsExpanded] = useState(isAncestorToCurrentPage);

const { vaultInfo, isVaultInfoLoading } = useVaultInfo({
workspaceId: owner.sId,
vaultId: vault.sId,
disabled: !isExpanded,
});

return (
<Tree.Item
label={vault.kind === "global" ? "Company Data" : vault.name}
collapsed={!isExpanded}
onItemClick={() => router.push(vaultPath)}
isSelected={router.asPath === vaultPath}
onChevronClick={() => setIsExpanded(!isExpanded)}
visual={
vault.kind === "global"
? RootItemIconWrapper(PlanetIcon)
: RootItemIconWrapper(LockIcon)
}
size="md"
areActionsFading={false}
>
{isExpanded && (
<Tree isLoading={isVaultInfoLoading}>
{vaultInfo?.categories &&
DATA_SOURCE_OR_VIEW_CATEGORIES.map(
(c) =>
vaultInfo.categories[c] && (
<VaultCategoryItem
key={c}
category={c}
owner={owner}
vault={vault}
/>
)
)}
</Tree>
)}
</Tree.Item>
);
};

const DATA_SOURCE_OR_VIEW_SUB_ITEMS: {
[key: string]: {
label: string;
icon: ReactElement<{
className?: string;
}>;
dataSourceOrView: "data_sources" | "data_source_views";
};
} = {
managed: {
label: "Connected Data",
icon: SubItemIconItemWrapper(CloudArrowLeftRightIcon),
dataSourceOrView: "data_source_views",
},
files: {
label: "Files",
icon: SubItemIconItemWrapper(FolderIcon),
dataSourceOrView: "data_sources",
},
webfolder: {
label: "Websites",
icon: SubItemIconItemWrapper(GlobeAltIcon),
dataSourceOrView: "data_sources",
},
apps: {
label: "Apps",
icon: SubItemIconItemWrapper(CommandLineIcon),
dataSourceOrView: "data_sources",
},
};

const VaultDataSourceOrViewItem = ({
owner,
vault,
item,
}: {
owner: LightWorkspaceType;
vault: VaultType;
item: DataSourceOrViewInfo;
}): ReactElement => {
const router = useRouter();
const configuration = item.connectorProvider
? CONNECTOR_CONFIGURATIONS[item.connectorProvider]
: null;

const LogoComponent = configuration?.logoComponent ?? FolderIcon;
const label = configuration?.name ?? item.name;
const viewType =
DATA_SOURCE_OR_VIEW_SUB_ITEMS[item.category].dataSourceOrView;
const dataSourceOrViewPath = `/w/${owner.sId}/data-sources/vaults/${vault.sId}/categories/${item.category}/${viewType}/${item.sId}`;

return (
<Tree.Item
type="leaf"
isSelected={router.asPath === dataSourceOrViewPath}
onItemClick={() => router.push(dataSourceOrViewPath)}
label={label}
visual={SubItemIconItemWrapper(LogoComponent)}
areActionsFading={false}
/>
);
};

const VaultCategoryItem = ({
owner,
vault,
category,
}: {
owner: LightWorkspaceType;
vault: VaultType;
category: string;
}) => {
const router = useRouter();

const vaultCategoryPath = `/w/${owner.sId}/data-sources/vaults/${vault.sId}/categories/${category}`;
const isAncestorToCurrentPage = router.asPath.includes(vaultCategoryPath);

// Unfold the vault's category if it's an ancestor of the current page.
const [isExpanded, setIsExpanded] = useState(isAncestorToCurrentPage);

const categoryDetails = DATA_SOURCE_OR_VIEW_SUB_ITEMS[category];
const { isVaultDataSourceOrViewsLoading, vaultDataSourceOrViews } =
useVaultDataSourceOrViews({
workspaceId: owner.sId,
vaultId: vault.sId,
category,
type: categoryDetails.dataSourceOrView,
disabled: !isExpanded,
});

return (
<Tree.Item
label={categoryDetails.label}
collapsed={!isExpanded}
onItemClick={() => router.push(vaultCategoryPath)}
isSelected={router.asPath === vaultCategoryPath}
onChevronClick={() => setIsExpanded(!isExpanded)}
visual={categoryDetails.icon}
areActionsFading={false}
>
{isExpanded && (
<Tree isLoading={isVaultDataSourceOrViewsLoading}>
{vaultDataSourceOrViews &&
vaultDataSourceOrViews.map((ds) => (
<VaultDataSourceOrViewItem
key={ds.sId}
owner={owner}
vault={vault}
item={ds}
/>
))}
</Tree>
)}
</Tree.Item>
);
};
8 changes: 4 additions & 4 deletions front/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion front/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"@amplitude/analytics-browser": "^2.5.2",
"@amplitude/analytics-node": "^1.3.5",
"@auth0/nextjs-auth0": "^3.5.0",
"@dust-tt/sparkle": "^0.2.203",
"@dust-tt/sparkle": "^0.2.204",
"@dust-tt/types": "file:../types",
"@emoji-mart/data": "^1.1.2",
"@emoji-mart/react": "^1.1.1",
Expand Down
Loading
Loading