index.tsx 6.5 KB

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