FormWrapper.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  1. import React, { Component } from "react";
  2. import styled from "styled-components";
  3. import _ from "lodash";
  4. import { Section, FormElement } from "shared/types";
  5. import { Context } from "shared/Context";
  6. import TabRegion from "components/TabRegion";
  7. import ValuesForm from "components/values-form/ValuesForm";
  8. import SaveButton from "../SaveButton";
  9. type PropsType = {
  10. formData: any;
  11. onSubmit?: (formValues: any) => void;
  12. saveValuesStatus?: string | null;
  13. // Handle additional non-form tabs
  14. // TODO: find cleaner way to share submitValues w/ rerun jobs button
  15. renderTabContents?: (currentTab: string, submitValues?: any) => any;
  16. tabOptions?: any[];
  17. tabOptionsOnly?: boolean;
  18. // Allow external control of state
  19. valuesToOverride?: any;
  20. clearValuesToOverride?: () => void;
  21. // External values made available to all child components
  22. externalValues?: any;
  23. // Display and debugger settings
  24. isInModal?: boolean;
  25. isReadOnly?: boolean;
  26. showStateDebugger?: boolean;
  27. // TabRegion props to pass through
  28. color?: string;
  29. addendum?: any;
  30. };
  31. type StateType = {
  32. metaState: any;
  33. requiredFields: string[];
  34. currentTab: string;
  35. tabOptions: { value: string; label: string }[];
  36. };
  37. /**
  38. * Renders from raw JSON form data and manages form state.
  39. *
  40. * To control values using external state prop in "valuesToOverride" (refer to
  41. * FormDebugger or LaunchTemplate for example usage).
  42. *
  43. * TODO: Handle passing in valuesToOverride at same time as formData
  44. */
  45. export default class FormWrapper extends Component<PropsType, StateType> {
  46. state = {
  47. metaState: {} as any,
  48. requiredFields: [] as string[],
  49. currentTab: "",
  50. tabOptions: [] as { value: string; label: string }[],
  51. };
  52. updateTabs = (resetState?: boolean, callback?: any) => {
  53. if (resetState) {
  54. let tabOptions = [] as { value: string; label: string }[];
  55. let tabs = this.props.formData?.tabs;
  56. let requiredFields = [] as string[];
  57. let metaState: any = {};
  58. if (tabs) {
  59. tabs.forEach((tab: any, i: number) => {
  60. if (tab?.name && tab.label) {
  61. // If a tab is valid, extract state
  62. tab.sections?.forEach((section: Section, i: number) => {
  63. section?.contents?.forEach((item: FormElement, i: number) => {
  64. if (item === null || item === undefined) {
  65. return;
  66. }
  67. if (
  68. item.type === "variable" &&
  69. item.variable &&
  70. item.settings?.default
  71. ) {
  72. metaState[item.variable] = { value: item.settings.default };
  73. return;
  74. }
  75. // If no name is assigned use values.yaml variable as identifier
  76. let key = item.name || item.variable;
  77. let def =
  78. item.settings &&
  79. item.settings.unit &&
  80. !item.settings.omitUnitFromValue
  81. ? `${item.settings.default}${item.settings.unit}`
  82. : item.settings?.default;
  83. def = (item.value && item.value[0]) || def;
  84. if (item.type === "checkbox") {
  85. def = item.value && item.value[0];
  86. }
  87. // Handle add to list of required fields
  88. if (item.required && key) {
  89. requiredFields.push(key);
  90. }
  91. let value: any = def;
  92. switch (item.type) {
  93. case "checkbox":
  94. value = def || false;
  95. break;
  96. case "string-input":
  97. value = def || "";
  98. break;
  99. case "string-input-password":
  100. value = def || item.settings.default;
  101. case "array-input":
  102. value = def || [];
  103. break;
  104. case "env-key-value-array":
  105. value = def || {};
  106. break;
  107. case "key-value-array":
  108. value = def || {};
  109. break;
  110. case "number-input":
  111. value = def?.toString() ? def : "";
  112. break;
  113. case "select":
  114. value = def || item.settings.options[0].value;
  115. break;
  116. case "provider-select":
  117. let providerMap: any = {
  118. gke: "gcp",
  119. eks: "aws",
  120. doks: "do",
  121. };
  122. def = providerMap[this.context.currentCluster.service];
  123. value = def || "aws";
  124. break;
  125. case "base-64":
  126. value = def || "";
  127. case "base-64-password":
  128. value = def || "";
  129. default:
  130. }
  131. if (value !== null && value !== undefined) {
  132. metaState[key] = { value };
  133. }
  134. });
  135. });
  136. if (!this.props.tabOptionsOnly) {
  137. tabOptions.push({ value: tab.name, label: tab.label });
  138. }
  139. }
  140. });
  141. }
  142. if (this.props.tabOptions?.length > 0) {
  143. tabOptions = tabOptions.concat(this.props.tabOptions);
  144. }
  145. if (tabOptions.length > 0) {
  146. this.setState(
  147. {
  148. tabOptions: tabOptions,
  149. currentTab:
  150. this.state.currentTab === ""
  151. ? tabOptions[0].value
  152. : this.state.currentTab,
  153. metaState,
  154. requiredFields: requiredFields,
  155. },
  156. callback
  157. );
  158. } else {
  159. this.setState({ tabOptions }, callback);
  160. }
  161. } else {
  162. // TODO: refactor by consolidating w/ above
  163. // Handle change only to external tabs (e.g. DevOps mode toggle)
  164. let tabOptions = [] as { value: string; label: string }[];
  165. let tabs = this.props.formData?.tabs;
  166. if (tabs) {
  167. tabs.forEach((tab: any, i: number) => {
  168. if (tab?.name && tab.label) {
  169. tabOptions.push({ value: tab.name, label: tab.label });
  170. }
  171. });
  172. }
  173. if (this.props.tabOptions?.length > 0) {
  174. tabOptions = tabOptions.concat(this.props.tabOptions);
  175. }
  176. this.setState({ tabOptions }, callback);
  177. }
  178. };
  179. componentDidMount() {
  180. this.updateTabs(true, () => {
  181. this.setState(
  182. {
  183. metaState: {
  184. ...this.state.metaState,
  185. ...this.props.valuesToOverride,
  186. },
  187. },
  188. () => {
  189. this.props.clearValuesToOverride &&
  190. this.props.clearValuesToOverride();
  191. }
  192. );
  193. });
  194. }
  195. componentDidUpdate(prevProps: any) {
  196. // Override metaState values set from outside FormWrapper
  197. if (
  198. this.props.valuesToOverride &&
  199. !_.isEmpty(this.props.valuesToOverride) &&
  200. !_.isEqual(prevProps.valuesToOverride, this.props.valuesToOverride)
  201. ) {
  202. this.setState(
  203. {
  204. metaState: {
  205. ...this.state.metaState,
  206. ...this.props.valuesToOverride,
  207. },
  208. },
  209. () => {
  210. // Seems redundant with below but need to ensure no leaked state updates
  211. if (
  212. !_.isEqual(prevProps.tabOptions, this.props.tabOptions) ||
  213. !_.isEqual(prevProps.formData, this.props.formData)
  214. ) {
  215. let formHasChanged = !_.isEqual(
  216. prevProps.formData,
  217. this.props.formData
  218. );
  219. this.updateTabs(formHasChanged);
  220. }
  221. this.props.clearValuesToOverride &&
  222. this.props.clearValuesToOverride();
  223. }
  224. );
  225. } else if (
  226. !_.isEqual(prevProps.tabOptions, this.props.tabOptions) ||
  227. !_.isEqual(prevProps.formData, this.props.formData)
  228. ) {
  229. let formHasChanged = !_.isEqual(prevProps.formData, this.props.formData);
  230. this.updateTabs(formHasChanged);
  231. }
  232. }
  233. isSet = (value: any) => {
  234. if (
  235. value === null ||
  236. value === undefined ||
  237. value === "" ||
  238. value === false
  239. ) {
  240. return false;
  241. }
  242. return true;
  243. };
  244. isDisabled = () => {
  245. if (this.props.saveValuesStatus == "loading") {
  246. return true;
  247. }
  248. let requiredMissing = false;
  249. this.state.requiredFields?.forEach((requiredKey: string, i: number) => {
  250. if (!this.isSet(this.state.metaState[requiredKey]?.value)) {
  251. requiredMissing = true;
  252. }
  253. });
  254. return requiredMissing;
  255. };
  256. renderTabContents = () => {
  257. let tabs = this.props.formData?.tabs;
  258. if (tabs) {
  259. let matchedTab = null as any;
  260. tabs.forEach((tab: any, i: number) => {
  261. if (tab?.name === this.state.currentTab) {
  262. matchedTab = tab;
  263. }
  264. });
  265. if (matchedTab) {
  266. return (
  267. <ValuesForm
  268. externalValues={this.props.externalValues}
  269. disabled={this.props.isReadOnly}
  270. metaState={this.state.metaState}
  271. setMetaState={(key: string, value: any) => {
  272. let metaState: any = this.state.metaState;
  273. metaState[key] = { value };
  274. this.setState({ metaState });
  275. }}
  276. sections={matchedTab.sections}
  277. />
  278. );
  279. }
  280. }
  281. // If no form tabs match, check against external tabs
  282. if (this.props.renderTabContents) {
  283. // TODO: find a cleaner way to share submissionValues w/ rerun button
  284. let submissionValues: any = {};
  285. Object.keys(this.state.metaState)?.forEach((key: string, i: number) => {
  286. submissionValues[key] = this.state.metaState[key]?.value;
  287. });
  288. return this.props.renderTabContents(
  289. this.state.currentTab,
  290. submissionValues
  291. );
  292. }
  293. return <div>No matched tabs found.</div>;
  294. };
  295. renderStateDebugger = () => {
  296. if (this.props.showStateDebugger) {
  297. return (
  298. <>
  299. <StateDisplay>
  300. <Header>FormWrapper State</Header>
  301. <ScrollWrapper>
  302. {JSON.stringify(this.state.metaState, undefined, 2)}
  303. </ScrollWrapper>
  304. </StateDisplay>
  305. </>
  306. );
  307. }
  308. };
  309. handleSubmit = () => {
  310. // Extract metaState values
  311. let submissionValues: any = {};
  312. Object.keys(this.state.metaState)?.forEach((key: string, i: number) => {
  313. submissionValues[key] = this.state.metaState[key]?.value;
  314. });
  315. this.props.onSubmit && this.props.onSubmit(submissionValues);
  316. };
  317. showSaveButton = (): boolean => {
  318. if (this.props.isReadOnly || this.state.tabOptions?.length === 0) {
  319. return false;
  320. }
  321. let tabs = this.props.formData?.tabs;
  322. if (tabs) {
  323. let matchedTab = null as any;
  324. tabs.forEach((tab: any, i: number) => {
  325. if (tab?.name === this.state.currentTab) {
  326. matchedTab = tab;
  327. }
  328. });
  329. if (matchedTab) {
  330. return true;
  331. }
  332. }
  333. // Check if current tab is among non-form tab options
  334. let nonFormTabValues = this.props.tabOptions?.map((tab: any, i: number) => {
  335. return tab.value;
  336. });
  337. if (nonFormTabValues && nonFormTabValues.includes(this.state.currentTab)) {
  338. return false;
  339. }
  340. return true;
  341. };
  342. renderContents = (showSave: boolean) => {
  343. return (
  344. <>
  345. <TabRegion
  346. options={this.state.tabOptions}
  347. currentTab={this.state.currentTab}
  348. setCurrentTab={(x: string) => this.setState({ currentTab: x })}
  349. addendum={this.props.addendum}
  350. color={this.props.color}
  351. >
  352. {this.renderTabContents()}
  353. </TabRegion>
  354. {showSave && (
  355. <SaveButton
  356. disabled={this.isDisabled()}
  357. text="Deploy"
  358. onClick={this.handleSubmit}
  359. status={
  360. this.isDisabled() && this.props.saveValuesStatus != "loading"
  361. ? "Missing required fields"
  362. : this.props.saveValuesStatus
  363. }
  364. makeFlush={!this.props.isInModal}
  365. />
  366. )}
  367. {this.renderStateDebugger()}
  368. </>
  369. );
  370. };
  371. render() {
  372. let showSave = this.showSaveButton();
  373. return (
  374. <>
  375. {this.props.isInModal ? (
  376. <StyledValuesWrapper showSave={showSave}>
  377. {this.renderContents(showSave)}
  378. </StyledValuesWrapper>
  379. ) : (
  380. <PaddedWrapper>
  381. <StyledValuesWrapper showSave={showSave}>
  382. {this.renderContents(showSave)}
  383. </StyledValuesWrapper>
  384. </PaddedWrapper>
  385. )}
  386. </>
  387. );
  388. }
  389. }
  390. FormWrapper.contextType = Context;
  391. const Spacer = styled.div`
  392. width: 100%;
  393. height: 200px;
  394. background: red;
  395. position: relative;
  396. `;
  397. const TabWrapper = styled.div`
  398. min-height: 100px;
  399. display: flex;
  400. align-items: center;
  401. justify-content: center;
  402. `;
  403. const ScrollWrapper = styled.div`
  404. padding: 20px;
  405. overflow-y: auto;
  406. max-height: 300px;
  407. padding-top: 15px;
  408. `;
  409. const Header = styled.div`
  410. width: 100%;
  411. height: 40px;
  412. color: #ffffff;
  413. font-weight: 500;
  414. padding-left: 17px;
  415. background: #00000022;
  416. display: flex;
  417. align-items: center;
  418. `;
  419. const StateDisplay = styled.pre`
  420. width: 100%;
  421. font-size: 13px;
  422. display:
  423. overflow: hidden;
  424. border-radius: 5px;
  425. position: relative;
  426. line-height: 1.5em;
  427. color: #aaaabb;
  428. background: #ffffff11;
  429. `;
  430. const StyledValuesWrapper = styled.div<{ showSave: boolean }>`
  431. width: 100%;
  432. padding: 0;
  433. height: ${(props) => (props.showSave ? "calc(100% - 55px)" : "100%")};
  434. `;
  435. const PaddedWrapper = styled.div`
  436. padding-bottom: 65px;
  437. position: relative;
  438. `;