create a hubspot connector (#482)

This commit is contained in:
nlp8899
2023-10-02 13:13:23 -04:00
committed by GitHub
parent dbe33959c0
commit c666f35cd0
13 changed files with 407 additions and 0 deletions

BIN
web/public/HubSpot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,223 @@
"use client";
import * as Yup from "yup";
import { HubSpotIcon, 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,
ConnectorIndexingStatus,
HubSpotConfig,
HubSpotCredentialJson,
} 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 hubSpotConnectorIndexingStatuses: ConnectorIndexingStatus<
HubSpotConfig,
HubSpotCredentialJson
>[] = connectorIndexingStatuses.filter(
(connectorIndexingStatus) =>
connectorIndexingStatus.connector.source === "hubspot"
);
const hubSpotCredential: Credential<HubSpotCredentialJson> = credentialsData.filter(
(credential) => credential.credential_json?.hubspot_access_token
)[0];
return (
<>
{popup}
<p className="text-sm">
This connector allows you to sync all your HubSpot Tickets into Danswer.
</p>
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
Step 1: Provide your Credentials
</h2>
{hubSpotCredential ? (
<>
<div className="flex mb-1 text-sm">
<p className="my-auto">Existing Access Token: </p>
<p className="ml-1 italic my-auto max-w-md truncate">
{hubSpotCredential.credential_json?.hubspot_access_token}
</p>
<button
className="ml-1 hover:bg-gray-700 rounded-full p-1"
onClick={async () => {
if (hubSpotConnectorIndexingStatuses.length > 0) {
setPopup({
type: "error",
message:
"Must delete all connectors before deleting credentials",
});
return;
}
await adminDeleteCredential(hubSpotCredential.id);
refreshCredentials();
}}
>
<TrashIcon />
</button>
</div>
</>
) : (
<>
<p className="text-sm">
To use the HubSpot connector, provide the HubSpot Access Token.
</p>
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2">
<CredentialForm<HubSpotCredentialJson>
formBody={
<>
<TextFormField
name="hubspot_access_token"
label="HubSpot Access Token:"
type="password"
/>
</>
}
validationSchema={Yup.object().shape({
hubspot_access_token: Yup.string().required(
"Please enter your HubSpot Access Token"
)
})}
initialValues={{
hubspot_access_token: "",
}}
onSubmit={(isSuccess) => {
if (isSuccess) {
refreshCredentials();
}
}}
/>
</div>
</>
)}
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
Step 2: Start indexing!
</h2>
{hubSpotCredential ? (
!hubSpotConnectorIndexingStatuses.length ? (
<>
<p className="text-sm mb-2">
Click the button below to start indexing! We will pull the latest
tickets from HubSpot every <b>10</b> minutes.
</p>
<div className="flex">
<ConnectorForm<HubSpotConfig>
nameBuilder={() => "HubSpotConnector"}
source="hubspot"
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, hubSpotCredential.id);
mutate("/api/manage/admin/connector/indexing-status");
}
}}
/>
</div>
</>
) : (
<>
<p className="text-sm mb-2">
HubSpot connector is setup! We are pulling the latest tickets from HubSpot
every <b>10</b> minutes.
</p>
<ConnectorsTable<HubSpotConfig, HubSpotCredentialJson>
connectorIndexingStatuses={hubSpotConnectorIndexingStatuses}
liveCredential={hubSpotCredential}
getCredential={(credential) => {
return (
<div>
<p>{credential.credential_json.hubspot_access_token}</p>
</div>
);
}}
onCredentialLink={async (connectorId) => {
if (hubSpotCredential) {
await linkCredential(connectorId, hubSpotCredential.id);
mutate("/api/manage/admin/connector/indexing-status");
}
}}
onUpdate={() =>
mutate("/api/manage/admin/connector/indexing-status")
}
/>
</>
)
) : (
<>
<p className="text-sm">
Please provide your access token in Step 1 first! Once done with
that, you can then start indexing all your HubSpot tickets.
</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">
<HubSpotIcon size={32} />
<h1 className="text-3xl font-bold pl-2">HubSpot</h1>
</div>
<Main />
</div>
);
}

View File

@@ -19,6 +19,7 @@ import {
LinearIcon,
UsersIcon,
ThumbsUpIcon,
HubSpotIcon,
BookmarkIcon,
CPUIcon,
} from "@/components/icons/icons";
@@ -192,6 +193,15 @@ export default async function AdminLayout({
),
link: "/admin/connectors/file",
},
{
name: (
<div className="flex">
<HubSpotIcon size={16} />
<div className="ml-1">HubSpot</div>
</div>
),
link: "/admin/connectors/hubspot",
},
],
},
{

View File

@@ -41,6 +41,7 @@ import confluenceSVG from "../../../public/Confluence.svg";
import guruIcon from "../../../public/Guru.svg";
import zulipIcon from "../../../public/Zulip.png";
import linearIcon from "../../../public/Linear.png";
import hubSpotIcon from "../../../public/HubSpot.png";
interface IconProps {
size?: number;
@@ -421,3 +422,18 @@ export const GuruIcon = ({
<Image src={guruIcon} alt="Logo" width="96" height="96" />
</div>
);
export const HubSpotIcon = ({
size = 16,
className = defaultTailwindCSS,
}: IconProps) => {
return (
<div
// HubSpot Icon has a bit more surrounding whitespace than other icons, which is why we need to adjust it here
style={{ width: `${size + 4}px`, height: `${size + 4}px` }}
className={`w-[${size + 4}px] h-[${size + 4}px] -m-0.5 ` + className}
>
<Image src={hubSpotIcon} alt="Logo" width="96" height="96" />
</div>
);
};

View File

@@ -27,6 +27,7 @@ const sources: Source[] = [
{ displayName: "Notion", internalName: "notion" },
{ displayName: "Zulip", internalName: "zulip" },
{ displayName: "Linear", internalName: "linear" },
{ displayName: "HubSpot", internalName: "hubspot" },
];
interface SourceSelectorProps {

View File

@@ -14,6 +14,7 @@ import {
SlabIcon,
SlackIcon,
ZulipIcon,
HubSpotIcon,
} from "./icons/icons";
interface SourceMetadata {
@@ -108,6 +109,12 @@ export const getSourceMetadata = (sourceType: ValidSources): SourceMetadata => {
displayName: "Linear",
adminPageLink: "/admin/connectors/linear",
};
case "hubspot":
return {
icon: HubSpotIcon,
displayName: "HubSpot",
adminPageLink: "/admin/connectors/hubspot",
};
default:
throw new Error("Invalid source type");
}

View File

@@ -21,6 +21,7 @@ export type ValidSources =
| "guru"
| "zulip"
| "linear"
| "hubspot"
| "file";
export type ValidInputTypes = "load_state" | "poll" | "event";
export type ValidStatuses =
@@ -106,6 +107,8 @@ export interface ZulipConfig {
export interface NotionConfig {}
export interface HubSpotConfig {}
export interface IndexAttemptSnapshot {
status: ValidStatuses | null;
num_docs_indexed: number;
@@ -204,6 +207,10 @@ export interface LinearCredentialJson {
linear_api_key: string;
}
export interface HubSpotCredentialJson {
hubspot_access_token: string;
}
// DELETION
export interface DeletionAttemptSnapshot {