mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-07-01 02:00:48 +02:00
Request Tracker Connector (#709)
Contributed by Evan! Thanks for the contribution! - Minor linting and rebasing done by Yuhong, everything else from Evan --------- Co-authored-by: Evan Sarmiento <e.sarmiento@soax.com> Co-authored-by: Evan <esarmien@fas.harvard.edu>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
.env
|
.env
|
||||||
.DS_store
|
.DS_store
|
||||||
.venv
|
.venv
|
||||||
|
.mypy_cache
|
||||||
|
1
backend/.gitignore
vendored
1
backend/.gitignore
vendored
@ -1,4 +1,5 @@
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
|
.mypy_cache
|
||||||
.idea/
|
.idea/
|
||||||
site_crawls/
|
site_crawls/
|
||||||
.ipynb_checkpoints/
|
.ipynb_checkpoints/
|
||||||
|
@ -41,6 +41,7 @@ class DocumentSource(str, Enum):
|
|||||||
SLACK = "slack"
|
SLACK = "slack"
|
||||||
WEB = "web"
|
WEB = "web"
|
||||||
GOOGLE_DRIVE = "google_drive"
|
GOOGLE_DRIVE = "google_drive"
|
||||||
|
REQUESTTRACKER = "requesttracker"
|
||||||
GITHUB = "github"
|
GITHUB = "github"
|
||||||
GURU = "guru"
|
GURU = "guru"
|
||||||
BOOKSTACK = "bookstack"
|
BOOKSTACK = "bookstack"
|
||||||
|
@ -21,6 +21,7 @@ from danswer.connectors.linear.connector import LinearConnector
|
|||||||
from danswer.connectors.models import InputType
|
from danswer.connectors.models import InputType
|
||||||
from danswer.connectors.notion.connector import NotionConnector
|
from danswer.connectors.notion.connector import NotionConnector
|
||||||
from danswer.connectors.productboard.connector import ProductboardConnector
|
from danswer.connectors.productboard.connector import ProductboardConnector
|
||||||
|
from danswer.connectors.requesttracker.connector import RequestTrackerConnector
|
||||||
from danswer.connectors.slab.connector import SlabConnector
|
from danswer.connectors.slab.connector import SlabConnector
|
||||||
from danswer.connectors.slack.connector import SlackLoadConnector
|
from danswer.connectors.slack.connector import SlackLoadConnector
|
||||||
from danswer.connectors.slack.connector import SlackPollConnector
|
from danswer.connectors.slack.connector import SlackPollConnector
|
||||||
@ -53,6 +54,7 @@ def identify_connector_class(
|
|||||||
DocumentSource.SLAB: SlabConnector,
|
DocumentSource.SLAB: SlabConnector,
|
||||||
DocumentSource.NOTION: NotionConnector,
|
DocumentSource.NOTION: NotionConnector,
|
||||||
DocumentSource.ZULIP: ZulipConnector,
|
DocumentSource.ZULIP: ZulipConnector,
|
||||||
|
DocumentSource.REQUESTTRACKER: RequestTrackerConnector,
|
||||||
DocumentSource.GURU: GuruConnector,
|
DocumentSource.GURU: GuruConnector,
|
||||||
DocumentSource.LINEAR: LinearConnector,
|
DocumentSource.LINEAR: LinearConnector,
|
||||||
DocumentSource.HUBSPOT: HubSpotConnector,
|
DocumentSource.HUBSPOT: HubSpotConnector,
|
||||||
|
1
backend/danswer/connectors/requesttracker/.gitignore
vendored
Normal file
1
backend/danswer/connectors/requesttracker/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
.env
|
152
backend/danswer/connectors/requesttracker/connector.py
Normal file
152
backend/danswer/connectors/requesttracker/connector.py
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from datetime import timezone
|
||||||
|
from logging import DEBUG as LOG_LVL_DEBUG
|
||||||
|
from typing import Any
|
||||||
|
from typing import List
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from rt.rest1 import ALL_QUEUES
|
||||||
|
from rt.rest1 import Rt
|
||||||
|
|
||||||
|
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 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()
|
||||||
|
|
||||||
|
|
||||||
|
class RequestTrackerError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RequestTrackerConnector(PollConnector):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
batch_size: int = INDEX_BATCH_SIZE,
|
||||||
|
) -> None:
|
||||||
|
self.batch_size = batch_size
|
||||||
|
|
||||||
|
def txn_link(self, tid: int, txn: int) -> str:
|
||||||
|
return f"{self.rt_base_url}/Ticket/Display.html?id={tid}&txn={txn}"
|
||||||
|
|
||||||
|
def build_doc_sections_from_txn(
|
||||||
|
self, connection: Rt, ticket_id: int
|
||||||
|
) -> List[Section]:
|
||||||
|
Sections: List[Section] = []
|
||||||
|
|
||||||
|
get_history_resp = connection.get_history(ticket_id)
|
||||||
|
|
||||||
|
if get_history_resp is None:
|
||||||
|
raise RequestTrackerError(f"Ticket {ticket_id} cannot be found")
|
||||||
|
|
||||||
|
for tx in get_history_resp:
|
||||||
|
Sections.append(
|
||||||
|
Section(
|
||||||
|
link=self.txn_link(ticket_id, int(tx["id"])),
|
||||||
|
text="\n".join(
|
||||||
|
[
|
||||||
|
f"{k}:\n{v}\n" if k != "Attachments" else ""
|
||||||
|
for (k, v) in tx.items()
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return Sections
|
||||||
|
|
||||||
|
def load_credentials(self, credentials: dict[str, Any]) -> Optional[dict[str, Any]]:
|
||||||
|
self.rt_username = credentials.get("requesttracker_username")
|
||||||
|
self.rt_password = credentials.get("requesttracker_password")
|
||||||
|
self.rt_base_url = credentials.get("requesttracker_base_url")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# This does not include RT file attachments yet.
|
||||||
|
def _process_tickets(
|
||||||
|
self, start: datetime, end: datetime
|
||||||
|
) -> GenerateDocumentsOutput:
|
||||||
|
if any([self.rt_username, self.rt_password, self.rt_base_url]) is None:
|
||||||
|
raise ConnectorMissingCredentialError("requesttracker")
|
||||||
|
|
||||||
|
Rt0 = Rt(
|
||||||
|
f"{self.rt_base_url}/REST/1.0/",
|
||||||
|
self.rt_username,
|
||||||
|
self.rt_password,
|
||||||
|
)
|
||||||
|
|
||||||
|
Rt0.login()
|
||||||
|
|
||||||
|
d0 = start.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
d1 = end.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
tickets = Rt0.search(
|
||||||
|
Queue=ALL_QUEUES,
|
||||||
|
raw_query=f"Updated > '{d0}' AND Updated < '{d1}'",
|
||||||
|
)
|
||||||
|
|
||||||
|
doc_batch: List[Document] = []
|
||||||
|
|
||||||
|
for ticket in tickets:
|
||||||
|
ticket_keys_to_omit = ["id", "Subject"]
|
||||||
|
tid: int = int(ticket["numerical_id"])
|
||||||
|
ticketLink: str = f"{self.rt_base_url}/Ticket/Display.html?id={tid}"
|
||||||
|
logger.info(f"Processing ticket {tid}")
|
||||||
|
doc = Document(
|
||||||
|
id=ticket["id"],
|
||||||
|
sections=[Section(link=ticketLink, text=f"{ticket['Subject']}\n")]
|
||||||
|
+ self.build_doc_sections_from_txn(Rt0, tid),
|
||||||
|
source=DocumentSource.REQUESTTRACKER,
|
||||||
|
semantic_identifier=ticket["Subject"],
|
||||||
|
metadata={
|
||||||
|
key: value
|
||||||
|
for key, value in ticket.items()
|
||||||
|
if key not in ticket_keys_to_omit
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
doc_batch.append(doc)
|
||||||
|
|
||||||
|
if len(doc_batch) >= self.batch_size:
|
||||||
|
yield doc_batch
|
||||||
|
doc_batch = []
|
||||||
|
|
||||||
|
if doc_batch:
|
||||||
|
yield doc_batch
|
||||||
|
|
||||||
|
def poll_source(
|
||||||
|
self, start: SecondsSinceUnixEpoch, end: SecondsSinceUnixEpoch
|
||||||
|
) -> GenerateDocumentsOutput:
|
||||||
|
# Keep query short, only look behind 1 day at maximum
|
||||||
|
one_day_ago: float = end - (24 * 60 * 60)
|
||||||
|
_start: float = start if start > one_day_ago else one_day_ago
|
||||||
|
start_datetime = datetime.fromtimestamp(_start, tz=timezone.utc)
|
||||||
|
end_datetime = datetime.fromtimestamp(end, tz=timezone.utc)
|
||||||
|
yield from self._process_tickets(start_datetime, end_datetime)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
logger.setLevel(LOG_LVL_DEBUG)
|
||||||
|
rt_connector = RequestTrackerConnector()
|
||||||
|
rt_connector.load_credentials(
|
||||||
|
{
|
||||||
|
"requesttracker_username": os.getenv("RT_USERNAME"),
|
||||||
|
"requesttracker_password": os.getenv("RT_PASSWORD"),
|
||||||
|
"requesttracker_base_url": os.getenv("RT_BASE_URL"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
current = time.time()
|
||||||
|
one_day_ago = current - (24 * 60 * 60) # 1 days
|
||||||
|
latest_docs = rt_connector.poll_source(one_day_ago, current)
|
||||||
|
|
||||||
|
for doc in latest_docs:
|
||||||
|
print(doc)
|
@ -37,6 +37,7 @@ pydantic==1.10.7
|
|||||||
PyGithub==1.58.2
|
PyGithub==1.58.2
|
||||||
pypdf==3.17.0
|
pypdf==3.17.0
|
||||||
pytest-playwright==0.3.2
|
pytest-playwright==0.3.2
|
||||||
|
python-dotenv==1.0.0
|
||||||
python-multipart==0.0.6
|
python-multipart==0.0.6
|
||||||
requests==2.31.0
|
requests==2.31.0
|
||||||
requests-oauthlib==1.3.1
|
requests-oauthlib==1.3.1
|
||||||
@ -44,6 +45,7 @@ retry==0.9.2 # This pulls in py which is in CVE-2022-42969, must remove py from
|
|||||||
rfc3986==1.5.0
|
rfc3986==1.5.0
|
||||||
# need to pin `safetensors` version, since the latest versions requires
|
# need to pin `safetensors` version, since the latest versions requires
|
||||||
# building from source using Rust
|
# building from source using Rust
|
||||||
|
rt==3.1.2
|
||||||
safetensors==0.3.1
|
safetensors==0.3.1
|
||||||
sentence-transformers==2.2.2
|
sentence-transformers==2.2.2
|
||||||
slack-sdk==3.20.2
|
slack-sdk==3.20.2
|
||||||
|
BIN
web/public/RequestTracker.png
Normal file
BIN
web/public/RequestTracker.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
236
web/src/app/admin/connectors/requesttracker/page.tsx
Normal file
236
web/src/app/admin/connectors/requesttracker/page.tsx
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as Yup from "yup";
|
||||||
|
import { TrashIcon, RequestTrackerIcon } from "@/components/icons/icons"; // Make sure you have a Document360 icon
|
||||||
|
import { fetcher } from "@/lib/fetcher";
|
||||||
|
import useSWR, { useSWRConfig } from "swr";
|
||||||
|
import { LoadingAnimation } from "@/components/Loading";
|
||||||
|
import { HealthCheckBanner } from "@/components/health/healthcheck";
|
||||||
|
import {
|
||||||
|
RequestTrackerConfig,
|
||||||
|
RequestTrackerCredentialJson,
|
||||||
|
ConnectorIndexingStatus,
|
||||||
|
Credential,
|
||||||
|
} from "@/lib/types"; // Modify or create these types as required
|
||||||
|
import { adminDeleteCredential, linkCredential } from "@/lib/credential";
|
||||||
|
import { CredentialForm } from "@/components/admin/connectors/CredentialForm";
|
||||||
|
import {
|
||||||
|
TextFormField,
|
||||||
|
TextArrayFieldBuilder,
|
||||||
|
} from "@/components/admin/connectors/Field";
|
||||||
|
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
|
||||||
|
import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
|
||||||
|
import { usePublicCredentials } from "@/lib/hooks";
|
||||||
|
|
||||||
|
const MainSection = () => {
|
||||||
|
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,
|
||||||
|
error: isCredentialsError,
|
||||||
|
refreshCredentials,
|
||||||
|
} = usePublicCredentials();
|
||||||
|
|
||||||
|
if (
|
||||||
|
(!connectorIndexingStatuses && isConnectorIndexingStatusesLoading) ||
|
||||||
|
(!credentialsData && isCredentialsLoading)
|
||||||
|
) {
|
||||||
|
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 requestTrackerConnectorIndexingStatuses: ConnectorIndexingStatus<
|
||||||
|
RequestTrackerConfig,
|
||||||
|
RequestTrackerCredentialJson
|
||||||
|
>[] = connectorIndexingStatuses.filter(
|
||||||
|
(connectorIndexingStatus) =>
|
||||||
|
connectorIndexingStatus.connector.source === "requesttracker"
|
||||||
|
);
|
||||||
|
|
||||||
|
const requestTrackerCredential:
|
||||||
|
| Credential<RequestTrackerCredentialJson>
|
||||||
|
| undefined = credentialsData.find(
|
||||||
|
(credential) => credential.credential_json?.requesttracker_username
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||||
|
Step 1: Provide Request Tracker credentials
|
||||||
|
</h2>
|
||||||
|
{requestTrackerCredential ? (
|
||||||
|
<>
|
||||||
|
<div className="flex mb-1 text-sm">
|
||||||
|
<p className="my-auto">Existing Request Tracker username: </p>
|
||||||
|
<p className="ml-1 italic my-auto">
|
||||||
|
{requestTrackerCredential.credential_json.requesttracker_username}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
className="ml-1 hover:bg-gray-700 rounded-full p-1"
|
||||||
|
onClick={async () => {
|
||||||
|
await adminDeleteCredential(requestTrackerCredential.id);
|
||||||
|
refreshCredentials();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TrashIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p className="text-sm mb-2">
|
||||||
|
To use the Request Tracker connector, provide a Request Tracker
|
||||||
|
username, password, and base url.
|
||||||
|
</p>
|
||||||
|
<p className="text-sm mb-2">
|
||||||
|
This connector currently supports{" "}
|
||||||
|
<a href="https://rt-wiki.bestpractical.com/wiki/REST">
|
||||||
|
Request Tracker REST API 1.0
|
||||||
|
</a>
|
||||||
|
,{" "}
|
||||||
|
<b>not the latest REST API 2.0 introduced in Request Tracker 5.0</b>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2">
|
||||||
|
<CredentialForm<RequestTrackerCredentialJson>
|
||||||
|
formBody={
|
||||||
|
<>
|
||||||
|
<TextFormField
|
||||||
|
name="requesttracker_username"
|
||||||
|
label="Request Tracker username:"
|
||||||
|
/>
|
||||||
|
<TextFormField
|
||||||
|
name="requesttracker_password"
|
||||||
|
label="Request Tracker password:"
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
<TextFormField
|
||||||
|
name="requesttracker_base_url"
|
||||||
|
label="Request Tracker base url:"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
validationSchema={Yup.object().shape({
|
||||||
|
requesttracker_username: Yup.string().required(
|
||||||
|
"Please enter your Request Tracker username"
|
||||||
|
),
|
||||||
|
requesttracker_password: Yup.string().required(
|
||||||
|
"Please enter your Request Tracker password"
|
||||||
|
),
|
||||||
|
requesttracker_base_url: Yup.string()
|
||||||
|
.url()
|
||||||
|
.required(
|
||||||
|
"Please enter the base url of your RT installation"
|
||||||
|
),
|
||||||
|
})}
|
||||||
|
initialValues={{
|
||||||
|
requesttracker_username: "",
|
||||||
|
requesttracker_password: "",
|
||||||
|
requesttracker_base_url: "",
|
||||||
|
}}
|
||||||
|
onSubmit={(isSuccess) => {
|
||||||
|
if (isSuccess) {
|
||||||
|
refreshCredentials();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{requestTrackerConnectorIndexingStatuses.length > 0 && (
|
||||||
|
<>
|
||||||
|
<p className="text-sm mb-2">
|
||||||
|
We index the most recently updated tickets from each Request Tracker
|
||||||
|
instance listed below regularly.
|
||||||
|
</p>
|
||||||
|
<p className="text-sm mb-2">
|
||||||
|
The initial poll at this time retrieves tickets updated in the past
|
||||||
|
hour. All subsequent polls execute every ten minutes. This should be
|
||||||
|
configurable in the future.
|
||||||
|
</p>
|
||||||
|
<div className="mb-2">
|
||||||
|
<ConnectorsTable<RequestTrackerConfig, RequestTrackerCredentialJson>
|
||||||
|
connectorIndexingStatuses={
|
||||||
|
requestTrackerConnectorIndexingStatuses
|
||||||
|
}
|
||||||
|
liveCredential={requestTrackerCredential}
|
||||||
|
getCredential={(credential) =>
|
||||||
|
credential.credential_json.requesttracker_base_url
|
||||||
|
}
|
||||||
|
onUpdate={() =>
|
||||||
|
mutate("/api/manage/admin/connector/indexing-status")
|
||||||
|
}
|
||||||
|
onCredentialLink={async (connectorId) => {
|
||||||
|
if (requestTrackerCredential) {
|
||||||
|
await linkCredential(
|
||||||
|
connectorId,
|
||||||
|
requestTrackerCredential.id
|
||||||
|
);
|
||||||
|
mutate("/api/manage/admin/connector/indexing-status");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{requestTrackerCredential &&
|
||||||
|
requestTrackerConnectorIndexingStatuses.length === 0 ? (
|
||||||
|
<div className="border-solid border-gray-600 border rounded-md p-6 mt-4">
|
||||||
|
<h2 className="font-bold mb-3">
|
||||||
|
Step 2: (Re)initialize connection to Request Tracker installation
|
||||||
|
</h2>
|
||||||
|
<ConnectorForm<RequestTrackerConfig>
|
||||||
|
nameBuilder={(values) =>
|
||||||
|
`RequestTracker-${requestTrackerCredential.credential_json.requesttracker_base_url}`
|
||||||
|
}
|
||||||
|
ccPairNameBuilder={(values) =>
|
||||||
|
`Request Tracker ${requestTrackerCredential.credential_json.requesttracker_base_url}`
|
||||||
|
}
|
||||||
|
source="requesttracker"
|
||||||
|
inputType="poll"
|
||||||
|
validationSchema={Yup.object().shape({})}
|
||||||
|
formBody={<></>}
|
||||||
|
initialValues={{}}
|
||||||
|
credentialId={requestTrackerCredential.id}
|
||||||
|
refreshFreq={10 * 60} // 10 minutes
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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">
|
||||||
|
<RequestTrackerIcon size={32} />
|
||||||
|
<h1 className="text-3xl font-bold pl-2">Request Tracker</h1>
|
||||||
|
</div>
|
||||||
|
<MainSection />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -23,6 +23,7 @@ import {
|
|||||||
BookmarkIcon,
|
BookmarkIcon,
|
||||||
CPUIcon,
|
CPUIcon,
|
||||||
Document360Icon,
|
Document360Icon,
|
||||||
|
RequestTrackerIcon,
|
||||||
GoogleSitesIcon,
|
GoogleSitesIcon,
|
||||||
GongIcon,
|
GongIcon,
|
||||||
ZoomInIcon,
|
ZoomInIcon,
|
||||||
@ -223,6 +224,15 @@ export async function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
),
|
),
|
||||||
link: "/admin/connectors/hubspot",
|
link: "/admin/connectors/hubspot",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: (
|
||||||
|
<div className="flex">
|
||||||
|
<RequestTrackerIcon size={16} />
|
||||||
|
<div className="ml-1">Request Tracker</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
link: "/admin/connectors/requesttracker",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: (
|
name: (
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
|
@ -40,6 +40,7 @@ import jiraSVG from "../../../public/Jira.svg";
|
|||||||
import confluenceSVG from "../../../public/Confluence.svg";
|
import confluenceSVG from "../../../public/Confluence.svg";
|
||||||
import guruIcon from "../../../public/Guru.svg";
|
import guruIcon from "../../../public/Guru.svg";
|
||||||
import gongIcon from "../../../public/Gong.png";
|
import gongIcon from "../../../public/Gong.png";
|
||||||
|
import requestTrackerIcon from "../../../public/RequestTracker.png";
|
||||||
import zulipIcon from "../../../public/Zulip.png";
|
import zulipIcon from "../../../public/Zulip.png";
|
||||||
import linearIcon from "../../../public/Linear.png";
|
import linearIcon from "../../../public/Linear.png";
|
||||||
import hubSpotIcon from "../../../public/HubSpot.png";
|
import hubSpotIcon from "../../../public/HubSpot.png";
|
||||||
@ -427,6 +428,18 @@ export const GuruIcon = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const RequestTrackerIcon = ({
|
||||||
|
size = 16,
|
||||||
|
className = defaultTailwindCSS,
|
||||||
|
}: IconProps) => (
|
||||||
|
<div
|
||||||
|
style={{ width: `${size}px`, height: `${size}px` }}
|
||||||
|
className={`w-[${size}px] h-[${size}px] ` + className}
|
||||||
|
>
|
||||||
|
<Image src={requestTrackerIcon} alt="Logo" width="96" height="96" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
export const GongIcon = ({
|
export const GongIcon = ({
|
||||||
size = 16,
|
size = 16,
|
||||||
className = defaultTailwindCSS,
|
className = defaultTailwindCSS,
|
||||||
|
@ -26,6 +26,7 @@ const sources: Source[] = [
|
|||||||
{ displayName: "Linear", internalName: "linear" },
|
{ displayName: "Linear", internalName: "linear" },
|
||||||
{ displayName: "HubSpot", internalName: "hubspot" },
|
{ displayName: "HubSpot", internalName: "hubspot" },
|
||||||
{ displayName: "Document360", internalName: "document360" },
|
{ displayName: "Document360", internalName: "document360" },
|
||||||
|
{ displayName: "Request Tracker", internalName: "requesttracker" },
|
||||||
{ displayName: "Google Sites", internalName: "google_sites" },
|
{ displayName: "Google Sites", internalName: "google_sites" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ import {
|
|||||||
HubSpotIcon,
|
HubSpotIcon,
|
||||||
Document360Icon,
|
Document360Icon,
|
||||||
GoogleSitesIcon,
|
GoogleSitesIcon,
|
||||||
|
RequestTrackerIcon,
|
||||||
ZendeskIcon,
|
ZendeskIcon,
|
||||||
} from "./icons/icons";
|
} from "./icons/icons";
|
||||||
|
|
||||||
@ -131,6 +132,12 @@ export const getSourceMetadata = (sourceType: ValidSources): SourceMetadata => {
|
|||||||
displayName: "Document360",
|
displayName: "Document360",
|
||||||
adminPageLink: "/admin/connectors/document360",
|
adminPageLink: "/admin/connectors/document360",
|
||||||
};
|
};
|
||||||
|
case "requesttracker":
|
||||||
|
return {
|
||||||
|
icon: RequestTrackerIcon,
|
||||||
|
displayName: "Request Tracker",
|
||||||
|
adminPageLink: "/admin/connectors/requesttracker",
|
||||||
|
};
|
||||||
case "google_sites":
|
case "google_sites":
|
||||||
return {
|
return {
|
||||||
icon: GoogleSitesIcon,
|
icon: GoogleSitesIcon,
|
||||||
|
@ -24,6 +24,7 @@ export type ValidSources =
|
|||||||
| "linear"
|
| "linear"
|
||||||
| "hubspot"
|
| "hubspot"
|
||||||
| "document360"
|
| "document360"
|
||||||
|
| "requesttracker"
|
||||||
| "file"
|
| "file"
|
||||||
| "google_sites"
|
| "google_sites"
|
||||||
| "zendesk";
|
| "zendesk";
|
||||||
@ -119,6 +120,8 @@ export interface NotionConfig {}
|
|||||||
|
|
||||||
export interface HubSpotConfig {}
|
export interface HubSpotConfig {}
|
||||||
|
|
||||||
|
export interface RequestTrackerConfig {}
|
||||||
|
|
||||||
export interface Document360Config {
|
export interface Document360Config {
|
||||||
workspace: string;
|
workspace: string;
|
||||||
categories?: string[];
|
categories?: string[];
|
||||||
@ -240,6 +243,12 @@ export interface HubSpotCredentialJson {
|
|||||||
hubspot_access_token: string;
|
hubspot_access_token: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RequestTrackerCredentialJson {
|
||||||
|
requesttracker_username: string;
|
||||||
|
requesttracker_password: string;
|
||||||
|
requesttracker_base_url: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Document360CredentialJson {
|
export interface Document360CredentialJson {
|
||||||
portal_id: string;
|
portal_id: string;
|
||||||
document360_api_token: string;
|
document360_api_token: string;
|
||||||
|
Reference in New Issue
Block a user