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(
connector_specific_config=index_attempt.connector_specific_config,
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_updated=index_attempt.time_updated,
docs_indexed=0

View File

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

View File

@ -17,11 +17,6 @@ export default function Page() {
<h1 className="text-3xl font-bold pl-2">Github PRs</h1>
</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 */}
<h2 className="text-xl font-bold pl-2 mb-2 mt-6 ml-auto mr-auto">
Request Indexing

View File

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

View File

@ -3,14 +3,12 @@ import { Formik, Form, FormikHelpers } from "formik";
import * as Yup from "yup";
import { Popup } from "../../../../components/admin/connectors/Popup";
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({
slack_bot_token: Yup.string().required("Please enter your Slack Bot Token"),
workspace_id: Yup.string().required("Please enter your Workspace ID"),
pull_frequency: Yup.number().required(
"Please enter a pull frequency (in minutes). 0 => no pulling from slack"
),
pull_frequency: Yup.number().optional(),
});
const handleSubmit = async (
@ -49,12 +47,12 @@ const handleSubmit = async (
return isSuccess;
};
interface SlackFormProps {
interface Props {
existingSlackConfig: SlackConfig;
onSubmit: (isSuccess: boolean) => void;
}
export const SlackForm: React.FC<SlackFormProps> = ({
export const InitialSetupForm: React.FC<Props> = ({
existingSlackConfig,
onSubmit,
}) => {
@ -79,14 +77,22 @@ export const SlackForm: React.FC<SlackFormProps> = ({
<Form>
<TextFormField name="slack_bot_token" label="Slack Bot Token:" />
<TextFormField name="workspace_id" label="Workspace ID:" />
<TextFormField name="pull_frequency" label="Pull Frequency:" />
<TextFormField
name="pull_frequency"
label="Pull Frequency (in minutes):"
/>
<div className="flex">
<button
type="submit"
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"
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>
)}
</Formik>

View File

@ -1,15 +1,11 @@
"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 { fetcher } from "@/lib/fetcher";
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 { InitialSetupForm } from "./InitialSetupForm";
const MainSection = () => {
// TODO: add back in once this is ready
@ -36,26 +32,15 @@ const MainSection = () => {
}
return (
<div>
<h2 className="text-xl font-bold pl-2 mb-2 mt-6 ml-auto mr-auto">
Status
</h2>
{
<ConnectorStatus
status={
data.pull_frequency !== 0
? ConnectorStatusEnum.Running
: 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
<div className="mx-auto">
<h2 className="text-xl font-bold mb-3 ml-auto mr-auto">Config</h2>
<p className="text-sm mb-4">
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
details on setting up the Danswer Slack App, see the docs here (TODO).
</p>
<div className="border border-gray-700 rounded-md p-3">
<InitialSetupForm
existingSlackConfig={data}
onSubmit={() => mutate("/api/admin/connectors/slack/config")}
/>

View File

@ -1,6 +1,6 @@
"use client";
import useSWR, { useSWRConfig } from "swr";
import useSWR from "swr";
import * as Yup from "yup";
import { BasicTable } from "@/components/admin/connectors/BasicTable";
@ -11,9 +11,10 @@ import { fetcher } from "@/lib/fetcher";
import {
IndexAttempt,
ListIndexingResponse,
} from "../../../../components/admin/connectors/interfaces";
} from "../../../../components/admin/connectors/types";
import { IndexForm } from "@/components/admin/connectors/Form";
import { TextFormField } from "@/components/admin/connectors/Field";
import { useRouter } from "next/navigation";
const COLUMNS = [
{ header: "Base URL", key: "url" },
@ -23,7 +24,8 @@ const COLUMNS = [
];
export default function Web() {
const { mutate } = useSWRConfig();
const router = useRouter();
const { data, isLoading, error } = useSWR<ListIndexingResponse>(
"/api/admin/connectors/web/index-attempt",
fetcher
@ -71,7 +73,7 @@ export default function Web() {
initialValues={{ base_url: "" }}
onSubmit={(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 { Sidebar } from "@/components/admin/connectors/Sidebar";
import {
NotebookIcon,
GithubIcon,
GlobeIcon,
GoogleDriveIcon,
@ -27,10 +28,24 @@ export default async function AdminLayout({
<Header user={user} />
<div className="bg-gray-900 pt-8 flex">
<Sidebar
title="Connectors"
title="Connector"
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: [
{
name: (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,12 @@
"use client";
import { ValidSources } from "@/lib/types";
import {
Globe,
SlackLogo,
GithubLogo,
GoogleDriveLogo,
Notebook,
} from "@phosphor-icons/react";
interface IconProps {
@ -14,6 +16,13 @@ interface IconProps {
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 = ({
size = "16",
className = defaultTailwindCSS,

View File

@ -1,9 +1,7 @@
import React from "react";
import { Globe, SlackLogo, GoogleDriveLogo } from "@phosphor-icons/react";
import "tailwindcss/tailwind.css";
import { Quote, Document } from "./types";
import { LoadingAnimation } from "../Loading";
import { GithubIcon } from "../icons/icons";
import { getSourceIcon } from "../source";
interface SearchResultsDisplayProps {
answer: string | null;
@ -12,24 +10,6 @@ interface SearchResultsDisplayProps {
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> = ({
answer,
quotes,
@ -76,7 +56,7 @@ export const SearchResultsDisplay: React.FC<SearchResultsDisplayProps> = ({
target="_blank"
rel="noopener noreferrer"
>
{getSourceIcon(quoteInfo.source_type)}
{getSourceIcon(quoteInfo.source_type, "20")}
<p className="truncate break-all">
{quoteInfo.semantic_identifier || quoteInfo.document_id}
</p>
@ -103,7 +83,7 @@ export const SearchResultsDisplay: React.FC<SearchResultsDisplayProps> = ({
target="_blank"
rel="noopener noreferrer"
>
{getSourceIcon(doc.source_type)}
{getSourceIcon(doc.source_type, "20")}
<p className="truncate break-all">
{doc.semantic_identifier || doc.document_id}
</p>

View File

@ -1,7 +1,9 @@
import { ValidSources } from "@/lib/types";
export interface Quote {
document_id: string;
link: string;
source_type: string;
source_type: ValidSources;
blurb: string;
semantic_identifier: string | null;
}
@ -9,7 +11,7 @@ export interface Quote {
export interface Document {
document_id: string;
link: string;
source_type: string;
source_type: ValidSources;
blurb: string;
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;
role: "basic" | "admin";
}
export type ValidSources = "web" | "github" | "slack" | "google_drive";