accountAuthorizations.tsx 4.1 KB

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