mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-10-11 05:36:03 +02:00
Add general status page + standardize the experience a bit
This commit is contained in:
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -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>
|
@@ -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")}
|
||||||
/>
|
/>
|
||||||
|
@@ -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");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
154
web/src/app/admin/indexing/status/page.tsx
Normal file
154
web/src/app/admin/indexing/status/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@@ -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: (
|
||||||
|
@@ -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;
|
||||||
|
@@ -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>
|
||||||
);
|
);
|
||||||
|
@@ -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";
|
||||||
|
|
||||||
|
@@ -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 }) => (
|
||||||
|
@@ -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";
|
|
@@ -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,
|
||||||
|
@@ -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>
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
56
web/src/components/source.tsx
Normal file
56
web/src/components/source.tsx
Normal 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;
|
||||||
|
};
|
@@ -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";
|
||||||
|
Reference in New Issue
Block a user