import {Fragment} from 'react'; import {RouteComponentProps} from 'react-router'; import styled from '@emotion/styled'; import startCase from 'lodash/startCase'; import Access from 'sentry/components/acl/access'; import {Alert, AlertProps} from 'sentry/components/alert'; import DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent'; import EmptyMessage from 'sentry/components/emptyMessage'; import ExternalLink from 'sentry/components/links/externalLink'; import Panel from 'sentry/components/panels/panel'; import Tag from 'sentry/components/tag'; import {Tooltip} from 'sentry/components/tooltip'; import {IconClose, IconDocs, IconGeneric, IconGithub, IconProject} from 'sentry/icons'; import {t} from 'sentry/locale'; import PluginIcon from 'sentry/plugins/components/pluginIcon'; import {space} from 'sentry/styles/space'; import { IntegrationFeature, IntegrationInstallationStatus, IntegrationType, Organization, } from 'sentry/types'; import { IntegrationAnalyticsKey, IntegrationEventParameters, } from 'sentry/utils/analytics/integrations'; import { getCategories, getIntegrationFeatureGate, trackIntegrationAnalytics, } from 'sentry/utils/integrationUtil'; import marked, {singleLineRenderer} from 'sentry/utils/marked'; import BreadcrumbTitle from 'sentry/views/settings/components/settingsBreadcrumb/breadcrumbTitle'; import RequestIntegrationButton from './integrationRequest/RequestIntegrationButton'; import IntegrationStatus from './integrationStatus'; export type Tab = 'overview' | 'configurations' | 'features'; interface AlertType extends AlertProps { text: string; } type State = { tab: Tab; } & DeprecatedAsyncComponent['state']; type Props = { organization: Organization; } & RouteComponentProps<{integrationSlug: string}, {}> & DeprecatedAsyncComponent['props']; class AbstractIntegrationDetailedView< P extends Props = Props, S extends State = State, > extends DeprecatedAsyncComponent { tabs: Tab[] = ['overview', 'configurations']; componentDidMount() { super.componentDidMount(); const {location} = this.props; const value = location.query.tab === 'configurations' ? 'configurations' : 'overview'; // eslint-disable-next-line react/no-did-mount-set-state this.setState({tab: value}); } onLoadAllEndpointsSuccess() { this.trackIntegrationAnalytics('integrations.integration_viewed', { integration_tab: this.state.tab, }); } /** * Abstract methods defined below */ // The analytics type used in analytics which is snake case get integrationType(): IntegrationType { // Allow children to implement this throw new Error('Not implemented'); } get description(): string { // Allow children to implement this throw new Error('Not implemented'); } get author(): string | undefined { // Allow children to implement this throw new Error('Not implemented'); } get alerts(): AlertType[] { // default is no alerts return []; } // Returns a list of the resources displayed at the bottom of the overview card get resourceLinks(): Array<{title: string; url: string}> { // Allow children to implement this throw new Error('Not implemented'); } get installationStatus(): IntegrationInstallationStatus | null { // Allow children to implement this throw new Error('Not implemented'); } get integrationName(): string { // Allow children to implement this throw new Error('Not implemented'); } // Checks to see if integration requires admin access to install, doc integrations don't get requiresAccess(): boolean { // default is integration requires access to install return true; } // Returns an array of RawIntegrationFeatures which is used in feature gating // and displaying what the integration does get featureData(): IntegrationFeature[] { // Allow children to implement this throw new Error('Not implemented'); } getIcon(title: string) { switch (title) { case 'View Source': return ; case 'Report Issue': return ; case 'Documentation': case 'Splunk Setup Instructions': case 'Trello Setup Instructions': return ; default: return ; } } onTabChange = (value: Tab) => { this.trackIntegrationAnalytics('integrations.integration_tab_clicked', { integration_tab: value, }); this.setState({tab: value}); }; // Returns the string that is shown as the title of a tab getTabDisplay(tab: Tab): string { // default is return the tab return tab; } // Render the button at the top which is usually just an installation button renderTopButton( _disabledFromFeatures: boolean, // from the feature gate _userHasAccess: boolean // from user permissions ): React.ReactElement { // Allow children to implement this throw new Error('Not implemented'); } // Returns the permission descriptions, only use by Sentry Apps renderPermissions(): React.ReactElement | null { // default is don't render permissions return null; } renderEmptyConfigurations() { return ( ); } // Returns the list of configurations for the integration renderConfigurations() { // Allow children to implement this throw new Error('Not implemented'); } /** * Actually implemented methods below */ get integrationSlug() { return this.props.params.integrationSlug; } // Wrapper around trackIntegrationAnalytics that automatically provides many fields and the org trackIntegrationAnalytics = ( eventKey: IntegrationAnalyticsKey, options?: Partial ) => { options = options || {}; // If we use this intermediate type we get type checking on the things we care about trackIntegrationAnalytics(eventKey, { view: 'integrations_directory_integration_detail', integration: this.integrationSlug, integration_type: this.integrationType, already_installed: this.installationStatus !== 'Not Installed', // pending counts as installed here organization: this.props.organization, ...options, }); }; // Returns the props as needed by the hooks integrations:feature-gates get featureProps() { const {organization} = this.props; const featureData = this.featureData; // Prepare the features list const features = featureData.map(f => ({ featureGate: f.featureGate, description: ( ), })); return {organization, features}; } cleanTags() { return getCategories(this.featureData); } renderAlert(): React.ReactNode { return null; } renderAdditionalCTA(): React.ReactNode { return null; } renderIntegrationIcon() { return ; } renderRequestIntegrationButton() { return ( ); } renderAddInstallButton(hideButtonIfDisabled = false) { const {IntegrationFeatures} = getIntegrationFeatureGate(); return ( {({disabled, disabledReason}) => ( {({hasAccess}) => ( {!hideButtonIfDisabled && disabled ? (
) : ( this.renderTopButton(disabled, hasAccess) )} )} {disabled && } )} ); } // Returns the content shown in the top section of the integration detail renderTopSection() { const tags = this.cleanTags(); return ( {this.renderIntegrationIcon()} {this.integrationName} {this.installationStatus && ( )} {tags.map(feature => ( {startCase(feature)} ))} {this.renderAddInstallButton()} {this.renderAdditionalCTA()} ); } // Returns the tabs divider with the clickable tabs renderTabs() { // TODO: Convert to styled component return (
    {this.tabs.map(tabName => (
  • this.onTabChange(tabName)} > {this.getTabDisplay(tabName)}
  • ))}
); } // Returns the information about the integration description and features renderInformationCard() { const {FeatureList} = getIntegrationFeatureGate(); return ( {this.renderPermissions()} {this.alerts.map((alert, i) => ( ))} {!!this.author && ( {t('Created By')}
{this.author}
)} {this.resourceLinks.map(({title, url}) => ( {this.getIcon(title)} {title} ))}
); } renderBody() { return ( {this.renderAlert()} {this.renderTopSection()} {this.renderTabs()} {this.state.tab === 'overview' ? this.renderInformationCard() : this.renderConfigurations()} ); } } const Flex = styled('div')` display: flex; align-items: center; `; const FlexContainer = styled('div')` flex: 1; `; const CapitalizedLink = styled('a')` text-transform: capitalize; `; const StyledTag = styled(Tag)` text-transform: none; &:not(:first-child) { margin-left: ${space(0.5)}; } `; const NameContainer = styled('div')` display: flex; align-items: flex-start; flex-direction: column; justify-content: center; padding-left: ${space(2)}; `; const Name = styled('div')` font-weight: bold; font-size: 1.4em; margin-bottom: ${space(0.5)}; `; const IconCloseCircle = styled(IconClose)` color: ${p => p.theme.dangerText}; margin-right: ${space(1)}; `; const DisabledNotice = styled(({reason, ...p}: {reason: React.ReactNode}) => (
{reason}
))` padding-top: ${space(0.5)}; font-size: 0.9em; `; const FeatureListItem = styled('span')` line-height: 24px; `; const Description = styled('div')` li { margin-bottom: 6px; } `; const Metadata = styled(Flex)` display: grid; grid-auto-rows: max-content; grid-auto-flow: row; gap: ${space(1)}; font-size: 0.9em; margin-left: ${space(4)}; margin-right: 100px; align-self: flex-start; `; const AuthorInfo = styled('div')` margin-bottom: ${space(3)}; `; const ExternalLinkContainer = styled('div')` display: grid; grid-template-columns: max-content 1fr; gap: ${space(1)}; align-items: center; `; const StatusWrapper = styled('div')` margin-bottom: ${space(0.5)}; padding-left: ${space(2)}; `; const DisableWrapper = styled('div')` margin-left: auto; align-self: center; display: flex; flex-direction: column; align-items: center; `; const CreatedContainer = styled('div')` text-transform: uppercase; padding-bottom: ${space(1)}; color: ${p => p.theme.gray300}; font-weight: 600; font-size: 12px; `; const TopSectionWrapper = styled('div')` display: flex; justify-content: space-between; `; export default AbstractIntegrationDetailedView;