Răsfoiți Sursa

Merge branch 'master' of https://github.com/porter-dev/porter into beta.3.consolidation

mergin
Alexander Belanger 5 ani în urmă
părinte
comite
182ed50e89

+ 1 - 0
dashboard/package.json

@@ -8,6 +8,7 @@
     "@types/lodash": "^4.14.165",
     "@types/markdown-to-jsx": "^6.11.3",
     "@types/qs": "^6.9.5",
+    "@types/random-words": "^1.1.0",
     "ace-builds": "^1.4.12",
     "axios": "^0.20.0",
     "dotenv": "^8.2.0",

+ 12 - 6
dashboard/src/components/SaveButton.tsx

@@ -32,13 +32,19 @@ export default class SaveButton extends Component<PropsType, StateType> {
             <LoadingGif src={loading} /> Updating . . .
           </StatusWrapper>
         );
+      } else if (this.props.status === 'error') {
+        return (
+          <StatusWrapper successful={false}>
+            <i className="material-icons">error_outline</i> Could not update
+          </StatusWrapper>
+        );
+      } else {
+        return (
+          <StatusWrapper successful={false}>
+            <i className="material-icons">error_outline</i> {this.props.status}
+          </StatusWrapper>
+        );
       }
-
-      return (
-        <StatusWrapper successful={false}>
-          <i className="material-icons">error_outline</i> Could not update
-        </StatusWrapper>
-      );
     }
   }
 

+ 27 - 7
dashboard/src/components/values-form/InputRow.tsx

@@ -6,10 +6,11 @@ type PropsType = {
   type: string,
   value: string | number,
   setValue: (x: string | number) => void,
-  unit?: string
-  placeholder?: string
-  width?: string
-  disabled?: boolean
+  unit?: string,
+  placeholder?: string,
+  width?: string,
+  disabled?: boolean,
+  isRequired?: boolean,
 };
 
 type StateType = {
@@ -28,12 +29,22 @@ export default class InputRow extends Component<PropsType, StateType> {
       this.props.setValue(e.target.value);
     }
   }
