index.tsx 5.8 KB

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