accountIdentities.tsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. import {Fragment} from 'react';
  2. import {RouteComponentProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import moment from 'moment';
  5. import {disconnectIdentity} from 'sentry/actionCreators/account';
  6. import {Alert} from 'sentry/components/alert';
  7. import {Button} from 'sentry/components/button';
  8. import Confirm from 'sentry/components/confirm';
  9. import DateTime from 'sentry/components/dateTime';
  10. import EmptyMessage from 'sentry/components/emptyMessage';
  11. import Panel from 'sentry/components/panels/panel';
  12. import PanelBody from 'sentry/components/panels/panelBody';
  13. import PanelHeader from 'sentry/components/panels/panelHeader';
  14. import PanelItem from 'sentry/components/panels/panelItem';
  15. import Tag from 'sentry/components/tag';
  16. import {t, tct} from 'sentry/locale';
  17. import {space} from 'sentry/styles/space';
  18. import {UserIdentityCategory, UserIdentityConfig, UserIdentityStatus} from 'sentry/types';
  19. import DeprecatedAsyncView from 'sentry/views/deprecatedAsyncView';
  20. import IdentityIcon from 'sentry/views/settings/components/identityIcon';
  21. import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
  22. import TextBlock from 'sentry/views/settings/components/text/textBlock';
  23. const ENDPOINT = '/users/me/user-identities/';
  24. type Props = RouteComponentProps<{}, {}>;
  25. type State = {
  26. identities: UserIdentityConfig[] | null;
  27. } & DeprecatedAsyncView['state'];
  28. class AccountIdentities extends DeprecatedAsyncView<Props, State> {
  29. getDefaultState() {
  30. return {
  31. ...super.getDefaultState(),
  32. identities: [],
  33. };
  34. }
  35. getEndpoints(): ReturnType<DeprecatedAsyncView['getEndpoints']> {
  36. return [['identities', ENDPOINT]];
  37. }
  38. getTitle() {
  39. return t('Identities');
  40. }
  41. renderItem = (identity: UserIdentityConfig) => {
  42. return (
  43. <IdentityPanelItem key={`${identity.category}:${identity.id}`}>
  44. <InternalContainer>
  45. <IdentityIcon providerId={identity.provider.key} />
  46. <IdentityText isSingleLine={!identity.dateAdded}>
  47. <IdentityName>{identity.provider.name}</IdentityName>
  48. {identity.dateAdded && <IdentityDateTime date={moment(identity.dateAdded)} />}
  49. </IdentityText>
  50. </InternalContainer>
  51. <InternalContainer>
  52. <TagWrapper>
  53. {identity.category === UserIdentityCategory.SOCIAL_IDENTITY && (
  54. <Tag type="default">{t('Legacy')}</Tag>
  55. )}
  56. {identity.category !== UserIdentityCategory.ORG_IDENTITY && (
  57. <Tag type="default">
  58. {identity.isLogin ? t('Sign In') : t('Integration')}
  59. </Tag>
  60. )}
  61. {identity.organization && (
  62. <Tag type="highlight">{identity.organization.slug}</Tag>
  63. )}
  64. </TagWrapper>
  65. {this.renderButton(identity)}
  66. </InternalContainer>
  67. </IdentityPanelItem>
  68. );
  69. };
  70. renderButton(identity: UserIdentityConfig) {
  71. return identity.status === UserIdentityStatus.CAN_DISCONNECT ? (
  72. <Confirm
  73. onConfirm={() => this.handleDisconnect(identity)}
  74. priority="danger"
  75. confirmText={t('Disconnect')}
  76. message={
  77. <Fragment>
  78. <Alert type="error" showIcon>
  79. {tct('Disconnect Your [provider] Identity?', {
  80. provider: identity.provider.name,
  81. })}
  82. </Alert>
  83. <TextBlock>
  84. {identity.isLogin
  85. ? t(
  86. 'After disconnecting, you will need to use a password or another identity to sign in.'
  87. )
  88. : t("This action can't be undone.")}
  89. </TextBlock>
  90. </Fragment>
  91. }
  92. >
  93. <Button size="sm">{t('Disconnect')}</Button>
  94. </Confirm>
  95. ) : (
  96. <Button
  97. size="sm"
  98. disabled
  99. title={
  100. identity.status === UserIdentityStatus.NEEDED_FOR_GLOBAL_AUTH
  101. ? t(
  102. 'You need this identity to sign into your account. If you want to disconnect it, set a password first.'
  103. )
  104. : identity.status === UserIdentityStatus.NEEDED_FOR_ORG_AUTH
  105. ? t('You need this identity to access your organization.')
  106. : null
  107. }
  108. >
  109. {t('Disconnect')}
  110. </Button>
  111. );
  112. }
  113. handleDisconnect = (identity: UserIdentityConfig) => {
  114. disconnectIdentity(identity, () => this.reloadData());
  115. };
  116. itemOrder = (a: UserIdentityConfig, b: UserIdentityConfig) => {
  117. function categoryRank(c: UserIdentityConfig) {
  118. return [
  119. UserIdentityCategory.GLOBAL_IDENTITY,
  120. UserIdentityCategory.SOCIAL_IDENTITY,
  121. UserIdentityCategory.ORG_IDENTITY,
  122. ].indexOf(c.category);
  123. }
  124. if (a.provider.name !== b.provider.name) {
  125. return a.provider.name < b.provider.name ? -1 : 1;
  126. }
  127. if (a.category !== b.category) {
  128. return categoryRank(a) - categoryRank(b);
  129. }
  130. if ((a.organization?.name ?? '') !== (b.organization?.name ?? '')) {
  131. return (a.organization?.name ?? '') < (b.organization?.name ?? '') ? -1 : 1;
  132. }
  133. return 0;
  134. };
  135. renderBody() {
  136. const appIdentities = this.state.identities
  137. ?.filter(identity => identity.category !== UserIdentityCategory.ORG_IDENTITY)
  138. .sort(this.itemOrder);
  139. const orgIdentities = this.state.identities
  140. ?.filter(identity => identity.category === UserIdentityCategory.ORG_IDENTITY)
  141. .sort(this.itemOrder);
  142. return (
  143. <Fragment>
  144. <SettingsPageHeader title="Identities" />
  145. <Panel>
  146. <PanelHeader>{t('Application Identities')}</PanelHeader>
  147. <PanelBody>
  148. {!appIdentities?.length ? (
  149. <EmptyMessage>
  150. {t(
  151. 'There are no application identities associated with your Sentry account'
  152. )}
  153. </EmptyMessage>
  154. ) : (
  155. appIdentities.map(this.renderItem)
  156. )}
  157. </PanelBody>
  158. </Panel>
  159. <Panel>
  160. <PanelHeader>{t('Organization Identities')}</PanelHeader>
  161. <PanelBody>
  162. {!orgIdentities?.length ? (
  163. <EmptyMessage>
  164. {t(
  165. 'There are no organization identities associated with your Sentry account'
  166. )}
  167. </EmptyMessage>
  168. ) : (
  169. orgIdentities.map(this.renderItem)
  170. )}
  171. </PanelBody>
  172. </Panel>
  173. </Fragment>
  174. );
  175. }
  176. }
  177. const IdentityPanelItem = styled(PanelItem)`
  178. align-items: center;
  179. justify-content: space-between;
  180. `;
  181. const InternalContainer = styled('div')`
  182. display: flex;
  183. flex-direction: row;
  184. justify-content: center;
  185. `;
  186. const IdentityText = styled('div')<{isSingleLine?: boolean}>`
  187. height: 36px;
  188. display: flex;
  189. flex-direction: column;
  190. justify-content: ${p => (p.isSingleLine ? 'center' : 'space-between')};
  191. margin-left: ${space(1.5)};
  192. `;
  193. const IdentityName = styled('div')`
  194. font-weight: bold;
  195. `;
  196. const IdentityDateTime = styled(DateTime)`
  197. font-size: ${p => p.theme.fontSizeRelativeSmall};
  198. color: ${p => p.theme.subText};
  199. `;
  200. const TagWrapper = styled('div')`
  201. display: flex;
  202. align-items: center;
  203. justify-content: flex-start;
  204. flex-grow: 1;
  205. margin-right: ${space(1)};
  206. `;
  207. export default AccountIdentities;