From d03ac44744042ee92ac90e4d7952112111579e6f Mon Sep 17 00:00:00 2001 From: Yuhong Sun Date: Fri, 28 Jul 2023 14:27:02 -0700 Subject: [PATCH] Guru Connector (#177) Co-authored-by: Weves --- backend/danswer/configs/constants.py | 1 + .../danswer/connectors/bookstack/connector.py | 16 +- .../connectors/confluence/connector.py | 25 +- .../connectors/danswer_jira/connector.py | 9 +- backend/danswer/connectors/factory.py | 2 + .../danswer/connectors/github/connector.py | 5 +- backend/danswer/connectors/guru/__init__.py | 0 backend/danswer/connectors/guru/connector.py | 110 +++++++++ backend/danswer/connectors/models.py | 8 + backend/danswer/connectors/slab/connector.py | 8 +- backend/danswer/connectors/slack/connector.py | 5 +- backend/danswer/utils/text_processing.py | 9 + web/public/Guru.svg | 1 + web/src/app/admin/connectors/guru/page.tsx | 233 ++++++++++++++++++ web/src/app/admin/layout.tsx | 10 + web/src/components/icons/icons.tsx | 13 + web/src/components/search/Filters.tsx | 1 + web/src/components/source.tsx | 7 + web/src/lib/types.ts | 11 +- 19 files changed, 427 insertions(+), 47 deletions(-) create mode 100644 backend/danswer/connectors/guru/__init__.py create mode 100644 backend/danswer/connectors/guru/connector.py create mode 100644 web/public/Guru.svg create mode 100644 web/src/app/admin/connectors/guru/page.tsx diff --git a/backend/danswer/configs/constants.py b/backend/danswer/configs/constants.py index ec3a06ed5640..5074efe81ef6 100644 --- a/backend/danswer/configs/constants.py +++ b/backend/danswer/configs/constants.py @@ -22,6 +22,7 @@ class DocumentSource(str, Enum): WEB = "web" GOOGLE_DRIVE = "google_drive" GITHUB = "github" + GURU = "guru" BOOKSTACK = "bookstack" CONFLUENCE = "confluence" SLAB = "slab" diff --git a/backend/danswer/connectors/bookstack/connector.py b/backend/danswer/connectors/bookstack/connector.py index 538dd9689ac4..1571a66c4f2b 100644 --- a/backend/danswer/connectors/bookstack/connector.py +++ b/backend/danswer/connectors/bookstack/connector.py @@ -4,22 +4,17 @@ from collections.abc import Callable from datetime import datetime from typing import Any -from bs4 import BeautifulSoup from danswer.configs.app_configs import INDEX_BATCH_SIZE from danswer.configs.constants import DocumentSource -from danswer.configs.constants import HTML_SEPARATOR from danswer.connectors.bookstack.client import BookStackApiClient 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 - - -class BookstackClientNotSetUpError(PermissionError): - def __init__(self) -> None: - super().__init__("BookStack Client is not set up, was load_credentials called?") +from danswer.utils.text_processing import parse_html_page_basic class BookstackConnector(LoadConnector, PollConnector): @@ -135,8 +130,7 @@ class BookstackConnector(LoadConnector, PollConnector): page_html = ( "

" + html.escape(page_name) + "

" + str(page_data.get("html")) ) - soup = BeautifulSoup(page_html, "html.parser") - text = soup.get_text(HTML_SEPARATOR) + text = parse_html_page_basic(page_html) time.sleep(0.1) return Document( id="page:" + page_id, @@ -148,7 +142,7 @@ class BookstackConnector(LoadConnector, PollConnector): def load_from_state(self) -> GenerateDocumentsOutput: if self.bookstack_client is None: - raise BookstackClientNotSetUpError() + raise ConnectorMissingCredentialError("Bookstack") return self.poll_source(None, None) @@ -156,7 +150,7 @@ class BookstackConnector(LoadConnector, PollConnector): self, start: SecondsSinceUnixEpoch | None, end: SecondsSinceUnixEpoch | None ) -> GenerateDocumentsOutput: if self.bookstack_client is None: - raise BookstackClientNotSetUpError() + raise ConnectorMissingCredentialError("Bookstack") transform_by_endpoint: dict[ str, Callable[[BookStackApiClient, dict], Document] diff --git a/backend/danswer/connectors/confluence/connector.py b/backend/danswer/connectors/confluence/connector.py index 6b2edee8c1cc..0cc7eb117d83 100644 --- a/backend/danswer/connectors/confluence/connector.py +++ b/backend/danswer/connectors/confluence/connector.py @@ -6,16 +6,16 @@ from typing import Any from urllib.parse import urlparse from atlassian import Confluence # type:ignore -from bs4 import BeautifulSoup from danswer.configs.app_configs import INDEX_BATCH_SIZE from danswer.configs.constants import DocumentSource -from danswer.configs.constants import HTML_SEPARATOR 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.utils.text_processing import parse_html_page_basic # Potential Improvements # 1. If wiki page instead of space, do a search of all the children of the page instead of index all in the space @@ -23,13 +23,6 @@ from danswer.connectors.models import Section # 3. Segment into Sections for more accurate linking, can split by headers but make sure no text/ordering is lost -class ConfluenceClientNotSetUpError(PermissionError): - def __init__(self) -> None: - super().__init__( - "Confluence Client is not set up, was load_credentials called?" - ) - - def extract_confluence_keys_from_url(wiki_url: str) -> tuple[str, str]: """Sample https://danswer.atlassian.net/wiki/spaces/1234abcd/overview @@ -59,8 +52,7 @@ def _comment_dfs( ) -> str: for comment_page in comment_pages: comment_html = comment_page["body"]["storage"]["value"] - soup = BeautifulSoup(comment_html, "html.parser") - comments_str += "\nComment:\n" + soup.get_text(HTML_SEPARATOR) + comments_str += "\nComment:\n" + parse_html_page_basic(comment_html) child_comment_pages = confluence_client.get_page_child_by_type( comment_page["id"], type="comment", @@ -101,7 +93,7 @@ class ConfluenceConnector(LoadConnector, PollConnector): doc_batch: list[Document] = [] if self.confluence_client is None: - raise ConfluenceClientNotSetUpError() + raise ConnectorMissingCredentialError("Confluence") batch = self.confluence_client.get_all_pages_from_space( self.space, @@ -116,8 +108,9 @@ class ConfluenceConnector(LoadConnector, PollConnector): if time_filter is None or time_filter(last_modified): page_html = page["body"]["storage"]["value"] - soup = BeautifulSoup(page_html, "html.parser") - page_text = page.get("title", "") + "\n" + soup.get_text(HTML_SEPARATOR) + page_text = ( + page.get("title", "") + "\n" + parse_html_page_basic(page_html) + ) comment_pages = self.confluence_client.get_page_child_by_type( page["id"], type="comment", @@ -146,7 +139,7 @@ class ConfluenceConnector(LoadConnector, PollConnector): def load_from_state(self) -> GenerateDocumentsOutput: if self.confluence_client is None: - raise ConfluenceClientNotSetUpError() + raise ConnectorMissingCredentialError("Confluence") start_ind = 0 while True: @@ -162,7 +155,7 @@ class ConfluenceConnector(LoadConnector, PollConnector): self, start: SecondsSinceUnixEpoch, end: SecondsSinceUnixEpoch ) -> GenerateDocumentsOutput: if self.confluence_client is None: - raise ConfluenceClientNotSetUpError() + raise ConnectorMissingCredentialError("Confluence") start_time = datetime.fromtimestamp(start, tz=timezone.utc) end_time = datetime.fromtimestamp(end, tz=timezone.utc) diff --git a/backend/danswer/connectors/danswer_jira/connector.py b/backend/danswer/connectors/danswer_jira/connector.py index 2518d5e3f3d7..e5c3609c3476 100644 --- a/backend/danswer/connectors/danswer_jira/connector.py +++ b/backend/danswer/connectors/danswer_jira/connector.py @@ -9,6 +9,7 @@ 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.utils.logger import setup_logger @@ -97,9 +98,7 @@ class JiraConnector(LoadConnector, PollConnector): def load_from_state(self) -> GenerateDocumentsOutput: if self.jira_client is None: - raise PermissionError( - "Jira Client is not set up, was load_credentials called?" - ) + raise ConnectorMissingCredentialError("Jira") start_ind = 0 while True: @@ -121,9 +120,7 @@ class JiraConnector(LoadConnector, PollConnector): self, start: SecondsSinceUnixEpoch, end: SecondsSinceUnixEpoch ) -> GenerateDocumentsOutput: if self.jira_client is None: - raise PermissionError( - "Jira Client is not set up, was load_credentials called?" - ) + raise ConnectorMissingCredentialError("Jira") start_date_str = datetime.fromtimestamp(start, tz=timezone.utc).strftime( "%Y-%m-%d %H:%M" diff --git a/backend/danswer/connectors/factory.py b/backend/danswer/connectors/factory.py index d6dcc70b3cee..01bd1ca71dbb 100644 --- a/backend/danswer/connectors/factory.py +++ b/backend/danswer/connectors/factory.py @@ -9,6 +9,7 @@ from danswer.connectors.file.connector import LocalFileConnector from danswer.connectors.github.connector import GithubConnector from danswer.connectors.google_drive.connector import GoogleDriveConnector from danswer.connectors.notion.connector import NotionConnector +from danswer.connectors.guru.connector import GuruConnector from danswer.connectors.interfaces import BaseConnector from danswer.connectors.interfaces import EventConnector from danswer.connectors.interfaces import LoadConnector @@ -46,6 +47,7 @@ def identify_connector_class( DocumentSource.PRODUCTBOARD: ProductboardConnector, DocumentSource.SLAB: SlabConnector, DocumentSource.NOTION: NotionConnector, + DocumentSource.GURU: GuruConnector, } connector_by_source = connector_map.get(source, {}) diff --git a/backend/danswer/connectors/github/connector.py b/backend/danswer/connectors/github/connector.py index a85476c02451..109995465a4c 100644 --- a/backend/danswer/connectors/github/connector.py +++ b/backend/danswer/connectors/github/connector.py @@ -6,6 +6,7 @@ from danswer.configs.app_configs import INDEX_BATCH_SIZE from danswer.configs.constants import DocumentSource from danswer.connectors.interfaces import GenerateDocumentsOutput from danswer.connectors.interfaces import LoadConnector +from danswer.connectors.models import ConnectorMissingCredentialError from danswer.connectors.models import Document from danswer.connectors.models import Section from danswer.utils.logger import setup_logger @@ -48,9 +49,7 @@ class GithubConnector(LoadConnector): def load_from_state(self) -> GenerateDocumentsOutput: if self.github_client is None: - raise PermissionError( - "Github Client is not set up, was load_credentials called?" - ) + raise ConnectorMissingCredentialError("GitHub") repo = self.github_client.get_repo(f"{self.repo_owner}/{self.repo_name}") pull_requests = repo.get_pulls(state=self.state_filter) for pr_batch in get_pr_batches(pull_requests, self.batch_size): diff --git a/backend/danswer/connectors/guru/__init__.py b/backend/danswer/connectors/guru/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/backend/danswer/connectors/guru/connector.py b/backend/danswer/connectors/guru/connector.py new file mode 100644 index 000000000000..4f648ebff67c --- /dev/null +++ b/backend/danswer/connectors/guru/connector.py @@ -0,0 +1,110 @@ +import json +from datetime import datetime +from datetime import timezone +from typing import Any + +import requests +from danswer.configs.app_configs import INDEX_BATCH_SIZE +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.utils.logger import setup_logger +from danswer.utils.text_processing import parse_html_page_basic + +# Potential Improvements +# 1. Support fetching per collection via collection token (configured at connector creation) + +GURU_API_BASE = "https://api.getguru.com/api/v1/" +GURU_QUERY_ENDPOINT = GURU_API_BASE + "search/query" +GURU_CARDS_URL = "https://app.getguru.com/card/" +logger = setup_logger() + + +def unixtime_to_guru_time_str(unix_time: SecondsSinceUnixEpoch) -> str: + date_obj = datetime.fromtimestamp(unix_time, tz=timezone.utc) + date_str = date_obj.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + tz_str = date_obj.strftime("%z") + return date_str + tz_str + + +class GuruConnector(LoadConnector, PollConnector): + def __init__( + self, + batch_size: int = INDEX_BATCH_SIZE, + guru_user: str | None = None, + guru_user_token: str | None = None, + ) -> None: + self.batch_size = batch_size + self.guru_user = guru_user + self.guru_user_token = guru_user_token + + def load_credentials(self, credentials: dict[str, Any]) -> dict[str, Any] | None: + self.guru_user = credentials["guru_user"] + self.guru_user_token = credentials["guru_user_token"] + return None + + def _process_cards( + self, start_str: str | None = None, end_str: str | None = None + ) -> GenerateDocumentsOutput: + if self.guru_user is None or self.guru_user_token is None: + raise ConnectorMissingCredentialError("Guru") + + doc_batch: list[Document] = [] + + session = requests.Session() + session.auth = (self.guru_user, self.guru_user_token) + + params: dict[str, str | int] = {"maxResults": self.batch_size} + + if start_str is not None and end_str is not None: + params["q"] = f"lastModified >= {start_str} AND lastModified < {end_str}" + + current_url = GURU_QUERY_ENDPOINT # This is how they handle pagination, a different url will be provided + while True: + response = session.get(current_url, params=params) + + if response.status_code == 204: + break + + cards = json.loads(response.text) + for card in cards: + title = card["preferredPhrase"] + link = GURU_CARDS_URL + card["slug"] + content_text = title + "\n" + parse_html_page_basic(card["content"]) + + doc_batch.append( + Document( + id=card["id"], + sections=[Section(link=link, text=content_text)], + source=DocumentSource.GURU, + semantic_identifier=title, + metadata={}, + ) + ) + + if len(doc_batch) >= self.batch_size: + yield doc_batch + doc_batch = [] + + if not hasattr(response, "links") or not response.links: + break + current_url = response.links["next-page"]["url"] + + if doc_batch: + yield doc_batch + + def load_from_state(self) -> GenerateDocumentsOutput: + return self._process_cards() + + def poll_source( + self, start: SecondsSinceUnixEpoch, end: SecondsSinceUnixEpoch + ) -> GenerateDocumentsOutput: + start_time = unixtime_to_guru_time_str(start) + end_time = unixtime_to_guru_time_str(end) + + return self._process_cards(start_time, end_time) diff --git a/backend/danswer/connectors/models.py b/backend/danswer/connectors/models.py index 9abaa934722a..bc657e95bdea 100644 --- a/backend/danswer/connectors/models.py +++ b/backend/danswer/connectors/models.py @@ -6,6 +6,14 @@ from danswer.configs.constants import DocumentSource from pydantic import BaseModel +class ConnectorMissingCredentialError(PermissionError): + def __init__(self, connector_name: str) -> None: + connector_name = connector_name or "Unknown" + super().__init__( + f"{connector_name} connector missing credentials, was load_credentials called?" + ) + + @dataclass class Section: link: str diff --git a/backend/danswer/connectors/slab/connector.py b/backend/danswer/connectors/slab/connector.py index 7dc1e76d41d8..2b491eed2b60 100644 --- a/backend/danswer/connectors/slab/connector.py +++ b/backend/danswer/connectors/slab/connector.py @@ -13,6 +13,7 @@ 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.utils.logger import setup_logger @@ -24,11 +25,6 @@ SLAB_API_URL = "https://api.slab.com/v1/graphql" logger = setup_logger() -class SlabBotTokenNotFoundError(PermissionError): - def __init__(self) -> None: - super().__init__("Slab Bot Token not found, was load_credentials called?") - - def run_graphql_request( graphql_query: dict, bot_token: str, max_tries: int = SLAB_GRAPHQL_MAX_TRIES ) -> str: @@ -179,7 +175,7 @@ class SlabConnector(LoadConnector, PollConnector): doc_batch: list[Document] = [] if self.slab_bot_token is None: - raise SlabBotTokenNotFoundError() + raise ConnectorMissingCredentialError("Slab") all_post_ids: list[str] = get_all_post_ids(self.slab_bot_token) diff --git a/backend/danswer/connectors/slack/connector.py b/backend/danswer/connectors/slack/connector.py index 88b944456c45..fb99372ba052 100644 --- a/backend/danswer/connectors/slack/connector.py +++ b/backend/danswer/connectors/slack/connector.py @@ -12,6 +12,7 @@ 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.connectors.slack.utils import get_message_link @@ -285,9 +286,7 @@ class SlackPollConnector(PollConnector): self, start: SecondsSinceUnixEpoch, end: SecondsSinceUnixEpoch ) -> GenerateDocumentsOutput: if self.client is None: - raise PermissionError( - "Slack Client is not set up, was load_credentials called?" - ) + raise ConnectorMissingCredentialError("Slack") documents: list[Document] = [] for document in get_all_docs( diff --git a/backend/danswer/utils/text_processing.py b/backend/danswer/utils/text_processing.py index 28da1bb99b45..7d79f9bf67ff 100644 --- a/backend/danswer/utils/text_processing.py +++ b/backend/danswer/utils/text_processing.py @@ -1,3 +1,7 @@ +from bs4 import BeautifulSoup +from danswer.configs.constants import HTML_SEPARATOR + + def clean_model_quote(quote: str, trim_length: int) -> str: quote_clean = quote.strip() if quote_clean[0] == '"': @@ -29,3 +33,8 @@ def shared_precompare_cleanup(text: str) -> str: text = text.replace("-", "") return text + + +def parse_html_page_basic(text: str) -> str: + soup = BeautifulSoup(text, "html.parser") + return soup.get_text(HTML_SEPARATOR) diff --git a/web/public/Guru.svg b/web/public/Guru.svg new file mode 100644 index 000000000000..b72aeb5b6c9d --- /dev/null +++ b/web/public/Guru.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/app/admin/connectors/guru/page.tsx b/web/src/app/admin/connectors/guru/page.tsx new file mode 100644 index 000000000000..a4cfe4c4850e --- /dev/null +++ b/web/src/app/admin/connectors/guru/page.tsx @@ -0,0 +1,233 @@ +"use client"; + +import * as Yup from "yup"; +import { GuruIcon, TrashIcon } from "@/components/icons/icons"; +import { TextFormField } from "@/components/admin/connectors/Field"; +import { HealthCheckBanner } from "@/components/health/healthcheck"; +import { CredentialForm } from "@/components/admin/connectors/CredentialForm"; +import { + Credential, + ProductboardConfig, + ConnectorIndexingStatus, + GuruConfig, + GuruCredentialJson, +} from "@/lib/types"; +import useSWR, { useSWRConfig } from "swr"; +import { fetcher } from "@/lib/fetcher"; +import { LoadingAnimation } from "@/components/Loading"; +import { deleteCredential, linkCredential } from "@/lib/credential"; +import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm"; +import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable"; +import { usePopup } from "@/components/admin/connectors/Popup"; + +const Main = () => { + const { popup, setPopup } = usePopup(); + + const { mutate } = useSWRConfig(); + const { + data: connectorIndexingStatuses, + isLoading: isConnectorIndexingStatusesLoading, + error: isConnectorIndexingStatusesError, + } = useSWR[]>( + "/api/manage/admin/connector/indexing-status", + fetcher + ); + const { + data: credentialsData, + isLoading: isCredentialsLoading, + isValidating: isCredentialsValidating, + error: isCredentialsError, + } = useSWR[]>("/api/manage/credential", fetcher); + + if ( + isConnectorIndexingStatusesLoading || + isCredentialsLoading || + isCredentialsValidating + ) { + return ; + } + + if (isConnectorIndexingStatusesError || !connectorIndexingStatuses) { + return
Failed to load connectors
; + } + + if (isCredentialsError || !credentialsData) { + return
Failed to load credentials
; + } + + const guruConnectorIndexingStatuses: ConnectorIndexingStatus[] = + connectorIndexingStatuses.filter( + (connectorIndexingStatus) => + connectorIndexingStatus.connector.source === "guru" + ); + const guruCredential: Credential = credentialsData.filter( + (credential) => credential.credential_json?.guru_user + )[0]; + + return ( + <> + {popup} +

+ This connector allows you to sync all your Guru Cards into Danswer. +

+ +

+ Step 1: Provide your Credentials +

+ + {guruCredential ? ( + <> +
+

Existing Access Token:

+

+ {guruCredential.credential_json?.guru_user_token} +

+ +
+ + ) : ( + <> +

+ To use the Guru connector, first follow the guide{" "} + + here + {" "} + to generate a User Token. +

+
+ + formBody={ + <> + + + + } + validationSchema={Yup.object().shape({ + guru_user: Yup.string().required( + "Please enter your Guru username" + ), + guru_user_token: Yup.string().required( + "Please enter your Guru access token" + ), + })} + initialValues={{ + guru_user: "", + guru_user_token: "", + }} + onSubmit={(isSuccess) => { + if (isSuccess) { + mutate("/api/manage/credential"); + } + }} + /> +
+ + )} + +

+ Step 2: Start indexing! +

+ {guruCredential ? ( + !guruConnectorIndexingStatuses.length ? ( + <> +

+ Click the button below to start indexing! We will pull the latest + features, components, and products from Guru every 10{" "} + minutes. +

+
+ + nameBuilder={() => "GuruConnector"} + source="guru" + inputType="poll" + formBody={null} + validationSchema={Yup.object().shape({})} + initialValues={{}} + refreshFreq={10 * 60} // 10 minutes + onSubmit={async (isSuccess, responseJson) => { + if (isSuccess && responseJson) { + await linkCredential(responseJson.id, guruCredential.id); + mutate("/api/manage/admin/connector/indexing-status"); + } + }} + /> +
+ + ) : ( + <> +

+ Guru connector is setup! We are pulling the latest cards from Guru + every 10 minutes. +

+ + connectorIndexingStatuses={guruConnectorIndexingStatuses} + liveCredential={guruCredential} + getCredential={(credential) => { + return ( +
+

{credential.credential_json.guru_user}

+
+ ); + }} + onCredentialLink={async (connectorId) => { + if (guruCredential) { + await linkCredential(connectorId, guruCredential.id); + mutate("/api/manage/admin/connector/indexing-status"); + } + }} + onUpdate={() => + mutate("/api/manage/admin/connector/indexing-status") + } + /> + + ) + ) : ( + <> +

+ Please provide your access token in Step 1 first! Once done with + that, you can then start indexing all your Guru cards. +

+ + )} + + ); +}; + +export default function Page() { + return ( +
+
+ +
+
+ +

Guru

+
+
+
+ ); +} diff --git a/web/src/app/admin/layout.tsx b/web/src/app/admin/layout.tsx index b9e3b43a0849..26542d9e678b 100644 --- a/web/src/app/admin/layout.tsx +++ b/web/src/app/admin/layout.tsx @@ -9,6 +9,7 @@ import { KeyIcon, BookstackIcon, ConfluenceIcon, + GuruIcon, FileIcon, JiraIcon, SlabIcon, @@ -131,6 +132,15 @@ export default async function AdminLayout({ ), link: "/admin/connectors/notion", }, + { + name: ( +
+ +
Guru
+
+ ), + link: "/admin/connectors/guru", + }, { name: (
diff --git a/web/src/components/icons/icons.tsx b/web/src/components/icons/icons.tsx index 68a4f66a623d..aa347efebbc7 100644 --- a/web/src/components/icons/icons.tsx +++ b/web/src/components/icons/icons.tsx @@ -18,6 +18,7 @@ import { FaFile, FaGlobe } from "react-icons/fa"; import Image from "next/image"; import jiraSVG from "../../../public/Jira.svg"; import confluenceSVG from "../../../public/Confluence.svg"; +import guruIcon from "../../../public/Guru.svg"; interface IconProps { size?: number; @@ -237,3 +238,15 @@ export const NotionIcon = ({
); }; + +export const GuruIcon = ({ + size = 16, + className = defaultTailwindCSS, +}: IconProps) => ( +
+ Logo +
+); diff --git a/web/src/components/search/Filters.tsx b/web/src/components/search/Filters.tsx index 49b30c3e60ac..d913649c04b7 100644 --- a/web/src/components/search/Filters.tsx +++ b/web/src/components/search/Filters.tsx @@ -14,6 +14,7 @@ const sources: Source[] = [ { displayName: "Slab", internalName: "slab" }, { displayName: "Github PRs", internalName: "github" }, { displayName: "Web", internalName: "web" }, + { displayName: "Guru", internalName: "guru" }, { displayName: "File", internalName: "file" }, { displayName: "Notion", internalName: "notion" }, ]; diff --git a/web/src/components/source.tsx b/web/src/components/source.tsx index 24854fd72e37..e810b6884f1b 100644 --- a/web/src/components/source.tsx +++ b/web/src/components/source.tsx @@ -6,6 +6,7 @@ import { GithubIcon, GlobeIcon, GoogleDriveIcon, + GuruIcon, JiraIcon, NotionIcon, ProductboardIcon, @@ -87,6 +88,12 @@ export const getSourceMetadata = (sourceType: ValidSources): SourceMetadata => { displayName: "Notion", adminPageLink: "/admin/connectors/notion", }; + case "guru": + return { + icon: GuruIcon, + displayName: "Guru", + adminPageLink: "/admin/connectors/guru", + }; default: throw new Error("Invalid source type"); } diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index 812e4b4044e8..84bccc5674b6 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -17,8 +17,9 @@ export type ValidSources = | "jira" | "productboard" | "slab" - | "file" - | "notion"; + | "notion" + | "guru" + | "file"; export type ValidInputTypes = "load_state" | "poll" | "event"; // CONNECTORS @@ -72,6 +73,8 @@ export interface SlabConfig { base_url: string; } +export interface GuruConfig {} + export interface FileConfig { file_locations: string[]; } @@ -139,3 +142,7 @@ export interface SlabCredentialJson { export interface NotionCredentialJson { notion_integration_token: string; } +export interface GuruCredentialJson { + guru_user: string; + guru_user_token: string; +}