integrationServerlessRow.tsx 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. import {Fragment, useCallback, useMemo, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import {
  4. addErrorMessage,
  5. addLoadingMessage,
  6. addSuccessMessage,
  7. } from 'sentry/actionCreators/indicator';
  8. import {Button} from 'sentry/components/button';
  9. import Switch from 'sentry/components/switchButton';
  10. import {t} from 'sentry/locale';
  11. import {space} from 'sentry/styles/space';
  12. import type {
  13. OrganizationIntegration,
  14. ServerlessFunction,
  15. } from 'sentry/types/integrations';
  16. import {trackAnalytics} from 'sentry/utils/analytics';
  17. import useApi from 'sentry/utils/useApi';
  18. import useOrganization from 'sentry/utils/useOrganization';
  19. interface IntegrationServerlessRowProps {
  20. integration: OrganizationIntegration;
  21. onUpdate: (serverlessFunctionUpdate: Partial<ServerlessFunction>) => void;
  22. serverlessFunction: ServerlessFunction;
  23. }
  24. export function IntegrationServerlessRow({
  25. integration,
  26. onUpdate,
  27. serverlessFunction,
  28. }: IntegrationServerlessRowProps) {
  29. const api = useApi();
  30. const organization = useOrganization();
  31. const [isSubmitting, setIsSubmitting] = useState(false);
  32. const endpoint = `/organizations/${organization.slug}/integrations/${integration.id}/serverless-functions/`;
  33. const {version} = serverlessFunction;
  34. // during optimistic update, we might be enabled without a version
  35. const versionText =
  36. serverlessFunction.enabled && version > 0 ? (
  37. <Fragment>&nbsp;|&nbsp;v{version}</Fragment>
  38. ) : null;
  39. const recordAction = useCallback(
  40. (action: 'enable' | 'disable' | 'updateVersion') => {
  41. trackAnalytics('integrations.serverless_function_action', {
  42. integration: integration.provider.key,
  43. integration_type: 'first_party',
  44. action,
  45. organization,
  46. });
  47. },
  48. [integration.provider.key, organization]
  49. );
  50. const handleUpdate = useCallback(async () => {
  51. const data = {
  52. action: 'updateVersion',
  53. target: serverlessFunction.name,
  54. };
  55. try {
  56. setIsSubmitting(true);
  57. // don't know the latest version but at least optimistically remove the update button
  58. onUpdate({outOfDate: false});
  59. addLoadingMessage();
  60. recordAction('updateVersion');
  61. const resp = await api.requestPromise(endpoint, {
  62. method: 'POST',
  63. data,
  64. });
  65. // update remaining after response
  66. onUpdate(resp);
  67. addSuccessMessage(t('Success'));
  68. } catch (err) {
  69. // restore original on failure
  70. onUpdate(serverlessFunction);
  71. addErrorMessage(err.responseJSON?.detail ?? t('Error occurred'));
  72. }
  73. setIsSubmitting(false);
  74. }, [api, endpoint, onUpdate, recordAction, serverlessFunction]);
  75. const handleToggle = useCallback(async () => {
  76. const action = serverlessFunction.enabled ? 'disable' : 'enable';
  77. const data = {
  78. action,
  79. target: serverlessFunction.name,
  80. };
  81. try {
  82. addLoadingMessage();
  83. setIsSubmitting(true);
  84. // optimistically update enable state
  85. onUpdate({enabled: !serverlessFunction.enabled});
  86. recordAction(action);
  87. const resp = await api.requestPromise(endpoint, {
  88. method: 'POST',
  89. data,
  90. });
  91. // update remaining after response
  92. onUpdate(resp);
  93. addSuccessMessage(t('Success'));
  94. } catch (err) {
  95. // restore original on failure
  96. onUpdate(serverlessFunction);
  97. addErrorMessage(err.responseJSON?.detail ?? t('Error occurred'));
  98. }
  99. setIsSubmitting(false);
  100. }, [api, endpoint, onUpdate, recordAction, serverlessFunction]);
  101. const layerStatus = useMemo(() => {
  102. if (!serverlessFunction.outOfDate) {
  103. return serverlessFunction.enabled ? t('Latest') : t('Disabled');
  104. }
  105. return (
  106. <UpdateButton size="sm" priority="primary" onClick={handleUpdate}>
  107. {t('Update')}
  108. </UpdateButton>
  109. );
  110. }, [serverlessFunction.outOfDate, serverlessFunction.enabled, handleUpdate]);
  111. return (
  112. <Item>
  113. <NameWrapper>
  114. <NameRuntimeVersionWrapper>
  115. <Name>{serverlessFunction.name}</Name>
  116. <RuntimeAndVersion>
  117. <DetailWrapper>{serverlessFunction.runtime}</DetailWrapper>
  118. <DetailWrapper>{versionText}</DetailWrapper>
  119. </RuntimeAndVersion>
  120. </NameRuntimeVersionWrapper>
  121. </NameWrapper>
  122. <LayerStatusWrapper>{layerStatus}</LayerStatusWrapper>
  123. <StyledSwitch
  124. isActive={serverlessFunction.enabled}
  125. isDisabled={isSubmitting}
  126. size="sm"
  127. toggle={handleToggle}
  128. />
  129. </Item>
  130. );
  131. }
  132. const Item = styled('div')`
  133. padding: ${space(2)};
  134. &:not(:last-child) {
  135. border-bottom: 1px solid ${p => p.theme.innerBorder};
  136. }
  137. display: grid;
  138. grid-column-gap: ${space(1)};
  139. align-items: center;
  140. grid-template-columns: 2fr 1fr 0.5fr;
  141. grid-template-areas: 'function-name layer-status enable-switch';
  142. `;
  143. const ItemWrapper = styled('span')`
  144. height: 32px;
  145. vertical-align: middle;
  146. display: flex;
  147. align-items: center;
  148. `;
  149. const NameWrapper = styled(ItemWrapper)`
  150. grid-area: function-name;
  151. `;
  152. const LayerStatusWrapper = styled(ItemWrapper)`
  153. grid-area: layer-status;
  154. `;
  155. const StyledSwitch = styled(Switch)`
  156. grid-area: enable-switch;
  157. `;
  158. const UpdateButton = styled(Button)``;
  159. const NameRuntimeVersionWrapper = styled('div')`
  160. display: flex;
  161. flex-direction: column;
  162. `;
  163. const Name = styled(`span`)`
  164. padding-bottom: ${space(1)};
  165. `;
  166. const RuntimeAndVersion = styled('div')`
  167. display: flex;
  168. flex-direction: row;
  169. color: ${p => p.theme.gray300};
  170. `;
  171. const DetailWrapper = styled('div')`
  172. line-height: 1.2;
  173. `;