From 3b546ba1c3d7f934ac0d7d06f67baa90331275d8 Mon Sep 17 00:00:00 2001 From: Chris Weaver <25087905+Weves@users.noreply.github.com> Date: Wed, 26 Jul 2023 22:20:12 -0700 Subject: [PATCH] Make Google Drive connectors editable (#237) --- .../google-drive/ConnectorEditPopup.tsx | 55 +++++++ .../GoogleDriveConnectorsTable.tsx | 150 ++++++++++++++++++ .../admin/connectors/google-drive/page.tsx | 120 +++++++------- .../admin/connectors/google-drive/utils.ts | 6 + .../admin/connectors/BasicTable.tsx | 9 +- .../admin/connectors/ConnectorForm.tsx | 109 ++++++++++++- web/src/components/icons/icons.tsx | 16 ++ web/src/lib/types.ts | 2 +- 8 files changed, 396 insertions(+), 71 deletions(-) create mode 100644 web/src/app/admin/connectors/google-drive/ConnectorEditPopup.tsx create mode 100644 web/src/app/admin/connectors/google-drive/GoogleDriveConnectorsTable.tsx create mode 100644 web/src/app/admin/connectors/google-drive/utils.ts diff --git a/web/src/app/admin/connectors/google-drive/ConnectorEditPopup.tsx b/web/src/app/admin/connectors/google-drive/ConnectorEditPopup.tsx new file mode 100644 index 000000000000..1fc6a16b3cac --- /dev/null +++ b/web/src/app/admin/connectors/google-drive/ConnectorEditPopup.tsx @@ -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; + onSubmit: () => void; +} + +export const ConnectorEditPopup = ({ existingConnector, onSubmit }: Props) => { + return ( +
+
event.stopPropagation()} + > +
+

+ Update Google Drive Connector +