+
+  renderRequiredWarning = () => {
+    if (this.props.isRequired && this.props.value === '') {
+      return (
+        <Warning>
+          <i className="material-icons">error_outline</i>
+        </Warning>
+      );
+    }
+  }
   
   render() {
     let { label, value, type, unit, placeholder, width } = this.props;
     return (
       <StyledInputRow>
-        <Label>{label}</Label>
+        <Label>{label} {this.props.isRequired ? ' *' : null}</Label>
         <InputWrapper>
           <Input
             readOnly={this.state.readOnly} onFocus={() => this.setState({ readOnly: false })}
@@ -41,10 +52,11 @@ export default class InputRow extends Component<PropsType, StateType> {
             placeholder={placeholder}
             width={width}
             type={type}
-            value={value || ''}
+            value={value}
             onChange={this.handleChange}
           />
-          <Unit>{unit}</Unit>
+          {unit ? <Unit>{unit}</Unit> : null}
+          {this.renderRequiredWarning()}
         </InputWrapper>
       </StyledInputRow>
     );
@@ -52,7 +64,15 @@ export default class InputRow extends Component<PropsType, StateType> {
 }
 
 const Unit = styled.div`
+  margin-right: 8px;
+`;
 
+const Warning = styled.div`
+  margin-bottom: -3px;
+  > i {
+    font-size: 18px;
+    color: #fcba03;
+  }
 `;
 
 const InputWrapper = styled.div`

+ 38 - 69
dashboard/src/components/values-form/ValuesForm.tsx

@@ -6,7 +6,6 @@ import { Section, FormElement } from '../../shared/types';
 import { Context } from '../../shared/Context';
 import api from '../../shared/api';
 
-import SaveButton from '../SaveButton';
 import CheckboxRow from './CheckboxRow';
 import InputRow from './InputRow';
 import SelectRow from './SelectRow';
@@ -15,55 +14,24 @@ import Heading from './Heading';
 import ResourceTab from '../ResourceTab';
 
 type PropsType = {
-  onSubmit: (formValues: any) => void,
   sections?: Section[],
-  disabled?: boolean,
-  saveValuesStatus?: string | null,
+  metaState?: any,
+  setMetaState?: any,
 };
 
 type StateType = any;
 
 export default class ValuesForm extends Component<PropsType, StateType> {
-
-  updateFormState() {
-    let formState: any = {};
-    this.props.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.value && item.value[0]) || (item.settings && item.settings.default);
-
-        switch (item.type) {
-          case 'checkbox':
-            formState[key] = def ? def : false;
-            break;
-          case 'string-input':
-            formState[key] = def ? def : '';
-            break;
-          case 'number-input':
-            formState[key] = def.toString() ? def : '';
-            break;
-          case 'select':
-            formState[key] = def ? def : item.settings.options[0].value;
-            break;
-          default:
-        }
-      });
-    });
-    this.setState(formState);
-  }
-
-  // Initialize corresponding state fields for form blocks
   componentDidMount() {
-    this.updateFormState();
+    console.log('hola senorita')
   }
-
-  componentDidUpdate(prevProps: PropsType) {
-    if (this.props.sections !== prevProps.sections) {
-      this.updateFormState();
+  getInputValue = (item: FormElement) => {
+    let key = item.name || item.variable;
+    let value = this.props.metaState[key];
+    if (item.settings && item.settings.unit && value) {
+      value = value.split(item.settings.unit)[0]
     }
+    return value;
   }
 
   renderSection = (section: Section) => {
@@ -96,8 +64,8 @@ export default class ValuesForm extends Component<PropsType, StateType> {
           return (
             <CheckboxRow
               key={i}
-              checked={this.state[key]}
-              toggle={() => this.setState({ [key]: !this.state[key] })}
+              checked={this.props.metaState[key]}
+              toggle={() => this.props.setMetaState({ [key]: !this.props.metaState[key] })}
               label={item.label}
             />
           );
@@ -105,9 +73,15 @@ export default class ValuesForm extends Component<PropsType, StateType> {
           return (
             <InputRow
               key={i}
+              isRequired={item.required}
               type='text'
-              value={this.state[key]}
-              setValue={(x: string) => this.setState({ [key]: x })}
+              value={this.getInputValue(item)}
+              setValue={(x: string) => {
+                if (item.settings && item.settings.unit && x !== '') {
+                  x = x + item.settings.unit;
+                }
+                this.props.setMetaState({ [key]: x });
+              }}
               label={item.label}
               unit={item.settings ? item.settings.unit : null}
             />
@@ -116,9 +90,18 @@ export default class ValuesForm extends Component<PropsType, StateType> {
           return (
             <InputRow
               key={i}
+              isRequired={item.required}
               type='number'
-              value={this.state[key]}
-              setValue={(x: number) => this.setState({ [key]: x })}
+              value={this.getInputValue(item)}
+              setValue={(x: number) => {
+                let val = x.toString();
+                if (Number.isNaN(x)) {
+                  val = '';
+                } else if (item.settings && item.settings.unit) {
+                  val = val + item.settings.unit;
+                }
+                this.props.setMetaState({ [key]: val });
+              }}
               label={item.label}
               unit={item.settings ? item.settings.unit : null}
             />
@@ -127,8 +110,8 @@ export default class ValuesForm extends Component<PropsType, StateType> {
           return (
             <SelectRow
               key={i}
-              value={this.state[key]}
-              setActiveValue={(val) => this.setState({ [key]: val })}
+              value={this.props.metaState[key]}
+              setActiveValue={(val) => this.props.setMetaState({ [key]: val })}
               options={item.settings.options}
               dropdownLabel=''
               label={item.label}
@@ -140,11 +123,11 @@ export default class ValuesForm extends Component<PropsType, StateType> {
   }
 
   renderFormContents = () => {
-    if (this.state) {
+    if (this.props.metaState) {
       return this.props.sections.map((section: Section, i: number) => {
         // Hide collapsible section if deciding field is false
         if (section.show_if) {
-          if (!this.state[section.show_if]) {
+          if (!this.props.metaState[section.show_if]) {
             return null;
           }
         }
@@ -160,19 +143,10 @@ export default class ValuesForm extends Component<PropsType, StateType> {
 
   render() {
     return (
-      <Wrapper>
-        <StyledValuesForm>
-          <DarkMatter />
-          {this.renderFormContents()}
-        </StyledValuesForm>
-        <SaveButton
-          disabled={this.props.disabled}
-          text='Deploy'
-          onClick={() => this.props.onSubmit(this.state)}
-          status={this.props.saveValuesStatus}
-          makeFlush={true}
-        />
-      </Wrapper>
+      <StyledValuesForm>
+        <DarkMatter />
+        {this.renderFormContents()}
+      </StyledValuesForm>
     );
   }
 }
@@ -188,11 +162,6 @@ const DarkMatter = styled.div`
   margin-top: 0px;
 `;
 
-const Wrapper = styled.div`
-  width: 100%;
-  height: 100%;
-`;
-
 const StyledValuesForm = styled.div`
   width: 100%;
   height: 100%;

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

@@ -0,0 +1,107 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+
+import { Section, FormElement } from '../../shared/types';
+
+import SaveButton from '../SaveButton';
+
+type PropsType = {
+  formTabs: any,
+  onSubmit: (formValues: any) => void,
+  disabled?: boolean,
+  saveValuesStatus?: string | null,
+  isInModal?: boolean,
+};
+
+type StateType = any;
+
+// 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() {
+    console.log('here')
+    console.log(this.props.formTabs)
+    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.value && item.value[0]) || (item.settings && item.settings.default);
+
+            // 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 'number-input':
+                metaState[key] = def.toString() ? def : '';
+                break;
+              case 'select':
+                metaState[key] = def ? def : item.settings.options[0].value;
+                break;
+              default:
+            }
+          });
+        });
+      }
+    });
+    this.setState(metaState);
+  }
+
+  // Initialize corresponding state fields for form blocks
+  componentDidMount() {
+    this.updateFormState();
+  }
+
+  componentDidUpdate(prevProps: PropsType) {
+    if (this.props.formTabs !== prevProps.formTabs) {
+      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('')
+  }
+
+  render() {
+    let renderFunc: any = this.props.children;
+    return (
+      <StyledValuesWrapper isInModal={this.props.isInModal}>
+        {renderFunc(this.state, (x: any) => this.setState(x))}
+        <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}
+        />
+      </StyledValuesWrapper>
+    );
+  }
+}
+
+const StyledValuesWrapper = styled.div`
+  width: 100%;
+  height: ${(props: { isInModal: boolean }) => props.isInModal ? '100%' : 'calc(100% + 65px)'};
+  padding-bottom: ${(props: { isInModal: boolean }) => props.isInModal ? '' : '65px'};
+`;

