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
23 changed files with 676 additions and 77 deletions

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;
}