mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-06-29 17:20:44 +02:00
Blob Storage (#1705)
S3 + OCI + Google Cloud Storage + R2 --------- Co-authored-by: Art Matsak <5328078+artmatsak@users.noreply.github.com>
This commit is contained in:
@ -100,6 +100,17 @@ class DocumentSource(str, Enum):
|
|||||||
CLICKUP = "clickup"
|
CLICKUP = "clickup"
|
||||||
MEDIAWIKI = "mediawiki"
|
MEDIAWIKI = "mediawiki"
|
||||||
WIKIPEDIA = "wikipedia"
|
WIKIPEDIA = "wikipedia"
|
||||||
|
S3 = "s3"
|
||||||
|
R2 = "r2"
|
||||||
|
GOOGLE_CLOUD_STORAGE = "google_cloud_storage"
|
||||||
|
OCI_STORAGE = "oci_storage"
|
||||||
|
|
||||||
|
|
||||||
|
class BlobType(str, Enum):
|
||||||
|
R2 = "r2"
|
||||||
|
S3 = "s3"
|
||||||
|
GOOGLE_CLOUD_STORAGE = "google_cloud_storage"
|
||||||
|
OCI_STORAGE = "oci_storage"
|
||||||
|
|
||||||
|
|
||||||
class DocumentIndexType(str, Enum):
|
class DocumentIndexType(str, Enum):
|
||||||
|
0
backend/danswer/connectors/blob/__init__.py
Normal file
0
backend/danswer/connectors/blob/__init__.py
Normal file
277
backend/danswer/connectors/blob/connector.py
Normal file
277
backend/danswer/connectors/blob/connector.py
Normal file
@ -0,0 +1,277 @@
|
|||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
from datetime import timezone
|
||||||
|
from io import BytesIO
|
||||||
|
from typing import Any
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import boto3
|
||||||
|
from botocore.client import Config
|
||||||
|
from mypy_boto3_s3 import S3Client
|
||||||
|
|
||||||
|
from danswer.configs.app_configs import INDEX_BATCH_SIZE
|
||||||
|
from danswer.configs.constants import BlobType
|
||||||
|
from danswer.configs.constants import DocumentSource
|
||||||
|
from danswer.connectors.interfaces import GenerateDocumentsOutput
|
||||||
|
from danswer.connectors.interfaces import LoadConnector
|
||||||
|
from danswer.connectors.interfaces import PollConnector
|
||||||
|
from danswer.connectors.interfaces import SecondsSinceUnixEpoch
|
||||||
|
from danswer.connectors.models import ConnectorMissingCredentialError
|
||||||
|
from danswer.connectors.models import Document
|
||||||
|
from danswer.connectors.models import Section
|
||||||
|
from danswer.file_processing.extract_file_text import extract_file_text
|
||||||
|
from danswer.utils.logger import setup_logger
|
||||||
|
|
||||||
|
logger = setup_logger()
|
||||||
|
|
||||||
|
|
||||||
|
class BlobStorageConnector(LoadConnector, PollConnector):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
bucket_type: str,
|
||||||
|
bucket_name: str,
|
||||||
|
prefix: str = "",
|
||||||
|
batch_size: int = INDEX_BATCH_SIZE,
|
||||||
|
) -> None:
|
||||||
|
self.bucket_type: BlobType = BlobType(bucket_type)
|
||||||
|
self.bucket_name = bucket_name
|
||||||
|
self.prefix = prefix if not prefix or prefix.endswith("/") else prefix + "/"
|
||||||
|
self.batch_size = batch_size
|
||||||
|
self.s3_client: Optional[S3Client] = None
|
||||||
|
|
||||||
|
def load_credentials(self, credentials: dict[str, Any]) -> dict[str, Any] | None:
|
||||||
|
"""Checks for boto3 credentials based on the bucket type.
|
||||||
|
(1) R2: Access Key ID, Secret Access Key, Account ID
|
||||||
|
(2) S3: AWS Access Key ID, AWS Secret Access Key
|
||||||
|
(3) GOOGLE_CLOUD_STORAGE: Access Key ID, Secret Access Key, Project ID
|
||||||
|
(4) OCI_STORAGE: Namespace, Region, Access Key ID, Secret Access Key
|
||||||
|
|
||||||
|
For each bucket type, the method initializes the appropriate S3 client:
|
||||||
|
- R2: Uses Cloudflare R2 endpoint with S3v4 signature
|
||||||
|
- S3: Creates a standard boto3 S3 client
|
||||||
|
- GOOGLE_CLOUD_STORAGE: Uses Google Cloud Storage endpoint
|
||||||
|
- OCI_STORAGE: Uses Oracle Cloud Infrastructure Object Storage endpoint
|
||||||
|
|
||||||
|
Raises ConnectorMissingCredentialError if required credentials are missing.
|
||||||
|
Raises ValueError for unsupported bucket types.
|
||||||
|
"""
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Loading credentials for {self.bucket_name} or type {self.bucket_type}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.bucket_type == BlobType.R2:
|
||||||
|
if not all(
|
||||||
|
credentials.get(key)
|
||||||
|
for key in ["r2_access_key_id", "r2_secret_access_key", "account_id"]
|
||||||
|
):
|
||||||
|
raise ConnectorMissingCredentialError("Cloudflare R2")
|
||||||
|
self.s3_client = boto3.client(
|
||||||
|
"s3",
|
||||||
|
endpoint_url=f"https://{credentials['account_id']}.r2.cloudflarestorage.com",
|
||||||
|
aws_access_key_id=credentials["r2_access_key_id"],
|
||||||
|
aws_secret_access_key=credentials["r2_secret_access_key"],
|
||||||
|
region_name="auto",
|
||||||
|
config=Config(signature_version="s3v4"),
|
||||||
|
)
|
||||||
|
|
||||||
|
elif self.bucket_type == BlobType.S3:
|
||||||
|
if not all(
|
||||||
|
credentials.get(key)
|
||||||
|
for key in ["aws_access_key_id", "aws_secret_access_key"]
|
||||||
|
):
|
||||||
|
raise ConnectorMissingCredentialError("Google Cloud Storage")
|
||||||
|
|
||||||
|
session = boto3.Session(
|
||||||
|
aws_access_key_id=credentials["aws_access_key_id"],
|
||||||
|
aws_secret_access_key=credentials["aws_secret_access_key"],
|
||||||
|
)
|
||||||
|
self.s3_client = session.client("s3")
|
||||||
|
|
||||||
|
elif self.bucket_type == BlobType.GOOGLE_CLOUD_STORAGE:
|
||||||
|
if not all(
|
||||||
|
credentials.get(key) for key in ["access_key_id", "secret_access_key"]
|
||||||
|
):
|
||||||
|
raise ConnectorMissingCredentialError("Google Cloud Storage")
|
||||||
|
|
||||||
|
self.s3_client = boto3.client(
|
||||||
|
"s3",
|
||||||
|
endpoint_url="https://storage.googleapis.com",
|
||||||
|
aws_access_key_id=credentials["access_key_id"],
|
||||||
|
aws_secret_access_key=credentials["secret_access_key"],
|
||||||
|
region_name="auto",
|
||||||
|
)
|
||||||
|
|
||||||
|
elif self.bucket_type == BlobType.OCI_STORAGE:
|
||||||
|
if not all(
|
||||||
|
credentials.get(key)
|
||||||
|
for key in ["namespace", "region", "access_key_id", "secret_access_key"]
|
||||||
|
):
|
||||||
|
raise ConnectorMissingCredentialError("Oracle Cloud Infrastructure")
|
||||||
|
|
||||||
|
self.s3_client = boto3.client(
|
||||||
|
"s3",
|
||||||
|
endpoint_url=f"https://{credentials['namespace']}.compat.objectstorage.{credentials['region']}.oraclecloud.com",
|
||||||
|
aws_access_key_id=credentials["access_key_id"],
|
||||||
|
aws_secret_access_key=credentials["secret_access_key"],
|
||||||
|
region_name=credentials["region"],
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unsupported bucket type: {self.bucket_type}")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _download_object(self, key: str) -> bytes:
|
||||||
|
if self.s3_client is None:
|
||||||
|
raise ConnectorMissingCredentialError("Blob storage")
|
||||||
|
object = self.s3_client.get_object(Bucket=self.bucket_name, Key=key)
|
||||||
|
return object["Body"].read()
|
||||||
|
|
||||||
|
# NOTE: Left in as may be useful for one-off access to documents and sharing across orgs.
|
||||||
|
# def _get_presigned_url(self, key: str) -> str:
|
||||||
|
# if self.s3_client is None:
|
||||||
|
# raise ConnectorMissingCredentialError("Blog storage")
|
||||||
|
|
||||||
|
# url = self.s3_client.generate_presigned_url(
|
||||||
|
# "get_object",
|
||||||
|
# Params={"Bucket": self.bucket_name, "Key": key},
|
||||||
|
# ExpiresIn=self.presign_length,
|
||||||
|
# )
|
||||||
|
# return url
|
||||||
|
|
||||||
|
def _get_blob_link(self, key: str) -> str:
|
||||||
|
if self.s3_client is None:
|
||||||
|
raise ConnectorMissingCredentialError("Blob storage")
|
||||||
|
|
||||||
|
if self.bucket_type == BlobType.R2:
|
||||||
|
account_id = self.s3_client.meta.endpoint_url.split("//")[1].split(".")[0]
|
||||||
|
return f"https://{account_id}.r2.cloudflarestorage.com/{self.bucket_name}/{key}"
|
||||||
|
|
||||||
|
elif self.bucket_type == BlobType.S3:
|
||||||
|
region = self.s3_client.meta.region_name
|
||||||
|
return f"https://{self.bucket_name}.s3.{region}.amazonaws.com/{key}"
|
||||||
|
|
||||||
|
elif self.bucket_type == BlobType.GOOGLE_CLOUD_STORAGE:
|
||||||
|
return f"https://storage.cloud.google.com/{self.bucket_name}/{key}"
|
||||||
|
|
||||||
|
elif self.bucket_type == BlobType.OCI_STORAGE:
|
||||||
|
namespace = self.s3_client.meta.endpoint_url.split("//")[1].split(".")[0]
|
||||||
|
region = self.s3_client.meta.region_name
|
||||||
|
return f"https://objectstorage.{region}.oraclecloud.com/n/{namespace}/b/{self.bucket_name}/o/{key}"
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unsupported bucket type: {self.bucket_type}")
|
||||||
|
|
||||||
|
def _yield_blob_objects(
|
||||||
|
self,
|
||||||
|
start: datetime,
|
||||||
|
end: datetime,
|
||||||
|
) -> GenerateDocumentsOutput:
|
||||||
|
if self.s3_client is None:
|
||||||
|
raise ConnectorMissingCredentialError("Blog storage")
|
||||||
|
|
||||||
|
paginator = self.s3_client.get_paginator("list_objects_v2")
|
||||||
|
pages = paginator.paginate(Bucket=self.bucket_name, Prefix=self.prefix)
|
||||||
|
|
||||||
|
batch: list[Document] = []
|
||||||
|
for page in pages:
|
||||||
|
if "Contents" not in page:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for obj in page["Contents"]:
|
||||||
|
if obj["Key"].endswith("/"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
last_modified = obj["LastModified"].replace(tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
if not start <= last_modified <= end:
|
||||||
|
continue
|
||||||
|
|
||||||
|
downloaded_file = self._download_object(obj["Key"])
|
||||||
|
link = self._get_blob_link(obj["Key"])
|
||||||
|
name = os.path.basename(obj["Key"])
|
||||||
|
|
||||||
|
try:
|
||||||
|
text = extract_file_text(
|
||||||
|
name,
|
||||||
|
BytesIO(downloaded_file),
|
||||||
|
break_on_unprocessable=False,
|
||||||
|
)
|
||||||
|
batch.append(
|
||||||
|
Document(
|
||||||
|
id=f"{self.bucket_type}:{self.bucket_name}:{obj['Key']}",
|
||||||
|
sections=[Section(link=link, text=text)],
|
||||||
|
source=DocumentSource(self.bucket_type.value),
|
||||||
|
semantic_identifier=name,
|
||||||
|
doc_updated_at=last_modified,
|
||||||
|
metadata={},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if len(batch) == self.batch_size:
|
||||||
|
yield batch
|
||||||
|
batch = []
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(
|
||||||
|
f"Error decoding object {obj['Key']} as UTF-8: {e}"
|
||||||
|
)
|
||||||
|
if batch:
|
||||||
|
yield batch
|
||||||
|
|
||||||
|
def load_from_state(self) -> GenerateDocumentsOutput:
|
||||||
|
logger.info("Loading blob objects")
|
||||||
|
return self._yield_blob_objects(
|
||||||
|
start=datetime(1970, 1, 1, tzinfo=timezone.utc),
|
||||||
|
end=datetime.now(timezone.utc),
|
||||||
|
)
|
||||||
|
|
||||||
|
def poll_source(
|
||||||
|
self, start: SecondsSinceUnixEpoch, end: SecondsSinceUnixEpoch
|
||||||
|
) -> GenerateDocumentsOutput:
|
||||||
|
if self.s3_client is None:
|
||||||
|
raise ConnectorMissingCredentialError("Blog storage")
|
||||||
|
|
||||||
|
start_datetime = datetime.fromtimestamp(start, tz=timezone.utc)
|
||||||
|
end_datetime = datetime.fromtimestamp(end, tz=timezone.utc)
|
||||||
|
|
||||||
|
for batch in self._yield_blob_objects(start_datetime, end_datetime):
|
||||||
|
yield batch
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
credentials_dict = {
|
||||||
|
"aws_access_key_id": os.environ.get("AWS_ACCESS_KEY_ID"),
|
||||||
|
"aws_secret_access_key": os.environ.get("AWS_SECRET_ACCESS_KEY"),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Initialize the connector
|
||||||
|
connector = BlobStorageConnector(
|
||||||
|
bucket_type=os.environ.get("BUCKET_TYPE") or "s3",
|
||||||
|
bucket_name=os.environ.get("BUCKET_NAME") or "test",
|
||||||
|
prefix="",
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
connector.load_credentials(credentials_dict)
|
||||||
|
document_batch_generator = connector.load_from_state()
|
||||||
|
for document_batch in document_batch_generator:
|
||||||
|
print("First batch of documents:")
|
||||||
|
for doc in document_batch:
|
||||||
|
print(f"Document ID: {doc.id}")
|
||||||
|
print(f"Semantic Identifier: {doc.semantic_identifier}")
|
||||||
|
print(f"Source: {doc.source}")
|
||||||
|
print(f"Updated At: {doc.doc_updated_at}")
|
||||||
|
print("Sections:")
|
||||||
|
for section in doc.sections:
|
||||||
|
print(f" - Link: {section.link}")
|
||||||
|
print(f" - Text: {section.text[:100]}...")
|
||||||
|
print("---")
|
||||||
|
break
|
||||||
|
|
||||||
|
except ConnectorMissingCredentialError as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"An unexpected error occurred: {e}")
|
@ -5,6 +5,7 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
from danswer.configs.constants import DocumentSource
|
from danswer.configs.constants import DocumentSource
|
||||||
from danswer.connectors.axero.connector import AxeroConnector
|
from danswer.connectors.axero.connector import AxeroConnector
|
||||||
|
from danswer.connectors.blob.connector import BlobStorageConnector
|
||||||
from danswer.connectors.bookstack.connector import BookstackConnector
|
from danswer.connectors.bookstack.connector import BookstackConnector
|
||||||
from danswer.connectors.clickup.connector import ClickupConnector
|
from danswer.connectors.clickup.connector import ClickupConnector
|
||||||
from danswer.connectors.confluence.connector import ConfluenceConnector
|
from danswer.connectors.confluence.connector import ConfluenceConnector
|
||||||
@ -90,6 +91,10 @@ def identify_connector_class(
|
|||||||
DocumentSource.CLICKUP: ClickupConnector,
|
DocumentSource.CLICKUP: ClickupConnector,
|
||||||
DocumentSource.MEDIAWIKI: MediaWikiConnector,
|
DocumentSource.MEDIAWIKI: MediaWikiConnector,
|
||||||
DocumentSource.WIKIPEDIA: WikipediaConnector,
|
DocumentSource.WIKIPEDIA: WikipediaConnector,
|
||||||
|
DocumentSource.S3: BlobStorageConnector,
|
||||||
|
DocumentSource.R2: BlobStorageConnector,
|
||||||
|
DocumentSource.GOOGLE_CLOUD_STORAGE: BlobStorageConnector,
|
||||||
|
DocumentSource.OCI_STORAGE: BlobStorageConnector,
|
||||||
}
|
}
|
||||||
connector_by_source = connector_map.get(source, {})
|
connector_by_source = connector_map.get(source, {})
|
||||||
|
|
||||||
@ -115,7 +120,6 @@ def identify_connector_class(
|
|||||||
raise ConnectorMissingException(
|
raise ConnectorMissingException(
|
||||||
f"Connector for source={source} does not accept input_type={input_type}"
|
f"Connector for source={source} does not accept input_type={input_type}"
|
||||||
)
|
)
|
||||||
|
|
||||||
return connector
|
return connector
|
||||||
|
|
||||||
|
|
||||||
|
@ -71,4 +71,4 @@ uvicorn==0.21.1
|
|||||||
zulip==0.8.2
|
zulip==0.8.2
|
||||||
hubspot-api-client==8.1.0
|
hubspot-api-client==8.1.0
|
||||||
zenpy==2.0.41
|
zenpy==2.0.41
|
||||||
dropbox==11.36.2
|
dropbox==11.36.2
|
@ -19,3 +19,4 @@ types-regex==2023.3.23.1
|
|||||||
types-requests==2.28.11.17
|
types-requests==2.28.11.17
|
||||||
types-retry==0.9.9.3
|
types-retry==0.9.9.3
|
||||||
types-urllib3==1.26.25.11
|
types-urllib3==1.26.25.11
|
||||||
|
boto3-stubs[s3]==1.34.133
|
BIN
web/public/GoogleCloudStorage.png
Normal file
BIN
web/public/GoogleCloudStorage.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
1
web/public/OCI.svg
Normal file
1
web/public/OCI.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg style="display:block" class="u30-oicn" xmlns="http://www.w3.org/2000/svg" width="32" height="21" viewBox="0 0 32 21"><path fill="#C74634" d="M9.9,20.1c-5.5,0-9.9-4.4-9.9-9.9c0-5.5,4.4-9.9,9.9-9.9h11.6c5.5,0,9.9,4.4,9.9,9.9c0,5.5-4.4,9.9-9.9,9.9H9.9 M21.2,16.6c3.6,0,6.4-2.9,6.4-6.4c0-3.6-2.9-6.4-6.4-6.4h-11c-3.6,0-6.4,2.9-6.4,6.4s2.9,6.4,6.4,6.4H21.2"></path></svg>
|
After Width: | Height: | Size: 371 B |
BIN
web/public/S3.png
Normal file
BIN
web/public/S3.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 51 KiB |
BIN
web/public/r2.webp
Normal file
BIN
web/public/r2.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 938 B |
257
web/src/app/admin/connectors/google-storage/page.tsx
Normal file
257
web/src/app/admin/connectors/google-storage/page.tsx
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { AdminPageTitle } from "@/components/admin/Title";
|
||||||
|
import { HealthCheckBanner } from "@/components/health/healthcheck";
|
||||||
|
import { GoogleStorageIcon, TrashIcon } from "@/components/icons/icons";
|
||||||
|
import { LoadingAnimation } from "@/components/Loading";
|
||||||
|
import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
|
||||||
|
import { CredentialForm } from "@/components/admin/connectors/CredentialForm";
|
||||||
|
import { TextFormField } from "@/components/admin/connectors/Field";
|
||||||
|
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||||
|
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
|
||||||
|
import { adminDeleteCredential, linkCredential } from "@/lib/credential";
|
||||||
|
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||||
|
import { ErrorCallout } from "@/components/ErrorCallout";
|
||||||
|
import { usePublicCredentials } from "@/lib/hooks";
|
||||||
|
import { ConnectorIndexingStatus, Credential } from "@/lib/types";
|
||||||
|
|
||||||
|
import { GCSConfig, GCSCredentialJson } from "@/lib/types";
|
||||||
|
|
||||||
|
import { Card, Select, SelectItem, Text, Title } from "@tremor/react";
|
||||||
|
import useSWR, { useSWRConfig } from "swr";
|
||||||
|
import * as Yup from "yup";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
const GCSMain = () => {
|
||||||
|
const { popup, setPopup } = usePopup();
|
||||||
|
const { mutate } = useSWRConfig();
|
||||||
|
const {
|
||||||
|
data: connectorIndexingStatuses,
|
||||||
|
isLoading: isConnectorIndexingStatusesLoading,
|
||||||
|
error: connectorIndexingStatusesError,
|
||||||
|
} = useSWR<ConnectorIndexingStatus<any, any>[]>(
|
||||||
|
"/api/manage/admin/connector/indexing-status",
|
||||||
|
errorHandlingFetcher
|
||||||
|
);
|
||||||
|
const {
|
||||||
|
data: credentialsData,
|
||||||
|
isLoading: isCredentialsLoading,
|
||||||
|
error: credentialsError,
|
||||||
|
refreshCredentials,
|
||||||
|
} = usePublicCredentials();
|
||||||
|
|
||||||
|
if (
|
||||||
|
(!connectorIndexingStatuses && isConnectorIndexingStatusesLoading) ||
|
||||||
|
(!credentialsData && isCredentialsLoading)
|
||||||
|
) {
|
||||||
|
return <LoadingAnimation text="Loading" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connectorIndexingStatusesError || !connectorIndexingStatuses) {
|
||||||
|
return (
|
||||||
|
<ErrorCallout
|
||||||
|
errorTitle="Something went wrong :("
|
||||||
|
errorMsg={connectorIndexingStatusesError?.info?.detail}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credentialsError || !credentialsData) {
|
||||||
|
return (
|
||||||
|
<ErrorCallout
|
||||||
|
errorTitle="Something went wrong :("
|
||||||
|
errorMsg={credentialsError?.info?.detail}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const gcsConnectorIndexingStatuses: ConnectorIndexingStatus<
|
||||||
|
GCSConfig,
|
||||||
|
GCSCredentialJson
|
||||||
|
>[] = connectorIndexingStatuses.filter(
|
||||||
|
(connectorIndexingStatus) =>
|
||||||
|
connectorIndexingStatus.connector.source === "google_cloud_storage"
|
||||||
|
);
|
||||||
|
|
||||||
|
const gcsCredential: Credential<GCSCredentialJson> | undefined =
|
||||||
|
credentialsData.find(
|
||||||
|
(credential) => credential.credential_json?.project_id
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{popup}
|
||||||
|
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||||
|
Step 1: Provide your GCS access info
|
||||||
|
</Title>
|
||||||
|
{gcsCredential ? (
|
||||||
|
<>
|
||||||
|
<div className="flex mb-1 text-sm">
|
||||||
|
<p className="my-auto">Existing GCS Access Key ID: </p>
|
||||||
|
<p className="ml-1 italic my-auto">
|
||||||
|
{gcsCredential.credential_json.access_key_id}
|
||||||
|
</p>
|
||||||
|
{", "}
|
||||||
|
<p className="ml-1 my-auto">Secret Access Key: </p>
|
||||||
|
<p className="ml-1 italic my-auto">
|
||||||
|
{gcsCredential.credential_json.secret_access_key}
|
||||||
|
</p>{" "}
|
||||||
|
<button
|
||||||
|
className="ml-1 hover:bg-hover rounded p-1"
|
||||||
|
onClick={async () => {
|
||||||
|
if (gcsConnectorIndexingStatuses.length > 0) {
|
||||||
|
setPopup({
|
||||||
|
type: "error",
|
||||||
|
message:
|
||||||
|
"Must delete all connectors before deleting credentials",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await adminDeleteCredential(gcsCredential.id);
|
||||||
|
refreshCredentials();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TrashIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Text>
|
||||||
|
<ul className="list-disc mt-2 ml-4">
|
||||||
|
<li>
|
||||||
|
Provide your GCS Project ID, Client Email, and Private Key for
|
||||||
|
authentication.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
These credentials will be used to access your GCS buckets.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Text>
|
||||||
|
<Card className="mt-4">
|
||||||
|
<CredentialForm<GCSCredentialJson>
|
||||||
|
formBody={
|
||||||
|
<>
|
||||||
|
<TextFormField name="project_id" label="GCS Project ID:" />
|
||||||
|
<TextFormField name="access_key_id" label="Access Key ID:" />
|
||||||
|
<TextFormField
|
||||||
|
name="secret_access_key"
|
||||||
|
label="Secret Access Key:"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
validationSchema={Yup.object().shape({
|
||||||
|
secret_access_key: Yup.string().required(
|
||||||
|
"Client Email is required"
|
||||||
|
),
|
||||||
|
access_key_id: Yup.string().required("Private Key is required"),
|
||||||
|
})}
|
||||||
|
initialValues={{
|
||||||
|
secret_access_key: "",
|
||||||
|
access_key_id: "",
|
||||||
|
}}
|
||||||
|
onSubmit={(isSuccess) => {
|
||||||
|
if (isSuccess) {
|
||||||
|
refreshCredentials();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||||
|
Step 2: Which GCS bucket do you want to make searchable?
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
{gcsConnectorIndexingStatuses.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||||
|
GCS indexing status
|
||||||
|
</Title>
|
||||||
|
<Text className="mb-2">
|
||||||
|
The latest changes are fetched every 10 minutes.
|
||||||
|
</Text>
|
||||||
|
<div className="mb-2">
|
||||||
|
<ConnectorsTable<GCSConfig, GCSCredentialJson>
|
||||||
|
includeName={true}
|
||||||
|
connectorIndexingStatuses={gcsConnectorIndexingStatuses}
|
||||||
|
liveCredential={gcsCredential}
|
||||||
|
getCredential={(credential) => {
|
||||||
|
return <div></div>;
|
||||||
|
}}
|
||||||
|
onCredentialLink={async (connectorId) => {
|
||||||
|
if (gcsCredential) {
|
||||||
|
await linkCredential(connectorId, gcsCredential.id);
|
||||||
|
mutate("/api/manage/admin/connector/indexing-status");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onUpdate={() =>
|
||||||
|
mutate("/api/manage/admin/connector/indexing-status")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{gcsCredential && (
|
||||||
|
<>
|
||||||
|
<Card className="mt-4">
|
||||||
|
<h2 className="font-bold mb-3">Create Connection</h2>
|
||||||
|
<Text className="mb-4">
|
||||||
|
Press connect below to start the connection to your GCS bucket.
|
||||||
|
</Text>
|
||||||
|
<ConnectorForm<GCSConfig>
|
||||||
|
nameBuilder={(values) => `GCSConnector-${values.bucket_name}`}
|
||||||
|
ccPairNameBuilder={(values) =>
|
||||||
|
`GCSConnector-${values.bucket_name}`
|
||||||
|
}
|
||||||
|
source="google_cloud_storage"
|
||||||
|
inputType="poll"
|
||||||
|
formBodyBuilder={(values) => (
|
||||||
|
<div>
|
||||||
|
<TextFormField name="bucket_name" label="Bucket Name:" />
|
||||||
|
<TextFormField
|
||||||
|
name="prefix"
|
||||||
|
label="Path Prefix (optional):"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
validationSchema={Yup.object().shape({
|
||||||
|
bucket_type: Yup.string()
|
||||||
|
.oneOf(["google_cloud_storage"])
|
||||||
|
.required("Bucket type must be google_cloud_storage"),
|
||||||
|
bucket_name: Yup.string().required(
|
||||||
|
"Please enter the name of the GCS bucket to index, e.g. my-gcs-bucket"
|
||||||
|
),
|
||||||
|
prefix: Yup.string().default(""),
|
||||||
|
})}
|
||||||
|
initialValues={{
|
||||||
|
bucket_type: "google_cloud_storage",
|
||||||
|
bucket_name: "",
|
||||||
|
prefix: "",
|
||||||
|
}}
|
||||||
|
refreshFreq={60 * 60 * 24} // 1 day
|
||||||
|
credentialId={gcsCredential.id}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<div className="mx-auto container">
|
||||||
|
<div className="mb-4">
|
||||||
|
<HealthCheckBanner />
|
||||||
|
</div>
|
||||||
|
<AdminPageTitle
|
||||||
|
icon={<GoogleStorageIcon size={32} />}
|
||||||
|
title="Google Cloud Storage"
|
||||||
|
/>
|
||||||
|
<GCSMain />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
272
web/src/app/admin/connectors/oracle-storage/page.tsx
Normal file
272
web/src/app/admin/connectors/oracle-storage/page.tsx
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { AdminPageTitle } from "@/components/admin/Title";
|
||||||
|
import { HealthCheckBanner } from "@/components/health/healthcheck";
|
||||||
|
import { OCIStorageIcon, TrashIcon } from "@/components/icons/icons";
|
||||||
|
import { LoadingAnimation } from "@/components/Loading";
|
||||||
|
import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
|
||||||
|
import { CredentialForm } from "@/components/admin/connectors/CredentialForm";
|
||||||
|
import { TextFormField } from "@/components/admin/connectors/Field";
|
||||||
|
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||||
|
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
|
||||||
|
import { adminDeleteCredential, linkCredential } from "@/lib/credential";
|
||||||
|
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||||
|
import { ErrorCallout } from "@/components/ErrorCallout";
|
||||||
|
import { usePublicCredentials } from "@/lib/hooks";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ConnectorIndexingStatus,
|
||||||
|
Credential,
|
||||||
|
OCIConfig,
|
||||||
|
OCICredentialJson,
|
||||||
|
R2Config,
|
||||||
|
R2CredentialJson,
|
||||||
|
} from "@/lib/types";
|
||||||
|
import { Card, Select, SelectItem, Text, Title } from "@tremor/react";
|
||||||
|
import useSWR, { useSWRConfig } from "swr";
|
||||||
|
import * as Yup from "yup";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
const OCIMain = () => {
|
||||||
|
const { popup, setPopup } = usePopup();
|
||||||
|
|
||||||
|
const { mutate } = useSWRConfig();
|
||||||
|
const {
|
||||||
|
data: connectorIndexingStatuses,
|
||||||
|
isLoading: isConnectorIndexingStatusesLoading,
|
||||||
|
error: connectorIndexingStatusesError,
|
||||||
|
} = useSWR<ConnectorIndexingStatus<any, any>[]>(
|
||||||
|
"/api/manage/admin/connector/indexing-status",
|
||||||
|
errorHandlingFetcher
|
||||||
|
);
|
||||||
|
const {
|
||||||
|
data: credentialsData,
|
||||||
|
isLoading: isCredentialsLoading,
|
||||||
|
error: credentialsError,
|
||||||
|
refreshCredentials,
|
||||||
|
} = usePublicCredentials();
|
||||||
|
|
||||||
|
if (
|
||||||
|
(!connectorIndexingStatuses && isConnectorIndexingStatusesLoading) ||
|
||||||
|
(!credentialsData && isCredentialsLoading)
|
||||||
|
) {
|
||||||
|
return <LoadingAnimation text="Loading" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connectorIndexingStatusesError || !connectorIndexingStatuses) {
|
||||||
|
return (
|
||||||
|
<ErrorCallout
|
||||||
|
errorTitle="Something went wrong :("
|
||||||
|
errorMsg={connectorIndexingStatusesError?.info?.detail}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credentialsError || !credentialsData) {
|
||||||
|
return (
|
||||||
|
<ErrorCallout
|
||||||
|
errorTitle="Something went wrong :("
|
||||||
|
errorMsg={credentialsError?.info?.detail}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ociConnectorIndexingStatuses: ConnectorIndexingStatus<
|
||||||
|
OCIConfig,
|
||||||
|
OCICredentialJson
|
||||||
|
>[] = connectorIndexingStatuses.filter(
|
||||||
|
(connectorIndexingStatus) =>
|
||||||
|
connectorIndexingStatus.connector.source === "oci_storage"
|
||||||
|
);
|
||||||
|
|
||||||
|
const ociCredential: Credential<OCICredentialJson> | undefined =
|
||||||
|
credentialsData.find((credential) => credential.credential_json?.namespace);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{popup}
|
||||||
|
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||||
|
Step 1: Provide your access info
|
||||||
|
</Title>
|
||||||
|
{ociCredential ? (
|
||||||
|
<>
|
||||||
|
{" "}
|
||||||
|
<div className="flex mb-1 text-sm">
|
||||||
|
<p className="my-auto">Existing OCI Access Key ID: </p>
|
||||||
|
<p className="ml-1 italic my-auto">
|
||||||
|
{ociCredential.credential_json.access_key_id}
|
||||||
|
</p>
|
||||||
|
{", "}
|
||||||
|
<p className="ml-1 my-auto">Namespace: </p>
|
||||||
|
<p className="ml-1 italic my-auto">
|
||||||
|
{ociCredential.credential_json.namespace}
|
||||||
|
</p>{" "}
|
||||||
|
<button
|
||||||
|
className="ml-1 hover:bg-hover rounded p-1"
|
||||||
|
onClick={async () => {
|
||||||
|
if (ociConnectorIndexingStatuses.length > 0) {
|
||||||
|
setPopup({
|
||||||
|
type: "error",
|
||||||
|
message:
|
||||||
|
"Must delete all connectors before deleting credentials",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await adminDeleteCredential(ociCredential.id);
|
||||||
|
refreshCredentials();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TrashIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Text>
|
||||||
|
<ul className="list-disc mt-2 ml-4">
|
||||||
|
<li>
|
||||||
|
Provide your OCI Access Key ID, Secret Access Key, Namespace,
|
||||||
|
and Region for authentication.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
These credentials will be used to access your OCI buckets.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Text>
|
||||||
|
<Card className="mt-4">
|
||||||
|
<CredentialForm<OCICredentialJson>
|
||||||
|
formBody={
|
||||||
|
<>
|
||||||
|
<TextFormField
|
||||||
|
name="access_key_id"
|
||||||
|
label="OCI Access Key ID:"
|
||||||
|
/>
|
||||||
|
<TextFormField
|
||||||
|
name="secret_access_key"
|
||||||
|
label="OCI Secret Access Key:"
|
||||||
|
/>
|
||||||
|
<TextFormField name="namespace" label="Namespace:" />
|
||||||
|
<TextFormField name="region" label="Region:" />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
validationSchema={Yup.object().shape({
|
||||||
|
access_key_id: Yup.string().required(
|
||||||
|
"OCI Access Key ID is required"
|
||||||
|
),
|
||||||
|
secret_access_key: Yup.string().required(
|
||||||
|
"OCI Secret Access Key is required"
|
||||||
|
),
|
||||||
|
namespace: Yup.string().required("Namespace is required"),
|
||||||
|
region: Yup.string().required("Region is required"),
|
||||||
|
})}
|
||||||
|
initialValues={{
|
||||||
|
access_key_id: "",
|
||||||
|
secret_access_key: "",
|
||||||
|
namespace: "",
|
||||||
|
region: "",
|
||||||
|
}}
|
||||||
|
onSubmit={(isSuccess) => {
|
||||||
|
if (isSuccess) {
|
||||||
|
refreshCredentials();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||||
|
Step 2: Which OCI bucket do you want to make searchable?
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
{ociConnectorIndexingStatuses.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||||
|
OCI indexing status
|
||||||
|
</Title>
|
||||||
|
<Text className="mb-2">
|
||||||
|
The latest changes are fetched every 10 minutes.
|
||||||
|
</Text>
|
||||||
|
<div className="mb-2">
|
||||||
|
<ConnectorsTable<OCIConfig, OCICredentialJson>
|
||||||
|
includeName={true}
|
||||||
|
connectorIndexingStatuses={ociConnectorIndexingStatuses}
|
||||||
|
liveCredential={ociCredential}
|
||||||
|
getCredential={(credential) => {
|
||||||
|
return <div></div>;
|
||||||
|
}}
|
||||||
|
onCredentialLink={async (connectorId) => {
|
||||||
|
if (ociCredential) {
|
||||||
|
await linkCredential(connectorId, ociCredential.id);
|
||||||
|
mutate("/api/manage/admin/connector/indexing-status");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onUpdate={() =>
|
||||||
|
mutate("/api/manage/admin/connector/indexing-status")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{ociCredential && (
|
||||||
|
<>
|
||||||
|
<Card className="mt-4">
|
||||||
|
<h2 className="font-bold mb-3">Create Connection</h2>
|
||||||
|
<Text className="mb-4">
|
||||||
|
Press connect below to start the connection to your OCI bucket.
|
||||||
|
</Text>
|
||||||
|
<ConnectorForm<OCIConfig>
|
||||||
|
nameBuilder={(values) => `OCIConnector-${values.bucket_name}`}
|
||||||
|
ccPairNameBuilder={(values) =>
|
||||||
|
`OCIConnector-${values.bucket_name}`
|
||||||
|
}
|
||||||
|
source="oci_storage"
|
||||||
|
inputType="poll"
|
||||||
|
formBodyBuilder={(values) => (
|
||||||
|
<div>
|
||||||
|
<TextFormField name="bucket_name" label="Bucket Name:" />
|
||||||
|
<TextFormField
|
||||||
|
name="prefix"
|
||||||
|
label="Path Prefix (optional):"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
validationSchema={Yup.object().shape({
|
||||||
|
bucket_type: Yup.string()
|
||||||
|
.oneOf(["oci_storage"])
|
||||||
|
.required("Bucket type must be oci_storage"),
|
||||||
|
bucket_name: Yup.string().required(
|
||||||
|
"Please enter the name of the OCI bucket to index, e.g. my-test-bucket"
|
||||||
|
),
|
||||||
|
prefix: Yup.string().default(""),
|
||||||
|
})}
|
||||||
|
initialValues={{
|
||||||
|
bucket_type: "oci_storage",
|
||||||
|
bucket_name: "",
|
||||||
|
prefix: "",
|
||||||
|
}}
|
||||||
|
refreshFreq={60 * 60 * 24} // 1 day
|
||||||
|
credentialId={ociCredential.id}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<div className="mx-auto container">
|
||||||
|
<div className="mb-4">
|
||||||
|
<HealthCheckBanner />
|
||||||
|
</div>
|
||||||
|
<AdminPageTitle
|
||||||
|
icon={<OCIStorageIcon size={32} />}
|
||||||
|
title="Oracle Cloud Infrastructure"
|
||||||
|
/>
|
||||||
|
<OCIMain />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
265
web/src/app/admin/connectors/r2/page.tsx
Normal file
265
web/src/app/admin/connectors/r2/page.tsx
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { AdminPageTitle } from "@/components/admin/Title";
|
||||||
|
import { HealthCheckBanner } from "@/components/health/healthcheck";
|
||||||
|
import { R2Icon, S3Icon, TrashIcon } from "@/components/icons/icons";
|
||||||
|
import { LoadingAnimation } from "@/components/Loading";
|
||||||
|
import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
|
||||||
|
import { CredentialForm } from "@/components/admin/connectors/CredentialForm";
|
||||||
|
import { TextFormField } from "@/components/admin/connectors/Field";
|
||||||
|
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||||
|
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
|
||||||
|
import { adminDeleteCredential, linkCredential } from "@/lib/credential";
|
||||||
|
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||||
|
import { ErrorCallout } from "@/components/ErrorCallout";
|
||||||
|
import { usePublicCredentials } from "@/lib/hooks";
|
||||||
|
import {
|
||||||
|
ConnectorIndexingStatus,
|
||||||
|
Credential,
|
||||||
|
R2Config,
|
||||||
|
R2CredentialJson,
|
||||||
|
} from "@/lib/types";
|
||||||
|
import { Card, Select, SelectItem, Text, Title } from "@tremor/react";
|
||||||
|
import useSWR, { useSWRConfig } from "swr";
|
||||||
|
import * as Yup from "yup";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
const R2Main = () => {
|
||||||
|
const { popup, setPopup } = usePopup();
|
||||||
|
|
||||||
|
const { mutate } = useSWRConfig();
|
||||||
|
const {
|
||||||
|
data: connectorIndexingStatuses,
|
||||||
|
isLoading: isConnectorIndexingStatusesLoading,
|
||||||
|
error: connectorIndexingStatusesError,
|
||||||
|
} = useSWR<ConnectorIndexingStatus<any, any>[]>(
|
||||||
|
"/api/manage/admin/connector/indexing-status",
|
||||||
|
errorHandlingFetcher
|
||||||
|
);
|
||||||
|
const {
|
||||||
|
data: credentialsData,
|
||||||
|
isLoading: isCredentialsLoading,
|
||||||
|
error: credentialsError,
|
||||||
|
refreshCredentials,
|
||||||
|
} = usePublicCredentials();
|
||||||
|
|
||||||
|
if (
|
||||||
|
(!connectorIndexingStatuses && isConnectorIndexingStatusesLoading) ||
|
||||||
|
(!credentialsData && isCredentialsLoading)
|
||||||
|
) {
|
||||||
|
return <LoadingAnimation text="Loading" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connectorIndexingStatusesError || !connectorIndexingStatuses) {
|
||||||
|
return (
|
||||||
|
<ErrorCallout
|
||||||
|
errorTitle="Something went wrong :("
|
||||||
|
errorMsg={connectorIndexingStatusesError?.info?.detail}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credentialsError || !credentialsData) {
|
||||||
|
return (
|
||||||
|
<ErrorCallout
|
||||||
|
errorTitle="Something went wrong :("
|
||||||
|
errorMsg={credentialsError?.info?.detail}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const r2ConnectorIndexingStatuses: ConnectorIndexingStatus<
|
||||||
|
R2Config,
|
||||||
|
R2CredentialJson
|
||||||
|
>[] = connectorIndexingStatuses.filter(
|
||||||
|
(connectorIndexingStatus) =>
|
||||||
|
connectorIndexingStatus.connector.source === "r2"
|
||||||
|
);
|
||||||
|
|
||||||
|
const r2Credential: Credential<R2CredentialJson> | undefined =
|
||||||
|
credentialsData.find(
|
||||||
|
(credential) => credential.credential_json?.account_id
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{popup}
|
||||||
|
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||||
|
Step 1: Provide your access info
|
||||||
|
</Title>
|
||||||
|
{r2Credential ? (
|
||||||
|
<>
|
||||||
|
{" "}
|
||||||
|
<div className="flex mb-1 text-sm">
|
||||||
|
<p className="my-auto">Existing R2 Access Key ID: </p>
|
||||||
|
<p className="ml-1 italic my-auto">
|
||||||
|
{r2Credential.credential_json.r2_access_key_id}
|
||||||
|
</p>
|
||||||
|
{", "}
|
||||||
|
<p className="ml-1 my-auto">Account ID: </p>
|
||||||
|
<p className="ml-1 italic my-auto">
|
||||||
|
{r2Credential.credential_json.account_id}
|
||||||
|
</p>{" "}
|
||||||
|
<button
|
||||||
|
className="ml-1 hover:bg-hover rounded p-1"
|
||||||
|
onClick={async () => {
|
||||||
|
if (r2ConnectorIndexingStatuses.length > 0) {
|
||||||
|
setPopup({
|
||||||
|
type: "error",
|
||||||
|
message:
|
||||||
|
"Must delete all connectors before deleting credentials",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await adminDeleteCredential(r2Credential.id);
|
||||||
|
refreshCredentials();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TrashIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Text>
|
||||||
|
<ul className="list-disc mt-2 ml-4">
|
||||||
|
<li>
|
||||||
|
Provide your R2 Access Key ID, Secret Access Key, and Account ID
|
||||||
|
for authentication.
|
||||||
|
</li>
|
||||||
|
<li>These credentials will be used to access your R2 buckets.</li>
|
||||||
|
</ul>
|
||||||
|
</Text>
|
||||||
|
<Card className="mt-4">
|
||||||
|
<CredentialForm<R2CredentialJson>
|
||||||
|
formBody={
|
||||||
|
<>
|
||||||
|
<TextFormField
|
||||||
|
name="r2_access_key_id"
|
||||||
|
label="R2 Access Key ID:"
|
||||||
|
/>
|
||||||
|
<TextFormField
|
||||||
|
name="r2_secret_access_key"
|
||||||
|
label="R2 Secret Access Key:"
|
||||||
|
/>
|
||||||
|
<TextFormField name="account_id" label="Account ID:" />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
validationSchema={Yup.object().shape({
|
||||||
|
r2_access_key_id: Yup.string().required(
|
||||||
|
"R2 Access Key ID is required"
|
||||||
|
),
|
||||||
|
r2_secret_access_key: Yup.string().required(
|
||||||
|
"R2 Secret Access Key is required"
|
||||||
|
),
|
||||||
|
account_id: Yup.string().required("Account ID is required"),
|
||||||
|
})}
|
||||||
|
initialValues={{
|
||||||
|
r2_access_key_id: "",
|
||||||
|
r2_secret_access_key: "",
|
||||||
|
account_id: "",
|
||||||
|
}}
|
||||||
|
onSubmit={(isSuccess) => {
|
||||||
|
if (isSuccess) {
|
||||||
|
refreshCredentials();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||||
|
Step 2: Which R2 bucket do you want to make searchable?
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
{r2ConnectorIndexingStatuses.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||||
|
R2 indexing status
|
||||||
|
</Title>
|
||||||
|
<Text className="mb-2">
|
||||||
|
The latest changes are fetched every 10 minutes.
|
||||||
|
</Text>
|
||||||
|
<div className="mb-2">
|
||||||
|
<ConnectorsTable<R2Config, R2CredentialJson>
|
||||||
|
includeName={true}
|
||||||
|
connectorIndexingStatuses={r2ConnectorIndexingStatuses}
|
||||||
|
liveCredential={r2Credential}
|
||||||
|
getCredential={(credential) => {
|
||||||
|
return <div></div>;
|
||||||
|
}}
|
||||||
|
onCredentialLink={async (connectorId) => {
|
||||||
|
if (r2Credential) {
|
||||||
|
await linkCredential(connectorId, r2Credential.id);
|
||||||
|
mutate("/api/manage/admin/connector/indexing-status");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onUpdate={() =>
|
||||||
|
mutate("/api/manage/admin/connector/indexing-status")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{r2Credential && (
|
||||||
|
<>
|
||||||
|
<Card className="mt-4">
|
||||||
|
<h2 className="font-bold mb-3">Create Connection</h2>
|
||||||
|
<Text className="mb-4">
|
||||||
|
Press connect below to start the connection to your R2 bucket.
|
||||||
|
</Text>
|
||||||
|
<ConnectorForm<R2Config>
|
||||||
|
nameBuilder={(values) => `R2Connector-${values.bucket_name}`}
|
||||||
|
ccPairNameBuilder={(values) =>
|
||||||
|
`R2Connector-${values.bucket_name}`
|
||||||
|
}
|
||||||
|
source="r2"
|
||||||
|
inputType="poll"
|
||||||
|
formBodyBuilder={(values) => (
|
||||||
|
<div>
|
||||||
|
<TextFormField name="bucket_name" label="Bucket Name:" />
|
||||||
|
<TextFormField
|
||||||
|
name="prefix"
|
||||||
|
label="Path Prefix (optional):"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
validationSchema={Yup.object().shape({
|
||||||
|
bucket_type: Yup.string()
|
||||||
|
.oneOf(["r2"])
|
||||||
|
.required("Bucket type must be r2"),
|
||||||
|
bucket_name: Yup.string().required(
|
||||||
|
"Please enter the name of the r2 bucket to index, e.g. my-test-bucket"
|
||||||
|
),
|
||||||
|
prefix: Yup.string().default(""),
|
||||||
|
})}
|
||||||
|
initialValues={{
|
||||||
|
bucket_type: "r2",
|
||||||
|
bucket_name: "",
|
||||||
|
prefix: "",
|
||||||
|
}}
|
||||||
|
refreshFreq={60 * 60 * 24} // 1 day
|
||||||
|
credentialId={r2Credential.id}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const [selectedStorage, setSelectedStorage] = useState<string>("s3");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto container">
|
||||||
|
<div className="mb-4">
|
||||||
|
<HealthCheckBanner />
|
||||||
|
</div>
|
||||||
|
<AdminPageTitle icon={<R2Icon size={32} />} title="R2 Storage" />
|
||||||
|
<R2Main key={2} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
258
web/src/app/admin/connectors/s3/page.tsx
Normal file
258
web/src/app/admin/connectors/s3/page.tsx
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { AdminPageTitle } from "@/components/admin/Title";
|
||||||
|
import { HealthCheckBanner } from "@/components/health/healthcheck";
|
||||||
|
import { S3Icon, TrashIcon } from "@/components/icons/icons";
|
||||||
|
import { LoadingAnimation } from "@/components/Loading";
|
||||||
|
import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
|
||||||
|
import { CredentialForm } from "@/components/admin/connectors/CredentialForm";
|
||||||
|
import { TextFormField } from "@/components/admin/connectors/Field";
|
||||||
|
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||||
|
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
|
||||||
|
import { adminDeleteCredential, linkCredential } from "@/lib/credential";
|
||||||
|
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||||
|
import { ErrorCallout } from "@/components/ErrorCallout";
|
||||||
|
import { usePublicCredentials } from "@/lib/hooks";
|
||||||
|
import {
|
||||||
|
ConnectorIndexingStatus,
|
||||||
|
Credential,
|
||||||
|
S3Config,
|
||||||
|
S3CredentialJson,
|
||||||
|
} from "@/lib/types";
|
||||||
|
import { Card, Text, Title } from "@tremor/react";
|
||||||
|
import useSWR, { useSWRConfig } from "swr";
|
||||||
|
import * as Yup from "yup";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
const S3Main = () => {
|
||||||
|
const { popup, setPopup } = usePopup();
|
||||||
|
|
||||||
|
const { mutate } = useSWRConfig();
|
||||||
|
const {
|
||||||
|
data: connectorIndexingStatuses,
|
||||||
|
isLoading: isConnectorIndexingStatusesLoading,
|
||||||
|
error: connectorIndexingStatusesError,
|
||||||
|
} = useSWR<ConnectorIndexingStatus<any, any>[]>(
|
||||||
|
"/api/manage/admin/connector/indexing-status",
|
||||||
|
errorHandlingFetcher
|
||||||
|
);
|
||||||
|
const {
|
||||||
|
data: credentialsData,
|
||||||
|
isLoading: isCredentialsLoading,
|
||||||
|
error: credentialsError,
|
||||||
|
refreshCredentials,
|
||||||
|
} = usePublicCredentials();
|
||||||
|
|
||||||
|
if (
|
||||||
|
(!connectorIndexingStatuses && isConnectorIndexingStatusesLoading) ||
|
||||||
|
(!credentialsData && isCredentialsLoading)
|
||||||
|
) {
|
||||||
|
return <LoadingAnimation text="Loading" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connectorIndexingStatusesError || !connectorIndexingStatuses) {
|
||||||
|
return (
|
||||||
|
<ErrorCallout
|
||||||
|
errorTitle="Something went wrong :("
|
||||||
|
errorMsg={connectorIndexingStatusesError?.info?.detail}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credentialsError || !credentialsData) {
|
||||||
|
return (
|
||||||
|
<ErrorCallout
|
||||||
|
errorTitle="Something went wrong :("
|
||||||
|
errorMsg={credentialsError?.info?.detail}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const s3ConnectorIndexingStatuses: ConnectorIndexingStatus<
|
||||||
|
S3Config,
|
||||||
|
S3CredentialJson
|
||||||
|
>[] = connectorIndexingStatuses.filter(
|
||||||
|
(connectorIndexingStatus) =>
|
||||||
|
connectorIndexingStatus.connector.source === "s3"
|
||||||
|
);
|
||||||
|
|
||||||
|
const s3Credential: Credential<S3CredentialJson> | undefined =
|
||||||
|
credentialsData.find(
|
||||||
|
(credential) => credential.credential_json?.aws_access_key_id
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{popup}
|
||||||
|
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||||
|
Step 1: Provide your access info
|
||||||
|
</Title>
|
||||||
|
{s3Credential ? (
|
||||||
|
<>
|
||||||
|
{" "}
|
||||||
|
<div className="flex mb-1 text-sm">
|
||||||
|
<p className="my-auto">Existing AWS Access Key ID: </p>
|
||||||
|
<p className="ml-1 italic my-auto">
|
||||||
|
{s3Credential.credential_json.aws_access_key_id}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
className="ml-1 hover:bg-hover rounded p-1"
|
||||||
|
onClick={async () => {
|
||||||
|
if (s3ConnectorIndexingStatuses.length > 0) {
|
||||||
|
setPopup({
|
||||||
|
type: "error",
|
||||||
|
message:
|
||||||
|
"Must delete all connectors before deleting credentials",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await adminDeleteCredential(s3Credential.id);
|
||||||
|
refreshCredentials();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TrashIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Text>
|
||||||
|
<ul className="list-disc mt-2 ml-4">
|
||||||
|
<li>
|
||||||
|
If AWS Access Key ID and AWS Secret Access Key are provided,
|
||||||
|
they will be used for authenticating the connector.
|
||||||
|
</li>
|
||||||
|
<li>Otherwise, the Profile Name will be used (if provided).</li>
|
||||||
|
<li>
|
||||||
|
If no credentials are provided, then the connector will try to
|
||||||
|
authenticate with any default AWS credentials available.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Text>
|
||||||
|
<Card className="mt-4">
|
||||||
|
<CredentialForm<S3CredentialJson>
|
||||||
|
formBody={
|
||||||
|
<>
|
||||||
|
<TextFormField
|
||||||
|
name="aws_access_key_id"
|
||||||
|
label="AWS Access Key ID:"
|
||||||
|
/>
|
||||||
|
<TextFormField
|
||||||
|
name="aws_secret_access_key"
|
||||||
|
label="AWS Secret Access Key:"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
validationSchema={Yup.object().shape({
|
||||||
|
aws_access_key_id: Yup.string().default(""),
|
||||||
|
aws_secret_access_key: Yup.string().default(""),
|
||||||
|
})}
|
||||||
|
initialValues={{
|
||||||
|
aws_access_key_id: "",
|
||||||
|
aws_secret_access_key: "",
|
||||||
|
}}
|
||||||
|
onSubmit={(isSuccess) => {
|
||||||
|
if (isSuccess) {
|
||||||
|
refreshCredentials();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||||
|
Step 2: Which S3 bucket do you want to make searchable?
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
{s3ConnectorIndexingStatuses.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||||
|
S3 indexing status
|
||||||
|
</Title>
|
||||||
|
<Text className="mb-2">
|
||||||
|
The latest changes are fetched every 10 minutes.
|
||||||
|
</Text>
|
||||||
|
<div className="mb-2">
|
||||||
|
<ConnectorsTable<S3Config, S3CredentialJson>
|
||||||
|
includeName={true}
|
||||||
|
connectorIndexingStatuses={s3ConnectorIndexingStatuses}
|
||||||
|
liveCredential={s3Credential}
|
||||||
|
getCredential={(credential) => {
|
||||||
|
return <div></div>;
|
||||||
|
}}
|
||||||
|
onCredentialLink={async (connectorId) => {
|
||||||
|
if (s3Credential) {
|
||||||
|
await linkCredential(connectorId, s3Credential.id);
|
||||||
|
mutate("/api/manage/admin/connector/indexing-status");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onUpdate={() =>
|
||||||
|
mutate("/api/manage/admin/connector/indexing-status")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{s3Credential && (
|
||||||
|
<>
|
||||||
|
<Card className="mt-4">
|
||||||
|
<h2 className="font-bold mb-3">Create Connection</h2>
|
||||||
|
<Text className="mb-4">
|
||||||
|
Press connect below to start the connection to your S3 bucket.
|
||||||
|
</Text>
|
||||||
|
<ConnectorForm<S3Config>
|
||||||
|
nameBuilder={(values) => `S3Connector-${values.bucket_name}`}
|
||||||
|
ccPairNameBuilder={(values) =>
|
||||||
|
`S3Connector-${values.bucket_name}`
|
||||||
|
}
|
||||||
|
source="s3"
|
||||||
|
inputType="poll"
|
||||||
|
formBodyBuilder={(values) => (
|
||||||
|
<div>
|
||||||
|
<TextFormField name="bucket_name" label="Bucket Name:" />
|
||||||
|
<TextFormField
|
||||||
|
name="prefix"
|
||||||
|
label="Path Prefix (optional):"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
validationSchema={Yup.object().shape({
|
||||||
|
bucket_type: Yup.string()
|
||||||
|
.oneOf(["s3"])
|
||||||
|
.required("Bucket type must be s3"),
|
||||||
|
bucket_name: Yup.string().required(
|
||||||
|
"Please enter the name of the s3 bucket to index, e.g. my-test-bucket"
|
||||||
|
),
|
||||||
|
prefix: Yup.string().default(""),
|
||||||
|
})}
|
||||||
|
initialValues={{
|
||||||
|
bucket_type: "s3",
|
||||||
|
bucket_name: "",
|
||||||
|
prefix: "",
|
||||||
|
}}
|
||||||
|
refreshFreq={60 * 60 * 24} // 1 day
|
||||||
|
credentialId={s3Credential.id}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const [selectedStorage, setSelectedStorage] = useState<string>("s3");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto container">
|
||||||
|
<div className="mb-4">
|
||||||
|
<HealthCheckBanner />
|
||||||
|
</div>
|
||||||
|
<AdminPageTitle icon={<S3Icon size={32} />} title="S3 Storage" />
|
||||||
|
|
||||||
|
<S3Main key={1} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -27,7 +27,6 @@ export async function submitConnector<T>(
|
|||||||
): Promise<{ message: string; isSuccess: boolean; response?: Connector<T> }> {
|
): Promise<{ message: string; isSuccess: boolean; response?: Connector<T> }> {
|
||||||
const isUpdate = connectorId !== undefined;
|
const isUpdate = connectorId !== undefined;
|
||||||
|
|
||||||
let isSuccess = false;
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
BASE_CONNECTOR_URL + (isUpdate ? `/${connectorId}` : ""),
|
BASE_CONNECTOR_URL + (isUpdate ? `/${connectorId}` : ""),
|
||||||
@ -41,7 +40,6 @@ export async function submitConnector<T>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
isSuccess = true;
|
|
||||||
const responseJson = await response.json();
|
const responseJson = await response.json();
|
||||||
return { message: "Success!", isSuccess: true, response: responseJson };
|
return { message: "Success!", isSuccess: true, response: responseJson };
|
||||||
} else {
|
} else {
|
||||||
@ -162,7 +160,6 @@ export function ConnectorForm<T extends Yup.AnyObject>({
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { message, isSuccess, response } = await submitConnector<T>({
|
const { message, isSuccess, response } = await submitConnector<T>({
|
||||||
name: connectorName,
|
name: connectorName,
|
||||||
source,
|
source,
|
||||||
|
@ -44,6 +44,8 @@ import { SiBookstack } from "react-icons/si";
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import jiraSVG from "../../../public/Jira.svg";
|
import jiraSVG from "../../../public/Jira.svg";
|
||||||
import confluenceSVG from "../../../public/Confluence.svg";
|
import confluenceSVG from "../../../public/Confluence.svg";
|
||||||
|
import OCIStorageSVG from "../../../public/OCI.svg";
|
||||||
|
import googleCloudStorageIcon from "../../../public/GoogleCloudStorage.png";
|
||||||
import guruIcon from "../../../public/Guru.svg";
|
import guruIcon from "../../../public/Guru.svg";
|
||||||
import gongIcon from "../../../public/Gong.png";
|
import gongIcon from "../../../public/Gong.png";
|
||||||
import requestTrackerIcon from "../../../public/RequestTracker.png";
|
import requestTrackerIcon from "../../../public/RequestTracker.png";
|
||||||
@ -54,6 +56,8 @@ import document360Icon from "../../../public/Document360.png";
|
|||||||
import googleSitesIcon from "../../../public/GoogleSites.png";
|
import googleSitesIcon from "../../../public/GoogleSites.png";
|
||||||
import zendeskIcon from "../../../public/Zendesk.svg";
|
import zendeskIcon from "../../../public/Zendesk.svg";
|
||||||
import dropboxIcon from "../../../public/Dropbox.png";
|
import dropboxIcon from "../../../public/Dropbox.png";
|
||||||
|
import s3Icon from "../../../public/S3.png";
|
||||||
|
import r2Icon from "../../../public/r2.webp";
|
||||||
import salesforceIcon from "../../../public/Salesforce.png";
|
import salesforceIcon from "../../../public/Salesforce.png";
|
||||||
import sharepointIcon from "../../../public/Sharepoint.png";
|
import sharepointIcon from "../../../public/Sharepoint.png";
|
||||||
import teamsIcon from "../../../public/Teams.png";
|
import teamsIcon from "../../../public/Teams.png";
|
||||||
@ -423,6 +427,20 @@ export const ConfluenceIcon = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const OCIStorageIcon = ({
|
||||||
|
size = 16,
|
||||||
|
className = defaultTailwindCSS,
|
||||||
|
}: IconProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{ width: `${size + 4}px`, height: `${size + 4}px` }}
|
||||||
|
className={`w-[${size + 4}px] h-[${size + 4}px] -m-0.5 ` + className}
|
||||||
|
>
|
||||||
|
<Image src={OCIStorageSVG} alt="Logo" width="96" height="96" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const JiraIcon = ({
|
export const JiraIcon = ({
|
||||||
size = 16,
|
size = 16,
|
||||||
className = defaultTailwindCSS,
|
className = defaultTailwindCSS,
|
||||||
@ -452,6 +470,20 @@ export const ZulipIcon = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const GoogleStorageIcon = ({
|
||||||
|
size = 16,
|
||||||
|
className = defaultTailwindCSS,
|
||||||
|
}: IconProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{ width: `${size + 4}px`, height: `${size + 4}px` }}
|
||||||
|
className={`w-[${size + 4}px] h-[${size + 4}px] -m-0.5 ` + className}
|
||||||
|
>
|
||||||
|
<Image src={googleCloudStorageIcon} alt="Logo" width="96" height="96" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const ProductboardIcon = ({
|
export const ProductboardIcon = ({
|
||||||
size = 16,
|
size = 16,
|
||||||
className = defaultTailwindCSS,
|
className = defaultTailwindCSS,
|
||||||
@ -543,6 +575,30 @@ export const SalesforceIcon = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const R2Icon = ({
|
||||||
|
size = 16,
|
||||||
|
className = defaultTailwindCSS,
|
||||||
|
}: IconProps) => (
|
||||||
|
<div
|
||||||
|
style={{ width: `${size}px`, height: `${size}px` }}
|
||||||
|
className={`w-[${size}px] h-[${size}px] ` + className}
|
||||||
|
>
|
||||||
|
<Image src={r2Icon} alt="Logo" width="96" height="96" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const S3Icon = ({
|
||||||
|
size = 16,
|
||||||
|
className = defaultTailwindCSS,
|
||||||
|
}: IconProps) => (
|
||||||
|
<div
|
||||||
|
style={{ width: `${size}px`, height: `${size}px` }}
|
||||||
|
className={`w-[${size}px] h-[${size}px] ` + className}
|
||||||
|
>
|
||||||
|
<Image src={s3Icon} alt="Logo" width="96" height="96" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
export const SharepointIcon = ({
|
export const SharepointIcon = ({
|
||||||
size = 16,
|
size = 16,
|
||||||
className = defaultTailwindCSS,
|
className = defaultTailwindCSS,
|
||||||
|
@ -22,6 +22,7 @@ import {
|
|||||||
NotionIcon,
|
NotionIcon,
|
||||||
ProductboardIcon,
|
ProductboardIcon,
|
||||||
RequestTrackerIcon,
|
RequestTrackerIcon,
|
||||||
|
R2Icon,
|
||||||
SalesforceIcon,
|
SalesforceIcon,
|
||||||
SharepointIcon,
|
SharepointIcon,
|
||||||
TeamsIcon,
|
TeamsIcon,
|
||||||
@ -31,10 +32,14 @@ import {
|
|||||||
ZulipIcon,
|
ZulipIcon,
|
||||||
MediaWikiIcon,
|
MediaWikiIcon,
|
||||||
WikipediaIcon,
|
WikipediaIcon,
|
||||||
|
S3Icon,
|
||||||
|
OCIStorageIcon,
|
||||||
|
GoogleStorageIcon,
|
||||||
} from "@/components/icons/icons";
|
} from "@/components/icons/icons";
|
||||||
import { ValidSources } from "./types";
|
import { ValidSources } from "./types";
|
||||||
import { SourceCategory, SourceMetadata } from "./search/interfaces";
|
import { SourceCategory, SourceMetadata } from "./search/interfaces";
|
||||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||||
|
import internal from "stream";
|
||||||
|
|
||||||
interface PartialSourceMetadata {
|
interface PartialSourceMetadata {
|
||||||
icon: React.FC<{ size?: number; className?: string }>;
|
icon: React.FC<{ size?: number; className?: string }>;
|
||||||
@ -207,6 +212,26 @@ const SOURCE_METADATA_MAP: SourceMap = {
|
|||||||
displayName: "Clickup",
|
displayName: "Clickup",
|
||||||
category: SourceCategory.AppConnection,
|
category: SourceCategory.AppConnection,
|
||||||
},
|
},
|
||||||
|
s3: {
|
||||||
|
icon: S3Icon,
|
||||||
|
displayName: "S3",
|
||||||
|
category: SourceCategory.AppConnection,
|
||||||
|
},
|
||||||
|
r2: {
|
||||||
|
icon: R2Icon,
|
||||||
|
displayName: "R2",
|
||||||
|
category: SourceCategory.AppConnection,
|
||||||
|
},
|
||||||
|
oci_storage: {
|
||||||
|
icon: OCIStorageIcon,
|
||||||
|
displayName: "Oracle Storage",
|
||||||
|
category: SourceCategory.AppConnection,
|
||||||
|
},
|
||||||
|
google_cloud_storage: {
|
||||||
|
icon: GoogleStorageIcon,
|
||||||
|
displayName: "Google Storage",
|
||||||
|
category: SourceCategory.AppConnection,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function fillSourceMetadata(
|
function fillSourceMetadata(
|
||||||
@ -223,13 +248,21 @@ function fillSourceMetadata(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getSourceMetadata(sourceType: ValidSources): SourceMetadata {
|
export function getSourceMetadata(sourceType: ValidSources): SourceMetadata {
|
||||||
return fillSourceMetadata(SOURCE_METADATA_MAP[sourceType], sourceType);
|
const response = fillSourceMetadata(
|
||||||
|
SOURCE_METADATA_MAP[sourceType],
|
||||||
|
sourceType
|
||||||
|
);
|
||||||
|
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listSourceMetadata(): SourceMetadata[] {
|
export function listSourceMetadata(): SourceMetadata[] {
|
||||||
return Object.entries(SOURCE_METADATA_MAP).map(([source, metadata]) => {
|
const entries = Object.entries(SOURCE_METADATA_MAP).map(
|
||||||
return fillSourceMetadata(metadata, source as ValidSources);
|
([source, metadata]) => {
|
||||||
});
|
return fillSourceMetadata(metadata, source as ValidSources);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSourceDisplayName(sourceType: ValidSources): string | null {
|
export function getSourceDisplayName(sourceType: ValidSources): string | null {
|
||||||
|
@ -59,7 +59,11 @@ export type ValidSources =
|
|||||||
| "clickup"
|
| "clickup"
|
||||||
| "axero"
|
| "axero"
|
||||||
| "wikipedia"
|
| "wikipedia"
|
||||||
| "mediawiki";
|
| "mediawiki"
|
||||||
|
| "s3"
|
||||||
|
| "r2"
|
||||||
|
| "google_cloud_storage"
|
||||||
|
| "oci_storage";
|
||||||
|
|
||||||
export type ValidInputTypes = "load_state" | "poll" | "event";
|
export type ValidInputTypes = "load_state" | "poll" | "event";
|
||||||
export type ValidStatuses =
|
export type ValidStatuses =
|
||||||
@ -219,6 +223,30 @@ export interface ZendeskConfig {}
|
|||||||
|
|
||||||
export interface DropboxConfig {}
|
export interface DropboxConfig {}
|
||||||
|
|
||||||
|
export interface S3Config {
|
||||||
|
bucket_type: "s3";
|
||||||
|
bucket_name: string;
|
||||||
|
prefix: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface R2Config {
|
||||||
|
bucket_type: "r2";
|
||||||
|
bucket_name: string;
|
||||||
|
prefix: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GCSConfig {
|
||||||
|
bucket_type: "google_cloud_storage";
|
||||||
|
bucket_name: string;
|
||||||
|
prefix: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OCIConfig {
|
||||||
|
bucket_type: "oci_storage";
|
||||||
|
bucket_name: string;
|
||||||
|
prefix: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface MediaWikiBaseConfig {
|
export interface MediaWikiBaseConfig {
|
||||||
connector_name: string;
|
connector_name: string;
|
||||||
language_code: string;
|
language_code: string;
|
||||||
@ -400,6 +428,28 @@ export interface DropboxCredentialJson {
|
|||||||
dropbox_access_token: string;
|
dropbox_access_token: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface R2CredentialJson {
|
||||||
|
account_id: string;
|
||||||
|
r2_access_key_id: string;
|
||||||
|
r2_secret_access_key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface S3CredentialJson {
|
||||||
|
aws_access_key_id: string;
|
||||||
|
aws_secret_access_key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GCSCredentialJson {
|
||||||
|
access_key_id: string;
|
||||||
|
secret_access_key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OCICredentialJson {
|
||||||
|
namespace: string;
|
||||||
|
region: string;
|
||||||
|
access_key_id: string;
|
||||||
|
secret_access_key: string;
|
||||||
|
}
|
||||||
export interface SalesforceCredentialJson {
|
export interface SalesforceCredentialJson {
|
||||||
sf_username: string;
|
sf_username: string;
|
||||||
sf_password: string;
|
sf_password: string;
|
||||||
|
Reference in New Issue
Block a user