accountAuthorizations.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. import styled from '@emotion/styled';
  2. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  3. import {Button} from 'sentry/components/button';
  4. import EmptyMessage from 'sentry/components/emptyMessage';
  5. import ExternalLink from 'sentry/components/links/externalLink';
  6. import Link from 'sentry/components/links/link';
  7. import LoadingError from 'sentry/components/loadingError';
  8. import LoadingIndicator from 'sentry/components/loadingIndicator';
  9. import Panel from 'sentry/components/panels/panel';
  10. import PanelBody from 'sentry/components/panels/panelBody';
  11. import PanelHeader from 'sentry/components/panels/panelHeader';
  12. import PanelItem from 'sentry/components/panels/panelItem';
  13. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  14. import {IconDelete} from 'sentry/icons';
  15. import {t, tct} from 'sentry/locale';
  16. import {space} from 'sentry/styles/space';
  17. import type {ApiApplication} from 'sentry/types';
  18. import {setApiQueryData, useApiQuery, useQueryClient} from 'sentry/utils/queryClient';
  19. import useApi from 'sentry/utils/useApi';
  20. import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
  21. type Authorization = {
  22. application: ApiApplication;
  23. homepageUrl: string;
  24. id: string;
  25. scopes: string[];
  26. };
  27. function AccountAuthorizations() {
  28. const api = useApi();
  29. const queryClient = useQueryClient();
  30. const ENDPOINT = '/api-authorizations/';
  31. const {data, isLoading, isError, refetch} = useApiQuery<Authorization[]>([ENDPOINT], {
  32. staleTime: 0,
  33. });
  34. if (isLoading) {
  35. return <LoadingIndicator />;
  36. }
  37. if (isError) {
  38. return <LoadingError onRetry={refetch} />;
  39. }
  40. const handleRevoke = async (authorization: Authorization) => {
  41. const oldData = data;
  42. setApiQueryData<Authorization[]>(queryClient, [ENDPOINT], prevData =>
  43. prevData.filter(a => a.id !== authorization.id)
  44. );
  45. try {
  46. await api.requestPromise('/api-authorizations/', {
  47. method: 'DELETE',
  48. data: {authorization: authorization.id},
  49. });
  50. addSuccessMessage(t('Saved changes'));
  51. } catch (_err) {
  52. setApiQueryData<any>(queryClient, [ENDPOINT], oldData);
  53. addErrorMessage(t('Unable to save changes, please try again'));
  54. }
  55. };
  56. const isEmpty = data.length === 0;
  57. return (
  58. <SentryDocumentTitle title={t('Approved Applications')}>
  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={() => handleRevoke(authorization)}
  91. icon={<IconDelete />}
  92. aria-label={t('Delete')}
  93. data-test-id={authorization.id}
  94. />
  95. </PanelItemCenter>
  96. ))}
  97. </div>
  98. )}
  99. </PanelBody>
  100. </Panel>
  101. </SentryDocumentTitle>
  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: ${p => p.theme.fontWeightBold};
  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. `;