Forráskód Böngészése

Improve wizard instances search loading and pagination CORWEB-39

Don't blank the instances list while searching, instead show a loading spinner in the search box.
Don't blank the instances list while changing the page, instead show loading spinner.
Small WizardVms.js and SearchBox.js refactoring.
Fix JS error with caching and pagination.
Sergiu Miclea 8 éve
szülő
commit
d0b32e5a05

+ 10 - 3
src/components/SearchBox/SearchBox.js

@@ -25,8 +25,10 @@ class SearchBox extends Component {
     maxLines: PropTypes.number,
     minimize: PropTypes.bool,
     placeholder: PropTypes.string,
+    show: PropTypes.bool,
     onChange: PropTypes.func,
-    className: PropTypes.string
+    className: PropTypes.string,
+    isLoading: PropTypes.bool
   };
 
   static defaultProps = {
@@ -63,8 +65,12 @@ class SearchBox extends Component {
   }
 
   render() {
+    let renderLoading = () => this.props.isLoading ? <div className="taskIcon RUNNING"></div> : null
+    let hidden = this.props.show ? ' ' : ' hidden'
+
     return (
-      <div className={s.root}>
+      <div className={s.root + hidden}>
+        {renderLoading()}
         <input
           type="text"
           placeholder={this.props.placeholder}
@@ -72,7 +78,8 @@ class SearchBox extends Component {
           onChange={(e) => this.onChange(e)}
           onClick={(e) => this.toggleSearch(e)}
           onBlur={(e) => this.onBlurAction(e)}
-          className={s.searchBox + " " + (this.state.isMin ? s.minimize : "") + " searchBox " + this.props.className}
+          className={s.searchBox + " " + (this.state.isMin ? s.minimize : "") + " searchBox " +
+            this.props.className}
         />
       </div>
     );

+ 8 - 3
src/components/SearchBox/SearchBox.scss

@@ -21,9 +21,13 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 $searchStrokeColor: $gray-dark;
 
 .root {
+  position: relative;
+  :global(.taskIcon) {
+    position: absolute;
+    top: 2px;
+    right: 0px;
+  }
 }
-.input { }
-
 
 input[type="text"].searchBox {
   background-image: url('data:image/svg+xml;utf8,<svg width="14px" height="14px" viewBox="2 2 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><circle id="path-1" cx="8" cy="8" r="6"></circle><mask id="mask-2" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="12" height="12" fill="white"><use xlink:href="%23path-1"></use></mask></defs><use id="Oval-32" stroke="%23616770" mask="url(%23mask-2)" stroke-width="2" fill="%23FFFFFF" fill-rule="evenodd" xlink:href="%23path-1"></use><path d="M12,12 L15.5,15.5" id="Line" stroke="%23616770" stroke-width="1" stroke-linecap="round" fill="none"></path></svg>');
@@ -31,8 +35,9 @@ input[type="text"].searchBox {
   background-repeat: no-repeat;
   background-position: 8px 9px;
   padding-left: 36px;
+  padding-right: 30px;
   cursor: pointer;
-  width: 146px;
+  width: 152px;
   &:focus, &:hover {
     background-image: url('data:image/svg+xml;utf8,<svg width="14px" height="14px" viewBox="2 2 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><circle id="path-1" cx="8" cy="8" r="6"></circle><mask id="mask-2" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="12" height="12" fill="white"><use xlink:href="%23path-1"></use></mask></defs><use id="Oval-32" stroke="%230056B8" mask="url(%23mask-2)" stroke-width="2" fill="%23FFFFFF" fill-rule="evenodd" xlink:href="%23path-1"></use><path d="M12,12 L15.5,15.5" id="Line" stroke="%230056B8" stroke-width="1" stroke-linecap="round" fill="none"></path></svg>');
   }

+ 62 - 50
src/components/WizardVms/WizardVms.js

@@ -55,6 +55,7 @@ class WizardVms extends Component {
     this.state = {
       valid,
       queryText: '',
+      searching: false,
       page: 0,
       filterStatus: 'All',
       filteredData: this.props.data.instances ? this.props.data.instances.slice(0, itemsPerPage) : [],
@@ -78,9 +79,17 @@ class WizardVms extends Component {
   }
 
   processProps(props) {
-    if (props.data.instances) {
-      this.setState({ filteredData: props.data.instances.slice(
-        this.state.page * itemsPerPage, this.state.page * itemsPerPage + itemsPerPage) })
+    let isSearching = typeof props.data.searching === undefined ? this.state.searching : props.data.searching
+    if (props.data.instances && !isSearching) {
+      this.setState({
+        filteredData: props.data.instances.slice(
+          this.state.page * itemsPerPage, this.state.page * itemsPerPage + itemsPerPage),
+        searching: isSearching
+      })
+    } else {
+      this.setState({
+        searching: isSearching
+      })
     }
   }
 
@@ -115,21 +124,20 @@ class WizardVms extends Component {
 
     if (this.props.data.instances) {
       this.props.data.instances.forEach((vm) => {
-        if (
-          (this.state.filterStatus === "All" || this.state.filterStatus === vm.status)
-        ) {
+        if (this.state.filterStatus === "All" || this.state.filterStatus === vm.status) {
           queryResult.push(vm)
         }
       }, this)
     }
 
     if (this.state.queryText != queryText) {
+      this.props.setWizardState({ searching: true })
+
       if (this.timeout != null) {
         clearTimeout(this.timeout)
       }
       this.timeout = setTimeout(() => {
-        this.setState({ page: 0, filteredData: null, queryText: queryText }, () => {
-          this.props.setWizardState({ instances: null })
+        this.setState({ queryText: queryText }, () => {
           ConnectionsActions.loadInstances(
             { id: this.props.data.sourceCloud.credential.id },
             this.state.page,
@@ -160,9 +168,7 @@ class WizardVms extends Component {
   }
 
   toTitleCase(str) {
-    return str.replace(/\w\S*/g, (txt) => { // eslint-disable-line arrow-body-style
-      return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
-    });
+    return str.replace(/\w\S*/g, txt => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase())
   }
 
   isSelected(item) {
@@ -177,20 +183,21 @@ class WizardVms extends Component {
 
   nextPage() {
     if (this.state.filteredData && this.state.filteredData.length == itemsPerPage) {
+      this.props.setWizardState({ searching: true })
       this.setState({ page: this.state.page + 1 }, () => {
         ConnectionsActions.loadInstances(
           { id: this.props.data.sourceCloud.credential.id },
           this.state.page,
           this.state.queryText
         )
-        this.processProps({ data: { instances: this.props.data.instances } })
       })
     }
   }
 
   previousPage() {
     if (this.state.page > 0) {
-      this.setState({ page: this.state.page + -1 }, () => {
+      this.props.setWizardState({ searching: true })
+      this.setState({ page: this.state.page - 1 }, () => {
         ConnectionsActions.loadInstances(
           { id: this.props.data.sourceCloud.credential.id },
           this.state.page,
@@ -210,37 +217,42 @@ class WizardVms extends Component {
     )
   }
 
+  renderFilteredItems() {
+    if (this.state.filteredData && this.state.filteredData.length) {
+      let instances = this.state.filteredData.map((item, index) =>
+        <div className="item" key={"vm_" + index} onClick={(e) => this.checkVm(e, item)}>
+          <div className="checkbox-container">
+            <input
+              id={"vm_check_" + index}
+              type="checkbox"
+              checked={this.isSelected(item)}
+              onChange={(e) => this.checkVm(e, item)}
+              className="checkbox-normal"
+            />
+            <label htmlFor={"vm_check_" + index}></label>
+          </div>
+          <span className="cell cell-icon">
+            <div className="icon vm"></div>
+            <span className="details">
+              {item.instance_name}
+            </span>
+          </span>
+          <span className="cell">{item.num_cpu} vCPU | {item.memory_mb} MB RAM
+                {item.flavor_name && (" | " + item.flavor_name)}</span>
+        </div>
+      )
+      return instances
+    } else {
+      return <div className="no-results">Your search returned no results</div>
+    }
+  }
+
   renderSearch() {
-    let _this = this
+    if (this.props.data.instancesLoadState === 'success' || this.state.searching) {
+      return this.renderFilteredItems()
+    }
+
     switch (this.props.data.instancesLoadState) {
-      case "success":
-        if (this.state.filteredData && this.state.filteredData.length) {
-          let instances = this.state.filteredData.map((item, index) =>
-            <div className="item" key={ "vm_" + index } onClick={ (e) => _this.checkVm(e, item) }>
-              <div className="checkbox-container">
-                <input
-                  id={"vm_check_" + index}
-                  type="checkbox"
-                  checked={this.isSelected(item)}
-                  onChange={(e) => _this.checkVm(e, item)}
-                  className="checkbox-normal"
-                />
-                <label htmlFor={ "vm_check_" + index }></label>
-              </div>
-              <span className="cell cell-icon">
-                <div className="icon vm"></div>
-                <span className="details">
-                  {item.instance_name}
-                </span>
-              </span>
-              <span className="cell">{item.num_cpu} vCPU | {item.memory_mb} MB RAM
-                {item.flavor_name && (" | " + item.flavor_name)}</span>
-            </div>
-          )
-          return instances
-        } else {
-          return <div className="no-results">Your search returned no results</div>
-        }
       case "error":
         return (<div className="no-results">
           An error occurred while searching for instances <br />
@@ -254,14 +266,13 @@ class WizardVms extends Component {
   }
 
   render() {
-    let _this = this
     let vmStates = vmStatesConst.map(
       (state, index) =>
         <a
-          className={_this.state.filterStatus == state || (_this.state.filterStatus == null && state == "All") ?
+          className={this.state.filterStatus == state || (this.state.filterStatus == null && state == "All") ?
             "selected" : ""}
-          onClick={(e) => _this.filterStatus(e, state)} key={"status_" + index}
-        >{_this.toTitleCase(state)}</a>
+          onClick={(e) => this.filterStatus(e, state)} key={"status_" + index}
+        >{this.toTitleCase(state)}</a>
     )
     return (
       <div className={s.root}>
@@ -269,9 +280,10 @@ class WizardVms extends Component {
           <div className={s.topFilters}>
             <SearchBox
               placeholder="Search VMs"
+              isLoading={this.state.searching}
               value={this.state.queryText}
               onChange={(e) => this.searchVm(e)}
-              className={"searchBox" + (!(this.state.filteredData && this.state.filteredData.length) ? " hidden" : " ")}
+              show={(!this.state.filteredData || !!this.state.filteredData.length) || !!this.state.queryText}
             />
             <div className="category-filter hidden">
               {vmStates}
@@ -289,13 +301,13 @@ class WizardVms extends Component {
             (!(this.state.filteredData && this.state.filteredData.length) ? " hidden" : " ")}
           >
             <span
-              className={(this.state.page == 0 ? "disabled " : "") + s.prev}
+              className={(this.state.page === 0 || this.state.searching ? "disabled " : "") + s.prev}
               onClick={(e) => this.previousPage(e)}
             ></span>
             <span className={s.currentPage}>{this.state.page + 1}</span>
             <span
-              className={(this.state.filteredData && this.state.filteredData.length == itemsPerPage ?
-                " " : "disabled ") + s.next}
+              className={((this.state.filteredData && this.state.filteredData.length == itemsPerPage)
+                && !this.state.searching ? " " : "disabled ") + s.next}
               onClick={(e) => this.nextPage(e)}
             ></span>
           </div>

+ 11 - 9
src/stores/WizardStore/WizardStore.js

@@ -60,11 +60,10 @@ class WizardStore extends Reflux.Store
   onLoadInstances(endpoint, page = 0, queryText = "", cache = true, clearSelection = false) {
     this.setState({ instancesLoadState: 'loading' })
     if (cache && (this.state.instances && this.state.instances[page * itemsPerPage])) {
+      this.setState({ searching: false, instancesLoadState: 'success' })
       return;
     }
-    if (!cache) {
-      this.setState({ instances: null })
-    }
+
     let projectId = Reflux.GlobalState.userStore.currentUser.project.id
 
     if (clearSelection) {
@@ -76,6 +75,10 @@ class WizardStore extends Reflux.Store
       markerId = this.state.instances[(page - 1) * itemsPerPage + itemsPerPage - 1].id
     }
 
+    if (!cache) {
+      this.setState({ instances: null })
+    }
+
     let url = `${servicesUrl.coriolis}/${projectId}/endpoints/${endpoint.id}/instances?limit=${itemsPerPage}`
     if (markerId != null) {
       url = `${url}&marker=${markerId}`
@@ -87,10 +90,9 @@ class WizardStore extends Reflux.Store
     Api.sendAjaxRequest({
       url: url,
       method: "GET"
-    }).then(response => {
-      ConnectionsActions.loadInstances.completed(response, page)
-    }, ConnectionsActions.loadInstances.failed)
-      .catch(ConnectionsActions.loadInstances.failed);
+    }).then(response => ConnectionsActions.loadInstances.completed(response, page),
+      ConnectionsActions.loadInstances.failed
+      ).catch(ConnectionsActions.loadInstances.failed);
   }
 
   onLoadInstancesCompleted(response, page) {
@@ -103,11 +105,11 @@ class WizardStore extends Reflux.Store
       instances[(page * itemsPerPage) + index] = instance
     })
 
-    this.setState({ instances: instances, instancesLoadState: 'success' })
+    this.setState({ instances: instances, instancesLoadState: 'success', searching: false })
   }
 
   onLoadInstancesFailed() {
-    this.setState({ instances: [], instancesLoadState: 'error' })
+    this.setState({ instances: [], instancesLoadState: 'error', searching: false })
   }
 
   onLoadInstanceDetail(endpoint, instance) {