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

Show instances per page based on window height

Resizing and zooming affects the number of instances shown per page in
Wizard Instances selection page.

The minimum and maximum instances per page is configurable in
`config.js` at the `instancesPerPage:` line.
Sergiu Miclea 7 лет назад
Родитель
Сommit
ac71b2bceb

+ 1 - 1
src/components/atoms/Arrow/images/arrow-thick.js

@@ -3,7 +3,7 @@ export default color => `<?xml version="1.0" encoding="UTF-8"?>
     <!-- Generator: Sketch 52.3 (67297) - http://www.bohemiancoding.com/sketch -->
     <!-- Generator: Sketch 52.3 (67297) - http://www.bohemiancoding.com/sketch -->
     <title>Rectangle Copy</title>
     <title>Rectangle Copy</title>
     <desc>Created with Sketch.</desc>
     <desc>Created with Sketch.</desc>
-    <g id="Symbols" transform="translateY(-1px)" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
+    <g id="Symbols" transform="translate(0 -1)" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
         <g id="Icon/Chevron/Black" transform="translate(-5.000000, -1.000000)" stroke="${color}" stroke-width="1.5">
         <g id="Icon/Chevron/Black" transform="translate(-5.000000, -1.000000)" stroke="${color}" stroke-width="1.5">
             <polyline id="Rectangle-Copy" transform="translate(6.000000, 7.500000) rotate(315.000000) translate(-6.000000, -7.500000) " points="9.8890873 3.6109127 9.8890873 11.3890873 2.1109127 11.3890873"></polyline>
             <polyline id="Rectangle-Copy" transform="translate(6.000000, 7.500000) rotate(315.000000) translate(-6.000000, -7.500000) " points="9.8890873 3.6109127 9.8890873 11.3890873 2.1109127 11.3890873"></polyline>
         </g>
         </g>

+ 1 - 1
src/components/atoms/Arrow/images/arrow.js

@@ -20,7 +20,7 @@ version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/
     <desc>Created with Sketch.</desc>
     <desc>Created with Sketch.</desc>
     <defs></defs>
     <defs></defs>
     <g id="Coriolis" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"
     <g id="Coriolis" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"
-     stroke-linecap="round" stroke-linejoin="round" transform="rotate(90deg)">
+     stroke-linecap="round" stroke-linejoin="round">
         <g id="202-Replica-Executions" transform="translate(-1197.000000, -194.000000)" stroke="${color}">
         <g id="202-Replica-Executions" transform="translate(-1197.000000, -194.000000)" stroke="${color}">
             <g id="Icon/Chevron/Grey"
             <g id="Icon/Chevron/Grey"
             transform="translate(1200.000000, 200.000000) rotate(-90.000000)
             transform="translate(1200.000000, 200.000000) rotate(-90.000000)

+ 16 - 14
src/components/organisms/WizardInstances/test.jsx

@@ -30,15 +30,16 @@ let instances = [
   { id: 'i-2', flavor_name: 'Flavor name', instance_name: 'Instance name 2', num_cpu: 3, memory_mb: 1024 },
   { id: 'i-2', flavor_name: 'Flavor name', instance_name: 'Instance name 2', num_cpu: 3, memory_mb: 1024 },
   { id: 'i-3', flavor_name: 'Flavor name', instance_name: 'Instance name 3', num_cpu: 3, memory_mb: 1024 },
   { id: 'i-3', flavor_name: 'Flavor name', instance_name: 'Instance name 3', num_cpu: 3, memory_mb: 1024 },
 ]
 ]
+let onChunkSizeUpdate = () => { }
 
 
 describe('WizardInstances Component', () => {
 describe('WizardInstances Component', () => {
   it('has correct number of instances', () => {
   it('has correct number of instances', () => {
-    let wrapper = wrap({ instances, currentPage: 1 })
+    let wrapper = wrap({ instances, currentPage: 1, onChunkSizeUpdate })
     expect(wrapper.find('item-', true).length).toBe(instances.length)
     expect(wrapper.find('item-', true).length).toBe(instances.length)
   })
   })
 
 
   it('has correct instances info', () => {
   it('has correct instances info', () => {
-    let wrapper = wrap({ instances, currentPage: 1 })
+    let wrapper = wrap({ instances, currentPage: 1, onChunkSizeUpdate })
     instances.forEach(instance => {
     instances.forEach(instance => {
       expect(wrapper.find(`item-${instance.id}`).findText('itemName')).toBe(instance.instance_name)
       expect(wrapper.find(`item-${instance.id}`).findText('itemName')).toBe(instance.instance_name)
       expect(wrapper.find(`item-${instance.id}`).findText('itemDetails')).toBe(`${instance.num_cpu} vCPU | ${instance.memory_mb} MB RAM | ${instance.flavor_name}`)
       expect(wrapper.find(`item-${instance.id}`).findText('itemDetails')).toBe(`${instance.num_cpu} vCPU | ${instance.memory_mb} MB RAM | ${instance.flavor_name}`)
@@ -53,6 +54,7 @@ describe('WizardInstances Component', () => {
         { ...instances[0] },
         { ...instances[0] },
         { ...instances[2] },
         { ...instances[2] },
       ],
       ],
+      onChunkSizeUpdate,
     })
     })
     expect(wrapper.findText('selInfo')).toBe('2 instances selected')
     expect(wrapper.findText('selInfo')).toBe('2 instances selected')
     expect(wrapper.find('item-i-1').prop('selected')).toBe(true)
     expect(wrapper.find('item-i-1').prop('selected')).toBe(true)
@@ -61,56 +63,56 @@ describe('WizardInstances Component', () => {
   })
   })
 
 
   it('renders current page', () => {
   it('renders current page', () => {
-    let wrapper = wrap({ instances, currentPage: 2, chunkSize: 2 })
+    let wrapper = wrap({ instances, currentPage: 2, chunkSize: 2, onChunkSizeUpdate })
     expect(wrapper.findText('currentPage')).toBe('2 of 2')
     expect(wrapper.findText('currentPage')).toBe('2 of 2')
   })
   })
 
 
   it('renders previous page disabled if page is 1', () => {
   it('renders previous page disabled if page is 1', () => {
-    let wrapper = wrap({ instances, currentPage: 1 })
+    let wrapper = wrap({ instances, currentPage: 1, onChunkSizeUpdate })
     expect(wrapper.find('prevPageButton').prop('disabled')).toBe(true)
     expect(wrapper.find('prevPageButton').prop('disabled')).toBe(true)
   })
   })
 
 
   it('renders previous page enabled if page is greater than 1', () => {
   it('renders previous page enabled if page is greater than 1', () => {
-    let wrapper = wrap({ instances, currentPage: 3 })
+    let wrapper = wrap({ instances, currentPage: 3, onChunkSizeUpdate })
     expect(wrapper.find('prevPageButton').prop('disabled')).toBeFalsy()
     expect(wrapper.find('prevPageButton').prop('disabled')).toBeFalsy()
     expect(wrapper.find('loadingStatus').length).toBe(0)
     expect(wrapper.find('loadingStatus').length).toBe(0)
   })
   })
 
 
   it('renders loading', () => {
   it('renders loading', () => {
-    let wrapper = wrap({ instances, currentPage: 1, loading: true })
+    let wrapper = wrap({ instances, currentPage: 1, loading: true, onChunkSizeUpdate })
     expect(wrapper.find('loadingStatus').length).toBe(1)
     expect(wrapper.find('loadingStatus').length).toBe(1)
   })
   })
 
 
   it('renders searching', () => {
   it('renders searching', () => {
-    let wrapper = wrap({ instances, currentPage: 1, searching: true })
+    let wrapper = wrap({ instances, currentPage: 1, searching: true, onChunkSizeUpdate })
     expect(wrapper.find('searchInput').prop('loading')).toBe(true)
     expect(wrapper.find('searchInput').prop('loading')).toBe(true)
   })
   })
 
 
   it('renders search not found', () => {
   it('renders search not found', () => {
-    let wrapper = wrap({ instances: [], currentPage: 1, searchNotFound: true })
+    let wrapper = wrap({ instances: [], currentPage: 1, searchNotFound: true, onChunkSizeUpdate })
     expect(wrapper.findText('notFoundText')).toBe('Your search returned no results')
     expect(wrapper.findText('notFoundText')).toBe('Your search returned no results')
     expect(wrapper.find('loadingChunks').length).toBe(0)
     expect(wrapper.find('loadingChunks').length).toBe(0)
   })
   })
 
 
   it('renders loading page', () => {
   it('renders loading page', () => {
-    let wrapper = wrap({ instances, currentPage: 1, chunksLoading: true })
+    let wrapper = wrap({ instances, currentPage: 1, chunksLoading: true, onChunkSizeUpdate })
     expect(wrapper.find('loadingChunks').length).toBe(1)
     expect(wrapper.find('loadingChunks').length).toBe(1)
   })
   })
 
 
   it('enabled next page', () => {
   it('enabled next page', () => {
-    let wrapper = wrap({ instances, currentPage: 1 })
+    let wrapper = wrap({ instances, currentPage: 1, onChunkSizeUpdate })
     expect(wrapper.find('nextPageButton').prop('disabled')).toBe(true)
     expect(wrapper.find('nextPageButton').prop('disabled')).toBe(true)
-    wrapper = wrap({ instances, currentPage: 1, chunkSize: 2 })
+    wrapper = wrap({ instances, currentPage: 1, chunkSize: 2, onChunkSizeUpdate })
     expect(wrapper.find('nextPageButton').prop('disabled')).toBeFalsy()
     expect(wrapper.find('nextPageButton').prop('disabled')).toBeFalsy()
   })
   })
 
 
   it('dispatches next and previous page click, if enabled', () => {
   it('dispatches next and previous page click, if enabled', () => {
     let onPageClick = sinon.spy()
     let onPageClick = sinon.spy()
-    let wrapper = wrap({ instances, currentPage: 1, onPageClick })
+    let wrapper = wrap({ instances, currentPage: 1, onPageClick, onChunkSizeUpdate })
     wrapper.find('nextPageButton').click()
     wrapper.find('nextPageButton').click()
     wrapper.find('prevPageButton').click()
     wrapper.find('prevPageButton').click()
     expect(onPageClick.callCount).toBe(0)
     expect(onPageClick.callCount).toBe(0)
-    wrapper = wrap({ instances, currentPage: 2, onPageClick, chunkSize: 1 })
+    wrapper = wrap({ instances, currentPage: 2, onPageClick, chunkSize: 1, onChunkSizeUpdate })
     wrapper.find('nextPageButton').click()
     wrapper.find('nextPageButton').click()
     wrapper.find('prevPageButton').click()
     wrapper.find('prevPageButton').click()
     expect(onPageClick.callCount).toBe(2)
     expect(onPageClick.callCount).toBe(2)
@@ -118,7 +120,7 @@ describe('WizardInstances Component', () => {
 
 
   it('dispaches reload click', () => {
   it('dispaches reload click', () => {
     let onReloadClick = sinon.spy()
     let onReloadClick = sinon.spy()
-    let wrapper = wrap({ instances, currentPage: 1, onReloadClick })
+    let wrapper = wrap({ instances, currentPage: 1, onReloadClick, onChunkSizeUpdate })
     wrapper.find('reloadButton').click()
     wrapper.find('reloadButton').click()
     expect(onReloadClick.calledOnce).toBe(true)
     expect(onReloadClick.calledOnce).toBe(true)
   })
   })

+ 24 - 3
src/components/pages/WizardPage/WizardPage.jsx

@@ -16,6 +16,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 import React from 'react'
 import React from 'react'
 import styled from 'styled-components'
 import styled from 'styled-components'
+import autobind from 'autobind-decorator'
 import { observer } from 'mobx-react'
 import { observer } from 'mobx-react'
 
 
 import WizardTemplate from '../../templates/WizardTemplate'
 import WizardTemplate from '../../templates/WizardTemplate'
@@ -68,14 +69,23 @@ class WizardPage extends React.Component<Props, State> {
 
 
   contentRef: WizardPageContent
   contentRef: WizardPageContent
 
 
+  get instancesChunkSize() {
+    let { min, max } = wizardConfig.instancesPerPage
+    const instancesTableDiff = 505
+    const instancesItemHeight = 67
+    return Math.min(max, Math.max(min, Math.floor((window.innerHeight - instancesTableDiff) / instancesItemHeight)))
+  }
+
   componentWillMount() {
   componentWillMount() {
     this.initializeState()
     this.initializeState()
+    this.handleResize()
   }
   }
 
 
   componentDidMount() {
   componentDidMount() {
     document.title = 'Coriolis Wizard'
     document.title = 'Coriolis Wizard'
     KeyboardManager.onEnter('wizard', () => { this.handleEnterKey() })
     KeyboardManager.onEnter('wizard', () => { this.handleEnterKey() })
     KeyboardManager.onEsc('wizard', () => { this.handleEscKey() })
     KeyboardManager.onEsc('wizard', () => { this.handleEscKey() })
+    window.addEventListener('resize', this.handleResize)
   }
   }
 
 
   componentWillReceiveProps(newProps: Props) {
   componentWillReceiveProps(newProps: Props) {
@@ -90,6 +100,12 @@ class WizardPage extends React.Component<Props, State> {
     wizardStore.clearData()
     wizardStore.clearData()
     instanceStore.cancelIntancesChunksLoading()
     instanceStore.cancelIntancesChunksLoading()
     KeyboardManager.removeKeyDown('wizard')
     KeyboardManager.removeKeyDown('wizard')
+    window.removeEventListener('resize', this.handleResize, false)
+  }
+
+  @autobind
+  handleResize() {
+    instanceStore.updateChunkSize(this.instancesChunkSize)
   }
   }
 
 
   handleEnterKey() {
   handleEnterKey() {
@@ -182,7 +198,7 @@ class WizardPage extends React.Component<Props, State> {
       endpointStore.getConnectionInfo(source).then(() => {
       endpointStore.getConnectionInfo(source).then(() => {
         if (source) {
         if (source) {
           // Preload instances for 'vms' page
           // Preload instances for 'vms' page
-          instanceStore.loadInstancesInChunks(source.id)
+          instanceStore.loadInstancesInChunks(source.id, this.instancesChunkSize)
         }
         }
       }).catch(() => {
       }).catch(() => {
         this.handleSourceEndpointChange(null)
         this.handleSourceEndpointChange(null)
@@ -228,7 +244,7 @@ class WizardPage extends React.Component<Props, State> {
 
 
   handleInstancesReloadClick() {
   handleInstancesReloadClick() {
     if (wizardStore.data.source) {
     if (wizardStore.data.source) {
-      instanceStore.reloadInstances(wizardStore.data.source.id)
+      instanceStore.reloadInstances(wizardStore.data.source.id, this.instancesChunkSize)
     }
     }
   }
   }
 
 
@@ -242,6 +258,10 @@ class WizardPage extends React.Component<Props, State> {
     instanceStore.setPage(page)
     instanceStore.setPage(page)
   }
   }
 
 
+  handleInstanceChunkSizeUpdate(chunkSize: number) {
+    instanceStore.updateChunkSize(chunkSize)
+  }
+
   handleOptionsChange(field: Field, value: any) {
   handleOptionsChange(field: Field, value: any) {
     wizardStore.updateData({ networks: null })
     wizardStore.updateData({ networks: null })
     wizardStore.updateOptions({ field, value })
     wizardStore.updateOptions({ field, value })
@@ -333,7 +353,7 @@ class WizardPage extends React.Component<Props, State> {
           // Check if user has permission for this endpoint
           // Check if user has permission for this endpoint
           endpointStore.getConnectionInfo(source).then(() => {
           endpointStore.getConnectionInfo(source).then(() => {
             // Preload instances for 'vms' page
             // Preload instances for 'vms' page
-            instanceStore.loadInstancesInChunks(source.id)
+            instanceStore.loadInstancesInChunks(source.id, this.instancesChunkSize)
           }).catch(() => {
           }).catch(() => {
             this.handleSourceEndpointChange(null)
             this.handleSourceEndpointChange(null)
           })
           })
@@ -477,6 +497,7 @@ class WizardPage extends React.Component<Props, State> {
             onInstancesReloadClick={() => { this.handleInstancesReloadClick() }}
             onInstancesReloadClick={() => { this.handleInstancesReloadClick() }}
             onInstanceClick={instance => { this.handleInstanceClick(instance) }}
             onInstanceClick={instance => { this.handleInstanceClick(instance) }}
             onInstancePageClick={page => { this.handleInstancePageClick(page) }}
             onInstancePageClick={page => { this.handleInstancePageClick(page) }}
+            onInstanceChunkSizeUpdate={chunkSize => { this.handleInstanceChunkSizeUpdate(chunkSize) }}
             onOptionsChange={(field, value) => { this.handleOptionsChange(field, value) }}
             onOptionsChange={(field, value) => { this.handleOptionsChange(field, value) }}
             onNetworkChange={(sourceNic, targetNetwork) => { this.handleNetworkChange(sourceNic, targetNetwork) }}
             onNetworkChange={(sourceNic, targetNetwork) => { this.handleNetworkChange(sourceNic, targetNetwork) }}
             onAddScheduleClick={schedule => { this.handleAddScheduleClick(schedule) }}
             onAddScheduleClick={schedule => { this.handleAddScheduleClick(schedule) }}

+ 1 - 0
src/config.js

@@ -85,6 +85,7 @@ export const wizardConfig = {
     { id: 'schedule', title: 'Schedule', breadcrumb: 'Schedule', excludeFrom: 'migration' },
     { id: 'schedule', title: 'Schedule', breadcrumb: 'Schedule', excludeFrom: 'migration' },
     { id: 'summary', title: 'Summary', breadcrumb: 'Summary' },
     { id: 'summary', title: 'Summary', breadcrumb: 'Summary' },
   ],
   ],
+  instancesPerPage: { min: 3, max: Infinity },
 }
 }
 
 
 // A list of providers for which `destination-options` API call(s) will be made in the Wizard
 // A list of providers for which `destination-options` API call(s) will be made in the Wizard

+ 16 - 7
src/stores/InstanceStore.js

@@ -115,7 +115,7 @@ class InstanceStore {
   lastEndpointId: string
   lastEndpointId: string
   reqId: number
   reqId: number
 
 
-  @action loadInstancesInChunks(endpointId: string, reload?: boolean) {
+  @action loadInstancesInChunks(endpointId: string, chunkSize?: number = 6, reload?: boolean) {
     ApiCaller.cancelRequests(`${endpointId}-chunk`)
     ApiCaller.cancelRequests(`${endpointId}-chunk`)
 
 
     this.backgroundInstances = []
     this.backgroundInstances = []
@@ -129,7 +129,7 @@ class InstanceStore {
 
 
     let loadNextChunk = (lastEndpointId?: string) => {
     let loadNextChunk = (lastEndpointId?: string) => {
       let currentEndpointId = endpointId
       let currentEndpointId = endpointId
-      InstanceSource.loadInstancesChunk(currentEndpointId, this.chunkSize, lastEndpointId, `${endpointId}-chunk`)
+      InstanceSource.loadInstancesChunk(currentEndpointId, chunkSize, lastEndpointId, `${endpointId}-chunk`)
         .then(instances => {
         .then(instances => {
           if (currentEndpointId !== this.lastEndpointId) {
           if (currentEndpointId !== this.lastEndpointId) {
             return
             return
@@ -141,7 +141,7 @@ class InstanceStore {
           }
           }
           this.instancesLoading = false
           this.instancesLoading = false
 
 
-          if (instances.length < this.chunkSize) {
+          if (instances.length < chunkSize) {
             this.backgroundChunksLoading = false
             this.backgroundChunksLoading = false
             return
             return
           }
           }
@@ -199,11 +199,12 @@ class InstanceStore {
 
 
     this.searching = true
     this.searching = true
     this.searchChunksLoading = true
     this.searchChunksLoading = true
+    let chunkSize = this.chunkSize
 
 
     let loadNextChunk = (lastEndpointId?: string) => {
     let loadNextChunk = (lastEndpointId?: string) => {
       InstanceSource.loadInstancesChunk(
       InstanceSource.loadInstancesChunk(
         endpointId,
         endpointId,
-        this.chunkSize,
+        chunkSize,
         lastEndpointId,
         lastEndpointId,
         `${endpointId}-chunk-search`,
         `${endpointId}-chunk-search`,
         searchText
         searchText
@@ -216,7 +217,7 @@ class InstanceStore {
         this.searchedInstances = [...this.searchedInstances, ...instances]
         this.searchedInstances = [...this.searchedInstances, ...instances]
         this.searching = false
         this.searching = false
         this.searchNotFound = Boolean(this.searchedInstances.length === 0)
         this.searchNotFound = Boolean(this.searchedInstances.length === 0)
-        if (instances.length < this.chunkSize) {
+        if (instances.length < chunkSize) {
           this.searchChunksLoading = false
           this.searchChunksLoading = false
         }
         }
         return loadNextChunk(instances[instances.length - 1].id)
         return loadNextChunk(instances[instances.length - 1].id)
@@ -225,22 +226,30 @@ class InstanceStore {
     loadNextChunk()
     loadNextChunk()
   }
   }
 
 
-  @action reloadInstances(endpointId: string) {
+  @action reloadInstances(endpointId: string, chunkSize?: number) {
     this.searchNotFound = false
     this.searchNotFound = false
     this.searchText = ''
     this.searchText = ''
     this.currentPage = 1
     this.currentPage = 1
-    this.loadInstancesInChunks(endpointId, true)
+    this.loadInstancesInChunks(endpointId, chunkSize, true)
   }
   }
 
 
   @action cancelIntancesChunksLoading() {
   @action cancelIntancesChunksLoading() {
     ApiCaller.cancelRequests(`${this.lastEndpointId}-chunk`)
     ApiCaller.cancelRequests(`${this.lastEndpointId}-chunk`)
     this.lastEndpointId = ''
     this.lastEndpointId = ''
+    this.searchNotFound = false
+    this.searchText = ''
+    this.currentPage = 1
   }
   }
 
 
   @action setPage(page: number) {
   @action setPage(page: number) {
     this.currentPage = page
     this.currentPage = page
   }
   }
 
 
+  @action updateChunkSize(chunkSize: number) {
+    this.currentPage = 1
+    this.chunkSize = chunkSize
+  }
+
   @action loadInstancesDetails(endpointId: string, instancesInfo: Instance[], useLocalStorage?: boolean, quietError?: boolean): Promise<void> {
   @action loadInstancesDetails(endpointId: string, instancesInfo: Instance[], useLocalStorage?: boolean, quietError?: boolean): Promise<void> {
     // Use reqId to be able to uniquely identify the request so all but the latest request can be igonred and canceled
     // Use reqId to be able to uniquely identify the request so all but the latest request can be igonred and canceled
     this.reqId = !this.reqId ? 1 : this.reqId + 1
     this.reqId = !this.reqId ? 1 : this.reqId + 1