Просмотр исходного кода

Merge branch 'master' of https://github.com/porter-dev/porter

mergin
Alexander Belanger 5 лет назад
Родитель
Сommit
ffd413aa96
33 измененных файлов с 1179 добавлено и 641 удалено
  1. BIN
      dashboard/src/assets/Light Gradient 08.png
  2. BIN
      dashboard/src/assets/gradient.png
  3. 11 1
      dashboard/src/components/TabSelector.tsx
  4. 4 4
      dashboard/src/components/image-selector/TagList.tsx
  5. 0 1
      dashboard/src/components/repo-selector/ContentsList.tsx
  6. 13 9
      dashboard/src/components/values-form/CheckboxRow.tsx
  7. 319 0
      dashboard/src/components/values-form/FormDebugger.tsx
  8. 448 0
      dashboard/src/components/values-form/FormWrapper.tsx
  9. 5 3
      dashboard/src/components/values-form/InputRow.tsx
  10. 4 5
      dashboard/src/components/values-form/KeyValueArray.tsx
  11. 90 68
      dashboard/src/components/values-form/ValuesForm.tsx
  12. 0 177
      dashboard/src/components/values-form/ValuesWrapper.tsx
  13. 13 8
      dashboard/src/main/Main.tsx
  14. 12 11
      dashboard/src/main/auth/Login.tsx
  15. 17 17
      dashboard/src/main/auth/Register.tsx
  16. 6 10
      dashboard/src/main/home/Home.tsx
  17. 0 2
      dashboard/src/main/home/cluster-dashboard/ClusterDashboard.tsx
  18. 0 1
      dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx
  19. 38 83
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx
  20. 25 54
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedJobChart.tsx
  21. 86 42
      dashboard/src/main/home/dashboard/Dashboard.tsx
  22. 44 103
      dashboard/src/main/home/launch/expanded-template/LaunchTemplate.tsx
  23. 0 1
      dashboard/src/main/home/modals/UpdateClusterModal.tsx
  24. 2 4
      dashboard/src/main/home/navbar/Navbar.tsx
  25. 6 2
      dashboard/src/main/home/new-project/NewProject.tsx
  26. 1 1
      dashboard/src/main/home/provisioner/AWSFormSection.tsx
  27. 1 1
      dashboard/src/main/home/provisioner/DOFormSection.tsx
  28. 1 1
      dashboard/src/main/home/provisioner/GCPFormSection.tsx
  29. 12 17
      dashboard/src/main/home/provisioner/ProvisionerSettings.tsx
  30. 3 1
      dashboard/src/main/home/sidebar/ProjectSection.tsx
  31. 1 1
      dashboard/src/shared/Context.tsx
  32. 3 1
      dashboard/src/shared/types.tsx
  33. 14 12
      internal/models/templates.go

BIN
dashboard/src/assets/Light Gradient 08.png


BIN
dashboard/src/assets/gradient.png


+ 11 - 1
dashboard/src/components/TabSelector.tsx

