integrationServerlessRow.tsx 5.6 KB

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