Add general status page + standardize the experience a bit

This commit is contained in:
Weves
2023-05-16 01:13:03 -07:00
committed by Chris Weaver
parent 0d9595733b
commit 17ed660166
19 changed files with 399 additions and 145 deletions

View File

@@ -115,6 +115,29 @@ def list_index_attempts(
IndexAttemptSnapshot( IndexAttemptSnapshot(
connector_specific_config=index_attempt.connector_specific_config, connector_specific_config=index_attempt.connector_specific_config,
status=index_attempt.status, status=index_attempt.status,
source=index_attempt.source,
time_created=index_attempt.time_created,
time_updated=index_attempt.time_updated,
docs_indexed=0
if not index_attempt.document_ids
else len(index_attempt.document_ids),
)
for index_attempt in index_attempts
]
)
@router.get("/connectors/index-attempt")
def list_all_index_attempts(
_: User = Depends(current_admin_user),
) -> ListIndexAttemptsResponse:
index_attempts = fetch_index_attempts()
return ListIndexAttemptsResponse(
index_attempts=[
IndexAttemptSnapshot(
connector_specific_config=index_attempt.connector_specific_config,
status=index_attempt.status,
source=index_attempt.source,
time_created=index_attempt.time_created, time_created=index_attempt.time_created,
time_updated=index_attempt.time_updated, time_updated=index_attempt.time_updated,
docs_indexed=0 docs_indexed=0

View File

@@ -1,6 +1,7 @@
from datetime import datetime from datetime import datetime
from typing import Any from typing import Any
from danswer.configs.constants import DocumentSource
from danswer.datastores.interfaces import DatastoreFilter from danswer.datastores.interfaces import DatastoreFilter
from danswer.db.models import IndexingStatus from danswer.db.models import IndexingStatus
from pydantic import BaseModel from pydantic import BaseModel
@@ -53,6 +54,7 @@ class UserByEmail(BaseModel):
class IndexAttemptSnapshot(BaseModel): class IndexAttemptSnapshot(BaseModel):
connector_specific_config: dict[str, Any] connector_specific_config: dict[str, Any]
status: IndexingStatus status: IndexingStatus
source: DocumentSource
time_created: datetime time_created: datetime
time_updated: datetime time_updated: datetime
docs_indexed: int docs_indexed: int

View File

@@ -17,11 +17,6 @@ export default function Page() {
<h1 className="text-3xl font-bold pl-2">Github PRs</h1> <h1 className="text-3xl font-bold pl-2">Github PRs</h1>
</div> </div>
<h2 className="text-xl font-bold pl-2 mb-2 mt-6 ml-auto mr-auto">
Status
</h2>
<ConnectorStatus status={ConnectorStatusEnum.Setup} source="github" />
{/* TODO: make this periodic */} {/* TODO: make this periodic */}
<h2 className="text-xl font-bold pl-2 mb-2 mt-6 ml-auto mr-auto"> <h2 className="text-xl font-bold pl-2 mb-2 mt-6 ml-auto mr-auto">
Request Indexing Request Indexing

View File

@@ -1,7 +1,10 @@
"use client"; "use client";
import * as Yup from "yup"; import * as Yup from "yup";
import { IndexForm } from "@/components/admin/connectors/Form"; import {
IndexForm,
submitIndexRequest,
} from "@/components/admin/connectors/Form";
import { import {
ConnectorStatusEnum, ConnectorStatusEnum,
ConnectorStatus, ConnectorStatus,
@@ -10,8 +13,13 @@ import { GoogleDriveIcon } from "@/components/icons/icons";
import useSWR from "swr"; import useSWR from "swr";
import { fetcher } from "@/lib/fetcher"; import { fetcher } from "@/lib/fetcher";
import { LoadingAnimation } from "@/components/Loading"; import { LoadingAnimation } from "@/components/Loading";
import { useRouter } from "next/navigation";
import { Popup } from "@/components/admin/connectors/Popup";
import { useState } from "react";
export default function Page() { export default function Page() {
const router = useRouter();
const { const {
data: isAuthenticatedData, data: isAuthenticatedData,
isLoading: isAuthenticatedLoading, isLoading: isAuthenticatedLoading,
@@ -29,6 +37,11 @@ export default function Page() {
fetcher fetcher
); );
const [popup, setPopup] = useState<{
message: string;
type: "success" | "error";
} | null>(null);
const header = ( const header = (
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex"> <div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
<GoogleDriveIcon size="32" /> <GoogleDriveIcon size="32" />
@@ -73,33 +86,58 @@ export default function Page() {
if (isAuthenticatedData.authenticated) { if (isAuthenticatedData.authenticated) {
return ( return (
<div className="mx-auto"> <div>
{header} {header}
{popup && <Popup message={popup.message} type={popup.type} />}
<h2 className="text-xl font-bold pl-2 mb-2 mt-6 ml-auto mr-auto"> {/* TODO: add periodic support */}
Status <h2 className="text-xl font-bold mb-2 ml-auto mr-auto">
Request Indexing
</h2> </h2>
<ConnectorStatus <p className="text-sm mb-2">
status={ConnectorStatusEnum.Setup} Index the all docs in the setup Google Drive account.
source="google_drive" </p>
/> <div className="mt-2 mb-4">
<button
{/* TODO: make this periodic */} type="submit"
<div className="w-fit mt-2"> className={
<IndexForm "bg-slate-500 hover:bg-slate-700 text-white " +
source="google_drive" "font-bold py-2 px-4 rounded focus:outline-none " +
formBody={null} "focus:shadow-outline w-full max-w-sm mx-auto"
validationSchema={Yup.object().shape({})} }
initialValues={{}} onClick={async () => {
onSubmit={(isSuccess) => console.log(isSuccess)} const { message, isSuccess } = await submitIndexRequest(
/> "google_drive",
{}
);
if (isSuccess) {
setPopup({
message,
type: isSuccess ? "success" : "error",
});
setTimeout(() => {
setPopup(null);
}, 3000);
router.push("/admin/indexing/status");
}
}}
>
Index
</button>
</div> </div>
{/* {/* TODO: add ability to add more accounts / switch account */}
TODO: add back ability add more accounts / switch account <div className="mb-2">
<h2 className="text-xl font-bold mb-2 ml-auto mr-auto">
Re-Authenticate
</h2>
<p className="text-sm mb-4">
If you want to switch Google Drive accounts, you can re-authenticate
below.
</p>
<a <a
className={ className={
"group relative w-64 flex justify-center " + "group relative w-64 " +
"py-2 px-4 border border-transparent text-sm " + "py-2 px-4 border border-transparent text-sm " +
"font-medium rounded-md text-white bg-red-600 " + "font-medium rounded-md text-white bg-red-600 " +
"hover:bg-red-700 focus:outline-none focus:ring-2 " + "hover:bg-red-700 focus:outline-none focus:ring-2 " +
@@ -107,8 +145,9 @@ export default function Page() {
} }
href={authorizationUrlData.auth_url} href={authorizationUrlData.auth_url}
> >
Re-Authenticate Authenticate with Google Drive
</a> */} </a>
</div>
</div> </div>
); );
} }

View File

@@ -3,14 +3,12 @@ import { Formik, Form, FormikHelpers } from "formik";
import * as Yup from "yup"; import * as Yup from "yup";
import { Popup } from "../../../../components/admin/connectors/Popup"; import { Popup } from "../../../../components/admin/connectors/Popup";
import { TextFormField } from "../../../../components/admin/connectors/Field"; import { TextFormField } from "../../../../components/admin/connectors/Field";
import { SlackConfig } from "../../../../components/admin/connectors/interfaces"; import { SlackConfig } from "../../../../components/admin/connectors/types";
const validationSchema = Yup.object().shape({ const validationSchema = Yup.object().shape({
slack_bot_token: Yup.string().required("Please enter your Slack Bot Token"), slack_bot_token: Yup.string().required("Please enter your Slack Bot Token"),
workspace_id: Yup.string().required("Please enter your Workspace ID"), workspace_id: Yup.string().required("Please enter your Workspace ID"),
pull_frequency: Yup.number().required( pull_frequency: Yup.number().optional(),
"Please enter a pull frequency (in minutes). 0 => no pulling from slack"
),
}); });
const handleSubmit = async ( const handleSubmit = async (
@@ -49,12 +47,12 @@ const handleSubmit = async (
return isSuccess; return isSuccess;
}; };
interface SlackFormProps { interface Props {
existingSlackConfig: SlackConfig; existingSlackConfig: SlackConfig;
onSubmit: (isSuccess: boolean) => void; onSubmit: (isSuccess: boolean) => void;
} }
export const SlackForm: React.FC<SlackFormProps> = ({ export const InitialSetupForm: React.FC<Props> = ({
existingSlackConfig, existingSlackConfig,
onSubmit, onSubmit,
}) => { }) => {
@@ -79,14 +77,22 @@ export const SlackForm: React.FC<SlackFormProps> = ({
<Form> <Form>
<TextFormField name="slack_bot_token" label="Slack Bot Token:" /> <TextFormField name="slack_bot_token" label="Slack Bot Token:" />
<TextFormField name="workspace_id" label="Workspace ID:" /> <TextFormField name="workspace_id" label="Workspace ID:" />
<TextFormField name="pull_frequency" label="Pull Frequency:" /> <TextFormField
<button name="pull_frequency"
type="submit" label="Pull Frequency (in minutes):"
disabled={isSubmitting} />
className="bg-slate-500 hover:bg-slate-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline w-full" <div className="flex">
> <button
Update type="submit"
</button> disabled={isSubmitting}
className={
"mx-auto bg-slate-500 hover:bg-slate-700 text-white font-bold py-2 " +
"px-4 max-w-sm rounded focus:outline-none focus:shadow-outline w-full"
}
>
Update
</button>
</div>
</Form> </Form>
)} )}
</Formik> </Formik>

View File

@@ -1,15 +1,11 @@
"use client"; "use client";
import {
ConnectorStatus,
ConnectorStatusEnum,
} from "@/components/admin/connectors/ConnectorStatus";
import { SlackForm } from "@/app/admin/connectors/slack/SlackForm";
import { SlackIcon } from "@/components/icons/icons"; import { SlackIcon } from "@/components/icons/icons";
import { fetcher } from "@/lib/fetcher"; import { fetcher } from "@/lib/fetcher";
import useSWR, { useSWRConfig } from "swr"; import useSWR, { useSWRConfig } from "swr";
import { SlackConfig } from "../../../../components/admin/connectors/interfaces"; import { SlackConfig } from "../../../../components/admin/connectors/types";
import { LoadingAnimation } from "@/components/Loading"; import { LoadingAnimation } from "@/components/Loading";
import { InitialSetupForm } from "./InitialSetupForm";
const MainSection = () => { const MainSection = () => {
// TODO: add back in once this is ready // TODO: add back in once this is ready
@@ -36,26 +32,15 @@ const MainSection = () => {
} }
return ( return (
<div> <div className="mx-auto">
<h2 className="text-xl font-bold pl-2 mb-2 mt-6 ml-auto mr-auto"> <h2 className="text-xl font-bold mb-3 ml-auto mr-auto">Config</h2>
Status <p className="text-sm mb-4">
</h2> To use the Slack connector, you must first provide a Slack bot token
{ corresponding to the Slack App set up in your workspace. For more
<ConnectorStatus details on setting up the Danswer Slack App, see the docs here (TODO).
status={ </p>
data.pull_frequency !== 0 <div className="border border-gray-700 rounded-md p-3">
? ConnectorStatusEnum.Running <InitialSetupForm
: ConnectorStatusEnum.NotSetup
}
source="slack"
/>
}
<h2 className="text-xl font-bold pl-2 mb-2 mt-6 ml-auto mr-auto">
Config
</h2>
<div className="border-solid border-gray-600 border rounded-md p-6">
<SlackForm
existingSlackConfig={data} existingSlackConfig={data}
onSubmit={() => mutate("/api/admin/connectors/slack/config")} onSubmit={() => mutate("/api/admin/connectors/slack/config")}
/> />

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import useSWR, { useSWRConfig } from "swr"; import useSWR from "swr";
import * as Yup from "yup"; import * as Yup from "yup";
import { BasicTable } from "@/components/admin/connectors/BasicTable"; import { BasicTable } from "@/components/admin/connectors/BasicTable";
@@ -11,9 +11,10 @@ import { fetcher } from "@/lib/fetcher";
import { import {
IndexAttempt, IndexAttempt,
ListIndexingResponse, ListIndexingResponse,
} from "../../../../components/admin/connectors/interfaces"; } from "../../../../components/admin/connectors/types";
import { IndexForm } from "@/components/admin/connectors/Form"; import { IndexForm } from "@/components/admin/connectors/Form";
import { TextFormField } from "@/components/admin/connectors/Field"; import { TextFormField } from "@/components/admin/connectors/Field";
import { useRouter } from "next/navigation";
const COLUMNS = [ const COLUMNS = [
{ header: "Base URL", key: "url" }, { header: "Base URL", key: "url" },
@@ -23,7 +24,8 @@ const COLUMNS = [
]; ];
export default function Web() { export default function Web() {
const { mutate } = useSWRConfig(); const router = useRouter();
const { data, isLoading, error } = useSWR<ListIndexingResponse>( const { data, isLoading, error } = useSWR<ListIndexingResponse>(
"/api/admin/connectors/web/index-attempt", "/api/admin/connectors/web/index-attempt",
fetcher fetcher
@@ -71,7 +73,7 @@ export default function Web() {
initialValues={{ base_url: "" }} initialValues={{ base_url: "" }}
onSubmit={(success) => { onSubmit={(success) => {
if (success) { if (success) {
mutate("/api/admin/connectors/web/index-attempt"); router.push("/admin/indexing/status");
} }
}} }}
/> />

View File

@@ -0,0 +1,154 @@
"use client";
import useSWR, { useSWRConfig } from "swr";
import { BasicTable } from "@/components/admin/connectors/BasicTable";
import { LoadingAnimation } from "@/components/Loading";
import { timeAgo } from "@/lib/time";
import { NotebookIcon } from "@/components/icons/icons";
import { fetcher } from "@/lib/fetcher";
import {
IndexAttempt,
ListIndexingResponse,
} from "@/components/admin/connectors/types";
import { getSourceMetadata } from "@/components/source";
import { CheckCircle } from "@phosphor-icons/react";
import { submitIndexRequest } from "@/components/admin/connectors/Form";
import { useState } from "react";
import { Popup } from "@/components/admin/connectors/Popup";
const getModifiedSource = (indexAttempt: IndexAttempt) => {
return indexAttempt.source === "web"
? indexAttempt.source + indexAttempt.connector_specific_config?.base_url
: indexAttempt.source;
};
const getLatestIndexAttemptsBySource = (indexAttempts: IndexAttempt[]) => {
const latestIndexAttemptsBySource = new Map<string, IndexAttempt>();
indexAttempts.forEach((indexAttempt) => {
const source = getModifiedSource(indexAttempt);
const existingIndexAttempt = latestIndexAttemptsBySource.get(source);
if (
!existingIndexAttempt ||
indexAttempt.time_updated > existingIndexAttempt.time_updated
) {
latestIndexAttemptsBySource.set(source, indexAttempt);
}
});
return latestIndexAttemptsBySource;
};
export default function Status() {
const { mutate } = useSWRConfig();
const { data, isLoading, error } = useSWR<ListIndexingResponse>(
"/api/admin/connectors/index-attempt",
fetcher
);
const [popup, setPopup] = useState<{
message: string;
type: "success" | "error";
} | null>(null);
// TODO: don't retrieve all index attempts, just the latest ones for each source
const latestIndexAttemptsBySource = getLatestIndexAttemptsBySource(
data?.index_attempts || []
);
const latestSuccessfulIndexAttemptsBySource = getLatestIndexAttemptsBySource(
data?.index_attempts?.filter(
(indexAttempt) => indexAttempt.status === "success"
) || []
);
return (
<div className="mx-auto">
{popup && <Popup message={popup.message} type={popup.type} />}
<div className="border-solid border-gray-600 border-b pb-2 mb-4 flex">
<NotebookIcon size="32" />
<h1 className="text-3xl font-bold pl-2">Indexing Status</h1>
</div>
{isLoading ? (
<LoadingAnimation text="Loading" />
) : error ? (
<div>Error loading indexing history</div>
) : (
<BasicTable
columns={[
{ header: "Connector", key: "connector" },
{ header: "Status", key: "status" },
{ header: "Last Indexed", key: "indexed_at" },
{ header: "Docs Indexed", key: "docs_indexed" },
{ header: "Re-Index", key: "reindex" },
]}
data={Array.from(latestIndexAttemptsBySource.values()).map(
(indexAttempt) => {
const sourceMetadata = getSourceMetadata(indexAttempt.source);
const successfulIndexAttempt =
latestSuccessfulIndexAttemptsBySource.get(
getModifiedSource(indexAttempt)
);
return {
indexed_at:
timeAgo(successfulIndexAttempt?.time_updated) || "-",
docs_indexed: successfulIndexAttempt?.docs_indexed
? `${successfulIndexAttempt?.docs_indexed} documents`
: "-",
connector: (
<a
className="text-blue-500 flex"
href={sourceMetadata.adminPageLink}
>
{sourceMetadata.icon({ size: "20" })}
<div className="ml-1">
{sourceMetadata.displayName}
{indexAttempt.source === "web" &&
indexAttempt.connector_specific_config?.base_url &&
` [${indexAttempt.connector_specific_config?.base_url}]`}
</div>
</a>
),
status:
indexAttempt.status === "success" ? (
<div className="text-green-600 flex">
<CheckCircle className="my-auto mr-1" size="18" />
Success
</div>
) : (
<div className="text-gray-400">In Progress...</div>
),
reindex: (
<button
className={
"group relative " +
"py-1 px-2 border border-transparent text-sm " +
"font-medium rounded-md text-white bg-red-800 " +
"hover:bg-red-900 focus:outline-none focus:ring-2 " +
"focus:ring-offset-2 focus:ring-red-500 mx-auto"
}
onClick={async () => {
const { message, isSuccess } = await submitIndexRequest(
indexAttempt.source,
indexAttempt.connector_specific_config
);
setPopup({
message,
type: isSuccess ? "success" : "error",
});
setTimeout(() => {
setPopup(null);
}, 3000);
mutate("/api/admin/connectors/index-attempt");
}}
>
Index
</button>
),
};
}
)}
/>
)}
</div>
);
}

View File

@@ -1,6 +1,7 @@
import { Header } from "@/components/Header"; import { Header } from "@/components/Header";
import { Sidebar } from "@/components/admin/connectors/Sidebar"; import { Sidebar } from "@/components/admin/connectors/Sidebar";
import { import {
NotebookIcon,
GithubIcon, GithubIcon,
GlobeIcon, GlobeIcon,
GoogleDriveIcon, GoogleDriveIcon,
@@ -27,10 +28,24 @@ export default async function AdminLayout({
<Header user={user} /> <Header user={user} />
<div className="bg-gray-900 pt-8 flex"> <div className="bg-gray-900 pt-8 flex">
<Sidebar <Sidebar
title="Connectors" title="Connector"
collections={[ collections={[
{ {
name: "Connectors", name: "Indexing",
items: [
{
name: (
<div className="flex">
<NotebookIcon size="16" />
<div className="ml-1">Status</div>
</div>
),
link: "/admin/indexing/status",
},
],
},
{
name: "Connector Settings",
items: [ items: [
{ {
name: ( name: (

View File

@@ -6,7 +6,6 @@ import { UserCircle } from "@phosphor-icons/react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import "tailwindcss/tailwind.css";
interface HeaderProps { interface HeaderProps {
user: User; user: User;

View File

@@ -25,8 +25,8 @@ export const BasicTable: FC<BasicTableProps> = ({ columns, data }) => {
key={index} key={index}
className={ className={
"px-4 py-2 font-bold" + "px-4 py-2 font-bold" +
(index === 0 ? " rounded-tl-md" : "") + (index === 0 ? " rounded-tl-sm" : "") +
(index === columns.length - 1 ? " rounded-tr-md" : "") (index === columns.length - 1 ? " rounded-tr-sm" : "")
} }
> >
{column.header} {column.header}
@@ -38,22 +38,11 @@ export const BasicTable: FC<BasicTableProps> = ({ columns, data }) => {
{data.map((row, rowIndex) => ( {data.map((row, rowIndex) => (
<tr key={rowIndex} className="text-sm"> <tr key={rowIndex} className="text-sm">
{columns.map((column, colIndex) => { {columns.map((column, colIndex) => {
let entryClassName = "px-4 py-2 border-b border-gray-700";
const isFinalRow = rowIndex === data.length - 1;
if (colIndex === 0) {
entryClassName += " border-l";
if (isFinalRow) {
entryClassName += " rounded-bl-md";
}
}
if (colIndex === columns.length - 1) {
entryClassName += " border-r";
if (isFinalRow) {
entryClassName += " rounded-br-md";
}
}
return ( return (
<td key={colIndex} className={entryClassName}> <td
key={colIndex}
className="py-2 px-4 border-b border-gray-800"
>
{row[column.key]} {row[column.key]}
</td> </td>
); );

View File

@@ -3,10 +3,10 @@
import { import {
IndexAttempt, IndexAttempt,
ListIndexingResponse, ListIndexingResponse,
ValidSources, } from "@/components/admin/connectors/types";
} from "@/components/admin/connectors/interfaces";
import { fetcher } from "@/lib/fetcher"; import { fetcher } from "@/lib/fetcher";
import { timeAgo } from "@/lib/time"; import { timeAgo } from "@/lib/time";
import { ValidSources } from "@/lib/types";
import { CheckCircle, MinusCircle } from "@phosphor-icons/react"; import { CheckCircle, MinusCircle } from "@phosphor-icons/react";
import useSWR from "swr"; import useSWR from "swr";

View File

@@ -2,17 +2,12 @@ import React, { useState } from "react";
import { Formik, Form, FormikHelpers } from "formik"; import { Formik, Form, FormikHelpers } from "formik";
import * as Yup from "yup"; import * as Yup from "yup";
import { Popup } from "./Popup"; import { Popup } from "./Popup";
import { ValidSources } from "./interfaces"; import { ValidSources } from "@/lib/types";
const handleSubmit = async ( export const submitIndexRequest = async (
source: ValidSources, source: ValidSources,
values: Yup.AnyObject, values: Yup.AnyObject
{ setSubmitting }: FormikHelpers<Yup.AnyObject>, ): Promise<{ message: string; isSuccess: boolean }> => {
setPopup: (
popup: { message: string; type: "success" | "error" } | null
) => void
): Promise<boolean> => {
setSubmitting(true);
let isSuccess = false; let isSuccess = false;
try { try {
const response = await fetch( const response = await fetch(
@@ -28,19 +23,13 @@ const handleSubmit = async (
if (response.ok) { if (response.ok) {
isSuccess = true; isSuccess = true;
setPopup({ message: "Success!", type: "success" }); return { message: "Success!", isSuccess: true };
} else { } else {
const errorData = await response.json(); const errorData = await response.json();
setPopup({ message: `Error: ${errorData.detail}`, type: "error" }); return { message: `Error: ${errorData.detail}`, isSuccess: false };
} }
} catch (error) { } catch (error) {
setPopup({ message: `Error: ${error}`, type: "error" }); return { message: `Error: ${error}`, isSuccess: false };
} finally {
setSubmitting(false);
setTimeout(() => {
setPopup(null);
}, 3000);
return isSuccess;
} }
}; };
@@ -73,12 +62,18 @@ export function IndexForm<YupObjectType extends Yup.AnyObject>({
initialValues={initialValues} initialValues={initialValues}
validationSchema={validationSchema} validationSchema={validationSchema}
onSubmit={(values, formikHelpers) => { onSubmit={(values, formikHelpers) => {
handleSubmit( formikHelpers.setSubmitting(true);
source, submitIndexRequest(source, {
{ ...values, ...additionalNonFormValues }, ...values,
formikHelpers as FormikHelpers<Yup.AnyObject>, ...additionalNonFormValues,
setPopup }).then(({ message, isSuccess }) => {
).then((isSuccess) => onSubmit(isSuccess)); setPopup({ message, type: isSuccess ? "success" : "error" });
formikHelpers.setSubmitting(false);
setTimeout(() => {
setPopup(null);
}, 3000);
onSubmit(isSuccess);
});
}} }}
> >
{({ isSubmitting }) => ( {({ isSubmitting }) => (

View File

@@ -1,3 +1,5 @@
import { ValidSources } from "@/lib/types";
export interface SlackConfig { export interface SlackConfig {
slack_bot_token: string; slack_bot_token: string;
workspace_id: string; workspace_id: string;
@@ -7,6 +9,7 @@ export interface SlackConfig {
export interface IndexAttempt { export interface IndexAttempt {
connector_specific_config: { [key: string]: any }; connector_specific_config: { [key: string]: any };
status: "success" | "failure" | "in_progress" | "not_started"; status: "success" | "failure" | "in_progress" | "not_started";
source: ValidSources;
time_created: string; time_created: string;
time_updated: string; time_updated: string;
docs_indexed: number; docs_indexed: number;
@@ -15,5 +18,3 @@ export interface IndexAttempt {
export interface ListIndexingResponse { export interface ListIndexingResponse {
index_attempts: IndexAttempt[]; index_attempts: IndexAttempt[];
} }
export type ValidSources = "web" | "github" | "slack" | "google_drive";

View File

@@ -1,10 +1,12 @@
"use client"; "use client";
import { ValidSources } from "@/lib/types";
import { import {
Globe, Globe,
SlackLogo, SlackLogo,
GithubLogo, GithubLogo,
GoogleDriveLogo, GoogleDriveLogo,
Notebook,
} from "@phosphor-icons/react"; } from "@phosphor-icons/react";
interface IconProps { interface IconProps {
@@ -14,6 +16,13 @@ interface IconProps {
const defaultTailwindCSS = "text-blue-400 my-auto flex flex-shrink-0"; const defaultTailwindCSS = "text-blue-400 my-auto flex flex-shrink-0";
export const NotebookIcon = ({
size = "16",
className = defaultTailwindCSS,
}: IconProps) => {
return <Notebook size={size} className={className} />;
};
export const GlobeIcon = ({ export const GlobeIcon = ({
size = "16", size = "16",
className = defaultTailwindCSS, className = defaultTailwindCSS,

View File

@@ -1,9 +1,7 @@
import React from "react"; import React from "react";
import { Globe, SlackLogo, GoogleDriveLogo } from "@phosphor-icons/react";
import "tailwindcss/tailwind.css";
import { Quote, Document } from "./types"; import { Quote, Document } from "./types";
import { LoadingAnimation } from "../Loading"; import { LoadingAnimation } from "../Loading";
import { GithubIcon } from "../icons/icons"; import { getSourceIcon } from "../source";
interface SearchResultsDisplayProps { interface SearchResultsDisplayProps {
answer: string | null; answer: string | null;
@@ -12,24 +10,6 @@ interface SearchResultsDisplayProps {
isFetching: boolean; isFetching: boolean;
} }
const ICON_SIZE = "20";
const ICON_STYLE = "text-blue-600 my-auto mr-1 flex flex-shrink-0";
const getSourceIcon = (sourceType: string) => {
switch (sourceType) {
case "web":
return <Globe size={ICON_SIZE} className={ICON_STYLE} />;
case "slack":
return <SlackLogo size={ICON_SIZE} className={ICON_STYLE} />;
case "google_drive":
return <GoogleDriveLogo size={ICON_SIZE} className={ICON_STYLE} />;
case "github":
return <GithubIcon size={ICON_SIZE} className={ICON_STYLE} />;
default:
return null;
}
};
export const SearchResultsDisplay: React.FC<SearchResultsDisplayProps> = ({ export const SearchResultsDisplay: React.FC<SearchResultsDisplayProps> = ({
answer, answer,
quotes, quotes,
@@ -76,7 +56,7 @@ export const SearchResultsDisplay: React.FC<SearchResultsDisplayProps> = ({
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
{getSourceIcon(quoteInfo.source_type)} {getSourceIcon(quoteInfo.source_type, "20")}
<p className="truncate break-all"> <p className="truncate break-all">
{quoteInfo.semantic_identifier || quoteInfo.document_id} {quoteInfo.semantic_identifier || quoteInfo.document_id}
</p> </p>
@@ -103,7 +83,7 @@ export const SearchResultsDisplay: React.FC<SearchResultsDisplayProps> = ({
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
{getSourceIcon(doc.source_type)} {getSourceIcon(doc.source_type, "20")}
<p className="truncate break-all"> <p className="truncate break-all">
{doc.semantic_identifier || doc.document_id} {doc.semantic_identifier || doc.document_id}
</p> </p>

View File

@@ -1,7 +1,9 @@
import { ValidSources } from "@/lib/types";
export interface Quote { export interface Quote {
document_id: string; document_id: string;
link: string; link: string;
source_type: string; source_type: ValidSources;
blurb: string; blurb: string;
semantic_identifier: string | null; semantic_identifier: string | null;
} }
@@ -9,7 +11,7 @@ export interface Quote {
export interface Document { export interface Document {
document_id: string; document_id: string;
link: string; link: string;
source_type: string; source_type: ValidSources;
blurb: string; blurb: string;
semantic_identifier: string | null; semantic_identifier: string | null;
} }

View File

@@ -0,0 +1,56 @@
import { ValidSources } from "@/lib/types";
import {
GithubIcon,
GlobeIcon,
GoogleDriveIcon,
SlackIcon,
} from "./icons/icons";
interface SourceMetadata {
icon: React.FC<{ size?: string; className?: string }>;
displayName: string;
adminPageLink: string;
}
export const getSourceMetadata = (sourceType: ValidSources): SourceMetadata => {
switch (sourceType) {
case "web":
return {
icon: GlobeIcon,
displayName: "Web",
adminPageLink: "/admin/connectors/web",
};
case "slack":
return {
icon: SlackIcon,
displayName: "Slack",
adminPageLink: "/admin/connectors/slack",
};
case "google_drive":
return {
icon: GoogleDriveIcon,
displayName: "Google Drive",
adminPageLink: "/admin/connectors/google-drive",
};
case "github":
return {
icon: GithubIcon,
displayName: "Github PRs",
adminPageLink: "/admin/connectors/github",
};
default:
throw new Error("Invalid source type");
}
};
export const getSourceIcon = (sourceType: ValidSources, iconSize: string) => {
return getSourceMetadata(sourceType).icon({
size: iconSize,
});
};
export const getSourceDisplayName = (
sourceType: ValidSources
): string | null => {
return getSourceMetadata(sourceType).displayName;
};

View File

@@ -6,3 +6,5 @@ export interface User {
is_verified: string; is_verified: string;
role: "basic" | "admin"; role: "basic" | "admin";
} }
export type ValidSources = "web" | "github" | "slack" | "google_drive";