Skip to content

Commit

Permalink
Data source / vaults navigation (#6745)
Browse files Browse the repository at this point in the history
* Bump sparkle to 0.2.204

* Add feature flag + data sources tab

* Add data sources / vaults navigation

* Unfold ancestor items

* Address comments from review

* Address comments from review
  • Loading branch information
flvndvd authored and albandum committed Aug 28, 2024
1 parent ec29a1e commit 2f04f6c
Show file tree
Hide file tree
Showing 11 changed files with 661 additions and 6 deletions.
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

0 comments on commit 2f04f6c

Please sign in to comment.