index.tsx 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. import {Fragment} from 'react';
  2. import type {RouteComponentProps} from 'react-router';
  3. import {
  4. addErrorMessage,
  5. addLoadingMessage,
  6. addSuccessMessage,
  7. } from 'sentry/actionCreators/indicator';
  8. import {hasEveryAccess} from 'sentry/components/acl/access';
  9. import {Button} from 'sentry/components/button';
  10. import EmptyMessage from 'sentry/components/emptyMessage';
  11. import ExternalLink from 'sentry/components/links/externalLink';
  12. import Pagination from 'sentry/components/pagination';
  13. import Panel from 'sentry/components/panels/panel';
  14. import {IconAdd, IconFlag} from 'sentry/icons';
  15. import {t, tct} from 'sentry/locale';
  16. import type {Organization, Project, ProjectKey} from 'sentry/types';
  17. import routeTitleGen from 'sentry/utils/routeTitle';
  18. import withOrganization from 'sentry/utils/withOrganization';
  19. import DeprecatedAsyncView from 'sentry/views/deprecatedAsyncView';
  20. import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
  21. import TextBlock from 'sentry/views/settings/components/text/textBlock';
  22. import PermissionAlert from 'sentry/views/settings/project/permissionAlert';
  23. import KeyRow from './keyRow';
  24. type Props = {
  25. organization: Organization;
  26. project: Project;
  27. } & RouteComponentProps<{projectId: string}, {}>;
  28. type State = {
  29. keyList: ProjectKey[];
  30. } & DeprecatedAsyncView['state'];
  31. class ProjectKeys extends DeprecatedAsyncView<Props, State> {
  32. getTitle() {
  33. const {projectId} = this.props.params;
  34. return routeTitleGen(t('Client Keys'), projectId, false);
  35. }
  36. getEndpoints(): ReturnType<DeprecatedAsyncView['getEndpoints']> {
  37. const {organization} = this.props;
  38. const {projectId} = this.props.params;
  39. return [['keyList', `/projects/${organization.slug}/${projectId}/keys/`]];
  40. }
  41. /**
  42. * Optimistically remove key
  43. */
  44. handleRemoveKey = async (data: ProjectKey) => {
  45. const oldKeyList = [...this.state.keyList];
  46. addLoadingMessage(t('Revoking key\u2026'));
  47. this.setState(state => ({
  48. keyList: state.keyList.filter(key => key.id !== data.id),
  49. }));
  50. const {organization} = this.props;
  51. const {projectId} = this.props.params;
  52. try {
  53. await this.api.requestPromise(
  54. `/projects/${organization.slug}/${projectId}/keys/${data.id}/`,
  55. {
  56. method: 'DELETE',
  57. }
  58. );
  59. addSuccessMessage(t('Revoked key'));
  60. } catch (_err) {
  61. this.setState({
  62. keyList: oldKeyList,
  63. });
  64. addErrorMessage(t('Unable to revoke key'));
  65. }
  66. };
  67. handleToggleKey = async (isActive: boolean, data: ProjectKey) => {
  68. const oldKeyList = [...this.state.keyList];
  69. addLoadingMessage(t('Saving changes\u2026'));
  70. this.setState(state => {
  71. const keyList = state.keyList.map(key => {
  72. if (key.id === data.id) {
  73. return {
  74. ...key,
  75. isActive: !data.isActive,
  76. };
  77. }
  78. return key;
  79. });
  80. return {keyList};
  81. });
  82. const {organization} = this.props;
  83. const {projectId} = this.props.params;
  84. try {
  85. await this.api.requestPromise(
  86. `/projects/${organization.slug}/${projectId}/keys/${data.id}/`,
  87. {
  88. method: 'PUT',
  89. data: {isActive},
  90. }
  91. );
  92. addSuccessMessage(isActive ? t('Enabled key') : t('Disabled key'));
  93. } catch (_err) {
  94. addErrorMessage(isActive ? t('Error enabling key') : t('Error disabling key'));
  95. this.setState({keyList: oldKeyList});
  96. }
  97. };
  98. handleCreateKey = async () => {
  99. const {organization} = this.props;
  100. const {projectId} = this.props.params;
  101. try {
  102. const data: ProjectKey = await this.api.requestPromise(
  103. `/projects/${organization.slug}/${projectId}/keys/`,
  104. {
  105. method: 'POST',
  106. }
  107. );
  108. this.setState(state => ({
  109. keyList: [...state.keyList, data],
  110. }));
  111. addSuccessMessage(t('Created a new key.'));
  112. } catch (_err) {
  113. addErrorMessage(t('Unable to create new key. Please try again.'));
  114. }
  115. };
  116. renderEmpty() {
  117. return (
  118. <Panel>
  119. <EmptyMessage
  120. icon={<IconFlag size="xl" />}
  121. description={t('There are no keys active for this project.')}
  122. />
  123. </Panel>
  124. );
  125. }
  126. renderResults() {
  127. const {location, organization, project, routes, params} = this.props;
  128. const {projectId} = params;
  129. const hasAccess = hasEveryAccess(['project:write'], {organization, project});
  130. return (
  131. <Fragment>
  132. {this.state.keyList.map(key => (
  133. <KeyRow
  134. hasWriteAccess={hasAccess}
  135. key={key.id}
  136. orgId={organization.slug}
  137. projectId={projectId}
  138. project={this.props.project}
  139. data={key}
  140. onToggle={this.handleToggleKey}
  141. onRemove={this.handleRemoveKey}
  142. routes={routes}
  143. location={location}
  144. params={params}
  145. />
  146. ))}
  147. <Pagination pageLinks={this.state.keyListPageLinks} />
  148. </Fragment>
  149. );
  150. }
  151. renderBody() {
  152. const {organization, project} = this.props;
  153. const isEmpty = !this.state.keyList.length;
  154. const hasAccess = hasEveryAccess(['project:write'], {organization, project});
  155. return (
  156. <div data-test-id="project-keys">
  157. <SettingsPageHeader
  158. title={t('Client Keys')}
  159. action={
  160. <Button
  161. onClick={this.handleCreateKey}
  162. size="sm"
  163. priority="primary"
  164. icon={<IconAdd isCircled />}
  165. disabled={!hasAccess}
  166. >
  167. {t('Generate New Key')}
  168. </Button>
  169. }
  170. />
  171. <TextBlock>
  172. {tct(
  173. `To send data to Sentry you will need to configure an SDK with a client key
  174. (usually referred to as the [code:SENTRY_DSN] value). For more
  175. information on integrating Sentry with your application take a look at our
  176. [link:documentation].`,
  177. {
  178. link: (
  179. <ExternalLink href="https://docs.sentry.io/platform-redirect/?next=/configuration/options/" />
  180. ),
  181. code: <code />,
  182. }
  183. )}
  184. </TextBlock>
  185. <PermissionAlert project={project} />
  186. {isEmpty ? this.renderEmpty() : this.renderResults()}
  187. </div>
  188. );
  189. }
  190. }
  191. export default withOrganization(ProjectKeys);