accountAuthorizations.tsx 4.3 KB

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