mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-09-20 13:05:49 +02:00
File connector (#93)
* Initial backend changes for file connector * Add another background job to clean up files * UI + tweaks for backend
This commit is contained in:
59
web/package-lock.json
generated
59
web/package-lock.json
generated
@@ -22,6 +22,7 @@
|
||||
"postcss": "^8.4.23",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-icons": "^4.8.0",
|
||||
"swr": "^2.1.5",
|
||||
"tailwindcss": "^3.3.1",
|
||||
@@ -713,6 +714,14 @@
|
||||
"resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz",
|
||||
"integrity": "sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag=="
|
||||
},
|
||||
"node_modules/attr-accept": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz",
|
||||
"integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/autoprefixer": {
|
||||
"version": "10.4.14",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz",
|
||||
@@ -1744,6 +1753,17 @@
|
||||
"node": "^10.12.0 || >=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/file-selector": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz",
|
||||
"integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
@@ -3267,6 +3287,22 @@
|
||||
"react": "^18.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dropzone": {
|
||||
"version": "14.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz",
|
||||
"integrity": "sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==",
|
||||
"dependencies": {
|
||||
"attr-accept": "^2.2.2",
|
||||
"file-selector": "^0.6.0",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.8 || 18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-fast-compare": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
|
||||
@@ -4562,6 +4598,11 @@
|
||||
"resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz",
|
||||
"integrity": "sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag=="
|
||||
},
|
||||
"attr-accept": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz",
|
||||
"integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg=="
|
||||
},
|
||||
"autoprefixer": {
|
||||
"version": "10.4.14",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz",
|
||||
@@ -5311,6 +5352,14 @@
|
||||
"flat-cache": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"file-selector": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz",
|
||||
"integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==",
|
||||
"requires": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
@@ -6315,6 +6364,16 @@
|
||||
"scheduler": "^0.23.0"
|
||||
}
|
||||
},
|
||||
"react-dropzone": {
|
||||
"version": "14.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz",
|
||||
"integrity": "sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==",
|
||||
"requires": {
|
||||
"attr-accept": "^2.2.2",
|
||||
"file-selector": "^0.6.0",
|
||||
"prop-types": "^15.8.1"
|
||||
}
|
||||
},
|
||||
"react-fast-compare": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
|
||||
|
@@ -23,6 +23,7 @@
|
||||
"postcss": "^8.4.23",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-icons": "^4.8.0",
|
||||
"swr": "^2.1.5",
|
||||
"tailwindcss": "^3.3.1",
|
||||
|
58
web/src/app/admin/connectors/file/FileUpload.tsx
Normal file
58
web/src/app/admin/connectors/file/FileUpload.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
// components/FileUpload.tsx
|
||||
import { ChangeEvent, FC, useState } from "react";
|
||||
import React from "react";
|
||||
import Dropzone from "react-dropzone";
|
||||
|
||||
interface FileUploadProps {
|
||||
selectedFiles: File[];
|
||||
setSelectedFiles: (files: File[]) => void;
|
||||
}
|
||||
|
||||
export const FileUpload: FC<FileUploadProps> = ({
|
||||
selectedFiles,
|
||||
setSelectedFiles,
|
||||
}) => {
|
||||
const [dragActive, setDragActive] = useState(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Dropzone
|
||||
onDrop={(acceptedFiles) => {
|
||||
setSelectedFiles(acceptedFiles);
|
||||
setDragActive(false);
|
||||
}}
|
||||
onDragLeave={() => setDragActive(false)}
|
||||
onDragEnter={() => setDragActive(true)}
|
||||
>
|
||||
{({ getRootProps, getInputProps }) => (
|
||||
<section>
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className={
|
||||
"flex flex-col items-center w-full px-4 py-12 rounded " +
|
||||
"shadow-lg tracking-wide border border-gray-700 cursor-pointer" +
|
||||
(dragActive ? " border-blue-500" : "")
|
||||
}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
<b>Drag and drop some files here, or click to select files</b>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</Dropzone>
|
||||
|
||||
{selectedFiles.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<h2 className="font-bold">Selected Files</h2>
|
||||
<ul>
|
||||
{selectedFiles.map((file) => (
|
||||
<div key={file.name} className="flex">
|
||||
<p className="text-sm mr-2">{file.name}</p>
|
||||
</div>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
262
web/src/app/admin/connectors/file/page.tsx
Normal file
262
web/src/app/admin/connectors/file/page.tsx
Normal file
@@ -0,0 +1,262 @@
|
||||
"use client";
|
||||
|
||||
import useSWR, { useSWRConfig } from "swr";
|
||||
|
||||
import { FileIcon } from "@/components/icons/icons";
|
||||
import { fetcher } from "@/lib/fetcher";
|
||||
import { HealthCheckBanner } from "@/components/health/healthcheck";
|
||||
import { ConnectorIndexingStatus, FileConfig } from "@/lib/types";
|
||||
import { linkCredential } from "@/lib/credential";
|
||||
import { FileUpload } from "./FileUpload";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/Button";
|
||||
import { Popup, PopupSpec } from "@/components/admin/connectors/Popup";
|
||||
import { createConnector, runConnector } from "@/lib/connector";
|
||||
import { BasicTable } from "@/components/admin/connectors/BasicTable";
|
||||
import { CheckCircle, XCircle } from "@phosphor-icons/react";
|
||||
import { Spinner } from "@/components/Spinner";
|
||||
|
||||
const COLUMNS = [
|
||||
{ header: "File names", key: "fileNames" },
|
||||
{ header: "Status", key: "status" },
|
||||
];
|
||||
|
||||
const getNameFromPath = (path: string) => {
|
||||
const pathParts = path.split("/");
|
||||
return pathParts[pathParts.length - 1];
|
||||
};
|
||||
|
||||
export default function File() {
|
||||
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
|
||||
const [filesAreUploading, setFilesAreUploading] = useState<boolean>(false);
|
||||
const [popup, setPopup] = useState<{
|
||||
message: string;
|
||||
type: "success" | "error";
|
||||
} | null>(null);
|
||||
const setPopupWithExpiration = (popupSpec: PopupSpec | null) => {
|
||||
setPopup(popupSpec);
|
||||
setTimeout(() => {
|
||||
setPopup(null);
|
||||
}, 4000);
|
||||
};
|
||||
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
const { data: connectorIndexingStatuses } = useSWR<
|
||||
ConnectorIndexingStatus<any>[]
|
||||
>("/api/manage/admin/connector/indexing-status", fetcher);
|
||||
|
||||
const fileIndexingStatuses: ConnectorIndexingStatus<FileConfig>[] =
|
||||
connectorIndexingStatuses?.filter(
|
||||
(connectorIndexingStatus) =>
|
||||
connectorIndexingStatus.connector.source === "file"
|
||||
) ?? [];
|
||||
|
||||
const inProgressFileIndexingStatuses =
|
||||
fileIndexingStatuses.filter(
|
||||
(connectorIndexingStatus) =>
|
||||
connectorIndexingStatus.last_status === "in_progress" ||
|
||||
connectorIndexingStatus.last_status === "not_started"
|
||||
) ?? [];
|
||||
|
||||
const successfulFileIndexingStatuses = fileIndexingStatuses.filter(
|
||||
(connectorIndexingStatus) =>
|
||||
connectorIndexingStatus.last_status === "success"
|
||||
);
|
||||
|
||||
const failedFileIndexingStatuses = fileIndexingStatuses.filter(
|
||||
(connectorIndexingStatus) =>
|
||||
connectorIndexingStatus.last_status === "failed"
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mx-auto container">
|
||||
<div className="mb-4">
|
||||
<HealthCheckBanner />
|
||||
</div>
|
||||
<div className="border-solid border-gray-600 border-b pb-2 mb-4 flex">
|
||||
<FileIcon size="32" />
|
||||
<h1 className="text-3xl font-bold pl-2">File</h1>
|
||||
</div>
|
||||
{popup && <Popup message={popup.message} type={popup.type} />}
|
||||
{filesAreUploading && <Spinner />}
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">Upload Files</h2>
|
||||
<p className="text-sm mb-2">
|
||||
Specify files below, click the <b>Upload</b> button, and the contents of
|
||||
these files will be searchable via Danswer!
|
||||
</p>
|
||||
<div className="flex">
|
||||
<div className="mx-auto max-w-3xl w-full">
|
||||
<FileUpload
|
||||
selectedFiles={selectedFiles}
|
||||
setSelectedFiles={setSelectedFiles}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="mt-4 w-48"
|
||||
fullWidth
|
||||
disabled={selectedFiles.length === 0}
|
||||
onClick={async () => {
|
||||
const uploadCreateAndTriggerConnector = async () => {
|
||||
const formData = new FormData();
|
||||
|
||||
selectedFiles.forEach((file) => {
|
||||
formData.append("files", file);
|
||||
});
|
||||
|
||||
const response = await fetch(
|
||||
"/api/manage/admin/connector/file/upload",
|
||||
{ method: "POST", body: formData }
|
||||
);
|
||||
const responseJson = await response.json();
|
||||
if (!response.ok) {
|
||||
setPopupWithExpiration({
|
||||
message: `Unable to upload files - ${responseJson.detail}`,
|
||||
type: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const filePaths = responseJson.file_paths as string[];
|
||||
const [connectorErrorMsg, connector] =
|
||||
await createConnector<FileConfig>({
|
||||
name: "FileConnector-" + Date.now(),
|
||||
source: "file",
|
||||
input_type: "load_state",
|
||||
connector_specific_config: {
|
||||
file_locations: filePaths,
|
||||
},
|
||||
refresh_freq: null,
|
||||
disabled: false,
|
||||
});
|
||||
if (connectorErrorMsg || !connector) {
|
||||
setPopupWithExpiration({
|
||||
message: `Unable to create connector - ${connectorErrorMsg}`,
|
||||
type: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const credentialResponse = await linkCredential(
|
||||
connector.id,
|
||||
0
|
||||
);
|
||||
if (credentialResponse.detail) {
|
||||
setPopupWithExpiration({
|
||||
message: `Unable to link connector to credential - ${credentialResponse.detail}`,
|
||||
type: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const runConnectorErrorMsg = await runConnector(connector.id, [
|
||||
0,
|
||||
]);
|
||||
if (runConnectorErrorMsg) {
|
||||
setPopupWithExpiration({
|
||||
message: `Unable to run connector - ${runConnectorErrorMsg}`,
|
||||
type: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
mutate("/api/manage/admin/connector/indexing-status");
|
||||
setSelectedFiles([]);
|
||||
setPopupWithExpiration({
|
||||
type: "success",
|
||||
message: "Successfully uploaded files!",
|
||||
});
|
||||
};
|
||||
|
||||
setFilesAreUploading(true);
|
||||
try {
|
||||
await uploadCreateAndTriggerConnector();
|
||||
} catch (e) {
|
||||
console.log("Failed to index filels: ", e);
|
||||
}
|
||||
setFilesAreUploading(false);
|
||||
}}
|
||||
>
|
||||
Upload!
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{inProgressFileIndexingStatuses.length > 0 && (
|
||||
<>
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
In Progress File Indexing
|
||||
</h2>
|
||||
<BasicTable
|
||||
columns={COLUMNS}
|
||||
data={inProgressFileIndexingStatuses.map(
|
||||
(connectorIndexingStatus) => {
|
||||
return {
|
||||
fileNames:
|
||||
connectorIndexingStatus.connector.connector_specific_config.file_locations
|
||||
.map(getNameFromPath)
|
||||
.join(", "),
|
||||
status: "In Progress",
|
||||
};
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{successfulFileIndexingStatuses.length > 0 && (
|
||||
<>
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
Successful File Indexing
|
||||
</h2>
|
||||
<BasicTable
|
||||
columns={COLUMNS}
|
||||
data={successfulFileIndexingStatuses.map(
|
||||
(connectorIndexingStatus) => {
|
||||
return {
|
||||
fileNames:
|
||||
connectorIndexingStatus.connector.connector_specific_config.file_locations
|
||||
.map(getNameFromPath)
|
||||
.join(", "),
|
||||
status: (
|
||||
<div className="text-emerald-600 flex">
|
||||
<CheckCircle className="my-auto mr-1" size="18" /> Success
|
||||
</div>
|
||||
),
|
||||
};
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{failedFileIndexingStatuses.length > 0 && (
|
||||
<>
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
Failed File Indexing
|
||||
</h2>
|
||||
<p className="text-sm mb-3">
|
||||
The following files failed to be indexed. Please contact an
|
||||
administrator to resolve this issue.
|
||||
</p>
|
||||
<BasicTable
|
||||
columns={COLUMNS}
|
||||
data={failedFileIndexingStatuses.map((connectorIndexingStatus) => {
|
||||
return {
|
||||
fileNames:
|
||||
connectorIndexingStatus.connector.connector_specific_config.file_locations
|
||||
.map(getNameFromPath)
|
||||
.join(", "),
|
||||
status: (
|
||||
<div className="text-red-600 flex">
|
||||
<XCircle className="my-auto mr-1" size="18" /> Failed
|
||||
</div>
|
||||
),
|
||||
};
|
||||
})}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -8,6 +8,7 @@ import {
|
||||
SlackIcon,
|
||||
KeyIcon,
|
||||
ConfluenceIcon,
|
||||
FileIcon,
|
||||
} from "@/components/icons/icons";
|
||||
import { DISABLE_AUTH } from "@/lib/constants";
|
||||
import { getCurrentUserSS } from "@/lib/userSS";
|
||||
@@ -62,15 +63,6 @@ export default async function AdminLayout({
|
||||
),
|
||||
link: "/admin/connectors/slack",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<GlobeIcon size="16" />
|
||||
<div className="ml-1">Web</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/connectors/web",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
@@ -98,6 +90,24 @@ export default async function AdminLayout({
|
||||
),
|
||||
link: "/admin/connectors/confluence",
|
||||
},
|
||||
{
|
||||
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",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@@ -3,6 +3,7 @@ interface Props {
|
||||
children: JSX.Element | string;
|
||||
disabled?: boolean;
|
||||
fullWidth?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Button = ({
|
||||
@@ -10,6 +11,7 @@ export const Button = ({
|
||||
children,
|
||||
disabled = false,
|
||||
fullWidth = false,
|
||||
className = "",
|
||||
}: Props) => {
|
||||
return (
|
||||
<button
|
||||
@@ -17,9 +19,11 @@ export const Button = ({
|
||||
"group relative " +
|
||||
(fullWidth ? "w-full " : "") +
|
||||
"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"
|
||||
"font-medium rounded-md text-white " +
|
||||
"focus:outline-none focus:ring-2 " +
|
||||
"focus:ring-offset-2 focus:ring-red-500 mx-auto " +
|
||||
(disabled ? "bg-gray-700 " : "bg-red-800 hover:bg-red-900 ") +
|
||||
className
|
||||
}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
|
9
web/src/components/Spinner.tsx
Normal file
9
web/src/components/Spinner.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import "./spinner.css";
|
||||
|
||||
export const Spinner = () => {
|
||||
return (
|
||||
<div className="fixed top-0 left-0 z-50 w-screen h-screen bg-black bg-opacity-50 flex items-center justify-center">
|
||||
<div className="loader ease-linear rounded-full border-8 border-t-8 border-gray-200 h-8 w-8"></div>
|
||||
</div>
|
||||
);
|
||||
};
|
@@ -11,7 +11,7 @@ import {
|
||||
Plug,
|
||||
} from "@phosphor-icons/react";
|
||||
import { SiConfluence, SiGithub, SiGoogledrive, SiSlack } from "react-icons/si";
|
||||
import { FaGlobe } from "react-icons/fa";
|
||||
import { FaFile, FaGlobe } from "react-icons/fa";
|
||||
|
||||
interface IconProps {
|
||||
size?: string;
|
||||
@@ -76,6 +76,13 @@ export const GlobeIcon = ({
|
||||
return <FaGlobe size={size} className={className} />;
|
||||
};
|
||||
|
||||
export const FileIcon = ({
|
||||
size = "16",
|
||||
className = defaultTailwindCSS,
|
||||
}: IconProps) => {
|
||||
return <FaFile size={size} className={className} />;
|
||||
};
|
||||
|
||||
export const SlackIcon = ({
|
||||
size = "16",
|
||||
className = defaultTailwindCSS,
|
||||
|
@@ -10,6 +10,7 @@ const sources: Source[] = [
|
||||
{ displayName: "Confluence", internalName: "confluence" },
|
||||
{ displayName: "Github PRs", internalName: "github" },
|
||||
{ displayName: "Web", internalName: "web" },
|
||||
{ displayName: "File", internalName: "file" },
|
||||
];
|
||||
|
||||
interface SourceSelectorProps {
|
||||
|
@@ -131,7 +131,10 @@ export const SearchResultsDisplay: React.FC<SearchResultsDisplayProps> = ({
|
||||
className="text-sm border-b border-gray-800 mb-3"
|
||||
>
|
||||
<a
|
||||
className="rounded-lg flex font-bold"
|
||||
className={
|
||||
"rounded-lg flex font-bold " +
|
||||
(doc.link ? "" : "pointer-events-none")
|
||||
}
|
||||
href={doc.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { ValidSources } from "@/lib/types";
|
||||
import {
|
||||
ConfluenceIcon,
|
||||
FileIcon,
|
||||
GithubIcon,
|
||||
GlobeIcon,
|
||||
GoogleDriveIcon,
|
||||
@@ -21,6 +22,12 @@ export const getSourceMetadata = (sourceType: ValidSources): SourceMetadata => {
|
||||
displayName: "Web",
|
||||
adminPageLink: "/admin/connectors/web",
|
||||
};
|
||||
case "file":
|
||||
return {
|
||||
icon: FileIcon,
|
||||
displayName: "File",
|
||||
adminPageLink: "/admin/connectors/file",
|
||||
};
|
||||
case "slack":
|
||||
return {
|
||||
icon: SlackIcon,
|
||||
|
23
web/src/components/spinner.css
Normal file
23
web/src/components/spinner.css
Normal file
@@ -0,0 +1,23 @@
|
||||
.loader {
|
||||
border-top-color: #2876aa;
|
||||
-webkit-animation: spinner 1.5s linear infinite;
|
||||
animation: spinner 1.5s linear infinite;
|
||||
}
|
||||
|
||||
@-webkit-keyframes spinner {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spinner {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
@@ -1,8 +1,18 @@
|
||||
import { Connector, ConnectorBase } from "./types";
|
||||
|
||||
async function handleResponse(
|
||||
response: Response
|
||||
): Promise<[string | null, any]> {
|
||||
const responseJson = await response.json();
|
||||
if (response.ok) {
|
||||
return [null, responseJson];
|
||||
}
|
||||
return [responseJson.detail, null];
|
||||
}
|
||||
|
||||
export async function createConnector<T>(
|
||||
connector: ConnectorBase<T>
|
||||
): Promise<Connector<T>> {
|
||||
): Promise<[string | null, Connector<T> | null]> {
|
||||
const response = await fetch(`/api/manage/admin/connector`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -10,7 +20,7 @@ export async function createConnector<T>(
|
||||
},
|
||||
body: JSON.stringify(connector),
|
||||
});
|
||||
return response.json();
|
||||
return handleResponse(response);
|
||||
}
|
||||
|
||||
export async function updateConnector<T>(
|
||||
@@ -23,7 +33,7 @@ export async function updateConnector<T>(
|
||||
},
|
||||
body: JSON.stringify(connector),
|
||||
});
|
||||
return response.json();
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
export async function deleteConnector<T>(
|
||||
@@ -35,5 +45,20 @@ export async function deleteConnector<T>(
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
return response.json();
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
export async function runConnector(
|
||||
connectorId: number,
|
||||
credentialIds: number[] | null = null
|
||||
): Promise<string | null> {
|
||||
const response = await fetch("/api/manage/admin/connector/run-once", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ connector_id: connectorId, credentialIds }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
return (await response.json()).detail;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@@ -8,7 +8,7 @@ export async function deleteCredential<T>(credentialId: number) {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function linkCredential<T>(
|
||||
export async function linkCredential(
|
||||
connectorId: number,
|
||||
credentialId: number
|
||||
) {
|
||||
|
@@ -12,7 +12,8 @@ export type ValidSources =
|
||||
| "github"
|
||||
| "slack"
|
||||
| "google_drive"
|
||||
| "confluence";
|
||||
| "confluence"
|
||||
| "file";
|
||||
export type ValidInputTypes = "load_state" | "poll" | "event";
|
||||
|
||||
// CONNECTORS
|
||||
@@ -21,7 +22,7 @@ export interface ConnectorBase<T> {
|
||||
input_type: ValidInputTypes;
|
||||
source: ValidSources;
|
||||
connector_specific_config: T;
|
||||
refresh_freq: number;
|
||||
refresh_freq: number | null;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
@@ -49,6 +50,10 @@ export interface SlackConfig {
|
||||
workspace: string;
|
||||
}
|
||||
|
||||
export interface FileConfig {
|
||||
file_locations: string[];
|
||||
}
|
||||
|
||||
export interface ConnectorIndexingStatus<T> {
|
||||
connector: Connector<T>;
|
||||
public_doc: boolean;
|
||||
|
Reference in New Issue
Block a user