mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-04-02 08:58:11 +02:00
Add some additional FE components
This commit is contained in:
parent
f045bbed70
commit
fb1fbbee5c
@ -1,292 +1,9 @@
|
||||
import { Header } from "@/components/Header";
|
||||
import { Sidebar } from "@/components/admin/connectors/Sidebar";
|
||||
import {
|
||||
NotebookIcon,
|
||||
GithubIcon,
|
||||
GlobeIcon,
|
||||
GoogleDriveIcon,
|
||||
SlackIcon,
|
||||
KeyIcon,
|
||||
BookstackIcon,
|
||||
ConfluenceIcon,
|
||||
GuruIcon,
|
||||
GongIcon,
|
||||
FileIcon,
|
||||
JiraIcon,
|
||||
SlabIcon,
|
||||
NotionIcon,
|
||||
ZulipIcon,
|
||||
ProductboardIcon,
|
||||
LinearIcon,
|
||||
UsersIcon,
|
||||
ThumbsUpIcon,
|
||||
HubSpotIcon,
|
||||
BookmarkIcon,
|
||||
CPUIcon,
|
||||
} from "@/components/icons/icons";
|
||||
import { getAuthDisabledSS, getCurrentUserSS } from "@/lib/userSS";
|
||||
import { redirect } from "next/navigation";
|
||||
import { Layout } from "@/components/admin/Layout";
|
||||
|
||||
export default async function AdminLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const [authDisabled, user] = await Promise.all([
|
||||
getAuthDisabledSS(),
|
||||
getCurrentUserSS(),
|
||||
]);
|
||||
|
||||
if (!authDisabled) {
|
||||
if (!user) {
|
||||
return redirect("/auth/login");
|
||||
}
|
||||
if (user.role !== "admin") {
|
||||
return redirect("/");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header user={user} />
|
||||
<div className="bg-gray-900 pt-8 pb-8 flex">
|
||||
<Sidebar
|
||||
title="Connector"
|
||||
collections={[
|
||||
{
|
||||
name: "Indexing",
|
||||
items: [
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<NotebookIcon size={18} />
|
||||
<div className="ml-1">Status</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/indexing/status",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Connector Settings",
|
||||
items: [
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<SlackIcon size={16} />
|
||||
<div className="ml-1">Slack</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/connectors/slack",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<GithubIcon size={16} />
|
||||
<div className="ml-1">Github</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/connectors/github",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<GoogleDriveIcon size={16} />
|
||||
<div className="ml-1">Google Drive</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/connectors/google-drive",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<ConfluenceIcon size={16} />
|
||||
<div className="ml-1">Confluence</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/connectors/confluence",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<JiraIcon size={16} />
|
||||
<div className="ml-1">Jira</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/connectors/jira",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<LinearIcon size={16} />
|
||||
<div className="ml-1">Linear</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/connectors/linear",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<ProductboardIcon size={16} />
|
||||
<div className="ml-1">Productboard</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/connectors/productboard",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<SlabIcon size={16} />
|
||||
<div className="ml-1">Slab</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/connectors/slab",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<NotionIcon size={16} />
|
||||
<div className="ml-1">Notion</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/connectors/notion",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<GuruIcon size={16} />
|
||||
<div className="ml-1">Guru</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/connectors/guru",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<GongIcon size={16} />
|
||||
<div className="ml-1">Gong</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/connectors/gong",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<BookstackIcon size={16} />
|
||||
<div className="ml-1">BookStack</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/connectors/bookstack",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<ZulipIcon size={16} />
|
||||
<div className="ml-1">Zulip</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/connectors/zulip",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<GlobeIcon size={16} />
|
||||
<div className="ml-1">Web</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/connectors/web",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<FileIcon size={16} />
|
||||
<div className="ml-1">File</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/connectors/file",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<HubSpotIcon size={16} />
|
||||
<div className="ml-1">HubSpot</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/connectors/hubspot",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Keys",
|
||||
items: [
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<KeyIcon size={18} />
|
||||
<div className="ml-1">OpenAI</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/keys/openai",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "User Management",
|
||||
items: [
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<UsersIcon size={18} />
|
||||
<div className="ml-1">Users</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/users",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Document Management",
|
||||
items: [
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<BookmarkIcon size={18} />
|
||||
<div className="ml-1">Document Sets</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/documents/sets",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<ThumbsUpIcon size={18} />
|
||||
<div className="ml-1">Feedback</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/documents/feedback",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Bots",
|
||||
items: [
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<CPUIcon size={18} />
|
||||
<div className="ml-1">Slack Bot</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/bot",
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<div className="px-12 min-h-screen bg-gray-900 text-gray-100 w-full">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return await Layout({ children });
|
||||
}
|
||||
|
280
web/src/components/Dropdown.tsx
Normal file
280
web/src/components/Dropdown.tsx
Normal file
@ -0,0 +1,280 @@
|
||||
import { ChangeEvent, FC, useEffect, useRef, useState } from "react";
|
||||
import { ChevronDownIcon } from "./icons/icons";
|
||||
|
||||
export interface Option {
|
||||
name: string;
|
||||
value: string;
|
||||
description?: string;
|
||||
metadata?: { [key: string]: any };
|
||||
}
|
||||
|
||||
interface DropdownProps {
|
||||
options: Option[];
|
||||
selected: string;
|
||||
onSelect: (selected: Option) => void;
|
||||
}
|
||||
|
||||
export const Dropdown: FC<DropdownProps> = ({
|
||||
options,
|
||||
selected,
|
||||
onSelect,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const selectedName = options.find(
|
||||
(option) => option.value === selected
|
||||
)?.name;
|
||||
|
||||
const handleSelect = (option: Option) => {
|
||||
onSelect(option);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative inline-block text-left w-full" ref={dropdownRef}>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
className={`inline-flex
|
||||
justify-center
|
||||
w-full
|
||||
px-4
|
||||
py-3
|
||||
text-sm
|
||||
bg-gray-700
|
||||
border
|
||||
border-gray-300
|
||||
rounded-md
|
||||
shadow-sm
|
||||
hover:bg-gray-700
|
||||
focus:ring focus:ring-offset-0 focus:ring-1 focus:ring-offset-gray-800 focus:ring-blue-800
|
||||
`}
|
||||
id="options-menu"
|
||||
aria-expanded="true"
|
||||
aria-haspopup="true"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
{selectedName ? <p>{selectedName}</p> : "Select an option..."}
|
||||
<ChevronDownIcon className="text-gray-400 my-auto ml-auto" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isOpen ? (
|
||||
<div className="origin-top-right absolute left-0 mt-3 w-full rounded-md shadow-lg bg-gray-700 border-2 border-gray-600">
|
||||
<div
|
||||
role="menu"
|
||||
aria-orientation="vertical"
|
||||
aria-labelledby="options-menu"
|
||||
>
|
||||
{options.map((option, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => handleSelect(option)}
|
||||
className={
|
||||
`w-full text-left block px-4 py-2.5 text-sm hover:bg-gray-800` +
|
||||
(index !== 0 ? " border-t-2 border-gray-600" : "")
|
||||
}
|
||||
role="menuitem"
|
||||
>
|
||||
<p className="font-medium">{option.name}</p>
|
||||
{option.description && (
|
||||
<div>
|
||||
<p className="text-xs text-gray-300">
|
||||
{option.description}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const StandardDropdownOption = ({
|
||||
index,
|
||||
option,
|
||||
handleSelect,
|
||||
}: {
|
||||
index: number;
|
||||
option: Option;
|
||||
handleSelect: (option: Option) => void;
|
||||
}) => {
|
||||
return (
|
||||
<button
|
||||
onClick={() => handleSelect(option)}
|
||||
className={`w-full text-left block px-4 py-2.5 text-sm hover:bg-gray-800 ${
|
||||
index !== 0 ? " border-t-2 border-gray-600" : ""
|
||||
}`}
|
||||
role="menuitem"
|
||||
>
|
||||
<p className="font-medium">{option.name}</p>
|
||||
{option.description && (
|
||||
<div>
|
||||
<p className="text-xs text-gray-300">{option.description}</p>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
interface MultiSelectDropdownProps {
|
||||
options: Option[];
|
||||
onSelect: (selected: Option) => void;
|
||||
itemComponent?: FC<{ option: Option }>;
|
||||
}
|
||||
|
||||
export const SearchMultiSelectDropdown: FC<MultiSelectDropdownProps> = ({
|
||||
options,
|
||||
onSelect,
|
||||
itemComponent,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleSelect = (option: Option) => {
|
||||
onSelect(option);
|
||||
setIsOpen(false);
|
||||
setSearchTerm(""); // Clear search term after selection
|
||||
};
|
||||
|
||||
const filteredOptions = options.filter((option) =>
|
||||
option.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative inline-block text-left w-full" ref={dropdownRef}>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
value={searchTerm}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (!searchTerm) {
|
||||
setIsOpen(true);
|
||||
}
|
||||
if (!e.target.value) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
setSearchTerm(e.target.value);
|
||||
}}
|
||||
onFocus={() => setIsOpen(true)}
|
||||
className={`inline-flex
|
||||
justify-between
|
||||
w-full
|
||||
px-4
|
||||
py-2
|
||||
text-sm
|
||||
bg-gray-700
|
||||
rounded-md
|
||||
shadow-sm
|
||||
focus:ring focus:ring-offset-0 focus:ring-1 focus:ring-offset-gray-800 focus:ring-blue-800`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className={`absolute top-0 right-0
|
||||
text-sm
|
||||
h-full px-2 border-l border-gray-800`}
|
||||
aria-expanded="true"
|
||||
aria-haspopup="true"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
<ChevronDownIcon className="text-gray-400 my-auto" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isOpen && (
|
||||
<div
|
||||
className={`origin-top-right
|
||||
absolute
|
||||
left-0
|
||||
mt-3
|
||||
w-full
|
||||
rounded-md
|
||||
shadow-lg
|
||||
bg-gray-700
|
||||
border-2
|
||||
border-gray-600
|
||||
max-h-80
|
||||
overflow-y-auto
|
||||
overscroll-contain`}
|
||||
>
|
||||
<div
|
||||
role="menu"
|
||||
aria-orientation="vertical"
|
||||
aria-labelledby="options-menu"
|
||||
>
|
||||
{filteredOptions.length ? (
|
||||
filteredOptions.map((option, index) =>
|
||||
itemComponent ? (
|
||||
<div
|
||||
key={option.name}
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
handleSelect(option);
|
||||
}}
|
||||
>
|
||||
{itemComponent({ option })}
|
||||
</div>
|
||||
) : (
|
||||
<StandardDropdownOption
|
||||
key={index}
|
||||
option={option}
|
||||
index={index}
|
||||
handleSelect={handleSelect}
|
||||
/>
|
||||
)
|
||||
)
|
||||
) : (
|
||||
<button
|
||||
key={0}
|
||||
className={`w-full text-left block px-4 py-2.5 text-sm hover:bg-gray-800`}
|
||||
role="menuitem"
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
No matches found...
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
38
web/src/components/Modal.tsx
Normal file
38
web/src/components/Modal.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
interface ModalProps {
|
||||
children: JSX.Element | string;
|
||||
onOutsideClick: () => void;
|
||||
title?: JSX.Element | string;
|
||||
}
|
||||
|
||||
export const Modal: React.FC<ModalProps> = ({
|
||||
children,
|
||||
onOutsideClick,
|
||||
title,
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className={`
|
||||
fixed inset-0 bg-black bg-opacity-50
|
||||
flex items-center justify-center z-50
|
||||
`}
|
||||
onClick={onOutsideClick}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
bg-gray-800 rounded-lg border border-gray-700
|
||||
shadow-lg relative w-1/2 text-sm
|
||||
`}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
{title && (
|
||||
<h2 className="text-xl font-bold mb-3 border-b border-gray-600 pt-4 pb-3 bg-gray-700 px-6">
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
278
web/src/components/admin/Layout.tsx
Normal file
278
web/src/components/admin/Layout.tsx
Normal file
@ -0,0 +1,278 @@
|
||||
import { Header } from "@/components/Header";
|
||||
import { Sidebar } from "@/components/admin/connectors/Sidebar";
|
||||
import {
|
||||
NotebookIcon,
|
||||
GithubIcon,
|
||||
GlobeIcon,
|
||||
GoogleDriveIcon,
|
||||
SlackIcon,
|
||||
KeyIcon,
|
||||
BookstackIcon,
|
||||
ConfluenceIcon,
|
||||
GuruIcon,
|
||||
FileIcon,
|
||||
JiraIcon,
|
||||
SlabIcon,
|
||||
NotionIcon,
|
||||
ZulipIcon,
|
||||
ProductboardIcon,
|
||||
LinearIcon,
|
||||
UsersIcon,
|
||||
ThumbsUpIcon,
|
||||
HubSpotIcon,
|
||||
BookmarkIcon,
|
||||
CPUIcon,
|
||||
} from "@/components/icons/icons";
|
||||
import { getAuthDisabledSS, getCurrentUserSS } from "@/lib/userSS";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export async function Layout({ children }: { children: React.ReactNode }) {
|
||||
const [authDisabled, user] = await Promise.all([
|
||||
getAuthDisabledSS(),
|
||||
getCurrentUserSS(),
|
||||
]);
|
||||
|
||||
if (!authDisabled) {
|
||||
if (!user) {
|
||||
return redirect("/auth/login");
|
||||
}
|
||||
if (user.role !== "admin") {
|
||||
return redirect("/");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header user={user} />
|
||||
<div className="bg-gray-900 pt-8 pb-8 flex">
|
||||
<Sidebar
|
||||
title="Connector"
|
||||
collections={[
|
||||
{
|
||||
name: "Indexing",
|
||||
items: [
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<NotebookIcon size={18} />
|
||||
<div className="ml-1">Status</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/indexing/status",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Connector Settings",
|
||||
items: [
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<SlackIcon size={16} />
|
||||
<div className="ml-1">Slack</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/connectors/slack",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<GithubIcon size={16} />
|
||||
<div className="ml-1">Github</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/connectors/github",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<GoogleDriveIcon size={16} />
|
||||
<div className="ml-1">Google Drive</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/connectors/google-drive",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<ConfluenceIcon size={16} />
|
||||
<div className="ml-1">Confluence</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/connectors/confluence",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<JiraIcon size={16} />
|
||||
<div className="ml-1">Jira</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/connectors/jira",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<LinearIcon size={16} />
|
||||
<div className="ml-1">Linear</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/connectors/linear",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<ProductboardIcon size={16} />
|
||||
<div className="ml-1">Productboard</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/connectors/productboard",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<SlabIcon size={16} />
|
||||
<div className="ml-1">Slab</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/connectors/slab",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<NotionIcon size={16} />
|
||||
<div className="ml-1">Notion</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/connectors/notion",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<GuruIcon size={16} />
|
||||
<div className="ml-1">Guru</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/connectors/guru",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<BookstackIcon size={16} />
|
||||
<div className="ml-1">BookStack</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/connectors/bookstack",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<ZulipIcon size={16} />
|
||||
<div className="ml-1">Zulip</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/connectors/zulip",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<GlobeIcon size={16} />
|
||||
<div className="ml-1">Web</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/connectors/web",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<FileIcon size={16} />
|
||||
<div className="ml-1">File</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/connectors/file",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<HubSpotIcon size={16} />
|
||||
<div className="ml-1">HubSpot</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/connectors/hubspot",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Keys",
|
||||
items: [
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<KeyIcon size={18} />
|
||||
<div className="ml-1">OpenAI</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/keys/openai",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "User Management",
|
||||
items: [
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<UsersIcon size={18} />
|
||||
<div className="ml-1">Users</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/users",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Document Management",
|
||||
items: [
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<BookmarkIcon size={18} />
|
||||
<div className="ml-1">Document Sets</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/documents/sets",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<ThumbsUpIcon size={18} />
|
||||
<div className="ml-1">Feedback</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/documents/feedback",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Bots",
|
||||
items: [
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<CPUIcon size={18} />
|
||||
<div className="ml-1">Slack Bot</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/bot",
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<div className="px-12 min-h-screen bg-gray-900 text-gray-100 w-full">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -14,9 +14,14 @@ type TableData = {
|
||||
interface BasicTableProps {
|
||||
columns: Column[];
|
||||
data: TableData[];
|
||||
onSelect?: (row: TableData) => void;
|
||||
}
|
||||
|
||||
export const BasicTable: FC<BasicTableProps> = ({ columns, data }) => {
|
||||
export const BasicTable: FC<BasicTableProps> = ({
|
||||
columns,
|
||||
data,
|
||||
onSelect,
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<table className="w-full table-auto">
|
||||
@ -46,7 +51,14 @@ export const BasicTable: FC<BasicTableProps> = ({ columns, data }) => {
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((row, rowIndex) => (
|
||||
<tr key={rowIndex} className="text-sm">
|
||||
<tr
|
||||
key={rowIndex}
|
||||
className={
|
||||
"text-sm" +
|
||||
(onSelect ? " hover:bg-gray-800 cursor-pointer" : "")
|
||||
}
|
||||
onClick={() => onSelect && onSelect(row)}
|
||||
>
|
||||
{columns.map((column, colIndex) => {
|
||||
const isRightAligned = column?.alignment === "right";
|
||||
return (
|
||||
|
@ -9,8 +9,7 @@ import {
|
||||
} from "formik";
|
||||
import * as Yup from "yup";
|
||||
import { FormBodyBuilder } from "./types";
|
||||
import { FC, useEffect, useRef, useState } from "react";
|
||||
import { ChevronDownIcon } from "@/components/icons/icons";
|
||||
import { Dropdown, Option } from "@/components/Dropdown";
|
||||
|
||||
interface TextFormFieldProps {
|
||||
name: string;
|
||||
@ -182,12 +181,6 @@ export function TextArrayFieldBuilder<T extends Yup.AnyObject>(
|
||||
return _TextArrayField;
|
||||
}
|
||||
|
||||
interface Option {
|
||||
name: string;
|
||||
value: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface SelectorFormFieldProps {
|
||||
name: string;
|
||||
label: string;
|
||||
@ -227,101 +220,3 @@ export function SelectorFormField({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface DropdownProps {
|
||||
options: Option[];
|
||||
selected: string;
|
||||
onSelect: (selected: Option) => void;
|
||||
}
|
||||
|
||||
const Dropdown: FC<DropdownProps> = ({ options, selected, onSelect }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const selectedName = options.find(
|
||||
(option) => option.value === selected
|
||||
)?.name;
|
||||
|
||||
const handleSelect = (option: Option) => {
|
||||
onSelect(option);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative inline-block text-left w-full" ref={dropdownRef}>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
className={`inline-flex
|
||||
justify-center
|
||||
w-full
|
||||
px-4
|
||||
py-3
|
||||
text-sm
|
||||
bg-gray-700
|
||||
border
|
||||
border-gray-300
|
||||
rounded-md
|
||||
shadow-sm
|
||||
hover:bg-gray-700
|
||||
focus:ring focus:ring-offset-0 focus:ring-1 focus:ring-offset-gray-800 focus:ring-blue-800
|
||||
`}
|
||||
id="options-menu"
|
||||
aria-expanded="true"
|
||||
aria-haspopup="true"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
{selectedName ? <p>{selectedName}</p> : "Select an option..."}
|
||||
<ChevronDownIcon className="text-gray-400 my-auto ml-auto" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isOpen ? (
|
||||
<div className="origin-top-right absolute left-0 mt-3 w-full rounded-md shadow-lg bg-gray-700 border-2 border-gray-600">
|
||||
<div
|
||||
role="menu"
|
||||
aria-orientation="vertical"
|
||||
aria-labelledby="options-menu"
|
||||
>
|
||||
{options.map((option, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => handleSelect(option)}
|
||||
className={
|
||||
`w-full text-left block px-4 py-2.5 text-sm hover:bg-gray-800` +
|
||||
(index !== 0 ? " border-t-2 border-gray-600" : "")
|
||||
}
|
||||
role="menuitem"
|
||||
>
|
||||
<p className="font-medium">{option.name}</p>
|
||||
{option.description && (
|
||||
<div>
|
||||
<p className="text-xs text-gray-300">
|
||||
{option.description}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user