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

Merge pull request #744 from smiclea/validate-hub

Add validation process to Bare Metal Server modal
Daniel Vincze 3 лет назад
Родитель
Сommit
3a82c024d5

+ 131 - 24
src/components/modules/MetalHubModule/MetalHubModal/MetalHubModal.tsx

@@ -14,7 +14,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import React from "react";
 import { observer } from "mobx-react";
-import styled from "styled-components";
+import styled, { css } from "styled-components";
 
 import type { Field as FieldType } from "@src/@types/Field";
 import Button from "@src/components/ui/Button";
@@ -26,6 +26,8 @@ import { ThemeProps } from "@src/components/Theme";
 import LoadingButton from "@src/components/ui/LoadingButton";
 import { MetalHubServer } from "@src/@types/MetalHub";
 import image from "./images/server.svg";
+import metalHubStore from "@src/stores/MetalHubStore";
+import StatusIcon from "@src/components/ui/StatusComponents/StatusIcon";
 
 const Wrapper = styled.div`
   padding: 48px 32px 32px 32px;
@@ -51,25 +53,70 @@ const Buttons = styled.div`
   display: flex;
   justify-content: space-between;
 `;
+const StatusHeader = styled.div`
+  display: flex;
+  align-items: center;
+`;
+const StatusMessage = styled.div`
+  display: flex;
+  align-items: center;
+  flex-direction: column;
+
+  > div {
+    text-align: center;
+    &:first-child {
+      margin-bottom: 8px;
+    }
+  }
+`;
+const StatusIconStyled = styled(StatusIcon)``;
+const Status = styled.div<{ layout: "vertical" | "horizontal" }>`
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  margin-top: 16px;
+
+  ${StatusHeader} {
+    ${({ layout }) =>
+      layout === "vertical"
+        ? css`
+            flex-direction: column;
+          `
+        : ""}
+  }
+
+  ${StatusMessage} {
+    ${({ layout }) =>
+      layout === "horizontal"
+        ? css`
+            margin-left: 8px;
+          `
+        : ""}
+  }
+
+  ${StatusIconStyled} {
+    ${({ layout }) =>
+      layout === "vertical"
+        ? css`
+            margin-bottom: 8px;
+          `
+        : ""}
+  }
+`;
 
 type Props = {
-  loading: boolean;
+  server?: MetalHubServer;
+  loading?: boolean;
   onRequestClose: () => void;
-} & (
-  | {
-      server: MetalHubServer;
-      onEditClick: (apiEndpoint: string) => void;
-    }
-  | {
-      server?: undefined;
-      onAddClick: (apiEndpoint: string) => void;
-    }
-);
+  onUpdateDone: () => void;
+};
 
 type State = {
   host: string;
   port: string;
+  saving: boolean;
   highlightFieldNames: string[];
+  showSuccess: boolean;
 };
 @observer
 class MetalHubModal extends React.Component<Props, State> {
@@ -77,8 +124,14 @@ class MetalHubModal extends React.Component<Props, State> {
     host: "",
     port: "",
     highlightFieldNames: [],
+    saving: false,
+    showSuccess: false,
   };
 
+  get loading() {
+    return this.state.saving || this.props.loading;
+  }
+
   componentDidMount() {
     KeyboardManager.onEnter(
       "MetalHubNewModal",
@@ -99,18 +152,39 @@ class MetalHubModal extends React.Component<Props, State> {
 
   componentWillUnmount() {
     KeyboardManager.removeKeyDown("MetalHubNewModal");
+    metalHubStore.clearValidationError();
   }
 
-  handleAddClick() {
+  // Used to store the newly added server in case of validation error since a PATCH request is needed afterwards
+  serverAddedForValidation: number | null = null;
+
+  async handleAddClick() {
     if (this.highlightFields()) {
       return;
     }
 
     const endpointUrl = `https://${this.state.host}:${this.state.port}/api/v1`;
