mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-09-27 20:38:32 +02:00
Add Google Drive admin page
This commit is contained in:
@@ -11,16 +11,18 @@ from danswer.utils.logging import setup_logger
|
|||||||
from google.auth.transport.requests import Request # type: ignore
|
from google.auth.transport.requests import Request # type: ignore
|
||||||
from google.oauth2.credentials import Credentials # type: ignore
|
from google.oauth2.credentials import Credentials # type: ignore
|
||||||
from google_auth_oauthlib.flow import InstalledAppFlow # type: ignore
|
from google_auth_oauthlib.flow import InstalledAppFlow # type: ignore
|
||||||
from googleapiclient import discovery # type: ignore
|
|
||||||
|
|
||||||
logger = setup_logger()
|
logger = setup_logger()
|
||||||
|
|
||||||
SCOPES = ["https://www.googleapis.com/auth/drive.readonly"]
|
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:
|
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
|
creds = None
|
||||||
if os.path.exists(GOOGLE_DRIVE_TOKENS_JSON):
|
if os.path.exists(GOOGLE_DRIVE_TOKENS_JSON):
|
||||||
creds = Credentials.from_authorized_user_file(GOOGLE_DRIVE_TOKENS_JSON, SCOPES)
|
creds = Credentials.from_authorized_user_file(GOOGLE_DRIVE_TOKENS_JSON, SCOPES)
|
||||||
@@ -100,7 +102,7 @@ def save_access_tokens(
|
|||||||
creds = flow.credentials
|
creds = flow.credentials
|
||||||
|
|
||||||
os.makedirs(os.path.dirname(token_path), exist_ok=True)
|
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())
|
token_file.write(creds.to_json())
|
||||||
|
|
||||||
if not get_drive_tokens(token_path):
|
if not get_drive_tokens(token_path):
|
||||||
|
@@ -4,8 +4,8 @@ import * as Yup from "yup";
|
|||||||
import { IndexForm } from "@/components/admin/connectors/Form";
|
import { IndexForm } from "@/components/admin/connectors/Form";
|
||||||
import {
|
import {
|
||||||
ConnectorStatus,
|
ConnectorStatus,
|
||||||
ReccuringConnectorStatus,
|
ConnectorStatusEnum,
|
||||||
} from "@/components/admin/connectors/RecurringConnectorStatus";
|
} from "@/components/admin/connectors/ConnectorStatus";
|
||||||
import { GithubIcon } from "@/components/icons/icons";
|
import { GithubIcon } from "@/components/icons/icons";
|
||||||
import { TextFormField } from "@/components/admin/connectors/Field";
|
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">
|
<h2 className="text-xl font-bold pl-2 mb-2 mt-6 ml-auto mr-auto">
|
||||||
Status
|
Status
|
||||||
</h2>
|
</h2>
|
||||||
<ReccuringConnectorStatus
|
<ConnectorStatus status={ConnectorStatusEnum.Setup} source="github" />
|
||||||
status={ConnectorStatus.Running}
|
|
||||||
source="github"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* TODO: make this periodic */}
|
{/* TODO: make this periodic */}
|
||||||
<h2 className="text-xl font-bold pl-2 mb-2 mt-6 ml-auto mr-auto">
|
<h2 className="text-xl font-bold pl-2 mb-2 mt-6 ml-auto mr-auto">
|
||||||
|
@@ -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))
|
||||||
|
);
|
||||||
|
};
|
144
web/src/app/admin/connectors/google-drive/page.tsx
Normal file
144
web/src/app/admin/connectors/google-drive/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@@ -3,7 +3,7 @@ import { Formik, Form, FormikHelpers } from "formik";
|
|||||||
import * as Yup from "yup";
|
import * as Yup from "yup";
|
||||||
import { Popup } from "../../../../components/admin/connectors/Popup";
|
import { Popup } from "../../../../components/admin/connectors/Popup";
|
||||||
import { TextFormField } from "../../../../components/admin/connectors/Field";
|
import { TextFormField } from "../../../../components/admin/connectors/Field";
|
||||||
import { SlackConfig } from "../interfaces";
|
import { SlackConfig } from "../../../../components/admin/connectors/interfaces";
|
||||||
|
|
||||||
const validationSchema = Yup.object().shape({
|
const validationSchema = Yup.object().shape({
|
||||||
slack_bot_token: Yup.string().required("Please enter your Slack Bot Token"),
|
slack_bot_token: Yup.string().required("Please enter your Slack Bot Token"),
|
||||||
|
@@ -2,14 +2,14 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
ConnectorStatus,
|
ConnectorStatus,
|
||||||
ReccuringConnectorStatus,
|
ConnectorStatusEnum,
|
||||||
} from "@/components/admin/connectors/RecurringConnectorStatus";
|
} from "@/components/admin/connectors/ConnectorStatus";
|
||||||
import { SlackForm } from "@/app/admin/connectors/slack/SlackForm";
|
import { SlackForm } from "@/app/admin/connectors/slack/SlackForm";
|
||||||
import { SlackIcon } from "@/components/icons/icons";
|
import { SlackIcon } from "@/components/icons/icons";
|
||||||
import { fetcher } from "@/lib/fetcher";
|
import { fetcher } from "@/lib/fetcher";
|
||||||
import useSWR, { useSWRConfig } from "swr";
|
import useSWR, { useSWRConfig } from "swr";
|
||||||
import { SlackConfig } from "../interfaces";
|
import { SlackConfig } from "../../../../components/admin/connectors/interfaces";
|
||||||
import { ThinkingAnimation } from "@/components/Thinking";
|
import { LoadingAnimation } from "@/components/Loading";
|
||||||
|
|
||||||
const MainSection = () => {
|
const MainSection = () => {
|
||||||
// TODO: add back in once this is ready
|
// TODO: add back in once this is ready
|
||||||
@@ -26,7 +26,7 @@ const MainSection = () => {
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="mt-16">
|
<div className="mt-16">
|
||||||
<ThinkingAnimation text="Loading" />
|
<LoadingAnimation text="Loading" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (error || !data) {
|
} else if (error || !data) {
|
||||||
@@ -41,11 +41,11 @@ const MainSection = () => {
|
|||||||
Status
|
Status
|
||||||
</h2>
|
</h2>
|
||||||
{
|
{
|
||||||
<ReccuringConnectorStatus
|
<ConnectorStatus
|
||||||
status={
|
status={
|
||||||
data.pull_frequency !== 0
|
data.pull_frequency !== 0
|
||||||
? ConnectorStatus.Running
|
? ConnectorStatusEnum.Running
|
||||||
: ConnectorStatus.NotSetup
|
: ConnectorStatusEnum.NotSetup
|
||||||
}
|
}
|
||||||
source="slack"
|
source="slack"
|
||||||
/>
|
/>
|
||||||
|
@@ -4,11 +4,14 @@ import useSWR, { useSWRConfig } from "swr";
|
|||||||
import * as Yup from "yup";
|
import * as Yup from "yup";
|
||||||
|
|
||||||
import { BasicTable } from "@/components/admin/connectors/BasicTable";
|
import { BasicTable } from "@/components/admin/connectors/BasicTable";
|
||||||
import { ThinkingAnimation } from "@/components/Thinking";
|
import { LoadingAnimation } from "@/components/Loading";
|
||||||
import { timeAgo } from "@/lib/time";
|
import { timeAgo } from "@/lib/time";
|
||||||
import { GlobeIcon } from "@/components/icons/icons";
|
import { GlobeIcon } from "@/components/icons/icons";
|
||||||
import { fetcher } from "@/lib/fetcher";
|
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 { IndexForm } from "@/components/admin/connectors/Form";
|
||||||
import { TextFormField } from "@/components/admin/connectors/Field";
|
import { TextFormField } from "@/components/admin/connectors/Field";
|
||||||
|
|
||||||
@@ -78,7 +81,7 @@ export default function Web() {
|
|||||||
Indexing History
|
Indexing History
|
||||||
</h2>
|
</h2>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<ThinkingAnimation text="Loading" />
|
<LoadingAnimation text="Loading" />
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<div>Error loading indexing history</div>
|
<div>Error loading indexing history</div>
|
||||||
) : (
|
) : (
|
||||||
|
@@ -1,6 +1,11 @@
|
|||||||
import { Header } from "@/components/Header";
|
import { Header } from "@/components/Header";
|
||||||
import { Sidebar } from "@/components/admin/connectors/Sidebar";
|
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 { getCurrentUserSS } from "@/lib/userSS";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
@@ -54,6 +59,15 @@ export default async function AdminLayout({
|
|||||||
),
|
),
|
||||||
link: "/admin/connectors/github",
|
link: "/admin/connectors/github",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: (
|
||||||
|
<div className="flex">
|
||||||
|
<GoogleDriveIcon size="16" />
|
||||||
|
<div className="ml-1">Google Drive</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
link: "/admin/connectors/google-drive",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
@@ -1,28 +1,7 @@
|
|||||||
|
import { getDomain } from "@/lib/redirectSS";
|
||||||
import { buildUrl } from "@/lib/userSS";
|
import { buildUrl } from "@/lib/userSS";
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
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) => {
|
export const GET = async (request: NextRequest) => {
|
||||||
// Wrapper around the FastAPI endpoint /auth/google/callback,
|
// Wrapper around the FastAPI endpoint /auth/google/callback,
|
||||||
// which adds back a redirect to the main app.
|
// which adds back a redirect to the main app.
|
||||||
|
@@ -1,13 +1,11 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import "./thinking.css";
|
import "./loading.css";
|
||||||
|
|
||||||
interface ThinkingAnimationProps {
|
interface LoadingAnimationProps {
|
||||||
text?: string;
|
text?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ThinkingAnimation: React.FC<ThinkingAnimationProps> = ({
|
export const LoadingAnimation: React.FC<LoadingAnimationProps> = ({ text }) => {
|
||||||
text,
|
|
||||||
}) => {
|
|
||||||
const [dots, setDots] = useState("...");
|
const [dots, setDots] = useState("...");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -30,9 +28,9 @@ export const ThinkingAnimation: React.FC<ThinkingAnimationProps> = ({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="thinking-animation flex">
|
<div className="loading-animation flex">
|
||||||
<div className="mx-auto">
|
<div className="mx-auto">
|
||||||
{text || "Thinking"}
|
{text === undefined ? "Thinking" : text}
|
||||||
<span className="dots">{dots}</span>
|
<span className="dots">{dots}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@@ -1,25 +1,34 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ListIndexingResponse } from "@/app/admin/connectors/interfaces";
|
import {
|
||||||
|
IndexAttempt,
|
||||||
|
ListIndexingResponse,
|
||||||
|
ValidSources,
|
||||||
|
} from "@/components/admin/connectors/interfaces";
|
||||||
import { fetcher } from "@/lib/fetcher";
|
import { fetcher } from "@/lib/fetcher";
|
||||||
import { timeAgo } from "@/lib/time";
|
import { timeAgo } from "@/lib/time";
|
||||||
import { CheckCircle, MinusCircle } from "@phosphor-icons/react";
|
import { CheckCircle, MinusCircle } from "@phosphor-icons/react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
export enum ConnectorStatus {
|
export enum ConnectorStatusEnum {
|
||||||
|
Setup = "Setup",
|
||||||
Running = "Running",
|
Running = "Running",
|
||||||
NotSetup = "Not Setup",
|
NotSetup = "Not Setup",
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ReccuringConnectorStatusProps {
|
const sortIndexAttemptsByTimeUpdated = (a: IndexAttempt, b: IndexAttempt) => {
|
||||||
status: ConnectorStatus;
|
if (a.time_updated === b.time_updated) {
|
||||||
source: string;
|
return 0;
|
||||||
|
}
|
||||||
|
return a.time_updated > b.time_updated ? -1 : 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ConnectorStatusProps {
|
||||||
|
status: ConnectorStatusEnum;
|
||||||
|
source: ValidSources;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ReccuringConnectorStatus = ({
|
export const ConnectorStatus = ({ status, source }: ConnectorStatusProps) => {
|
||||||
status,
|
|
||||||
source,
|
|
||||||
}: ReccuringConnectorStatusProps) => {
|
|
||||||
const { data } = useSWR<ListIndexingResponse>(
|
const { data } = useSWR<ListIndexingResponse>(
|
||||||
`/api/admin/connectors/${source}/index-attempt`,
|
`/api/admin/connectors/${source}/index-attempt`,
|
||||||
fetcher
|
fetcher
|
||||||
@@ -27,14 +36,12 @@ export const ReccuringConnectorStatus = ({
|
|||||||
|
|
||||||
const lastSuccessfulAttempt = data?.index_attempts
|
const lastSuccessfulAttempt = data?.index_attempts
|
||||||
.filter((attempt) => attempt.status === "success")
|
.filter((attempt) => attempt.status === "success")
|
||||||
.sort((a, b) => {
|
.sort(sortIndexAttemptsByTimeUpdated)[0];
|
||||||
if (a.time_updated === b.time_updated) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return a.time_updated > b.time_updated ? -1 : 1;
|
|
||||||
})[0];
|
|
||||||
|
|
||||||
if (status === ConnectorStatus.Running) {
|
if (
|
||||||
|
status === ConnectorStatusEnum.Running ||
|
||||||
|
status == ConnectorStatusEnum.Setup
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-emerald-600 flex align-middle text-center">
|
<div className="text-emerald-600 flex align-middle text-center">
|
||||||
@@ -43,7 +50,7 @@ export const ReccuringConnectorStatus = ({
|
|||||||
</div>
|
</div>
|
||||||
{lastSuccessfulAttempt && (
|
{lastSuccessfulAttempt && (
|
||||||
<p className="text-xs my-auto ml-1">
|
<p className="text-xs my-auto ml-1">
|
||||||
Last updated {timeAgo(lastSuccessfulAttempt.time_updated)}
|
Last indexed {timeAgo(lastSuccessfulAttempt.time_updated)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
@@ -2,8 +2,7 @@ import React, { useState } from "react";
|
|||||||
import { Formik, Form, FormikHelpers } from "formik";
|
import { Formik, Form, FormikHelpers } from "formik";
|
||||||
import * as Yup from "yup";
|
import * as Yup from "yup";
|
||||||
import { Popup } from "./Popup";
|
import { Popup } from "./Popup";
|
||||||
|
import { ValidSources } from "./interfaces";
|
||||||
type ValidSources = "web" | "github";
|
|
||||||
|
|
||||||
const handleSubmit = async (
|
const handleSubmit = async (
|
||||||
source: ValidSources,
|
source: ValidSources,
|
||||||
|
@@ -15,3 +15,5 @@ export interface IndexAttempt {
|
|||||||
export interface ListIndexingResponse {
|
export interface ListIndexingResponse {
|
||||||
index_attempts: IndexAttempt[];
|
index_attempts: IndexAttempt[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ValidSources = "web" | "github" | "slack" | "google_drive";
|
@@ -1,6 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Globe, SlackLogo, GithubLogo } from "@phosphor-icons/react";
|
import {
|
||||||
|
Globe,
|
||||||
|
SlackLogo,
|
||||||
|
GithubLogo,
|
||||||
|
GoogleDriveLogo,
|
||||||
|
} from "@phosphor-icons/react";
|
||||||
|
|
||||||
interface IconProps {
|
interface IconProps {
|
||||||
size?: string;
|
size?: string;
|
||||||
@@ -29,3 +34,10 @@ export const GithubIcon = ({
|
|||||||
}: IconProps) => {
|
}: IconProps) => {
|
||||||
return <GithubLogo size={size} className={className} />;
|
return <GithubLogo size={size} className={className} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const GoogleDriveIcon = ({
|
||||||
|
size = "16",
|
||||||
|
className = defaultTailwindCSS,
|
||||||
|
}: IconProps) => {
|
||||||
|
return <GoogleDriveLogo size={size} className={className} />;
|
||||||
|
};
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
.thinking {
|
.loading {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
@@ -2,7 +2,7 @@ import React from "react";
|
|||||||
import { Globe, SlackLogo, GoogleDriveLogo } from "@phosphor-icons/react";
|
import { Globe, SlackLogo, GoogleDriveLogo } from "@phosphor-icons/react";
|
||||||
import "tailwindcss/tailwind.css";
|
import "tailwindcss/tailwind.css";
|
||||||
import { Quote, Document } from "./types";
|
import { Quote, Document } from "./types";
|
||||||
import { ThinkingAnimation } from "../Thinking";
|
import { LoadingAnimation } from "../Loading";
|
||||||
import { GithubIcon } from "../icons/icons";
|
import { GithubIcon } from "../icons/icons";
|
||||||
|
|
||||||
interface SearchResultsDisplayProps {
|
interface SearchResultsDisplayProps {
|
||||||
@@ -38,7 +38,7 @@ export const SearchResultsDisplay: React.FC<SearchResultsDisplayProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
if (!answer) {
|
if (!answer) {
|
||||||
if (isFetching) {
|
if (isFetching) {
|
||||||
return <ThinkingAnimation />;
|
return <LoadingAnimation />;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
23
web/src/lib/redirectSS.ts
Normal file
23
web/src/lib/redirectSS.ts
Normal 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;
|
||||||
|
};
|
@@ -8,29 +8,29 @@ export const timeAgo = (dateString: string | undefined): string | null => {
|
|||||||
const secondsDiff = Math.floor((now.getTime() - date.getTime()) / 1000);
|
const secondsDiff = Math.floor((now.getTime() - date.getTime()) / 1000);
|
||||||
|
|
||||||
if (secondsDiff < 60) {
|
if (secondsDiff < 60) {
|
||||||
return `${secondsDiff} seconds ago`;
|
return `${secondsDiff} second(s) ago`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const minutesDiff = Math.floor(secondsDiff / 60);
|
const minutesDiff = Math.floor(secondsDiff / 60);
|
||||||
if (minutesDiff < 60) {
|
if (minutesDiff < 60) {
|
||||||
return `${minutesDiff} minutes ago`;
|
return `${minutesDiff} minute(s) ago`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hoursDiff = Math.floor(minutesDiff / 60);
|
const hoursDiff = Math.floor(minutesDiff / 60);
|
||||||
if (hoursDiff < 24) {
|
if (hoursDiff < 24) {
|
||||||
return `${hoursDiff} hours ago`;
|
return `${hoursDiff} hour(s) ago`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const daysDiff = Math.floor(hoursDiff / 24);
|
const daysDiff = Math.floor(hoursDiff / 24);
|
||||||
if (daysDiff < 30) {
|
if (daysDiff < 30) {
|
||||||
return `${daysDiff} days ago`;
|
return `${daysDiff} day(s) ago`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const monthsDiff = Math.floor(daysDiff / 30);
|
const monthsDiff = Math.floor(daysDiff / 30);
|
||||||
if (monthsDiff < 12) {
|
if (monthsDiff < 12) {
|
||||||
return `${monthsDiff} months ago`;
|
return `${monthsDiff} month(s) ago`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const yearsDiff = Math.floor(monthsDiff / 12);
|
const yearsDiff = Math.floor(monthsDiff / 12);
|
||||||
return `${yearsDiff} years ago`;
|
return `${yearsDiff} year(s) ago`;
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user