projectServiceHooks.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. import {Fragment} from 'react';
  2. import {
  3. addErrorMessage,
  4. addLoadingMessage,
  5. clearIndicators,
  6. } from 'sentry/actionCreators/indicator';
  7. import {LinkButton} from 'sentry/components/button';
  8. import EmptyMessage from 'sentry/components/emptyMessage';
  9. import FieldGroup from 'sentry/components/forms/fieldGroup';
  10. import Link from 'sentry/components/links/link';
  11. import LoadingError from 'sentry/components/loadingError';
  12. import LoadingIndicator from 'sentry/components/loadingIndicator';
  13. import Panel from 'sentry/components/panels/panel';
  14. import PanelAlert from 'sentry/components/panels/panelAlert';
  15. import PanelBody from 'sentry/components/panels/panelBody';
  16. import PanelHeader from 'sentry/components/panels/panelHeader';
  17. import Switch from 'sentry/components/switchButton';
  18. import Truncate from 'sentry/components/truncate';
  19. import {IconAdd} from 'sentry/icons';
  20. import {t} from 'sentry/locale';
  21. import type {ServiceHook} from 'sentry/types/integrations';
  22. import {
  23. setApiQueryData,
  24. useApiQuery,
  25. useMutation,
  26. useQueryClient,
  27. } from 'sentry/utils/queryClient';
  28. import useApi from 'sentry/utils/useApi';
  29. import useOrganization from 'sentry/utils/useOrganization';
  30. import {useParams} from 'sentry/utils/useParams';
  31. import withOrganization from 'sentry/utils/withOrganization';
  32. import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
  33. type RowProps = {
  34. hook: ServiceHook;
  35. onToggleActive: () => void;
  36. orgId: string;
  37. projectId: string;
  38. };
  39. function ServiceHookRow({orgId, projectId, hook, onToggleActive}: RowProps) {
  40. return (
  41. <FieldGroup
  42. label={
  43. <Link
  44. data-test-id="project-service-hook"
  45. to={`/settings/${orgId}/projects/${projectId}/hooks/${hook.id}/`}
  46. >
  47. <Truncate value={hook.url} />
  48. </Link>
  49. }
  50. help={
  51. <small>
  52. {hook.events && hook.events.length !== 0 ? (
  53. hook.events.join(', ')
  54. ) : (
  55. <em>{t('no events configured')}</em>
  56. )}
  57. </small>
  58. }
  59. >
  60. <Switch isActive={hook.status === 'active'} size="lg" toggle={onToggleActive} />
  61. </FieldGroup>
  62. );
  63. }
  64. function ProjectServiceHooks() {
  65. const organization = useOrganization();
  66. const {projectId} = useParams<{projectId: string}>();
  67. const api = useApi({persistInFlight: true});
  68. const queryClient = useQueryClient();
  69. const {
  70. data: hookList,
  71. isPending,
  72. isError,
  73. refetch,
  74. } = useApiQuery<ServiceHook[]>([`/projects/${organization.slug}/${projectId}/hooks/`], {
  75. staleTime: 0,
  76. });
  77. const onToggleActiveMutation = useMutation({
  78. mutationFn: ({hook}: {hook: ServiceHook}) => {
  79. return api.requestPromise(
  80. `/projects/${organization.slug}/${projectId}/hooks/${hook.id}/`,
  81. {
  82. method: 'PUT',
  83. data: {
  84. isActive: hook.status !== 'active',
  85. },
  86. }
  87. );
  88. },
  89. onMutate: () => {
  90. addLoadingMessage(t('Saving changes\u2026'));
  91. },
  92. onSuccess: data => {
  93. clearIndicators();
  94. setApiQueryData<ServiceHook[]>(
  95. queryClient,
  96. [`/projects/${organization.slug}/${projectId}/hooks/`],
  97. oldHookList => {
  98. return oldHookList.map(h => {
  99. if (h.id === data.id) {
  100. return {
  101. ...h,
  102. ...data,
  103. };
  104. }
  105. return h;
  106. });
  107. }
  108. );
  109. },
  110. onError: () => {
  111. addErrorMessage(t('Unable to remove application. Please try again.'));
  112. },
  113. });
  114. if (isPending) {
  115. return <LoadingIndicator />;
  116. }
  117. if (isError) {
  118. return <LoadingError onRetry={refetch} />;
  119. }
  120. const renderEmpty = () => {
  121. return (
  122. <EmptyMessage>
  123. {t('There are no service hooks associated with this project.')}
  124. </EmptyMessage>
  125. );
  126. };
  127. const renderResults = () => {
  128. return (
  129. <Fragment>
  130. <PanelHeader key="header">{t('Service Hook')}</PanelHeader>
  131. <PanelBody key="body">
  132. <PanelAlert type="info" showIcon>
  133. {t(
  134. 'Service Hooks are an early adopter preview feature and will change in the future.'
  135. )}
  136. </PanelAlert>
  137. {hookList?.map(hook => (
  138. <ServiceHookRow
  139. key={hook.id}
  140. orgId={organization.slug}
  141. projectId={projectId}
  142. hook={hook}
  143. onToggleActive={() => onToggleActiveMutation.mutate({hook})}
  144. />
  145. ))}
  146. </PanelBody>
  147. </Fragment>
  148. );
  149. };
  150. const body = hookList && hookList.length > 0 ? renderResults() : renderEmpty();
  151. return (
  152. <Fragment>
  153. <SettingsPageHeader
  154. title={t('Service Hooks')}
  155. action={
  156. organization.access.includes('project:write') ? (
  157. <LinkButton
  158. data-test-id="new-service-hook"
  159. to={`/settings/${organization.slug}/projects/${projectId}/hooks/new/`}
  160. size="sm"
  161. priority="primary"
  162. icon={<IconAdd isCircled />}
  163. >
  164. {t('Create New Hook')}
  165. </LinkButton>
  166. ) : null
  167. }
  168. />
  169. <Panel>{body}</Panel>
  170. </Fragment>
  171. );
  172. }
  173. export default withOrganization(ProjectServiceHooks);