index.tsx 6.0 KB

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