awsLambdaFunctionSelect.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. import {Component, Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import reduce from 'lodash/reduce';
  4. import {computed, makeObservable} from 'mobx';
  5. import {Observer} from 'mobx-react';
  6. import Form from 'sentry/components/forms/form';
  7. import JsonForm from 'sentry/components/forms/jsonForm';
  8. import FormModel from 'sentry/components/forms/model';
  9. import {JsonFormObject} from 'sentry/components/forms/type';
  10. import List from 'sentry/components/list';
  11. import ListItem from 'sentry/components/list/listItem';
  12. import LoadingIndicator from 'sentry/components/loadingIndicator';
  13. import {PanelHeader} from 'sentry/components/panels';
  14. import Switch from 'sentry/components/switchButton';
  15. import Tooltip from 'sentry/components/tooltip';
  16. import {t, tn} from 'sentry/locale';
  17. import FooterWithButtons from './components/footerWithButtons';
  18. import HeaderWithHelp from './components/headerWithHelp';
  19. const LAMBDA_COUNT_THRESHOLD = 10;
  20. type LambdaFunction = {FunctionName: string; Runtime: string};
  21. type Props = {
  22. initialStepNumber: number;
  23. lambdaFunctions: LambdaFunction[];
  24. };
  25. type State = {
  26. submitting: boolean;
  27. };
  28. const getLabel = (func: LambdaFunction) => func.FunctionName;
  29. export default class AwsLambdaFunctionSelect extends Component<Props, State> {
  30. constructor(props: Props) {
  31. super(props);
  32. makeObservable(this, {allStatesToggled: computed});
  33. }
  34. state: State = {
  35. submitting: false,
  36. };
  37. model = new FormModel();
  38. get initialData() {
  39. const {lambdaFunctions} = this.props;
  40. const initialData = lambdaFunctions.reduce((accum, func) => {
  41. accum[func.FunctionName] = true;
  42. return accum;
  43. }, {});
  44. return initialData;
  45. }
  46. get lambdaFunctions() {
  47. return this.props.lambdaFunctions.sort((a, b) =>
  48. getLabel(a).toLowerCase() < getLabel(b).toLowerCase() ? -1 : 1
  49. );
  50. }
  51. get enabledCount() {
  52. const data = this.model.getTransformedData();
  53. return reduce(data, (acc: number, val: boolean) => (val ? acc + 1 : acc), 0);
  54. }
  55. get allStatesToggled() {
  56. // check if any of the lambda functions have a falsy value
  57. // no falsy values means everything is enabled
  58. return Object.values(this.model.getData()).every(val => val);
  59. }
  60. get formFields() {
  61. const data = this.model.getTransformedData();
  62. return Object.entries(data).map(([name, value]) => ({name, value}));
  63. }
  64. handleSubmit = () => {
  65. this.setState({submitting: true});
  66. };
  67. handleToggle = () => {
  68. const newState = !this.allStatesToggled;
  69. this.lambdaFunctions.forEach(lambda => {
  70. this.model.setValue(lambda.FunctionName, newState, {quiet: true});
  71. });
  72. };
  73. renderWhatWeFound = () => {
  74. const count = this.lambdaFunctions.length;
  75. return (
  76. <h4>
  77. {tn(
  78. 'We found %s function with a Node or Python runtime',
  79. 'We found %s functions with Node or Python runtimes',
  80. count
  81. )}
  82. </h4>
  83. );
  84. };
  85. renderLoadingScreen = () => {
  86. const count = this.enabledCount;
  87. const text =
  88. count > LAMBDA_COUNT_THRESHOLD
  89. ? t('This might take a while\u2026', count)
  90. : t('This might take a sec\u2026');
  91. return (
  92. <LoadingWrapper>
  93. <StyledLoadingIndicator />
  94. <h4>{t('Adding Sentry to %s functions', count)}</h4>
  95. {text}
  96. </LoadingWrapper>
  97. );
  98. };
  99. renderCore = () => {
  100. const {initialStepNumber} = this.props;
  101. const FormHeader = (
  102. <StyledPanelHeader>
  103. {t('Lambda Functions')}
  104. <SwitchHolder>
  105. <Observer>
  106. {() => (
  107. <Tooltip
  108. title={this.allStatesToggled ? t('Disable All') : t('Enable All')}
  109. position="left"
  110. >
  111. <StyledSwitch
  112. size="lg"
  113. name="toggleAll"
  114. toggle={this.handleToggle}
  115. isActive={this.allStatesToggled}
  116. />
  117. </Tooltip>
  118. )}
  119. </Observer>
  120. </SwitchHolder>
  121. </StyledPanelHeader>
  122. );
  123. const formFields: JsonFormObject = {
  124. fields: this.lambdaFunctions.map(func => ({
  125. name: func.FunctionName,
  126. type: 'boolean',
  127. required: false,
  128. label: getLabel(func),
  129. alignRight: true,
  130. })),
  131. };
  132. return (
  133. <List symbol="colored-numeric" initialCounterValue={initialStepNumber}>
  134. <ListItem>
  135. <Header>{this.renderWhatWeFound()}</Header>
  136. {t('Decide which functions you would like to enable for Sentry monitoring')}
  137. <StyledForm
  138. initialData={this.initialData}
  139. model={this.model}
  140. apiEndpoint="/extensions/aws_lambda/setup/"
  141. hideFooter
  142. preventFormResetOnUnmount
  143. >
  144. <JsonForm renderHeader={() => FormHeader} forms={[formFields]} />
  145. </StyledForm>
  146. </ListItem>
  147. <Fragment />
  148. </List>
  149. );
  150. };
  151. render() {
  152. return (
  153. <Fragment>
  154. <HeaderWithHelp docsUrl="https://docs.sentry.io/product/integrations/cloud-monitoring/aws-lambda/" />
  155. <Wrapper>
  156. {this.state.submitting ? this.renderLoadingScreen() : this.renderCore()}
  157. </Wrapper>
  158. <Observer>
  159. {() => (
  160. <FooterWithButtons
  161. formProps={{
  162. action: '/extensions/aws_lambda/setup/',
  163. method: 'post',
  164. onSubmit: this.handleSubmit,
  165. }}
  166. formFields={this.formFields}
  167. buttonText={t('Finish Setup')}
  168. disabled={
  169. this.model.isError || this.model.isSaving || this.state.submitting
  170. }
  171. />
  172. )}
  173. </Observer>
  174. </Fragment>
  175. );
  176. }
  177. }
  178. const Wrapper = styled('div')`
  179. padding: 100px 50px 50px 50px;
  180. `;
  181. // TODO(ts): Understand why styled is not correctly inheriting props here
  182. const StyledForm = styled(Form)<Form['props']>`
  183. margin-top: 10px;
  184. `;
  185. const Header = styled('div')`
  186. text-align: left;
  187. margin-bottom: 10px;
  188. `;
  189. const LoadingWrapper = styled('div')`
  190. padding: 50px;
  191. text-align: center;
  192. `;
  193. const StyledLoadingIndicator = styled(LoadingIndicator)`
  194. margin: 0;
  195. `;
  196. const SwitchHolder = styled('div')`
  197. display: flex;
  198. `;
  199. const StyledSwitch = styled(Switch)`
  200. margin: auto;
  201. `;
  202. // padding is based on fom control width
  203. const StyledPanelHeader = styled(PanelHeader)`
  204. padding-right: 36px;
  205. `;