-    if (this.props.server) {
-      this.props.onEditClick(endpointUrl);
-    } else {
-      this.props.onAddClick(endpointUrl);
+    this.setState({ saving: true });
+    const serverId = this.props.server?.id || this.serverAddedForValidation;
+    let validationResult = false;
+    try {
+      if (serverId) {
+        await metalHubStore.patchServer(serverId, endpointUrl);
+        validationResult = await metalHubStore.validateServer(serverId);
+      } else {
+        const addedServer = await metalHubStore.addServer(endpointUrl);
+        this.serverAddedForValidation = addedServer.id;
+        validationResult = await metalHubStore.validateServer(addedServer.id);
+      }
+    } finally {
+      if (!validationResult) {
+        this.setState({ saving: false });
+      } else {
+        this.setState({ saving: false, showSuccess: true });
+        setTimeout(() => {
+          this.props.onUpdateDone();
+        }, 2000);
+      }
     }
   }
 
@@ -151,7 +225,7 @@ class MetalHubModal extends React.Component<Props, State> {
         highlight={Boolean(
           this.state.highlightFieldNames.find(n => n === field.name)
         )}
-        disabledLoading={this.props.loading}
+        disabledLoading={this.loading || this.state.showSuccess}
       />
     );
   }
@@ -192,27 +266,59 @@ class MetalHubModal extends React.Component<Props, State> {
   renderButtons() {
     return (
       <Buttons>
-        <Button secondary large onClick={this.props.onRequestClose}>
+        <Button
+          secondary
+          large
+          onClick={this.props.onRequestClose}
+          disabled={this.state.showSuccess}
+        >
           Cancel
         </Button>
-        {this.props.loading ? (
-          <LoadingButton large>
-            {this.props.server ? "Updating ..." : "Adding ..."}
-          </LoadingButton>
+        {this.loading ? (
+          <LoadingButton large>Validating ...</LoadingButton>
         ) : (
           <Button
             large
             onClick={() => {
               this.handleAddClick();
             }}
+            disabled={this.state.showSuccess}
           >
-            {this.props.server ? "Update" : "Add"}
+            Validate and save
           </Button>
         )}
       </Buttons>
     );
   }
 
+  renderValidationStatus() {
+    if (
+      !this.state.saving &&
+      !this.state.showSuccess &&
+      !metalHubStore.validationError.length
+    ) {
+      return null;
+    }
+    const message = this.state.saving
+      ? "Validating ..."
+      : metalHubStore.validationError.length
+      ? metalHubStore.validationError.map(e => <div key="e">{e}</div>)
+      : "Validation successful";
+    const status = this.state.saving
+      ? "RUNNING"
+      : metalHubStore.validationError.length
+      ? "ERROR"
+      : "COMPLETED";
+    return (
+      <Status layout={status === "ERROR" ? "vertical" : "horizontal"}>
+        <StatusHeader>
+          <StatusIconStyled status={status} />
+          <StatusMessage>{message}</StatusMessage>
+        </StatusHeader>
+      </Status>
+    );
+  }
+
   render() {
     return (
       <Modal
@@ -224,6 +330,7 @@ class MetalHubModal extends React.Component<Props, State> {
       >
         <Wrapper>
           <Image />
+          {this.renderValidationStatus()}
           {this.renderForm()}
           {this.renderButtons()}
         </Wrapper>

+ 5 - 12
src/components/smart/MetalHubServerDetailsPage/MetalHubServerDetailsPage.tsx

@@ -46,7 +46,6 @@ type State = {
   creatingReplica: boolean;
   creatingMigration: boolean;
   showEditServerModal: boolean;
-  updatingServer: boolean;
 };
 @observer
 class MetalHubServerDetailsPage extends React.Component<Props, State> {
@@ -55,7 +54,6 @@ class MetalHubServerDetailsPage extends React.Component<Props, State> {
     creatingReplica: false,
     creatingMigration: false,
     showEditServerModal: false,
-    updatingServer: false,
   };
 
   componentDidMount() {
@@ -151,14 +149,9 @@ class MetalHubServerDetailsPage extends React.Component<Props, State> {
     }
   }
 
-  async handleEditServer(apiEndpoint: string) {
-    this.setState({ updatingServer: true });
-    await metalHubStore.patchServer(
-      metalHubStore.serverDetails!.id,
-      apiEndpoint
-    );
+  async handleUpdateDone() {
     await this.loadData();
-    this.setState({ showEditServerModal: false, updatingServer: false });
+    this.setState({ showEditServerModal: false });
   }
 
   render() {
@@ -258,10 +251,10 @@ class MetalHubServerDetailsPage extends React.Component<Props, State> {
         ) : null}
         {this.state.showEditServerModal ? (
           <MetalHubModal
-            loading={this.state.updatingServer}
+            loading={metalHubStore.loadingServerDetails}
             server={metalHubStore.serverDetails!}
-            onEditClick={e => {
-              this.handleEditServer(e);
+            onUpdateDone={() => {
+              this.handleUpdateDone();
             }}
             onRequestClose={() => {
               this.setState({ showEditServerModal: false });

+ 3 - 6
src/components/smart/MetalHubServersPage/MetalHubServersPage.tsx

@@ -106,10 +106,8 @@ class MetalHubServersPage extends React.Component<{ history: any }, State> {
     this.setState({ showNewServerModal: true, modalIsOpen: true });
   }
 
-  async handleNewServer(endpoint: string) {
-    await metalHubStore.addServer(endpoint);
+  async handleNewServerDone() {
     this.setState({ showNewServerModal: false, modalIsOpen: false });
-    await metalHubStore.getServers();
   }
 
   handleRemoveAction() {
@@ -330,9 +328,8 @@ class MetalHubServersPage extends React.Component<{ history: any }, State> {
         />
         {this.state.showNewServerModal ? (
           <MetalHubModal
-            loading={metalHubStore.loadingNewServer}
-            onAddClick={e => {
-              this.handleNewServer(e);
+            onUpdateDone={() => {
+              this.handleNewServerDone();
             }}
             onRequestClose={() => {
               this.setState({ showNewServerModal: false, modalIsOpen: false });

+ 49 - 15
src/stores/MetalHubStore.ts

@@ -30,8 +30,6 @@ class MetalHubStore {
 
   @observable loadingFingerprintError = "";
 
-  @observable loadingNewServer = false;
-
   @observable serverDetails: MetalHubServer | null = null;
 
   @observable loadingServerDetails = false;
@@ -40,6 +38,10 @@ class MetalHubStore {
 
   @observable refreshingServer = false;
 
+  @observable validating = false;
+
+  @observable validationError: string[] = [];
+
   async getMetalHubEndpoint() {
     return source.getMetalHubEndpoint();
   }
@@ -85,17 +87,9 @@ class MetalHubStore {
   }
 
   @action async addServer(endpoint: string) {
-    this.loadingNewServer = true;
-    try {
-      const addedServer = await source.addServer(endpoint);
-      runInAction(() => {
-        this.servers.push(addedServer);
-      });
-    } finally {
-      runInAction(() => {
-        this.loadingNewServer = false;
-      });
-    }
+    const addedServer = await source.addServer(endpoint);
+    await this.getServers({ showLoading: false });
+    return addedServer;
   }
 
   @action async getServerDetails(serverId: number) {
@@ -130,21 +124,61 @@ class MetalHubStore {
         this.updatingServer = false;
       });
     }
+    await this.getServers({ showLoading: false });
   }
 
-  @action async refreshServer(serverId: number) {
-    this.refreshingServer = true;
+  @action async validateServer(serverId: number) {
+    this.validating = true;
+    this.validationError = [];
+
+    let server = await this.refreshServer(serverId, { showLoading: false });
+    if (server.active) {
+      this.validating = false;
+      return true;
+    }
+    for (let i = 0; i < 5; i++) {
+      server = await this.refreshServer(serverId, {
+        showLoading: false,
+      });
+      if (server.active) {
+        break;
+      }
+      await new Promise(resolve => setTimeout(resolve, 1000));
+    }
+    this.validating = false;
+    if (server.active) {
+      return true;
+    }
+    this.validationError = [
+      "Incorrect server hostname and/or port. Coriolis Snapshot Agent might be stopped.",
+      "Please ensure that the Coriolis Snapshot Agent is running on the specified hostname and port.",
+    ];
+    return false;
+  }
+
+  @action async refreshServer(
+    serverId: number,
+    opts?: { showLoading?: boolean }
+  ) {
+    if (!opts || opts.showLoading) {
+      this.refreshingServer = true;
+    }
     try {
       const server = await source.refreshServer(serverId);
       runInAction(() => {
         this.serverDetails = server;
       });
+      return server;
     } finally {
       runInAction(() => {
         this.refreshingServer = false;
       });
     }
   }
+
+  @action clearValidationError() {
+    this.validationError = [];
+  }
 }
 
 export default new MetalHubStore();