diff --git a/web/src/app/admin/layout.tsx b/web/src/app/admin/layout.tsx index 4e1ba7722..026139fe6 100644 --- a/web/src/app/admin/layout.tsx +++ b/web/src/app/admin/layout.tsx @@ -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 ( -
-
-
- - -
Status
-
- ), - link: "/admin/indexing/status", - }, - ], - }, - { - name: "Connector Settings", - items: [ - { - name: ( -
- -
Slack
-
- ), - link: "/admin/connectors/slack", - }, - { - name: ( -
- -
Github
-
- ), - link: "/admin/connectors/github", - }, - { - name: ( -
- -
Google Drive
-
- ), - link: "/admin/connectors/google-drive", - }, - { - name: ( -
- -
Confluence
-
- ), - link: "/admin/connectors/confluence", - }, - { - name: ( -
- -
Jira
-
- ), - link: "/admin/connectors/jira", - }, - { - name: ( -
- -
Linear
-
- ), - link: "/admin/connectors/linear", - }, - { - name: ( -
- -
Productboard
-
- ), - link: "/admin/connectors/productboard", - }, - { - name: ( -
- -
Slab
-
- ), - link: "/admin/connectors/slab", - }, - { - name: ( -
- -
Notion
-
- ), - link: "/admin/connectors/notion", - }, - { - name: ( -
- -
Guru
-
- ), - link: "/admin/connectors/guru", - }, - { - name: ( -
- -
Gong
-
- ), - link: "/admin/connectors/gong", - }, - { - name: ( -
- -
BookStack
-
- ), - link: "/admin/connectors/bookstack", - }, - { - name: ( -
- -
Zulip
-
- ), - link: "/admin/connectors/zulip", - }, - { - name: ( -
- -
Web
-
- ), - link: "/admin/connectors/web", - }, - { - name: ( -
- -
File
-
- ), - link: "/admin/connectors/file", - }, - { - name: ( -
- -
HubSpot
-
- ), - link: "/admin/connectors/hubspot", - }, - ], - }, - { - name: "Keys", - items: [ - { - name: ( -
- -
OpenAI
-
- ), - link: "/admin/keys/openai", - }, - ], - }, - { - name: "User Management", - items: [ - { - name: ( -
- -
Users
-
- ), - link: "/admin/users", - }, - ], - }, - { - name: "Document Management", - items: [ - { - name: ( -
- -
Document Sets
-
- ), - link: "/admin/documents/sets", - }, - { - name: ( -
- -
Feedback
-
- ), - link: "/admin/documents/feedback", - }, - ], - }, - { - name: "Bots", - items: [ - { - name: ( -
- -
Slack Bot
-
- ), - link: "/admin/bot", - }, - ], - }, - ]} - /> -
- {children} -
-
- - ); + return await Layout({ children }); } diff --git a/web/src/components/Dropdown.tsx b/web/src/components/Dropdown.tsx new file mode 100644 index 000000000..9903bcbf6 --- /dev/null +++ b/web/src/components/Dropdown.tsx @@ -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 = ({ + options, + selected, + onSelect, +}) => { + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(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 ( +
+
+ +
+ + {isOpen ? ( +
+
+ {options.map((option, index) => ( + + ))} +
+
+ ) : null} +
+ ); +}; + +const StandardDropdownOption = ({ + index, + option, + handleSelect, +}: { + index: number; + option: Option; + handleSelect: (option: Option) => void; +}) => { + return ( + + ); +}; + +interface MultiSelectDropdownProps { + options: Option[]; + onSelect: (selected: Option) => void; + itemComponent?: FC<{ option: Option }>; +} + +export const SearchMultiSelectDropdown: FC = ({ + options, + onSelect, + itemComponent, +}) => { + const [isOpen, setIsOpen] = useState(false); + const [searchTerm, setSearchTerm] = useState(""); + const dropdownRef = useRef(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 ( +
+
+ ) => { + 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()} + /> + +
+ + {isOpen && ( +
+
+ {filteredOptions.length ? ( + filteredOptions.map((option, index) => + itemComponent ? ( +
{ + setIsOpen(false); + handleSelect(option); + }} + > + {itemComponent({ option })} +
+ ) : ( + + ) + ) + ) : ( + + )} +
+
+ )} +
+ ); +}; diff --git a/web/src/components/Modal.tsx b/web/src/components/Modal.tsx new file mode 100644 index 000000000..4442a5aa4 --- /dev/null +++ b/web/src/components/Modal.tsx @@ -0,0 +1,38 @@ +interface ModalProps { + children: JSX.Element | string; + onOutsideClick: () => void; + title?: JSX.Element | string; +} + +export const Modal: React.FC = ({ + children, + onOutsideClick, + title, +}) => { + return ( +
+
+
event.stopPropagation()} + > + {title && ( +

+ {title} +

+ )} + {children} +
+
+
+ ); +}; diff --git a/web/src/components/admin/Layout.tsx b/web/src/components/admin/Layout.tsx new file mode 100644 index 000000000..d7add9dac --- /dev/null +++ b/web/src/components/admin/Layout.tsx @@ -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 ( +
+
+
+ + +
Status
+
+ ), + link: "/admin/indexing/status", + }, + ], + }, + { + name: "Connector Settings", + items: [ + { + name: ( +
+ +
Slack
+
+ ), + link: "/admin/connectors/slack", + }, + { + name: ( +
+ +
Github
+
+ ), + link: "/admin/connectors/github", + }, + { + name: ( +
+ +
Google Drive
+
+ ), + link: "/admin/connectors/google-drive", + }, + { + name: ( +
+ +
Confluence
+
+ ), + link: "/admin/connectors/confluence", + }, + { + name: ( +
+ +
Jira
+
+ ), + link: "/admin/connectors/jira", + }, + { + name: ( +
+ +
Linear
+
+ ), + link: "/admin/connectors/linear", + }, + { + name: ( +
+ +
Productboard
+
+ ), + link: "/admin/connectors/productboard", + }, + { + name: ( +
+ +
Slab
+
+ ), + link: "/admin/connectors/slab", + }, + { + name: ( +
+ +
Notion
+
+ ), + link: "/admin/connectors/notion", + }, + { + name: ( +
+ +
Guru
+
+ ), + link: "/admin/connectors/guru", + }, + { + name: ( +
+ +
BookStack
+
+ ), + link: "/admin/connectors/bookstack", + }, + { + name: ( +
+ +
Zulip
+
+ ), + link: "/admin/connectors/zulip", + }, + { + name: ( +
+ +
Web
+
+ ), + link: "/admin/connectors/web", + }, + { + name: ( +
+ +
File
+
+ ), + link: "/admin/connectors/file", + }, + { + name: ( +
+ +
HubSpot
+
+ ), + link: "/admin/connectors/hubspot", + }, + ], + }, + { + name: "Keys", + items: [ + { + name: ( +
+ +
OpenAI
+
+ ), + link: "/admin/keys/openai", + }, + ], + }, + { + name: "User Management", + items: [ + { + name: ( +
+ +
Users
+
+ ), + link: "/admin/users", + }, + ], + }, + { + name: "Document Management", + items: [ + { + name: ( +
+ +
Document Sets
+
+ ), + link: "/admin/documents/sets", + }, + { + name: ( +
+ +
Feedback
+
+ ), + link: "/admin/documents/feedback", + }, + ], + }, + { + name: "Bots", + items: [ + { + name: ( +
+ +
Slack Bot
+
+ ), + link: "/admin/bot", + }, + ], + }, + ]} + /> +
+ {children} +
+
+ + ); +} diff --git a/web/src/components/admin/connectors/BasicTable.tsx b/web/src/components/admin/connectors/BasicTable.tsx index d33f2d3ab..8fae28e1d 100644 --- a/web/src/components/admin/connectors/BasicTable.tsx +++ b/web/src/components/admin/connectors/BasicTable.tsx @@ -14,9 +14,14 @@ type TableData = { interface BasicTableProps { columns: Column[]; data: TableData[]; + onSelect?: (row: TableData) => void; } -export const BasicTable: FC = ({ columns, data }) => { +export const BasicTable: FC = ({ + columns, + data, + onSelect, +}) => { return (
@@ -46,7 +51,14 @@ export const BasicTable: FC = ({ columns, data }) => { {data.map((row, rowIndex) => ( - + onSelect && onSelect(row)} + > {columns.map((column, colIndex) => { const isRightAligned = column?.alignment === "right"; return ( diff --git a/web/src/components/admin/connectors/Field.tsx b/web/src/components/admin/connectors/Field.tsx index 19d2af13e..5e38dba15 100644 --- a/web/src/components/admin/connectors/Field.tsx +++ b/web/src/components/admin/connectors/Field.tsx @@ -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( return _TextArrayField; } -interface Option { - name: string; - value: string; - description?: string; -} - interface SelectorFormFieldProps { name: string; label: string; @@ -227,101 +220,3 @@ export function SelectorFormField({ ); } - -interface DropdownProps { - options: Option[]; - selected: string; - onSelect: (selected: Option) => void; -} - -const Dropdown: FC = ({ options, selected, onSelect }) => { - const [isOpen, setIsOpen] = useState(false); - const dropdownRef = useRef(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 ( -
-
- -
- - {isOpen ? ( -
-
- {options.map((option, index) => ( - - ))} -
-
- ) : null} -
- ); -};