123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314 |
- import {Fragment, useState} from 'react';
- import type {WithRouterProps} from 'react-router';
- import {components} from 'react-select';
- import styled from '@emotion/styled';
- import {Button} from 'sentry/components/button';
- import SelectControl from 'sentry/components/forms/controls/selectControl';
- import JsonForm from 'sentry/components/forms/jsonForm';
- import IdBadge from 'sentry/components/idBadge';
- import Panel from 'sentry/components/panels/panel';
- import PanelBody from 'sentry/components/panels/panelBody';
- import PanelHeader from 'sentry/components/panels/panelHeader';
- import {IconAdd, IconDelete} from 'sentry/icons';
- import {t} from 'sentry/locale';
- import ConfigStore from 'sentry/stores/configStore';
- import {space} from 'sentry/styles/space';
- import {Organization, Project} from 'sentry/types';
- import {useApiQuery} from 'sentry/utils/queryClient';
- import withSentryRouter from 'sentry/utils/withSentryRouter';
- import {NotificationOptionsObject} from 'sentry/views/settings/account/notifications/constants';
- import {NOTIFICATION_SETTING_FIELDS_V2} from 'sentry/views/settings/account/notifications/fields2';
- import {OrganizationSelectHeader} from 'sentry/views/settings/account/notifications/organizationSelectHeader';
- type Value = 'always' | 'never' | 'subscribe_only' | 'committed_only';
- const getLabelForValue = (value: Value) => {
- switch (value) {
- case 'always':
- return t('On');
- case 'never':
- return t('Off');
- case 'subscribe_only':
- return t('Subscribed Only');
- case 'committed_only':
- return t('Committed Only');
- default:
- return '';
- }
- };
- export type NotificationSettingsByProjectsBaseProps = {
- entityType: 'project' | 'organization';
- handleAddNotificationOption: (
- notificationOption: Omit<NotificationOptionsObject, 'id'>
- ) => void;
- handleRemoveNotificationOption: (id: string) => void;
- notificationOptions: NotificationOptionsObject[];
- notificationType: string;
- };
- type Props = {
- organizations: Organization[];
- } & NotificationSettingsByProjectsBaseProps &
- WithRouterProps;
- function NotificationSettingsByEntity(props: Props) {
- const {
- entityType,
- handleAddNotificationOption,
- handleRemoveNotificationOption,
- notificationOptions,
- notificationType,
- organizations,
- router,
- location,
- } = props;
- const [selectedEntityId, setSelectedEntityId] = useState<string | null>(null);
- const [selectedValue, setSelectedValue] = useState<Value | null>(null);
- const customerDomain = ConfigStore.get('customerDomain');
- const orgFromSubdomain = organizations.find(
- ({slug}) => slug === customerDomain?.subdomain
- )?.id;
- const orgId =
- location?.query?.organizationId ?? orgFromSubdomain ?? organizations[0]?.id;
- const orgSlug =
- organizations.find(({id}) => id === orgId)?.slug || organizations[0]?.slug;
- // loads all the projects for an org
- const {data: projects} = useApiQuery<Project[]>(
- [
- `/organizations/${orgSlug}/projects/`,
- {
- query: {
- all_projects: '1',
- collapse: 'latestDeploys',
- },
- },
- ],
- {staleTime: Infinity}
- );
- // always loading all projects even though we only need it sometimes
- const entities = entityType === 'project' ? projects || [] : organizations;
- const handleOrgChange = (organizationId: string) => {
- router.replace({
- ...location,
- query: {organizationId},
- });
- };
- const handleAdd = () => {
- // should never happen
- if (!selectedEntityId || !selectedValue) {
- return;
- }
- const data = {
- type: notificationType,
- scopeType: entityType,
- scopeIdentifier: selectedEntityId,
- value: selectedValue,
- };
- setSelectedEntityId(null);
- setSelectedValue(null);
- handleAddNotificationOption(data);
- };
- const renderOverrides = () => {
- const matchedOptions = notificationOptions.filter(
- option => option.type === notificationType && option.scopeType === entityType
- );
- return matchedOptions.map(option => {
- const entity = (entities as any[]).find(
- ({id}) => id.toString() === option.scopeIdentifier.toString()
- );
- if (!entity) {
- return null;
- }
- const handleDelete = async (id: string) => {
- await handleRemoveNotificationOption(id);
- };
- const idBadgeProps =
- entityType === 'project'
- ? {project: entity as Project}
- : {
- organization: entity as Organization,
- };
- return (
- <Item key={entity.id}>
- <IdBadge
- {...idBadgeProps}
- avatarSize={20}
- displayName={entity.slug}
- avatarProps={{consistentWidth: true}}
- disableLink
- />
- {getLabelForValue(option.value)}
- <Button
- aria-label={t('Delete')}
- size="sm"
- priority="default"
- icon={<IconDelete />}
- onClick={() => handleDelete(option.id)}
- />
- </Item>
- );
- });
- };
- const customValueContainer = containerProps => {
- // if no value set, we want to return the default component that is rendered
- const entity = entityById[selectedEntityId || ''];
- if (!entity) {
- return <components.ValueContainer {...containerProps} />;
- }
- const idBadgeProps =
- entityType === 'project'
- ? {project: entity as Project}
- : {
- organization: entity as Organization,
- };
- return (
- <components.ValueContainer {...containerProps}>
- <IdBadge
- {...idBadgeProps}
- avatarSize={20}
- displayName={entity.slug}
- avatarProps={{consistentWidth: true}}
- disableLink
- />
- </components.ValueContainer>
- );
- };
- const handleSelectProject = ({value}: {value: string}) => {
- setSelectedEntityId(value);
- };
- const handleSelectValue = ({value}: {value: string}) => {
- setSelectedValue(value as Value);
- };
- // create maps by the project id for constant time lookups
- const entityById: Record<string, Organization | Project> = Object.fromEntries(
- entities.map(entity => [entity.id, entity])
- );
- const entityOptions: {label: string; value: Value}[] = (entities as any[])
- .filter(({id}: Project | Organization) => {
- const match = notificationOptions.find(
- option =>
- option.scopeType === entityType &&
- option.scopeIdentifier.toString() === id.toString() &&
- option.type === notificationType
- );
- return !match;
- })
- .map(({slug, id}) => ({label: slug, value: id}));
- const customOptionProject = entityProps => {
- const entity = entityById[entityProps.value];
- // Should never happen for a dropdown item
- if (!entity) {
- return null;
- }
- const idBadgeProps =
- entityType === 'project'
- ? {project: entity as Project}
- : {
- organization: entity as Organization,
- };
- return (
- <components.Option {...entityProps}>
- <IdBadge
- {...idBadgeProps}
- avatarSize={20}
- displayName={entity.slug}
- avatarProps={{consistentWidth: true}}
- disableLink
- />
- </components.Option>
- );
- };
- const valueOptions = NOTIFICATION_SETTING_FIELDS_V2[notificationType].choices;
- return (
- <Fragment>
- <Panel>
- <StyledPanelHeader>
- <OrganizationSelectHeader
- organizations={organizations}
- organizationId={orgId}
- handleOrgChange={handleOrgChange}
- />
- </StyledPanelHeader>
- <Item>
- {/* TODO: enable search for sentry projects */}
- <SelectControl
- placeholder={
- entityType === 'project'
- ? t('Sentry project\u2026')
- : t('Sentry organization\u2026')
- }
- name={entityType}
- options={entityOptions}
- components={{
- Option: customOptionProject,
- ValueContainer: customValueContainer,
- }}
- onChange={handleSelectProject}
- value={selectedEntityId}
- />
- <SelectControl
- placeholder={t('Select\u2026')}
- value={selectedValue}
- name="value"
- choices={valueOptions}
- onChange={handleSelectValue}
- />
- <AddProjectWrapper>
- <Button
- disabled={!selectedEntityId || !selectedValue}
- size="sm"
- priority="primary"
- onClick={handleAdd}
- icon={<IconAdd />}
- aria-label={t('Add override')}
- />
- </AddProjectWrapper>
- </Item>
- <PanelBody>{renderOverrides()}</PanelBody>
- </Panel>
- </Fragment>
- );
- }
- // loading all projects and orgs
- export default withSentryRouter(NotificationSettingsByEntity);
- const StyledPanelHeader = styled(PanelHeader)`
- flex-wrap: wrap;
- gap: ${space(1)};
- & > form:last-child {
- flex-grow: 1;
- }
- `;
- export const StyledJsonForm = styled(JsonForm)`
- ${Panel} {
- border: 0;
- margin-bottom: 0;
- }
- `;
- const AddProjectWrapper = styled('div')``;
- const Item = styled('div')`
- min-height: 60px;
- padding: ${space(2)};
- display: grid;
- grid-column-gap: ${space(1)};
- align-items: center;
- grid-template-columns: 2.5fr 1fr min-content;
- `;
|