diff --git a/src/sentry/static/sentry/images/logos/logo-perforce.svg b/src/sentry/static/sentry/images/logos/logo-perforce.svg new file mode 100644 index 00000000000000..eb8c0c234101f5 --- /dev/null +++ b/src/sentry/static/sentry/images/logos/logo-perforce.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/static/app/icons/iconPerforce.tsx b/static/app/icons/iconPerforce.tsx new file mode 100644 index 00000000000000..73512b9bba1766 --- /dev/null +++ b/static/app/icons/iconPerforce.tsx @@ -0,0 +1,10 @@ +import type {SVGIconProps} from './svgIcon'; +import {SvgIcon} from './svgIcon'; + +export function IconPerforce(props: SVGIconProps) { + return ( + + + + ); +} diff --git a/static/app/icons/index.tsx b/static/app/icons/index.tsx index db9dbe8842decf..5f39dbc9541ec8 100644 --- a/static/app/icons/index.tsx +++ b/static/app/icons/index.tsx @@ -85,6 +85,7 @@ export {IconNumber} from './iconNumber'; export {IconOpen} from './iconOpen'; export {IconPanel} from './iconPanel'; export {IconPause} from './iconPause'; +export {IconPerforce} from './iconPerforce'; export {IconPin} from './iconPin'; export {IconPlay} from './iconPlay'; export {IconPrevent} from './iconPrevent'; diff --git a/static/app/plugins/components/pluginIcon.tsx b/static/app/plugins/components/pluginIcon.tsx index 8736193dc69520..b413d24a72d0e4 100644 --- a/static/app/plugins/components/pluginIcon.tsx +++ b/static/app/plugins/components/pluginIcon.tsx @@ -17,6 +17,7 @@ import jumpcloud from 'sentry-logos/logo-jumpcloud.svg'; import msteams from 'sentry-logos/logo-msteams.svg'; import opsgenie from 'sentry-logos/logo-opsgenie.svg'; import pagerduty from 'sentry-logos/logo-pagerduty.svg'; +import perforce from 'sentry-logos/logo-perforce.svg'; import pivotal from 'sentry-logos/logo-pivotaltracker.svg'; import pushover from 'sentry-logos/logo-pushover.svg'; import redmine from 'sentry-logos/logo-redmine.svg'; @@ -57,6 +58,7 @@ const PLUGIN_ICONS = { msteams, opsgenie, pagerduty, + perforce, pivotal, pushover, redmine, diff --git a/static/app/types/integrations.tsx b/static/app/types/integrations.tsx index 0f47431aa62efa..97582eaa517da6 100644 --- a/static/app/types/integrations.tsx +++ b/static/app/types/integrations.tsx @@ -577,7 +577,7 @@ export type CodeOwner = { users_without_access: string[]; }; id: string; - provider: 'github' | 'gitlab'; + provider: 'github' | 'gitlab' | 'perforce'; raw: string; codeMapping?: RepositoryProjectPathConfig; ownershipSyntax?: string; diff --git a/static/app/utils/integrationUtil.tsx b/static/app/utils/integrationUtil.tsx index b71d10a168f486..1f7462cbfca238 100644 --- a/static/app/utils/integrationUtil.tsx +++ b/static/app/utils/integrationUtil.tsx @@ -9,6 +9,7 @@ import { IconGithub, IconGitlab, IconJira, + IconPerforce, IconSentry, IconVsts, } from 'sentry/icons'; @@ -206,6 +207,8 @@ export const getIntegrationIcon = ( case 'jira': case 'jira_server': return ; + case 'perforce': + return ; case 'vsts': return ; case 'codecov': @@ -230,6 +233,8 @@ export const getIntegrationDisplayName = (integrationType?: string) => { case 'jira': case 'jira_server': return 'Jira'; + case 'perforce': + return 'Perforce'; case 'vsts': return 'Azure DevOps'; case 'codecov': @@ -279,6 +284,8 @@ export function getCodeOwnerIcon( return ; case 'gitlab': return ; + case 'perforce': + return ; default: return ; } diff --git a/static/app/views/settings/organizationIntegrations/repositoryProjectPathConfigForm.tsx b/static/app/views/settings/organizationIntegrations/repositoryProjectPathConfigForm.tsx index 7f3f322e1ca036..ca42e505d1fe02 100644 --- a/static/app/views/settings/organizationIntegrations/repositoryProjectPathConfigForm.tsx +++ b/static/app/views/settings/organizationIntegrations/repositoryProjectPathConfigForm.tsx @@ -58,6 +58,10 @@ function RepositoryProjectPathConfigForm({ } ); + // Stream-based VCS (like Perforce) use streams/codelines instead of branches + // and don't require a default branch to be specified + const isStreamBased = integration.provider.key === 'perforce'; + // Effect to handle the case when integration repos data becomes available useEffect(() => { if (integrationReposData?.repos && selectedRepo) { @@ -93,13 +97,19 @@ function RepositoryProjectPathConfigForm({ { name: 'defaultBranch', type: 'string', - required: true, - label: t('Branch'), - placeholder: t('Type your branch'), + required: !isStreamBased, + label: isStreamBased ? t('Stream') : t('Branch'), + placeholder: isStreamBased + ? t('Type your stream (optional, e.g., main)') + : t('Type your branch'), showHelpInTooltip: true, - help: t( - 'If an event does not have a release tied to a commit, we will use this branch when linking to your source code.' - ), + help: isStreamBased + ? t( + 'Optional: Specify a stream/codeline (e.g., "main"). If not specified, the depot root will be used. Streams are part of the depot path in Perforce.' + ) + : t( + 'If an event does not have a release tied to a commit, we will use this branch when linking to your source code.' + ), }, { name: 'stackRoot', @@ -135,7 +145,7 @@ function RepositoryProjectPathConfigForm({ } const initialData = { - defaultBranch: 'main', + defaultBranch: isStreamBased ? '' : 'main', stackRoot: '', sourceRoot: '', repositoryId: existingConfig?.repoId, diff --git a/static/images/integrations/perforce.svg b/static/images/integrations/perforce.svg new file mode 100644 index 00000000000000..eb8c0c234101f5 --- /dev/null +++ b/static/images/integrations/perforce.svg @@ -0,0 +1,5 @@ + + + + +