import {Fragment} from 'react'; import type {RouteComponentProps} from 'react-router'; import styled from '@emotion/styled'; import EmptyMessage from 'sentry/components/emptyMessage'; import SelectField from 'sentry/components/forms/fields/selectField'; import Form from 'sentry/components/forms/form'; import ProjectBadge from 'sentry/components/idBadge/projectBadge'; import Pagination from 'sentry/components/pagination'; import Panel from 'sentry/components/panels/panel'; import PanelBody from 'sentry/components/panels/panelBody'; import PanelHeader from 'sentry/components/panels/panelHeader'; import {t} from 'sentry/locale'; import ConfigStore from 'sentry/stores/configStore'; import OrganizationsStore from 'sentry/stores/organizationsStore'; import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import {space} from 'sentry/styles/space'; import type {Organization} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; import type {UserEmail} from 'sentry/types/user'; import parseLinkHeader from 'sentry/utils/parseLinkHeader'; import withOrganizations from 'sentry/utils/withOrganizations'; import DeprecatedAsyncView from 'sentry/views/deprecatedAsyncView'; import type {FineTuneField} from 'sentry/views/settings/account/notifications/fields'; import {ACCOUNT_NOTIFICATION_FIELDS} from 'sentry/views/settings/account/notifications/fields'; import NotificationSettingsByType from 'sentry/views/settings/account/notifications/notificationSettingsByType'; import {OrganizationSelectHeader} from 'sentry/views/settings/account/notifications/organizationSelectHeader'; import { getNotificationTypeFromPathname, groupByOrganization, isGroupedByProject, } from 'sentry/views/settings/account/notifications/utils'; import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader'; import TextBlock from 'sentry/views/settings/components/text/textBlock'; const PanelBodyLineItem = styled(PanelBody)` font-size: 1rem; &:not(:last-child) { border-bottom: 1px solid ${p => p.theme.innerBorder}; } `; const accountNotifications = [ 'alerts', 'deploy', 'workflow', 'approval', 'quota', 'spikeProtection', 'reports', 'brokenMonitors', ]; type ANBPProps = { field: FineTuneField; projects: Project[]; }; function AccountNotificationsByProject({projects, field}: ANBPProps) { const projectsByOrg = groupByOrganization(projects); // eslint-disable-next-line @typescript-eslint/no-unused-vars const {title, description, ...fieldConfig} = field; // Display as select box in this view regardless of the type specified in the config const data = Object.values(projectsByOrg).map(org => ({ name: org.organization.name, projects: org.projects.map(project => ({ ...fieldConfig, // `name` key refers to field name // we use project.id because slugs are not unique across orgs name: project.id, label: ( ), })), })); return ( {data.map(({name, projects: projectFields}) => (
{projectFields.map(f => ( ))}
))}
); } type ANBOProps = { field: FineTuneField; }; function AccountNotificationsByOrganization({field}: ANBOProps) { const {organizations} = useLegacyStore(OrganizationsStore); // eslint-disable-next-line @typescript-eslint/no-unused-vars const {title, description, ...fieldConfig} = field; // Display as select box in this view regardless of the type specified in the config const data = organizations.map(org => ({ ...fieldConfig, // `name` key refers to field name // we use org.id to remain consistent project.id use (which is required because slugs are not unique across orgs) name: org.id, label: org.slug, })); return ( {data.map(f => ( ))} ); } type Props = DeprecatedAsyncView['props'] & RouteComponentProps<{fineTuneType: string}, {}> & { organizations: Organization[]; }; type State = DeprecatedAsyncView['state'] & { emails: UserEmail[] | null; emailsByProject: Record | null; notifications: Record | null; projects: Project[] | null; }; class AccountNotificationFineTuning extends DeprecatedAsyncView { getEndpoints(): ReturnType { const {fineTuneType: pathnameType} = this.props.params; const fineTuneType = getNotificationTypeFromPathname(pathnameType); const endpoints: ReturnType = [ ['notifications', '/users/me/notifications/'], ]; if (isGroupedByProject(fineTuneType)) { const organizationId = this.getOrganizationId(); endpoints.push(['projects', `/projects/`, {query: {organizationId}}]); } // special logic for email if (fineTuneType === 'email') { endpoints.push(['emails', '/users/me/emails/']); endpoints.push(['emailsByProject', `/users/me/notifications/email/`]); } return endpoints; } // Return a sorted list of user's verified emails get emailChoices() { return ( this.state.emails ?.filter(({isVerified}) => isVerified) ?.sort((a, b) => { // Sort by primary -> email if (a.isPrimary) { return -1; } if (b.isPrimary) { return 1; } return a.email < b.email ? -1 : 1; }) ?? [] ); } handleOrgChange = (organizationId: string) => { this.props.router.replace({ ...this.props.location, query: {organizationId}, }); }; getOrganizationId(): string | undefined { const {location, organizations} = this.props; const customerDomain = ConfigStore.get('customerDomain'); const orgFromSubdomain = organizations.find( ({slug}) => slug === customerDomain?.subdomain )?.id; return location?.query?.organizationId ?? orgFromSubdomain ?? organizations[0]?.id; } renderBody() { const {params, organizations} = this.props; const {fineTuneType: pathnameType} = params; const fineTuneType = getNotificationTypeFromPathname(pathnameType); if (accountNotifications.includes(fineTuneType)) { return ; } const {notifications, projects, emailsByProject, projectsPageLinks} = this.state; const isProject = isGroupedByProject(fineTuneType) && organizations.length > 0; const field = ACCOUNT_NOTIFICATION_FIELDS[fineTuneType]; // TODO(isabella): once GA, remove this if ( fineTuneType === 'quota' && organizations.some(org => org.features?.includes('spend-visibility-notifications')) ) { field.title = t('Spend Notifications'); field.description = t( 'Control the notifications you receive for organization spend.' ); } const {title, description} = field; const [stateKey] = isProject ? this.getEndpoints()[2] : []; const hasProjects = !!projects?.length; if (fineTuneType === 'email') { // Fetch verified email addresses field.options = this.emailChoices.map(({email}) => ({value: email, label: email})); } if (!notifications || (!emailsByProject && fineTuneType === 'email')) { return null; } const orgId = this.getOrganizationId(); const paginationObject = parseLinkHeader(projectsPageLinks ?? ''); const hasMore = paginationObject?.next?.results; const hasPrevious = paginationObject?.previous?.results; const mainContent = ( {isProject && hasProjects && ( )} {isProject && !hasProjects && ( {t('No projects found')} )} {!isProject && } ); return (
{description && {description}} {isProject ? ( {this.renderSearchInput({ placeholder: t('Search Projects'), url: `/projects/?organizationId=${orgId}`, stateKey, })} ) : ( {t('Organizations')} )} {/* Only email needs the form to change the emmail */} {fineTuneType === 'email' && emailsByProject ? (
{mainContent}
) : ( mainContent )}
{projects && (hasMore || hasPrevious) && ( )}
); } } const Heading = styled('div')` flex: 1; `; const StyledPanelHeader = styled(PanelHeader)` flex-wrap: wrap; gap: ${space(1)}; & > form:last-child { flex-grow: 1; } `; export default withOrganizations(AccountNotificationFineTuning);