Compare commits

...

13 Commits

Author SHA1 Message Date
Mohamed Mathari
ebe349ce49 fix non existent project url from jiraconfig 2025-09-12 14:55:13 +02:00
Me
af79c3540e formatting 2025-09-11 22:48:52 +02:00
Me
a9b3dacbed Progress reporting 2025-09-11 22:24:19 +02:00
Me
9162205723 comment on self.batch_size 2025-09-11 22:21:48 +02:00
Me
103dc43366 fix: remove duplicate comment in load_from_state 2025-09-11 22:18:39 +02:00
Mohamed Mathari
f97f9302ba Update backend/onyx/connectors/jira_service_management/connector.py
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-09-11 22:07:56 +02:00
Me
afaca66123 jql validation server-specific 2025-09-11 21:42:09 +02:00
Me
0ec6d72d4a Avoid embedding raw exception strings in failure messages 2025-09-11 21:06:03 +02:00
Mohamed Mathari
00ed378cad queries 2025-09-11 19:06:42 +02:00
Mohamed Mathari
4e3b53c0c0 PNG JiraServiceManagement.png 2025-09-11 11:29:34 +02:00
Mohamed Mathari
938fe1c789 Improve JSM connector descriptions and log 2025-09-11 11:21:48 +02:00
Mohamed Mathari
87f617a496 Add Jira Service Management connector to pull all tickets from a specified project 2025-09-11 10:33:25 +02:00
Mohamed Mathari
5adde78c84 Add Jira Service Management connector 2025-09-11 09:31:29 +02:00
14 changed files with 709 additions and 7 deletions

View File