@@ -44,6 +44,7 @@ export default class TabSelector extends Component<PropsType, StateType> {
     return (
       <StyledTabSelector>
         <TabWrapper>
+          <Line />
           {this.renderTabList()}
           <Tab lastItem={true} highlight={null}>
             {this.props.noBuffer ? null : <Buffer />}
@@ -55,6 +56,16 @@ export default class TabSelector extends Component<PropsType, StateType> {
   }
 }
 
+const Line = styled.div`
+  height: 1px;
+  position: absolute;
+  top: 29px;
+  z-index: 0;
+  left: 0;
+  background: #aaaabb55;
+  width: 100%;
+`;
+
 const Buffer = styled.div`
   width: 138px;
   height: 10px;
@@ -98,7 +109,6 @@ const StyledTabSelector = styled.div`
   display: flex;
   width: calc(100% - 2px);
   align-items: center;
-  border-bottom: 1px solid #aaaabb55;
   padding-bottom: 1px;
   margin-left: 1px;
   position: relative;

+ 4 - 4
dashboard/src/components/image-selector/TagList.tsx

@@ -8,7 +8,7 @@ import { Context } from "shared/Context";
 
 import Loading from "../Loading";
 
-var ecrRepoRegex = /(^[a-zA-Z0-9][a-zA-Z0-9-_]*)\.dkr\.ecr(\-fips)?\.([a-zA-Z0-9][a-zA-Z0-9-_]*)\.amazonaws\.com(\.cn)?/igm
+var ecrRepoRegex = /(^[a-zA-Z0-9][a-zA-Z0-9-_]*)\.dkr\.ecr(\-fips)?\.([a-zA-Z0-9][a-zA-Z0-9-_]*)\.amazonaws\.com(\.cn)?/gim;
 
 type PropsType = {
   setSelectedTag: (x: string) => void;
@@ -38,12 +38,12 @@ export default class TagList extends Component<PropsType, StateType> {
     let splits = this.props.selectedImageUrl.split("/");
     let repoName = splits[splits.length - 1];
 
-    let matches = this.props.selectedImageUrl.match(ecrRepoRegex)
+    let matches = this.props.selectedImageUrl.match(ecrRepoRegex);
 
     if (matches) {
-      repoName = this.props.selectedImageUrl.split(/\/(.+)/)[1]
+      repoName = this.props.selectedImageUrl.split(/\/(.+)/)[1];
     }
-  
+
     api
       .getImageTags(
         "<token>",

+ 0 - 1
dashboard/src/components/repo-selector/ContentsList.tsx

@@ -213,7 +213,6 @@ export default class ContentsList extends Component<PropsType, StateType> {
   };
 
   renderOverlay = () => {
-    console.log(this.props.procfilePath);
     if (this.props.procfilePath) {
       let processes = this.state.processes
         ? Object.keys(this.state.processes)

+ 13 - 9
dashboard/src/components/values-form/CheckboxRow.tsx

@@ -5,7 +5,8 @@ type PropsType = {
   label: string;
   checked: boolean;
   toggle: () => void;
-  required?: boolean;
+  isRequired?: boolean;
+  disabled?: boolean;
 };
 
 type StateType = {};
@@ -14,12 +15,15 @@ export default class CheckboxRow extends Component<PropsType, StateType> {
   render() {
     return (
       <StyledCheckboxRow>
-        <CheckboxWrapper onClick={this.props.toggle}>
+        <CheckboxWrapper
+          disabled={this.props.disabled}
+          onClick={!this.props.disabled && this.props.toggle}
+        >
           <Checkbox checked={this.props.checked}>
             <i className="material-icons">done</i>
           </Checkbox>
           {this.props.label}
-          {this.props.required && <Required>*</Required>}
+          {this.props.isRequired && <Required>*</Required>}
         </CheckboxWrapper>
       </StyledCheckboxRow>
     );
@@ -31,10 +35,11 @@ const Required = styled.section`
   color: #fc4976;
 `;
 
-const CheckboxWrapper = styled.div`
+const CheckboxWrapper = styled.div<{ disabled?: boolean }>`
   display: flex;
   align-items: center;
-  cursor: pointer;
+  cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
+  font-size: 13px;
   :hover {
     > div {
       background: #ffffff22;
@@ -42,14 +47,13 @@ const CheckboxWrapper = styled.div`
   }
 `;
 
-const Checkbox = styled.div`
+const Checkbox = styled.div<{ checked: boolean }>`
   width: 16px;
   height: 16px;
   border: 1px solid #ffffff55;
   margin: 1px 10px 0px 1px;
   border-radius: 3px;
-  background: ${(props: { checked: boolean }) =>
-    props.checked ? "#ffffff22" : "#ffffff11"};
+  background: ${(props) => (props.checked ? "#ffffff22" : "#ffffff11")};
   display: flex;
   align-items: center;
   justify-content: center;
@@ -57,7 +61,7 @@ const Checkbox = styled.div`
   > i {
     font-size: 12px;
     padding-left: 0px;
-    display: ${(props: { checked: boolean }) => (props.checked ? "" : "none")};
+    display: ${(props) => (props.checked ? "" : "none")};
   }
 `;
 

+ 319 - 0
dashboard/src/components/values-form/FormDebugger.tsx

@@ -0,0 +1,319 @@
+import React, { Component } from "react";
+import styled from "styled-components";
+import AceEditor from "react-ace";
+import FormWrapper from "components/values-form/FormWrapper";
+import CheckboxRow from "components/values-form/CheckboxRow";
+import InputRow from "components/values-form/InputRow";
+import yaml from "js-yaml";
+
+import "shared/ace-porter-theme";
+import "ace-builds/src-noconflict/mode-text";
+
+import Heading from "./Heading";
+import Helper from "./Helper";
+
+type PropsType = {
+  goBack: () => void;
+};
+
+type StateType = {
+  rawYaml: string;
+  showBonusTabs: boolean;
+  showStateDebugger: boolean;
+  valuesToOverride: any;
+  checkbox_a: boolean;
+  input_a: string;
+  isReadOnly: boolean;
+};
+
+const tabOptions = [
+  { value: "a", label: "Bonus Tab A" },
+  { value: "b", label: "Bonus Tab B" },
+];
+
+export default class FormDebugger extends Component<PropsType, StateType> {
+  state = {
+    rawYaml: initYaml,
+    showBonusTabs: false,
+    showStateDebugger: true,
+    valuesToOverride: {
+      checkbox_a: {
+        value: true,
+      },
+    } as any,
+    checkbox_a: true,
+    input_a: "",
+    isReadOnly: false,
+  };
+
+  renderTabContents = (currentTab: string) => {
+    return (
+      <TabWrapper>
+        {this.state.rawYaml.toString().slice(0, 300) || "No raw YAML inputted."}
+      </TabWrapper>
+    );
+  };
+
+  aceEditorRef = React.createRef<AceEditor>();
+  render() {
+    let formData = {};
+    try {
+      formData = yaml.load(this.state.rawYaml);
+    } catch (err: any) {
+      console.log("YAML parsing error.");
+    }
+    return (
+      <StyledFormDebugger>
+        <Button onClick={this.props.goBack}>
+          <i className="material-icons">keyboard_backspace</i>
+          Back
+        </Button>
+        <Heading>✨ Form.yaml Editor</Heading>
+        <Helper>Write and test form.yaml free of consequence.</Helper>
+
+        <EditorWrapper>
+          <AceEditor
+            ref={this.aceEditorRef}
+            mode="yaml"
+            value={this.state.rawYaml}
+            theme="porter"
+            onChange={(e: string) => this.setState({ rawYaml: e })}
+            name="codeEditor"
+            editorProps={{ $blockScrolling: true }}
+            height="450px"
+            width="100%"
+            style={{
+              borderRadius: "5px",
+              border: "1px solid #ffffff22",
+              marginTop: "27px",
+              marginBottom: "27px",
+            }}
+            showPrintMargin={false}
+            showGutter={true}
+            highlightActiveLine={true}
+          />
+        </EditorWrapper>
+
+        <CheckboxRow
+          label="Show form state debugger"
+          checked={this.state.showStateDebugger}
+          toggle={() =>
+            this.setState({ showStateDebugger: !this.state.showStateDebugger })
+          }
+        />
+        <CheckboxRow
+          label="Read-only"
+          checked={this.state.isReadOnly}
+          toggle={() =>
+            this.setState({
+              isReadOnly: !this.state.isReadOnly,
+            })
+          }
+        />
+        <CheckboxRow
+          label="Include non-form dummy tabs"
+          checked={this.state.showBonusTabs}
+          toggle={() =>
+            this.setState({ showBonusTabs: !this.state.showBonusTabs })
+          }
+        />
+        <CheckboxRow
+          label="checkbox_a"
+          checked={this.state.checkbox_a}
+          toggle={() =>
+            this.setState({
+              checkbox_a: !this.state.checkbox_a,
+
+              // Override the form value for checkbox_a
+              valuesToOverride: {
+                ...this.state.valuesToOverride,
+                checkbox_a: {
+                  value: !this.state.checkbox_a,
+                },
+              },
+            })
+          }
+        />
+        <InputRow
+          type="string"
+          value={this.state.input_a}
+          setValue={(x: string) =>
+            this.setState({
+              input_a: x,
+
+              // Override the form value for input_a
+              valuesToOverride: {
+                ...this.state.valuesToOverride,
+                input_a: {
+                  value: x,
+                },
+              },
+            })
+          }
+          label={"input_a"}
+          placeholder="ex: override text"
+        />
+
+        <Heading>🎨 Rendered Form</Heading>
+        <Br />
+        <FormWrapper
+          valuesToOverride={this.state.valuesToOverride}
+          clearValuesToOverride={() =>
+            this.setState({ valuesToOverride: null })
+          }
+          showStateDebugger={this.state.showStateDebugger}
+          formData={formData}
+          isReadOnly={this.state.isReadOnly}
+          tabOptions={this.state.showBonusTabs ? tabOptions : []}
+          renderTabContents={
+            this.state.showBonusTabs ? this.renderTabContents : null
+          }
+          onSubmit={(values: any) => {
+            alert("Check console output.");
+            console.log("Raw submission values:");
+            console.log(values);
+          }}
+        />
+      </StyledFormDebugger>
+    );
+  }
+}
+
+const Br = styled.div`
+  width: 100%;
+  height: 12px;
+`;
+
+const TabWrapper = styled.div`
+  background: #ffffff11;
+  height: 200px;
+  width: 100%;
+  border-radius: 5px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 13px;
+  overflow: auto;
+  padding: 50px;
+`;
+
+const EditorWrapper = styled.div`
+  .ace_editor,
+  .ace_editor * {
+    font-family: "Monaco", "Menlo", "Ubuntu Mono", "Droid Sans Mono", "Consolas",
+      monospace !important;
+    font-size: 12px !important;
+    font-weight: 400 !important;
+    letter-spacing: 0 !important;
+  }
+`;
+
+const StyledFormDebugger = styled.div`
+  position: relative;
+`;
+
+const Button = styled.div`
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  font-size: 13px;
+  cursor: pointer;
+  font-family: "Work Sans", sans-serif;
+  border-radius: 20px;
+  color: white;
+  height: 35px;
+  margin-left: -2px;
+  padding: 0px 8px;
+  width: 85px;
+  float: right;
+  padding-bottom: 1px;
+  font-weight: 500;
+  padding-right: 15px;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  cursor: pointer;
+  border: 2px solid #969fbbaa;
+  :hover {
+    background: #ffffff11;
+  }
+
+  > i {
+    color: white;
+    width: 18px;
+    height: 18px;
+    color: #969fbbaa;
+    font-weight: 600;
+    font-size: 14px;
+    border-radius: 20px;
+    display: flex;
+    align-items: center;
+    margin-right: 5px;
+    justify-content: center;
+  }
+`;
+
+const initYaml = `name: Porter Example
+hasSource: true
+tabs:
+- name: main
+  label: Main
+  sections:
+  - name: header
+    contents: 
+    - type: heading
+      label: 🍺 Porter Demo Form
+    - type: subtitle
+      name: command_description
+      label: Basic form demonstrating some of the features of form.yaml
+    - type: string-input
+      placeholder: "ex: pilsner"
+      label: Required Field A
+      required: true
+      variable: field_a
+    - type: string-input
+      placeholder: "ex: sapporo"
+      required: true
+      label: Required Field B
+      variable: field_b
+    - type: subtitle
+      label: "Note: Hidden required fields aren't supported yet (global only)"
+  - name: controlled-by-external
+    show_if: checkbox_a
+    contents:
+    - type: heading
+      label: Conditional Display (A)
+    - type: subtitle
+      label: This section can be externally controlled by the value of checkbox_a
+    - type: string-input
+      variable: input_a
+      placeholder: "Override w/ input_a"
+  - name: domain_name
+    show_if: ingress.custom_domain
+    contents:
+    - type: array-input
+      variable: ingress.hosts
+      label: Domain Name
+- name: env
+  label: Environment
+  sections:
+  - name: env_vars
+    contents:
+    - type: heading
+      label: Environment Variables
+    - type: subtitle
+      label: Set environment variables for your secrets and environment-specific configuration.
+    - type: env-key-value-array
+      label: 
+      variable: container.env.normal
+- name: advanced
+  label: Advanced
+  sections:
+  - name: advanced
+    contents:
+    - type: heading
+      label: Some Header
+    - type: subtitle
+      label: Some helper text
+`;

+ 448 - 0
dashboard/src/components/values-form/FormWrapper.tsx

@@ -0,0 +1,448 @@
+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;
+
+  // Handle additional non-form tabs
+  renderTabContents?: (currentTab: string) => 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).
+ */
+export default class FormWrapper extends Component<PropsType, StateType> {
+  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 = {};
+      if (tabs) {
+        tabs.forEach((tab: any, i: number) => {
+          if (tab?.name && tab.label) {
+            // 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] = 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) {
+        tabOptions = tabOptions.concat(this.props.tabOptions);
+      }
+      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;
+      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 }, 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 &&
+      !_.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)
+    ) {
+      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 = () => {
+    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 (
+          <ValuesForm
+            externalValues={this.props.externalValues}
+            disabled={this.props.isReadOnly}
+            metaState={this.state.metaState}
+            setMetaState={(key: string, value: any) => {
+              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) {
+      return this.props.renderTabContents(this.state.currentTab);
+    }
+    return <div>No matched tabs found.</div>;
+  };
+
+  renderStateDebugger = () => {
+    if (this.props.showStateDebugger) {
+      return (
+        <>
+          <StateDisplay>
+            <Header>FormWrapper State</Header>
+            <ScrollWrapper>
+              {JSON.stringify(this.state.metaState, undefined, 2)}
+            </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);
+  };
+
+  showSaveButton = (): boolean => {
+    if (this.props.isReadOnly || this.state.tabOptions?.length === 0) {
+      return false;
+    }
+
+    // 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 (
+      <>
+        <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 showSave={showSave}>
+            {this.renderContents(showSave)}
+          </StyledValuesWrapper>
+        ) : (
+          <PaddedWrapper>
+            <StyledValuesWrapper showSave={showSave}>
+              {this.renderContents(showSave)}
+            </StyledValuesWrapper>
+          </PaddedWrapper>
+        )}
+      </>
+    );
+  }
+}
+
+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;
+`;

+ 5 - 3
dashboard/src/components/values-form/InputRow.tsx

@@ -34,9 +34,11 @@ export default class InputRow extends Component<PropsType, StateType> {
     let { label, value, type, unit, placeholder, width } = this.props;
     return (
       <StyledInputRow>
-        <Label>
-          {label} <Required>{this.props.isRequired ? " *" : null}</Required>
-        </Label>
+        {label && (
+          <Label>
+            {label} <Required>{this.props.isRequired ? " *" : null}</Required>
+          </Label>
+        )}
         <InputWrapper>
           <Input
             readOnly={this.state.readOnly}

+ 4 - 5
dashboard/src/components/values-form/KeyValueArray.tsx

@@ -14,8 +14,7 @@ type PropsType = {
   setValues: (x: any) => void;
   width?: string;
   disabled?: boolean;
-  namespace?: string;
-  clusterId?: number;
+  externalValues?: any;
   envLoader?: boolean;
   fileUpload?: boolean;
   secretOption?: boolean;
@@ -153,8 +152,8 @@ export default class KeyValueArray extends Component<PropsType, StateType> {
           height="342px"
         >
           <LoadEnvGroupModal
-            namespace={this.props.namespace}
-            clusterId={this.props.clusterId}
+            namespace={this.props.externalValues?.namespace}
+            clusterId={this.props.externalValues?.clusterId}
             closeModal={() => this.setState({ showEnvModal: false })}
             setValues={(values: any) => {
               this.props.setValues(values);
@@ -275,7 +274,7 @@ export default class KeyValueArray extends Component<PropsType, StateType> {
                 <i className="material-icons">add</i> Add Row
               </AddRowButton>
               <Spacer />
-              {this.props.namespace && this.props.envLoader && (
+              {this.props.externalValues?.namespace && this.props.envLoader && (
                 <LoadButton
                   onClick={() =>
                     this.setState({ showEnvModal: !this.state.showEnvModal })

+ 90 - 68
dashboard/src/components/values-form/ValuesForm.tsx

@@ -18,12 +18,10 @@ import KeyValueArray from "./KeyValueArray";
 type PropsType = {
   sections?: Section[];
   metaState?: any;
-  setMetaState?: any;
+  setMetaState?: (key: string, value: any) => void;
   handleEnvChange?: (x: any) => void;
   disabled?: boolean;
-  namespace?: string;
-  clusterId?: number;
-  procfileProcess?: string;
+  externalValues?: any;
 };
 
 type StateType = any;
@@ -31,29 +29,32 @@ type StateType = any;
 // Requires an internal representation unlike other values components because metaState value underdetermines input order
 export default class ValuesForm extends Component<PropsType, StateType> {
   getInputValue = (item: FormElement) => {
-    let key = item.name || item.variable;
-    let value = this.props.metaState[key];
+    if (item) {
+      let key = item.name || item.variable;
+      let value = this.props.metaState[key]?.value;
 
-    if (item.settings && item.settings.unit && value && value.includes) {
-      value = value.split(item.settings.unit)[0];
+      if (
+        item.settings &&
+        item.settings.unit &&
+        value &&
+        value.includes &&
+        !item.settings.omitUnitFromValue
+      ) {
+        value = value.split(item.settings.unit)[0];
+      }
+      return value;
     }
-    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;
-
-      // ugly exception to hide start command option when procfile process is set.
-      if (
-        (item.variable === "container.command" ||
-          (item.type == "subtitle" && item.name == "command_description")) &&
-        this.props.procfileProcess
-      ) {
+      if (!item) {
         return;
       }
 
+      // If no name is assigned use values.yaml variable as identifier
+      let key = item.name || item.variable;
+
       switch (item.type) {
         case "heading":
           return <Heading key={i}>{item.label}</Heading>;
@@ -62,7 +63,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
@@ -79,10 +80,12 @@ export default class ValuesForm extends Component<PropsType, StateType> {
         case "checkbox":
           return (
             <CheckboxRow
-              key={i}
-              checked={this.props.metaState[key]}
+              key={key}
+              disabled={this.props.disabled}
+              isRequired={item.required}
+              checked={this.props.metaState[key]?.value}
               toggle={() =>
-                this.props.setMetaState({ [key]: !this.props.metaState[key] })
+                this.props.setMetaState(key, !this.props.metaState[key]?.value)
               }
               label={item.label}
             />
@@ -90,20 +93,19 @@ 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}
-              values={this.props.metaState[key]}
+              externalValues={this.props.externalValues}
+              values={this.props.metaState[key]?.value}
               setValues={(x: any) => {
-                this.props.setMetaState({ [key]: x });
+                this.props.setMetaState(key, x);
 
                 // Need to pull env vars out of form.yaml for createGHA build env vars
                 if (
                   this.props.handleEnvChange &&
                   key === "container.env.normal"
                 ) {
-                  this.props.handleEnvChange(x);
+                  // this.props.handleEnvChange(x);
                 }
               }}
               label={item.label}
@@ -114,21 +116,10 @@ export default class ValuesForm extends Component<PropsType, StateType> {
         case "key-value-array":
           return (
             <KeyValueArray
-              key={i}
-              namespace={this.props.namespace}
-              clusterId={this.props.clusterId}
-              values={this.props.metaState[key]}
-              setValues={(x: any) => {
-                this.props.setMetaState({ [key]: x });
-
-                // Need to pull env vars out of form.yaml for createGHA build env vars
-                if (
-                  this.props.handleEnvChange &&
-                  key === "container.env.normal"
-                ) {
-                  this.props.handleEnvChange(x);
-                }
-              }}
+              key={key}
+              externalValues={this.props.externalValues}
+              values={this.props.metaState[key]?.value}
+              setValues={(x: any) => this.props.setMetaState(key, x)}
               label={item.label}
               disabled={this.props.disabled}
             />
@@ -136,10 +127,10 @@ export default class ValuesForm extends Component<PropsType, StateType> {
         case "array-input":
           return (
             <InputArray
-              key={i}
-              values={this.props.metaState[key]}
+              key={key}
+              values={this.props.metaState[key]?.value}
               setValues={(x: string[]) => {
-                this.props.setMetaState({ [key]: x });
+                this.props.setMetaState(key, x);
               }}
               label={item.label}
               disabled={this.props.disabled}
@@ -148,15 +139,22 @@ export default class ValuesForm extends Component<PropsType, StateType> {
         case "string-input":
           return (
             <InputRow
-              key={i}
+              key={key}
+              placeholder={item.placeholder}
               isRequired={item.required}
               type="text"
               value={this.getInputValue(item)}
               setValue={(x: string) => {
-                if (item.settings && item.settings.unit && x !== "") {
+                console.log("dafuq");
+                if (
+                  item.settings &&
+                  item.settings.unit &&
+                  x !== "" &&
+                  !item.settings.omitUnitFromValue
+                ) {
                   x = x + item.settings.unit;
                 }
-                this.props.setMetaState({ [key]: x });
+                this.props.setMetaState(key, x);
               }}
               label={item.label}
               unit={item.settings ? item.settings.unit : null}
@@ -166,15 +164,20 @@ 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)}
               setValue={(x: string) => {
-                if (item.settings && item.settings.unit && x !== "") {
+                if (
+                  item.settings &&
+                  item.settings.unit &&
+                  x !== "" &&
+                  !item.settings.omitUnitFromValue
+                ) {
                   x = x + item.settings.unit;
                 }
-                this.props.setMetaState({ [key]: x });
+                this.props.setMetaState(key, x);
               }}
               label={item.label}
               unit={item.settings ? item.settings.unit : null}
@@ -184,8 +187,9 @@ export default class ValuesForm extends Component<PropsType, StateType> {
         case "number-input":
           return (
             <InputRow
-              key={i}
+              key={key}
               isRequired={item.required}
+              placeholder={item.placeholder}
               type="number"
               value={this.getInputValue(item)}
               setValue={(x: number) => {
@@ -195,12 +199,17 @@ export default class ValuesForm extends Component<PropsType, StateType> {
                 }
 
                 // Convert to string if unit is set
-                if (item.settings && item.settings.unit) {
+                console.log("huh", item);
+                if (
+                  item.settings &&
+                  item.settings.unit &&
+                  !item.settings.omitUnitFromValue
+                ) {
                   val = x.toString();
                   val = val + item.settings.unit;
                 }
 
-                this.props.setMetaState({ [key]: val });
+                this.props.setMetaState(key, val);
               }}
               label={item.label}
               unit={item.settings ? item.settings.unit : null}
@@ -210,9 +219,9 @@ export default class ValuesForm extends Component<PropsType, StateType> {
         case "select":
           return (
             <SelectRow
-              key={i}
-              value={this.props.metaState[key]}
-              setActiveValue={(val) => this.props.setMetaState({ [key]: val })}
+              key={key}
+              value={this.props.metaState[key]?.value}
+              setActiveValue={(val) => this.props.setMetaState(key, val)}
               options={item.settings.options}
               dropdownLabel=""
               label={item.label}
@@ -221,9 +230,9 @@ export default class ValuesForm extends Component<PropsType, StateType> {
         case "provider-select":
           return (
             <SelectRow
-              key={i}
-              value={this.props.metaState[key]}
-              setActiveValue={(val) => this.props.setMetaState({ [key]: val })}
+              key={key}
+              value={this.props.metaState[key]?.value}
+              setActiveValue={(val) => this.props.setMetaState(key, val)}
               options={[
                 { value: "aws", label: "Amazon Web Services (AWS)" },
                 { value: "gcp", label: "Google Cloud Platform (GCP)" },
@@ -238,15 +247,20 @@ 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)}
               setValue={(x: string) => {
-                if (item.settings && item.settings.unit && x !== "") {
+                if (
+                  item.settings &&
+                  item.settings.unit &&
+                  x !== "" &&
+                  !item.settings.omitUnitFromValue
+                ) {
                   x = x + item.settings.unit;
                 }
-                this.props.setMetaState({ [key]: btoa(x) });
+                this.props.setMetaState(key, btoa(x));
               }}
               label={item.label}
               unit={item.settings ? item.settings.unit : null}
@@ -256,15 +270,20 @@ 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)}
               setValue={(x: string) => {
-                if (item.settings && item.settings.unit && x !== "") {
+                if (
+                  item.settings &&
+                  item.settings.unit &&
+                  x !== "" &&
+                  !item.settings.omitUnitFromValue
+                ) {
                   x = x + item.settings.unit;
                 }
-                this.props.setMetaState({ [key]: btoa(x) });
+                this.props.setMetaState(key, btoa(x));
               }}
               label={item.label}
               unit={item.settings ? item.settings.unit : null}
@@ -281,7 +300,10 @@ export default class ValuesForm extends Component<PropsType, StateType> {
       return this.props.sections.map((section: Section, i: number) => {
         // Hide collapsible section if deciding field is false
         if (section.show_if) {
-          if (!this.props.metaState[section.show_if]) {
+          if (
+            !this.props.metaState[section.show_if] ||
+            this.props.metaState[section.show_if].value === false
+          ) {
             return null;
           }
         }

+ 0 - 177
dashboard/src/components/values-form/ValuesWrapper.tsx

@@ -1,177 +0,0 @@
-import React, { Component } from "react";
-import styled from "styled-components";
-
-import { Section, FormElement } from "../../shared/types";
-import { Context } from "../../shared/Context";
-
-import SaveButton from "../SaveButton";
-
-type PropsType = {
-  formTabs: any;
-  onSubmit: (formValues: any) => void;
-  disabled?: boolean;
-  saveValuesStatus?: string | null;
-  isInModal?: boolean;
-  currentTab?: string; // For resetting state when flipping b/w tabs in ExpandedChart
-  renderSaveButton?: boolean;
-};
-
-type StateType = any;
-
-const providerMap: any = {
-  gke: "gcp",
-  eks: "aws",
-  doks: "do",
-};
-
-// Manages the consolidated state of all form tabs ("metastate")
-export default class ValuesWrapper extends Component<PropsType, StateType> {
-  // No need to render, so OK to set as class variable outside of state
-  requiredFields: string[] = [];
-
-  updateFormState() {
-    let metaState: any = {};
-    this.props.formTabs.forEach((tab: any, i: number) => {
-      // TODO: reconcile tab.name and tab.value
-      if (tab.name || (tab.value && tab.value.includes("@"))) {
-        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;
-
-            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[0];
-            }
-
-            // Handle add to list of required fields
-            if (item.required) {
-              key && this.requiredFields.push(key);
-            }
-
-            switch (item.type) {
-              case "checkbox":
-                metaState[key] = def ? def : false;
-                break;
-              case "string-input":
-                metaState[key] = def ? def : "";
-                break;
-              case "string-input-password":
-                metaState[key] = def ? def : item.settings.default;
-              case "array-input":
-                metaState[key] = def ? def : [];
-                break;
-              case "env-key-value-array":
-                metaState[key] = def ? def : {};
-                break;
-              case "key-value-array":
-                metaState[key] = def ? def : {};
-                break;
-              case "number-input":
-                metaState[key] = def.toString() ? def : "";
-                break;
-              case "select":
-                metaState[key] = def ? def : item.settings.options[0].value;
-                break;
-              case "provider-select":
-                def = providerMap[this.context.currentCluster.service];
-                metaState[key] = def ? def : "aws";
-                break;
-              case "base-64":
-                metaState[key] = def ? def : "";
-              case "base-64-password":
-                metaState[key] = def ? def : "";
-              default:
-            }
-          });
-        });
-      }
-    });
-    this.setState(metaState);
-  }
-
-  // Initialize corresponding state fields for form blocks
-  componentDidMount() {
-    this.updateFormState();
-  }
-
-  componentDidUpdate(prevProps: PropsType) {
-    if (
-      this.props.formTabs !== prevProps.formTabs ||
-      this.props.currentTab !== prevProps.currentTab
-    ) {
-      this.updateFormState();
-    }
-  }
-
-  // Checks if all required fields are set
-  isDisabled = (): boolean => {
-    let valueIndicators: any[] = [];
-    this.requiredFields.forEach((field: string, i: number) => {
-      valueIndicators.push(this.state[field] && true);
-    });
-    return valueIndicators.includes(false) || valueIndicators.includes("");
-  };
-
-  renderButton = () => {
-    if (this.props.renderSaveButton) {
-      let { formTabs, currentTab } = this.props;
-      let tab = formTabs.find(
-        (t: any) => t.name === currentTab || t.value === currentTab
-      );
-      if (tab && tab.context && tab.context.type === "helm/values") {
-        return (
-          <SaveButton
-            disabled={this.isDisabled() || this.props.disabled}
-            text="Deploy"
-            onClick={() => this.props.onSubmit(this.state)}
-            status={
-              this.isDisabled()
-                ? "Missing required fields"
-                : this.props.saveValuesStatus
-            }
-            makeFlush={true}
-          />
-        );
-      }
-    }
-  };
-
-  render() {
-    let renderFunc: any = this.props.children;
-    if (this.props.isInModal) {
-      return (
-        <StyledValuesWrapper>
-          {renderFunc(this.state, (x: any) => this.setState(x))}
-          {this.renderButton()}
-        </StyledValuesWrapper>
-      );
-    }
-    return (
-      <PaddedWrapper>
-        <StyledValuesWrapper>
-          {renderFunc(this.state, (x: any) => this.setState(x))}
-          {this.renderButton()}
-        </StyledValuesWrapper>
-      </PaddedWrapper>
-    );
-  }
-}
-
-ValuesWrapper.contextType = Context;
-
-const StyledValuesWrapper = styled.div`
-  width: 100%;
-  padding: 0;
-  height: calc(100% - 65px);
-`;
-
-const PaddedWrapper = styled.div`
-  padding-bottom: 65px;
-  position: relative;
-`;

+ 13 - 8
dashboard/src/main/Main.tsx

@@ -55,13 +55,14 @@ export default class Main extends Component<PropsType, StateType> {
         }
       })
       .catch((err) => this.setState({ isLoggedIn: false, loading: false }));
-    
-    api.getCapabilities("", {}, {})
-    .then((res) => {
-      console.log(res.data)
-      this.setState({local: !res.data?.provisioner})
-    })
-    .catch((err) => console.log(err));
+
+    api
+      .getCapabilities("", {}, {})
+      .then((res) => {
+        console.log(res.data);
+        this.setState({ local: !res.data?.provisioner });
+      })
+      .catch((err) => console.log(err));
   }
 
   initialize = () => {
@@ -109,7 +110,11 @@ export default class Main extends Component<PropsType, StateType> {
     }
 
     // if logged in but not verified, block until email verification
-    if (!this.state.local && this.state.isLoggedIn && !this.state.isEmailVerified) {
+    if (
+      !this.state.local &&
+      this.state.isLoggedIn &&
+      !this.state.isEmailVerified
+    ) {
       return (
         <Switch>
           <Route

+ 12 - 11
dashboard/src/main/auth/Login.tsx

@@ -40,11 +40,12 @@ export default class Login extends Component<PropsType, StateType> {
       : document.addEventListener("keydown", this.handleKeyDown);
 
     // get capabilities to case on github
-    api.getCapabilities("", {}, {})
-    .then((res) => {
-      this.setState({hasGithub: res.data?.github})
-    })
-    .catch((err) => console.log(err));
+    api
+      .getCapabilities("", {}, {})
+      .then((res) => {
+        this.setState({ hasGithub: res.data?.github });
+      })
+      .catch((err) => console.log(err));
   }
 
   componentWillUnmount() {
@@ -119,19 +120,19 @@ export default class Login extends Component<PropsType, StateType> {
       return (
         <>
           <OAuthButton onClick={this.githubRedirect}>
-          <IconWrapper>
-            <Icon src={github} />
-            Log in with GitHub
-          </IconWrapper>
+            <IconWrapper>
+              <Icon src={github} />
+              Log in with GitHub
+            </IconWrapper>
           </OAuthButton>
           <OrWrapper>
             <Line />
             <Or>or</Or>
           </OrWrapper>
         </>
-      )
+      );
     }
-  }
+  };
 
   render() {
     let { email, password, credentialError, emailError } = this.state;

+ 17 - 17
dashboard/src/main/auth/Register.tsx

@@ -38,11 +38,12 @@ export default class Register extends Component<PropsType, StateType> {
     document.addEventListener("keydown", this.handleKeyDown);
 
     // get capabilities to case on github
-    api.getCapabilities("", {}, {})
-    .then((res) => {      
-      this.setState({hasGithub: res.data?.github})
-    })
-    .catch((err) => console.log(err));
+    api
+      .getCapabilities("", {}, {})
+      .then((res) => {
+        this.setState({ hasGithub: res.data?.github });
+      })
+      .catch((err) => console.log(err));
   }
 
   componentWillUnmount() {
@@ -120,20 +121,19 @@ export default class Register extends Component<PropsType, StateType> {
       return (
         <>
           <OAuthButton onClick={this.githubRedirect}>
-              <IconWrapper>
-                <Icon src={github} />
-                Sign up with GitHub
-              </IconWrapper>
-            </OAuthButton>
-            <OrWrapper>
-              <Line />
-              <Or>or</Or>
-            </OrWrapper>
+            <IconWrapper>
+              <Icon src={github} />
+              Sign up with GitHub
+            </IconWrapper>
+          </OAuthButton>
+          <OrWrapper>
+            <Line />
+            <Or>or</Or>
+          </OrWrapper>
         </>
-      )
+      );
     }
-  }
-
+  };
 
   render() {
     let {

+ 6 - 10
dashboard/src/main/home/Home.tsx

@@ -87,19 +87,15 @@ class Home extends Component<PropsType, StateType> {
     if (!currentProject) return;
 
     api
-      .getCapabilities(
-        "<token>",
-        {},
-        {}
-      )
+      .getCapabilities("<token>", {}, {})
       .then((res) => {
-        console.log(res.data)
-        this.context.setCapabilities(res.data)
+        console.log(res.data);
+        this.context.setCapabilities(res.data);
       })
       .catch((err) => {
-        console.log(err)
+        console.log(err);
       });
-  }
+  };
 
   getProjects = (id?: number) => {
     let { user, setProjects } = this.context;
@@ -269,7 +265,7 @@ class Home extends Component<PropsType, StateType> {
       return (
         <DashboardWrapper>
           <Placeholder>
-            <Bold>Porter - Getting Started</Bold>
+            <Bold>Porter - Getting</Bold>
             <br />
             <br />
             1. Navigate to{" "}

+ 0 - 2
dashboard/src/main/home/cluster-dashboard/ClusterDashboard.tsx

@@ -1,9 +1,7 @@
 import React, { Component } from "react";
 import styled from "styled-components";
-import gradient from "assets/gradient.jpg";
 import monojob from "assets/monojob.png";
 import monoweb from "assets/monoweb.png";
-import sliders from "assets/sliders.svg";
 
 import { Context } from "shared/Context";
 import { ChartType, ClusterType } from "shared/types";

+ 0 - 1
dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx

@@ -1,6 +1,5 @@
 import React, { Component } from "react";
 import styled from "styled-components";
-import yaml from "js-yaml";
 import close from "assets/close.png";
 import key from "assets/key.svg";
 import _ from "lodash";

+ 38 - 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,29 @@ 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={this.onSubmit}
+              saveValuesStatus={this.state.saveValuesStatus}
+              externalValues={{
+                namespace: this.props.namespace,
+                clusterId: this.context.currentCluster.id,
+              }}
+              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 +687,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;
@@ -965,6 +919,7 @@ const StyledExpandedChart = styled.div`
   animation-fill-mode: forwards;
   padding: 25px;
   display: flex;
+  overflow: hidden;
   flex-direction: column;
 
   @keyframes floatIn {

+ 25 - 54
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedJobChart.tsx

@@ -14,8 +14,7 @@ import Loading from "components/Loading";
 import TabRegion from "components/TabRegion";
 import JobList from "./jobs/JobList";
 import SettingsSection from "./SettingsSection";
-import ValuesWrapper from "components/values-form/ValuesWrapper";
-import ValuesForm from "components/values-form/ValuesForm";
+import FormWrapper from "components/values-form/FormWrapper";
 
 type PropsType = {
   namespace: string;
@@ -36,6 +35,7 @@ type StateType = {
   showDeleteOverlay: boolean;
   deleting: boolean;
   saveValuesStatus: string | null;
+  formData: any;
 };
 
 export default class ExpandedJobChart extends Component<PropsType, StateType> {
@@ -50,6 +50,7 @@ export default class ExpandedJobChart extends Component<PropsType, StateType> {
     showDeleteOverlay: false,
     deleting: false,
     saveValuesStatus: null as string | null,
+    formData: {} as any,
   };
 
   // Retrieve full chart data (includes form and values)
@@ -229,15 +230,10 @@ export default class ExpandedJobChart extends Component<PropsType, StateType> {
 
       return date2.getTime() - date1.getTime();
     });
-
-    console.log("JOBS ARE", jobs);
-
     this.setState({ jobs });
   };
 
-  renderTabContents = () => {
-    let currentTab = this.state.currentTab;
-
+  renderTabContents = (currentTab: string) => {
     switch (currentTab) {
       case "jobs":
         return (
@@ -262,48 +258,14 @@ export default class ExpandedJobChart extends Component<PropsType, StateType> {
           />
         );
       default:
-        if (this.state.tabOptions && currentTab && currentTab.includes("@")) {
-          return (
-            <TabWrapper>
-              <ValuesWrapper
-                formTabs={this.state.tabOptions}
-                onSubmit={this.handleSaveValues}
-                saveValuesStatus={this.state.saveValuesStatus}
-                isInModal={true}
-                currentTab={currentTab}
-                renderSaveButton={false}
-              >
-                {(metaState: any, setMetaState: any) => {
-                  return this.state.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}
-                          disabled={true}
-                        />
-                      );
-                    }
-                  });
-                }}
-              </ValuesWrapper>
-              <SaveButton
-                text="Rerun Job"
-                onClick={() => this.handleSaveValues()}
-                status={this.state.saveValuesStatus}
-                makeFlush={true}
-              />
-            </TabWrapper>
-          );
-        }
     }
   };
 
   updateTabs() {
     let formData = this.state.currentChart.form;
+    if (formData) {
+      this.setState({ formData });
+    }
     let tabOptions = [] as any[];
 
     // Append universal tabs
@@ -312,7 +274,7 @@ export default class ExpandedJobChart extends Component<PropsType, StateType> {
     if (formData) {
       formData.tabs.map((tab: any, i: number) => {
         tabOptions.push({
-          value: "@" + tab.name,
+          value: tab.name,
           label: tab.label,
           sections: tab.sections,
           context: tab.context,
@@ -435,14 +397,16 @@ export default class ExpandedJobChart extends Component<PropsType, StateType> {
             </CloseButton>
           </HeaderWrapper>
 
-          <TabRegion
-            currentTab={this.state.currentTab}
-            setCurrentTab={(x: string) => this.setState({ currentTab: x })}
-            options={this.state.tabOptions}
-            color={null}
-          >
-            {this.renderTabContents()}
-          </TabRegion>
+          <BodyWrapper>
+            <FormWrapper
+              formData={this.state.formData}
+              isReadOnly={true}
+              tabOptions={this.state.tabOptions}
+              isInModal={true}
+              renderTabContents={this.renderTabContents}
+              tabOptionsOnly={true}
+            />
+          </BodyWrapper>
         </StyledExpandedChart>
       </>
     );
@@ -451,6 +415,12 @@ export default class ExpandedJobChart extends Component<PropsType, StateType> {
 
 ExpandedJobChart.contextType = Context;
 
+const BodyWrapper = styled.div`
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+`;
+
 const TabWrapper = styled.div`
   height: 100%;
   width: 100%;
@@ -632,6 +602,7 @@ const StyledExpandedChart = styled.div`
   animation-fill-mode: forwards;
   padding: 25px;
   display: flex;
+  overflow: hidden;
   flex-direction: column;
 
   @keyframes floatIn {

+ 86 - 42
dashboard/src/main/home/dashboard/Dashboard.tsx

@@ -1,7 +1,7 @@
 import React, { Component } from "react";
 import styled from "styled-components";
 
-import gradient from "assets/gradient.jpg";
+import gradient from "assets/gradient.png";
 import { Context } from "shared/Context";
 import { InfraType } from "shared/types";
 import api from "shared/api";
@@ -11,6 +11,7 @@ import ClusterPlaceholderContainer from "./ClusterPlaceholderContainer";
 import { RouteComponentProps, withRouter } from "react-router";
 import TabRegion from "components/TabRegion";
 import Provisioner from "../provisioner/Provisioner";
+import FormDebugger from "components/values-form/FormDebugger";
 
 import { setSearchParam } from "shared/routing";
 
@@ -30,11 +31,17 @@ const tabOptionStrings = ["overview", "create-cluster", "provisioner"];
 
 type StateType = {
   infras: InfraType[];
+  pressingCtrl: boolean;
+  pressingK: boolean;
+  showFormDebugger: boolean;
 };
 
 class Dashboard extends Component<PropsType, StateType> {
   state = {
     infras: [] as InfraType[],
+    pressingCtrl: false,
+    pressingK: false,
+    showFormDebugger: false,
   };
 
   refreshInfras = () => {
@@ -54,8 +61,35 @@ class Dashboard extends Component<PropsType, StateType> {
 
   componentDidMount() {
     this.refreshInfras();
+    document.addEventListener("keydown", this.handleKeyDown);
+    document.addEventListener("keyup", this.handleKeyUp);
   }
 
+  componentWillUnmount() {
+    document.removeEventListener("keydown", this.handleKeyDown);
+    document.removeEventListener("keyup", this.handleKeyUp);
+  }
+
+  handleKeyDown = (e: KeyboardEvent): void => {
+    let { pressingK, pressingCtrl } = this.state;
+    if (e.key === "Meta" || e.key === "Control") {
+      this.setState({ pressingCtrl: true });
+    }
+    if (e.key === "k") {
+      this.setState({ pressingK: true });
+    }
+    if (e.key === "z" && pressingK && pressingCtrl) {
+      this.setState({ pressingK: false, pressingCtrl: false });
+      this.setState({ showFormDebugger: !this.state.showFormDebugger });
+    }
+  };
+
+  handleKeyUp = (e: KeyboardEvent): void => {
+    if (e.key === "Meta" || e.key === "Control" || e.key === "k") {
+      this.setState({ pressingCtrl: false, pressingK: false });
+    }
+  };
+
   componentDidUpdate(prevProps: PropsType) {
     if (this.props.projectId && prevProps.projectId !== this.props.projectId) {
       this.refreshInfras();
@@ -102,53 +136,62 @@ class Dashboard extends Component<PropsType, StateType> {
       { label: "Project Overview", value: "overview" },
       { label: "Create a Cluster", value: "create-cluster" },
       { label: "Provisioner Status", value: "provisioner" },
-    ]
-    
+    ];
+
     if (!capabilities?.provisioner) {
-      tabOptions = [
-        { label: "Project Overview", value: "overview" },
-      ]
+      tabOptions = [{ label: "Project Overview", value: "overview" }];
     }
 
     return (
       <>
         {currentProject && (
           <DashboardWrapper>
-            <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>
         )}
       </>
@@ -217,7 +260,7 @@ const LineBreak = styled.div`
   width: calc(100% - 0px);
   height: 2px;
   background: #ffffff20;
-  margin: 10px 0px 35px;
+  margin: 10px 0px 20px;
 `;
 
 const Overlay = styled.div`
@@ -241,6 +284,7 @@ const DashboardImage = styled.img`
   height: 45px;
   width: 45px;
   border-radius: 5px;
+  box-shadow: 0 2px 5px 4px #00000011;
 `;
 
 const DashboardIcon = styled.div`

+ 44 - 103
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";
 
@@ -58,6 +57,7 @@ type StateType = {
   folderPath: string | null;
   selectedRegistry: any | null;
   env: any;
+  valuesToOverride: any | null;
 };
 
 const defaultActionConfig: ActionConfigType = {
@@ -93,6 +93,7 @@ class LaunchTemplate extends Component<PropsType, StateType> {
     folderPath: null as string | null,
     selectedRegistry: null as any | null,
     env: {},
+    valuesToOverride: null as any | null,
   };
 
   createGHAction = (chartName: string, chartNamespace: string) => {
@@ -382,15 +383,6 @@ class LaunchTemplate extends Component<PropsType, StateType> {
       procfilePath,
     } = this.state;
 
-    // if (
-    //   sourceType === "repo" &&
-    //   !dockerfilePath &&
-    //   folderPath &&
-    //   !procfilePath
-    // ) {
-    //   return "Procfile not detected.";
-    // }
-
     if (!this.submitIsDisabled()) {
       return this.state.saveValuesStatus;
     }
@@ -414,53 +406,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}
-      >
-        {(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: "" });
@@ -546,13 +491,23 @@ 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}
+            saveValuesStatus={this.state.saveValuesStatus}
+            valuesToOverride={this.state.valuesToOverride}
+            clearValuesToOverride={() =>
+              this.setState({ valuesToOverride: null })
+            }
+            externalValues={{
+              namespace: this.state.selectedNamespace,
+              clusterId: this.context.currentCluster.id,
+            }}
+            onSubmit={
+              this.props.currentTab === "docker"
+                ? this.onSubmit
+                : this.onSubmitAddon
+            }
+          />
         </>
       );
     } else {
@@ -586,17 +541,19 @@ class LaunchTemplate extends Component<PropsType, StateType> {
     if (this.state.sourceType === "") {
       return (
         <BlockList>
-          {capabilities.github && (<Block
-            onClick={() => {
-              this.setState({ sourceType: "repo" });
-            }}
-          >
-            <BlockIcon src="https://3.bp.blogspot.com/-xhNpNJJyQhk/XIe4GY78RQI/AAAAAAAAItc/ouueFUj2Hqo5dntmnKqEaBJR4KQ4Q2K3ACK4BGAYYCw/s1600/logo%2Bgit%2Bicon.png" />
-            <BlockTitle>Git Repository</BlockTitle>
-            <BlockDescription>
-              Deploy using source from a Git repo.
-            </BlockDescription>
-          </Block>)}
+          {capabilities.github && (
+            <Block
+              onClick={() => {
+                this.setState({ sourceType: "repo" });
+              }}
+            >
+              <BlockIcon src="https://3.bp.blogspot.com/-xhNpNJJyQhk/XIe4GY78RQI/AAAAAAAAItc/ouueFUj2Hqo5dntmnKqEaBJR4KQ4Q2K3ACK4BGAYYCw/s1600/logo%2Bgit%2Bicon.png" />
+              <BlockTitle>Git Repository</BlockTitle>
+              <BlockDescription>
+                Deploy using source from a Git repo.
+              </BlockDescription>
+            </Block>
+          )}
           <Block
             onClick={() => {
               this.setState({ sourceType: "registry" });
@@ -645,32 +602,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>
@@ -699,7 +630,17 @@ class LaunchTemplate extends Component<PropsType, StateType> {
             }
             procfileProcess={this.state.procfileProcess}
             setProcfileProcess={(procfileProcess: string) =>
-              this.setState({ procfileProcess })
+              this.setState({
+                procfileProcess,
+                valuesToOverride: {
+                  "container.command": {
+                    value: procfileProcess || "",
+                  },
+                  showStartCommand: {
+                    value: !procfileProcess,
+                  },
+                },
+              })
             }
             setBranch={(branch: string) => this.setState({ branch })}
             setDockerfilePath={(x: string) =>

+ 0 - 1
dashboard/src/main/home/modals/UpdateClusterModal.tsx

@@ -1,7 +1,6 @@
 import React, { Component } from "react";
 import styled from "styled-components";
 import close from "assets/close.png";
-import gradient from "assets/gradient.jpg";
 
 import api from "shared/api";
 import { Context } from "shared/Context";

+ 2 - 4
dashboard/src/main/home/navbar/Navbar.tsx

@@ -42,11 +42,9 @@ export default class Navbar extends Component<PropsType, StateType> {
 
   renderFeedbackButton = () => {
     if (this.context?.capabilities?.provisioner) {
-      return (
-        <Feedback currentView={this.props.currentView} />
-      )
+      return <Feedback currentView={this.props.currentView} />;
     }
-  }
+  };
 
   render() {
     return (

+ 6 - 2
dashboard/src/main/home/new-project/NewProject.tsx

@@ -1,7 +1,7 @@
 import React, { Component } from "react";
 import styled from "styled-components";
 
-import gradient from "assets/gradient.jpg";
+import gradient from "assets/gradient.png";
 import { Context } from "shared/Context";
 import { isAlphanumeric } from "shared/common";
 
@@ -59,7 +59,11 @@ export default class NewProject extends Component<PropsType, StateType> {
             width="470px"
           />
         </InputWrapper>
-        <ProvisionerSettings isInNewProject={true} projectName={projectName} provisioner={capabilities?.provisioner} />
+        <ProvisionerSettings
+          isInNewProject={true}
+          projectName={projectName}
+          provisioner={capabilities?.provisioner}
+        />
         <Br />
       </StyledNewProject>
     );

+ 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({

+ 12 - 17
dashboard/src/main/home/provisioner/ProvisionerSettings.tsx

@@ -52,7 +52,6 @@ class NewProject extends Component<PropsType, StateType> {
     }
 
     let renderSkipHelper = () => {
-
       if (!this.props.provisioner) {
         return;
       }
@@ -139,7 +138,9 @@ class NewProject extends Component<PropsType, StateType> {
     let { selectedProvider } = this.state;
     let { isInNewProject } = this.props;
     let { provisioner } = this.props;
-    let helper = provisioner ? "Note: Provisioning can take up to 15 minutes" : ""
+    let helper = provisioner
+      ? "Note: Provisioning can take up to 15 minutes"
+      : "";
 
     if (isInNewProject && !selectedProvider) {
       return (
@@ -161,24 +162,22 @@ class NewProject extends Component<PropsType, StateType> {
             helper={helper}
           />
         </>
-      )
+      );
     }
-  }
+  };
 
   componentDidMount() {
     let { provisioner } = this.props;
 
     if (!provisioner) {
-      this.setState({selectedProvider: "skipped"})
+      this.setState({ selectedProvider: "skipped" });
     }
   }
 
   componentDidUpdate(prevProps: PropsType) {
-    if (
-      prevProps.provisioner !== this.props.provisioner
-    ) {
+    if (prevProps.provisioner !== this.props.provisioner) {
       if (!this.props.provisioner) {
-        this.setState({selectedProvider: "skipped"})
+        this.setState({ selectedProvider: "skipped" });
       }
     }
   }
@@ -194,22 +193,18 @@ class NewProject extends Component<PropsType, StateType> {
         <>
           Select your hosting backend:<Required>*</Required>
         </>
-      )
+      );
     } else {
-      return (
-        "Need a cluster? Provision through Porter:"
-      )
+      return "Need a cluster? Provision through Porter:";
     }
-  }
+  };
 
   render() {
     let { selectedProvider } = this.state;
 
     return (
       <StyledProvisionerSettings>
-        <Helper>
-          {this.renderHelperText()}
-        </Helper>
+        <Helper>{this.renderHelperText()}</Helper>
         {!selectedProvider ? (
           <BlockList>
             {providers.map((provider: string, i: number) => {

+ 3 - 1
dashboard/src/main/home/sidebar/ProjectSection.tsx

@@ -1,6 +1,6 @@
 import React, { Component } from "react";
 import styled from "styled-components";
-import gradient from "assets/gradient.jpg";
+import gradient from "assets/gradient.png";
 
 import { Context } from "shared/Context";
 import { ProjectType } from "shared/types";
@@ -210,6 +210,8 @@ const Letter = styled.div`
   height: 100%;
   width: 100%;
   position: absolute;
+  padding-bottom: 2px;
+  font-weight: 500;
   background: #00000028;
   top: 0;
   left: 0;

+ 1 - 1
dashboard/src/shared/Context.tsx

@@ -65,7 +65,7 @@ class ContextProvider extends Component {
     },
     capabilities: null as CapabilityType,
     setCapabilities: (capabilities: CapabilityType) => {
-      this.setState({ capabilities })
+      this.setState({ capabilities });
     },
     clearContext: () => {
       this.setState({

+ 3 - 1
dashboard/src/shared/types.tsx

@@ -104,10 +104,12 @@ export interface FormElement {
   required?: boolean;
   name?: string;
   variable?: string;
+  placeholder?: string;
   value?: any;
   settings?: {
     default?: number | string | boolean;
     options?: any[];
+    omitUnitFromValue?: boolean;
     unit?: string;
   };
 }
@@ -171,4 +173,4 @@ export interface ActionConfigType {
 export interface CapabilityType {
   github: boolean;
   provisioner: boolean;
-}
+}

+ 14 - 12
internal/models/templates.go

@@ -42,18 +42,20 @@ type FormSection struct {
 
 // FormContent is a form's atomic unit
 type FormContent struct {
-	Context  *FormContext `yaml:"context" json:"context"`
-	Type     string       `yaml:"type" json:"type"`
-	Label    string       `yaml:"label" json:"label"`
-	Required bool         `json:"required"`
-	Name     string       `yaml:"name,omitempty" json:"name,omitempty"`
-	Variable string       `yaml:"variable,omitempty" json:"variable,omitempty"`
-	Value    interface{}  `yaml:"value,omitempty" json:"value,omitempty"`
-	Settings struct {
-		Default     interface{} `yaml:"default,omitempty" json:"default,omitempty"`
-		Unit        interface{} `yaml:"unit,omitempty" json:"unit,omitempty"`
-		Options     interface{} `yaml:"options,omitempty" json:"options,omitempty"`
-		Placeholder string      `yaml:"placeholder,omitempty" json:"placeholder,omitempty"`
+	Context     *FormContext `yaml:"context" json:"context"`
+	Type        string       `yaml:"type" json:"type"`
+	Label       string       `yaml:"label" json:"label"`
+	Required    bool         `json:"required"`
+	Name        string       `yaml:"name,omitempty" json:"name,omitempty"`
+	Variable    string       `yaml:"variable,omitempty" json:"variable,omitempty"`
+	Placeholder string       `yaml:"placeholder,omitempty" json:"placeholder,omitempty"`
+	Value       interface{}  `yaml:"value,omitempty" json:"value,omitempty"`
+	Settings    struct {
+		Default           interface{} `yaml:"default,omitempty" json:"default,omitempty"`
+		Unit              interface{} `yaml:"unit,omitempty" json:"unit,omitempty"`
+		OmitUnitFromValue bool        `yaml:"omitUnitFromValue,omitempty" json:"omitUnitFromValue,omitempty"`
+		Options           interface{} `yaml:"options,omitempty" json:"options,omitempty"`
+		Placeholder       string      `yaml:"placeholder,omitempty" json:"placeholder,omitempty"`
 	} `yaml:"settings,omitempty" json:"settings,omitempty"`
 }