Files
sentinel/static/app/views/settings/account/accountNotificationFineTuning.tsx
2024-02-15 17:01:50 +00:00

313 lines
9.7 KiB
TypeScript

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, Project, UserEmail} from 'sentry/types';
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',
];
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: (
<ProjectBadge
project={project}
avatarSize={20}
displayName={project.slug}
avatarProps={{consistentWidth: true}}
disableLink
/>
),
})),
}));
return (
<Fragment>
{data.map(({name, projects: projectFields}) => (
<div key={name}>
{projectFields.map(f => (
<PanelBodyLineItem key={f.name}>
<SelectField
defaultValue={f.defaultValue}
name={f.name}
options={f.options}
label={f.label}
/>
</PanelBodyLineItem>
))}
</div>
))}
</Fragment>
);
}
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 (
<Fragment>
{data.map(f => (
<PanelBodyLineItem key={f.name}>
<SelectField
defaultValue={f.defaultValue}
name={f.name}
options={f.options}
label={f.label}
/>
</PanelBodyLineItem>
))}
</Fragment>
);
}
type Props = DeprecatedAsyncView['props'] &
RouteComponentProps<{fineTuneType: string}, {}> & {
organizations: Organization[];
};
type State = DeprecatedAsyncView['state'] & {
emails: UserEmail[] | null;
emailsByProject: Record<string, any> | null;
notifications: Record<string, any> | null;
projects: Project[] | null;
};
class AccountNotificationFineTuning extends DeprecatedAsyncView<Props, State> {
getEndpoints(): ReturnType<DeprecatedAsyncView['getEndpoints']> {
const {fineTuneType: pathnameType} = this.props.params;
const fineTuneType = getNotificationTypeFromPathname(pathnameType);
const endpoints: ReturnType<DeprecatedAsyncView['getEndpoints']> = [
['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 <NotificationSettingsByType notificationType={fineTuneType} />;
}
const {notifications, projects, emailsByProject, projectsPageLinks} = this.state;
const isProject = isGroupedByProject(fineTuneType) && organizations.length > 0;
const field = ACCOUNT_NOTIFICATION_FIELDS[fineTuneType];
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 = (
<Fragment>
{isProject && hasProjects && (
<AccountNotificationsByProject projects={projects!} field={field} />
)}
{isProject && !hasProjects && (
<EmptyMessage>{t('No projects found')}</EmptyMessage>
)}
{!isProject && <AccountNotificationsByOrganization field={field} />}
</Fragment>
);
return (
<div>
<SettingsPageHeader title={title} />
{description && <TextBlock>{description}</TextBlock>}
<Panel>
<StyledPanelHeader hasButtons={isProject}>
{isProject ? (
<Fragment>
<OrganizationSelectHeader
organizations={organizations}
organizationId={orgId}
handleOrgChange={this.handleOrgChange}
/>
{this.renderSearchInput({
placeholder: t('Search Projects'),
url: `/projects/?organizationId=${orgId}`,
stateKey,
})}
</Fragment>
) : (
<Heading>{t('Organizations')}</Heading>
)}
</StyledPanelHeader>
<PanelBody>
{/* Only email needs the form to change the emmail */}
{fineTuneType === 'email' && emailsByProject ? (
<Form
saveOnBlur
apiMethod="PUT"
apiEndpoint="/users/me/notifications/email/"
initialData={emailsByProject}
>
{mainContent}
</Form>
) : (
mainContent
)}
</PanelBody>
</Panel>
{projects && (hasMore || hasPrevious) && (
<Pagination pageLinks={projectsPageLinks} />
)}
</div>
);
}
}
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);