Browse Source

basic launch/expanded formwrapper replacement

jusrhee 5 years ago
parent
commit
1c8135009f

+ 2 - 2
dashboard/src/components/values-form/CheckboxRow.tsx

@@ -5,7 +5,7 @@ type PropsType = {
   label: string;
   checked: boolean;
   toggle: () => void;
-  required?: boolean;
+  isRequired?: boolean;
 };
 
 type StateType = {};
@@ -19,7 +19,7 @@ export default class CheckboxRow extends Component<PropsType, StateType> {
             <i className="material-icons">done</i>
           </Checkbox>
           {this.props.label}
-          {this.props.required && <Required>*</Required>}
+          {this.props.isRequired && <Required>*</Required>}
         </CheckboxWrapper>
       </StyledCheckboxRow>
     );

+ 24 - 12
dashboard/src/components/values-form/FormDebugger.tsx

@@ -5,7 +5,7 @@ import FormWrapper from "components/values-form/FormWrapper";
 import CheckboxRow from "components/values-form/CheckboxRow";
 import yaml from "js-yaml";
 
-import "shared/ace-porter-theme"
+import "shared/ace-porter-theme";
 import "ace-builds/src-noconflict/mode-text";
 
 import Heading from "./Heading";
@@ -31,15 +31,16 @@ export default class FormDebugger extends Component<PropsType, StateType> {
     rawYaml: "",
     showBonusTabs: false,
     showStateDebugger: true,
-  }
+  };
 
   renderTabContents = (currentTab: string) => {
+    console.log("oofok");
     return (
       <TabWrapper>
         {this.state.rawYaml.toString().slice(0, 300) || "No raw YAML inputted."}
       </TabWrapper>
-    )
-  }
+    );
+  };
 
   aceEditorRef = React.createRef<AceEditor>();
   render() {
@@ -47,7 +48,7 @@ export default class FormDebugger extends Component<PropsType, StateType> {
     try {
       formData = yaml.load(this.state.rawYaml);
     } catch (err: any) {
-      console.log("YAML parsing error.")
+      console.log("YAML parsing error.");
     }
     return (
       <StyledFormDebugger>
@@ -69,11 +70,11 @@ export default class FormDebugger extends Component<PropsType, StateType> {
             editorProps={{ $blockScrolling: true }}
             height="300px"
             width="100%"
-            style={{ 
-              borderRadius: "5px", 
+            style={{
+              borderRadius: "5px",
               border: "1px solid #ffffff22",
               marginTop: "27px",
-              marginBottom: "27px"
+              marginBottom: "27px",
             }}
             showPrintMargin={false}
             showGutter={true}
@@ -84,12 +85,16 @@ export default class FormDebugger extends Component<PropsType, StateType> {
         <CheckboxRow
           label="Show form state debugger"
           checked={this.state.showStateDebugger}
-          toggle={() => this.setState({ showStateDebugger: !this.state.showStateDebugger })}
+          toggle={() =>
+            this.setState({ showStateDebugger: !this.state.showStateDebugger })
+          }
         />
         <CheckboxRow
           label="Include non-form dummy tabs"
           checked={this.state.showBonusTabs}
-          toggle={() => this.setState({ showBonusTabs: !this.state.showBonusTabs })}
+          toggle={() =>
+            this.setState({ showBonusTabs: !this.state.showBonusTabs })
+          }
         />
 
         <Heading>🎨 Rendered Form</Heading>
@@ -98,7 +103,14 @@ export default class FormDebugger extends Component<PropsType, StateType> {
           showStateDebugger={this.state.showStateDebugger}
           formData={formData}
           tabOptions={this.state.showBonusTabs ? tabOptions : []}
-          renderTabContents={this.state.showBonusTabs ? this.renderTabContents : null}
+          renderTabContents={
+            this.state.showBonusTabs ? this.renderTabContents : null
+          }
+          onSubmit={(values: any) => {
+            alert("Check console output.");
+            console.log("Raw submission values:");
+            console.log(values);
+          }}
         />
       </StyledFormDebugger>
     );
@@ -178,4 +190,4 @@ const Button = styled.div`
     margin-right: 5px;
     justify-content: center;
   }
-`;
+`;

+ 208 - 136
dashboard/src/components/values-form/FormWrapper.tsx

@@ -5,6 +5,7 @@ import { Section, FormElement } from "shared/types";
 import { Context } from "shared/Context";
 import TabRegion from "components/TabRegion";
 import ValuesForm from "components/values-form/ValuesForm";
+import _ from "lodash";
 
 import SaveButton from "../SaveButton";
 
@@ -15,114 +16,139 @@ type PropsType = {
   saveValuesStatus?: string | null;
   isInModal?: boolean;
   renderTabContents?: (currentTab: string) => any;
-  tabOptions: any[];
+  tabOptions?: any[];
+
+  // TabRegion props to pass through
+  color?: string;
+  addendum?: any;
   // overrideValues?: any;
 };
 
 type StateType = {
   currentTab: string;
-  tabOptions: { value: string, label: string }[];
+  tabOptions: { value: string; label: string }[];
   metaState: any;
+  requiredFields: string[];
 };
 
 export default class FormWrapper extends Component<PropsType, StateType> {
   state = {
     currentTab: "",
-    tabOptions: null as { value: string, label: string }[],
-    metaState: {},
-  }
-
-  updateTabs = () => {
-    let tabOptions = [] as { value: string, label: string }[];
-    let tabs = this.props.formData?.tabs;
-    let requiredFields = [] as string[];
-    let metaState: any = {};
-    if (tabs) {
-      tabs.forEach((tab: any, i: number) => {
-        if (tab.name && tab.label) {
+    tabOptions: [] as { value: string; label: string }[],
+    metaState: {} as any,
+    requiredFields: [] as string[],
+  };
 
-          // If a tab is valid, first extract state
-          tab.sections.forEach((section: Section, i: number) => {
-            section.contents.forEach((item: FormElement, i: number) => {
+  updateTabs = (resetState?: boolean) => {
+    if (resetState) {
+      let tabOptions = [] as { value: string; label: string }[];
+      let tabs = this.props.formData?.tabs;
+      let requiredFields = [] as string[];
+      let metaState: any = {};
+      if (tabs) {
+        tabs.forEach((tab: any, i: number) => {
+          if (tab.name && tab.label) {
+            // If a tab is valid, first extract state
+            tab.sections.forEach((section: Section, i: number) => {
+              section.contents.forEach((item: FormElement, i: number) => {
+                // If no name is assigned use values.yaml variable as identifier
+                let key = item.name || item.variable;
 
-              // 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.default}${item.settings.unit}`
+                    : item.settings?.default;
+                def = (item.value && item.value[0]) || def;
 
-              let def =
-                item.settings && item.settings.unit
-                  ? `${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];
+                }
 
-              if (item.type === "checkbox") {
-                def = item.value && item.value[0];
-              }
+                // Handle add to list of required fields
+                if (item.required && key) {
+                  requiredFields.push(key);
+                }
 
-              // 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 };
-              }
+                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 };
+                }
+              });
             });
-          });
-          tabOptions.push({ value: tab.name, label: tab.label });
-        }
-      });
-    }
-    if (this.props.tabOptions.length > 0) {
-      tabOptions = tabOptions.concat(this.props.tabOptions);
-    }
-    if (tabOptions.length > 0) {
-      this.setState({ 
-        tabOptions: tabOptions,
-        currentTab: tabOptions[0].value,
-        metaState,
-      });
+            tabOptions.push({ value: tab.name, label: tab.label });
+          }
+        });
+      }
+      if (this.props.tabOptions?.length > 0) {
+        tabOptions = tabOptions.concat(this.props.tabOptions);
+      }
+      if (tabOptions.length > 0) {
+        this.setState({
+          tabOptions: tabOptions,
+          currentTab: tabOptions[0].value,
+          metaState,
+          requiredFields: requiredFields,
+        });
+      } else {
+        this.setState({ tabOptions });
+      }
+    } 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;
+      if (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) {
+        tabOptions = tabOptions.concat(this.props.tabOptions);
+      }
+      this.setState({ tabOptions });
     }
-  }
+  };
 
   componentDidMount() {
     this.updateTabs();
@@ -130,16 +156,34 @@ export default class FormWrapper extends Component<PropsType, StateType> {
 
   componentDidUpdate(prevProps: any) {
     if (
-      prevProps.tabOptions !== this.props.tabOptions || 
-      prevProps.formData !== this.props.formData
+      !_.isEqual(prevProps.tabOptions, this.props.tabOptions) ||
+      !_.isEqual(prevProps.formData, this.props.formData)
     ) {
       this.updateTabs();
     }
   }
 
+  isSet = (value: any) => {
+    if (
+      value === null ||
+      value === undefined ||
+      value === "" ||
+      value === false
+    ) {
+      return false;
+    }
+    return true;
+  };
+
   isDisabled = () => {
-    return false;
-  }
+    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;
@@ -168,10 +212,9 @@ export default class FormWrapper extends Component<PropsType, StateType> {
     // If no form tabs match, check against external tabs
     if (this.props.renderTabContents) {
       return this.props.renderTabContents(this.state.currentTab);
-    } else {
-      return <h1>nothin</h1>
     }
-  }
+    return <div>No matched tabs found.</div>;
+  };
 
   renderStateDebugger = () => {
     if (this.props.showStateDebugger) {
@@ -184,54 +227,76 @@ export default class FormWrapper extends Component<PropsType, StateType> {
             </ScrollWrapper>
           </StateDisplay>
         </>
-      )
+      );
     }
-  }
+  };
+
+  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);
+  };
 
-  renderContents = () => {
+  showSaveButton = (): boolean => {
+    // 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 (
       <>
-        <TabWrapper>
-          <TabRegion
-            options={this.state.tabOptions}
-            currentTab={this.state.currentTab}
-            setCurrentTab={(x: string) => this.setState({ currentTab: x })}
-          >
-            {this.renderTabContents()}
-          </TabRegion>
-        </TabWrapper>
-        <SaveButton
-          disabled={this.isDisabled()}
-          text="Deploy"
-          onClick={() => this.props.onSubmit(this.state)}
-          status={
-            this.isDisabled()
-              ? "Missing required fields"
-              : this.props.saveValuesStatus
-          }
-          makeFlush={true}
-        />
+        <TabRegion
+          options={this.state.tabOptions}
+          currentTab={this.state.currentTab}
+          setCurrentTab={(x: string) => this.setState({ currentTab: x })}
+          addendum={this.props.addendum}
+          color={this.props.color}
+        >
+          {this.renderTabContents()}
+        </TabRegion>
+        {showSave && (
+          <SaveButton
+            disabled={this.isDisabled()}
+            text="Deploy"
+            onClick={this.handleSubmit}
+            status={
+              this.isDisabled()
+                ? "Missing required fields"
+                : this.props.saveValuesStatus
+            }
+            makeFlush={!this.props.isInModal}
+          />
+        )}
         {this.renderStateDebugger()}
       </>
     );
-  }
+  };
 
   render() {
+    let showSave = this.showSaveButton();
     return (
       <>
-        { 
-          this.props.isInModal ? (
-            <StyledValuesWrapper>
-              {this.renderContents()}
+        {this.props.isInModal ? (
+          <StyledValuesWrapper showSave={showSave}>
+            {this.renderContents(showSave)}
+          </StyledValuesWrapper>
+        ) : (
+          <PaddedWrapper>
+            <StyledValuesWrapper showSave={showSave}>
+              {this.renderContents(showSave)}
             </StyledValuesWrapper>
-          ) : (
-            <PaddedWrapper>
-              <StyledValuesWrapper>
-                {this.renderContents()}
-              </StyledValuesWrapper>
-            </PaddedWrapper>
-          )
-        }
+          </PaddedWrapper>
+        )}
       </>
     );
   }
@@ -239,6 +304,13 @@ export default class FormWrapper extends Component<PropsType, StateType> {
 
 FormWrapper.contextType = Context;
 
+const Spacer = styled.div`
+  width: 100%;
+  height: 200px;
+  background: red;
+  position: relative;
+`;
+
 const TabWrapper = styled.div`
   min-height: 100px;
   display: flex;
@@ -249,7 +321,7 @@ const TabWrapper = styled.div`
 const ScrollWrapper = styled.div`
   padding: 20px;
   overflow-y: auto;
-  max-height: 400px;
+  max-height: 300px;
   padding-top: 15px;
 `;
 
@@ -276,10 +348,10 @@ const StateDisplay = styled.pre`
   background: #ffffff11;
 `;
 
-const StyledValuesWrapper = styled.div`
+const StyledValuesWrapper = styled.div<{ showSave: boolean }>`
   width: 100%;
   padding: 0;
-  height: calc(100% - 65px);
+  height: ${(props) => (props.showSave ? "calc(100% - 55px)" : "100%")};
 `;
 
 const PaddedWrapper = styled.div`

+ 14 - 14
dashboard/src/components/values-form/ValuesForm.tsx

@@ -39,10 +39,9 @@ export default class ValuesForm extends Component<PropsType, StateType> {
     }
     return value;
   };
-  
+
   renderSection = (section: Section) => {
     return section.contents.map((item: FormElement, i: number) => {
-      
       // If no name is assigned use values.yaml variable as identifier
       let key = item.name || item.variable;
 
@@ -54,7 +53,7 @@ export default class ValuesForm extends Component<PropsType, StateType> {
         case "resource-list":
           if (Array.isArray(item.value)) {
             return (
-              <ResourceList key={i}>
+              <ResourceList key={key}>
                 {item.value.map((resource: any, i: number) => {
                   return (
                     <ExpandableResource
@@ -71,7 +70,8 @@ export default class ValuesForm extends Component<PropsType, StateType> {
         case "checkbox":
           return (
             <CheckboxRow
-              key={i}
+              key={key}
+              isRequired={item.required}
               checked={this.props.metaState[key]?.value}
               toggle={() =>
                 this.props.setMetaState(key, !this.props.metaState[key]?.value)
@@ -82,7 +82,7 @@ export default class ValuesForm extends Component<PropsType, StateType> {
         case "env-key-value-array":
           return (
             <KeyValueArray
-              key={i}
+              key={key}
               envLoader={true}
               namespace={this.props.namespace}
               clusterId={this.props.clusterId}
@@ -106,7 +106,7 @@ export default class ValuesForm extends Component<PropsType, StateType> {
         case "key-value-array":
           return (
             <KeyValueArray
-              key={i}
+              key={key}
               namespace={this.props.namespace}
               clusterId={this.props.clusterId}
               values={this.props.metaState[key]?.value}
@@ -128,7 +128,7 @@ export default class ValuesForm extends Component<PropsType, StateType> {
         case "array-input":
           return (
             <InputArray
-              key={i}
+              key={key}
               values={this.props.metaState[key]?.value}
               setValues={(x: string[]) => {
                 this.props.setMetaState(key, x);
@@ -140,7 +140,7 @@ export default class ValuesForm extends Component<PropsType, StateType> {
         case "string-input":
           return (
             <InputRow
-              key={i}
+              key={key}
               isRequired={item.required}
               type="text"
               value={this.getInputValue(item)}
@@ -158,7 +158,7 @@ export default class ValuesForm extends Component<PropsType, StateType> {
         case "string-input-password":
           return (
             <InputRow
-              key={i}
+              key={key}
               isRequired={item.required}
               type="password"
               value={this.getInputValue(item)}
@@ -176,7 +176,7 @@ export default class ValuesForm extends Component<PropsType, StateType> {
         case "number-input":
           return (
             <InputRow
-              key={i}
+              key={key}
               isRequired={item.required}
               type="number"
               value={this.getInputValue(item)}
@@ -202,7 +202,7 @@ export default class ValuesForm extends Component<PropsType, StateType> {
         case "select":
           return (
             <SelectRow
-              key={i}
+              key={key}
               value={this.props.metaState[key]?.value}
               setActiveValue={(val) => this.props.setMetaState(key, val)}
               options={item.settings.options}
@@ -213,7 +213,7 @@ export default class ValuesForm extends Component<PropsType, StateType> {
         case "provider-select":
           return (
             <SelectRow
-              key={i}
+              key={key}
               value={this.props.metaState[key]?.value}
               setActiveValue={(val) => this.props.setMetaState(key, val)}
               options={[
@@ -230,7 +230,7 @@ export default class ValuesForm extends Component<PropsType, StateType> {
         case "base-64":
           return (
             <Base64InputRow
-              key={i}
+              key={key}
               isRequired={item.required}
               type="text"
               value={this.getInputValue(item)}
@@ -248,7 +248,7 @@ export default class ValuesForm extends Component<PropsType, StateType> {
         case "base-64-password":
           return (
             <Base64InputRow
-              key={i}
+              key={key}
               isRequired={item.required}
               type="password"
               value={this.getInputValue(item)}

+ 36 - 83
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx

@@ -16,9 +16,7 @@ import api from "shared/api";
 import ConfirmOverlay from "components/ConfirmOverlay";
 import Loading from "components/Loading";
 import StatusIndicator from "components/StatusIndicator";
-import TabRegion from "components/TabRegion";
-import ValuesWrapper from "components/values-form/ValuesWrapper";
-import ValuesForm from "components/values-form/ValuesForm";
+import FormWrapper from "components/values-form/FormWrapper";
 import RevisionSection from "./RevisionSection";
 import ValuesYaml from "./ValuesYaml";
 import GraphSection from "./GraphSection";
@@ -45,8 +43,6 @@ type StateType = {
   isPreview: boolean;
   devOpsMode: boolean;
   tabOptions: any[];
-  tabContents: any;
-  currentTab: string | null;
   saveValuesStatus: string | null;
   forceRefreshRevisions: boolean; // Update revisions after upgrading values
   controllers: Record<string, Record<string, any>>;
@@ -54,6 +50,7 @@ type StateType = {
   url: string | null;
   showDeleteOverlay: boolean;
   deleting: boolean;
+  formData: any;
 };
 
 export default class ExpandedChart extends Component<PropsType, StateType> {
@@ -66,8 +63,6 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
     isPreview: false,
     devOpsMode: localStorage.getItem("devOpsMode") === "true",
     tabOptions: [] as any[],
-    tabContents: [] as any,
-    currentTab: null as string | null,
     saveValuesStatus: null as string | null,
     forceRefreshRevisions: false,
     controllers: {} as Record<string, Record<string, any>>,
@@ -75,6 +70,7 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
     url: null as string | null,
     showDeleteOverlay: false,
     deleting: false,
+    formData: {} as any,
   };
 
   // Retrieve full chart data (includes form and values)
@@ -102,14 +98,6 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
           { currentChart: res.data, loading: false },
           res.data
         );
-        // // if the current tab is manifests or chart overview, update components as well
-        // if (this.state.currentTab == "graph" || this.state.currentTab == "list") {
-        //   this.updateComponents({ currentChart: res.data, loading: false }, currentChart);
-        // } else {
-        //   this.setState({ currentChart: res.data, loading: false }, () => {
-        //     this.updateTabs()
-        //   })
-        // }
       })
       .catch(console.log);
   };
@@ -295,15 +283,8 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
       });
   };
 
-  renderTabContents = () => {
-    let {
-      currentTab,
-      podSelectors,
-      components,
-      showRevisions,
-      saveValuesStatus,
-      tabOptions,
-    } = this.state;
+  renderTabContents = (currentTab: string) => {
+    let { components, showRevisions } = this.state;
     let { setSidebar } = this.props;
     let { currentChart } = this.state;
     let chart = currentChart;
@@ -347,56 +328,17 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
           <ValuesYaml currentChart={chart} refreshChart={this.refreshChart} />
         );
       default:
-        if (tabOptions && currentTab && currentTab.includes("@")) {
-          return (
-            <ValuesWrapper
-              formTabs={tabOptions}
-              onSubmit={this.onSubmit}
-              saveValuesStatus={this.state.saveValuesStatus}
-              isInModal={true}
-              currentTab={currentTab}
-              renderSaveButton={true}
-            >
-              {(metaState: any, setMetaState: any) => {
-                return tabOptions.map((tab: any, i: number) => {
-                  // If tab is current, render
-                  if (tab.value === currentTab) {
-                    return (
-                      <ValuesForm
-                        key={i}
-                        metaState={metaState}
-                        setMetaState={setMetaState}
-                        sections={tab.sections}
-                        // For env group loader
-                        namespace={this.props.namespace}
-                      />
-                    );
-                  }
-                });
-              }}
-            </ValuesWrapper>
-          );
-        }
     }
   };
 
   updateTabs() {
     let formData = this.state.currentChart.form;
-    let tabOptions = [] as any[];
-
-    // Generate form tabs if form.yaml exists
     if (formData) {
-      formData.tabs.map((tab: any, i: number) => {
-        tabOptions.push({
-          value: "@" + tab.name,
-          label: tab.label,
-          sections: tab.sections,
-          context: tab.context,
-        });
-      });
+      this.setState({ formData });
     }
 
-    // Append universal tabs
+    // Collate non-form tabs
+    let tabOptions = [] as any[];
     tabOptions.push({ label: "Status", value: "status" });
 
     if (this.props.isMetricsInstalled) {
@@ -714,23 +656,28 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
               status={status}
             />
           </HeaderWrapper>
-
-          <TabRegion
-            currentTab={this.state.currentTab}
-            setCurrentTab={(x: string) => this.setState({ currentTab: x })}
-            options={this.state.tabOptions}
-            color={this.state.isPreview ? "#f5cb42" : null}
-            addendum={
-              <TabButton
-                onClick={this.toggleDevOpsMode}
-                devOpsMode={this.state.devOpsMode}
-              >
-                <i className="material-icons">offline_bolt</i> DevOps Mode
-              </TabButton>
-            }
-          >
-            {this.renderTabContents()}
-          </TabRegion>
+          <BodyWrapper>
+            <FormWrapper
+              formData={this.state.formData}
+              tabOptions={this.state.tabOptions}
+              isInModal={true}
+              renderTabContents={this.renderTabContents}
+              onSubmit={(values: any) => {
+                alert("Check console output.");
+                console.log("Raw submission values:");
+                console.log(values);
+              }}
+              color={this.state.isPreview ? "#f5cb42" : null}
+              addendum={
+                <TabButton
+                  onClick={this.toggleDevOpsMode}
+                  devOpsMode={this.state.devOpsMode}
+                >
+                  <i className="material-icons">offline_bolt</i> DevOps Mode
+                </TabButton>
+              }
+            />
+          </BodyWrapper>
         </StyledExpandedChart>
       </>
     );
@@ -739,6 +686,12 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
 
 ExpandedChart.contextType = Context;
 
+const BodyWrapper = styled.div`
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+`;
+
 const DeleteOverlay = styled.div`
   position: absolute;
   top: 0px;

+ 50 - 48
dashboard/src/main/home/dashboard/Dashboard.tsx

@@ -74,9 +74,9 @@ class Dashboard extends Component<PropsType, StateType> {
     let { pressingK, pressingCtrl } = this.state;
     if (e.key === "Meta" || e.key === "Control") {
       this.setState({ pressingCtrl: true });
-    } 
+    }
     if (e.key === "k") {
-      this.setState({ pressingK: true }); 
+      this.setState({ pressingK: true });
     }
     if (e.key === "z" && pressingK && pressingCtrl) {
       this.setState({ pressingK: false, pressingCtrl: false });
@@ -86,8 +86,8 @@ class Dashboard extends Component<PropsType, StateType> {
 
   handleKeyUp = (e: KeyboardEvent): void => {
     if (e.key === "Meta" || e.key === "Control" || e.key === "k") {
-      this.setState({ pressingCtrl: false, pressingK: false, });
-    } 
+      this.setState({ pressingCtrl: false, pressingK: false });
+    }
   };
 
   componentDidUpdate(prevProps: PropsType) {
@@ -135,50 +135,52 @@ class Dashboard extends Component<PropsType, StateType> {
       <>
         {currentProject && (
           <DashboardWrapper>
-            {
-              this.state.showFormDebugger ? (
-                <FormDebugger 
-                  goBack={() => this.setState({ showFormDebugger: false })} 
-                />
-              ) : (
-                <>
-                  <TitleSection>
-                    <DashboardIcon>
-                      <DashboardImage src={gradient} />
-                      <Overlay>
-                        {currentProject && currentProject.name[0].toUpperCase()}
-                      </Overlay>
-                    </DashboardIcon>
-                    <Title>{currentProject && currentProject.name}</Title>
-                    {this.context.currentProject.roles.filter((obj: any) => {
-                      return obj.user_id === this.context.user.userId;
-                    })[0].kind === "admin" && (
-                      <i className="material-icons" onClick={onShowProjectSettings}>
-                        more_vert
-                      </i>
-                    )}
-                  </TitleSection>
-
-                  <InfoSection>
-                    <TopRow>
-                      <InfoLabel>
-                        <i className="material-icons">info</i> Info
-                      </InfoLabel>
-                    </TopRow>
-                    <Description>
-                      Project overview for {currentProject && currentProject.name}.
-                    </Description>
-                  </InfoSection>
-                  <TabRegion
-                    currentTab={this.currentTab()}
-                    setCurrentTab={this.setCurrentTab}
-                    options={tabOptions}
-                  >
-                    {this.renderTabContents()}
-                  </TabRegion>
-                </>
-              )
-            }
+            {this.state.showFormDebugger ? (
+              <FormDebugger
+                goBack={() => this.setState({ showFormDebugger: false })}
+              />
+            ) : (
+              <>
+                <TitleSection>
+                  <DashboardIcon>
+                    <DashboardImage src={gradient} />
+                    <Overlay>
+                      {currentProject && currentProject.name[0].toUpperCase()}
+                    </Overlay>
+                  </DashboardIcon>
+                  <Title>{currentProject && currentProject.name}</Title>
+                  {this.context.currentProject.roles.filter((obj: any) => {
+                    return obj.user_id === this.context.user.userId;
+                  })[0].kind === "admin" && (
+                    <i
+                      className="material-icons"
+                      onClick={onShowProjectSettings}
+                    >
+                      more_vert
+                    </i>
+                  )}
+                </TitleSection>
+
+                <InfoSection>
+                  <TopRow>
+                    <InfoLabel>
+                      <i className="material-icons">info</i> Info
+                    </InfoLabel>
+                  </TopRow>
+                  <Description>
+                    Project overview for {currentProject && currentProject.name}
+                    .
+                  </Description>
+                </InfoSection>
+                <TabRegion
+                  currentTab={this.currentTab()}
+                  setCurrentTab={this.setCurrentTab}
+                  options={tabOptions}
+                >
+                  {this.renderTabContents()}
+                </TabRegion>
+              </>
+            )}
           </DashboardWrapper>
         )}
       </>

+ 1 - 1
dashboard/src/main/home/launch/expanded-template/ExpandedTemplate.tsx

@@ -55,7 +55,7 @@ export default class ExpandedTemplate extends Component<PropsType, StateType> {
       .then((res) => {
         let { form, values, markdown, metadata } = res.data;
         let keywords = metadata.keywords;
-        console.log(form)
+        console.log(form);
         this.setState({
           form,
           values,

+ 14 - 88
dashboard/src/main/home/launch/expanded-template/LaunchTemplate.tsx

@@ -19,8 +19,7 @@ import TabRegion from "components/TabRegion";
 import InputRow from "components/values-form/InputRow";
 import SaveButton from "components/SaveButton";
 import ActionConfEditor from "components/repo-selector/ActionConfEditor";
-import ValuesWrapper from "components/values-form/ValuesWrapper";
-import ValuesForm from "components/values-form/ValuesForm";
+import FormWrapper from "components/values-form/FormWrapper";
 import RadioSelector from "components/RadioSelector";
 import { isAlphanumeric } from "shared/common";
 
@@ -96,7 +95,7 @@ class LaunchTemplate extends Component<PropsType, StateType> {
     env: {},
     overrideValues: {
       abba: false,
-      tracy: "mcgrady"
+      tracy: "mcgrady",
     },
   };
 
@@ -419,54 +418,6 @@ class LaunchTemplate extends Component<PropsType, StateType> {
     return "No application source specified";
   };
 
-  renderTabContents = () => {
-    return (
-      <ValuesWrapper
-        formTabs={this.props.form?.tabs}
-        onSubmit={
-          this.props.currentTab === "docker"
-            ? this.onSubmit
-            : this.onSubmitAddon
-        }
-        saveValuesStatus={this.getStatus()}
-        disabled={this.submitIsDisabled()}
-        renderSaveButton={true}
-        overrideValues={this.state.overrideValues}
-      >
-        {(metaState: any, setMetaState: any) => {
-          if (!metaState) {
-            return;
-          }
-
-          // handle when procfileProcess is already specified
-          if (this.state.procfileProcess) {
-            metaState["container.command"] = this.state.procfileProcess;
-          }
-
-          return this.props.form?.tabs.map((tab: any, i: number) => {
-            // If tab is current, render
-            if (tab.name === this.state.currentTab) {
-              return (
-                <ValuesForm
-                  metaState={metaState}
-                  handleEnvChange={(x: any) => this.setState({ env: x })}
-                  setMetaState={setMetaState}
-                  key={tab.name}
-                  sections={tab.sections}
-                  // For env group loader
-                  namespace={this.state.selectedNamespace}
-                  clusterId={this.state.selectedClusterId}
-                  // For procfile process
-                  procfileProcess={this.state.procfileProcess}
-                />
-              );
-            }
-          });
-        }}
-      </ValuesWrapper>
-    );
-  };
-
   componentDidMount() {
     if (this.props.currentTemplate.name !== "docker") {
       this.setState({ saveValuesStatus: "" });
@@ -552,13 +503,14 @@ class LaunchTemplate extends Component<PropsType, StateType> {
           <Subtitle>
             Configure additional settings for this template. (Optional)
           </Subtitle>
-          <TabRegion
-            options={this.state.tabOptions}
-            currentTab={this.state.currentTab}
-            setCurrentTab={(x: string) => this.setState({ currentTab: x })}
-          >
-            {this.renderTabContents()}
-          </TabRegion>
+          <FormWrapper
+            formData={this.props.form}
+            onSubmit={(values: any) => {
+              alert("Check console output.");
+              console.log("Raw submission values:");
+              console.log(values);
+            }}
+          />
         </>
       );
     } else {
@@ -649,32 +601,6 @@ class LaunchTemplate extends Component<PropsType, StateType> {
           <br />
         </StyledSourceBox>
       );
-    } else if (this.state.repoType === "" && false) {
-      return (
-        <StyledSourceBox>
-          <CloseButton onClick={() => this.setState({ sourceType: "" })}>
-            <CloseButtonImg src={close} />
-          </CloseButton>
-          <Subtitle>
-            Are you using an existing Dockerfile from your repo?
-            <Required>*</Required>
-          </Subtitle>
-          <RadioSelector
-            options={[
-              {
-                value: "dockerfile",
-                label: "Yes, I am using an existing Dockerfile",
-              },
-              {
-                value: "buildpack",
-                label: "No, I am not using an existing Dockerfile",
-              },
-            ]}
-            selected={this.state.repoType}
-            setSelected={(x: string) => this.setState({ repoType: x })}
-          />
-        </StyledSourceBox>
-      );
     } else {
       return (
         <StyledSourceBox>
@@ -703,12 +629,12 @@ class LaunchTemplate extends Component<PropsType, StateType> {
             }
             procfileProcess={this.state.procfileProcess}
             setProcfileProcess={(procfileProcess: string) =>
-              this.setState({ 
-                procfileProcess, 
+              this.setState({
+                procfileProcess,
                 overrideValues: {
                   "container.command": procfileProcess || "",
-                  showStartCommand: !procfileProcess
-                } 
+                  showStartCommand: !procfileProcess,
+                },
               })
             }
             setBranch={(branch: string) => this.setState({ branch })}

+ 1 - 1
dashboard/src/main/home/provisioner/AWSFormSection.tsx

@@ -429,7 +429,7 @@ class AWSFormSection extends Component<PropsType, StateType> {
             .
           </Helper>
           <CheckboxRow
-            required={true}
+            isRequired={true}
             checked={this.state.provisionConfirmed}
             toggle={() =>
               this.setState({

+ 1 - 1
dashboard/src/main/home/provisioner/DOFormSection.tsx

@@ -280,7 +280,7 @@ export default class DOFormSection extends Component<PropsType, StateType> {
             .
           </Helper>
           <CheckboxRow
-            required={true}
+            isRequired={true}
             checked={this.state.provisionConfirmed}
             toggle={() =>
               this.setState({

+ 1 - 1
dashboard/src/main/home/provisioner/GCPFormSection.tsx

@@ -388,7 +388,7 @@ class GCPFormSection extends Component<PropsType, StateType> {
             .
           </Helper>
           <CheckboxRow
-            required={true}
+            isRequired={true}
             checked={this.state.provisionConfirmed}
             toggle={() =>
               this.setState({