accountAuthorizations.tsx 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. import type {RouteComponentProps} from 'react-router';
  2. import styled from '@emotion/styled';
  3. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  4. import {Button} from 'sentry/components/button';
  5. import EmptyMessage from 'sentry/components/emptyMessage';
  6. import ExternalLink from 'sentry/components/links/externalLink';
  7. import Link from 'sentry/components/links/link';
  8. import Panel from 'sentry/components/panels/panel';
  9. import PanelBody from 'sentry/components/panels/panelBody';
  10. import PanelHeader from 'sentry/components/panels/panelHeader';
  11. import PanelItem from 'sentry/components/panels/panelItem';
  12. import {IconDelete} from 'sentry/icons';
  13. import {t, tct} from 'sentry/locale';
  14. import {space} from 'sentry/styles/space';
  15. import type {ApiApplication} from 'sentry/types';
  16. import DeprecatedAsyncView from 'sentry/views/deprecatedAsyncView';
  17. import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
  18. type Authorization = {
  19. application: ApiApplication;
  20. homepageUrl: string;
  21. id: string;
  22. scopes: string[];
  23. };
  24. type Props = RouteComponentProps<{}, {}>;
  25. type State = {
  26. data: Authorization[];
  27. } & DeprecatedAsyncView['state'];
  28. class AccountAuthorizations extends DeprecatedAsyncView<Props, State> {
  29. getEndpoints(): ReturnType<DeprecatedAsyncView['getEndpoints']> {
  30. return [['data', '/api-authorizations/']];
  31. }
  32. getTitle() {
  33. return 'Approved Applications';
  34. }
  35. handleRevoke = authorization => {
  36. const oldData = this.state.data;
  37. this.setState(
  38. state => ({
  39. data: state.data.filter(({id}) => id !== authorization.id),
  40. }),
  41. async () => {
  42. try {
  43. await this.api.requestPromise('/api-authorizations/', {
  44. method: 'DELETE',
  45. data: {authorization: authorization.id},
  46. });
  47. addSuccessMessage(t('Saved changes'));
  48. } catch (_err) {
  49. this.setState({
  50. data: oldData,
  51. });
  52. addErrorMessage(t('Unable to save changes, please try again'));
  53. }
  54. }
  55. );
  56. };
  57. renderBody() {
  58. const {data} = this.state;
  59. const isEmpty = data.length === 0;
  60. return (
  61. <div>
  62. <SettingsPageHeader title="Authorized Applications" />
  63. <Description>
  64. {tct('You can manage your own applications via the [link:API dashboard].', {
  65. link: <Link to="/settings/account/api/" />,
  66. })}
  67. </Description>
  68. <Panel>
  69. <PanelHeader>{t('Approved Applications')}</PanelHeader>
  70. <PanelBody>
  71. {isEmpty && (
  72. <EmptyMessage>
  73. {t("You haven't approved any third party applications.")}
  74. </EmptyMessage>
  75. )}
  76. {!isEmpty && (
  77. <div>
  78. {data.map(authorization => (
  79. <PanelItemCenter key={authorization.id}>
  80. <ApplicationDetails>
  81. <ApplicationName>{authorization.application.name}</ApplicationName>
  82. {authorization.homepageUrl && (
  83. <Url>
  84. <ExternalLink href={authorization.homepageUrl}>
  85. {authorization.homepageUrl}
  86. </ExternalLink>
  87. </Url>
  88. )}
  89. <Scopes>{authorization.scopes.join(', ')}</Scopes>
  90. </ApplicationDetails>
  91. <Button
  92. size="sm"
  93. onClick={() => this.handleRevoke(authorization)}
  94. icon={<IconDelete />}
  95. aria-label={t('Delete')}
  96. />
  97. </PanelItemCenter>
  98. ))}
  99. </div>
  100. )}
  101. </PanelBody>
  102. </Panel>
  103. </div>
  104. );
  105. }
  106. }
  107. export default AccountAuthorizations;
  108. const Description = styled('p')`
  109. font-size: ${p => p.theme.fontSizeRelativeSmall};
  110. margin-bottom: ${space(4)};
  111. `;
  112. const PanelItemCenter = styled(PanelItem)`
  113. align-items: center;
  114. `;
  115. const ApplicationDetails = styled('div')`
  116. display: flex;
  117. flex: 1;
  118. flex-direction: column;
  119. `;
  120. const ApplicationName = styled('div')`
  121. font-weight: ${p => p.theme.fontWeightBold};
  122. margin-bottom: ${space(0.5)};
  123. `;
  124. /**
  125. * Intentionally wrap <a> so that it does not take up full width and cause
  126. * hit box issues
  127. */
  128. const Url = styled('div')`
  129. margin-bottom: ${space(0.5)};
  130. font-size: ${p => p.theme.fontSizeRelativeSmall};
  131. `;
  132. const Scopes = styled('div')`
  133. color: ${p => p.theme.gray300};
  134. font-size: ${p => p.theme.fontSizeRelativeSmall};
  135. `;