mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-09-28 12:58:41 +02:00
Make Google Drive connectors editable (#237)
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import { UpdateConnectorForm } from "@/components/admin/connectors/ConnectorForm";
|
||||
import { TextArrayFieldBuilder } from "@/components/admin/connectors/Field";
|
||||
import { XIcon } from "@/components/icons/icons";
|
||||
import { Connector, GoogleDriveConfig } from "@/lib/types";
|
||||
import * as Yup from "yup";
|
||||
import { googleDriveConnectorNameBuilder } from "./utils";
|
||||
|
||||
interface Props {
|
||||
existingConnector: Connector<GoogleDriveConfig>;
|
||||
onSubmit: () => void;
|
||||
}
|
||||
|
||||
export const ConnectorEditPopup = ({ existingConnector, onSubmit }: Props) => {
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
||||
onClick={onSubmit}
|
||||
>
|
||||
<div
|
||||
className="bg-gray-800 p-6 rounded border border-gray-700 shadow-lg relative w-1/2 text-sm"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="flex border-b border-gray-600 pb-2 mb-2">
|
||||
<h3 className="text-lg font-semibold w-full">
|
||||
Update Google Drive Connector
|
||||
</h3>
|
||||
<div onClick={onSubmit}>
|
||||
<XIcon
|
||||
size={30}
|
||||
className="my-auto flex flex-shrink-0 cursor-pointer hover:text-blue-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<UpdateConnectorForm<GoogleDriveConfig>
|
||||
nameBuilder={googleDriveConnectorNameBuilder}
|
||||
existingConnector={existingConnector}
|
||||
formBodyBuilder={TextArrayFieldBuilder({
|
||||
name: "folder_paths",
|
||||
label: "Folder Paths",
|
||||
})}
|
||||
validationSchema={Yup.object().shape({
|
||||
folder_paths: Yup.array()
|
||||
.of(
|
||||
Yup.string().required(
|
||||
"Please specify a folder path for your google drive e.g. 'Engineering/Materials'"
|
||||
)
|
||||
)
|
||||
.required(),
|
||||
})}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@@ -0,0 +1,150 @@
|
||||
import { Button } from "@/components/Button";
|
||||
import { BasicTable } from "@/components/admin/connectors/BasicTable";
|
||||
import { PopupSpec } from "@/components/admin/connectors/Popup";
|
||||
import { StatusRow } from "@/components/admin/connectors/table/ConnectorsTable";
|
||||
import { PencilIcon } from "@/components/icons/icons";
|
||||
import { deleteConnector } from "@/lib/connector";
|
||||
import { GoogleDriveConfig, ConnectorIndexingStatus } from "@/lib/types";
|
||||
import { useSWRConfig } from "swr";
|
||||
import { useState } from "react";
|
||||
import { ConnectorEditPopup } from "./ConnectorEditPopup";
|
||||
|
||||
interface EditableColumnProps {
|
||||
connectorIndexingStatus: ConnectorIndexingStatus<GoogleDriveConfig>;
|
||||
}
|
||||
|
||||
const EditableColumn = ({ connectorIndexingStatus }: EditableColumnProps) => {
|
||||
const { mutate } = useSWRConfig();
|
||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isEditing && (
|
||||
<ConnectorEditPopup
|
||||
existingConnector={connectorIndexingStatus.connector}
|
||||
onSubmit={() => {
|
||||
setIsEditing(false);
|
||||
mutate("/api/manage/admin/connector/indexing-status");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="flex w-4">
|
||||
<div
|
||||
onClick={() => {
|
||||
setIsEditing(true);
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<div className="mr-2">
|
||||
<PencilIcon size={20} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface TableProps {
|
||||
googleDriveConnectorIndexingStatuses: ConnectorIndexingStatus<GoogleDriveConfig>[];
|
||||
setPopup: (popupSpec: PopupSpec | null) => void;
|
||||
}
|
||||
|
||||
export const GoogleDriveConnectorsTable = ({
|
||||
googleDriveConnectorIndexingStatuses,
|
||||
setPopup,
|
||||
}: TableProps) => {
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
// Sorting to maintain a consistent ordering
|
||||
const sortedGoogleDriveConnectorIndexingStatuses = [
|
||||
...googleDriveConnectorIndexingStatuses,
|
||||
];
|
||||
sortedGoogleDriveConnectorIndexingStatuses.sort(
|
||||
(a, b) => a.connector.id - b.connector.id
|
||||
);
|
||||
|
||||
return (
|
||||
<BasicTable
|
||||
columns={[
|
||||
{
|
||||
header: "",
|
||||
key: "edit",
|
||||
width: 4,
|
||||
},
|
||||
{
|
||||
header: "Folder Paths",
|
||||
key: "folder_paths",
|
||||
},
|
||||
{
|
||||
header: "Status",
|
||||
key: "status",
|
||||
},
|
||||
{
|
||||
header: "Delete",
|
||||
key: "delete",
|
||||
},
|
||||
]}
|
||||
data={sortedGoogleDriveConnectorIndexingStatuses.map(
|
||||
(connectorIndexingStatus) => ({
|
||||
edit: (
|
||||
<EditableColumn connectorIndexingStatus={connectorIndexingStatus} />
|
||||
),
|
||||
folder_paths:
|
||||
(
|
||||
connectorIndexingStatus.connector.connector_specific_config
|
||||
.folder_paths || []
|
||||
).length > 0 ? (
|
||||
<div key={connectorIndexingStatus.connector.id}>
|
||||
{(
|
||||
connectorIndexingStatus.connector.connector_specific_config
|
||||
.folder_paths || []
|
||||
).map((path) => (
|
||||
<div key={path}>
|
||||
<i> - {path}</i>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<i>All Folders</i>
|
||||
),
|
||||
status: (
|
||||
<StatusRow
|
||||
connectorIndexingStatus={connectorIndexingStatus}
|
||||
hasCredentialsIssue={
|
||||
connectorIndexingStatus.connector.credential_ids.length === 0
|
||||
}
|
||||
setPopup={setPopup}
|
||||
onUpdate={() => {
|
||||
mutate("/api/manage/admin/connector/indexing-status");
|
||||
}}
|
||||
/>
|
||||
),
|
||||
delete: (
|
||||
<Button
|
||||
onClick={() => {
|
||||
deleteConnector(connectorIndexingStatus.connector.id).then(
|
||||
(errorMsg) => {
|
||||
if (errorMsg) {
|
||||
setPopup({
|
||||
message: `Unable to delete existing connector - ${errorMsg}`,
|
||||
type: "error",
|
||||
});
|
||||
} else {
|
||||
setPopup({
|
||||
message: "Successfully deleted connector!",
|
||||
type: "success",
|
||||
});
|
||||
mutate("/api/manage/admin/connector/indexing-status");
|
||||
}
|
||||
}
|
||||
);
|
||||
}}
|
||||
>
|
||||
Delete Connector
|
||||
</Button>
|
||||
),
|
||||
})
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
@@ -16,14 +16,14 @@ import {
|
||||
GoogleDriveConfig,
|
||||
GoogleDriveCredentialJson,
|
||||
} from "@/lib/types";
|
||||
import { deleteConnector } from "@/lib/connector";
|
||||
import { StatusRow } from "@/components/admin/connectors/table/ConnectorsTable";
|
||||
import { setupGoogleDriveOAuth } from "@/lib/googleDrive";
|
||||
import Cookies from "js-cookie";
|
||||
import { GOOGLE_DRIVE_AUTH_IS_ADMIN_COOKIE_NAME } from "@/lib/constants";
|
||||
import { deleteCredential, linkCredential } from "@/lib/credential";
|
||||
import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
|
||||
import { TextArrayFieldBuilder } from "@/components/admin/connectors/Field";
|
||||
import { GoogleDriveConnectorsTable } from "./GoogleDriveConnectorsTable";
|
||||
import { googleDriveConnectorNameBuilder } from "./utils";
|
||||
|
||||
const AppCredentialUpload = ({
|
||||
setPopup,
|
||||
@@ -100,6 +100,7 @@ interface GoogleDriveConnectorManagementProps {
|
||||
| Credential<GoogleDriveCredentialJson>
|
||||
| undefined;
|
||||
googleDriveConnectorIndexingStatus: ConnectorIndexingStatus<GoogleDriveConfig> | null;
|
||||
googleDriveConnectorIndexingStatuses: ConnectorIndexingStatus<GoogleDriveConfig>[];
|
||||
credentialIsLinked: boolean;
|
||||
setPopup: (popupSpec: PopupSpec | null) => void;
|
||||
}
|
||||
@@ -107,6 +108,7 @@ interface GoogleDriveConnectorManagementProps {
|
||||
const GoogleDriveConnectorManagement = ({
|
||||
googleDrivePublicCredential,
|
||||
googleDriveConnectorIndexingStatus,
|
||||
googleDriveConnectorIndexingStatuses,
|
||||
credentialIsLinked,
|
||||
setPopup,
|
||||
}: GoogleDriveConnectorManagementProps) => {
|
||||
@@ -133,9 +135,7 @@ const GoogleDriveConnectorManagement = ({
|
||||
<div className="border-solid border-gray-600 border rounded-md p-6 mt-4">
|
||||
<h2 className="font-bold mb-3">Add Connector</h2>
|
||||
<ConnectorForm<GoogleDriveConfig>
|
||||
nameBuilder={(values) =>
|
||||
`GoogleDriveConnector-${values.folder_paths.join("_")}`
|
||||
}
|
||||
nameBuilder={googleDriveConnectorNameBuilder}
|
||||
source="google_drive"
|
||||
inputType="poll"
|
||||
formBodyBuilder={TextArrayFieldBuilder({
|
||||
@@ -205,39 +205,7 @@ const GoogleDriveConnectorManagement = ({
|
||||
return (
|
||||
<div>
|
||||
<div className="text-sm">
|
||||
<div className="flex">
|
||||
The Google Drive connector is setup! <b className="mx-2">Status:</b>{" "}
|
||||
<StatusRow
|
||||
connectorIndexingStatus={googleDriveConnectorIndexingStatus}
|
||||
hasCredentialsIssue={
|
||||
googleDriveConnectorIndexingStatus.connector.credential_ids
|
||||
.length === 0
|
||||
}
|
||||
setPopup={setPopup}
|
||||
onUpdate={() => {
|
||||
mutate("/api/manage/admin/connector/indexing-status");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/* Need to do the seemingly unnecessary handling for undefined `folder_paths` for backwards compatibility */}
|
||||
{(
|
||||
googleDriveConnectorIndexingStatus.connector.connector_specific_config
|
||||
.folder_paths || []
|
||||
).length > 0 && (
|
||||
<div className="mt-3">
|
||||
It is setup to index the following folders:{" "}
|
||||
<div className="mx-2">
|
||||
{googleDriveConnectorIndexingStatus.connector.connector_specific_config.folder_paths.map(
|
||||
(path) => (
|
||||
<div key={path}>
|
||||
- <i>{path}</i>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<p className="mt-3">
|
||||
<p className="my-3">
|
||||
Checkout the{" "}
|
||||
<a href="/admin/indexing/status" className="text-blue-500">
|
||||
status page
|
||||
@@ -246,29 +214,58 @@ const GoogleDriveConnectorManagement = ({
|
||||
Google Drive every <b>10</b> minutes.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
deleteConnector(googleDriveConnectorIndexingStatus.connector.id).then(
|
||||
(errorMsg) => {
|
||||
if (errorMsg) {
|
||||
setPopup({
|
||||
message: `Unable to delete existing connector - ${errorMsg}`,
|
||||
type: "error",
|
||||
});
|
||||
} else {
|
||||
setPopup({
|
||||
message: "Successfully deleted connector!",
|
||||
type: "success",
|
||||
});
|
||||
mutate("/api/manage/admin/connector/indexing-status");
|
||||
}
|
||||
{googleDriveConnectorIndexingStatuses.length > 0 && (
|
||||
<div className="text-sm mb-2 font-bold">Existing Connectors:</div>
|
||||
)}
|
||||
<GoogleDriveConnectorsTable
|
||||
googleDriveConnectorIndexingStatuses={
|
||||
googleDriveConnectorIndexingStatuses
|
||||
}
|
||||
setPopup={setPopup}
|
||||
/>
|
||||
<h2 className="font-bold mt-3 text-sm">Add New Connector:</h2>
|
||||
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2">
|
||||
<ConnectorForm<GoogleDriveConfig>
|
||||
nameBuilder={(values) =>
|
||||
`GoogleDriveConnector-${
|
||||
values.folder_paths && values.folder_paths.join("_")
|
||||
}`
|
||||
}
|
||||
source="google_drive"
|
||||
inputType="poll"
|
||||
formBodyBuilder={TextArrayFieldBuilder({
|
||||
name: "folder_paths",
|
||||
label: "Folder Paths",
|
||||
subtext:
|
||||
"Specify 0 or more folder paths to index! For example, specifying the path " +
|
||||
"'Engineering/Materials' will cause us to only index all files contained " +
|
||||
"within the 'Materials' folder within the 'Engineering' folder. " +
|
||||
"If no folder paths are specified, we will index all documents in your drive.",
|
||||
})}
|
||||
validationSchema={Yup.object().shape({
|
||||
folder_paths: Yup.array()
|
||||
.of(
|
||||
Yup.string().required(
|
||||
"Please specify a folder path for your google drive e.g. 'Engineering/Materials'"
|
||||
)
|
||||
)
|
||||
.required(),
|
||||
})}
|
||||
initialValues={{
|
||||
folder_paths: [],
|
||||
}}
|
||||
refreshFreq={10 * 60} // 10 minutes
|
||||
onSubmit={async (isSuccess, responseJson) => {
|
||||
if (isSuccess && responseJson) {
|
||||
await linkCredential(
|
||||
responseJson.id,
|
||||
googleDrivePublicCredential.id
|
||||
);
|
||||
mutate("/api/manage/admin/connector/indexing-status");
|
||||
}
|
||||
);
|
||||
}}
|
||||
className="mt-2"
|
||||
>
|
||||
Delete Connector
|
||||
</Button>
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -486,6 +483,9 @@ const Main = () => {
|
||||
<GoogleDriveConnectorManagement
|
||||
googleDrivePublicCredential={googleDrivePublicCredential}
|
||||
googleDriveConnectorIndexingStatus={googleDriveConnectorIndexingStatus}
|
||||
googleDriveConnectorIndexingStatuses={
|
||||
googleDriveConnectorIndexingStatuses
|
||||
}
|
||||
credentialIsLinked={credentialIsLinked}
|
||||
setPopup={setPopupWithExpiration}
|
||||
/>
|
||||
|
6
web/src/app/admin/connectors/google-drive/utils.ts
Normal file
6
web/src/app/admin/connectors/google-drive/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { GoogleDriveConfig } from "@/lib/types";
|
||||
|
||||
export const googleDriveConnectorNameBuilder = (values: GoogleDriveConfig) =>
|
||||
`GoogleDriveConnector-${
|
||||
values.folder_paths && values.folder_paths.join("_")
|
||||
}`;
|
@@ -3,6 +3,7 @@ import React, { FC } from "react";
|
||||
type Column = {
|
||||
header: string;
|
||||
key: string;
|
||||
width?: number;
|
||||
};
|
||||
|
||||
type TableData = {
|
||||
@@ -26,7 +27,8 @@ export const BasicTable: FC<BasicTableProps> = ({ columns, data }) => {
|
||||
className={
|
||||
"px-4 py-2 font-bold" +
|
||||
(index === 0 ? " rounded-tl-sm" : "") +
|
||||
(index === columns.length - 1 ? " rounded-tr-sm" : "")
|
||||
(index === columns.length - 1 ? " rounded-tr-sm" : "") +
|
||||
(column.width ? ` w-${column.width}` : "")
|
||||
}
|
||||
>
|
||||
{column.header}
|
||||
@@ -41,7 +43,10 @@ export const BasicTable: FC<BasicTableProps> = ({ columns, data }) => {
|
||||
return (
|
||||
<td
|
||||
key={colIndex}
|
||||
className="py-2 px-4 border-b border-gray-800"
|
||||
className={
|
||||
"py-2 px-4 border-b border-gray-800" +
|
||||
(column.width ? ` w-${column.width}` : "")
|
||||
}
|
||||
>
|
||||
{row[column.key]}
|
||||
</td>
|
||||
|
@@ -11,18 +11,26 @@ import {
|
||||
import { deleteConnectorIfExists } from "@/lib/connector";
|
||||
import { FormBodyBuilder, RequireAtLeastOne } from "./types";
|
||||
|
||||
const BASE_CONNECTOR_URL = "/api/manage/admin/connector";
|
||||
|
||||
export async function submitConnector<T>(
|
||||
connector: ConnectorBase<T>
|
||||
connector: ConnectorBase<T>,
|
||||
connectorId?: number
|
||||
): Promise<{ message: string; isSuccess: boolean; response?: Connector<T> }> {
|
||||
const isUpdate = connectorId !== undefined;
|
||||
|
||||
let isSuccess = false;
|
||||
try {
|
||||
const response = await fetch(`/api/manage/admin/connector`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(connector),
|
||||
});
|
||||
const response = await fetch(
|
||||
BASE_CONNECTOR_URL + (isUpdate ? `/${connectorId}` : ""),
|
||||
{
|
||||
method: isUpdate ? "PATCH" : "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(connector),
|
||||
}
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
isSuccess = true;
|
||||
@@ -139,3 +147,88 @@ export function ConnectorForm<T extends Yup.AnyObject>({
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface UpdateConnectorBaseProps<T extends Yup.AnyObject> {
|
||||
nameBuilder?: (values: T) => string;
|
||||
existingConnector: Connector<T>;
|
||||
// If both are specified, uses formBody
|
||||
formBody?: JSX.Element | null;
|
||||
formBodyBuilder?: FormBodyBuilder<T>;
|
||||
validationSchema: Yup.ObjectSchema<T>;
|
||||
onSubmit?: (isSuccess: boolean, responseJson?: Connector<T>) => void;
|
||||
}
|
||||
|
||||
type UpdateConnectorFormProps<T extends Yup.AnyObject> = RequireAtLeastOne<
|
||||
UpdateConnectorBaseProps<T>,
|
||||
"formBody" | "formBodyBuilder"
|
||||
>;
|
||||
|
||||
export function UpdateConnectorForm<T extends Yup.AnyObject>({
|
||||
nameBuilder,
|
||||
existingConnector,
|
||||
formBody,
|
||||
formBodyBuilder,
|
||||
validationSchema,
|
||||
onSubmit,
|
||||
}: UpdateConnectorFormProps<T>): JSX.Element {
|
||||
const [popup, setPopup] = useState<{
|
||||
message: string;
|
||||
type: "success" | "error";
|
||||
} | null>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
{popup && <Popup message={popup.message} type={popup.type} />}
|
||||
<Formik
|
||||
initialValues={existingConnector.connector_specific_config}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={async (values, formikHelpers) => {
|
||||
formikHelpers.setSubmitting(true);
|
||||
|
||||
const { message, isSuccess, response } = await submitConnector<T>(
|
||||
{
|
||||
name: nameBuilder ? nameBuilder(values) : existingConnector.name,
|
||||
source: existingConnector.source,
|
||||
input_type: existingConnector.input_type,
|
||||
connector_specific_config: values,
|
||||
refresh_freq: existingConnector.refresh_freq,
|
||||
disabled: false,
|
||||
},
|
||||
existingConnector.id
|
||||
);
|
||||
|
||||
setPopup({ message, type: isSuccess ? "success" : "error" });
|
||||
formikHelpers.setSubmitting(false);
|
||||
if (isSuccess) {
|
||||
formikHelpers.resetForm();
|
||||
}
|
||||
setTimeout(() => {
|
||||
setPopup(null);
|
||||
}, 4000);
|
||||
if (onSubmit) {
|
||||
onSubmit(isSuccess, response);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting, values }) => (
|
||||
<Form>
|
||||
{formBody ? formBody : formBodyBuilder && formBodyBuilder(values)}
|
||||
<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 max-w-sm mx-auto"
|
||||
}
|
||||
>
|
||||
Update
|
||||
</button>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@@ -10,6 +10,8 @@ import {
|
||||
Link,
|
||||
Plug,
|
||||
Brain,
|
||||
PencilSimple,
|
||||
X,
|
||||
} from "@phosphor-icons/react";
|
||||
import { SiBookstack } from "react-icons/si";
|
||||
import { FaFile, FaGlobe } from "react-icons/fa";
|
||||
@@ -101,6 +103,20 @@ export const BrainIcon = ({
|
||||
return <Brain size={size} className={className} />;
|
||||
};
|
||||
|
||||
export const PencilIcon = ({
|
||||
size = 16,
|
||||
className = defaultTailwindCSS,
|
||||
}: IconProps) => {
|
||||
return <PencilSimple size={size} className={className} />;
|
||||
};
|
||||
|
||||
export const XIcon = ({
|
||||
size = 16,
|
||||
className = defaultTailwindCSS,
|
||||
}: IconProps) => {
|
||||
return <X size={size} className={className} />;
|
||||
};
|
||||
|
||||
//
|
||||
// COMPANY LOGOS
|
||||
//
|
||||
|
@@ -48,7 +48,7 @@ export interface GithubConfig {
|
||||
}
|
||||
|
||||
export interface GoogleDriveConfig {
|
||||
folder_paths: string[];
|
||||
folder_paths?: string[];
|
||||
}
|
||||
|
||||
export interface BookstackConfig {}
|
||||
|
Reference in New Issue
Block a user