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

Merge pull request #978 from porter-dev/0.7.0-form-fix-validation

[0.7.0] [Forms, Frontend] Fix form validation bug
jusrhee 4 лет назад
Родитель
Сommit
69c6ab032f

+ 9 - 1
dashboard/src/components/porter-form/PorterForm.tsx

@@ -41,6 +41,7 @@ interface Props {
   showStateDebugger?: boolean;
   currentTab: string;
   setCurrentTab: (nt: string) => void;
+  isLaunch?: boolean;
 }
 
 const PorterForm: React.FC<Props> = (props) => {
@@ -102,11 +103,14 @@ const PorterForm: React.FC<Props> = (props) => {
     let options = (props.leftTabOptions || [])
       .concat(
         formData?.tabs?.map((tab) => {
+          if (props.isLaunch && tab?.settings?.omitFromLaunch) {
+            return undefined;
+          }
           return { label: tab.label, value: tab.name };
         })
       )
       .concat(props.rightTabOptions || []);
-    return options.filter((x) => x !== undefined);
+    return options.filter((x) => !!x);
   };
 
   const showSaveButton = (): boolean => {
@@ -131,10 +135,13 @@ const PorterForm: React.FC<Props> = (props) => {
 
   const renderTab = (): JSX.Element => {
     if (!formData) {
+      console.log("hm fuck");
       return props.renderTabContents(currentTab);
     }
 
     const tab = formData.tabs?.filter((tab) => tab.name == currentTab)[0];
+    console.log("currentTab", currentTab);
+    console.log("tab", tab);
 
     // Handle external tab
     if (!tab) {
@@ -173,6 +180,7 @@ const PorterForm: React.FC<Props> = (props) => {
     return props.saveValuesStatus;
   };
 
+  console.log(getTabOptions());
   return (
     <>
       <TabRegion

+ 44 - 12
dashboard/src/components/porter-form/PorterFormContextProvider.tsx

@@ -58,12 +58,15 @@ export const PorterFormContextProvider: React.FC<Props> = (props) => {
               ...state.components,
               [action.id]: {
                 state: action.initValue,
-                validation: {
-                  ...{
-                    validated: false,
-                  },
-                  ...action.initValidation,
+              },
+            },
+            validation: {
+              ...state.validation,
+              [action.id]: {
+                ...{
+                  validated: false,
                 },
+                ...action.initValidation,
               },
             },
           };
@@ -94,9 +97,12 @@ export const PorterFormContextProvider: React.FC<Props> = (props) => {
             ...state.components,
             [action.id]: {
               ...state.components[action.id],
-              validation: action.updateFunc(
-                state.components[action.id].validation
-              ),
+            },
+          },
+          validation: {
+            ...state.validation,
+            [action.id]: {
+              ...action.updateFunc(state.validation[action.id]),
             },
           },
         };
@@ -128,8 +134,36 @@ export const PorterFormContextProvider: React.FC<Props> = (props) => {
     return ret;
   };
 
+  const getInitialValidation = (data: PorterFormData) => {
+    const ret: Record<string, any> = {};
+    data?.tabs?.map((tab, i) =>
+      tab.sections?.map((section, j) =>
+        section.contents?.map((field, k) => {
+          if (
+            field.type == "heading" ||
+            field.type == "subtitle" ||
+            field.type == "resource-list" ||
+            field.type == "service-ip-list" ||
+            field.type == "velero-create-backup"
+          )
+            return;
+          if (
+            field.required &&
+            (field.settings?.default || (field.value && field.value[0]))
+          ) {
+            ret[`${i}-${j}-${k}`] = {
+              validated: true,
+            };
+          }
+        })
+      )
+    );
+    return ret;
+  };
+
   const [state, dispatch] = useReducer(handleAction, {
     components: {},
+    validation: getInitialValidation(props.rawFormData),
     variables: {
       ...props.initialVariables,
       ...getInitialVariables(props.rawFormData),
@@ -310,7 +344,7 @@ export const PorterFormContextProvider: React.FC<Props> = (props) => {
             return;
           // fields that have defaults can't be required since we can always
           // compute their value
-          if (field.required && !field.settings?.default) {
+          if (field.required) {
             requiredIds.push(field.id);
           }
           if (!mapping[field.variable]) {
@@ -327,9 +361,7 @@ export const PorterFormContextProvider: React.FC<Props> = (props) => {
     Validate the form based on a list of required ids
    */
   const doValidation = (requiredIds: string[]) =>
-    requiredIds
-      ?.map((id) => state.components[id]?.validation.validated)
-      .every((x) => x);
+    requiredIds?.map((id) => state.validation[id]?.validated).every((x) => x);
 
   const formData = computeFormStructure(
     restructureToNewFields(props.rawFormData),

+ 11 - 1
dashboard/src/components/porter-form/PorterFormWrapper.tsx

@@ -18,6 +18,7 @@ type PropsType = {
   addendum?: any;
   saveValuesStatus?: string;
   showStateDebugger?: boolean;
+  isLaunch?: boolean;
 };
 
 const PorterFormWrapper: React.FunctionComponent<PropsType> = ({
@@ -34,6 +35,7 @@ const PorterFormWrapper: React.FunctionComponent<PropsType> = ({
   addendum,
   saveValuesStatus,
   showStateDebugger,
+  isLaunch,
 }) => {
   const hashCode = (s: string) => {
     return s.split("").reduce(function (a, b) {
@@ -46,7 +48,13 @@ const PorterFormWrapper: React.FunctionComponent<PropsType> = ({
     if (leftTabOptions?.length > 0) {
       return leftTabOptions[0].value;
     } else if (formData?.tabs?.length > 0) {
-      return formData?.tabs[0].name;
+      let includedTabs = formData.tabs;
+      if (isLaunch) {
+        includedTabs = formData.tabs.filter(
+          (tab: any) => !tab?.settings?.omitFromLaunch
+        );
+      }
+      return includedTabs[0].name;
     } else if (rightTabOptions?.length > 0) {
       return rightTabOptions[0].value;
     } else {
@@ -54,6 +62,7 @@ const PorterFormWrapper: React.FunctionComponent<PropsType> = ({
     }
   };
 
+  // Lifted into PorterFormWrapper to allow tab to be remembered on re-render (e.g., on revision select)
   const [currentTab, setCurrentTab] = useState(getInitialTab());
 
   return (
@@ -77,6 +86,7 @@ const PorterFormWrapper: React.FunctionComponent<PropsType> = ({
           saveValuesStatus={saveValuesStatus}
           currentTab={currentTab}
           setCurrentTab={setCurrentTab}
+          isLaunch={isLaunch}
         />
       </PorterFormContextProvider>
     </React.Fragment>

+ 18 - 15
dashboard/src/components/porter-form/field-components/Input.tsx

@@ -8,6 +8,15 @@ import {
   StringInputFieldState,
 } from "../types";
 
+const clipOffUnit = (unit: string, x: string) => {
+  if (typeof x === "string" && unit) {
+    return unit === x.slice(x.length - unit.length, x.length)
+      ? x.slice(0, x.length - unit.length)
+      : x;
+  }
+  return x;
+};
+
 const Input: React.FC<InputField> = ({
   id,
   variable,
@@ -19,18 +28,6 @@ const Input: React.FC<InputField> = ({
   isReadOnly,
   value,
 }) => {
-  const clipOffUnit = (x: string) => {
-    let unit = settings?.unit;
-    if (typeof x === "string" && unit) {
-      return unit === x.slice(x.length - unit.length, x.length) ? (
-        x.slice(0, x.length - unit.length)
-      ) : (
-        x
-      );
-    }
-    return x;
-  }
-
   const {
     state,
     variables,
@@ -39,11 +36,13 @@ const Input: React.FC<InputField> = ({
   } = useFormField<StringInputFieldState>(id, {
     initValidation: {
       validated: value
-        ? value[0] !== undefined
+        ? value[0] !== undefined && value[0] !== ""
         : settings?.default != undefined,
     },
     initVars: {
-      [variable]: value ? clipOffUnit(value[0]) : settings?.default,
+      [variable]: value
+        ? clipOffUnit(settings?.unit, value[0])
+        : settings?.default,
     },
   });
 
@@ -94,7 +93,11 @@ export const getFinalVariablesForStringInput: GetFinalVariablesFunction = (
   vars,
   props: InputField
 ) => {
-  const val = vars[props.variable] || props.settings?.default;
+  const val =
+    vars[props.variable] ||
+    (props.value
+      ? clipOffUnit(props.settings?.unit, props.value[0])
+      : props.settings?.default);
   return {
     [props.variable]:
       props.settings?.unit && !props.settings.omitUnitFromValue

+ 1 - 2
dashboard/src/components/porter-form/field-components/KeyValueArray.tsx

@@ -138,10 +138,9 @@ const KeyValueArray: React.FC<Props> = (props) => {
       obj[key] = value;
     });
     return obj;
-  }
+  };
 
   const renderEnvModal = () => {
-    console.log(state.values)
     if (state.showEnvModal) {
       return (
         <Modal

+ 6 - 1
dashboard/src/components/porter-form/types.ts

@@ -145,6 +145,9 @@ export interface Tab {
   name: string;
   label: string;
   sections: Section[];
+  settings?: {
+    omitFromLaunch?: boolean;
+  };
 }
 
 export interface PorterFormData {
@@ -194,9 +197,11 @@ export interface PorterFormState {
   components: {
     [key: string]: {
       state: PorterFormFieldFieldState;
-      validation: PorterFormFieldValidationState;
     };
   };
+  validation: {
+    [key: string]: PorterFormFieldValidationState;
+  };
   variables: PorterFormVariableList;
 }
 

+ 11 - 13
dashboard/src/main/home/Home.tsx

@@ -485,9 +485,9 @@ class Home extends Component<PropsType, StateType> {
   };
 
   render() {
-    let { 
-      currentModal, 
-      setCurrentModal, 
+    let {
+      currentModal,
+      setCurrentModal,
       currentProject,
       currentOverlay,
       setCurrentOverlay,
@@ -578,16 +578,14 @@ class Home extends Component<PropsType, StateType> {
           </Modal>
         )}
 
-        {
-          currentOverlay && (
-            <ConfirmOverlay
-              show={true}
-              message={currentOverlay.message}
-              onYes={currentOverlay.onYes}
-              onNo={currentOverlay.onNo}
-            />
-          )
-        }
+        {currentOverlay && (
+          <ConfirmOverlay
+            show={true}
+            message={currentOverlay.message}
+            onYes={currentOverlay.onYes}
+            onNo={currentOverlay.onNo}
+          />
+        )}
 
         {this.renderSidebar()}
 

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

@@ -392,30 +392,30 @@ class ExpandedEnvGroup extends Component<PropsType, StateType> {
             </LastDeployed>
           </InfoWrapper>
 
-          {
-            this.state.deleting ? (
-              <>
-                <LineBreak />
-                <Placeholder>
-                  <TextWrap>
-                    <Header>
-                      <Spinner src={loading} /> Deleting "{this.state.envGroup.name}"
-                    </Header>
-                    You will be automatically redirected after deletion is complete.
-                  </TextWrap>
-                </Placeholder>
-              </>
-            ) : (
-              <TabRegion
-                currentTab={this.state.currentTab}
-                setCurrentTab={(x: string) => this.setState({ currentTab: x })}
-                options={this.state.tabOptions}
-                color={null}
-              >
-                {this.renderTabContents()}
-              </TabRegion>
-            )
-          }
+          {this.state.deleting ? (
+            <>
+              <LineBreak />
+              <Placeholder>
+                <TextWrap>
+                  <Header>
+                    <Spinner src={loading} /> Deleting "
+                    {this.state.envGroup.name}"
+                  </Header>
+                  You will be automatically redirected after deletion is
+                  complete.
+                </TextWrap>
+              </Placeholder>
+            </>
+          ) : (
+            <TabRegion
+              currentTab={this.state.currentTab}
+              setCurrentTab={(x: string) => this.setState({ currentTab: x })}
+              options={this.state.tabOptions}
+              color={null}
+            >
+              {this.renderTabContents()}
+            </TabRegion>
+          )}
         </StyledExpandedChart>
       </>
     );

+ 60 - 61
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx

@@ -89,9 +89,9 @@ const ExpandedChart: React.FC<Props> = (props) => {
     closeWebsocket,
   } = useWebsockets();
 
-  const { 
-    currentCluster, 
-    currentProject, 
+  const {
+    currentCluster,
+    currentProject,
     setCurrentError,
     setCurrentOverlay,
   } = useContext(Context);
@@ -468,7 +468,7 @@ const ExpandedChart: React.FC<Props> = (props) => {
         (tab: any) => !liveTabs.includes(tab.value)
       );
     }
-    
+
     setLeftTabOptions(leftTabOptions);
     setRightTabOptions(rightTabOptions);
   };
@@ -693,66 +693,65 @@ const ExpandedChart: React.FC<Props> = (props) => {
             </LastDeployed>
           </InfoWrapper>
         </HeaderWrapper>
-        {
-          deleting ? (
-            <>
-              <LineBreak />
-              <Placeholder>
-                <TextWrap>
-                  <Header>
-                    <Spinner src={loadingSrc} /> Deleting "{currentChart.name}"
-                  </Header>
-                  You will be automatically redirected after deletion is complete.
-                </TextWrap>
-              </Placeholder>
-            </>
-          ) : (
-            <>
-              <RevisionSection
-                showRevisions={showRevisions}
-                toggleShowRevisions={() => {
-                  setShowRevisions(!showRevisions);
+        {deleting ? (
+          <>
+            <LineBreak />
+            <Placeholder>
+              <TextWrap>
+                <Header>
+                  <Spinner src={loadingSrc} /> Deleting "{currentChart.name}"
+                </Header>
+                You will be automatically redirected after deletion is complete.
+              </TextWrap>
+            </Placeholder>
+          </>
+        ) : (
+          <>
+            <RevisionSection
+              showRevisions={showRevisions}
+              toggleShowRevisions={() => {
+                setShowRevisions(!showRevisions);
+              }}
+              chart={currentChart}
+              refreshChart={() => getChartData(currentChart)}
+              setRevision={setRevision}
+              forceRefreshRevisions={forceRefreshRevisions}
+              refreshRevisionsOff={() => setForceRefreshRevisions(false)}
+              status={chartStatus}
+              shouldUpdate={
+                currentChart.latest_version &&
+                currentChart.latest_version !==
+                  currentChart.chart.metadata.version
+              }
+              latestVersion={currentChart.latest_version}
+              upgradeVersion={handleUpgradeVersion}
+            />
+            <BodyWrapper>
+              <PorterFormWrapper
+                formData={currentChart.form}
+                valuesToOverride={{
+                  namespace: props.namespace,
+                  clusterId: currentCluster.id,
                 }}
-                chart={currentChart}
-                refreshChart={() => getChartData(currentChart)}
-                setRevision={setRevision}
-                forceRefreshRevisions={forceRefreshRevisions}
-                refreshRevisionsOff={() => setForceRefreshRevisions(false)}
-                status={chartStatus}
-                shouldUpdate={
-                  currentChart.latest_version &&
-                  currentChart.latest_version !== currentChart.chart.metadata.version
+                renderTabContents={renderTabContents}
+                isReadOnly={
+                  imageIsPlaceholder ||
+                  !isAuthorized("application", "", ["get", "update"])
+                }
+                onSubmit={onSubmit}
+                rightTabOptions={rightTabOptions}
+                leftTabOptions={leftTabOptions}
+                color={isPreview ? "#f5cb42" : null}
+                addendum={
+                  <TabButton onClick={toggleDevOpsMode} devOpsMode={devOpsMode}>
+                    <i className="material-icons">offline_bolt</i> DevOps Mode
+                  </TabButton>
                 }
-                latestVersion={currentChart.latest_version}
-                upgradeVersion={handleUpgradeVersion}
+                saveValuesStatus={saveValuesStatus}
               />
-              <BodyWrapper>
-                <PorterFormWrapper
-                  formData={currentChart.form}
-                  valuesToOverride={{
-                    namespace: props.namespace,
-                    clusterId: currentCluster.id,
-                  }}
-                  renderTabContents={renderTabContents}
-                  isReadOnly={
-                    imageIsPlaceholder ||
-                    !isAuthorized("application", "", ["get", "update"])
-                  }
-                  onSubmit={onSubmit}
-                  rightTabOptions={rightTabOptions}
-                  leftTabOptions={leftTabOptions}
-                  color={isPreview ? "#f5cb42" : null}
-                  addendum={
-                    <TabButton onClick={toggleDevOpsMode} devOpsMode={devOpsMode}>
-                      <i className="material-icons">offline_bolt</i> DevOps Mode
-                    </TabButton>
-                  }
-                  saveValuesStatus={saveValuesStatus}
-                />
-              </BodyWrapper>
-            </>
-          )
-        }
+            </BodyWrapper>
+          </>
+        )}
       </StyledExpandedChart>
     </>
   );

+ 5 - 1
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChartWrapper.tsx

@@ -93,7 +93,11 @@ class ExpandedChartWrapper extends Component<PropsType, StateType> {
     let { baseRoute, namespace } = match.params as any;
     let { loading, currentChart } = this.state;
     if (loading) {
-      return <LoadingWrapper><Loading /></LoadingWrapper>;
+      return (
+        <LoadingWrapper>
+          <Loading />
+        </LoadingWrapper>
+      );
     } else if (currentChart && baseRoute === "jobs") {
       return (
         <ExpandedJobChart

+ 41 - 42
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedJobChart.tsx

@@ -587,48 +587,47 @@ class ExpandedJobChart extends Component<PropsType, StateType> {
             </InfoWrapper>
           </HeaderWrapper>
 
-          {
-            this.state.deleting ? (
-              <>
-                <LineBreak />
-                <Placeholder>
-                  <TextWrap>
-                    <Header>
-                      <Spinner src={loading} /> Deleting "{currentChart.name}"
-                    </Header>
-                    You will be automatically redirected after deletion is complete.
-                  </TextWrap>
-                </Placeholder>
-              </>
-            ) : (
-              <BodyWrapper>
-                {(this.state.leftTabOptions?.length > 0 ||
-                  this.state.formData.tabs?.length > 0 ||
-                  this.state.rightTabOptions?.length > 0) && (
-                  <PorterFormWrapper
-                    formData={this.state.formData}
-                    valuesToOverride={{
-                      namespace: chart.namespace,
-                      clusterId: this.props.currentCluster.id,
-                    }}
-                    renderTabContents={this.renderTabContents}
-                    isReadOnly={
-                      this.state.imageIsPlaceholder ||
-                      !this.props.isAuthorized("job", "", ["get", "update"])
-                    }
-                    onSubmit={(formValues) => {
-                      console.log(formValues)
-                      this.handleSaveValues(formValues, false)
-                    }}
-                    leftTabOptions={this.state.leftTabOptions}
-                    rightTabOptions={this.state.rightTabOptions}
-                    saveValuesStatus={this.state.saveValuesStatus}
-                    saveButtonText="Save Config"
-                  />
-                )}
-              </BodyWrapper>
-            )
-          }
+          {this.state.deleting ? (
+            <>
+              <LineBreak />
+              <Placeholder>
+                <TextWrap>
+                  <Header>
+                    <Spinner src={loading} /> Deleting "{currentChart.name}"
+                  </Header>
+                  You will be automatically redirected after deletion is
+                  complete.
+                </TextWrap>
+              </Placeholder>
+            </>
+          ) : (
+            <BodyWrapper>
+              {(this.state.leftTabOptions?.length > 0 ||
+                this.state.formData.tabs?.length > 0 ||
+                this.state.rightTabOptions?.length > 0) && (
+                <PorterFormWrapper
+                  formData={this.state.formData}
+                  valuesToOverride={{
+                    namespace: chart.namespace,
+                    clusterId: this.props.currentCluster.id,
+                  }}
+                  renderTabContents={this.renderTabContents}
+                  isReadOnly={
+                    this.state.imageIsPlaceholder ||
+                    !this.props.isAuthorized("job", "", ["get", "update"])
+                  }
+                  onSubmit={(formValues) => {
+                    console.log(formValues);
+                    this.handleSaveValues(formValues, false);
+                  }}
+                  leftTabOptions={this.state.leftTabOptions}
+                  rightTabOptions={this.state.rightTabOptions}
+                  saveValuesStatus={this.state.saveValuesStatus}
+                  saveButtonText="Save Config"
+                />
+              )}
+            </BodyWrapper>
+          )}
         </StyledExpandedChart>
       </>
     );

+ 1 - 3
dashboard/src/main/home/launch/launch-flow/SettingsPage.tsx

@@ -151,9 +151,7 @@ class SettingsPage extends Component<PropsType, StateType> {
               namespace: selectedNamespace,
               clusterId: this.context.currentCluster.id,
             }}
-            //externalValues={{
-            //  isLaunch: true,
-            //}}
+            isLaunch={true}
             isReadOnly={
               !this.props.isAuthorized("namespace", "", ["get", "create"])
             }

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

@@ -26,9 +26,9 @@ export interface GlobalContextType {
   currentModalData: any;
   setCurrentModal: (currentModal: string, currentModalData?: any) => void;
   currentOverlay: {
-    message: string,
-    onYes: any,
-    onNo: any,
+    message: string;
+    onYes: any;
+    onNo: any;
   };
   setCurrentOverlay: (x: any) => void;
   currentError: string | null;

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

@@ -267,9 +267,9 @@ export interface ContextProps {
   currentModalData: any;
   setCurrentModal: (currentModal: string, currentModalData?: any) => void;
   currentOverlay: {
-    message: string,
-    onYes: any,
-    onNo: any,
+    message: string;
+    onYes: any;
+    onNo: any;
   };
   setCurrentOverlay: (x: any) => void;
   currentError?: string;