@@ -163,6 +163,7 @@ class DocumentSource(str, Enum):
OUTLINE = "outline"
CONFLUENCE = "confluence"
JIRA = "jira"
JIRA_SERVICE_MANAGEMENT = "jira_service_management"
SLAB = "slab"
PRODUCTBOARD = "productboard"
FILE = "file"
@@ -546,6 +547,7 @@ DocumentSourceDescription: dict[DocumentSource, str] = {
DocumentSource.OUTLINE: "outline data",
DocumentSource.CONFLUENCE: "confluence data (pages, spaces, etc.)",
DocumentSource.JIRA: "jira data (issues, tickets, projects, etc.)",
DocumentSource.JIRA_SERVICE_MANAGEMENT: "jira service management data (issues, tickets, projects, etc.)",
DocumentSource.SLAB: "slab data",
DocumentSource.PRODUCTBOARD: "productboard data (boards, etc.)",
DocumentSource.FILE: "files",

View File

@@ -41,6 +41,9 @@ from onyx.connectors.interfaces import EventConnector
from onyx.connectors.interfaces import LoadConnector
from onyx.connectors.interfaces import PollConnector
from onyx.connectors.jira.connector import JiraConnector
from onyx.connectors.jira_service_management.connector import (
JiraServiceManagementConnector,
)
from onyx.connectors.linear.connector import LinearConnector
from onyx.connectors.loopio.connector import LoopioConnector
from onyx.connectors.mediawiki.wiki import MediaWikiConnector
@@ -91,6 +94,7 @@ def identify_connector_class(
DocumentSource.OUTLINE: OutlineConnector,
DocumentSource.CONFLUENCE: ConfluenceConnector,
DocumentSource.JIRA: JiraConnector,
DocumentSource.JIRA_SERVICE_MANAGEMENT: JiraServiceManagementConnector,
DocumentSource.PRODUCTBOARD: ProductboardConnector,
DocumentSource.SLAB: SlabConnector,
DocumentSource.NOTION: NotionConnector,

View File

@@ -0,0 +1,566 @@
import copy
from datetime import datetime
from datetime import timedelta
from datetime import timezone
from typing import Any
from jira import JIRA
from jira.resources import Issue
from typing_extensions import override
from onyx.configs.app_configs import INDEX_BATCH_SIZE
from onyx.configs.app_configs import JIRA_CONNECTOR_LABELS_TO_SKIP
from onyx.configs.app_configs import JIRA_CONNECTOR_MAX_TICKET_SIZE
from onyx.configs.constants import DocumentSource
from onyx.connectors.cross_connector_utils.miscellaneous_utils import (
is_atlassian_date_error,
)
from onyx.connectors.cross_connector_utils.miscellaneous_utils import time_str_to_utc
from onyx.connectors.exceptions import ConnectorValidationError
from onyx.connectors.exceptions import CredentialExpiredError
from onyx.connectors.exceptions import InsufficientPermissionsError
from onyx.connectors.exceptions import UnexpectedValidationError
from onyx.connectors.interfaces import CheckpointedConnector
from onyx.connectors.interfaces import CheckpointOutput
from onyx.connectors.interfaces import GenerateDocumentsOutput
from onyx.connectors.interfaces import GenerateSlimDocumentOutput
from onyx.connectors.interfaces import LoadConnector
from onyx.connectors.interfaces import SecondsSinceUnixEpoch
from onyx.connectors.interfaces import SlimConnector
from onyx.connectors.jira.access import get_project_permissions
from onyx.connectors.jira.connector import _is_cloud_client
from onyx.connectors.jira.connector import _JIRA_FULL_PAGE_SIZE
from onyx.connectors.jira.connector import _JIRA_SLIM_PAGE_SIZE
from onyx.connectors.jira.connector import _perform_jql_search
from onyx.connectors.jira.connector import JiraConnectorCheckpoint
from onyx.connectors.jira.connector import make_checkpoint_callback
from onyx.connectors.jira.connector import ONE_HOUR
from onyx.connectors.jira.utils import best_effort_basic_expert_info
from onyx.connectors.jira.utils import best_effort_get_field_from_issue
from onyx.connectors.jira.utils import build_jira_client
from onyx.connectors.jira.utils import build_jira_url
from onyx.connectors.jira.utils import extract_text_from_adf
from onyx.connectors.jira.utils import get_comment_strs
from onyx.connectors.jira.utils import get_jira_project_key_from_issue
from onyx.connectors.models import ConnectorFailure
from onyx.connectors.models import ConnectorMissingCredentialError
from onyx.connectors.models import Document
from onyx.connectors.models import DocumentFailure
from onyx.connectors.models import SlimDocument
from onyx.connectors.models import TextSection
from onyx.indexing.indexing_heartbeat import IndexingHeartbeatInterface
from onyx.utils.logger import setup_logger
logger = setup_logger()
# Constants for Jira Service Management field names
_FIELD_REPORTER = "reporter"
_FIELD_ASSIGNEE = "assignee"
_FIELD_PRIORITY = "priority"
_FIELD_STATUS = "status"
_FIELD_RESOLUTION = "resolution"
_FIELD_LABELS = "labels"
_FIELD_KEY = "key"
_FIELD_CREATED = "created"
_FIELD_DUEDATE = "duedate"
_FIELD_ISSUETYPE = "issuetype"
_FIELD_PARENT = "parent"
_FIELD_ASSIGNEE_EMAIL = "assignee_email"
_FIELD_REPORTER_EMAIL = "reporter_email"
_FIELD_PROJECT = "project"
_FIELD_PROJECT_NAME = "project_name"
_FIELD_UPDATED = "updated"
_FIELD_RESOLUTION_DATE = "resolutiondate"
_FIELD_RESOLUTION_DATE_KEY = "resolution_date"
def process_jira_service_management_issue(
jira_client: JIRA,
issue: Issue,
comment_email_blacklist: tuple[str, ...] = (),
labels_to_skip: set[str] | None = None,
) -> Document | None:
"""Process a Jira Service Management issue into a Document.
Args:
jira_client: The Jira client instance
issue: The Jira Service Management issue to process
comment_email_blacklist: Tuple of email addresses to exclude from comments
Returns:
A Document object containing the processed issue data, or None if processing fails
"""
if labels_to_skip:
if any(label in issue.fields.labels for label in labels_to_skip):
logger.info(
f"Skipping {issue.key} because it has a label to skip. Found "
f"labels: {issue.fields.labels}. Labels to skip: {labels_to_skip}."
)
return None
if isinstance(issue.fields.description, str):
description = issue.fields.description
else:
description = extract_text_from_adf(issue.raw["fields"]["description"])
comments = get_comment_strs(
issue=issue,
comment_email_blacklist=comment_email_blacklist,
)
ticket_content = f"{description}\n" + "\n".join(
[f"Comment: {comment}" for comment in comments if comment]
)
# Check ticket size
if len(ticket_content.encode("utf-8")) > JIRA_CONNECTOR_MAX_TICKET_SIZE:
logger.info(
f"Skipping {issue.key} because it exceeds the maximum size of "
f"{JIRA_CONNECTOR_MAX_TICKET_SIZE} bytes."
)
return None
page_url = build_jira_url(jira_client, issue.key)
metadata_dict: dict[str, str | list[str]] = {}
people = set()
# Extract reporter information
creator = best_effort_get_field_from_issue(issue, _FIELD_REPORTER)
if creator is not None and (
basic_expert_info := best_effort_basic_expert_info(creator)
):
people.add(basic_expert_info)
metadata_dict[_FIELD_REPORTER] = basic_expert_info.get_semantic_name()
if email := basic_expert_info.get_email():
metadata_dict[_FIELD_REPORTER_EMAIL] = email
# Extract assignee information
assignee = best_effort_get_field_from_issue(issue, _FIELD_ASSIGNEE)
if assignee is not None and (
basic_expert_info := best_effort_basic_expert_info(assignee)
):
people.add(basic_expert_info)
metadata_dict[_FIELD_ASSIGNEE] = basic_expert_info.get_semantic_name()
if email := basic_expert_info.get_email():
metadata_dict[_FIELD_ASSIGNEE_EMAIL] = email
# Extract other metadata
metadata_dict[_FIELD_KEY] = issue.key
if priority := best_effort_get_field_from_issue(issue, _FIELD_PRIORITY):
metadata_dict[_FIELD_PRIORITY] = priority.name
if status := best_effort_get_field_from_issue(issue, _FIELD_STATUS):
metadata_dict[_FIELD_STATUS] = status.name
if resolution := best_effort_get_field_from_issue(issue, _FIELD_RESOLUTION):
metadata_dict[_FIELD_RESOLUTION] = resolution.name
if labels := best_effort_get_field_from_issue(issue, _FIELD_LABELS):
metadata_dict[_FIELD_LABELS] = labels
if created := best_effort_get_field_from_issue(issue, _FIELD_CREATED):
metadata_dict[_FIELD_CREATED] = created
if updated := best_effort_get_field_from_issue(issue, _FIELD_UPDATED):
metadata_dict[_FIELD_UPDATED] = updated
if duedate := best_effort_get_field_from_issue(issue, _FIELD_DUEDATE):
metadata_dict[_FIELD_DUEDATE] = duedate
if issuetype := best_effort_get_field_from_issue(issue, _FIELD_ISSUETYPE):
metadata_dict[_FIELD_ISSUETYPE] = issuetype.name
if resolutiondate := best_effort_get_field_from_issue(
issue, _FIELD_RESOLUTION_DATE
):
metadata_dict[_FIELD_RESOLUTION_DATE_KEY] = resolutiondate
parent = best_effort_get_field_from_issue(issue, _FIELD_PARENT)
if parent is not None:
metadata_dict[_FIELD_PARENT] = parent.key
project = best_effort_get_field_from_issue(issue, _FIELD_PROJECT)
if project is not None:
metadata_dict[_FIELD_PROJECT_NAME] = project.name
metadata_dict[_FIELD_PROJECT] = project.key
else:
logger.error(f"Project should exist but does not for {issue.key}")
return Document(
id=page_url,
sections=[TextSection(link=page_url, text=ticket_content)],
source=DocumentSource.JIRA_SERVICE_MANAGEMENT,
semantic_identifier=f"{issue.key}: {issue.fields.summary}",
title=f"{issue.key} {issue.fields.summary}",
doc_updated_at=time_str_to_utc(issue.fields.updated),
primary_owners=list(people) or None,
metadata=metadata_dict,
)
class JiraServiceManagementConnector(
CheckpointedConnector[JiraConnectorCheckpoint], LoadConnector, SlimConnector
):
def __init__(
self,
jira_service_management_base_url: str,
project_key: str | None = None,
comment_email_blacklist: list[str] | None = None,
batch_size: int = INDEX_BATCH_SIZE,
# if a ticket has one of the labels specified in this list, we will just
# skip it. This is generally used to avoid indexing extra sensitive
# tickets.
labels_to_skip: list[str] = JIRA_CONNECTOR_LABELS_TO_SKIP,
# Custom JQL query to filter Jira Service Management issues
jql_query: str | None = None,
) -> None:
self.batch_size = batch_size
self.jira_base = jira_service_management_base_url.rstrip("/")
self.jira_project = project_key
self._comment_email_blacklist = comment_email_blacklist or []
self.labels_to_skip = set(labels_to_skip)
self.jql_query = jql_query
self._jira_client: JIRA | None = None
@property
def comment_email_blacklist(self) -> tuple:
return tuple(email.strip() for email in self._comment_email_blacklist)
@property
def jira_client(self) -> JIRA:
if self._jira_client is None:
raise ConnectorMissingCredentialError("Jira Service Management")
return self._jira_client
@property
def quoted_jira_project(self) -> str:
# Quote the project name to handle reserved words
if not self.jira_project:
return ""
return f'"{self.jira_project}"'
def load_credentials(self, credentials: dict[str, Any]) -> dict[str, Any] | None:
# Convert JSM credentials to standard Jira format for client
jira_credentials = {
"jira_user_email": credentials["jira_service_management_email"],
"jira_api_token": credentials["jira_service_management_api_token"],
}
self._jira_client = build_jira_client(
credentials=jira_credentials,
jira_base=self.jira_base,
)
return None
def _get_jql_query(
self, start: SecondsSinceUnixEpoch, end: SecondsSinceUnixEpoch
) -> str:
"""Get the JQL query for Service Management issues based on configuration and time range.
If a custom JQL query is provided, it will be used and combined with time constraints.
Otherwise, the query will be constructed based on project key (if provided) or
will default to filtering service desk projects.
Args:
start: Start timestamp (seconds since Unix epoch) for the query time range
end: End timestamp (seconds since Unix epoch) for the query time range
Returns:
JQL query string that filters for service management issues within the specified
time range.
"""
start_date_str = datetime.fromtimestamp(start, tz=timezone.utc).strftime(
"%Y-%m-%d %H:%M"
)
end_date_str = datetime.fromtimestamp(end, tz=timezone.utc).strftime(
"%Y-%m-%d %H:%M"
)
time_jql = f"updated >= '{start_date_str}' AND updated <= '{end_date_str}'"
# If custom JQL query is provided, use it and combine with time constraints
if self.jql_query:
return f"({self.jql_query}) AND {time_jql}"
# Use project key if provided - focus on that specific project
if self.jira_project:
base_jql = f"project = {self.quoted_jira_project}"
return f"{base_jql} AND {time_jql}"
# If no specific project, filter to only service desk projects
# This ensures we only pull JSM tickets, not regular Jira tickets
service_desk_jql = "project in projectsWhereUserHasPermission('Browse Projects') AND projectType = 'service_desk'"
return f"{service_desk_jql} AND {time_jql}"
def load_from_checkpoint(
self,
start: SecondsSinceUnixEpoch,
end: SecondsSinceUnixEpoch,
checkpoint: JiraConnectorCheckpoint,
) -> CheckpointOutput[JiraConnectorCheckpoint]:
jql = self._get_jql_query(start, end)
try:
return self._load_from_checkpoint(jql, checkpoint)
except Exception as e:
if is_atlassian_date_error(e):
jql = self._get_jql_query(start - ONE_HOUR, end)
return self._load_from_checkpoint(jql, checkpoint)
raise e
def _load_from_checkpoint(
self, jql: str, checkpoint: JiraConnectorCheckpoint
) -> CheckpointOutput[JiraConnectorCheckpoint]:
# Get the current offset from checkpoint or start at 0
starting_offset = checkpoint.offset or 0
current_offset = starting_offset
new_checkpoint = copy.deepcopy(checkpoint)
checkpoint_callback = make_checkpoint_callback(new_checkpoint)
for issue in _perform_jql_search(
jira_client=self.jira_client,
jql=jql,
start=current_offset,
max_results=_JIRA_FULL_PAGE_SIZE,
all_issue_ids=new_checkpoint.all_issue_ids,
checkpoint_callback=checkpoint_callback,
nextPageToken=new_checkpoint.cursor,
ids_done=new_checkpoint.ids_done,
):
issue_key = issue.key
try:
if document := process_jira_service_management_issue(
jira_client=self.jira_client,
issue=issue,
comment_email_blacklist=self.comment_email_blacklist,
labels_to_skip=self.labels_to_skip,
):
yield document
except Exception as e:
# Log the full exception for debugging but use a generic message for the failure
logger.error(
f"Failed to process Jira Service Management issue {issue_key}: "
f"Error type: {type(e).__name__}, Status code: {getattr(e, 'status_code', 'N/A')}"
)
yield ConnectorFailure(
failed_document=DocumentFailure(
document_id=issue_key,
document_link=build_jira_url(self.jira_client, issue_key),
),
failure_message="Failed to process Jira Service Management issue due to an unexpected error",
exception=e,
)
current_offset += 1
# Update checkpoint
self.update_checkpoint_for_next_run(
new_checkpoint, current_offset, starting_offset, _JIRA_FULL_PAGE_SIZE
)
return new_checkpoint
def update_checkpoint_for_next_run(
self,
checkpoint: JiraConnectorCheckpoint,
current_offset: int,
starting_offset: int,
page_size: int,
) -> None:
if _is_cloud_client(self.jira_client):
# other updates done in the checkpoint callback
checkpoint.has_more = (
len(checkpoint.all_issue_ids) > 0 or not checkpoint.ids_done
)
else:
checkpoint.offset = current_offset
# if we didn't retrieve a full batch, we're done
checkpoint.has_more = current_offset - starting_offset == page_size
def retrieve_all_slim_documents(
self,
start: SecondsSinceUnixEpoch | None = None,
end: SecondsSinceUnixEpoch | None = None,
callback: IndexingHeartbeatInterface | None = None,
) -> GenerateSlimDocumentOutput:
one_day = timedelta(hours=24).total_seconds()
start = start or 0
end = (
end or datetime.now().timestamp() + one_day
) # we add one day to account for any potential timezone issues
jql = self._get_jql_query(start, end)
checkpoint = self.build_dummy_checkpoint()
checkpoint_callback = make_checkpoint_callback(checkpoint)
prev_offset = 0
current_offset = 0
slim_doc_batch = []
while checkpoint.has_more:
if callback and callback.should_stop():
raise RuntimeError("retrieve_all_slim_documents: Stop signal detected")
for issue in _perform_jql_search(
jira_client=self.jira_client,
jql=jql,
start=current_offset,
# Use Jira-optimized page size for slim operations rather than self.batch_size
# Slim docs only need basic metadata, so larger batches (500) are efficient
# Full document processing uses smaller batches (50) due to content processing overhead
max_results=_JIRA_SLIM_PAGE_SIZE,
all_issue_ids=checkpoint.all_issue_ids,
checkpoint_callback=checkpoint_callback,
nextPageToken=checkpoint.cursor,
ids_done=checkpoint.ids_done,
):
project_key = get_jira_project_key_from_issue(issue=issue)
if not project_key:
continue
issue_key = best_effort_get_field_from_issue(issue, _FIELD_KEY)
id = build_jira_url(self.jira_client, issue_key)
slim_doc_batch.append(
SlimDocument(
id=id,
external_access=get_project_permissions(
jira_client=self.jira_client, jira_project=project_key
),
)
)
current_offset += 1
if len(slim_doc_batch) >= _JIRA_SLIM_PAGE_SIZE:
if callback:
callback.progress(
"retrieve_all_slim_documents", len(slim_doc_batch)
)
yield slim_doc_batch
slim_doc_batch = []
self.update_checkpoint_for_next_run(
checkpoint, current_offset, prev_offset, _JIRA_SLIM_PAGE_SIZE
)
prev_offset = current_offset
if slim_doc_batch:
yield slim_doc_batch
def load_from_state(self) -> GenerateDocumentsOutput:
"""Load all documents from JSM without time constraints (for full reindex)"""
# Use a very wide time range to get all documents
start_time = 0 # Unix epoch
end_time = int(datetime.now().timestamp())
checkpoint = self.build_dummy_checkpoint()
for doc_or_failure in self.load_from_checkpoint(
start_time, end_time, checkpoint
):
if isinstance(doc_or_failure, Document):
yield [doc_or_failure]
# Skip failures in load_from_state to avoid interrupting full reindex
def validate_connector_settings(self) -> None:
if self._jira_client is None:
raise ConnectorMissingCredentialError("Jira Service Management")
# If a custom JQL query is set, validate it's valid
if self.jql_query:
try:
# Try to execute the JQL query with a small limit to validate its syntax
# Use next(iter(...), None) to get just the first result without
# forcing evaluation of all results
if _is_cloud_client(self.jira_client):
# For Jira Cloud, we need to provide all_issue_ids parameter
dummy_checkpoint = self.build_dummy_checkpoint()
next(
iter(
_perform_jql_search(
jira_client=self.jira_client,
jql=self.jql_query,
start=0,
max_results=1,
all_issue_ids=dummy_checkpoint.all_issue_ids,
nextPageToken=dummy_checkpoint.cursor,
ids_done=dummy_checkpoint.ids_done,
)
),
None,
)
else:
# For Jira Server
next(
iter(
_perform_jql_search(
jira_client=self.jira_client,
jql=self.jql_query,
start=0,
max_results=1,
)
),
None,
)
except Exception as e:
self._handle_jira_connector_settings_error(e)
# If a specific project is set, validate it exists
elif self.jira_project:
try:
self.jira_client.project(self.jira_project)
except Exception as e:
self._handle_jira_connector_settings_error(e)
else:
# If neither JQL nor project specified, validate we can access the Jira API
try:
# Try to list projects to validate access
self.jira_client.projects()
except Exception as e:
self._handle_jira_connector_settings_error(e)
def _handle_jira_connector_settings_error(self, e: Exception) -> None:
"""Helper method to handle Jira API errors consistently.
Extracts error messages from the Jira API response for all status codes when possible,
providing more user-friendly error messages.
Args:
e: The exception raised by the Jira API
Raises:
CredentialExpiredError: If the status code is 401
InsufficientPermissionsError: If the status code is 403
ConnectorValidationError: For other HTTP errors with extracted error messages
"""
status_code = getattr(e, "status_code", None)
error_type = type(e).__name__
logger.error(
f"Jira Service Management API error during validation. Status code: {status_code}, Error type: {error_type}"
)
# Handle specific status codes with appropriate exceptions
if status_code == 401:
raise CredentialExpiredError(
"Jira Service Management credential appears to be expired or invalid (HTTP 401)."
)
elif status_code == 403:
raise InsufficientPermissionsError(
"Your Jira Service Management token does not have sufficient permissions for this configuration (HTTP 403)."
)
elif status_code == 429:
raise ConnectorValidationError(
"Validation failed due to Jira Service Management rate-limits being exceeded. Please try again later."
)
# Try to extract original error message from the response
error_message = getattr(e, "text", None)
if error_message is None:
raise UnexpectedValidationError(
f"Unexpected Jira Service Management error during validation: {e}"
)
raise ConnectorValidationError(
f"Validation failed due to Jira Service Management error: {error_message}"
)
@override
def validate_checkpoint_json(self, checkpoint_json: str) -> JiraConnectorCheckpoint:
return JiraConnectorCheckpoint.model_validate_json(checkpoint_json)
@override
def build_dummy_checkpoint(self) -> JiraConnectorCheckpoint:
return JiraConnectorCheckpoint(
has_more=True,
)

View File

@@ -23,6 +23,8 @@ def source_to_github_img_link(source: DocumentSource) -> str | None:
return "https://raw.githubusercontent.com/onyx-dot-app/onyx/main/backend/slackbot_images/Confluence.png"
if source == DocumentSource.JIRA.value:
return "https://raw.githubusercontent.com/onyx-dot-app/onyx/main/backend/slackbot_images/Jira.png"
if source == DocumentSource.JIRA_SERVICE_MANAGEMENT.value:
return "https://raw.githubusercontent.com/onyx-dot-app/onyx/main/backend/slackbot_images/JiraServiceManagement.png"
if source == DocumentSource.NOTION.value:
return "https://raw.githubusercontent.com/onyx-dot-app/onyx/main/web/public/Notion.png"
if source == DocumentSource.ZENDESK.value:

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

9
web/package-lock.json generated
View File

@@ -7964,6 +7964,7 @@
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
"integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
"dev": true,
"funding": [
{
"type": "github",
@@ -10961,6 +10962,7 @@
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
"dev": true,
"engines": {
"node": ">=0.8.19"
}
@@ -19187,7 +19189,8 @@
"node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"dev": true
},
"node_modules/simple-swizzle": {
"version": "0.2.2",
@@ -20081,7 +20084,8 @@
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true
},
"node_modules/thenify": {
"version": "3.3.1",
@@ -21341,6 +21345,7 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz",
"integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==",
"dev": true,
"dependencies": {
"imurmurhash": "^0.1.4",
"signal-exit": "^3.0.7"

View File

@@ -0,0 +1 @@
<svg width="418" height="419" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" overflow="hidden"><defs><clipPath id="clip0"><rect x="384" y="424" width="418" height="419"/></clipPath><linearGradient x1="234.002" y1="188.613" x2="269.859" y2="297.007" gradientUnits="userSpaceOnUse" spreadMethod="pad" id="fill1"><stop offset="0" stop-color="#0052CC"/><stop offset="1" stop-color="#2684FF"/></linearGradient></defs><g clip-path="url(#clip0)" transform="translate(-384 -424)"><path d="M237.245 175.859 322.338 176.307C333.086 176.307 339.356 188.847 333.086 197.804L207.239 365.75C167.827 336.192 159.766 280.21 189.324 240.35L237.245 175.859Z" fill="url(#fill1)" transform="matrix(1 0 0 1.00239 384 424)"/><path d="M189.327 240.35 104.681 239.902C93.9328 239.902 87.663 227.362 93.933 218.853L218.438 52.25C257.849 81.8086 265.909 137.791 236.352 177.65L189.327 240.35Z" fill="#2684FF" transform="matrix(1 0 0 1.00239 384 424)"/></g></svg>

After

Width:  |  Height:  |  Size: 988 B

View File

@@ -67,10 +67,12 @@ export const ConnectorTitle = ({
}
} else if (connector.source === "jira") {
const typedConnector = connector as Connector<JiraConfig>;
additionalMetadata.set(
"Jira Project URL",
typedConnector.connector_specific_config.jira_project_url
);
if (typedConnector.connector_specific_config.project_key) {
additionalMetadata.set(
"Jira Project Key",
typedConnector.connector_specific_config.project_key
);
}
} else if (connector.source === "slack") {
const typedConnector = connector as Connector<SlackConfig>;
if (

View File

@@ -36,6 +36,7 @@ import { SiBookstack } from "react-icons/si";
import { StaticImageData } from "next/image";
import outlinePNG from "../../../public/Outline.png";
import jiraSVG from "../../../public/Jira.svg";
import jiraServiceManagementSVG from "../../../public/JiraServiceManagement.svg";
import confluenceSVG from "../../../public/Confluence.svg";
import deepseekSVG from "../../../public/Deepseek.svg";
import openAISVG from "../../../public/Openai.svg";
@@ -1304,6 +1305,17 @@ export const JiraIcon = ({
<LogoIcon size={size + 4} className={`${className} -m-0.5`} src={jiraSVG} />
);
export const JiraServiceManagementIcon = ({
size = 16,
className = defaultTailwindCSS,
}: IconProps) => (
<LogoIcon
size={size + 4}
className={`${className} -m-0.5`}
src={jiraServiceManagementSVG}
/>
);
export const ZulipIcon = ({
size = 16,
className = defaultTailwindCSS,

View File

@@ -616,6 +616,83 @@ export const connectorConfigs: Record<
],
advanced_values: [],
},
jira_service_management: {
description: "Configure Jira Service Management connector",
subtext: `Configure which Jira Service Management content to index. You can index everything or specify a particular project.`,
values: [
{
type: "text",
query: "Enter the Jira Service Management base URL:",
label: "Jira Service Management Base URL",
name: "jira_service_management_base_url",
optional: false,
description:
"The base URL of your Jira Service Management instance (e.g., https://your-domain.atlassian.net)",
},
{
type: "tab",
name: "indexing_scope",
label: "How Should We Index Your Jira Service Management?",
optional: true,
tabs: [
{
value: "everything",
label: "Everything",
fields: [
{
type: "string_tab",
label: "Everything",
name: "everything",
description:
"This connector will index all JSM tickets and requests from service desk projects the provided credentials have access to!",
},
],
},
{
value: "project",
label: "Project",
fields: [
{
type: "text",
query: "Enter the project key:",
label: "Project Key",
name: "project_key",
description:
"The key of a specific service management project to index (e.g., 'SERVICEDESK').",
},
],
},
{
value: "jql",
label: "JQL Query",
fields: [
{
type: "text",
query: "Enter the JQL query:",
label: "JQL Query",
name: "jql_query",
description:
"A custom JQL query to filter Jira Service Management issues." +
"\n\nIMPORTANT: Do not include any time-based filters in the JQL query as that will conflict with the connector's logic. Additionally, do not include ORDER BY clauses." +
"\n\nSee Atlassian's [JQL documentation](https://support.atlassian.com/jira-software-cloud/docs/advanced-search-reference-jql-fields/) for more details on syntax.",
},
],
},
],
defaultTab: "everything",
},
{
type: "list",
query: "Enter email addresses to blacklist from comments:",
label: "Comment Email Blacklist",
name: "comment_email_blacklist",
description:
"This is generally useful to ignore certain bots. Add user emails which comments should NOT be indexed.",
optional: true,
},
],
advanced_values: [],
},
salesforce: {
description: "Configure Salesforce connector",
values: [
@@ -1606,7 +1683,14 @@ export interface ConfluenceConfig {
}
export interface JiraConfig {
jira_project_url: string;
jira_base_url: string;
project_key?: string;
comment_email_blacklist?: string[];
jql_query?: string;
}
export interface JiraServiceManagementConfig {
jira_service_management_base_url: string;
project_key?: string;
comment_email_blacklist?: string[];
jql_query?: string;

View File

@@ -82,6 +82,11 @@ export interface JiraServerCredentialJson {
jira_api_token: string;
}
export interface JiraServiceManagementCredentialJson {
jira_service_management_email: string | null;
jira_service_management_api_token: string;
}
export interface ProductboardCredentialJson {
productboard_access_token: string;
}
@@ -286,6 +291,10 @@ export const credentialTemplates: Record<ValidSources, any> = {
jira_user_email: null,
jira_api_token: "",
} as JiraCredentialJson,
jira_service_management: {
jira_service_management_email: null,
jira_service_management_api_token: "",
} as JiraServiceManagementCredentialJson,
productboard: { productboard_access_token: "" } as ProductboardCredentialJson,
slab: { slab_bot_token: "" } as SlabCredentialJson,
notion: { notion_integration_token: "" } as NotionCredentialJson,
@@ -482,6 +491,12 @@ export const credentialDisplayNames: Record<string, string> = {
jira_user_email: "Jira User Email (required for Jira Cloud)",
jira_api_token: "API or Personal Access Token",
// Jira Service Management
jira_service_management_email:
"Jira Service Management User Email (required for Jira Cloud)",
jira_service_management_api_token:
"Jira Service Management API or Personal Access Token",
// Productboard
productboard_access_token: "Productboard Access Token",

View File

@@ -17,6 +17,7 @@ import {
GuruIcon,
HubSpotIcon,
JiraIcon,
JiraServiceManagementIcon,
LinearIcon,
LoopioIcon,
NotionIcon,
@@ -227,6 +228,12 @@ export const SOURCE_METADATA_MAP: SourceMap = {
docs: "https://docs.onyx.app/admin/connectors/official/jira",
isPopular: true,
},
jira_service_management: {
icon: JiraServiceManagementIcon,
displayName: "Jira SM",
category: SourceCategory.TicketingAndTaskManagement,
docs: "https://docs.onyx.app/admin/connectors/official/jira_service_management",
},
zendesk: {
icon: ZendeskIcon,
displayName: "Zendesk",

View File

@@ -444,6 +444,7 @@ export enum ValidSources {
Outline = "outline",
Confluence = "confluence",
Jira = "jira",
JiraServiceManagement = "jira_service_management",
Productboard = "productboard",
Slab = "slab",
Notion = "notion",
@@ -499,6 +500,7 @@ export const federatedSourceToRegularSource = (
export const validAutoSyncSources = [
ValidSources.Confluence,
ValidSources.Jira,
ValidSources.JiraServiceManagement,
ValidSources.GoogleDrive,
ValidSources.Gmail,
ValidSources.Slack,