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

Merge pull request #48 from smiclea/CORWEB-39

Improve wizard instances search loading and pagination CORWEB-39 CORWEB-78
Dorin Paslaru 8 лет назад
Родитель
Сommit
af36bea6a4

+ 16 - 1
src/components/App/App.scss

@@ -33,7 +33,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 html {
 html {
   color: $black;
   color: $black;
-  font-weight: $weight-regular;
+  font-weight: $weight-light;
   font-size: 14px;
   font-size: 14px;
   font-family: $font-family-base;
   font-family: $font-family-base;
   line-height: 1.375; /* ~22px */
   line-height: 1.375; /* ~22px */
@@ -469,6 +469,21 @@ button {
   }
   }
 }
 }
 :global {
 :global {
+  .spinner,
+  .spinner:after {
+    border-radius: 50%;
+    width: 12px;
+    height: 12px;
+  }
+  .spinner {
+    position: relative;
+    border-top: 2px solid $gray;
+    border-right: 2px solid $gray;
+    border-bottom: 2px solid $gray;
+    border-left: 2px solid $blue;
+    transform: translateZ(0);
+    animation: rotate 2s infinite linear;
+  }
   .taskIcon {
   .taskIcon {
     width: 16px;
     width: 16px;
     height: 16px;
     height: 16px;

+ 1 - 2
src/components/MigrationWizard/MigrationWizard.scss

@@ -49,7 +49,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 .container {
 .container {
   flex: 1;
   flex: 1;
   margin: 0 auto;
   margin: 0 auto;
-  padding: 0px 32px 70px;
+  padding: 0px 0px 70px 32px;
   overflow-y: auto;
   overflow-y: auto;
   width: $wizard-content-width;
   width: $wizard-content-width;
 
 
@@ -66,7 +66,6 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
         background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iN3B4IiBoZWlnaHQ9IjEzcHgiIHZpZXdCb3g9Ii0xIC0xIDcgMTMiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PHBvbHlsaW5lIGlkPSJQYWdlLTEtQ29weSIgc3Ryb2tlPSIjNjE2NzcwIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDIuNTAwMDAwLCA1LjUwMDAwMCkgcm90YXRlKC0yNzAuMDAwMDAwKSB0cmFuc2xhdGUoLTIuNTAwMDAwLCAtNS41MDAwMDApICIgcG9pbnRzPSItMyA4IDIuNSAzIDggOCI+PC9wb2x5bGluZT48L3N2Zz4=');
         background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iN3B4IiBoZWlnaHQ9IjEzcHgiIHZpZXdCb3g9Ii0xIC0xIDcgMTMiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PHBvbHlsaW5lIGlkPSJQYWdlLTEtQ29weSIgc3Ryb2tlPSIjNjE2NzcwIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDIuNTAwMDAwLCA1LjUwMDAwMCkgcm90YXRlKC0yNzAuMDAwMDAwKSB0cmFuc2xhdGUoLTIuNTAwMDAwLCAtNS41MDAwMDApICIgcG9pbnRzPSItMyA4IDIuNSAzIDggOCI+PC9wb2x5bGluZT48L3N2Zz4=');
         width: 7px;
         width: 7px;
         height: 13px;
         height: 13px;
-        display: inline-block;
         margin-left: 3px;
         margin-left: 3px;
         margin-top: 3px;
         margin-top: 3px;
         float:right;
         float:right;

+ 23 - 11
src/components/SearchBox/SearchBox.js

@@ -25,8 +25,10 @@ class SearchBox extends Component {
     maxLines: PropTypes.number,
     maxLines: PropTypes.number,
     minimize: PropTypes.bool,
     minimize: PropTypes.bool,
     placeholder: PropTypes.string,
     placeholder: PropTypes.string,
+    show: PropTypes.bool,
     onChange: PropTypes.func,
     onChange: PropTypes.func,
-    className: PropTypes.string
+    className: PropTypes.string,
+    isLoading: PropTypes.bool
   };
   };
 
 
   static defaultProps = {
   static defaultProps = {
@@ -63,17 +65,27 @@ class SearchBox extends Component {
   }
   }
 
 
   render() {
   render() {
+    let renderLoading = () => this.props.isLoading ? <div className="spinner"></div> : null
+
+    if (this.props.show === false) {
+      return null
+    }
+
     return (
     return (
-      <div className={s.root}>
-        <input
-          type="text"
-          placeholder={this.props.placeholder}
-          value={this.state.queryText}
-          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}
-        />
+      <div className={s.root + ' ' + (this.props.className || '')}>
+        <div className={s.content}>
+          {renderLoading()}
+          <input
+            type="text"
+            placeholder={this.props.placeholder}
+            value={this.state.queryText}
+            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}
+          />
+        </div>
       </div>
       </div>
     );
     );
   }
   }

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

@@ -21,9 +21,17 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 $searchStrokeColor: $gray-dark;
 $searchStrokeColor: $gray-dark;
 
 
 .root {
 .root {
-}
-.input { }
+  :global(.spinner) {
+    position: absolute;
+    top: 8px;
+    right: 8px;
+  }
 
 
+  .content {
+    position: relative;
+    display: inline-block;
+  }
+}
 
 
 input[type="text"].searchBox {
 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>');
   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 +39,9 @@ input[type="text"].searchBox {
   background-repeat: no-repeat;
   background-repeat: no-repeat;
   background-position: 8px 9px;
   background-position: 8px 9px;
   padding-left: 36px;
   padding-left: 36px;
+  padding-right: 30px;
   cursor: pointer;
   cursor: pointer;
-  width: 146px;
+  width: 192px;
   &:focus, &:hover {
   &: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>');
     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>');
   }
   }

+ 98 - 68
src/components/WizardVms/WizardVms.js

@@ -24,7 +24,7 @@ import ConnectionsActions from '../../actions/ConnectionsActions';
 import LoadingIcon from '../LoadingIcon';
 import LoadingIcon from '../LoadingIcon';
 
 
 const title = 'Select instances to migrate';
 const title = 'Select instances to migrate';
-const vmStatesConst = ["All", "RUNNING", "PAUSED", "STOPPED"]
+const loadingStates = { IDLE: 0, QUERY: 1, PAGINATION: 2 }
 const searchTimeout = 1000;
 const searchTimeout = 1000;
 
 
 class WizardVms extends Component {
 class WizardVms extends Component {
@@ -50,11 +50,12 @@ class WizardVms extends Component {
       })
       })
     }
     }
 
 
-    this.retryLoadingInstances = this.retryLoadingInstances.bind(this)
+    this.reloadInstances = this.reloadInstances.bind(this)
 
 
     this.state = {
     this.state = {
       valid,
       valid,
       queryText: '',
       queryText: '',
+      loadingState: loadingStates.IDLE,
       page: 0,
       page: 0,
       filterStatus: 'All',
       filterStatus: 'All',
       filteredData: this.props.data.instances ? this.props.data.instances.slice(0, itemsPerPage) : [],
       filteredData: this.props.data.instances ? this.props.data.instances.slice(0, itemsPerPage) : [],
@@ -78,9 +79,17 @@ class WizardVms extends Component {
   }
   }
 
 
   processProps(props) {
   processProps(props) {
-    if (props.data.instances) {
-      this.setState({ filteredData: props.data.instances.slice(
-        this.state.page * itemsPerPage, this.state.page * itemsPerPage + itemsPerPage) })
+    let loadingState = typeof props.data.loadingState === undefined ? this.state.loadingState : props.data.loadingState
+    if (props.data.instances && !loadingState) {
+      this.setState({
+        filteredData: props.data.instances.slice(
+          this.state.page * itemsPerPage, this.state.page * itemsPerPage + itemsPerPage),
+        loadingState: loadingState.IDLE
+      })
+    } else {
+      this.setState({
+        loadingState: loadingState
+      })
     }
     }
   }
   }
 
 
@@ -115,21 +124,20 @@ class WizardVms extends Component {
 
 
     if (this.props.data.instances) {
     if (this.props.data.instances) {
       this.props.data.instances.forEach((vm) => {
       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)
           queryResult.push(vm)
         }
         }
       }, this)
       }, this)
     }
     }
 
 
     if (this.state.queryText != queryText) {
     if (this.state.queryText != queryText) {
+      this.props.setWizardState({ loadingState: loadingStates.QUERY })
+
       if (this.timeout != null) {
       if (this.timeout != null) {
         clearTimeout(this.timeout)
         clearTimeout(this.timeout)
       }
       }
       this.timeout = setTimeout(() => {
       this.timeout = setTimeout(() => {
-        this.setState({ page: 0, filteredData: null, queryText: queryText }, () => {
-          this.props.setWizardState({ instances: null })
+        this.setState({ queryText: queryText, page: 0 }, () => {
           ConnectionsActions.loadInstances(
           ConnectionsActions.loadInstances(
             { id: this.props.data.sourceCloud.credential.id },
             { id: this.props.data.sourceCloud.credential.id },
             this.state.page,
             this.state.page,
@@ -160,9 +168,7 @@ class WizardVms extends Component {
   }
   }
 
 
   toTitleCase(str) {
   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) {
   isSelected(item) {
@@ -177,20 +183,21 @@ class WizardVms extends Component {
 
 
   nextPage() {
   nextPage() {
     if (this.state.filteredData && this.state.filteredData.length == itemsPerPage) {
     if (this.state.filteredData && this.state.filteredData.length == itemsPerPage) {
+      this.props.setWizardState({ loadingState: loadingStates.PAGINATION })
       this.setState({ page: this.state.page + 1 }, () => {
       this.setState({ page: this.state.page + 1 }, () => {
         ConnectionsActions.loadInstances(
         ConnectionsActions.loadInstances(
           { id: this.props.data.sourceCloud.credential.id },
           { id: this.props.data.sourceCloud.credential.id },
           this.state.page,
           this.state.page,
           this.state.queryText
           this.state.queryText
         )
         )
-        this.processProps({ data: { instances: this.props.data.instances } })
       })
       })
     }
     }
   }
   }
 
 
   previousPage() {
   previousPage() {
     if (this.state.page > 0) {
     if (this.state.page > 0) {
-      this.setState({ page: this.state.page + -1 }, () => {
+      this.props.setWizardState({ loadingState: loadingStates.PAGINATION })
+      this.setState({ page: this.state.page - 1 }, () => {
         ConnectionsActions.loadInstances(
         ConnectionsActions.loadInstances(
           { id: this.props.data.sourceCloud.credential.id },
           { id: this.props.data.sourceCloud.credential.id },
           this.state.page,
           this.state.page,
@@ -201,7 +208,7 @@ class WizardVms extends Component {
     }
     }
   }
   }
 
 
-  retryLoadingInstances() {
+  reloadInstances() {
     ConnectionsActions.loadInstances(
     ConnectionsActions.loadInstances(
       { id: this.props.data.sourceCloud.credential.id },
       { id: this.props.data.sourceCloud.credential.id },
       this.state.page,
       this.state.page,
@@ -210,41 +217,53 @@ class WizardVms extends Component {
     )
     )
   }
   }
 
 
+  refreshButtonClick() {
+    this.props.setWizardState({ loadingState: loadingStates.PAGINATION })
+    this.setState({ page: 0 }, this.reloadInstances)
+  }
+
+  renderFilteredItems() {
+    if (this.state.filteredData && this.state.filteredData.length) {
+      let instances = this.state.filteredData.map((item, index) =>
+        <div className={'item ' + (this.isSelected(item) ? 'selected' : '')}
+          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() {
   renderSearch() {
-    let _this = this
+    if (this.props.data.instancesLoadState === 'success' || this.state.loadingState) {
+      return this.renderFilteredItems()
+    }
+
     switch (this.props.data.instancesLoadState) {
     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":
       case "error":
         return (<div className="no-results">
         return (<div className="no-results">
           An error occurred while searching for instances <br />
           An error occurred while searching for instances <br />
-          <button onClick={this.retryLoadingInstances}>Retry</button>
+          <button onClick={this.reloadInstances}>Retry</button>
         </div>)
         </div>)
       case "loading":
       case "loading":
         return <LoadingIcon padding={64} text="Loading instances.." />
         return <LoadingIcon padding={64} text="Loading instances.." />
@@ -253,49 +272,60 @@ class WizardVms extends Component {
     }
     }
   }
   }
 
 
+  renderSelectionInfo() {
+    if (this.state.filteredData && this.state.filteredData.length) {
+      return (
+        <div className={s.selectionInfo}>
+          <div className={s.selectionCount +
+            (!(this.state.filteredData && this.state.filteredData.length) ? " hidden" : " ")}
+          >
+            {this.instancesSelected()} instances selected
+                </div>
+          <div className={s.refreshButton}
+            onClick={this.refreshButtonClick.bind(this)}
+          >
+            <div className="refresh icon"></div>
+          </div>
+        </div>
+      )
+    }
+
+    return null
+  }
+
   render() {
   render() {
-    let _this = this
-    let vmStates = vmStatesConst.map(
-      (state, index) =>
-        <a
-          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>
-    )
     return (
     return (
       <div className={s.root}>
       <div className={s.root}>
         <div className={s.container}>
         <div className={s.container}>
           <div className={s.topFilters}>
           <div className={s.topFilters}>
             <SearchBox
             <SearchBox
               placeholder="Search VMs"
               placeholder="Search VMs"
+              isLoading={this.state.loadingState === loadingStates.QUERY}
               value={this.state.queryText}
               value={this.state.queryText}
               onChange={(e) => this.searchVm(e)}
               onChange={(e) => this.searchVm(e)}
-              className={"searchBox" + (!(this.state.filteredData && this.state.filteredData.length) ? " hidden" : " ")}
+              className={s.searchBox}
+              show={(!this.state.filteredData || !!this.state.filteredData.length)
+                || this.state.loadingState > 0 || !!this.state.queryText}
             />
             />
-            <div className="category-filter hidden">
-              {vmStates}
-            </div>
+            {this.renderSelectionInfo()}
           </div>
           </div>
           <div className="items-list instances">
           <div className="items-list instances">
             {this.renderSearch()}
             {this.renderSearch()}
           </div>
           </div>
-          <div className={s.selectionCount +
-            (!(this.state.filteredData && this.state.filteredData.length) ? " hidden" : " ")}
-          >
-            {this.instancesSelected()} instances selected
-          </div>
           <div className={s.pagination +
           <div className={s.pagination +
             (!(this.state.filteredData && this.state.filteredData.length) ? " hidden" : " ")}
             (!(this.state.filteredData && this.state.filteredData.length) ? " hidden" : " ")}
           >
           >
             <span
             <span
-              className={(this.state.page == 0 ? "disabled " : "") + s.prev}
+              className={(this.state.page === 0 || this.state.loadingState ? "disabled " : "") + s.prev}
               onClick={(e) => this.previousPage(e)}
               onClick={(e) => this.previousPage(e)}
             ></span>
             ></span>
-            <span className={s.currentPage}>{this.state.page + 1}</span>
+            <span className={s.currentPage}>{
+              this.state.loadingState === loadingStates.PAGINATION ?
+                <div className="spinner"></div> : this.state.page + 1
+            }</span>
             <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.loadingState ? " " : "disabled ") + s.next}
               onClick={(e) => this.nextPage(e)}
               onClick={(e) => this.nextPage(e)}
             ></span>
             ></span>
           </div>
           </div>

+ 43 - 25
src/components/WizardVms/WizardVms.scss

@@ -17,14 +17,6 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 @import '../variables.scss';
 @import '../variables.scss';
 
 
-.root {
-  :global(.searchBox) {
-    float: right;
-    margin-top: -5px;
-    width: 116px;
-  }
-}
-
 .container {
 .container {
   margin: 0 auto;
   margin: 0 auto;
   padding: 10px 0 40px;
   padding: 10px 0 40px;
@@ -34,13 +26,14 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
     :global(.item) {
     :global(.item) {
       position: relative;
       position: relative;
       cursor: pointer;
       cursor: pointer;
-      padding-left: 0;
-      width: 100%;
+      padding-left: 16px;
+      padding-right: 0;
       span:nth-child(2) {
       span:nth-child(2) {
         flex: 2;
         flex: 2;
       }
       }
       span:nth-child(3) {
       span:nth-child(3) {
-        justify-content: center;
+        justify-content: flex-end;
+        padding-right: 16px;
       }
       }
       span:nth-child(4) {
       span:nth-child(4) {
         flex: 2;
         flex: 2;
@@ -49,25 +42,45 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
         position: absolute;
         position: absolute;
         left: -32px;
         left: -32px;
         top: 24px;
         top: 24px;
+        opacity: 0;
+        transition: opacity $animation-swift-out;
+      }
+      &:hover, &:global(.selected) {
+        :global(.checkbox-container) {
+          opacity: 1;
+        }
       }
       }
     }
     }
   }
   }
   .topFilters {
   .topFilters {
-    &:after {
-      clear: both;
-      display: block;
-      content: " ";
-      height: 0;
+    display: flex;
+
+    .searchBox {
+      flex-grow: 1;
+    }
+
+    .refreshButton {
+      cursor: pointer;
+      padding: 8px 0 0 7px;
+      margin-top: 4px;
+      height: 19px;
+    }
+
+    .selectionInfo {
+      display: flex;
+    }
+    
+    .selectionCount {
+      height: 16px;
+      font-size: 14px;
+      margin-top: 12px;
+      color: $gray-dark;
+      border-right: 1px solid $gray-dark;
+      padding-right: 8px;
     }
     }
   }
   }
 }
 }
 
 
-.selectionCount {
-  font-size: 14px;
-  color: $gray-dark;
-  float: left;
-  margin-top: 22px;
-}
 .pagination {
 .pagination {
   font-size: 14px;
   font-size: 14px;
   color: $gray-dark;
   color: $gray-dark;
@@ -81,7 +94,6 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
     background-repeat: no-repeat;
     background-repeat: no-repeat;
     width: 32px;
     width: 32px;
     height: 32px;
     height: 32px;
-    display: inline-block;
     float: left;
     float: left;
     &:global(.disabled) {
     &:global(.disabled) {
       opacity: 0.3;
       opacity: 0.3;
@@ -94,7 +106,6 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
     background-repeat: no-repeat;
     background-repeat: no-repeat;
     width: 32px;
     width: 32px;
     height: 32px;
     height: 32px;
-    display: inline-block;
     float: left;
     float: left;
     &:global(.disabled) {
     &:global(.disabled) {
       opacity: 0.3;
       opacity: 0.3;
@@ -109,12 +120,19 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
     cursor: default;
     cursor: default;
     width: 32px;
     width: 32px;
     height: 32px;
     height: 32px;
-    display: inline-block;
     float: left;
     float: left;
     text-align: center;
     text-align: center;
     line-height: 32px;
     line-height: 32px;
     border-left: 1px solid $gray;
     border-left: 1px solid $gray;
     border-right: 1px solid $gray;
     border-right: 1px solid $gray;
+    :global(.spinner) {
+      top: 8px;
+      left: 8px;
+      border-top-color: $gray;
+      border-right-color: $gray;
+      border-bottom-color: $gray;
+      border-left-color: $gray-lighter;
+    }
   }
   }
   &:after {
   &:after {
     display: block;
     display: block;

+ 1 - 1
src/components/variables.scss

@@ -42,7 +42,7 @@ $header-text-color: $blue;
 
 
 $max-content-width:     100%;
 $max-content-width:     100%;
 $narrow-content-width: 928px;
 $narrow-content-width: 928px;
-$wizard-content-width: 768px;
+$wizard-content-width: 770px;
 
 
 $border-radius: 4px;
 $border-radius: 4px;
 
 

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