index.tsx 6.3 KB

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