+ 31 - 17
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx

@@ -14,6 +14,7 @@ import ValuesYaml from './ValuesYaml';
 import GraphSection from './GraphSection';
 import ListSection from './ListSection';
 import StatusSection from './status/StatusSection';
+import ValuesWrapper from '../../../../components/values-form/ValuesWrapper';
 import ValuesForm from '../../../../components/values-form/ValuesForm';
 import SettingsSection from './SettingsSection';
 import { NavLink } from 'react-router-dom';
@@ -212,23 +213,36 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
           />
         );
       default:
-        if (currentTab && currentTab.includes('@')) {
-          return tabOptions.map((tab: any, i: number) => {
-
-            // If tab is current, render
-            if (tab.value === currentTab) {
-              
-              return (
-                <ValuesFormWrapper key={i}>
-                  <ValuesForm 
-                    sections={tab.sections}
-                    onSubmit={this.onSubmit}
-                    saveValuesStatus={saveValuesStatus}
-                  />
-                </ValuesFormWrapper>
-              );
-            }
-          });
+        if (tabOptions && currentTab && currentTab.includes('@')) {
+          return (
+            <ValuesWrapper
+              formTabs={tabOptions}
+              onSubmit={this.onSubmit}
+              saveValuesStatus={this.state.saveValuesStatus}
+              isInModal={true}
+            >
+              {
+                (metaState: any, setMetaState: any) => {
+                  return tabOptions.map((tab: any, i: number) => {
+
+                    // If tab is current, render
+                    if (tab.value === currentTab) {
+                      return (
+                        <ValuesFormWrapper>
+                          <ValuesForm
+                            metaState={metaState}
+                            setMetaState={setMetaState}
+                            key={i}
+                            sections={tab.sections} 
+                          />
+                        </ValuesFormWrapper>
+                      );
+                    }
+                  });
+                }
+              }
+            </ValuesWrapper>
+          );
         }
     }
   }

+ 2 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/GraphDisplay.tsx

