From d76dbce09b65774a2c1799707e81a4185949d512 Mon Sep 17 00:00:00 2001 From: Weves Date: Sun, 14 May 2023 19:52:39 -0700 Subject: [PATCH] Add Google Drive admin page --- .../connectors/google_drive/connector_auth.py | 10 +- web/src/app/admin/connectors/github/page.tsx | 9 +- .../google-drive/auth/callback/route.ts | 20 +++ .../admin/connectors/google-drive/page.tsx | 144 ++++++++++++++++++ .../app/admin/connectors/slack/SlackForm.tsx | 2 +- web/src/app/admin/connectors/slack/page.tsx | 16 +- web/src/app/admin/connectors/web/page.tsx | 9 +- web/src/app/admin/layout.tsx | 16 +- web/src/app/auth/google/callback/route.ts | 23 +-- .../components/{Thinking.tsx => Loading.tsx} | 12 +- ...onnectorStatus.tsx => ConnectorStatus.tsx} | 41 ++--- web/src/components/admin/connectors/Form.tsx | 3 +- .../admin/connectors/interfaces.ts | 2 + web/src/components/icons/icons.tsx | 14 +- .../components/{thinking.css => loading.css} | 2 +- .../search/SearchResultsDisplay.tsx | 4 +- web/src/lib/redirectSS.ts | 23 +++ web/src/lib/time.ts | 12 +- 18 files changed, 281 insertions(+), 81 deletions(-) create mode 100644 web/src/app/admin/connectors/google-drive/auth/callback/route.ts create mode 100644 web/src/app/admin/connectors/google-drive/page.tsx rename web/src/components/{Thinking.tsx => Loading.tsx} (72%) rename web/src/components/admin/connectors/{RecurringConnectorStatus.tsx => ConnectorStatus.tsx} (58%) rename web/src/{app => components}/admin/connectors/interfaces.ts (84%) rename web/src/components/{thinking.css => loading.css} (93%) create mode 100644 web/src/lib/redirectSS.ts diff --git a/backend/danswer/connectors/google_drive/connector_auth.py b/backend/danswer/connectors/google_drive/connector_auth.py index bda6a3ffc409..ac8dd16bf1d7 100644 --- a/backend/danswer/connectors/google_drive/connector_auth.py +++ b/backend/danswer/connectors/google_drive/connector_auth.py @@ -11,16 +11,18 @@ from danswer.utils.logging import setup_logger from google.auth.transport.requests import Request # type: ignore from google.oauth2.credentials import Credentials # type: ignore from google_auth_oauthlib.flow import InstalledAppFlow # type: ignore -from googleapiclient import discovery # type: ignore logger = setup_logger() SCOPES = ["https://www.googleapis.com/auth/drive.readonly"] -FRONTEND_GOOGLE_DRIVE_REDIRECT = f"{WEB_DOMAIN}/auth/connectors/google_drive/callback" +FRONTEND_GOOGLE_DRIVE_REDIRECT = ( + f"{WEB_DOMAIN}/admin/connectors/google-drive/auth/callback" +) def backend_get_credentials() -> Credentials: - """This approach does not work for the one-box builds""" + """This approach does not work for production builds as it requires + a browser to be opened. It is used for local development only.""" creds = None if os.path.exists(GOOGLE_DRIVE_TOKENS_JSON): creds = Credentials.from_authorized_user_file(GOOGLE_DRIVE_TOKENS_JSON, SCOPES) @@ -100,7 +102,7 @@ def save_access_tokens( creds = flow.credentials os.makedirs(os.path.dirname(token_path), exist_ok=True) - with open(token_path, "w") as token_file: + with open(token_path, "w+") as token_file: token_file.write(creds.to_json()) if not get_drive_tokens(token_path): diff --git a/web/src/app/admin/connectors/github/page.tsx b/web/src/app/admin/connectors/github/page.tsx index 1504ed2be584..1604a2696476 100644 --- a/web/src/app/admin/connectors/github/page.tsx +++ b/web/src/app/admin/connectors/github/page.tsx @@ -4,8 +4,8 @@ import * as Yup from "yup"; import { IndexForm } from "@/components/admin/connectors/Form"; import { ConnectorStatus, - ReccuringConnectorStatus, -} from "@/components/admin/connectors/RecurringConnectorStatus"; + ConnectorStatusEnum, +} from "@/components/admin/connectors/ConnectorStatus"; import { GithubIcon } from "@/components/icons/icons"; import { TextFormField } from "@/components/admin/connectors/Field"; @@ -20,10 +20,7 @@ export default function Page() {