+
+ +
+
+ + 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} + /> +
+
+ ); +}; diff --git a/web/src/app/admin/connectors/google-drive/GoogleDriveConnectorsTable.tsx b/web/src/app/admin/connectors/google-drive/GoogleDriveConnectorsTable.tsx new file mode 100644 index 000000000000..eb5568945b1d --- /dev/null +++ b/web/src/app/admin/connectors/google-drive/GoogleDriveConnectorsTable.tsx @@ -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; +} + +const EditableColumn = ({ connectorIndexingStatus }: EditableColumnProps) => { + const { mutate } = useSWRConfig(); + const [isEditing, setIsEditing] = useState(false); + + return ( + <> + {isEditing && ( + { + setIsEditing(false); + mutate("/api/manage/admin/connector/indexing-status"); + }} + /> + )} +
+
{ + setIsEditing(true); + }} + className="cursor-pointer" + > +
+ +
+
+
+ + ); +}; + +interface TableProps { + googleDriveConnectorIndexingStatuses: ConnectorIndexingStatus[]; + 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 ( + ({ + edit: ( + + ), + folder_paths: + ( + connectorIndexingStatus.connector.connector_specific_config + .folder_paths || [] + ).length > 0 ? ( +
+ {( + connectorIndexingStatus.connector.connector_specific_config + .folder_paths || [] + ).map((path) => ( +
+ - {path} +
+ ))} +
+ ) : ( + All Folders + ), + status: ( + { + mutate("/api/manage/admin/connector/indexing-status"); + }} + /> + ), + delete: ( + + ), + }) + )} + /> + ); +}; diff --git a/web/src/app/admin/connectors/google-drive/page.tsx b/web/src/app/admin/connectors/google-drive/page.tsx index 872c883ac08b..34b0df491a6e 100644 --- a/web/src/app/admin/connectors/google-drive/page.tsx +++ b/web/src/app/admin/connectors/google-drive/page.tsx @@ -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 | undefined; googleDriveConnectorIndexingStatus: ConnectorIndexingStatus | null; + googleDriveConnectorIndexingStatuses: ConnectorIndexingStatus[]; 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 = ({

Add Connector

- nameBuilder={(values) => - `GoogleDriveConnector-${values.folder_paths.join("_")}` - } + nameBuilder={googleDriveConnectorNameBuilder} source="google_drive" inputType="poll" formBodyBuilder={TextArrayFieldBuilder({ @@ -205,39 +205,7 @@ const GoogleDriveConnectorManagement = ({ return (
-
- The Google Drive connector is setup! Status:{" "} - { - mutate("/api/manage/admin/connector/indexing-status"); - }} - /> -
- {/* Need to do the seemingly unnecessary handling for undefined `folder_paths` for backwards compatibility */} - {( - googleDriveConnectorIndexingStatus.connector.connector_specific_config - .folder_paths || [] - ).length > 0 && ( -
- It is setup to index the following folders:{" "} -
- {googleDriveConnectorIndexingStatus.connector.connector_specific_config.folder_paths.map( - (path) => ( -
- - {path} -
- ) - )} -
-
- )} -

+

Checkout the{" "} status page @@ -246,29 +214,58 @@ const GoogleDriveConnectorManagement = ({ Google Drive every 10 minutes.

- + }} + /> +
); }; @@ -486,6 +483,9 @@ const Main = () => { diff --git a/web/src/app/admin/connectors/google-drive/utils.ts b/web/src/app/admin/connectors/google-drive/utils.ts new file mode 100644 index 000000000000..ed7262e4496f --- /dev/null +++ b/web/src/app/admin/connectors/google-drive/utils.ts @@ -0,0 +1,6 @@ +import { GoogleDriveConfig } from "@/lib/types"; + +export const googleDriveConnectorNameBuilder = (values: GoogleDriveConfig) => + `GoogleDriveConnector-${ + values.folder_paths && values.folder_paths.join("_") + }`; diff --git a/web/src/components/admin/connectors/BasicTable.tsx b/web/src/components/admin/connectors/BasicTable.tsx index 8b2927b7cfb7..3cf00b044356 100644 --- a/web/src/components/admin/connectors/BasicTable.tsx +++ b/web/src/components/admin/connectors/BasicTable.tsx @@ -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 = ({ 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 = ({ columns, data }) => { return ( {row[column.key]} diff --git a/web/src/components/admin/connectors/ConnectorForm.tsx b/web/src/components/admin/connectors/ConnectorForm.tsx index 89f6e21fb0ce..c33aecba7247 100644 --- a/web/src/components/admin/connectors/ConnectorForm.tsx +++ b/web/src/components/admin/connectors/ConnectorForm.tsx @@ -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( - connector: ConnectorBase + connector: ConnectorBase, + connectorId?: number ): Promise<{ message: string; isSuccess: boolean; response?: Connector }> { + 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({ ); } + +interface UpdateConnectorBaseProps { + nameBuilder?: (values: T) => string; + existingConnector: Connector; + // If both are specified, uses formBody + formBody?: JSX.Element | null; + formBodyBuilder?: FormBodyBuilder; + validationSchema: Yup.ObjectSchema; + onSubmit?: (isSuccess: boolean, responseJson?: Connector) => void; +} + +type UpdateConnectorFormProps = RequireAtLeastOne< + UpdateConnectorBaseProps, + "formBody" | "formBodyBuilder" +>; + +export function UpdateConnectorForm({ + nameBuilder, + existingConnector, + formBody, + formBodyBuilder, + validationSchema, + onSubmit, +}: UpdateConnectorFormProps): JSX.Element { + const [popup, setPopup] = useState<{ + message: string; + type: "success" | "error"; + } | null>(null); + + return ( + <> + {popup && } + { + formikHelpers.setSubmitting(true); + + const { message, isSuccess, response } = await submitConnector( + { + 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 }) => ( +
+ {formBody ? formBody : formBodyBuilder && formBodyBuilder(values)} +
+ +
+
+ )} +
+ + ); +} diff --git a/web/src/components/icons/icons.tsx b/web/src/components/icons/icons.tsx index da56d7431a80..68a4f66a623d 100644 --- a/web/src/components/icons/icons.tsx +++ b/web/src/components/icons/icons.tsx @@ -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 ; }; +export const PencilIcon = ({ + size = 16, + className = defaultTailwindCSS, +}: IconProps) => { + return ; +}; + +export const XIcon = ({ + size = 16, + className = defaultTailwindCSS, +}: IconProps) => { + return ; +}; + // // COMPANY LOGOS // diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index 887bc0886a05..dba95c824cc6 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -48,7 +48,7 @@ export interface GithubConfig { } export interface GoogleDriveConfig { - folder_paths: string[]; + folder_paths?: string[]; } export interface BookstackConfig {}