import React, { Component } from "react"; import styled from "styled-components"; import _ from "lodash"; import { Section, FormElement } from "shared/types"; import { Context } from "shared/Context"; import TabRegion from "components/TabRegion"; import ValuesForm from "components/values-form/ValuesForm"; import SaveButton from "../SaveButton"; type PropsType = { formData: any; onSubmit?: (formValues: any) => void; saveValuesStatus?: string | null; saveButtonText?: string | null; // Handle additional non-form tabs // TODO: find cleaner way to share submitValues w/ rerun jobs button renderTabContents?: (currentTab: string, submitValues?: any) => any; tabOptions?: any[]; tabOptionsOnly?: boolean; // Allow external control of state valuesToOverride?: any; clearValuesToOverride?: () => void; // External values made available to all child components externalValues?: any; // Display and debugger settings isInModal?: boolean; isReadOnly?: boolean; showStateDebugger?: boolean; // TabRegion props to pass through color?: string; addendum?: any; }; type StateType = { metaState: any; requiredFields: string[]; currentTab: string; tabOptions: { value: string; label: string }[]; }; /** * Renders from raw JSON form data and manages form state. * * To control values using external state prop in "valuesToOverride" (refer to * FormDebugger or LaunchTemplate for example usage). * * TODO: Handle passing in valuesToOverride at same time as formData */ export default class FormWrapper extends Component { state = { metaState: {} as any, requiredFields: [] as string[], currentTab: "", tabOptions: [] as { value: string; label: string }[], }; updateTabs = (resetState?: boolean, callback?: any) => { if (resetState) { let tabOptions = [] as { value: string; label: string }[]; let tabs = this.props.formData?.tabs; let requiredFields = [] as string[]; let metaState: any = { "currentCluster.service.is_gcp": { value: this.context.currentCluster.service == "gke", }, "currentCluster.service.is_aws": { value: this.context.currentCluster.service == "eks", }, "currentCluster.service.is_do": { value: this.context.currentCluster.service == "doks", }, }; tabs?.forEach((tab: any, i: number) => { // Exclude value if omitFromLaunch is set let omit = tab.settings?.omitFromLaunch && this.props.externalValues?.isLaunch; if (tab?.name && tab.label && !omit) { // If a tab is valid, extract state tab.sections?.forEach((section: Section, i: number) => { section?.contents?.forEach((item: FormElement, i: number) => { if (item === null || item === undefined) { return; } if ( item.type === "variable" && item.variable && item.settings?.default ) { metaState[item.variable] = { value: item.settings.default }; return; } // If no name is assigned use values.yaml variable as identifier let key = item.name || item.variable; let def = item.settings && item.settings.unit && !item.settings.omitUnitFromValue ? `${item.settings.default}${item.settings.unit}` : item.settings?.default; def = (item.value && item.value[0]) || def; if (item.type === "checkbox") { def = item.value && item.value[0]; } // Handle add to list of required fields if (item.required && key) { requiredFields.push(key); } let value: any = def; switch (item.type) { case "checkbox": value = def || false; break; case "string-input": value = def || ""; break; case "string-input-password": value = def || item.settings.default; case "array-input": value = def || []; break; case "env-key-value-array": value = def || {}; break; case "key-value-array": value = def || {}; break; case "number-input": value = def?.toString() ? def : ""; break; case "select": value = def || item.settings.options[0].value; break; case "provider-select": let providerMap: any = { gke: "gcp", eks: "aws", doks: "do", }; def = providerMap[this.context.currentCluster.service]; value = def || "aws"; break; case "base-64": value = def || ""; case "base-64-password": value = def || ""; default: } if (value !== null && value !== undefined) { metaState[key] = { value }; } }); }); if (!this.props.tabOptionsOnly) { tabOptions.push({ value: tab.name, label: tab.label }); } } }); if (this.props.tabOptions?.length > 0) { let prependTabs = [] as { value: string; label: string }[]; let appendTabs = [] as { value: string; label: string }[]; this.props.tabOptions.forEach( (tab: { value: string; label: string }) => { if (tab.value === "status" || tab.value === "metrics") { prependTabs.push(tab); } else { appendTabs.push(tab); } } ); tabOptions = prependTabs.concat(tabOptions.concat(appendTabs)); } if (tabOptions.length > 0) { this.setState( { tabOptions: tabOptions, currentTab: this.state.currentTab === "" ? tabOptions[0].value : this.state.currentTab, metaState, requiredFields: requiredFields, }, callback ); } else { this.setState({ tabOptions }, callback); } } else { // TODO: refactor by consolidating w/ above // Handle change only to external tabs (e.g. DevOps mode toggle) let tabOptions = [] as { value: string; label: string }[]; let tabs = this.props.formData?.tabs; tabs?.forEach((tab: any, i: number) => { if (tab?.name && tab.label) { tabOptions.push({ value: tab.name, label: tab.label }); } }); if (this.props.tabOptions?.length > 0) { let prependTabs = [] as { value: string; label: string }[]; let appendTabs = [] as { value: string; label: string }[]; this.props.tabOptions.forEach( (tab: { value: string; label: string }) => { if (tab.value === "status" || tab.value === "metrics") { prependTabs.push(tab); } else { appendTabs.push(tab); } } ); tabOptions = prependTabs.concat(tabOptions.concat(appendTabs)); } this.setState({ tabOptions }, callback); } }; componentDidMount() { this.updateTabs(true, () => { this.setState( { metaState: { ...this.state.metaState, ...this.props.valuesToOverride, }, }, () => { this.props.clearValuesToOverride && this.props.clearValuesToOverride(); } ); }); } componentDidUpdate(prevProps: any) { // Override metaState values set from outside FormWrapper if ( this.props.valuesToOverride && !_.isEmpty(this.props.valuesToOverride) && !_.isEqual(prevProps.valuesToOverride, this.props.valuesToOverride) ) { this.setState( { metaState: { ...this.state.metaState, ...this.props.valuesToOverride, }, }, () => { // Seems redundant with below but need to ensure no leaked state updates if ( !_.isEqual(prevProps.tabOptions, this.props.tabOptions) || !_.isEqual(prevProps.formData, this.props.formData) ) { let formHasChanged = !_.isEqual( prevProps.formData, this.props.formData ); this.updateTabs(formHasChanged); } this.props.clearValuesToOverride && this.props.clearValuesToOverride(); } ); } else if ( !_.isEqual(prevProps.tabOptions, this.props.tabOptions) || !_.isEqual(prevProps.formData, this.props.formData) ) { if ( prevProps.tabOptions?.length === 0 && !_.isEqual(prevProps.tabOptions, this.props.tabOptions) ) { this.setState({ currentTab: "status" }); } let formHasChanged = !_.isEqual(prevProps.formData, this.props.formData); this.updateTabs(formHasChanged); } } isSet = (value: any) => { if ( value === null || value === undefined || value === "" || value === false ) { return false; } return true; }; isDisabled = () => { if (this.props.saveValuesStatus == "loading") { return true; } let requiredMissing = false; this.state.requiredFields?.forEach((requiredKey: string, i: number) => { if (!this.isSet(this.state.metaState[requiredKey]?.value)) { requiredMissing = true; } }); return requiredMissing; }; renderTabContents = () => { let tabs = this.props.formData?.tabs; if (tabs) { let matchedTab = null as any; tabs.forEach((tab: any, i: number) => { if (tab?.name === this.state.currentTab) { matchedTab = tab; } }); if (matchedTab) { return ( { let metaState: any = this.state.metaState; metaState[key] = { value }; this.setState({ metaState }); }} sections={matchedTab.sections} /> ); } } // If no form tabs match, check against external tabs if (this.props.renderTabContents) { // TODO: find a cleaner way to share submissionValues w/ rerun button let submissionValues: any = {}; Object.keys(this.state.metaState)?.forEach((key: string, i: number) => { submissionValues[key] = this.state.metaState[key]?.value; }); return this.props.renderTabContents( this.state.currentTab, submissionValues ); } return
No matched tabs found.
; }; renderStateDebugger = () => { if (this.props.showStateDebugger) { return ( <>
FormWrapper State
{JSON.stringify(this.state.metaState, undefined, 2)}
); } }; handleSubmit = () => { // Extract metaState values let submissionValues: any = {}; Object.keys(this.state.metaState)?.forEach((key: string, i: number) => { submissionValues[key] = this.state.metaState[key]?.value; }); this.props.onSubmit && this.props.onSubmit(submissionValues); }; showSaveButton = (): boolean => { if (this.props.isReadOnly || this.state.tabOptions?.length === 0) { return false; } let tabs = this.props.formData?.tabs; if (tabs) { let matchedTab = null as any; tabs.forEach((tab: any, i: number) => { if (tab?.name === this.state.currentTab) { matchedTab = tab; } }); if (matchedTab) { return true; } } // Check if current tab is among non-form tab options let nonFormTabValues = this.props.tabOptions?.map((tab: any, i: number) => { return tab.value; }); if (nonFormTabValues && nonFormTabValues.includes(this.state.currentTab)) { return false; } return true; }; renderContents = (showSave: boolean) => { return ( <> this.setState({ currentTab: x })} addendum={this.props.addendum} color={this.props.color} > {this.renderTabContents()} {showSave && ( )} {this.renderStateDebugger()} ); }; render() { let showSave = this.showSaveButton(); return ( <> {this.props.isInModal || !showSave ? ( {this.renderContents(showSave)} ) : ( {this.renderContents(showSave)} )} ); } } FormWrapper.contextType = Context; const Spacer = styled.div` width: 100%; height: 200px; background: red; position: relative; `; const TabWrapper = styled.div` min-height: 100px; display: flex; align-items: center; justify-content: center; `; const ScrollWrapper = styled.div` padding: 20px; overflow-y: auto; max-height: 300px; padding-top: 15px; `; const Header = styled.div` width: 100%; height: 40px; color: #ffffff; font-weight: 500; padding-left: 17px; background: #00000022; display: flex; align-items: center; `; const StateDisplay = styled.pre` width: 100%; font-size: 13px; display: overflow: hidden; border-radius: 5px; position: relative; line-height: 1.5em; color: #aaaabb; background: #ffffff11; `; const StyledValuesWrapper = styled.div<{ showSave: boolean }>` width: 100%; padding: 0; height: ${(props) => (props.showSave ? "calc(100% - 55px)" : "100%")}; `; const PaddedWrapper = styled.div` padding-bottom: 65px; position: relative; `;