@@ -98,6 +98,7 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
     let nodes = [] as NodeType[];
     let edges = [] as EdgeType[];
     if (!graph) {
+      console.log('nada')
       nodes = this.createNodes(components);
       edges = this.createEdges(components);
       this.setState({ nodes, edges });
@@ -136,6 +137,7 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
   // Live update on rollback/upgrade
   componentDidUpdate(prevProps: PropsType) {
     if (prevProps.components !== this.props.components) {
+      this.storeChartGraph();
       this.getChartGraph();
     }
   }

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

@@ -44,7 +44,6 @@ export default class ExpandedTemplate extends Component<PropsType, StateType> {
       if (err) {
         this.setState({ loading: false, error: true });
       } else {
-        console.log(res.data);
         let { form, values, markdown, metadata } = res.data;
         let keywords = metadata.keywords;
         this.setState({ form, values, markdown, keywords, loading: false, error: false });

+ 40 - 27
dashboard/src/main/home/templates/expanded-template/LaunchTemplate.tsx

@@ -9,6 +9,7 @@ import { PorterTemplate, ChoiceType, Cluster, StorageType } from '../../../../sh
 import Selector from '../../../../components/Selector';
 import ImageSelector from '../../../../components/image-selector/ImageSelector';
 import TabRegion from '../../../../components/TabRegion';
+import ValuesWrapper from '../../../../components/values-form/ValuesWrapper';
 import ValuesForm from '../../../../components/values-form/ValuesForm';
 
 type PropsType = {
@@ -37,7 +38,7 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
   state = {
     currentView: 'repo',
     clusterOptions: [] as { label: string, value: string }[],
-    saveValuesStatus: null as (string | null),
+    saveValuesStatus: 'No container image specified' as (string | null),
     selectedCluster: this.context.currentCluster.name,
     selectedNamespace: "default",
     selectedImageUrl: '' as string | null,
@@ -81,24 +82,33 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
   }
 
   renderTabContents = () => {
-    return this.props.form.tabs.map((tab: any, i: number) => {
-
-      // If tab is current, render
-      if (tab.name === this.state.currentTab) {
-        return (
-          <ValuesFormWrapper key={i}>
-            <ValuesForm 
-              key={tab.name}
-              sections={tab.sections} 
-              onSubmit={this.onSubmit}
-              disabled={false}
-              // disabled={!this.state.selectedImageUrl || this.state.selectedImageUrl === ''}
-              saveValuesStatus={this.state.saveValuesStatus}
-            />
-          </ValuesFormWrapper>
-        );
-      }
-    });
+    return (
+      <ValuesWrapper
+        formTabs={this.props.form.tabs}
+        onSubmit={this.onSubmit}
+        saveValuesStatus={this.state.saveValuesStatus}
+        disabled={!this.state.selectedImageUrl}
+      >
+        {
+          (metaState: any, setMetaState: any) => {
+            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}
+                    setMetaState={setMetaState}
+                    key={tab.name}
+                    sections={tab.sections} 
+                  />
+                );
+              }
+            });
+          }
+        }
+      </ValuesWrapper>
+    );
   }
 
   componentDidMount() {
@@ -139,6 +149,15 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
     });
   }
 
+  setSelectedImageUrl = (x: string) => {
+    if (x === '') {
+      this.setState({ saveValuesStatus: 'No container image specified' });
+    } else {
+      this.setState({ saveValuesStatus: '' });
+    }
+    this.setState({ selectedImageUrl: x });
+  }
+
   renderIcon = (icon: string) => {
     if (icon) {
       return <Icon src={icon} />
@@ -195,12 +214,12 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
           />
         </ClusterSection>
 
-        <Subtitle>Select the container image you would like to connect to this template (optional).</Subtitle>
+        <Subtitle>Select the container image you would like to connect to this template.</Subtitle>
         <Br />
         <ImageSelector
           selectedTag={this.state.selectedTag}
           selectedImageUrl={this.state.selectedImageUrl}
-          setSelectedImageUrl={(x: string) => this.setState({ selectedImageUrl: x })}
+          setSelectedImageUrl={this.setSelectedImageUrl}
           setSelectedTag={(x: string) => this.setState({ selectedTag: x })}
           forceExpanded={true}
           setCurrentView={this.props.setCurrentView}
@@ -222,12 +241,6 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
 
 LaunchTemplate.contextType = Context;
 
-const ValuesFormWrapper = styled.div`
-  width: 100%;
-  height: calc(100% + 65px);
-  padding-bottom: 65px;
-`;
-
 const Br = styled.div`
   width: 100%;
   height: 7px;

+ 8 - 6
dashboard/src/main/home/templates/expanded-template/TemplateInfo.tsx

@@ -32,11 +32,13 @@ export default class TemplateInfo extends Component<PropsType, StateType> {
   }
 
   renderTagList = () => {
-    return this.props.keywords.map((tag: string, i: number) => {
-      return (
-        <Tag key={i}>{tag}</Tag>
-      )
-    });
+    if (this.props.keywords) {
+      return this.props.keywords.map((tag: string, i: number) => {
+        return (
+          <Tag key={i}>{tag}</Tag>
+        )
+      });
+    }
   }
 
   renderMarkdown = () => {
@@ -50,7 +52,7 @@ export default class TemplateInfo extends Component<PropsType, StateType> {
   }
 
   renderTagSection = () => {
-    if (this.props.keywords.length > 0) {
+    if (this.props.keywords && this.props.keywords.length > 0) {
       return (
         <TagSection>
           <i className="material-icons">local_offer</i>

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

@@ -98,6 +98,7 @@ export interface Section {
 export interface FormElement {
   type: string,
   label: string,
+  required: boolean,
   name?: string,
   variable?: string,
   value?: any,