Status

- + {/* TODO: make this periodic */}

diff --git a/web/src/app/admin/connectors/google-drive/auth/callback/route.ts b/web/src/app/admin/connectors/google-drive/auth/callback/route.ts new file mode 100644 index 000000000000..637eeac704d8 --- /dev/null +++ b/web/src/app/admin/connectors/google-drive/auth/callback/route.ts @@ -0,0 +1,20 @@ +import { getDomain } from "@/lib/redirectSS"; +import { buildUrl } from "@/lib/userSS"; +import { NextRequest, NextResponse } from "next/server"; + +export const GET = async (request: NextRequest) => { + // Wrapper around the FastAPI endpoint /connectors/google-drive/callback, + // which adds back a redirect to the Google Drive admin page. + const url = new URL(buildUrl("/admin/connectors/google-drive/callback")); + url.search = request.nextUrl.search; + + const response = await fetch(url.toString()); + + if (!response.ok) { + return NextResponse.redirect(new URL("/auth/error", getDomain(request))); + } + + return NextResponse.redirect( + new URL("/admin/connectors/google-drive", getDomain(request)) + ); +}; diff --git a/web/src/app/admin/connectors/google-drive/page.tsx b/web/src/app/admin/connectors/google-drive/page.tsx new file mode 100644 index 000000000000..720d0ce0e5fc --- /dev/null +++ b/web/src/app/admin/connectors/google-drive/page.tsx @@ -0,0 +1,144 @@ +"use client"; + +import * as Yup from "yup"; +import { IndexForm } from "@/components/admin/connectors/Form"; +import { + ConnectorStatusEnum, + ConnectorStatus, +} from "@/components/admin/connectors/ConnectorStatus"; +import { GoogleDriveIcon } from "@/components/icons/icons"; +import useSWR from "swr"; +import { fetcher } from "@/lib/fetcher"; +import { LoadingAnimation } from "@/components/Loading"; + +export default function Page() { + const { + data: isAuthenticatedData, + isLoading: isAuthenticatedLoading, + error: isAuthenticatedError, + } = useSWR<{ authenticated: boolean }>( + "/api/admin/connectors/google-drive/check-auth", + fetcher + ); + const { + data: authorizationUrlData, + isLoading: authorizationUrlLoading, + error: authorizationUrlError, + } = useSWR<{ auth_url: string }>( + "/api/admin/connectors/google-drive/authorize", + fetcher + ); + + const header = ( +
+ +

Google Drive

+
+ ); + + let body = null; + if (isAuthenticatedLoading || authorizationUrlLoading) { + return ( +
+ {header} + +
+ ); + } + if ( + isAuthenticatedError || + isAuthenticatedData?.authenticated === undefined + ) { + return ( +
+ {header} +
+ Error loading Google Drive authentication status. Contact an + administrator. +
+
+ ); + } + if (authorizationUrlError || authorizationUrlData?.auth_url === undefined) { + return ( +
+ {header} +
+ Error loading Google Drive authentication URL. Contact an + administrator. +
+
+ ); + } + + if (isAuthenticatedData.authenticated) { + return ( +
+ {header} + +

+ Status +

+ + + {/* TODO: make this periodic */} +
+ console.log(isSuccess)} + /> +
+ + {/* + TODO: add back ability add more accounts / switch account + + Re-Authenticate + */} +
+ ); + } + + return ( +
+ {header} + +
+
+

Setup

+

+ To use the Google Drive connector, you must first provide + credentials via OAuth. This gives us read access to the docs in your + google drive account. +

+ + Authenticate with Google Drive + +
+
+
+ ); +} diff --git a/web/src/app/admin/connectors/slack/SlackForm.tsx b/web/src/app/admin/connectors/slack/SlackForm.tsx index 9dc23234c9fd..c0f08c90a00e 100644 --- a/web/src/app/admin/connectors/slack/SlackForm.tsx +++ b/web/src/app/admin/connectors/slack/SlackForm.tsx @@ -3,7 +3,7 @@ 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 "../interfaces"; +import { SlackConfig } from "../../../../components/admin/connectors/interfaces"; const validationSchema = Yup.object().shape({ slack_bot_token: Yup.string().required("Please enter your Slack Bot Token"), diff --git a/web/src/app/admin/connectors/slack/page.tsx b/web/src/app/admin/connectors/slack/page.tsx index 7a8727cc5071..8e5291cb27b5 100644 --- a/web/src/app/admin/connectors/slack/page.tsx +++ b/web/src/app/admin/connectors/slack/page.tsx @@ -2,14 +2,14 @@ import { ConnectorStatus, - ReccuringConnectorStatus, -} from "@/components/admin/connectors/RecurringConnectorStatus"; + 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 "../interfaces"; -import { ThinkingAnimation } from "@/components/Thinking"; +import { SlackConfig } from "../../../../components/admin/connectors/interfaces"; +import { LoadingAnimation } from "@/components/Loading"; const MainSection = () => { // TODO: add back in once this is ready @@ -26,7 +26,7 @@ const MainSection = () => { if (isLoading) { return (
- +
); } else if (error || !data) { @@ -41,11 +41,11 @@ const MainSection = () => { Status

{ - diff --git a/web/src/app/admin/connectors/web/page.tsx b/web/src/app/admin/connectors/web/page.tsx index 3fde88e69d96..b276c49c4752 100644 --- a/web/src/app/admin/connectors/web/page.tsx +++ b/web/src/app/admin/connectors/web/page.tsx @@ -4,11 +4,14 @@ import useSWR, { useSWRConfig } from "swr"; import * as Yup from "yup"; import { BasicTable } from "@/components/admin/connectors/BasicTable"; -import { ThinkingAnimation } from "@/components/Thinking"; +import { LoadingAnimation } from "@/components/Loading"; import { timeAgo } from "@/lib/time"; import { GlobeIcon } from "@/components/icons/icons"; import { fetcher } from "@/lib/fetcher"; -import { IndexAttempt, ListIndexingResponse } from "../interfaces"; +import { + IndexAttempt, + ListIndexingResponse, +} from "../../../../components/admin/connectors/interfaces"; import { IndexForm } from "@/components/admin/connectors/Form"; import { TextFormField } from "@/components/admin/connectors/Field"; @@ -78,7 +81,7 @@ export default function Web() { Indexing History {isLoading ? ( - + ) : error ? (
Error loading indexing history
) : ( diff --git a/web/src/app/admin/layout.tsx b/web/src/app/admin/layout.tsx index 064b9af3104c..862052bb409c 100644 --- a/web/src/app/admin/layout.tsx +++ b/web/src/app/admin/layout.tsx @@ -1,6 +1,11 @@ import { Header } from "@/components/Header"; import { Sidebar } from "@/components/admin/connectors/Sidebar"; -import { GithubIcon, GlobeIcon, SlackIcon } from "@/components/icons/icons"; +import { + GithubIcon, + GlobeIcon, + GoogleDriveIcon, + SlackIcon, +} from "@/components/icons/icons"; import { getCurrentUserSS } from "@/lib/userSS"; import { redirect } from "next/navigation"; @@ -54,6 +59,15 @@ export default async function AdminLayout({ ), link: "/admin/connectors/github", }, + { + name: ( +
+ +
Google Drive
+
+ ), + link: "/admin/connectors/google-drive", + }, ], }, ]} diff --git a/web/src/app/auth/google/callback/route.ts b/web/src/app/auth/google/callback/route.ts index 62696e408291..b69345bb52c6 100644 --- a/web/src/app/auth/google/callback/route.ts +++ b/web/src/app/auth/google/callback/route.ts @@ -1,28 +1,7 @@ +import { getDomain } from "@/lib/redirectSS"; import { buildUrl } from "@/lib/userSS"; import { NextRequest, NextResponse } from "next/server"; -const getDomain = (request: NextRequest) => { - // use env variable if set - if (process.env.BASE_URL) { - return process.env.BASE_URL; - } - - // next, try and build domain from headers - const requestedHost = request.headers.get("X-Forwarded-Host"); - const requestedPort = request.headers.get("X-Forwarded-Port"); - const requestedProto = request.headers.get("X-Forwarded-Proto"); - if (requestedHost) { - const url = request.nextUrl.clone(); - url.host = requestedHost; - url.protocol = requestedProto || url.protocol; - url.port = requestedPort || url.port; - return url.origin; - } - - // finally just use whatever is in the request - return request.nextUrl.origin; -}; - export const GET = async (request: NextRequest) => { // Wrapper around the FastAPI endpoint /auth/google/callback, // which adds back a redirect to the main app. diff --git a/web/src/components/Thinking.tsx b/web/src/components/Loading.tsx similarity index 72% rename from web/src/components/Thinking.tsx rename to web/src/components/Loading.tsx index 54e000fb6494..70cdd5239cb9 100644 --- a/web/src/components/Thinking.tsx +++ b/web/src/components/Loading.tsx @@ -1,13 +1,11 @@ import React, { useState, useEffect } from "react"; -import "./thinking.css"; +import "./loading.css"; -interface ThinkingAnimationProps { +interface LoadingAnimationProps { text?: string; } -export const ThinkingAnimation: React.FC = ({ - text, -}) => { +export const LoadingAnimation: React.FC = ({ text }) => { const [dots, setDots] = useState("..."); useEffect(() => { @@ -30,9 +28,9 @@ export const ThinkingAnimation: React.FC = ({ }, []); return ( -
+
- {text || "Thinking"} + {text === undefined ? "Thinking" : text} {dots}
diff --git a/web/src/components/admin/connectors/RecurringConnectorStatus.tsx b/web/src/components/admin/connectors/ConnectorStatus.tsx similarity index 58% rename from web/src/components/admin/connectors/RecurringConnectorStatus.tsx rename to web/src/components/admin/connectors/ConnectorStatus.tsx index 30b34a85249f..3b02b08191b1 100644 --- a/web/src/components/admin/connectors/RecurringConnectorStatus.tsx +++ b/web/src/components/admin/connectors/ConnectorStatus.tsx @@ -1,25 +1,34 @@ "use client"; -import { ListIndexingResponse } from "@/app/admin/connectors/interfaces"; +import { + IndexAttempt, + ListIndexingResponse, + ValidSources, +} from "@/components/admin/connectors/interfaces"; import { fetcher } from "@/lib/fetcher"; import { timeAgo } from "@/lib/time"; import { CheckCircle, MinusCircle } from "@phosphor-icons/react"; import useSWR from "swr"; -export enum ConnectorStatus { +export enum ConnectorStatusEnum { + Setup = "Setup", Running = "Running", NotSetup = "Not Setup", } -interface ReccuringConnectorStatusProps { - status: ConnectorStatus; - source: string; +const sortIndexAttemptsByTimeUpdated = (a: IndexAttempt, b: IndexAttempt) => { + if (a.time_updated === b.time_updated) { + return 0; + } + return a.time_updated > b.time_updated ? -1 : 1; +}; + +interface ConnectorStatusProps { + status: ConnectorStatusEnum; + source: ValidSources; } -export const ReccuringConnectorStatus = ({ - status, - source, -}: ReccuringConnectorStatusProps) => { +export const ConnectorStatus = ({ status, source }: ConnectorStatusProps) => { const { data } = useSWR( `/api/admin/connectors/${source}/index-attempt`, fetcher @@ -27,14 +36,12 @@ export const ReccuringConnectorStatus = ({ const lastSuccessfulAttempt = data?.index_attempts .filter((attempt) => attempt.status === "success") - .sort((a, b) => { - if (a.time_updated === b.time_updated) { - return 0; - } - return a.time_updated > b.time_updated ? -1 : 1; - })[0]; + .sort(sortIndexAttemptsByTimeUpdated)[0]; - if (status === ConnectorStatus.Running) { + if ( + status === ConnectorStatusEnum.Running || + status == ConnectorStatusEnum.Setup + ) { return (
@@ -43,7 +50,7 @@ export const ReccuringConnectorStatus = ({
{lastSuccessfulAttempt && (

- Last updated {timeAgo(lastSuccessfulAttempt.time_updated)} + Last indexed {timeAgo(lastSuccessfulAttempt.time_updated)}

)}
diff --git a/web/src/components/admin/connectors/Form.tsx b/web/src/components/admin/connectors/Form.tsx index 2daba47a0b87..e4b2f678b69a 100644 --- a/web/src/components/admin/connectors/Form.tsx +++ b/web/src/components/admin/connectors/Form.tsx @@ -2,8 +2,7 @@ import React, { useState } from "react"; import { Formik, Form, FormikHelpers } from "formik"; import * as Yup from "yup"; import { Popup } from "./Popup"; - -type ValidSources = "web" | "github"; +import { ValidSources } from "./interfaces"; const handleSubmit = async ( source: ValidSources, diff --git a/web/src/app/admin/connectors/interfaces.ts b/web/src/components/admin/connectors/interfaces.ts similarity index 84% rename from web/src/app/admin/connectors/interfaces.ts rename to web/src/components/admin/connectors/interfaces.ts index fa32867e9cb8..f11812c2b85e 100644 --- a/web/src/app/admin/connectors/interfaces.ts +++ b/web/src/components/admin/connectors/interfaces.ts @@ -15,3 +15,5 @@ export interface IndexAttempt { export interface ListIndexingResponse { index_attempts: IndexAttempt[]; } + +export type ValidSources = "web" | "github" | "slack" | "google_drive"; diff --git a/web/src/components/icons/icons.tsx b/web/src/components/icons/icons.tsx index 8c232e62cd5d..829f07a4b954 100644 --- a/web/src/components/icons/icons.tsx +++ b/web/src/components/icons/icons.tsx @@ -1,6 +1,11 @@ "use client"; -import { Globe, SlackLogo, GithubLogo } from "@phosphor-icons/react"; +import { + Globe, + SlackLogo, + GithubLogo, + GoogleDriveLogo, +} from "@phosphor-icons/react"; interface IconProps { size?: string; @@ -29,3 +34,10 @@ export const GithubIcon = ({ }: IconProps) => { return ; }; + +export const GoogleDriveIcon = ({ + size = "16", + className = defaultTailwindCSS, +}: IconProps) => { + return ; +}; diff --git a/web/src/components/thinking.css b/web/src/components/loading.css similarity index 93% rename from web/src/components/thinking.css rename to web/src/components/loading.css index 3637815eeebb..142bfec74a18 100644 --- a/web/src/components/thinking.css +++ b/web/src/components/loading.css @@ -1,4 +1,4 @@ -.thinking { +.loading { font-size: 1.5rem; font-weight: bold; } diff --git a/web/src/components/search/SearchResultsDisplay.tsx b/web/src/components/search/SearchResultsDisplay.tsx index 8a2e3d793c53..4ad66bb73ae7 100644 --- a/web/src/components/search/SearchResultsDisplay.tsx +++ b/web/src/components/search/SearchResultsDisplay.tsx @@ -2,7 +2,7 @@ import React from "react"; import { Globe, SlackLogo, GoogleDriveLogo } from "@phosphor-icons/react"; import "tailwindcss/tailwind.css"; import { Quote, Document } from "./types"; -import { ThinkingAnimation } from "../Thinking"; +import { LoadingAnimation } from "../Loading"; import { GithubIcon } from "../icons/icons"; interface SearchResultsDisplayProps { @@ -38,7 +38,7 @@ export const SearchResultsDisplay: React.FC = ({ }) => { if (!answer) { if (isFetching) { - return ; + return ; } return null; } diff --git a/web/src/lib/redirectSS.ts b/web/src/lib/redirectSS.ts new file mode 100644 index 000000000000..0b3950841227 --- /dev/null +++ b/web/src/lib/redirectSS.ts @@ -0,0 +1,23 @@ +import { NextRequest } from "next/server"; + +export const getDomain = (request: NextRequest) => { + // use env variable if set + if (process.env.BASE_URL) { + return process.env.BASE_URL; + } + + // next, try and build domain from headers + const requestedHost = request.headers.get("X-Forwarded-Host"); + const requestedPort = request.headers.get("X-Forwarded-Port"); + const requestedProto = request.headers.get("X-Forwarded-Proto"); + if (requestedHost) { + const url = request.nextUrl.clone(); + url.host = requestedHost; + url.protocol = requestedProto || url.protocol; + url.port = requestedPort || url.port; + return url.origin; + } + + // finally just use whatever is in the request + return request.nextUrl.origin; +}; diff --git a/web/src/lib/time.ts b/web/src/lib/time.ts index f01a9e234a2b..7c27e793ee08 100644 --- a/web/src/lib/time.ts +++ b/web/src/lib/time.ts @@ -8,29 +8,29 @@ export const timeAgo = (dateString: string | undefined): string | null => { const secondsDiff = Math.floor((now.getTime() - date.getTime()) / 1000); if (secondsDiff < 60) { - return `${secondsDiff} seconds ago`; + return `${secondsDiff} second(s) ago`; } const minutesDiff = Math.floor(secondsDiff / 60); if (minutesDiff < 60) { - return `${minutesDiff} minutes ago`; + return `${minutesDiff} minute(s) ago`; } const hoursDiff = Math.floor(minutesDiff / 60); if (hoursDiff < 24) { - return `${hoursDiff} hours ago`; + return `${hoursDiff} hour(s) ago`; } const daysDiff = Math.floor(hoursDiff / 24); if (daysDiff < 30) { - return `${daysDiff} days ago`; + return `${daysDiff} day(s) ago`; } const monthsDiff = Math.floor(daysDiff / 30); if (monthsDiff < 12) { - return `${monthsDiff} months ago`; + return `${monthsDiff} month(s) ago`; } const yearsDiff = Math.floor(monthsDiff / 12); - return `${yearsDiff} years ago`; + return `${yearsDiff} year(s) ago`; };