From 783696a6714af909f1fe830bc09633ad6baa7d75 Mon Sep 17 00:00:00 2001
From: Yuhong Sun
Date: Sun, 31 Mar 2024 14:45:20 -0700
Subject: [PATCH] Axero Spaces (#1276)
---
...er-build-push-backend-container-on-tag.yml | 1 +
backend/danswer/connectors/axero/connector.py | 61 +++--
web/src/app/admin/connectors/axero/page.tsx | 241 +++++++++---------
web/src/lib/types.ts | 5 +
4 files changed, 172 insertions(+), 136 deletions(-)
diff --git a/.github/workflows/docker-build-push-backend-container-on-tag.yml b/.github/workflows/docker-build-push-backend-container-on-tag.yml
index e95c143fb49..82aa24e6ab5 100644
--- a/.github/workflows/docker-build-push-backend-container-on-tag.yml
+++ b/.github/workflows/docker-build-push-backend-container-on-tag.yml
@@ -38,5 +38,6 @@ jobs:
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
+ # To run locally: trivy image --severity HIGH,CRITICAL danswer/danswer-backend
image-ref: docker.io/danswer/danswer-backend:${{ github.ref_name }}
severity: 'CRITICAL,HIGH'
diff --git a/backend/danswer/connectors/axero/connector.py b/backend/danswer/connectors/axero/connector.py
index e19fb39eb9c..fcb43955892 100644
--- a/backend/danswer/connectors/axero/connector.py
+++ b/backend/danswer/connectors/axero/connector.py
@@ -38,6 +38,7 @@ def _get_entities(
axero_base_url: str,
start: datetime,
end: datetime,
+ space_id: str | None = None,
) -> list[dict]:
endpoint = axero_base_url + "api/content/list"
page_num = 1
@@ -51,6 +52,10 @@ def _get_entities(
"SortOrder": "1", # descending
"StartPage": str(page_num),
}
+
+ if space_id is not None:
+ params["SpaceID"] = space_id
+
res = requests.get(endpoint, headers=_get_auth_header(api_key), params=params)
res.raise_for_status()
@@ -116,7 +121,8 @@ def _translate_content_to_doc(content: dict) -> Document:
class AxeroConnector(PollConnector):
def __init__(
self,
- base_url: str,
+ # Strings of the integer ids of the spaces
+ spaces: list[str] | None = None,
include_article: bool = True,
include_blog: bool = True,
include_wiki: bool = True,
@@ -129,20 +135,24 @@ class AxeroConnector(PollConnector):
self.include_wiki = include_wiki
self.include_forum = include_forum
self.batch_size = batch_size
+ self.space_ids = spaces
self.axero_key = None
-
- if not base_url.endswith("/"):
- base_url += "/"
- self.base_url = base_url
+ self.base_url = None
def load_credentials(self, credentials: dict[str, Any]) -> dict[str, Any] | None:
self.axero_key = credentials["axero_api_token"]
+ # As the API key specifically applies to a particular deployment, this is
+ # included as part of the credential
+ base_url = credentials["base_url"]
+ if not base_url.endswith("/"):
+ base_url += "/"
+ self.base_url = base_url
return None
def poll_source(
self, start: SecondsSinceUnixEpoch, end: SecondsSinceUnixEpoch
) -> GenerateDocumentsOutput:
- if not self.axero_key:
+ if not self.axero_key or not self.base_url:
raise ConnectorMissingCredentialError("Axero")
start_datetime = datetime.utcfromtimestamp(start).replace(tzinfo=timezone.utc)
@@ -158,26 +168,35 @@ class AxeroConnector(PollConnector):
if self.include_forum:
raise NotImplementedError("Forums for Axero not supported currently")
- for entity in entity_types:
- articles = _get_entities(
- entity_type=entity,
- api_key=self.axero_key,
- axero_base_url=self.base_url,
- start=start_datetime,
- end=end_datetime,
- )
- yield from process_in_batches(
- objects=articles,
- process_function=_translate_content_to_doc,
- batch_size=self.batch_size,
- )
+ iterable_space_ids = self.space_ids if self.space_ids else [None]
+
+ for space_id in iterable_space_ids:
+ for entity in entity_types:
+ axero_obj = _get_entities(
+ entity_type=entity,
+ api_key=self.axero_key,
+ axero_base_url=self.base_url,
+ start=start_datetime,
+ end=end_datetime,
+ space_id=space_id,
+ )
+ yield from process_in_batches(
+ objects=axero_obj,
+ process_function=_translate_content_to_doc,
+ batch_size=self.batch_size,
+ )
if __name__ == "__main__":
import os
- connector = AxeroConnector(base_url=os.environ["AXERO_BASE_URL"])
- connector.load_credentials({"axero_api_token": os.environ["AXERO_API_TOKEN"]})
+ connector = AxeroConnector()
+ connector.load_credentials(
+ {
+ "axero_api_token": os.environ["AXERO_API_TOKEN"],
+ "base_url": os.environ["AXERO_BASE_URL"],
+ }
+ )
current = time.time()
one_year_ago = current - 24 * 60 * 60 * 360
diff --git a/web/src/app/admin/connectors/axero/page.tsx b/web/src/app/admin/connectors/axero/page.tsx
index 532345d8320..b434f8528d0 100644
--- a/web/src/app/admin/connectors/axero/page.tsx
+++ b/web/src/app/admin/connectors/axero/page.tsx
@@ -1,30 +1,32 @@
"use client";
import * as Yup from "yup";
-import { AxeroIcon, LinearIcon, TrashIcon } from "@/components/icons/icons";
-import { TextFormField } from "@/components/admin/connectors/Field";
+import { AxeroIcon, TrashIcon } from "@/components/icons/icons";
+import { fetcher } from "@/lib/fetcher";
+import useSWR, { useSWRConfig } from "swr";
+import { LoadingAnimation } from "@/components/Loading";
import { HealthCheckBanner } from "@/components/health/healthcheck";
+import {
+ AxeroConfig,
+ AxeroCredentialJson,
+ ConnectorIndexingStatus,
+ Credential,
+} from "@/lib/types";
+import { adminDeleteCredential, linkCredential } from "@/lib/credential";
import { CredentialForm } from "@/components/admin/connectors/CredentialForm";
import {
- Credential,
- ConnectorIndexingStatus,
- LinearCredentialJson,
- AxeroCredentialJson,
-} from "@/lib/types";
-import useSWR, { useSWRConfig } from "swr";
-import { fetcher } from "@/lib/fetcher";
-import { LoadingAnimation } from "@/components/Loading";
-import { adminDeleteCredential, linkCredential } from "@/lib/credential";
-import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
+ TextFormField,
+ TextArrayFieldBuilder,
+ BooleanFormField,
+ TextArrayField,
+} from "@/components/admin/connectors/Field";
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
-import { usePopup } from "@/components/admin/connectors/Popup";
+import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
import { usePublicCredentials } from "@/lib/hooks";
-import { Card, Text, Title } from "@tremor/react";
+import { Button, Card, Divider, Text, Title } from "@tremor/react";
import { AdminPageTitle } from "@/components/admin/Title";
-const Main = () => {
- const { popup, setPopup } = usePopup();
-
+const MainSection = () => {
const { mutate } = useSWRConfig();
const {
data: connectorIndexingStatuses,
@@ -32,21 +34,19 @@ const Main = () => {
error: isConnectorIndexingStatusesError,
} = useSWR[]>(
"/api/manage/admin/connector/indexing-status",
- fetcher,
- { refreshInterval: 5000 } // 5 seconds
+ fetcher
);
+
const {
data: credentialsData,
isLoading: isCredentialsLoading,
error: isCredentialsError,
- isValidating: isCredentialsValidating,
refreshCredentials,
} = usePublicCredentials();
if (
- isConnectorIndexingStatusesLoading ||
- isCredentialsLoading ||
- isCredentialsValidating
+ (!connectorIndexingStatuses && isConnectorIndexingStatusesLoading) ||
+ (!credentialsData && isCredentialsLoading)
) {
return ;
}
@@ -60,7 +60,7 @@ const Main = () => {
}
const axeroConnectorIndexingStatuses: ConnectorIndexingStatus<
- {},
+ AxeroConfig,
AxeroCredentialJson
>[] = connectorIndexingStatuses.filter(
(connectorIndexingStatus) =>
@@ -73,40 +73,32 @@ const Main = () => {
return (
<>
- {popup}
- Step 1: Provide your Credentials
+ Step 1: Provide Axero API Key
-
{axeroCredential ? (
<>
- Existing API Key:
-
- {axeroCredential.credential_json?.axero_api_token}
+ Existing Axero API Key:
+
+ {axeroCredential.credential_json.axero_api_token}
-
+
>
) : (
<>
-
+
To use the Axero connector, first follow the guide{" "}
{
here
{" "}
to generate an API Key.
-
-
+
+
formBody={
<>
+
{
>
}
validationSchema={Yup.object().shape({
+ base_url: Yup.string().required(
+ "Please enter the base URL of your Axero instance"
+ ),
axero_api_token: Yup.string().required(
- "Please enter your Axero API Key!"
+ "Please enter your Axero API Token"
),
})}
initialValues={{
+ base_url: "",
axero_api_token: "",
}}
onSubmit={(isSuccess) => {
@@ -147,80 +144,94 @@ const Main = () => {
)}
- Step 2: Start indexing
+ Step 2: Which spaces do you want to connect?
- {axeroCredential ? (
+
+ {axeroConnectorIndexingStatuses.length > 0 && (
<>
- {axeroConnectorIndexingStatuses.length > 0 ? (
- <>
-
- We pull the latest Articles, Blogs, and{" "}
- Wikis every 10 minutes.
-
-
-
- connectorIndexingStatuses={axeroConnectorIndexingStatuses}
- liveCredential={axeroCredential}
- getCredential={(credential) => {
- return (
-
-
{credential.credential_json.axero_api_token}
-
- );
- }}
- onCredentialLink={async (connectorId) => {
- if (axeroCredential) {
- await linkCredential(connectorId, axeroCredential.id);
- mutate("/api/manage/admin/connector/indexing-status");
- }
- }}
- onUpdate={() =>
- mutate("/api/manage/admin/connector/indexing-status")
- }
- />
-
- >
- ) : (
-
- Create Connector
-
- Press connect below to start the connection Axero. We pull the
- latest Articles, Blogs, and Wikis every{" "}
- 10 minutes.
-
-
- nameBuilder={() => "AxeroConnector"}
- ccPairNameBuilder={() => "Axero"}
- source="axero"
- inputType="poll"
- formBody={
- <>
-
- >
- }
- validationSchema={Yup.object().shape({})}
- initialValues={{
- base_url: "",
- }}
- refreshFreq={10 * 60} // 10 minutes
- credentialId={axeroCredential.id}
- />
-
- )}
- >
- ) : (
- <>
-
- Please provide your access token in Step 1 first! Once done with
- that, you can then start indexing Linear.
+
+ We pull the latest Articles, Blogs, and Wikis{" "}
+ every 10 minutes.
+
+
+ connectorIndexingStatuses={axeroConnectorIndexingStatuses}
+ liveCredential={axeroCredential}
+ getCredential={(credential) =>
+ credential.credential_json.axero_api_token
+ }
+ specialColumns={[
+ {
+ header: "Space",
+ key: "spaces",
+ getValue: (ccPairStatus) => {
+ const connectorConfig =
+ ccPairStatus.connector.connector_specific_config;
+ return connectorConfig.spaces &&
+ connectorConfig.spaces.length > 0
+ ? connectorConfig.spaces.join(", ")
+ : "";
+ },
+ },
+ ]}
+ onUpdate={() =>
+ mutate("/api/manage/admin/connector/indexing-status")
+ }
+ onCredentialLink={async (connectorId) => {
+ if (axeroCredential) {
+ await linkCredential(connectorId, axeroCredential.id);
+ mutate("/api/manage/admin/connector/indexing-status");
+ }
+ }}
+ />
+
+
>
)}
+
+ {axeroCredential ? (
+
+ Configure an Axero Connector
+
+ nameBuilder={(values) =>
+ values.spaces
+ ? `AxeroConnector-${values.spaces.join("_")}`
+ : `AxeroConnector`
+ }
+ source="axero"
+ inputType="poll"
+ formBodyBuilder={(values) => {
+ return (
+ <>
+
+ {TextArrayFieldBuilder({
+ name: "spaces",
+ label: "Space IDs:",
+ subtext: `
+ Specify zero or more Spaces to index (by the Space IDs). If no Space IDs
+ are specified, all Spaces will be indexed.`,
+ })(values)}
+ >
+ );
+ }}
+ validationSchema={Yup.object().shape({
+ spaces: Yup.array()
+ .of(Yup.string().required("Space Ids cannot be empty"))
+ .required(),
+ })}
+ initialValues={{
+ spaces: [],
+ }}
+ refreshFreq={10 * 60} // 10 minutes
+ credentialId={axeroCredential.id}
+ />
+
+ ) : (
+
+ Please provide your Axero API Token in Step 1 first! Once done with
+ that, you can then specify which spaces you want to connect.
+
+ )}
>
);
};
@@ -234,7 +245,7 @@ export default function Page() {
} title="Axero" />
-
+
);
}
diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts
index f06e4515a29..d09ad6c9063 100644
--- a/web/src/lib/types.ts
+++ b/web/src/lib/types.ts
@@ -113,6 +113,10 @@ export interface SharepointConfig {
sites?: string[];
}
+export interface AxeroConfig {
+ spaces?: string[];
+}
+
export interface ProductboardConfig {}
export interface SlackConfig {
@@ -329,6 +333,7 @@ export interface SharepointCredentialJson {
}
export interface AxeroCredentialJson {
+ base_url: string;
axero_api_token: string;
}