FormWrapper.tsx 12 KB

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