Files
zeekayandHanzo Dev f888991a7a drop the Sentry wordmark from the react app
FSL-1.0-Apache-2.0 grants us the code, not the name. "Sentry" is
Functional Software's trademark, so every place the running app says it
to a user -- page titles, onboarding, empty states, toasts, settings
copy, the self-hosted footer -- now says Sentinel.

Attribution is the other half of the deal and stays untouched: upstream
copyright headers, LICENSE, NOTICE. So does anything where the mark is a
fact rather than a brand -- @sentry/* packages, sentry-cli, the wizard
and bundler plugins, docs.sentry.io, the whole gettingStartedDocs tree.
Renaming those would leave instructions that no longer work.

Nothing structural moved. No import, identifier, route path, feature
flag, API field or test id changed; the diff is 312 lines in and 312
out. Colocated specs were updated in lockstep with the copy they assert.
Marketing and community links point at hanzo.ai, only where a real page
exists to point at.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-10 18:01:22 -07:00

431 lines
12 KiB
TypeScript

import {Fragment} from 'react';
import styled from '@emotion/styled';
import {addErrorMessage} from 'sentry/actionCreators/indicator';
import type {RequestOptions} from 'sentry/api';
import {Alert} from 'sentry/components/alert';
import {Button} from 'sentry/components/button';
import type DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent';
import Form from 'sentry/components/forms/form';
import JsonForm from 'sentry/components/forms/jsonForm';
import type {JsonFormObject} from 'sentry/components/forms/types';
import HookOrDefault from 'sentry/components/hookOrDefault';
import Panel from 'sentry/components/panels/panel';
import PanelItem from 'sentry/components/panels/panelItem';
import {IconOpen} from 'sentry/icons';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {Integration, IntegrationProvider, ObjectStatus} from 'sentry/types';
import {getAlertText, getIntegrationStatus} from 'sentry/utils/integrationUtil';
import {normalizeUrl} from 'sentry/utils/withDomainRequired';
import withOrganization from 'sentry/utils/withOrganization';
import BreadcrumbTitle from 'sentry/views/settings/components/settingsBreadcrumb/breadcrumbTitle';
import type {Tab} from './abstractIntegrationDetailedView';
import AbstractIntegrationDetailedView from './abstractIntegrationDetailedView';
import {AddIntegrationButton} from './addIntegrationButton';
import InstalledIntegration from './installedIntegration';
// Show the features tab if the org has features for the integration
const integrationFeatures = ['github'];
const FirstPartyIntegrationAlert = HookOrDefault({
hookName: 'component:first-party-integration-alert',
defaultComponent: () => null,
});
const FirstPartyIntegrationAdditionalCTA = HookOrDefault({
hookName: 'component:first-party-integration-additional-cta',
defaultComponent: () => null,
});
type State = {
configurations: Integration[];
information: {providers: IntegrationProvider[]};
};
class IntegrationDetailedView extends AbstractIntegrationDetailedView<
AbstractIntegrationDetailedView['props'],
State & AbstractIntegrationDetailedView['state']
> {
tabs: Tab[] = ['overview', 'configurations', 'features'];
getEndpoints(): ReturnType<DeprecatedAsyncComponent['getEndpoints']> {
const {organization} = this.props;
const {integrationSlug} = this.props.params;
return [
[
'information',
`/organizations/${organization.slug}/config/integrations/?provider_key=${integrationSlug}`,
],
[
'configurations',
`/organizations/${organization.slug}/integrations/?provider_key=${integrationSlug}&includeConfig=0`,
],
];
}
get integrationType() {
return 'first_party' as const;
}
get provider() {
return this.state.information.providers[0];
}
get description() {
return this.metadata.description;
}
get author() {
return this.metadata.author;
}
get alerts() {
const provider = this.provider;
const metadata = this.metadata;
// The server response for integration installations includes old icon CSS classes
// We map those to the currently in use values to their react equivalents
// and fallback to IconFlag just in case.
const alerts = (metadata.aspects.alerts || []).map(item => ({
...item,
showIcon: true,
}));
if (!provider.canAdd && metadata.aspects.externalInstall) {
alerts.push({
type: 'warning',
showIcon: true,
text: metadata.aspects.externalInstall.noticeText,
});
}
return alerts;
}
get resourceLinks() {
const metadata = this.metadata;
return [
{url: metadata.source_url, title: 'View Source'},
{url: metadata.issue_url, title: 'Report Issue'},
];
}
get metadata() {
return this.provider.metadata;
}
get isEnabled() {
return this.state.configurations.length > 0;
}
get installationStatus() {
// TODO: add transations
const {configurations} = this.state;
const statusList = configurations.map(getIntegrationStatus);
// if we have conflicting statuses, we have a priority order
if (statusList.includes('active')) {
return 'Installed';
}
if (statusList.includes('disabled')) {
return 'Disabled';
}
if (statusList.includes('pending_deletion')) {
return 'Pending Deletion';
}
return 'Not Installed';
}
get integrationName() {
return this.provider.name;
}
get featureData() {
return this.metadata.features;
}
renderTabs() {
// TODO: Convert to styled component
const tabs = integrationFeatures.includes(this.provider.key)
? this.tabs
: this.tabs.filter(tab => tab !== 'features');
return (
<ul className="nav nav-tabs border-bottom" style={{paddingTop: '30px'}}>
{tabs.map(tabName => (
<li
key={tabName}
className={this.state.tab === tabName ? 'active' : ''}
onClick={() => this.onTabChange(tabName)}
>
<CapitalizedLink>{this.getTabDisplay(tabName)}</CapitalizedLink>
</li>
))}
</ul>
);
}
onInstall = (integration: Integration) => {
// send the user to the configure integration view for that integration
const {organization} = this.props;
this.props.router.push(
normalizeUrl(
`/settings/${organization.slug}/integrations/${integration.provider.key}/${integration.id}/`
)
);
};
onRemove = (integration: Integration) => {
const {organization} = this.props;
const origIntegrations = [...this.state.configurations];
const integrations = this.state.configurations.map(i =>
i.id === integration.id
? {...i, organizationIntegrationStatus: 'pending_deletion' as ObjectStatus}
: i
);
this.setState({configurations: integrations});
const options: RequestOptions = {
method: 'DELETE',
error: () => {
this.setState({configurations: origIntegrations});
addErrorMessage(t('Failed to remove Integration'));
},
};
this.api.request(
`/organizations/${organization.slug}/integrations/${integration.id}/`,
options
);
};
onDisable = (integration: Integration) => {
let url: string;
if (!integration.domainName) {
return;
}
const [domainName, orgName] = integration.domainName.split('/');
if (integration.accountType === 'User') {
url = `https://${domainName}/settings/installations/`;
} else {
url = `https://${domainName}/organizations/${orgName}/settings/installations/`;
}
window.open(url, '_blank');
};
handleExternalInstall = () => {
this.trackIntegrationAnalytics('integrations.installation_start');
};
renderAlert() {
return (
<FirstPartyIntegrationAlert
integrations={this.state.configurations ?? []}
hideCTA
/>
);
}
renderAdditionalCTA() {
return (
<FirstPartyIntegrationAdditionalCTA
integrations={this.state.configurations ?? []}
/>
);
}
renderTopButton(disabledFromFeatures: boolean, userHasAccess: boolean) {
const {organization} = this.props;
const provider = this.provider;
const {metadata} = provider;
const size = 'sm' as const;
const priority = 'primary' as const;
const buttonProps = {
style: {marginBottom: space(1)},
size,
priority,
'data-test-id': 'install-button',
disabled: disabledFromFeatures,
organization,
};
if (!userHasAccess) {
return this.renderRequestIntegrationButton();
}
if (provider.canAdd) {
return (
<AddIntegrationButton
provider={provider}
onAddIntegration={this.onInstall}
installStatus={this.installationStatus}
analyticsParams={{
view: 'integrations_directory_integration_detail',
already_installed: this.installationStatus !== 'Not Installed',
}}
{...buttonProps}
/>
);
}
if (metadata.aspects.externalInstall) {
return (
<Button
icon={<IconOpen />}
href={metadata.aspects.externalInstall.url}
onClick={this.handleExternalInstall}
external
{...buttonProps}
>
{metadata.aspects.externalInstall.buttonText}
</Button>
);
}
// This should never happen but we can't return undefined without some refactoring.
return <Fragment />;
}
renderConfigurations() {
const {configurations} = this.state;
const {organization} = this.props;
const provider = this.provider;
if (!configurations.length) {
return this.renderEmptyConfigurations();
}
const alertText = getAlertText(configurations);
return (
<Fragment>
{alertText && (
<Alert type="warning" showIcon>
{alertText}
</Alert>
)}
<Panel>
{configurations.map(integration => (
<PanelItem key={integration.id}>
<InstalledIntegration
organization={organization}
provider={provider}
integration={integration}
onRemove={this.onRemove}
onDisable={this.onDisable}
data-test-id={integration.id}
trackIntegrationAnalytics={this.trackIntegrationAnalytics}
requiresUpgrade={!!alertText}
/>
</PanelItem>
))}
</Panel>
</Fragment>
);
}
renderFeatures() {
const {configurations} = this.state;
const {organization} = this.props;
const hasIntegration = configurations ? configurations.length > 0 : false;
const endpoint = `/organizations/${organization.slug}/`;
const hasOrgWrite = organization.access.includes('org:write');
const forms: JsonFormObject[] = [
{
fields: [
{
name: 'githubPRBot',
type: 'boolean',
label: t('Enable Comments on Suspect Pull Requests'),
help: t(
'Allow Sentinel to comment on recent pull requests suspected of causing issues.'
),
disabled: !hasIntegration,
disabledReason: t(
'You must have a GitHub integration to enable this feature.'
),
},
{
name: 'githubOpenPRBot',
type: 'boolean',
label: t('Enable Comments on Open Pull Requests'),
help: t(
'Allow Sentinel to comment on open pull requests to show recent error issues for the code being changed.'
),
disabled: !hasIntegration,
disabledReason: t(
'You must have a GitHub integration to enable this feature.'
),
visible: ({features}) => features.includes('integrations-open-pr-comment'),
},
{
name: 'githubNudgeInvite',
type: 'boolean',
label: t('Enable Missing Member Detection'),
help: t(
'Allow Sentinel to detect users committing to your GitHub repositories that are not part of your Sentinel organization..'
),
disabled: !hasIntegration,
disabledReason: t(
'You must have a GitHub integration to enable this feature.'
),
visible: ({features}) => features.includes('integrations-gh-invite'),
},
],
},
];
const initialData = {
githubPRBot: organization.githubPRBot,
githubOpenPRBot: organization.githubOpenPRBot,
githubNudgeInvite: organization.githubNudgeInvite,
};
return (
<Form
apiMethod="PUT"
apiEndpoint={endpoint}
saveOnBlur
allowUndo
initialData={initialData}
onSubmitError={() => addErrorMessage('Unable to save change')}
>
<JsonForm
disabled={!hasOrgWrite}
features={organization.features}
forms={forms}
/>
</Form>
);
}
renderBody() {
return (
<Fragment>
<BreadcrumbTitle routes={this.props.routes} title={this.integrationName} />
{this.renderAlert()}
{this.renderTopSection()}
{this.renderTabs()}
{this.state.tab === 'overview'
? this.renderInformationCard()
: this.state.tab === 'configurations'
? this.renderConfigurations()
: this.renderFeatures()}
</Fragment>
);
}
}
export default withOrganization(IntegrationDetailedView);
const CapitalizedLink = styled('a')`
text-transform: capitalize;
`;