Gong Connector (#529)

---------

Co-authored-by: Weves <chrisweaver101@gmail.com>
This commit is contained in:
Yuhong Sun 2023-10-08 00:27:15 -07:00 committed by GitHub
parent c658ffd0b6
commit 7d3f8b7c8c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 676 additions and 77 deletions

View File

@ -60,6 +60,7 @@ class DocumentSource(str, Enum):
ZULIP = "zulip"
LINEAR = "linear"
HUBSPOT = "hubspot"
GONG = "gong"
class DocumentIndexType(str, Enum):

View File

@ -61,9 +61,9 @@ if __name__ == "__main__":
### Additional Required Changes:
#### Backend Changes
- Add a new type to
[DocumentSource](https://github.com/danswer-ai/danswer/blob/main/backend/danswer/configs/constants.py#L20)
[DocumentSource](https://github.com/danswer-ai/danswer/blob/main/backend/danswer/configs/constants.py)
- Add a mapping from DocumentSource (and optionally connector type) to the right connector class
[here](https://github.com/danswer-ai/danswer/blob/main/backend/danswer/connectors/factory.py#L32)
[here](https://github.com/danswer-ai/danswer/blob/main/backend/danswer/connectors/factory.py#L33)
#### Frontend Changes
- Create the new connector directory and admin page under `danswer/web/src/app/admin/connectors/`

View File

@ -7,6 +7,7 @@ from danswer.connectors.confluence.connector import ConfluenceConnector
from danswer.connectors.danswer_jira.connector import JiraConnector
from danswer.connectors.file.connector import LocalFileConnector
from danswer.connectors.github.connector import GithubConnector
from danswer.connectors.gong.connector import GongConnector
from danswer.connectors.google_drive.connector import GoogleDriveConnector
from danswer.connectors.guru.connector import GuruConnector
from danswer.connectors.hubspot.connector import HubSpotConnector
@ -52,6 +53,7 @@ def identify_connector_class(
DocumentSource.GURU: GuruConnector,
DocumentSource.LINEAR: LinearConnector,
DocumentSource.HUBSPOT: HubSpotConnector,
DocumentSource.GONG: GongConnector,
}
connector_by_source = connector_map.get(source, {})

View File

@ -13,6 +13,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.interfaces import PollConnector
from danswer.connectors.interfaces import SecondsSinceUnixEpoch
from danswer.connectors.models import ConnectorMissingCredentialError
from danswer.connectors.models import Document
@ -68,7 +69,7 @@ def _convert_issue_to_document(issue: Issue) -> Document:
)
class GithubConnector(LoadConnector):
class GithubConnector(LoadConnector, PollConnector):
def __init__(
self,
repo_owner: str,

View File

@ -0,0 +1,275 @@
import base64
from collections.abc import Generator
from datetime import datetime
from datetime import timedelta
from datetime import timezone
from typing import Any
from typing import cast
import requests
from danswer.configs.app_configs import CONTINUE_ON_CONNECTOR_FAILURE
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
logger = setup_logger()
GONG_BASE_URL = "https://us-34014.api.gong.io"
class GongConnector(LoadConnector, PollConnector):
def __init__(
self,
workspaces: list[str] | None = None,
batch_size: int = INDEX_BATCH_SIZE,
use_end_time: bool = False,
continue_on_fail: bool = CONTINUE_ON_CONNECTOR_FAILURE,
hide_user_info: bool = False,
) -> None:
self.workspaces = workspaces
self.batch_size: int = batch_size
self.continue_on_fail = continue_on_fail
self.auth_token_basic: str | None = None
self.use_end_time = use_end_time
self.hide_user_info = hide_user_info
def _get_auth_header(self) -> dict[str, str]:
if self.auth_token_basic is None:
raise ConnectorMissingCredentialError("Gong")
return {"Authorization": f"Basic {self.auth_token_basic}"}
def _get_workspace_id_map(self) -> dict[str, str]:
url = f"{GONG_BASE_URL}/v2/workspaces"
response = requests.get(url, headers=self._get_auth_header())
response.raise_for_status()
workspaces_details = response.json().get("workspaces")
return {workspace["name"]: workspace["id"] for workspace in workspaces_details}
def _get_transcript_batches(
self, start_datetime: str | None = None, end_datetime: str | None = None
) -> Generator[list[dict[str, Any]], None, None]:
url = f"{GONG_BASE_URL}/v2/calls/transcript"
body: dict[str, dict] = {"filter": {}}
if start_datetime:
body["filter"]["fromDateTime"] = start_datetime
if end_datetime:
body["filter"]["toDateTime"] = end_datetime
# The batch_ids in the previous method appears to be batches of call_ids to process
# In this method, we will retrieve transcripts for them in batches.
transcripts: list[dict[str, Any]] = []
workspace_list = self.workspaces or [None] # type: ignore
workspace_map = self._get_workspace_id_map() if self.workspaces else {}
for workspace in workspace_list:
if workspace:
logger.info(f"Updating workspace: {workspace}")
workspace_id = workspace_map.get(workspace)
if not workspace_id:
logger.error(f"Invalid workspace: {workspace}")
if not self.continue_on_fail:
raise ValueError(f"Invalid workspace: {workspace}")
continue
body["filter"]["workspaceId"] = workspace_id
else:
if "workspaceId" in body["filter"]:
del body["filter"]["workspaceId"]
while True:
response = requests.post(
url, headers=self._get_auth_header(), json=body
)
response.raise_for_status()
data = response.json()
call_transcripts = data.get("callTranscripts", [])
transcripts.extend(call_transcripts)
while len(transcripts) >= self.batch_size:
yield transcripts[: self.batch_size]
transcripts = transcripts[self.batch_size :]
cursor = data.get("records", {}).get("cursor")
if cursor:
body["cursor"] = cursor
else:
break
if transcripts:
yield transcripts
def _get_call_details_by_ids(self, call_ids: list[str]) -> dict:
url = f"{GONG_BASE_URL}/v2/calls/extensive"
body = {
"filter": {"callIds": call_ids},
"contentSelector": {"exposedFields": {"parties": True}},
}
response = requests.post(url, headers=self._get_auth_header(), json=body)
response.raise_for_status()
calls = response.json().get("calls")
call_to_metadata = {}
for call in calls:
call_to_metadata[call["metaData"]["id"]] = call
return call_to_metadata
@staticmethod
def _parse_parties(parties: list[dict]) -> dict[str, str]:
id_mapping = {}
for party in parties:
name = party.get("name")
email = party.get("emailAddress")
if name and email:
full_identifier = f"{name} ({email})"
elif name:
full_identifier = name
elif email:
full_identifier = email
else:
full_identifier = "Unknown"
id_mapping[party["speakerId"]] = full_identifier
return id_mapping
def _fetch_calls(
self, start_datetime: str | None = None, end_datetime: str | None = None
) -> GenerateDocumentsOutput:
for transcript_batch in self._get_transcript_batches(
start_datetime, end_datetime
):
doc_batch: list[Document] = []
call_ids = cast(
list[str],
[t.get("callId") for t in transcript_batch if t.get("callId")],
)
call_details_map = self._get_call_details_by_ids(call_ids)
for transcript in transcript_batch:
call_id = transcript.get("callId")
if not call_id or call_id not in call_details_map:
logger.error(
f"Couldn't get call information for Call ID: {call_id}"
)
if not self.continue_on_fail:
raise RuntimeError(
f"Couldn't get call information for Call ID: {call_id}"
)
continue
call_details = call_details_map[call_id]
call_metadata = call_details["metaData"]
call_parties = call_details["parties"]
id_to_name_map = self._parse_parties(call_parties)
# Keeping a separate dict here in case the parties info is incomplete
speaker_to_name: dict[str, str] = {}
transcript_text = ""
call_title = call_metadata["title"]
if call_title:
transcript_text += f"Call Title: {call_title}\n\n"
call_purpose = call_metadata["purpose"]
if call_purpose:
transcript_text += f"Call Description: {call_purpose}\n\n"
contents = transcript["transcript"]
for segment in contents:
speaker_id = segment.get("speakerId", "")
if speaker_id not in speaker_to_name:
if self.hide_user_info:
speaker_to_name[
speaker_id
] = f"User {len(speaker_to_name) + 1}"
else:
speaker_to_name[speaker_id] = id_to_name_map.get(
speaker_id, "Unknown"
)
speaker_name = speaker_to_name[speaker_id]
sentences = segment.get("sentences", {})
monolog = " ".join(
[sentence.get("text", "") for sentence in sentences]
)
transcript_text += f"{speaker_name}: {monolog}\n\n"
doc_batch.append(
Document(
id=call_id,
sections=[
Section(link=call_metadata["url"], text=transcript_text)
],
source=DocumentSource.GONG,
# Should not ever be Untitled as a call cannot be made without a Title
semantic_identifier=call_title or "Untitled",
metadata={"Start Time": call_metadata["started"]},
)
)
yield doc_batch
def load_credentials(self, credentials: dict[str, Any]) -> dict[str, Any] | None:
combined = (
f'{credentials["gong_access_key"]}:{credentials["gong_access_key_secret"]}'
)
self.auth_token_basic = base64.b64encode(combined.encode("utf-8")).decode(
"utf-8"
)
return None
def load_from_state(self) -> GenerateDocumentsOutput:
return self._fetch_calls()
def poll_source(
self, start: SecondsSinceUnixEpoch, end: SecondsSinceUnixEpoch
) -> GenerateDocumentsOutput:
# Because these are meeting start times, the meeting needs to end and be processed
# so adding a 1 day buffer and fetching by default till current time
start_datetime = datetime.fromtimestamp(start, tz=timezone.utc)
start_one_day_offset = start_datetime - timedelta(days=1)
start_time = start_one_day_offset.isoformat()
end_time = (
datetime.fromtimestamp(end, tz=timezone.utc).isoformat()
if self.use_end_time
else None
)
return self._fetch_calls(start_time, end_time)
if __name__ == "__main__":
import os
import time
connector = GongConnector()
connector.load_credentials(
{
"gong_access_key": os.environ["GONG_ACCESS_KEY"],
"gong_access_key_secret": os.environ["GONG_ACCESS_KEY_SECRET"],
}
)
current = time.time()
one_day_ago = current - 24 * 60 * 60 # 1 day
latest_docs = connector.poll_source(one_day_ago, current)
print(next(latest_docs))

BIN
web/public/Gong.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -213,14 +213,18 @@ const Main = () => {
{
header: "Url",
key: "url",
getValue: (connector) => (
getValue: (ccPairStatus) => (
<a
className="text-blue-500"
href={
connector.connector_specific_config.wiki_page_url
ccPairStatus.connector.connector_specific_config
.wiki_page_url
}
>
{connector.connector_specific_config.wiki_page_url}
{
ccPairStatus.connector.connector_specific_config
.wiki_page_url
}
</a>
),
},

View File

@ -216,8 +216,8 @@ const Main = () => {
{
header: "File Names",
key: "file_names",
getValue: (connector) =>
connector.connector_specific_config.file_locations
getValue: (ccPairStatus) =>
ccPairStatus.connector.connector_specific_config.file_locations
.map(getNameFromPath)
.join(", "),
},

View File

@ -156,8 +156,11 @@ const Main = () => {
{
header: "Repository",
key: "repository",
getValue: (connector) =>
`${connector.connector_specific_config.repo_owner}/${connector.connector_specific_config.repo_name}`,
getValue: (ccPairStatus) => {
const connectorConfig =
ccPairStatus.connector.connector_specific_config;
return `${connectorConfig.repo_owner}/${connectorConfig.repo_name}`;
},
},
]}
onUpdate={() =>

View File

@ -0,0 +1,257 @@
"use client";
import * as Yup from "yup";
import { GongIcon, TrashIcon } from "@/components/icons/icons";
import {
TextFormField,
TextArrayFieldBuilder,
} from "@/components/admin/connectors/Field";
import { HealthCheckBanner } from "@/components/health/healthcheck";
import { CredentialForm } from "@/components/admin/connectors/CredentialForm";
import {
Credential,
ConnectorIndexingStatus,
GongConfig,
GongCredentialJson,
} 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";
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
import { usePopup } from "@/components/admin/connectors/Popup";
import { usePublicCredentials } from "@/lib/hooks";
const Main = () => {
const { popup, setPopup } = usePopup();
const { mutate } = useSWRConfig();
const {
data: connectorIndexingStatuses,
isLoading: isConnectorIndexingStatusesLoading,
error: isConnectorIndexingStatusesError,
} = useSWR<ConnectorIndexingStatus<any, any>[]>(
"/api/manage/admin/connector/indexing-status",
fetcher
);
const {
data: credentialsData,
isLoading: isCredentialsLoading,
isValidating: isCredentialsValidating,
error: isCredentialsError,
refreshCredentials,
} = usePublicCredentials();
if (
isConnectorIndexingStatusesLoading ||
isCredentialsLoading ||
isCredentialsValidating
) {
return <LoadingAnimation text="Loading" />;
}
if (isConnectorIndexingStatusesError || !connectorIndexingStatuses) {
return <div>Failed to load connectors</div>;
}
if (isCredentialsError || !credentialsData) {
return <div>Failed to load credentials</div>;
}
const gongConnectorIndexingStatuses: ConnectorIndexingStatus<
GongConfig,
GongCredentialJson
>[] = connectorIndexingStatuses.filter(
(connectorIndexingStatus) =>
connectorIndexingStatus.connector.source === "gong"
);
const gongCredential: Credential<GongCredentialJson> | undefined =
credentialsData.find(
(credential) => credential.credential_json?.gong_access_key
);
return (
<>
{popup}
<p className="text-sm">
This connector allows you to sync all your Gong Transcripts into
Danswer. More details on how to setup the Gong connector can be found in{" "}
<a
className="text-blue-500"
href="https://docs.danswer.dev/connectors/gong"
target="_blank"
>
this guide.
</a>
</p>
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
Step 1: Provide your API Access info
</h2>
{gongCredential ? (
<>
<div className="flex mb-1 text-sm">
<p className="my-auto">Existing Access Key Secret: </p>
<p className="ml-1 italic my-auto max-w-md truncate">
{gongCredential.credential_json?.gong_access_key_secret}
</p>
<button
className="ml-1 hover:bg-gray-700 rounded-full p-1"
onClick={async () => {
if (gongConnectorIndexingStatuses.length > 0) {
setPopup({
type: "error",
message:
"Must delete all connectors before deleting credentials",
});
return;
}
await adminDeleteCredential(gongCredential.id);
refreshCredentials();
}}
>
<TrashIcon />
</button>
</div>
</>
) : (
<>
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2">
<CredentialForm<GongCredentialJson>
formBody={
<>
<TextFormField name="gong_access_key" label="Access Key:" />
<TextFormField
name="gong_access_key_secret"
label="Access Key Secret:"
type="password"
/>
</>
}
validationSchema={Yup.object().shape({
gong_access_key: Yup.string().required(
"Please enter your Gong Access Key"
),
gong_access_key_secret: Yup.string().required(
"Please enter your Gong Access Key Secret"
),
})}
initialValues={{
gong_access_key: "",
gong_access_key_secret: "",
}}
onSubmit={(isSuccess) => {
if (isSuccess) {
refreshCredentials();
}
}}
/>
</div>
</>
)}
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
Step 2: Which Workspaces do you want to make searchable?
</h2>
{gongConnectorIndexingStatuses.length > 0 && (
<>
<p className="text-sm mb-2">
We pull the latest transcript every <b>10</b> minutes.
</p>
<div className="mb-2">
<ConnectorsTable<GongConfig, GongCredentialJson>
connectorIndexingStatuses={gongConnectorIndexingStatuses}
liveCredential={gongCredential}
getCredential={(credential) =>
credential.credential_json.gong_access_key
}
specialColumns={[
{
header: "Workspaces",
key: "workspaces",
getValue: (ccPairStatus) =>
ccPairStatus.connector.connector_specific_config
.workspaces &&
ccPairStatus.connector.connector_specific_config.workspaces
.length > 0
? ccPairStatus.connector.connector_specific_config.workspaces.join(
", "
)
: "",
},
]}
includeName={true}
onUpdate={() =>
mutate("/api/manage/admin/connector/indexing-status")
}
onCredentialLink={async (connectorId) => {
if (gongCredential) {
await linkCredential(connectorId, gongCredential.id);
mutate("/api/manage/admin/connector/indexing-status");
}
}}
/>
</div>
</>
)}
{gongCredential ? (
<>
<div className="border-solid border-gray-600 border rounded-md p-6 mt-4">
<h2 className="font-bold mb-3">Create a new Gong Connector</h2>
<ConnectorForm<GongConfig>
nameBuilder={(values) =>
values.workspaces
? `GongConnector-${values.workspaces.join("_")}`
: `GongConnector-All`
}
source="gong"
inputType="poll"
formBodyBuilder={TextArrayFieldBuilder({
name: "workspaces",
label: "Workspaces:",
subtext:
"Specify 0 or more workspaces to index. Be sure to use the EXACT workspace name from Gong. " +
"If no workspaces are specified, transcripts from all workspaces will be indexed.",
})}
validationSchema={Yup.object().shape({
workspaces: Yup.array().of(
Yup.string().required("Workspace names must be strings")
),
})}
initialValues={{
workspaces: [],
}}
refreshFreq={10 * 60} // 10 minutes
credentialId={gongCredential.id}
/>
</div>
</>
) : (
<p className="text-sm">
Please provide your API Access Info in Step 1 first! Once done with
that, you can then start indexing all your Gong transcripts.
</p>
)}
</>
);
};
export default function Page() {
return (
<div className="mx-auto container">
<div className="mb-4">
<HealthCheckBanner />
</div>
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
<GongIcon size={32} />
<h1 className="text-3xl font-bold pl-2">Gong</h1>
</div>
<Main />
</div>
);
}

View File

@ -220,16 +220,18 @@ const Main = () => {
{
header: "Url",
key: "url",
getValue: (connector) => (
<a
className="text-blue-500"
href={
connector.connector_specific_config.jira_project_url
}
>
{connector.connector_specific_config.jira_project_url}
</a>
),
getValue: (ccPairStatus) => {
const connectorConfig =
ccPairStatus.connector.connector_specific_config;
return (
<a
className="text-blue-500"
href={connectorConfig.jira_project_url}
>
{connectorConfig.jira_project_url}
</a>
);
},
},
]}
onUpdate={() =>

View File

@ -185,14 +185,18 @@ const Main = () => {
{
header: "Url",
key: "url",
getValue: (connector) => (
<a
className="text-blue-500"
href={connector.connector_specific_config.base_url}
>
{connector.connector_specific_config.base_url}
</a>
),
getValue: (ccPairStatus) => {
const connectorConfig =
ccPairStatus.connector.connector_specific_config;
return (
<a
className="text-blue-500"
href={connectorConfig.base_url}
>
{connectorConfig.base_url}
</a>
);
},
},
]}
onUpdate={() =>

View File

@ -154,17 +154,20 @@ const MainSection = () => {
{
header: "Workspace",
key: "workspace",
getValue: (connector) =>
connector.connector_specific_config.workspace,
getValue: (ccPairStatus) =>
ccPairStatus.connector.connector_specific_config.workspace,
},
{
header: "Channels",
key: "channels",
getValue: (connector) =>
connector.connector_specific_config.channels &&
connector.connector_specific_config.channels.length > 0
? connector.connector_specific_config.channels.join(", ")
: "",
getValue: (ccPairStatus) => {
const connectorConfig =
ccPairStatus.connector.connector_specific_config;
return connectorConfig.channels &&
connectorConfig.channels.length > 0
? connectorConfig.channels.join(", ")
: "";
},
},
]}
onUpdate={() =>

View File

@ -119,24 +119,28 @@ export default function Web() {
{
header: "Base URL",
key: "base_url",
getValue: (connector) => (
<a
className="text-blue-500"
href={connector.connector_specific_config.base_url}
>
{connector.connector_specific_config.base_url}
</a>
),
getValue: (ccPairConfig) => {
const connectorConfig =
ccPairConfig.connector.connector_specific_config;
return (
<a className="text-blue-500" href={connectorConfig.base_url}>
{connectorConfig.base_url}
</a>
);
},
},
{
header: "Scrape Method",
key: "web_connector_type",
getValue: (connector) =>
connector.connector_specific_config.web_connector_type
getValue: (ccPairStatus) => {
const connectorConfig =
ccPairStatus.connector.connector_specific_config;
return connectorConfig.web_connector_type
? SCRAPE_TYPE_TO_PRETTY_NAME[
connector.connector_specific_config.web_connector_type
connectorConfig.web_connector_type
]
: "Recursive",
: "Recursive";
},
},
]}
onUpdate={() => mutate("/api/manage/admin/connector/indexing-status")}

View File

@ -151,14 +151,14 @@ const MainSection = () => {
{
header: "Realm name",
key: "realm_name",
getValue: (connector) =>
connector.connector_specific_config.realm_name,
getValue: (ccPairStatus) =>
ccPairStatus.connector.connector_specific_config.realm_name,
},
{
header: "Realm url",
key: "realm_url",
getValue: (connector) =>
connector.connector_specific_config.realm_url,
getValue: (ccPairStatus) =>
ccPairStatus.connector.connector_specific_config.realm_url,
},
]}
onUpdate={() =>

View File

@ -10,6 +10,7 @@ import {
BookstackIcon,
ConfluenceIcon,
GuruIcon,
GongIcon,
FileIcon,
JiraIcon,
SlabIcon,
@ -159,6 +160,15 @@ export default async function AdminLayout({
),
link: "/admin/connectors/guru",
},
{
name: (
<div className="flex">
<GongIcon size={16} />
<div className="ml-1">Gong</div>
</div>
),
link: "/admin/connectors/gong",
},
{
name: (
<div className="flex">

View File

@ -97,15 +97,24 @@ export function StatusRow<ConnectorConfigType, ConnectorCredentialType>({
);
}
interface ColumnSpecification<ConnectorConfigType> {
export interface ColumnSpecification<
ConnectorConfigType,
ConnectorCredentialType
> {
header: string;
key: string;
getValue: (
connector: Connector<ConnectorConfigType>
ccPairStatus: ConnectorIndexingStatus<
ConnectorConfigType,
ConnectorCredentialType
>
) => JSX.Element | string | undefined;
}
interface ConnectorsTableProps<ConnectorConfigType, ConnectorCredentialType> {
export interface ConnectorsTableProps<
ConnectorConfigType,
ConnectorCredentialType
> {
connectorIndexingStatuses: ConnectorIndexingStatus<
ConnectorConfigType,
ConnectorCredentialType
@ -116,7 +125,11 @@ interface ConnectorsTableProps<ConnectorConfigType, ConnectorCredentialType> {
) => JSX.Element | string;
onUpdate: () => void;
onCredentialLink?: (connectorId: number) => void;
specialColumns?: ColumnSpecification<ConnectorConfigType>[];
specialColumns?: ColumnSpecification<
ConnectorConfigType,
ConnectorCredentialType
>[];
includeName?: boolean;
}
export function ConnectorsTable<ConnectorConfigType, ConnectorCredentialType>({
@ -126,6 +139,7 @@ export function ConnectorsTable<ConnectorConfigType, ConnectorCredentialType>({
specialColumns,
onUpdate,
onCredentialLink,
includeName = false,
}: ConnectorsTableProps<ConnectorConfigType, ConnectorCredentialType>) {
const [popup, setPopup] = useState<{
message: string;
@ -136,6 +150,7 @@ export function ConnectorsTable<ConnectorConfigType, ConnectorCredentialType>({
getCredential !== undefined && onCredentialLink !== undefined;
const columns = [
...(includeName ? [{ header: "Name", key: "name" }] : []),
...(specialColumns ?? []),
{
header: "Status",
@ -202,10 +217,15 @@ export function ConnectorsTable<ConnectorConfigType, ConnectorCredentialType>({
? Object.fromEntries(
specialColumns.map(({ key, getValue }, i) => [
key,
getValue(connector),
getValue(connectorIndexingStatus),
])
)
: {}),
...(includeName
? {
name: connectorIndexingStatus.name || "",
}
: {}),
};
// index: (
// <IndexButtonForTable

View File

@ -12,6 +12,7 @@ import { TrashIcon } from "@/components/icons/icons";
import { updateConnector } from "@/lib/connector";
import { AttachCredentialButtonForTable } from "@/components/admin/connectors/buttons/AttachCredentialButtonForTable";
import { scheduleDeletionJobForConnector } from "@/lib/documentDeletion";
import { ConnectorsTableProps } from "./ConnectorsTable";
const SingleUseConnectorStatus = ({
indexingStatus,
@ -43,26 +44,6 @@ const SingleUseConnectorStatus = ({
return <div className="text-red-700">Failed</div>;
};
interface ColumnSpecification<ConnectorConfigType> {
header: string;
key: string;
getValue: (connector: Connector<ConnectorConfigType>) => JSX.Element | string;
}
interface ConnectorsTableProps<ConnectorConfigType, ConnectorCredentialType> {
connectorIndexingStatuses: ConnectorIndexingStatus<
ConnectorConfigType,
ConnectorCredentialType
>[];
liveCredential?: Credential<ConnectorCredentialType> | null;
getCredential?: (
credential: Credential<ConnectorCredentialType>
) => JSX.Element | string;
onUpdate: () => void;
onCredentialLink?: (connectorId: number) => void;
specialColumns?: ColumnSpecification<ConnectorConfigType>[];
}
export function SingleUseConnectorsTable<
ConnectorConfigType,
ConnectorCredentialType
@ -73,6 +54,7 @@ export function SingleUseConnectorsTable<
specialColumns,
onUpdate,
onCredentialLink,
includeName = false,
}: ConnectorsTableProps<ConnectorConfigType, ConnectorCredentialType>) {
const [popup, setPopup] = useState<{
message: string;
@ -181,7 +163,7 @@ export function SingleUseConnectorsTable<
? Object.fromEntries(
specialColumns.map(({ key, getValue }, i) => [
key,
getValue(connector),
getValue(connectorIndexingStatus),
])
)
: {}),

View File

@ -39,6 +39,7 @@ import Image from "next/image";
import jiraSVG from "../../../public/Jira.svg";
import confluenceSVG from "../../../public/Confluence.svg";
import guruIcon from "../../../public/Guru.svg";
import gongIcon from "../../../public/Gong.png";
import zulipIcon from "../../../public/Zulip.png";
import linearIcon from "../../../public/Linear.png";
import hubSpotIcon from "../../../public/HubSpot.png";
@ -423,6 +424,18 @@ export const GuruIcon = ({
</div>
);
export const GongIcon = ({
size = 16,
className = defaultTailwindCSS,
}: IconProps) => (
<div
style={{ width: `${size}px`, height: `${size}px` }}
className={`w-[${size}px] h-[${size}px] ` + className}
>
<Image src={gongIcon} alt="Logo" width="96" height="96" />
</div>
);
export const HubSpotIcon = ({
size = 16,
className = defaultTailwindCSS,

View File

@ -23,6 +23,7 @@ const sources: Source[] = [
{ displayName: "Github PRs", internalName: "github" },
{ displayName: "Web", internalName: "web" },
{ displayName: "Guru", internalName: "guru" },
{ displayName: "Gong", internalName: "gong" },
{ displayName: "File", internalName: "file" },
{ displayName: "Notion", internalName: "notion" },
{ displayName: "Zulip", internalName: "zulip" },

View File

@ -7,6 +7,7 @@ import {
GlobeIcon,
GoogleDriveIcon,
GuruIcon,
GongIcon,
JiraIcon,
LinearIcon,
NotionIcon,
@ -103,6 +104,12 @@ export const getSourceMetadata = (sourceType: ValidSources): SourceMetadata => {
displayName: "Guru",
adminPageLink: "/admin/connectors/guru",
};
case "gong":
return {
icon: GongIcon,
displayName: "Gong",
adminPageLink: "/admin/connectors/gong",
};
case "linear":
return {
icon: LinearIcon,

View File

@ -19,6 +19,7 @@ export type ValidSources =
| "slab"
| "notion"
| "guru"
| "gong"
| "zulip"
| "linear"
| "hubspot"
@ -96,6 +97,10 @@ export interface SlabConfig {
export interface GuruConfig {}
export interface GongConfig {
workspaces?: string[];
}
export interface FileConfig {
file_locations: string[];
}
@ -203,6 +208,11 @@ export interface GuruCredentialJson {
guru_user_token: string;
}
export interface GongCredentialJson {
gong_access_key: string;
gong_access_key_secret: string;
}
export interface LinearCredentialJson {
linear_api_key: string;
}