accountAuthorizations.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  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 {Organization} from 'sentry/types/organization';
  18. import type {ApiApplication} from 'sentry/types/user';
  19. import {setApiQueryData, useApiQuery, useQueryClient} from 'sentry/utils/queryClient';
  20. import useApi from 'sentry/utils/useApi';
  21. import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
  22. type Authorization = {
  23. application: ApiApplication;
  24. homepageUrl: string;
  25. id: string;
  26. organization: Organization | null;
  27. scopes: string[];
  28. };
  29. function AccountAuthorizations() {
  30. const api = useApi();
  31. const queryClient = useQueryClient();
  32. const ENDPOINT = '/api-authorizations/';
  33. const {data, isPending, isError, refetch} = useApiQuery<Authorization[]>([ENDPOINT], {
  34. staleTime: 0,
  35. });
  36. if (isPending) {
  37. return <LoadingIndicator />;
  38. }
  39. if (isError) {
  40. return <LoadingError onRetry={refetch} />;
  41. }
  42. const handleRevoke = async (authorization: Authorization) => {
  43. const oldData = data;
  44. setApiQueryData<Authorization[]>(queryClient, [ENDPOINT], prevData =>
  45. prevData.filter(a => a.id !== authorization.id)
  46. );
  47. try {
  48. await api.requestPromise('/api-authorizations/', {
  49. method: 'DELETE',
  50. data: {authorization: authorization.id},
  51. });
  52. addSuccessMessage(t('Saved changes'));
  53. } catch (_err) {
  54. setApiQueryData<any>(queryClient, [ENDPOINT], oldData);
  55. addErrorMessage(t('Unable to save changes, please try again'));
  56. }
  57. };
  58. const isEmpty = data.length === 0;
  59. return (
  60. <SentryDocumentTitle title={t('Approved Applications')}>
  61. <SettingsPageHeader title="Authorized Applications" />
  62. <Description>
  63. {tct('You can manage your own applications via the [link:API dashboard].', {
  64. link: <Link to="/settings/account/api/" />,
  65. })}
  66. </Description>
  67. <Panel>
  68. <PanelHeader>{t('Approved Applications')}</PanelHeader>
  69. <PanelBody>
  70. {isEmpty && (
  71. <EmptyMessage>
  72. {t("You haven't approved any third party applications.")}
  73. </EmptyMessage>
  74. )}
  75. {!isEmpty && (
  76. <div>
  77. {data.map(authorization => (
  78. <PanelItemCenter key={authorization.id}>
  79. <ApplicationDetails>
  80. <ApplicationName>{authorization.application.name}</ApplicationName>
  81. {authorization.homepageUrl && (
  82. <Url>
  83. <ExternalLink href={authorization.homepageUrl}>
  84. {authorization.homepageUrl}
  85. </ExternalLink>
  86. </Url>
  87. )}
  88. <DetailRow>{authorization.scopes.join(', ')}</DetailRow>
  89. {authorization.organization && (
  90. <DetailRow>
  91. {t('scopes are limited to ')}
  92. {authorization.organization.slug}
  93. </DetailRow>
  94. )}
  95. </ApplicationDetails>
  96. <Button
  97. size="sm"
  98. onClick={() => handleRevoke(authorization)}
  99. icon={<IconDelete />}
  100. aria-label={t('Delete')}
  101. data-test-id={authorization.id}
  102. />
  103. </PanelItemCenter>
  104. ))}
  105. </div>
  106. )}
  107. </PanelBody>
  108. </Panel>
  109. </SentryDocumentTitle>
  110. );
  111. }
  112. export default AccountAuthorizations;
  113. const Description = styled('p')`
  114. font-size: ${p => p.theme.fontSizeRelativeSmall};
  115. margin-bottom: ${space(4)};
  116. `;
  117. const PanelItemCenter = styled(PanelItem)`
  118. align-items: center;
  119. `;
  120. const ApplicationDetails = styled('div')`
  121. display: flex;
  122. flex: 1;
  123. flex-direction: column;
  124. `;
  125. const ApplicationName = styled('div')`
  126. font-weight: ${p => p.theme.fontWeightBold};
  127. margin-bottom: ${space(0.5)};
  128. `;
  129. /**
  130. * Intentionally wrap <a> so that it does not take up full width and cause
  131. * hit box issues
  132. */
  133. const Url = styled('div')`
  134. margin-bottom: ${space(0.5)};
  135. font-size: ${p => p.theme.fontSizeRelativeSmall};
  136. `;
  137. const DetailRow = styled('div')`
  138. color: ${p => p.theme.gray300};
  139. font-size: ${p => p.theme.fontSizeRelativeSmall};
  140. `;