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: (
+
+ ),
+ 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`;
};