|
@@ -24,38 +24,50 @@ import FilterList from '../../organisms/FilterList'
|
|
|
import PageHeader from '../../organisms/PageHeader'
|
|
import PageHeader from '../../organisms/PageHeader'
|
|
|
import AlertModal from '../../organisms/AlertModal'
|
|
import AlertModal from '../../organisms/AlertModal'
|
|
|
import MainListItem from '../../molecules/MainListItem'
|
|
import MainListItem from '../../molecules/MainListItem'
|
|
|
|
|
+import Modal from '../../molecules/Modal'
|
|
|
|
|
+import ReplicaExecutionOptions from '../../organisms/ReplicaExecutionOptions'
|
|
|
|
|
+import ReplicaMigrationOptions from '../../organisms/ReplicaMigrationOptions'
|
|
|
|
|
+
|
|
|
import type { MainItem } from '../../../types/MainItem'
|
|
import type { MainItem } from '../../../types/MainItem'
|
|
|
|
|
+import type { Action as DropdownAction } from '../../molecules/ActionDropdown'
|
|
|
|
|
+import type { Field } from '../../../types/Field'
|
|
|
|
|
|
|
|
import replicaItemImage from './images/replica.svg'
|
|
import replicaItemImage from './images/replica.svg'
|
|
|
import replicaLargeImage from './images/replica-large.svg'
|
|
import replicaLargeImage from './images/replica-large.svg'
|
|
|
|
|
|
|
|
import projectStore from '../../../stores/ProjectStore'
|
|
import projectStore from '../../../stores/ProjectStore'
|
|
|
import replicaStore from '../../../stores/ReplicaStore'
|
|
import replicaStore from '../../../stores/ReplicaStore'
|
|
|
|
|
+import migrationStore from '../../../stores/MigrationStore'
|
|
|
import scheduleStore from '../../../stores/ScheduleStore'
|
|
import scheduleStore from '../../../stores/ScheduleStore'
|
|
|
import endpointStore from '../../../stores/EndpointStore'
|
|
import endpointStore from '../../../stores/EndpointStore'
|
|
|
import notificationStore from '../../../stores/NotificationStore'
|
|
import notificationStore from '../../../stores/NotificationStore'
|
|
|
|
|
+
|
|
|
|
|
+import Palette from '../../styleUtils/Palette'
|
|
|
import configLoader from '../../../utils/Config'
|
|
import configLoader from '../../../utils/Config'
|
|
|
|
|
|
|
|
const Wrapper = styled.div``
|
|
const Wrapper = styled.div``
|
|
|
|
|
|
|
|
const SCHEDULE_POLL_TIMEOUT = 10000
|
|
const SCHEDULE_POLL_TIMEOUT = 10000
|
|
|
|
|
|
|
|
-const BulkActions = [
|
|
|
|
|
- { label: 'Execute', value: 'execute' },
|
|
|
|
|
- { label: 'Delete', value: 'delete' },
|
|
|
|
|
-]
|
|
|
|
|
-
|
|
|
|
|
type State = {
|
|
type State = {
|
|
|
- showDeleteReplicaConfirmation: boolean,
|
|
|
|
|
- confirmationItems: ?MainItem[],
|
|
|
|
|
modalIsOpen: boolean,
|
|
modalIsOpen: boolean,
|
|
|
|
|
+ selectedReplicas: MainItem[],
|
|
|
|
|
+ showCancelExecutionModal: boolean,
|
|
|
|
|
+ showExecutionOptionsModal: boolean,
|
|
|
|
|
+ showCreateMigrationsModal: boolean,
|
|
|
|
|
+ showDeleteDisksModal: boolean,
|
|
|
|
|
+ showDeleteReplicasModal: boolean,
|
|
|
}
|
|
}
|
|
|
@observer
|
|
@observer
|
|
|
class ReplicasPage extends React.Component<{ history: any }, State> {
|
|
class ReplicasPage extends React.Component<{ history: any }, State> {
|
|
|
state = {
|
|
state = {
|
|
|
- showDeleteReplicaConfirmation: false,
|
|
|
|
|
- confirmationItems: null,
|
|
|
|
|
modalIsOpen: false,
|
|
modalIsOpen: false,
|
|
|
|
|
+ selectedReplicas: [],
|
|
|
|
|
+ showCancelExecutionModal: false,
|
|
|
|
|
+ showCreateMigrationsModal: false,
|
|
|
|
|
+ showExecutionOptionsModal: false,
|
|
|
|
|
+ showDeleteDisksModal: false,
|
|
|
|
|
+ showDeleteReplicasModal: false,
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
pollTimeout: TimeoutID
|
|
pollTimeout: TimeoutID
|
|
@@ -119,35 +131,71 @@ class ReplicasPage extends React.Component<{ history: any }, State> {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- handleActionChange(items: MainItem[], action: string) {
|
|
|
|
|
- if (action === 'execute') {
|
|
|
|
|
- items.forEach(replica => {
|
|
|
|
|
- replicaStore.execute(replica.id)
|
|
|
|
|
- })
|
|
|
|
|
- notificationStore.alert('Executing replicas')
|
|
|
|
|
- } else if (action === 'delete') {
|
|
|
|
|
- this.setState({
|
|
|
|
|
- showDeleteReplicaConfirmation: true,
|
|
|
|
|
- confirmationItems: items,
|
|
|
|
|
- })
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ executeSelectedReplicas(fields: Field[]) {
|
|
|
|
|
+ this.state.selectedReplicas.forEach(replica => {
|
|
|
|
|
+ let actualReplica = replicaStore.replicas.find(r => r.id === replica.id)
|
|
|
|
|
+ if (actualReplica && this.isExecuteEnabled(actualReplica)) {
|
|
|
|
|
+ replicaStore.execute(replica.id, fields)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ notificationStore.alert('Executing selected replicas')
|
|
|
|
|
+ this.setState({ showExecutionOptionsModal: false })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- handleCloseDeleteReplicaConfirmation() {
|
|
|
|
|
- this.setState({
|
|
|
|
|
- showDeleteReplicaConfirmation: false,
|
|
|
|
|
- confirmationItems: null,
|
|
|
|
|
|
|
+ migrateSelectedReplicas(fields: Field[]) {
|
|
|
|
|
+ notificationStore.alert('Creating migrations from selected replicas')
|
|
|
|
|
+ Promise.all(this.state.selectedReplicas.map(replica => migrationStore.migrateReplica(replica.id, fields))).then(() => {
|
|
|
|
|
+ notificationStore.alert('Migrations successfully created from replicas.', 'success')
|
|
|
|
|
+ this.props.history.push('/migrations')
|
|
|
})
|
|
})
|
|
|
|
|
+ this.setState({ showCreateMigrationsModal: false })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- handleDeleteReplicaConfirmation() {
|
|
|
|
|
- if (!this.state.confirmationItems) {
|
|
|
|
|
- return
|
|
|
|
|
|
|
+ deleteSelectedReplicasDisks() {
|
|
|
|
|
+ this.state.selectedReplicas.forEach(replica => {
|
|
|
|
|
+ replicaStore.deleteDisks(replica.id)
|
|
|
|
|
+ })
|
|
|
|
|
+ this.setState({ showDeleteDisksModal: false })
|
|
|
|
|
+ notificationStore.alert('Deleting selected replicas\' disks')
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ cancelExecutions() {
|
|
|
|
|
+ this.state.selectedReplicas.forEach(replica => {
|
|
|
|
|
+ let actualReplica = replicaStore.replicas.find(r => r.id === replica.id)
|
|
|
|
|
+ let lastExecution = actualReplica && actualReplica.executions[actualReplica.executions.length - 1]
|
|
|
|
|
+ if (actualReplica && lastExecution && lastExecution.status === 'RUNNING') {
|
|
|
|
|
+ replicaStore.cancelExecution(replica.id, lastExecution.id)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ this.setState({ showCancelExecutionModal: false })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ getStatus(replica: ?MainItem): string {
|
|
|
|
|
+ if (!replica) {
|
|
|
|
|
+ return ''
|
|
|
|
|
+ }
|
|
|
|
|
+ let usableReplica = replica
|
|
|
|
|
+ if (usableReplica.executions && usableReplica.executions.length) {
|
|
|
|
|
+ return usableReplica.executions[usableReplica.executions.length - 1].status
|
|
|
}
|
|
}
|
|
|
- this.state.confirmationItems.forEach(replica => {
|
|
|
|
|
|
|
+ return ''
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ isExecuteEnabled(replica: ?MainItem): boolean {
|
|
|
|
|
+ if (!replica) {
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+ let usableReplica = replica
|
|
|
|
|
+ let originEndpoint = endpointStore.endpoints.find(e => e.id === usableReplica.origin_endpoint_id)
|
|
|
|
|
+ let targetEndpoint = endpointStore.endpoints.find(e => e.id === usableReplica.destination_endpoint_id)
|
|
|
|
|
+ return Boolean(originEndpoint && targetEndpoint && this.getStatus(usableReplica) !== 'RUNNING')
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ deleteSelectedReplicas() {
|
|
|
|
|
+ this.state.selectedReplicas.forEach(replica => {
|
|
|
replicaStore.delete(replica.id)
|
|
replicaStore.delete(replica.id)
|
|
|
})
|
|
})
|
|
|
- this.handleCloseDeleteReplicaConfirmation()
|
|
|
|
|
|
|
+ this.setState({ showDeleteReplicasModal: false })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
handleEmptyListButtonClick() {
|
|
handleEmptyListButtonClick() {
|
|
@@ -226,6 +274,35 @@ class ReplicasPage extends React.Component<{ history: any }, State> {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
render() {
|
|
render() {
|
|
|
|
|
+ let atLeastOneHasExecuteEnabled = false
|
|
|
|
|
+ let atLeaseOneIsRunning = false
|
|
|
|
|
+ this.state.selectedReplicas.forEach(replica => {
|
|
|
|
|
+ let storeReplica = replicaStore.replicas.find(r => r.id === replica.id)
|
|
|
|
|
+ atLeastOneHasExecuteEnabled = atLeastOneHasExecuteEnabled || this.isExecuteEnabled(storeReplica)
|
|
|
|
|
+ atLeaseOneIsRunning = atLeaseOneIsRunning || this.getStatus(storeReplica) === 'RUNNING'
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const BulkActions: DropdownAction[] = [{
|
|
|
|
|
+ label: 'Execute',
|
|
|
|
|
+ action: () => { this.setState({ showExecutionOptionsModal: true }) },
|
|
|
|
|
+ disabled: !atLeastOneHasExecuteEnabled,
|
|
|
|
|
+ }, {
|
|
|
|
|
+ label: 'Cancel',
|
|
|
|
|
+ disabled: !atLeaseOneIsRunning,
|
|
|
|
|
+ action: () => { this.setState({ showCancelExecutionModal: true }) },
|
|
|
|
|
+ }, {
|
|
|
|
|
+ label: 'Create Migrations',
|
|
|
|
|
+ color: Palette.primary,
|
|
|
|
|
+ action: () => { this.setState({ showCreateMigrationsModal: true }) },
|
|
|
|
|
+ }, {
|
|
|
|
|
+ label: 'Delete Disks',
|
|
|
|
|
+ action: () => { this.setState({ showDeleteDisksModal: true }) },
|
|
|
|
|
+ }, {
|
|
|
|
|
+ label: 'Delete Replicas',
|
|
|
|
|
+ color: Palette.alert,
|
|
|
|
|
+ action: () => { this.setState({ showDeleteReplicasModal: true }) },
|
|
|
|
|
+ }]
|
|
|
|
|
+
|
|
|
return (
|
|
return (
|
|
|
<Wrapper>
|
|
<Wrapper>
|
|
|
<MainTemplate
|
|
<MainTemplate
|
|
@@ -236,11 +313,11 @@ class ReplicasPage extends React.Component<{ history: any }, State> {
|
|
|
selectionLabel="replica"
|
|
selectionLabel="replica"
|
|
|
loading={replicaStore.loading}
|
|
loading={replicaStore.loading}
|
|
|
items={replicaStore.replicas}
|
|
items={replicaStore.replicas}
|
|
|
|
|
+ dropdownActions={BulkActions}
|
|
|
onItemClick={item => { this.handleItemClick(item) }}
|
|
onItemClick={item => { this.handleItemClick(item) }}
|
|
|
onReloadButtonClick={() => { this.handleReloadButtonClick() }}
|
|
onReloadButtonClick={() => { this.handleReloadButtonClick() }}
|
|
|
- actions={BulkActions}
|
|
|
|
|
- onActionChange={(items, action) => { this.handleActionChange(items, action) }}
|
|
|
|
|
itemFilterFunction={(...args) => this.itemFilterFunction(...args)}
|
|
itemFilterFunction={(...args) => this.itemFilterFunction(...args)}
|
|
|
|
|
+ onSelectedItemsChange={selectedReplicas => { this.setState({ selectedReplicas }) }}
|
|
|
renderItemComponent={options =>
|
|
renderItemComponent={options =>
|
|
|
(<MainListItem
|
|
(<MainListItem
|
|
|
{...options}
|
|
{...options}
|
|
@@ -274,14 +351,60 @@ class ReplicasPage extends React.Component<{ history: any }, State> {
|
|
|
/>
|
|
/>
|
|
|
}
|
|
}
|
|
|
/>
|
|
/>
|
|
|
- <AlertModal
|
|
|
|
|
- isOpen={this.state.showDeleteReplicaConfirmation}
|
|
|
|
|
- title="Delete Replicas?"
|
|
|
|
|
- message="Are you sure you want to delete the selected replicas?"
|
|
|
|
|
- extraMessage="Deleting a Coriolis Replica is permanent!"
|
|
|
|
|
- onConfirmation={() => { this.handleDeleteReplicaConfirmation() }}
|
|
|
|
|
- onRequestClose={() => { this.handleCloseDeleteReplicaConfirmation() }}
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ {this.state.showDeleteReplicasModal ? (
|
|
|
|
|
+ <AlertModal
|
|
|
|
|
+ isOpen
|
|
|
|
|
+ title="Delete Selected Replicas?"
|
|
|
|
|
+ message="Are you sure you want to delete the selected replicas?"
|
|
|
|
|
+ extraMessage="Deleting a Coriolis Replica is permanent!"
|
|
|
|
|
+ onConfirmation={() => { this.deleteSelectedReplicas() }}
|
|
|
|
|
+ onRequestClose={() => { this.setState({ showDeleteReplicasModal: false }) }}
|
|
|
|
|
+ />
|
|
|
|
|
+ ) : null}
|
|
|
|
|
+ {this.state.showCancelExecutionModal ? (
|
|
|
|
|
+ <AlertModal
|
|
|
|
|
+ isOpen
|
|
|
|
|
+ title="Cancel Executions?"
|
|
|
|
|
+ message="Are you sure you want to cancel the selected replicas executions?"
|
|
|
|
|
+ extraMessage=" "
|
|
|
|
|
+ onConfirmation={() => { this.cancelExecutions() }}
|
|
|
|
|
+ onRequestClose={() => { this.setState({ showCancelExecutionModal: false }) }}
|
|
|
|
|
+ />
|
|
|
|
|
+ ) : null}
|
|
|
|
|
+ {this.state.showExecutionOptionsModal ? (
|
|
|
|
|
+ <Modal
|
|
|
|
|
+ isOpen
|
|
|
|
|
+ title="New Executions for Selected Replicas"
|
|
|
|
|
+ onRequestClose={() => { this.setState({ showExecutionOptionsModal: false }) }}
|
|
|
|
|
+ >
|
|
|
|
|
+ <ReplicaExecutionOptions
|
|
|
|
|
+ onCancelClick={() => { this.setState({ showExecutionOptionsModal: false }) }}
|
|
|
|
|
+ onExecuteClick={fields => { this.executeSelectedReplicas(fields) }}
|
|
|
|
|
+ />
|
|
|
|
|
+ </Modal>
|
|
|
|
|
+ ) : null}
|
|
|
|
|
+ {this.state.showCreateMigrationsModal ? (
|
|
|
|
|
+ <Modal
|
|
|
|
|
+ isOpen
|
|
|
|
|
+ title="Create Migrations from Selected Replicas"
|
|
|
|
|
+ onRequestClose={() => { this.setState({ showCreateMigrationsModal: false }) }}
|
|
|
|
|
+ >
|
|
|
|
|
+ <ReplicaMigrationOptions
|
|
|
|
|
+ onCancelClick={() => { this.setState({ showCreateMigrationsModal: false }) }}
|
|
|
|
|
+ onMigrateClick={options => { this.migrateSelectedReplicas(options) }}
|
|
|
|
|
+ />
|
|
|
|
|
+ </Modal>
|
|
|
|
|
+ ) : null}
|
|
|
|
|
+ {this.state.showDeleteDisksModal ? (
|
|
|
|
|
+ <AlertModal
|
|
|
|
|
+ isOpen
|
|
|
|
|
+ title="Delete Selected Replicas Disks?"
|
|
|
|
|
+ message="Are you sure you want to delete the selected replicas' disks?"
|
|
|
|
|
+ extraMessage="Deleting Coriolis Replica Disks is permanent!"
|
|
|
|
|
+ onConfirmation={() => { this.deleteSelectedReplicasDisks() }}
|
|
|
|
|
+ onRequestClose={() => { this.setState({ showDeleteDisksModal: false }) }}
|
|
|
|
|
+ />
|
|
|
|
|
+ ) : null}
|
|
|
</Wrapper>
|
|
</Wrapper>
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|