Add some additional FE components

This commit is contained in:
Weves 2023-10-08 17:20:13 -07:00 committed by Chris Weaver
parent f045bbed70
commit fb1fbbee5c
6 changed files with 613 additions and 393 deletions

View File

@ -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 });
}

View 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>
);
};

View 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>
);
};

View 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>
);
}

View File

@ -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 (

View File

@ -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>
);
};