Add Google Drive admin page

This commit is contained in:
Weves
2023-05-14 19:52:39 -07:00
committed by Chris Weaver
parent ebf9459ae8
commit d76dbce09b
18 changed files with 281 additions and 81 deletions

View File

@@ -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() {
<h2 className="text-xl font-bold pl-2 mb-2 mt-6 ml-auto mr-auto">
Status
</h2>
<ReccuringConnectorStatus
status={ConnectorStatus.Running}
source="github"
/>
<ConnectorStatus status={ConnectorStatusEnum.Setup} source="github" />
{/* TODO: make this periodic */}
<h2 className="text-xl font-bold pl-2 mb-2 mt-6 ml-auto mr-auto">

View File

@@ -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))
);
};

View File

@@ -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 = (
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
<GoogleDriveIcon size="32" />
<h1 className="text-3xl font-bold pl-2">Google Drive</h1>
</div>
);
let body = null;
if (isAuthenticatedLoading || authorizationUrlLoading) {
return (
<div className="mx-auto">
{header}
<LoadingAnimation text="" />
</div>
);
}
if (
isAuthenticatedError ||
isAuthenticatedData?.authenticated === undefined
) {
return (
<div className="mx-auto">
{header}
<div className="text-red-500">
Error loading Google Drive authentication status. Contact an
administrator.
</div>
</div>
);
}
if (authorizationUrlError || authorizationUrlData?.auth_url === undefined) {
return (
<div className="mx-auto">
{header}
<div className="text-red-500">
Error loading Google Drive authentication URL. Contact an
administrator.
</div>
</div>
);
}
if (isAuthenticatedData.authenticated) {
return (
<div className="mx-auto">
{header}
<h2 className="text-xl font-bold pl-2 mb-2 mt-6 ml-auto mr-auto">
Status
</h2>
<ConnectorStatus
status={ConnectorStatusEnum.Setup}
source="google_drive"
/>
{/* TODO: make this periodic */}
<div className="w-fit mt-2">
<IndexForm
source="google_drive"
formBody={null}
validationSchema={Yup.object().shape({})}
initialValues={{}}
onSubmit={(isSuccess) => console.log(isSuccess)}
/>
</div>
{/*
TODO: add back ability add more accounts / switch account
<a
className={
"group relative w-64 flex justify-center " +
"py-2 px-4 border border-transparent text-sm " +
"font-medium rounded-md text-white bg-red-600 " +
"hover:bg-red-700 focus:outline-none focus:ring-2 " +
"focus:ring-offset-2 focus:ring-red-500 mx-auto"
}
href={authorizationUrlData.auth_url}
>
Re-Authenticate
</a> */}
</div>
);
}
return (
<div className="mx-auto">
{header}
<div className="flex">
<div className="max-w-2xl mx-auto border p-3 border-gray-700 rounded-md">
<h2 className="text-xl font-bold mb-2 mt-6 ml-auto mr-auto">Setup</h2>
<p className="text-sm mb-4">
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.
</p>
<a
className={
"group relative w-64 flex justify-center " +
"py-2 px-4 border border-transparent text-sm " +
"font-medium rounded-md text-white bg-red-600 " +
"hover:bg-red-700 focus:outline-none focus:ring-2 " +
"focus:ring-offset-2 focus:ring-red-500 mx-auto"
}
href={authorizationUrlData.auth_url}
>
Authenticate with Google Drive
</a>
</div>
</div>
</div>
);
}

View File

@@ -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"),

View File

@@ -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 (
<div className="mt-16">
<ThinkingAnimation text="Loading" />
<LoadingAnimation text="Loading" />
</div>
);
} else if (error || !data) {
@@ -41,11 +41,11 @@ const MainSection = () => {
Status
</h2>
{
<ReccuringConnectorStatus
<ConnectorStatus
status={
data.pull_frequency !== 0
? ConnectorStatus.Running
: ConnectorStatus.NotSetup
? ConnectorStatusEnum.Running
: ConnectorStatusEnum.NotSetup
}
source="slack"
/>

View File

@@ -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
</h2>
{isLoading ? (
<ThinkingAnimation text="Loading" />
<LoadingAnimation text="Loading" />
) : error ? (
<div>Error loading indexing history</div>
) : (

View File

@@ -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: (
<div className="flex">
<GoogleDriveIcon size="16" />
<div className="ml-1">Google Drive</div>
</div>
),
link: "/admin/connectors/google-drive",
},
],
},
]}

View File

@@ -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.

View File

@@ -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<ThinkingAnimationProps> = ({
text,
}) => {
export const LoadingAnimation: React.FC<LoadingAnimationProps> = ({ text }) => {
const [dots, setDots] = useState("...");
useEffect(() => {
@@ -30,9 +28,9 @@ export const ThinkingAnimation: React.FC<ThinkingAnimationProps> = ({
}, []);
return (
<div className="thinking-animation flex">
<div className="loading-animation flex">
<div className="mx-auto">
{text || "Thinking"}
{text === undefined ? "Thinking" : text}
<span className="dots">{dots}</span>
</div>
</div>

View File

@@ -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<ListIndexingResponse>(
`/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 (
<div>
<div className="text-emerald-600 flex align-middle text-center">
@@ -43,7 +50,7 @@ export const ReccuringConnectorStatus = ({
</div>
{lastSuccessfulAttempt && (
<p className="text-xs my-auto ml-1">
Last updated {timeAgo(lastSuccessfulAttempt.time_updated)}
Last indexed {timeAgo(lastSuccessfulAttempt.time_updated)}
</p>
)}
</div>

View File

@@ -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,

View File

@@ -15,3 +15,5 @@ export interface IndexAttempt {
export interface ListIndexingResponse {
index_attempts: IndexAttempt[];
}
export type ValidSources = "web" | "github" | "slack" | "google_drive";

View File

@@ -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 <GithubLogo size={size} className={className} />;
};
export const GoogleDriveIcon = ({
size = "16",
className = defaultTailwindCSS,
}: IconProps) => {
return <GoogleDriveLogo size={size} className={className} />;
};

View File

@@ -1,4 +1,4 @@
.thinking {
.loading {
font-size: 1.5rem;
font-weight: bold;
}

View File

@@ -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<SearchResultsDisplayProps> = ({
}) => {
if (!answer) {
if (isFetching) {
return <ThinkingAnimation />;
return <LoadingAnimation />;
}
return null;
}

23
web/src/lib/redirectSS.ts Normal file
View File

@@ -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;
};

View File

@@ -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`;
};