mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
326 lines
9.4 KiB
TypeScript
326 lines
9.4 KiB
TypeScript
import styled from '@emotion/styled';
|
|
|
|
import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
|
|
import {openModal} from 'sentry/actionCreators/modal';
|
|
import {
|
|
installSentryApp,
|
|
uninstallSentryApp,
|
|
} from 'sentry/actionCreators/sentryAppInstallations';
|
|
import {Button} from 'sentry/components/button';
|
|
import CircleIndicator from 'sentry/components/circleIndicator';
|
|
import Confirm from 'sentry/components/confirm';
|
|
import type DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent';
|
|
import SentryAppIcon from 'sentry/components/sentryAppIcon';
|
|
import {IconSubtract} from 'sentry/icons';
|
|
import {t, tct} from 'sentry/locale';
|
|
import {space} from 'sentry/styles/space';
|
|
import type {IntegrationFeature, SentryApp, SentryAppInstallation} from 'sentry/types';
|
|
import {toPermissions} from 'sentry/utils/consolidatedScopes';
|
|
import {getSentryAppInstallStatus} from 'sentry/utils/integrationUtil';
|
|
import {addQueryParamsToExistingUrl} from 'sentry/utils/queryString';
|
|
import {recordInteraction} from 'sentry/utils/recordSentryAppInteraction';
|
|
import withOrganization from 'sentry/utils/withOrganization';
|
|
|
|
import AbstractIntegrationDetailedView from './abstractIntegrationDetailedView';
|
|
import {SplitInstallationIdModal} from './SplitInstallationIdModal';
|
|
|
|
type State = {
|
|
appInstalls: SentryAppInstallation[];
|
|
featureData: IntegrationFeature[];
|
|
sentryApp: SentryApp;
|
|
};
|
|
|
|
type Tab = AbstractIntegrationDetailedView['state']['tab'];
|
|
|
|
class SentryAppDetailedView extends AbstractIntegrationDetailedView<
|
|
AbstractIntegrationDetailedView['props'],
|
|
State & AbstractIntegrationDetailedView['state']
|
|
> {
|
|
tabs: Tab[] = ['overview'];
|
|
getEndpoints(): ReturnType<DeprecatedAsyncComponent['getEndpoints']> {
|
|
const {
|
|
organization,
|
|
params: {integrationSlug},
|
|
} = this.props;
|
|
return [
|
|
['sentryApp', `/sentry-apps/${integrationSlug}/`],
|
|
['featureData', `/sentry-apps/${integrationSlug}/features/`],
|
|
['appInstalls', `/organizations/${organization.slug}/sentry-app-installations/`],
|
|
];
|
|
}
|
|
|
|
onLoadAllEndpointsSuccess() {
|
|
const {
|
|
organization,
|
|
params: {integrationSlug},
|
|
router,
|
|
} = this.props;
|
|
|
|
// redirect for internal integrations
|
|
if (this.sentryApp.status === 'internal') {
|
|
router.push(
|
|
`/settings/${organization.slug}/developer-settings/${integrationSlug}/`
|
|
);
|
|
return;
|
|
}
|
|
|
|
super.onLoadAllEndpointsSuccess();
|
|
recordInteraction(integrationSlug, 'sentry_app_viewed');
|
|
}
|
|
|
|
get integrationType() {
|
|
return 'sentry_app' as const;
|
|
}
|
|
|
|
get sentryApp() {
|
|
return this.state.sentryApp;
|
|
}
|
|
|
|
get description() {
|
|
return this.state.sentryApp.overview || '';
|
|
}
|
|
|
|
get author() {
|
|
return this.sentryApp.author;
|
|
}
|
|
|
|
get resourceLinks() {
|
|
// only show links for published sentry apps
|
|
if (this.sentryApp.status !== 'published') {
|
|
return [];
|
|
}
|
|
return [
|
|
{
|
|
title: 'Documentation',
|
|
url: `https://docs.sentry.io/product/integrations/${this.integrationSlug}/`,
|
|
},
|
|
];
|
|
}
|
|
|
|
get permissions() {
|
|
return toPermissions(this.sentryApp.scopes);
|
|
}
|
|
|
|
get installationStatus() {
|
|
return getSentryAppInstallStatus(this.install);
|
|
}
|
|
|
|
get integrationName() {
|
|
return this.sentryApp.name;
|
|
}
|
|
|
|
get featureData() {
|
|
return this.state.featureData;
|
|
}
|
|
|
|
get install() {
|
|
return this.state.appInstalls.find(i => i.app.slug === this.sentryApp.slug);
|
|
}
|
|
|
|
redirectUser = (install: SentryAppInstallation) => {
|
|
const {organization} = this.props;
|
|
const {sentryApp} = this.state;
|
|
const queryParams = {
|
|
installationId: install.uuid,
|
|
code: install.code,
|
|
orgSlug: organization.slug,
|
|
};
|
|
if (sentryApp.redirectUrl) {
|
|
const redirectUrl = addQueryParamsToExistingUrl(sentryApp.redirectUrl, queryParams);
|
|
window.location.assign(redirectUrl);
|
|
}
|
|
};
|
|
|
|
handleInstall = async () => {
|
|
const {organization} = this.props;
|
|
const {sentryApp} = this.state;
|
|
this.trackIntegrationAnalytics('integrations.installation_start', {
|
|
integration_status: sentryApp.status,
|
|
});
|
|
// installSentryApp adds a message on failure
|
|
const install = await installSentryApp(this.api, organization.slug, sentryApp);
|
|
|
|
// installation is complete if the status is installed
|
|
if (install.status === 'installed') {
|
|
this.trackIntegrationAnalytics('integrations.installation_complete', {
|
|
integration_status: sentryApp.status,
|
|
});
|
|
}
|
|
|
|
if (!sentryApp.redirectUrl) {
|
|
addSuccessMessage(t('%s successfully installed.', sentryApp.slug));
|
|
this.setState({appInstalls: [install, ...this.state.appInstalls]});
|
|
|
|
// hack for split so we can show the install ID to users for them to copy
|
|
// Will remove once the proper fix is in place
|
|
if (['split', 'split-dev', 'split-testing'].includes(sentryApp.slug)) {
|
|
openModal(({closeModal}) => (
|
|
<SplitInstallationIdModal
|
|
installationId={install.uuid}
|
|
closeModal={closeModal}
|
|
/>
|
|
));
|
|
}
|
|
} else {
|
|
this.redirectUser(install);
|
|
}
|
|
};
|
|
|
|
handleUninstall = async (install: SentryAppInstallation) => {
|
|
try {
|
|
await uninstallSentryApp(this.api, install);
|
|
this.trackIntegrationAnalytics('integrations.uninstall_completed', {
|
|
integration_status: this.sentryApp.status,
|
|
});
|
|
const appInstalls = this.state.appInstalls.filter(
|
|
i => i.app.slug !== this.sentryApp.slug
|
|
);
|
|
return this.setState({appInstalls});
|
|
} catch (error) {
|
|
return addErrorMessage(t('Unable to uninstall %s', this.sentryApp.name));
|
|
}
|
|
};
|
|
|
|
recordUninstallClicked = () => {
|
|
const sentryApp = this.sentryApp;
|
|
this.trackIntegrationAnalytics('integrations.uninstall_clicked', {
|
|
integration_status: sentryApp.status,
|
|
});
|
|
};
|
|
|
|
renderPermissions() {
|
|
const permissions = this.permissions;
|
|
if (!Object.keys(permissions).some(scope => permissions[scope].length > 0)) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<PermissionWrapper>
|
|
<Title>Permissions</Title>
|
|
{permissions.read.length > 0 && (
|
|
<Permission>
|
|
<Indicator />
|
|
<Text key="read">
|
|
{tct('[read] access to [resources] resources', {
|
|
read: <strong>Read</strong>,
|
|
resources: permissions.read.join(', '),
|
|
})}
|
|
</Text>
|
|
</Permission>
|
|
)}
|
|
{permissions.write.length > 0 && (
|
|
<Permission>
|
|
<Indicator />
|
|
<Text key="write">
|
|
{tct('[read] and [write] access to [resources] resources', {
|
|
read: <strong>Read</strong>,
|
|
write: <strong>Write</strong>,
|
|
resources: permissions.write.join(', '),
|
|
})}
|
|
</Text>
|
|
</Permission>
|
|
)}
|
|
{permissions.admin.length > 0 && (
|
|
<Permission>
|
|
<Indicator />
|
|
<Text key="admin">
|
|
{tct('[admin] access to [resources] resources', {
|
|
admin: <strong>Admin</strong>,
|
|
resources: permissions.admin.join(', '),
|
|
})}
|
|
</Text>
|
|
</Permission>
|
|
)}
|
|
</PermissionWrapper>
|
|
);
|
|
}
|
|
|
|
renderTopButton(disabledFromFeatures: boolean, userHasAccess: boolean) {
|
|
const install = this.install;
|
|
const capitalizedSlug =
|
|
this.integrationSlug.charAt(0).toUpperCase() + this.integrationSlug.slice(1);
|
|
if (install) {
|
|
return (
|
|
<Confirm
|
|
disabled={!userHasAccess}
|
|
message={tct('Are you sure you want to uninstall the [slug] installation?', {
|
|
slug: capitalizedSlug,
|
|
})}
|
|
onConfirm={() => this.handleUninstall(install)} // called when the user confirms the action
|
|
onConfirming={this.recordUninstallClicked} // called when the confirm modal opens
|
|
priority="danger"
|
|
>
|
|
<StyledUninstallButton size="sm" data-test-id="sentry-app-uninstall">
|
|
<IconSubtract isCircled style={{marginRight: space(0.75)}} />
|
|
{t('Uninstall')}
|
|
</StyledUninstallButton>
|
|
</Confirm>
|
|
);
|
|
}
|
|
|
|
if (userHasAccess) {
|
|
return (
|
|
<InstallButton
|
|
data-test-id="install-button"
|
|
disabled={disabledFromFeatures}
|
|
onClick={() => this.handleInstall()}
|
|
priority="primary"
|
|
size="sm"
|
|
style={{marginLeft: space(1)}}
|
|
>
|
|
{t('Accept & Install')}
|
|
</InstallButton>
|
|
);
|
|
}
|
|
|
|
return this.renderRequestIntegrationButton();
|
|
}
|
|
|
|
// no configurations for sentry apps
|
|
renderConfigurations() {
|
|
return null;
|
|
}
|
|
|
|
renderIntegrationIcon() {
|
|
return <SentryAppIcon sentryApp={this.sentryApp} size={50} />;
|
|
}
|
|
}
|
|
|
|
const Text = styled('p')`
|
|
margin: 0px 6px;
|
|
`;
|
|
|
|
const Permission = styled('div')`
|
|
display: flex;
|
|
`;
|
|
|
|
const PermissionWrapper = styled('div')`
|
|
padding-bottom: ${space(2)};
|
|
`;
|
|
|
|
const Title = styled('p')`
|
|
margin-bottom: ${space(1)};
|
|
font-weight: bold;
|
|
`;
|
|
|
|
const Indicator = styled(p => <CircleIndicator size={7} {...p} />)`
|
|
align-self: center;
|
|
color: ${p => p.theme.success};
|
|
`;
|
|
|
|
const InstallButton = styled(Button)`
|
|
margin-left: ${space(1)};
|
|
`;
|
|
|
|
const StyledUninstallButton = styled(Button)`
|
|
color: ${p => p.theme.gray300};
|
|
background: ${p => p.theme.background};
|
|
|
|
border: ${p => `1px solid ${p.theme.gray300}`};
|
|
box-sizing: border-box;
|
|
box-shadow: 0px 2px 1px rgba(0, 0, 0, 0.08);
|
|
border-radius: 4px;
|
|
`;
|
|
|
|
export default withOrganization(SentryAppDetailedView);
|