Sfoglia il codice sorgente

Add support for data restructuring in API lists

The API no longer returns everything (like executions and tasks) for
each item when listing replicas or migrations.

The executions are only loaded when opening a replica.
The tasks are loaded only for the selected execution, or in the case of
a migration, when opening a migration.

Some components relied on execution data being present when listing
replicas and migrations. Those components have been rewritten to use
alternative data available in the lists. Among the affected components,
the important ones are the notifications, the Timeline module inside the
Dashboard page, the replicas and migrations list item renderer.
Sergiu Miclea 5 anni fa
parent
commit
85992d2eb3
56 ha cambiato i file con 797 aggiunte e 639 eliminazioni
  1. 6 2
      src/@types/Execution.ts
  2. 35 11
      src/@types/MainItem.ts
  3. 12 12
      src/components/App.tsx
  4. 1 0
      src/components/atoms/StatusIcon/StatusIcon.tsx
  5. 1 0
      src/components/atoms/StatusIcon/story.tsx
  6. 2 0
      src/components/atoms/StatusPill/StatusPill.tsx
  7. 1 0
      src/components/atoms/StatusPill/story.tsx
  8. 66 27
      src/components/molecules/DeleteReplicaModal/DeleteReplicaModal.tsx
  9. 1 2
      src/components/molecules/DetailsNavigation/DetailsNavigation.tsx
  10. 6 5
      src/components/molecules/MainDetailsTable/MainDetailsTable.tsx
  11. 44 73
      src/components/molecules/MainListItem/MainListItem.tsx
  12. 5 1
      src/components/molecules/MainListItem/story.tsx
  13. 1 2
      src/components/molecules/NotificationDropdown/NotificationDropdown.tsx
  14. 1 1
      src/components/molecules/UserDropdown/UserDropdown.tsx
  15. 5 4
      src/components/organisms/DashboardContent/DashboardContent.tsx
  16. 1 1
      src/components/organisms/DashboardContent/modules/ActivityModule/ActivityModule.tsx
  17. 39 42
      src/components/organisms/DashboardContent/modules/ExecutionsModule/ExecutionsModule.tsx
  18. 5 5
      src/components/organisms/DashboardContent/modules/TopEndpointsModule/TopEndpointsModule.tsx
  19. 12 45
      src/components/organisms/DetailsContentHeader/DetailsContentHeader.tsx
  20. 13 10
      src/components/organisms/EditReplica/EditReplica.tsx
  21. 5 4
      src/components/organisms/EndpointDetailsContent/EndpointDetailsContent.tsx
  22. 55 31
      src/components/organisms/Executions/Executions.tsx
  23. 2 4
      src/components/organisms/FilterList/FilterList.tsx
  24. 6 20
      src/components/organisms/MainDetails/MainDetails.tsx
  25. 3 4
      src/components/organisms/MainList/MainList.tsx
  26. 3 3
      src/components/organisms/MigrationDetailsContent/MigrationDetailsContent.tsx
  27. 1 1
      src/components/organisms/ProjectDetailsContent/ProjectDetailsContent.tsx
  28. 11 5
      src/components/organisms/ReplicaDetailsContent/ReplicaDetailsContent.tsx
  29. 33 3
      src/components/organisms/Tasks/Tasks.tsx
  30. 1 1
      src/components/organisms/Tasks/story.tsx
  31. 1 1
      src/components/organisms/UserDetailsContent/UserDetailsContent.tsx
  32. 3 7
      src/components/pages/AssessmentDetailsPage/AssessmentDetailsPage.tsx
  33. 6 4
      src/components/pages/EndpointDetailsPage/EndpointDetailsPage.tsx
  34. 1 1
      src/components/pages/EndpointsPage/EndpointsPage.tsx
  35. 19 4
      src/components/pages/MigrationDetailsPage/MigrationDetailsPage.tsx
  36. 20 12
      src/components/pages/MigrationsPage/MigrationsPage.tsx
  37. 2 1
      src/components/pages/ProjectDetailsPage/ProjectDetailsPage.tsx
  38. 1 1
      src/components/pages/ProjectsPage/ProjectsPage.tsx
  39. 64 28
      src/components/pages/ReplicaDetailsPage/ReplicaDetailsPage.tsx
  40. 35 39
      src/components/pages/ReplicasPage/ReplicasPage.tsx
  41. 2 1
      src/components/pages/UserDetailsPage/UserDetailsPage.tsx
  42. 1 1
      src/components/pages/UsersPage/UsersPage.tsx
  43. 11 8
      src/components/pages/WizardPage/WizardPage.tsx
  44. 3 3
      src/sources/AssessmentSource.ts
  45. 6 6
      src/sources/MigrationSource.ts
  46. 8 48
      src/sources/NotificationSource.ts
  47. 69 58
      src/sources/ReplicaSource.ts
  48. 6 2
      src/sources/UserSource.ts
  49. 3 3
      src/sources/WizardSource.ts
  50. 2 2
      src/stores/AssessmentStore.ts
  51. 3 3
      src/stores/InstanceStore.ts
  52. 10 16
      src/stores/MigrationStore.ts
  53. 127 62
      src/stores/ReplicaStore.ts
  54. 11 3
      src/stores/UserStore.ts
  55. 4 4
      src/stores/WizardStore.ts
  56. 2 2
      src/utils/DateUtils.ts

+ 6 - 2
src/@types/Execution.ts

@@ -12,7 +12,7 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 */
 
 
-import type { Task } from './Task'
+import { Task } from './Task'
 
 
 export type Execution = {
 export type Execution = {
   id: string,
   id: string,
@@ -20,6 +20,10 @@ export type Execution = {
   status: string,
   status: string,
   created_at: Date,
   created_at: Date,
   updated_at: Date,
   updated_at: Date,
-  tasks: Task[],
+  deleted_at?: Date,
   type: 'replica_execution' | 'replica_disks_delete' | 'replica_deploy' | 'replica_update'
   type: 'replica_execution' | 'replica_disks_delete' | 'replica_deploy' | 'replica_update'
 }
 }
+
+export type ExecutionTasks = Execution & {
+  tasks: Task[]
+}

+ 35 - 11
src/@types/MainItem.ts

@@ -13,10 +13,10 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 */
 
 
 import type { Execution } from './Execution'
 import type { Execution } from './Execution'
-import type { Task } from './Task'
 import type { Instance } from './Instance'
 import type { Instance } from './Instance'
 import type { NetworkMap } from './Network'
 import type { NetworkMap } from './Network'
 import type { StorageMap } from './Endpoint'
 import type { StorageMap } from './Endpoint'
+import { Task } from './Task'
 
 
 export type MainItemInfo = {
 export type MainItemInfo = {
   export_info: {
   export_info: {
@@ -55,26 +55,50 @@ export type StorageMapping = {
     disk_id: string,
     disk_id: string,
   }[] | null,
   }[] | null,
 }
 }
-export type MainItem = {
+
+type BaseItem = {
   id: string,
   id: string,
-  executions: Execution[],
   name: string,
   name: string,
+  description?: string
   notes: string,
   notes: string,
-  status: string,
-  tasks: Task[],
-  created_at: Date,
-  updated_at: Date,
-  replica_id?: string,
+  created_at: string,
+  updated_at: string,
   origin_endpoint_id: string,
   origin_endpoint_id: string,
   destination_endpoint_id: string,
   destination_endpoint_id: string,
   instances: string[],
   instances: string[],
-  type: 'replica' | 'migration',
   info: { [prop: string]: MainItemInfo },
   info: { [prop: string]: MainItemInfo },
   destination_environment: { [prop: string]: any },
   destination_environment: { [prop: string]: any },
   source_environment: { [prop: string]: any },
   source_environment: { [prop: string]: any },
   transfer_result: { [prop: string]: Instance } | null,
   transfer_result: { [prop: string]: Instance } | null,
   replication_count?: number,
   replication_count?: number,
   storage_mappings?: StorageMapping | null,
   storage_mappings?: StorageMapping | null,
-  network_map?: TransferNetworkMap
-  [prop: string]: any
+  network_map?: TransferNetworkMap,
+  last_execution_status: string
+  user_id: string
+}
+
+export type ReplicaItem = BaseItem & {
+  type: 'replica',
 }
 }
+
+export type MigrationItem = BaseItem & {
+  type: 'migration',
+  replica_id?: string,
+}
+
+export type MigrationItemOptions = MigrationItem & {
+  skip_os_morphing: boolean,
+  shutdown_instances: boolean,
+}
+
+export type TransferItem = ReplicaItem | MigrationItem
+
+export type ReplicaItemDetails = ReplicaItem & {
+  executions: Execution[],
+}
+
+export type MigrationItemDetails = MigrationItem & {
+  tasks: Task[]
+}
+
+export type TransferItemDetails = ReplicaItemDetails | MigrationItemDetails

+ 12 - 12
src/components/App.tsx

@@ -177,21 +177,21 @@ class App extends React.Component<{}, State> {
             {renderRoute('/', DashboardPage, true)}
             {renderRoute('/', DashboardPage, true)}
             <Route path="/login" component={LoginPage} />
             <Route path="/login" component={LoginPage} />
             {renderRoute('/dashboard', DashboardPage)}
             {renderRoute('/dashboard', DashboardPage)}
-            {renderRoute('/replicas', ReplicasPage)}
-            {renderRoute('/replica/:id', ReplicaDetailsPage, true)}
-            {renderRoute('/replica/:page/:id', ReplicaDetailsPage)}
-            {renderRoute('/migrations', MigrationsPage)}
-            {renderRoute('/migration/:id', MigrationDetailsPage, true)}
-            {renderRoute('/migration/:page/:id', MigrationDetailsPage)}
-            {renderRoute('/endpoints', EndpointsPage)}
-            {renderRoute('/endpoint/:id', EndpointDetailsPage)}
+            {renderRoute('/replicas', ReplicasPage, true)}
+            {renderRoute('/replicas/:id', ReplicaDetailsPage, true)}
+            {renderRoute('/replicas/:id/:page', ReplicaDetailsPage)}
+            {renderRoute('/migrations', MigrationsPage, true)}
+            {renderRoute('/migrations/:id', MigrationDetailsPage, true)}
+            {renderRoute('/migrations/:id/:page', MigrationDetailsPage)}
+            {renderRoute('/endpoints', EndpointsPage, true)}
+            {renderRoute('/endpoints/:id', EndpointDetailsPage)}
             {renderRoute('/wizard/:type', WizardPage)}
             {renderRoute('/wizard/:type', WizardPage)}
             {renderOptionalRoute('planning', AssessmentsPage)}
             {renderOptionalRoute('planning', AssessmentsPage)}
             {renderOptionalRoute('planning', AssessmentDetailsPage, '/assessment/:info')}
             {renderOptionalRoute('planning', AssessmentDetailsPage, '/assessment/:info')}
-            {renderOptionalRoute('users', UsersPage)}
-            {renderOptionalRoute('users', UserDetailsPage, '/user/:id', true)}
-            {renderOptionalRoute('projects', ProjectsPage)}
-            {renderOptionalRoute('projects', ProjectDetailsPage, '/project/:id', true)}
+            {renderOptionalRoute('users', UsersPage, undefined, true)}
+            {renderOptionalRoute('users', UserDetailsPage, '/users/:id')}
+            {renderOptionalRoute('projects', ProjectsPage, undefined, true)}
+            {renderOptionalRoute('projects', ProjectDetailsPage, '/projects/:id')}
             {renderOptionalRoute('logging', LogsPage)}
             {renderOptionalRoute('logging', LogsPage)}
             {renderRoute('/streamlog', LogStreamPage)}
             {renderRoute('/streamlog', LogStreamPage)}
             <Route component={MessagePage} />
             <Route component={MessagePage} />

+ 1 - 0
src/components/atoms/StatusIcon/StatusIcon.tsx

@@ -90,6 +90,7 @@ const statuses = (status: any, props: any) => {
         background-image: ${getWarningUrl('#424242')};
         background-image: ${getWarningUrl('#424242')};
       `
       `
     case 'UNSCHEDULED':
     case 'UNSCHEDULED':
+    case 'UNEXECUTED':
       return css`
       return css`
         background-image: ${getWarningUrl(Palette.grayscale[2])};
         background-image: ${getWarningUrl(Palette.grayscale[2])};
       `
       `

+ 1 - 0
src/components/atoms/StatusIcon/story.tsx

@@ -23,6 +23,7 @@ const Wrapper = styled.div<any>`
 `
 `
 
 
 const STATUSES = [
 const STATUSES = [
+  'UNEXECUTED',
   'SCHEDULED',
   'SCHEDULED',
   'UNSCHEDULED',
   'UNSCHEDULED',
   'COMPLETED',
   'COMPLETED',

+ 2 - 0
src/components/atoms/StatusPill/StatusPill.tsx

@@ -100,6 +100,7 @@ const statuses = (status: any) => {
       `
       `
     case 'INFO':
     case 'INFO':
     case 'SCHEDULED':
     case 'SCHEDULED':
+    case 'UNEXECUTED':
       return null
       return null
     default:
     default:
       return null
       return null
@@ -143,6 +144,7 @@ const Wrapper = styled.div<any>`
   border-radius: 4px;
   border-radius: 4px;
   ${(props: any) => statuses(props.status)}
   ${(props: any) => statuses(props.status)}
   ${(props: any) => (props.status === 'INFO' ? getInfoStatusColor(props) : '')}
   ${(props: any) => (props.status === 'INFO' ? getInfoStatusColor(props) : '')}
+  text-transform: uppercase;
 `
 `
 
 
 type Props = {
 type Props = {

+ 1 - 0
src/components/atoms/StatusPill/story.tsx

@@ -22,6 +22,7 @@ const Wrapper = styled.div<any>`
   flex-wrap: wrap;
   flex-wrap: wrap;
 `
 `
 const STATUSES = [
 const STATUSES = [
+  'UNEXECUTED',
   'SCHEDULED',
   'SCHEDULED',
   'UNSCHEDULED',
   'UNSCHEDULED',
   'COMPLETED',
   'COMPLETED',

+ 66 - 27
src/components/molecules/DeleteReplicaModal/DeleteReplicaModal.tsx

@@ -50,10 +50,30 @@ const ButtonsColumn = styled.div<any>`
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
 `
 `
+const Loading = styled.div`
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  margin: 64px;
+`
+const LoadingMessage = styled.div`
+  max-width: 100%;
+  overflow: auto;
+  margin-top: 48px;
+  text-align: center;
+`
+const LoadingTitle = styled.div`
+  font-size: 18px;
+  margin-bottom: 8px;
+`
+const LoadingSubtitle = styled.div<any>`
+  color: ${Palette.grayscale[4]};
+`
 
 
 type Props = {
 type Props = {
   hasDisks: boolean,
   hasDisks: boolean,
   isMultiReplicaSelection?: boolean,
   isMultiReplicaSelection?: boolean,
+  loading?: boolean
   onDeleteReplica: () => void,
   onDeleteReplica: () => void,
   onDeleteDisks: () => void,
   onDeleteDisks: () => void,
   onRequestClose: () => void,
   onRequestClose: () => void,
@@ -93,41 +113,60 @@ class DeleteReplicaModal extends React.Component<Props> {
     )
     )
   }
   }
 
 
+  renderLoading() {
+    return (
+      <Loading>
+        <StatusImage loading />
+        <LoadingMessage>
+          <LoadingTitle>Validating Replicas Details</LoadingTitle>
+          <LoadingSubtitle>Please wait ...</LoadingSubtitle>
+        </LoadingMessage>
+      </Loading>
+    )
+  }
+
+  renderContent() {
+    const message = this.props.isMultiReplicaSelection ? 'Are you sure you want to delete the selected replicas?' : 'Are you sure you want to delete this replica?'
+
+    return (
+      <Wrapper>
+        <StatusImage status="QUESTION" />
+        <Message>{message}</Message>
+        { this.renderExtraMessage() }
+        <Buttons>
+          <Button secondary onClick={this.props.onRequestClose}>Cancel</Button>
+          <ButtonsColumn>
+            {this.props.hasDisks ? (
+              <Button
+                onClick={this.props.onDeleteDisks}
+                hollow
+                style={{ marginBottom: '16px' }}
+                alert
+              >
+                Delete Replica Disks
+              </Button>
+            ) : null}
+            <Button
+              onClick={this.props.onDeleteReplica}
+              alert
+            >
+              Delete Replica{this.props.isMultiReplicaSelection ? 's' : ''}
+            </Button>
+          </ButtonsColumn>
+        </Buttons>
+      </Wrapper>
+    )
+  }
+
   render() {
   render() {
     const title = this.props.isMultiReplicaSelection ? 'Delete Selected Replicas?' : 'Delete Replica?'
     const title = this.props.isMultiReplicaSelection ? 'Delete Selected Replicas?' : 'Delete Replica?'
-    const message = this.props.isMultiReplicaSelection ? 'Are you sure you want to delete the selected replicas?' : 'Are you sure you want to delete this replica?'
     return (
     return (
       <Modal
       <Modal
         isOpen
         isOpen
         title={title}
         title={title}
         onRequestClose={this.props.onRequestClose}
         onRequestClose={this.props.onRequestClose}
       >
       >
-        <Wrapper>
-          <StatusImage status="QUESTION" />
-          <Message>{message}</Message>
-          {this.renderExtraMessage()}
-          <Buttons>
-            <Button secondary onClick={this.props.onRequestClose}>Cancel</Button>
-            <ButtonsColumn>
-              {this.props.hasDisks ? (
-                <Button
-                  onClick={this.props.onDeleteDisks}
-                  hollow
-                  style={{ marginBottom: '16px' }}
-                  alert
-                >
-                  Delete Replica Disks
-                </Button>
-              ) : null}
-              <Button
-                onClick={this.props.onDeleteReplica}
-                alert
-              >
-                Delete Replica{this.props.isMultiReplicaSelection ? 's' : ''}
-              </Button>
-            </ButtonsColumn>
-          </Buttons>
-        </Wrapper>
+        {this.props.loading ? this.renderLoading() : this.renderContent()}
       </Modal>
       </Modal>
     )
     )
   }
   }

+ 1 - 2
src/components/molecules/DetailsNavigation/DetailsNavigation.tsx

@@ -46,10 +46,9 @@ class DetailsNavigation extends React.Component<Props> {
     return (
     return (
       this.props.items.map(item => (
       this.props.items.map(item => (
         <Item
         <Item
-          data-test-id={`detailsNavigation-${item.value}`}
           selected={item.value === this.props.selectedValue}
           selected={item.value === this.props.selectedValue}
           key={item.value || item.label}
           key={item.value || item.label}
-          to={this.props.customHref ? this.props.customHref(item) : `/${this.props.itemType || ''}${(item.value && '/') || ''}${item.value}/${this.props.itemId || ''}`}
+          to={this.props.customHref ? this.props.customHref(item) : `/${this.props.itemType || ''}s/${this.props.itemId || ''}${(item.value && '/') || ''}${item.value}`}
         >{item.label}
         >{item.label}
         </Item>
         </Item>
       ))
       ))

+ 6 - 5
src/components/molecules/MainDetailsTable/MainDetailsTable.tsx

@@ -22,7 +22,8 @@ import Palette from '../../styleUtils/Palette'
 import StyleProps from '../../styleUtils/StyleProps'
 import StyleProps from '../../styleUtils/StyleProps'
 
 
 import {
 import {
-  MainItem, TransferNetworkMap, isNetworkMapSecurityGroups, isNetworkMapSourceDest,
+  TransferNetworkMap, isNetworkMapSecurityGroups,
+  isNetworkMapSourceDest, TransferItem,
 } from '../../../@types/MainItem'
 } from '../../../@types/MainItem'
 import type { Instance, Nic, Disk } from '../../../@types/Instance'
 import type { Instance, Nic, Disk } from '../../../@types/Instance'
 import type { Network } from '../../../@types/Network'
 import type { Network } from '../../../@types/Network'
@@ -147,7 +148,7 @@ const ArrowIcon = styled.div<any>`
 export const TEST_ID = 'mainDetailsTable'
 export const TEST_ID = 'mainDetailsTable'
 
 
 export type Props = {
 export type Props = {
-  item?: MainItem | null,
+  item?: TransferItem | null,
   instancesDetails: Instance[],
   instancesDetails: Instance[],
   networks?: Network[],
   networks?: Network[],
 }
 }
@@ -279,7 +280,7 @@ class MainDetailsTable extends React.Component<Props, State> {
           destinationKey = destinationName as string
           destinationKey = destinationName as string
           destinationBody = getBody(transferDisk)
           destinationBody = getBody(transferDisk)
         }
         }
-      } else if (this.props.item && this.props.item.status === 'RUNNING' && this.props.item.type === 'migration') {
+      } else if (this.props.item?.type === 'migration' && this.props.item.last_execution_status === 'RUNNING') {
         destinationBody = ['Waiting for migration to finish']
         destinationBody = ['Waiting for migration to finish']
       }
       }
 
 
@@ -354,7 +355,7 @@ class MainDetailsTable extends React.Component<Props, State> {
             destinationNetworkName = destinationNic.network_name
             destinationNetworkName = destinationNic.network_name
             destinationBody = getBody(destinationNic)
             destinationBody = getBody(destinationNic)
           }
           }
-        } else if (this.props.item && this.props.item.status === 'RUNNING' && this.props.item.type === 'migration') {
+        } else if (this.props.item?.type === 'migration' && this.props.item.last_execution_status === 'RUNNING') {
           destinationBody = ['Waiting for migration to finish']
           destinationBody = ['Waiting for migration to finish']
         }
         }
 
 
@@ -387,7 +388,7 @@ class MainDetailsTable extends React.Component<Props, State> {
     if (transferResult) {
     if (transferResult) {
       destinationName = transferResult.instance_name || transferResult.name
       destinationName = transferResult.instance_name || transferResult.name
       destinationBody = getBody(transferResult)
       destinationBody = getBody(transferResult)
-    } else if (this.props.item && this.props.item.status === 'RUNNING' && this.props.item.type === 'migration') {
+    } else if (this.props.item?.type === 'migration' && this.props.item.last_execution_status === 'RUNNING') {
       destinationName = 'Waiting for migration to finish'
       destinationName = 'Waiting for migration to finish'
     }
     }
     const instanceName = instance.instance_name || instance.name
     const instanceName = instance.instance_name || instance.name

+ 44 - 73
src/components/molecules/MainListItem/MainListItem.tsx

@@ -21,12 +21,11 @@ import StatusPill from '../../atoms/StatusPill'
 import EndpointLogos from '../../atoms/EndpointLogos'
 import EndpointLogos from '../../atoms/EndpointLogos'
 import Palette from '../../styleUtils/Palette'
 import Palette from '../../styleUtils/Palette'
 import StyleProps from '../../styleUtils/StyleProps'
 import StyleProps from '../../styleUtils/StyleProps'
-import DateUtils from '../../../utils/DateUtils'
-import type { MainItem } from '../../../@types/MainItem'
-import type { Execution } from '../../../@types/Execution'
+import type { TransferItem } from '../../../@types/MainItem'
 
 
 import arrowImage from './images/arrow.svg'
 import arrowImage from './images/arrow.svg'
 import scheduleImage from './images/schedule.svg'
 import scheduleImage from './images/schedule.svg'
+import DateUtils from '../../../utils/DateUtils'
 
 
 const CheckboxStyled = styled(Checkbox)`
 const CheckboxStyled = styled(Checkbox)`
   opacity: ${props => (props.checked ? 1 : 0)};
   opacity: ${props => (props.checked ? 1 : 0)};
@@ -97,97 +96,74 @@ const EndpointImageArrow = styled.div<any>`
   margin: 0 16px;
   margin: 0 16px;
   background: url('${arrowImage}') center no-repeat;
   background: url('${arrowImage}') center no-repeat;
 `
 `
-const LastExecution = styled.div<any>`
-  min-width: 175px;
-  margin-right: 25px;
-`
 const ItemLabel = styled.div<any>`
 const ItemLabel = styled.div<any>`
   color: ${Palette.grayscale[4]};
   color: ${Palette.grayscale[4]};
 `
 `
 const ItemValue = styled.div<any>`
 const ItemValue = styled.div<any>`
   color: ${Palette.primary};
   color: ${Palette.primary};
 `
 `
-
-const TasksRemaining = styled.div<any>`
-  min-width: 114px;
+const Column = styled.div`
+  align-self: start;
 `
 `
 
 
 type Props = {
 type Props = {
-  item: MainItem,
+  item: TransferItem,
   onClick: () => void,
   onClick: () => void,
   selected: boolean,
   selected: boolean,
-  useTasksRemaining?: boolean,
   image: string,
   image: string,
   showScheduleIcon?: boolean,
   showScheduleIcon?: boolean,
   endpointType: (endpointId: string) => string,
   endpointType: (endpointId: string) => string,
+  getUserName: (userId: string) => string | undefined,
+  userNameLoading: boolean,
   onSelectedChange: (value: boolean) => void,
   onSelectedChange: (value: boolean) => void,
 }
 }
 @observer
 @observer
 class MainListItem extends React.Component<Props> {
 class MainListItem extends React.Component<Props> {
-  getLastExecution(): Execution | MainItem | null | undefined {
-    if (this.props.item.executions && this.props.item.executions.length) {
-      return this.props.item.executions[this.props.item.executions.length - 1]
-    }
-
-    if (typeof this.props.item.executions === 'undefined') {
-      return this.props.item
-    }
-
-    return null
-  }
-
   getStatus() {
   getStatus() {
-    const lastExecution = this.getLastExecution()
-    if (lastExecution) {
-      return lastExecution.status
-    }
-
-    return null
+    return this.props.item.last_execution_status
   }
   }
 
 
-  getTasksRemaining() {
-    const lastExecution = this.getLastExecution()
-
-    if (!lastExecution || !lastExecution.tasks || lastExecution.tasks.length === 0) {
-      return '-'
-    }
-
-    const unfinished = lastExecution.tasks.filter(task => task.status !== 'COMPLETED').length
-
-    if (unfinished === 0) {
-      return '-'
-    }
-
-    const total = lastExecution.tasks.length
-
-    return `${unfinished} of ${total}`
+  renderCreationDate() {
+    return (
+      <Column style={{ minWidth: '170px', maxWidth: '170px', marginRight: '25px' }}>
+        <ItemLabel>
+          Created
+        </ItemLabel>
+        <ItemValue>
+          {DateUtils.getLocalTime(this.props.item.created_at).format('DD MMMM YYYY, HH:mm')}
+        </ItemValue>
+      </Column>
+    )
   }
   }
 
 
-  getTotalExecutions() {
-    return (this.props.item.executions && this.props.item.executions.length) || '-'
+  renderUpdateDate() {
+    return (
+      <Column style={{ minWidth: '170px', maxWidth: '170px', marginRight: '25px' }}>
+        <ItemLabel>
+          Updated
+        </ItemLabel>
+        <ItemValue>
+          {this.props.item.updated_at ? DateUtils.getLocalTime(this.props.item.updated_at).format('DD MMMM YYYY, HH:mm') : '-'}
+        </ItemValue>
+      </Column>
+    )
   }
   }
 
 
-  renderLastExecution() {
-    const lastExecution = this.getLastExecution()
-    let label = 'Last Execution'
-    let time = '-'
-
-    if (this.props.item.executions === undefined) {
-      label = 'Created'
-      time = DateUtils.getLocalTime(lastExecution && lastExecution.created_at).format('DD MMMM YYYY, HH:mm')
-    } else if (lastExecution && (lastExecution.created_at || lastExecution.updated_at)) {
-      time = DateUtils.getLocalTime(lastExecution.updated_at || lastExecution.created_at).format('DD MMMM YYYY, HH:mm')
-    }
-
+  renderUser() {
     return (
     return (
-      <LastExecution>
+      <Column style={{ minWidth: '115px', maxWidth: '115px' }}>
         <ItemLabel>
         <ItemLabel>
-          {label}
+          User
         </ItemLabel>
         </ItemLabel>
-        <ItemValue>
-          {time}
+        <ItemValue
+          style={{
+            textOverflow: 'ellipsis',
+            overflow: 'hidden',
+          }}
+        >
+          {this.props.userNameLoading ? 'Loading...' : (this.props.getUserName(this.props.item.user_id) || this.props.item.user_id)}
         </ItemValue>
         </ItemValue>
-      </LastExecution>
+      </Column>
     )
     )
   }
   }
 
 
@@ -235,14 +211,9 @@ class MainListItem extends React.Component<Props> {
             </StatusWrapper>
             </StatusWrapper>
           </Title>
           </Title>
           {endpointImages}
           {endpointImages}
-          {this.renderLastExecution()}
-          <TasksRemaining>
-            <ItemLabel>{this.props.useTasksRemaining ? 'Tasks Remaining' : 'Total Executions'}</ItemLabel>
-            <ItemValue>{
-              this.props.useTasksRemaining ? this.getTasksRemaining() : this.getTotalExecutions()
-}
-            </ItemValue>
-          </TasksRemaining>
+          {this.renderCreationDate()}
+          {this.renderUpdateDate()}
+          {this.renderUser()}
         </Content>
         </Content>
       </Wrapper>
       </Wrapper>
     )
     )

+ 5 - 1
src/components/molecules/MainListItem/story.tsx

@@ -38,7 +38,9 @@ storiesOf('MainListItem', module)
       selected={false}
       selected={false}
       image="image"
       image="image"
       onSelectedChange={() => {}}
       onSelectedChange={() => {}}
-      onClick={() => {}}
+      onClick={() => { }}
+      getUserName={id => id}
+      userNameLoading={false}
     />
     />
   ))
   ))
   .add('running', () => (
   .add('running', () => (
@@ -49,5 +51,7 @@ storiesOf('MainListItem', module)
       image="image"
       image="image"
       onSelectedChange={() => { }}
       onSelectedChange={() => { }}
       onClick={() => { }}
       onClick={() => { }}
+      getUserName={id => id}
+      userNameLoading={false}
     />
     />
   ))
   ))

+ 1 - 2
src/components/molecules/NotificationDropdown/NotificationDropdown.tsx

@@ -264,8 +264,7 @@ class NotificationDropdown extends React.Component<Props, State> {
               onMouseDown={() => { this.itemMouseDown = true }}
               onMouseDown={() => { this.itemMouseDown = true }}
               onMouseUp={() => { this.itemMouseDown = false }}
               onMouseUp={() => { this.itemMouseDown = false }}
               onClick={() => { this.handleItemClick() }}
               onClick={() => { this.handleItemClick() }}
-              to={`/${item.type}${executionsHref}/${item.id}`}
-              data-test-id={`${testId}-${item.id}-item`}
+              to={`/${item.type}s/${item.id}/${executionsHref}`}
             >
             >
               <InfoColumn>
               <InfoColumn>
                 <MainItemInfo>
                 <MainItemInfo>

+ 1 - 1
src/components/molecules/UserDropdown/UserDropdown.tsx

@@ -163,7 +163,7 @@ class UserDropdown extends React.Component<Props, State> {
     const isAdmin = this.props.user.isAdmin
     const isAdmin = this.props.user.isAdmin
     if (isAdmin && navigationMenu.find(m => m.value === 'users'
     if (isAdmin && navigationMenu.find(m => m.value === 'users'
       && !configLoader.config.disabledPages.find(p => p === 'users') && (!m.requiresAdmin || isAdmin))) {
       && !configLoader.config.disabledPages.find(p => p === 'users') && (!m.requiresAdmin || isAdmin))) {
-      href = `/user/${this.props.user.id}`
+      href = `/users/${this.props.user.id}`
     }
     }
 
 
     return (
     return (

+ 5 - 4
src/components/organisms/DashboardContent/DashboardContent.tsx

@@ -25,12 +25,12 @@ import ExecutionsModule from './modules/ExecutionsModule'
 
 
 import Palette from '../../styleUtils/Palette'
 import Palette from '../../styleUtils/Palette'
 
 
-import type { MainItem } from '../../../@types/MainItem'
 import type { Endpoint } from '../../../@types/Endpoint'
 import type { Endpoint } from '../../../@types/Endpoint'
 import type { Project } from '../../../@types/Project'
 import type { Project } from '../../../@types/Project'
 import type { User } from '../../../@types/User'
 import type { User } from '../../../@types/User'
 import type { Licence } from '../../../@types/Licence'
 import type { Licence } from '../../../@types/Licence'
 import type { NotificationItemData } from '../../../@types/NotificationItem'
 import type { NotificationItemData } from '../../../@types/NotificationItem'
+import { ReplicaItem, MigrationItem } from '../../../@types/MainItem'
 
 
 const MIDDLE_WIDTHS = ['264px', '264px', '264px']
 const MIDDLE_WIDTHS = ['264px', '264px', '264px']
 
 
@@ -52,8 +52,8 @@ const MiddleMobileLayout = styled.div<any>`
 `
 `
 
 
 type Props = {
 type Props = {
-  replicas: MainItem[],
-  migrations: MainItem[],
+  replicas: ReplicaItem[],
+  migrations: MigrationItem[],
   endpoints: Endpoint[],
   endpoints: Endpoint[],
   projects: Project[],
   projects: Project[],
   replicasLoading: boolean,
   replicasLoading: boolean,
@@ -211,7 +211,8 @@ class DashboardContent extends React.Component<Props, State> {
         {this.renderMiddleModules()}
         {this.renderMiddleModules()}
         <ExecutionsModule
         <ExecutionsModule
           replicas={this.props.replicas}
           replicas={this.props.replicas}
-          loading={this.props.replicasLoading}
+          migrations={this.props.migrations}
+          loading={this.props.replicasLoading || this.props.migrationsLoading}
         />
         />
       </Wrapper>
       </Wrapper>
     )
     )

+ 1 - 1
src/components/organisms/DashboardContent/modules/ActivityModule/ActivityModule.tsx

@@ -106,7 +106,7 @@ class ActivityModule extends React.Component<Props> {
             return (
             return (
               <ListItem
               <ListItem
                 key={item.id}
                 key={item.id}
-                to={`/${item.type}${executionsHref}/${item.id}`}
+                to={`/${item.type}s/${item.id}/${executionsHref}`}
                 style={{
                 style={{
                   width: `calc(${this.props.large ? 50 : 100}% - 32px)`,
                   width: `calc(${this.props.large ? 50 : 100}% - 32px)`,
                   paddingTop: (i === 0 || i === 5) ? '16px' : '8px',
                   paddingTop: (i === 0 || i === 5) ? '16px' : '8px',

+ 39 - 42
src/components/organisms/DashboardContent/modules/ExecutionsModule/ExecutionsModule.tsx

@@ -24,10 +24,8 @@ import BarChart from '../../charts/BarChart'
 import Palette from '../../../../styleUtils/Palette'
 import Palette from '../../../../styleUtils/Palette'
 import StyleProps from '../../../../styleUtils/StyleProps'
 import StyleProps from '../../../../styleUtils/StyleProps'
 
 
-import type { MainItem } from '../../../../../@types/MainItem'
-import type { Execution } from '../../../../../@types/Execution'
-
 import emptyBackgroundImage from './images/empty-background.svg'
 import emptyBackgroundImage from './images/empty-background.svg'
+import { ReplicaItem, MigrationItem, TransferItem } from '../../../../../@types/MainItem'
 
 
 const INTERVALS = [
 const INTERVALS = [
   { label: 'Last {x} days', value: '30-days' },
   { label: 'Last {x} days', value: '30-days' },
@@ -132,7 +130,8 @@ const EmptyBackgroundImage = styled.div<any>`
 
 
 type Props = {
 type Props = {
   // eslint-disable-next-line react/no-unused-prop-types
   // eslint-disable-next-line react/no-unused-prop-types
-  replicas: MainItem[],
+  replicas: ReplicaItem[],
+  migrations: MigrationItem[],
   loading: boolean,
   loading: boolean,
 }
 }
 type GroupedData = {
 type GroupedData = {
@@ -142,8 +141,8 @@ type GroupedData = {
 }
 }
 type TooltipData = {
 type TooltipData = {
   title: string,
   title: string,
-  success: number,
-  failed: number,
+  migrations: number,
+  replicas: number,
 }
 }
 type State = {
 type State = {
   selectedPeriod: string,
   selectedPeriod: string,
@@ -151,7 +150,7 @@ type State = {
   tooltipPosition: { x: number, y: number },
   tooltipPosition: { x: number, y: number },
   tooltipData: TooltipData | null,
   tooltipData: TooltipData | null,
 }
 }
-const COLORS = ['#0044CA', '#2D74FF']
+const COLORS = ['#F91661', '#0044CB']
 
 
 @observer
 @observer
 class ExecutionsModule extends React.Component<Props, State> {
 class ExecutionsModule extends React.Component<Props, State> {
@@ -162,54 +161,52 @@ class ExecutionsModule extends React.Component<Props, State> {
     tooltipPosition: { x: 0, y: 0 },
     tooltipPosition: { x: 0, y: 0 },
   }
   }
 
 
-  UNSAFE_componentWillMount() {
-    this.groupExecutions(this.props)
+  componentDidMount() {
+    this.groupCreations(this.props)
   }
   }
 
 
   UNSAFE_componentWillReceiveProps(props: Props) {
   UNSAFE_componentWillReceiveProps(props: Props) {
-    this.groupExecutions(props)
+    this.groupCreations(props)
   }
   }
 
 
-  groupExecutions(props: Props) {
-    let executions: Execution[] = []
-    const replicas = props.replicas
-    replicas.forEach(replica => {
-      executions = [...executions, ...replica.executions]
-    })
+  groupCreations(props: Props) {
+    let creations: TransferItem[] = [...props.replicas, ...props.migrations]
+
     const periodUnit: any = this.state.selectedPeriod.split('-')[1]
     const periodUnit: any = this.state.selectedPeriod.split('-')[1]
     const periodValue: any = Number(this.state.selectedPeriod.split('-')[0])
     const periodValue: any = Number(this.state.selectedPeriod.split('-')[0])
     const oldestDate: Date = moment().subtract(periodValue, periodUnit).toDate()
     const oldestDate: Date = moment().subtract(periodValue, periodUnit).toDate()
-    executions = executions
-      .filter(e => new Date(e.updated_at || e.created_at).getTime() >= oldestDate.getTime())
-    executions.sort((a, b) => new Date(a.updated_at || a.created_at).getTime()
-      - new Date(b.updated_at || b.created_at).getTime())
-    this.groupByPeriod(executions, periodUnit)
+    creations = creations
+      .filter(e => new Date(e.created_at).getTime() >= oldestDate.getTime())
+    creations.sort((a, b) => new Date(a.created_at).getTime()
+      - new Date(b.created_at).getTime())
+
+    this.groupByPeriod(creations, periodUnit)
   }
   }
 
 
-  groupByPeriod(executions: Execution[], periodUnit: string) {
+  groupByPeriod(transferItems: TransferItem[], periodUnit: string) {
     const groupedData: GroupedData[] = []
     const groupedData: GroupedData[] = []
-    const periods: { [period: string]: { success: number, failed: number } } = {}
-    executions.forEach(e => {
-      const date = moment(new Date(e.updated_at || e.created_at))
+    const periods: { [period: string]: { replicas: number, migrations: number } } = {}
+    transferItems.forEach(item => {
+      const date = moment(new Date(item.created_at))
       const period: string = periodUnit === 'days' ? date.format('DD-MMM-YYYY_DD MMMM') : date.format('MMM-YYYY_MMMM YYYY')
       const period: string = periodUnit === 'days' ? date.format('DD-MMM-YYYY_DD MMMM') : date.format('MMM-YYYY_MMMM YYYY')
       if (!periods[period]) {
       if (!periods[period]) {
-        periods[period] = { success: 0, failed: 0 }
+        periods[period] = { replicas: 0, migrations: 0 }
       }
       }
-      if (e.status === 'COMPLETED') {
-        periods[period].success += 1
-      } else if (e.status === 'ERROR') {
-        periods[period].failed += 1
+      if (item.type === 'replica') {
+        periods[period].replicas += 1
+      } else if (item.type === 'migration') {
+        periods[period].migrations += 1
       }
       }
     })
     })
     Object.keys(periods).forEach(period => {
     Object.keys(periods).forEach(period => {
-      if (!periods[period].success && !periods[period].failed) {
+      if (!periods[period].replicas && !periods[period].migrations) {
         return
         return
       }
       }
       const label = period.split('_')[0]
       const label = period.split('_')[0]
       const title = period.split('_')[1]
       const title = period.split('_')[1]
       groupedData.push({
       groupedData.push({
         label: periodUnit === 'days' ? `${label.split('-')[0]} ${label.split('-')[1]}` : label.split('-')[0],
         label: periodUnit === 'days' ? `${label.split('-')[0]} ${label.split('-')[1]}` : label.split('-')[0],
-        values: [periods[period].failed, periods[period].success],
+        values: [periods[period].migrations, periods[period].replicas],
         data: title,
         data: title,
       })
       })
     })
     })
@@ -218,7 +215,7 @@ class ExecutionsModule extends React.Component<Props, State> {
 
 
   handleDropdownChange(selectedPeriod: string) {
   handleDropdownChange(selectedPeriod: string) {
     this.setState({ selectedPeriod }, () => {
     this.setState({ selectedPeriod }, () => {
-      this.groupExecutions(this.props)
+      this.groupCreations(this.props)
     })
     })
   }
   }
 
 
@@ -226,8 +223,8 @@ class ExecutionsModule extends React.Component<Props, State> {
     this.setState({
     this.setState({
       tooltipPosition: { x: position.x - 86, y: position.y },
       tooltipPosition: { x: position.x - 86, y: position.y },
       tooltipData: {
       tooltipData: {
-        failed: item.values[0],
-        success: item.values[1],
+        replicas: item.values[1],
+        migrations: item.values[0],
         title: item.data || '-',
         title: item.data || '-',
       },
       },
     })
     })
@@ -264,16 +261,16 @@ class ExecutionsModule extends React.Component<Props, State> {
         <TooltipHeader>{data.title}</TooltipHeader>
         <TooltipHeader>{data.title}</TooltipHeader>
         <TooltipBody>
         <TooltipBody>
           <TooltipRow>
           <TooltipRow>
-            <TooltipRowLabel>Total Executions</TooltipRowLabel>
-            <TooltipRowLabel>{data.success + data.failed}</TooltipRowLabel>
+            <TooltipRowLabel>Created</TooltipRowLabel>
+            <TooltipRowLabel>{data.replicas + data.migrations}</TooltipRowLabel>
           </TooltipRow>
           </TooltipRow>
           <TooltipRow>
           <TooltipRow>
-            <TooltipRowLabel>Successful</TooltipRowLabel>
-            <TooltipRowLabel>{data.success}</TooltipRowLabel>
+            <TooltipRowLabel>Replicas</TooltipRowLabel>
+            <TooltipRowLabel>{data.replicas}</TooltipRowLabel>
           </TooltipRow>
           </TooltipRow>
           <TooltipRow>
           <TooltipRow>
-            <TooltipRowLabel>Failed</TooltipRowLabel>
-            <TooltipRowLabel>{data.failed}</TooltipRowLabel>
+            <TooltipRowLabel>Migrations</TooltipRowLabel>
+            <TooltipRowLabel>{data.migrations}</TooltipRowLabel>
           </TooltipRow>
           </TooltipRow>
         </TooltipBody>
         </TooltipBody>
         <TooltipTip />
         <TooltipTip />
@@ -326,7 +323,7 @@ class ExecutionsModule extends React.Component<Props, State> {
   render() {
   render() {
     return (
     return (
       <Wrapper>
       <Wrapper>
-        <Title>Replica Executions</Title>
+        <Title>Items Created</Title>
         <Module>
         <Module>
           {this.props.replicas.length === 0 && this.props.loading
           {this.props.replicas.length === 0 && this.props.loading
             ? this.renderLoading() : this.renderChart()}
             ? this.renderLoading() : this.renderChart()}

+ 5 - 5
src/components/organisms/DashboardContent/modules/TopEndpointsModule/TopEndpointsModule.tsx

@@ -25,10 +25,10 @@ import PieChart from '../../charts/PieChart'
 import Palette from '../../../../styleUtils/Palette'
 import Palette from '../../../../styleUtils/Palette'
 import StyleProps from '../../../../styleUtils/StyleProps'
 import StyleProps from '../../../../styleUtils/StyleProps'
 
 
-import type { MainItem } from '../../../../../@types/MainItem'
 import type { Endpoint } from '../../../../../@types/Endpoint'
 import type { Endpoint } from '../../../../../@types/Endpoint'
 
 
 import endpointImage from './images/endpoint.svg'
 import endpointImage from './images/endpoint.svg'
+import { ReplicaItem, MigrationItem, TransferItem } from '../../../../../@types/MainItem'
 
 
 const Wrapper = styled.div<any>`
 const Wrapper = styled.div<any>`
   flex-grow: 1;
   flex-grow: 1;
@@ -137,9 +137,9 @@ type GroupedEndpoint = {
 }
 }
 type Props = {
 type Props = {
   // eslint-disable-next-line react/no-unused-prop-types
   // eslint-disable-next-line react/no-unused-prop-types
-  replicas: MainItem[],
+  replicas: ReplicaItem[],
   // eslint-disable-next-line react/no-unused-prop-types
   // eslint-disable-next-line react/no-unused-prop-types
-  migrations: MainItem[],
+  migrations: MigrationItem[],
   // eslint-disable-next-line react/no-unused-prop-types
   // eslint-disable-next-line react/no-unused-prop-types
   endpoints: Endpoint[],
   endpoints: Endpoint[],
   style: any,
   style: any,
@@ -172,7 +172,7 @@ class TopEndpointsModule extends React.Component<Props, State> {
 
 
   calculateGroupedEndpoints(props: Props) {
   calculateGroupedEndpoints(props: Props) {
     const groupedEndpoints: GroupedEndpoint[] = []
     const groupedEndpoints: GroupedEndpoint[] = []
-    const count = (mainItems: MainItem[], endpointId: string) => mainItems
+    const count = (mainItems: TransferItem[], endpointId: string) => mainItems
       .filter(r => r.destination_endpoint_id === endpointId
       .filter(r => r.destination_endpoint_id === endpointId
         || r.origin_endpoint_id === endpointId).length
         || r.origin_endpoint_id === endpointId).length
 
 
@@ -214,7 +214,7 @@ class TopEndpointsModule extends React.Component<Props, State> {
         {topData.map((item, i) => (
         {topData.map((item, i) => (
           <LegendItem key={item.endpoint.id}>
           <LegendItem key={item.endpoint.id}>
             <LegendBullet color={COLORS[i % COLORS.length]} />
             <LegendBullet color={COLORS[i % COLORS.length]} />
-            <LegendLabel to={`/endpoint/${item.endpoint.id}`}>{item.endpoint.name}</LegendLabel>
+            <LegendLabel to={`/endpoints/${item.endpoint.id}`}>{item.endpoint.name}</LegendLabel>
           </LegendItem>
           </LegendItem>
         ))}
         ))}
       </Legend>
       </Legend>

+ 12 - 45
src/components/organisms/DetailsContentHeader/DetailsContentHeader.tsx

@@ -17,8 +17,6 @@ import { observer } from 'mobx-react'
 import styled from 'styled-components'
 import styled from 'styled-components'
 import { Link } from 'react-router-dom'
 import { Link } from 'react-router-dom'
 
 
-import type { MainItem } from '../../../@types/MainItem'
-import type { Execution } from '../../../@types/Execution'
 import StatusPill from '../../atoms/StatusPill'
 import StatusPill from '../../atoms/StatusPill'
 import ActionDropdown from '../../molecules/ActionDropdown'
 import ActionDropdown from '../../molecules/ActionDropdown'
 import type { Action as DropdownAction } from '../../molecules/ActionDropdown'
 import type { Action as DropdownAction } from '../../molecules/ActionDropdown'
@@ -84,37 +82,21 @@ type Props = {
   dropdownActions?: DropdownAction[],
   dropdownActions?: DropdownAction[],
   backLink: string,
   backLink: string,
   typeImage?: string,
   typeImage?: string,
-  statusLabel?: string,
-  item: any,
   alertInfoPill?: boolean,
   alertInfoPill?: boolean,
   primaryInfoPill?: boolean,
   primaryInfoPill?: boolean,
+  statusPill?: string,
+  statusLabel?: string,
+  itemTitle?: string | null
+  itemType?: string
+  itemDescription?: string
 }
 }
 @observer
 @observer
 class DetailsContentHeader extends React.Component<Props> {
 class DetailsContentHeader extends React.Component<Props> {
-  getLastExecution(): MainItem | Execution | null | undefined {
-    if (this.props.item && this.props.item.executions && this.props.item.executions.length) {
-      return this.props.item.executions[this.props.item.executions.length - 1]
-    } if (this.props.item && typeof this.props.item.executions === 'undefined') {
-      return this.props.item
-    }
-
-    return null
-  }
-
-  getStatus() {
-    const lastExecution = this.getLastExecution()
-    if (lastExecution) {
-      return lastExecution.status
-    }
-
-    return null
-  }
-
   renderStatusPill() {
   renderStatusPill() {
-    if (!this.getStatus()) {
+    if (!this.props.statusPill) {
       return null
       return null
     }
     }
-    let statusLabel = this.getStatus()
+    let statusLabel = this.props.statusPill
     if (this.props.statusLabel) {
     if (this.props.statusLabel) {
       statusLabel = this.props.statusLabel
       statusLabel = this.props.statusLabel
     }
     }
@@ -122,14 +104,12 @@ class DetailsContentHeader extends React.Component<Props> {
       <StatusPills>
       <StatusPills>
         <StatusPill
         <StatusPill
           status="INFO"
           status="INFO"
-          label={this.props.item ? this.props.item.type && this.props.item.type.toUpperCase() : ''}
+          label={this.props.itemType}
           alert={this.props.alertInfoPill}
           alert={this.props.alertInfoPill}
           primary={this.props.primaryInfoPill}
           primary={this.props.primaryInfoPill}
-          data-test-id="dcHeader-infoPill"
         />
         />
         <StatusPill
         <StatusPill
-          data-test-id={`dcHeader-statusPill-${statusLabel || ''}`}
-          status={this.getStatus()}
+          status={this.props.statusPill}
           label={statusLabel || ''}
           label={statusLabel || ''}
         />
         />
       </StatusPills>
       </StatusPills>
@@ -151,36 +131,23 @@ class DetailsContentHeader extends React.Component<Props> {
   }
   }
 
 
   renderDescription() {
   renderDescription() {
-    if (!this.props.item || !this.props.item.description) {
+    if (!this.props.itemDescription) {
       return null
       return null
     }
     }
 
 
     return (
     return (
-      <Description data-test-id="dcHeader-description">{this.props.item.description}</Description>
+      <Description>{this.props.itemDescription}</Description>
     )
     )
   }
   }
 
 
   render() {
   render() {
-    let title = null
-    if (this.props.item) {
-      const { instances } = this.props.item
-      if (instances) {
-        title = instances[0]
-        if (instances.length > 1) {
-          title += ` (+${instances.length - 1} more)`
-        }
-      } else {
-        title = this.props.item.name
-      }
-    }
-
     return (
     return (
       <Wrapper>
       <Wrapper>
         <BackButton to={this.props.backLink} data-test-id="dcHeader-backButton" />
         <BackButton to={this.props.backLink} data-test-id="dcHeader-backButton" />
         <TypeImage image={this.props.typeImage} />
         <TypeImage image={this.props.typeImage} />
         <Title>
         <Title>
           <Status>
           <Status>
-            <Text title={title} data-test-id="dcHeader-title">{title}</Text>
+            <Text title={this.props.itemTitle}>{this.props.itemTitle}</Text>
             {this.renderStatusPill()}
             {this.renderStatusPill()}
             {this.renderDescription()}
             {this.renderDescription()}
           </Status>
           </Status>

+ 13 - 10
src/components/organisms/EditReplica/EditReplica.tsx

@@ -31,7 +31,9 @@ import WizardNetworks from '../WizardNetworks'
 import WizardOptions from '../WizardOptions'
 import WizardOptions from '../WizardOptions'
 import WizardStorage from '../WizardStorage/WizardStorage'
 import WizardStorage from '../WizardStorage/WizardStorage'
 
 
-import type { MainItem, UpdateData } from '../../../@types/MainItem'
+import type {
+  UpdateData, TransferItemDetails, MigrationItemDetails,
+} from '../../../@types/MainItem'
 import type { NavigationItem } from '../../molecules/Panel'
 import type { NavigationItem } from '../../molecules/Panel'
 import type { Endpoint, StorageBackend, StorageMap } from '../../../@types/Endpoint'
 import type { Endpoint, StorageBackend, StorageMap } from '../../../@types/Endpoint'
 import type { Field } from '../../../@types/Field'
 import type { Field } from '../../../@types/Field'
@@ -84,7 +86,7 @@ type Props = {
   isOpen: boolean,
   isOpen: boolean,
   onRequestClose: () => void,
   onRequestClose: () => void,
   onUpdateComplete: (redirectTo: string) => void,
   onUpdateComplete: (redirectTo: string) => void,
-  replica: MainItem,
+  replica: TransferItemDetails,
   destinationEndpoint: Endpoint,
   destinationEndpoint: Endpoint,
   sourceEndpoint: Endpoint,
   sourceEndpoint: Endpoint,
   instancesDetails: Instance[],
   instancesDetails: Instance[],
@@ -224,11 +226,12 @@ class EditReplica extends React.Component<Props, State> {
       const osData = replicaData[`${plugin.migrationImageMapFieldName}/${osMapping[1]}`]
       const osData = replicaData[`${plugin.migrationImageMapFieldName}/${osMapping[1]}`]
       return osData
       return osData
     }
     }
-    if (migrationFields.find(f => f.name === fieldName) && this.props.replica[fieldName]) {
-      return this.props.replica[fieldName]
+    const anyData = this.props.replica as any
+    if (migrationFields.find(f => f.name === fieldName) && anyData[fieldName]) {
+      return anyData[fieldName]
     }
     }
     if (fieldName === 'skip_os_morphing' && this.props.type === 'migration') {
     if (fieldName === 'skip_os_morphing' && this.props.type === 'migration') {
-      return migrationStore.getDefaultSkipOsMorphing(this.props.replica)
+      return migrationStore.getDefaultSkipOsMorphing(anyData)
     }
     }
     return defaultValue
     return defaultValue
   }
   }
@@ -439,12 +442,12 @@ class EditReplica extends React.Component<Props, State> {
     if (this.props.type === 'replica') {
     if (this.props.type === 'replica') {
       try {
       try {
         await replicaStore.update(
         await replicaStore.update(
-          this.props.replica,
+          this.props.replica as any,
           this.props.destinationEndpoint,
           this.props.destinationEndpoint,
           updateData, this.getDefaultStorage(), endpointStore.storageConfigDefault,
           updateData, this.getDefaultStorage(), endpointStore.storageConfigDefault,
         )
         )
         this.props.onRequestClose()
         this.props.onRequestClose()
-        this.props.onUpdateComplete(`/replica/executions/${this.props.replica.id}`)
+        this.props.onUpdateComplete(`/replicas/${this.props.replica.id}/executions`)
       } catch (err) {
       } catch (err) {
         this.setState({ updateDisabled: false })
         this.setState({ updateDisabled: false })
       }
       }
@@ -452,8 +455,8 @@ class EditReplica extends React.Component<Props, State> {
       try {
       try {
         const replicaDefaultStorage = this.props.replica.storage_mappings
         const replicaDefaultStorage = this.props.replica.storage_mappings
           && this.props.replica.storage_mappings.default
           && this.props.replica.storage_mappings.default
-        const migration: MainItem = await migrationStore.recreate(
-          this.props.replica,
+        const migration: MigrationItemDetails = await migrationStore.recreate(
+          this.props.replica as any,
           this.props.sourceEndpoint,
           this.props.sourceEndpoint,
           this.props.destinationEndpoint,
           this.props.destinationEndpoint,
           updateData,
           updateData,
@@ -463,7 +466,7 @@ class EditReplica extends React.Component<Props, State> {
         )
         )
         migrationStore.clearDetails()
         migrationStore.clearDetails()
         this.props.onRequestClose()
         this.props.onRequestClose()
-        this.props.onUpdateComplete(`/migration/tasks/${migration.id}`)
+        this.props.onUpdateComplete(`/migrations/${migration.id}/tasks`)
       } catch (err) {
       } catch (err) {
         this.setState({ updateDisabled: false })
         this.setState({ updateDisabled: false })
       }
       }

+ 5 - 4
src/components/organisms/EndpointDetailsContent/EndpointDetailsContent.tsx

@@ -25,13 +25,13 @@ import CopyMultilineValue from '../../atoms/CopyMultilineValue'
 import StatusImage from '../../atoms/StatusImage'
 import StatusImage from '../../atoms/StatusImage'
 
 
 import type { Endpoint } from '../../../@types/Endpoint'
 import type { Endpoint } from '../../../@types/Endpoint'
-import type { MainItem } from '../../../@types/MainItem'
 import StyleProps from '../../styleUtils/StyleProps'
 import StyleProps from '../../styleUtils/StyleProps'
 import Palette from '../../styleUtils/Palette'
 import Palette from '../../styleUtils/Palette'
 import DateUtils from '../../../utils/DateUtils'
 import DateUtils from '../../../utils/DateUtils'
 import LabelDictionary from '../../../utils/LabelDictionary'
 import LabelDictionary from '../../../utils/LabelDictionary'
 import configLoader from '../../../utils/Config'
 import configLoader from '../../../utils/Config'
 import { Region } from '../../../@types/Region'
 import { Region } from '../../../@types/Region'
+import { MigrationItem, ReplicaItem, TransferItem } from '../../../@types/MainItem'
 
 
 const Wrapper = styled.div<any>`
 const Wrapper = styled.div<any>`
   ${StyleProps.exactWidth(StyleProps.contentWidth)}
   ${StyleProps.exactWidth(StyleProps.contentWidth)}
@@ -81,7 +81,7 @@ type Props = {
   regions: Region[],
   regions: Region[],
   connectionInfo: Endpoint['connection_info'] | null,
   connectionInfo: Endpoint['connection_info'] | null,
   loading: boolean,
   loading: boolean,
-  usage: { migrations: MainItem[], replicas: MainItem[] },
+  usage: { migrations: MigrationItem[], replicas: ReplicaItem[] },
   onDeleteClick: () => void,
   onDeleteClick: () => void,
   onValidateClick: () => void,
   onValidateClick: () => void,
 }
 }
@@ -174,7 +174,7 @@ class EndpointDetailsContent extends React.Component<Props> {
     )
     )
   }
   }
 
 
-  renderUsage(items: MainItem[]) {
+  renderUsage(items: TransferItem[]) {
     return items.map(item => (
     return items.map(item => (
       <span>
       <span>
         <LinkStyled
         <LinkStyled
@@ -193,7 +193,8 @@ class EndpointDetailsContent extends React.Component<Props> {
     const {
     const {
       type, name, description, created_at, id,
       type, name, description, created_at, id,
     } = this.props.item || {}
     } = this.props.item || {}
-    const usage = this.props.usage.replicas.concat(this.props.usage.migrations)
+    const usage: TransferItem[] = this.props.usage.replicas
+      .concat(this.props.usage.migrations as any[])
 
 
     return (
     return (
       <Wrapper>
       <Wrapper>

+ 55 - 31
src/components/organisms/Executions/Executions.tsx

@@ -23,8 +23,7 @@ import Button from '../../atoms/Button'
 import Timeline from '../../molecules/Timeline'
 import Timeline from '../../molecules/Timeline'
 import Tasks from '../Tasks'
 import Tasks from '../Tasks'
 
 
-import type { MainItem } from '../../../@types/MainItem'
-import type { Execution } from '../../../@types/Execution'
+import type { Execution, ExecutionTasks } from '../../../@types/Execution'
 import Palette from '../../styleUtils/Palette'
 import Palette from '../../styleUtils/Palette'
 import DateUtils from '../../../utils/DateUtils'
 import DateUtils from '../../../utils/DateUtils'
 
 
@@ -85,8 +84,11 @@ const NoExecutionText = styled.div<any>`
 `
 `
 
 
 type Props = {
 type Props = {
-  item?: MainItem | null,
+  executions: Execution[],
+  executionsTasks: ExecutionTasks[],
   loading: boolean,
   loading: boolean,
+  tasksLoading: boolean,
+  onChange: (executionId: string) => void,
   onCancelExecutionClick: (execution: Execution | null, force?: boolean) => void,
   onCancelExecutionClick: (execution: Execution | null, force?: boolean) => void,
   onDeleteExecutionClick: (execution: Execution | null) => void,
   onDeleteExecutionClick: (execution: Execution | null) => void,
   onExecuteClick: () => void,
   onExecuteClick: () => void,
@@ -110,27 +112,27 @@ class Executions extends React.Component<Props, State> {
 
 
   setSelectedExecution(props: Props) {
   setSelectedExecution(props: Props) {
     const lastExecution = this.getLastExecution(props)
     const lastExecution = this.getLastExecution(props)
-    let selectExecution = null
+    let selectExecution: Execution | null | undefined = null
 
 
-    if (props.item && props.item.executions && this.props.item && this.props.item.executions) {
-      if (this.props.item.executions.length !== props.item.executions.length
+    if (props.executions && this.props.executions) {
+      if (this.props.executions.length !== props.executions.length
         && lastExecution && lastExecution.status === 'RUNNING') {
         && lastExecution && lastExecution.status === 'RUNNING') {
         selectExecution = lastExecution
         selectExecution = lastExecution
       }
       }
 
 
-      if (this.props.item.executions.length > props.item.executions.length) {
-        const isSelectedAvailable = props.item.executions.find(e => this.state.selectedExecution
+      if (this.props.executions.length > props.executions.length) {
+        const isSelectedAvailable = props.executions.find(e => this.state.selectedExecution
           && e.id === this.state.selectedExecution.id)
           && e.id === this.state.selectedExecution.id)
         if (!isSelectedAvailable) {
         if (!isSelectedAvailable) {
-          const lastIndex = this.props.item && this.props.item.executions
-            ? this.props.item.executions
+          const lastIndex = this.props.executions
+            ? this.props.executions
               .findIndex(e => this.state.selectedExecution
               .findIndex(e => this.state.selectedExecution
               && e.id === this.state.selectedExecution.id) : -1
               && e.id === this.state.selectedExecution.id) : -1
-          if (props.item && props.item.executions.length) {
-            if (props.item.executions.length - 1 >= lastIndex) {
-              selectExecution = props.item.executions[lastIndex]
+          if (props.executions.length) {
+            if (props.executions.length - 1 >= lastIndex) {
+              selectExecution = props.executions[lastIndex]
             } else {
             } else {
-              selectExecution = props.item.executions[lastIndex - 1]
+              selectExecution = props.executions[lastIndex - 1]
             }
             }
           }
           }
         }
         }
@@ -140,16 +142,22 @@ class Executions extends React.Component<Props, State> {
     if (!currentSelectedExecution) {
     if (!currentSelectedExecution) {
       this.setState({
       this.setState({
         selectedExecution: lastExecution || null,
         selectedExecution: lastExecution || null,
+      }, () => {
+        this.handleChange(lastExecution)
       })
       })
     } else if (selectExecution) {
     } else if (selectExecution) {
       this.setState({
       this.setState({
         selectedExecution: selectExecution,
         selectedExecution: selectExecution,
+      }, () => {
+        this.handleChange(selectExecution)
       })
       })
     } else if (this.hasExecutions(props)) {
     } else if (this.hasExecutions(props)) {
-      selectExecution = (props.item && props.item.executions
+      selectExecution = (props.executions
         .find(e => e.id === currentSelectedExecution.id)) || lastExecution
         .find(e => e.id === currentSelectedExecution.id)) || lastExecution
       this.setState({
       this.setState({
         selectedExecution: selectExecution || null,
         selectedExecution: selectExecution || null,
+      }, () => {
+        this.handleChange(selectExecution)
       })
       })
     } else {
     } else {
       this.setState({ selectedExecution: null })
       this.setState({ selectedExecution: null })
@@ -157,23 +165,31 @@ class Executions extends React.Component<Props, State> {
   }
   }
 
 
   getLastExecution(props: Props) {
   getLastExecution(props: Props) {
-    if (this.hasExecutions(props) && props.item) {
-      return props.item.executions[props.item.executions.length - 1]
+    if (this.hasExecutions(props)) {
+      return props.executions[props.executions.length - 1]
     }
     }
     return null
     return null
   }
   }
 
 
   hasExecutions(props: Props) {
   hasExecutions(props: Props) {
-    return Boolean(props.item && props.item.executions && props.item.executions.length)
+    return Boolean(props.executions.length)
+  }
+
+  handleChange(execution?: Execution | null) {
+    if (!execution) {
+      return
+    }
+
+    this.props.onChange(execution.id)
   }
   }
 
 
   handlePreviousExecutionClick() {
   handlePreviousExecutionClick() {
     const currentSelectedExecution = this.state.selectedExecution
     const currentSelectedExecution = this.state.selectedExecution
-    if (!this.props.item || !currentSelectedExecution) {
+    if (!this.props.executions.length || !currentSelectedExecution) {
       return
       return
     }
     }
 
 
-    const selectedIndex = this.props.item
+    const selectedIndex = this.props
       .executions.findIndex(e => e.id === currentSelectedExecution.id)
       .executions.findIndex(e => e.id === currentSelectedExecution.id)
 
 
     if (selectedIndex === 0) {
     if (selectedIndex === 0) {
@@ -181,27 +197,33 @@ class Executions extends React.Component<Props, State> {
     }
     }
 
 
     this.setState({
     this.setState({
-      selectedExecution: this.props.item ? this.props.item.executions[selectedIndex - 1] : null,
+      selectedExecution: this.props.executions[selectedIndex - 1],
+    }, () => {
+      this.handleChange(this.props.executions[selectedIndex - 1])
     })
     })
   }
   }
 
 
   handleNextExecutionClick() {
   handleNextExecutionClick() {
     const currentSelectedExecution = this.state.selectedExecution
     const currentSelectedExecution = this.state.selectedExecution
-    if (!this.props.item || !currentSelectedExecution) {
+    if (!this.props.executions.length || !currentSelectedExecution) {
       return
       return
     }
     }
-    const selectedIndex = this.props.item.executions
+    const selectedIndex = this.props.executions
       .findIndex(e => e.id === currentSelectedExecution.id)
       .findIndex(e => e.id === currentSelectedExecution.id)
 
 
-    if (!this.props.item || selectedIndex >= this.props.item.executions.length - 1) {
+    if (selectedIndex >= this.props.executions.length - 1) {
       return
       return
     }
     }
 
 
-    this.setState({ selectedExecution: this.props.item.executions[selectedIndex + 1] })
+    this.setState({ selectedExecution: this.props.executions[selectedIndex + 1] }, () => {
+      this.handleChange(this.props.executions[selectedIndex + 1])
+    })
   }
   }
 
 
   handleTimelineItemClick(item: Execution) {
   handleTimelineItemClick(item: Execution) {
-    this.setState({ selectedExecution: item })
+    this.setState({ selectedExecution: item }, () => {
+      this.handleChange(item)
+    })
   }
   }
 
 
   handleCancelExecutionClick() {
   handleCancelExecutionClick() {
@@ -232,7 +254,7 @@ class Executions extends React.Component<Props, State> {
 
 
     return (
     return (
       <Timeline
       <Timeline
-        items={this.props.item ? this.props.item.executions : null}
+        items={this.props.executions}
         selectedItem={this.state.selectedExecution}
         selectedItem={this.state.selectedExecution}
         onPreviousClick={() => { this.handlePreviousExecutionClick() }}
         onPreviousClick={() => { this.handlePreviousExecutionClick() }}
         onNextClick={() => { this.handleNextExecutionClick() }}
         onNextClick={() => { this.handleNextExecutionClick() }}
@@ -305,14 +327,16 @@ class Executions extends React.Component<Props, State> {
   }
   }
 
 
   renderTasks() {
   renderTasks() {
-    if (!this.state.selectedExecution || !this.state.selectedExecution.tasks
-      || !this.state.selectedExecution.tasks.length
-      || this.props.loading) {
+    if (this.props.loading || this.props.executions.length === 0) {
       return null
       return null
     }
     }
 
 
     return (
     return (
-      <Tasks items={this.state.selectedExecution.tasks} />
+      <Tasks
+        loading={this.props.tasksLoading}
+        items={this.props.executionsTasks
+          .find(e => e.id === this.state.selectedExecution?.id)?.tasks || []}
+      />
     )
     )
   }
   }
 
 

+ 2 - 4
src/components/organisms/FilterList/FilterList.tsx

@@ -22,8 +22,6 @@ import type { Action as DropdownAction } from '../../molecules/ActionDropdown'
 import type { ItemComponentProps } from '../MainList'
 import type { ItemComponentProps } from '../MainList'
 import MainList from '../MainList'
 import MainList from '../MainList'
 
 
-import type { MainItem } from '../../../@types/MainItem'
-
 import configLoader from '../../../utils/Config'
 import configLoader from '../../../utils/Config'
 
 
 const Wrapper = styled.div<any>`
 const Wrapper = styled.div<any>`
@@ -135,7 +133,7 @@ class FilterList extends React.Component<Props, State> {
     })
     })
   }
   }
 
 
-  handleItemSelectedChange(item: MainItem, selected: boolean) {
+  handleItemSelectedChange(item: any, selected: boolean) {
     const items = this.state.selectedItems.slice(0)
     const items = this.state.selectedItems.slice(0)
     const selectedItems = items.filter(i => item.id !== i.id) || []
     const selectedItems = items.filter(i => item.id !== i.id) || []
 
 
@@ -159,7 +157,7 @@ class FilterList extends React.Component<Props, State> {
     })
     })
   }
   }
 
 
-  filterItems(items: MainItem[], filterStatus?: string | null, filterText?: string): MainItem[] {
+  filterItems(items: any[], filterStatus?: string | null, filterText?: string): any[] {
     const newFilterStatus = filterStatus || this.state.filterStatus
     const newFilterStatus = filterStatus || this.state.filterStatus
     const newFilterText = typeof filterText === 'undefined' ? this.state.filterText : filterText
     const newFilterText = typeof filterText === 'undefined' ? this.state.filterText : filterText
     const filteredItems = items
     const filteredItems = items

+ 6 - 20
src/components/organisms/MainDetails/MainDetails.tsx

@@ -26,7 +26,6 @@ import CopyMultilineValue from '../../atoms/CopyMultilineValue'
 import PasswordValue from '../../atoms/PasswordValue'
 import PasswordValue from '../../atoms/PasswordValue'
 
 
 import type { Instance } from '../../../@types/Instance'
 import type { Instance } from '../../../@types/Instance'
-import type { MainItem } from '../../../@types/MainItem'
 import type { Endpoint } from '../../../@types/Endpoint'
 import type { Endpoint } from '../../../@types/Endpoint'
 import type { Network } from '../../../@types/Network'
 import type { Network } from '../../../@types/Network'
 import type { Field as FieldType } from '../../../@types/Field'
 import type { Field as FieldType } from '../../../@types/Field'
@@ -39,6 +38,7 @@ import LabelDictionary from '../../../utils/LabelDictionary'
 import { OptionsSchemaPlugin } from '../../../plugins/endpoint'
 import { OptionsSchemaPlugin } from '../../../plugins/endpoint'
 
 
 import arrowImage from './images/arrow.svg'
 import arrowImage from './images/arrow.svg'
+import { TransferItem } from '../../../@types/MainItem'
 
 
 const Wrapper = styled.div<any>`
 const Wrapper = styled.div<any>`
   display: flex;
   display: flex;
@@ -120,7 +120,7 @@ const PropertyValue = styled.div<any>`
 `
 `
 
 
 type Props = {
 type Props = {
-  item?: MainItem | null,
+  item?: TransferItem | null,
   destinationSchema: FieldType[],
   destinationSchema: FieldType[],
   destinationSchemaLoading: boolean,
   destinationSchemaLoading: boolean,
   sourceSchema: FieldType[],
   sourceSchema: FieldType[],
@@ -153,14 +153,6 @@ class MainDetails extends React.Component<Props, State> {
     return endpoint
     return endpoint
   }
   }
 
 
-  getLastExecution() {
-    if (this.props.item?.executions && this.props.item.executions.length) {
-      return this.props.item.executions[this.props.item.executions.length - 1]
-    }
-
-    return null
-  }
-
   getConnectedVms(networkId: string) {
   getConnectedVms(networkId: string) {
     if (this.props.instancesDetailsLoading) {
     if (this.props.instancesDetailsLoading) {
       return 'Loading...'
       return 'Loading...'
@@ -185,13 +177,7 @@ class MainDetails extends React.Component<Props, State> {
   }
   }
 
 
   renderLastExecutionTime() {
   renderLastExecutionTime() {
-    const lastExecution = this.getLastExecution()
-    const lastUpdate = lastExecution?.updated_at || lastExecution?.created_at
-    if (lastUpdate) {
-      return this.renderValue(DateUtils.getLocalTime(lastUpdate).format('YYYY-MM-DD HH:mm:ss'))
-    }
-
-    return null
+    return this.props.item ? this.renderValue(DateUtils.getLocalTime(this.props.item.updated_at).format('YYYY-MM-DD HH:mm:ss')) : '-'
   }
   }
 
 
   renderValue(value: string, dateTestId?: string) {
   renderValue(value: string, dateTestId?: string) {
@@ -208,7 +194,7 @@ class MainDetails extends React.Component<Props, State> {
     const endpoint = type === 'source' ? this.getSourceEndpoint() : this.getDestinationEndpoint()
     const endpoint = type === 'source' ? this.getSourceEndpoint() : this.getDestinationEndpoint()
 
 
     if (endpoint) {
     if (endpoint) {
-      return <ValueLink data-test-id={`mainDetails-name-${type}`} to={`/endpoint/${endpoint.id}`}>{endpoint.name}</ValueLink>
+      return <ValueLink to={`/endpoints/${endpoint.id}`}>{endpoint.name}</ValueLink>
     }
     }
 
 
     return endpointIsMissing
     return endpointIsMissing
@@ -353,11 +339,11 @@ class MainDetails extends React.Component<Props, State> {
               </Field>
               </Field>
             </Row>
             </Row>
           ) : null}
           ) : null}
-          {this.props.item && this.props.item.replica_id ? (
+          {this.props.item?.type === 'migration' && this.props.item.replica_id ? (
             <Row>
             <Row>
               <Field>
               <Field>
                 <Label>Created from Replica</Label>
                 <Label>Created from Replica</Label>
-                <ValueLink to={`/replica/${this.props.item.replica_id}`}>{this.props.item.replica_id}</ValueLink>
+                <ValueLink to={`/replicas/${this.props.item.replica_id}`}>{this.props.item.replica_id}</ValueLink>
               </Field>
               </Field>
             </Row>
             </Row>
           ) : null}
           ) : null}

+ 3 - 4
src/components/organisms/MainList/MainList.tsx

@@ -19,7 +19,6 @@ import styled from 'styled-components'
 import StatusImage from '../../atoms/StatusImage'
 import StatusImage from '../../atoms/StatusImage'
 import Button from '../../atoms/Button'
 import Button from '../../atoms/Button'
 
 
-import type { MainItem } from '../../../@types/MainItem'
 import Palette from '../../styleUtils/Palette'
 import Palette from '../../styleUtils/Palette'
 
 
 const Wrapper = styled.div<any>`
 const Wrapper = styled.div<any>`
@@ -28,7 +27,7 @@ const Wrapper = styled.div<any>`
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
   flex-grow: 1;
   flex-grow: 1;
-  min-width: 785px;
+  min-width: 900px;
 `
 `
 const Separator = styled.div<any>`
 const Separator = styled.div<any>`
   height: 1px;
   height: 1px;
@@ -89,8 +88,8 @@ type Props = {
   items: any[],
   items: any[],
   selectedItems: any[],
   selectedItems: any[],
   loading: boolean,
   loading: boolean,
-  onSelectedChange: (item: MainItem, checked: boolean) => void,
-  onItemClick: (item: MainItem) => void,
+  onSelectedChange: (item: any, checked: boolean) => void,
+  onItemClick: (item: any) => void,
   renderItemComponent: (componentProps: ItemComponentProps) => React.ReactNode,
   renderItemComponent: (componentProps: ItemComponentProps) => React.ReactNode,
   showEmptyList: boolean,
   showEmptyList: boolean,
   emptyListImage?: string | null,
   emptyListImage?: string | null,

+ 3 - 3
src/components/organisms/MigrationDetailsContent/MigrationDetailsContent.tsx

@@ -23,9 +23,9 @@ import Tasks from '../Tasks'
 import StyleProps from '../../styleUtils/StyleProps'
 import StyleProps from '../../styleUtils/StyleProps'
 
 
 import type { Instance } from '../../../@types/Instance'
 import type { Instance } from '../../../@types/Instance'
-import type { MainItem } from '../../../@types/MainItem'
 import type { Endpoint } from '../../../@types/Endpoint'
 import type { Endpoint } from '../../../@types/Endpoint'
 import type { Field } from '../../../@types/Field'
 import type { Field } from '../../../@types/Field'
+import { MigrationItemDetails } from '../../../@types/MainItem'
 
 
 const Wrapper = styled.div<any>`
 const Wrapper = styled.div<any>`
   display: flex;
   display: flex;
@@ -53,7 +53,7 @@ const NavigationItems = [
 ]
 ]
 
 
 type Props = {
 type Props = {
-  item: MainItem | null,
+  item: MigrationItemDetails | null,
   detailsLoading: boolean,
   detailsLoading: boolean,
   instancesDetails: Instance[],
   instancesDetails: Instance[],
   instancesDetailsLoading: boolean,
   instancesDetailsLoading: boolean,
@@ -110,7 +110,7 @@ class MigrationDetailsContent extends React.Component<Props> {
     return (
     return (
       <Tasks
       <Tasks
         items={this.props.item.tasks}
         items={this.props.item.tasks}
-        data-test-id="mdContent-tasks"
+        loading={this.props.detailsLoading}
       />
       />
     )
     )
   }
   }

+ 1 - 1
src/components/organisms/ProjectDetailsContent/ProjectDetailsContent.tsx

@@ -241,7 +241,7 @@ class ProjectDetailsContent extends React.Component<Props, State> {
         <UserName
         <UserName
           data-test-id={`pdContent-users-${user.name}`}
           data-test-id={`pdContent-users-${user.name}`}
           disabled={!user.enabled}
           disabled={!user.enabled}
-          to={`/user/${user.id}`}
+          to={`/users/${user.id}`}
         >{user.name}
         >{user.name}
         </UserName>,
         </UserName>,
         <DropdownLink
         <DropdownLink

+ 11 - 5
src/components/organisms/ReplicaDetailsContent/ReplicaDetailsContent.tsx

@@ -23,13 +23,13 @@ import MainDetails from '../MainDetails'
 import Executions from '../Executions'
 import Executions from '../Executions'
 import Schedule from '../Schedule'
 import Schedule from '../Schedule'
 import type { Instance } from '../../../@types/Instance'
 import type { Instance } from '../../../@types/Instance'
-import type { MainItem } from '../../../@types/MainItem'
 import type { Endpoint } from '../../../@types/Endpoint'
 import type { Endpoint } from '../../../@types/Endpoint'
-import type { Execution } from '../../../@types/Execution'
+import type { Execution, ExecutionTasks } from '../../../@types/Execution'
 import type { Network } from '../../../@types/Network'
 import type { Network } from '../../../@types/Network'
 import type { Field } from '../../../@types/Field'
 import type { Field } from '../../../@types/Field'
 import type { Schedule as ScheduleType } from '../../../@types/Schedule'
 import type { Schedule as ScheduleType } from '../../../@types/Schedule'
 import StyleProps from '../../styleUtils/StyleProps'
 import StyleProps from '../../styleUtils/StyleProps'
+import { ReplicaItemDetails } from '../../../@types/MainItem'
 
 
 const Wrapper = styled.div<any>`
 const Wrapper = styled.div<any>`
   display: flex;
   display: flex;
@@ -70,7 +70,7 @@ const NavigationItems = [
 
 
 type TimezoneValue = 'utc' | 'local'
 type TimezoneValue = 'utc' | 'local'
 type Props = {
 type Props = {
-  item?: MainItem | null,
+  item?: ReplicaItemDetails | null,
   endpoints: Endpoint[],
   endpoints: Endpoint[],
   sourceSchema: Field[],
   sourceSchema: Field[],
   sourceSchemaLoading: boolean,
   sourceSchemaLoading: boolean,
@@ -82,7 +82,11 @@ type Props = {
   scheduleStore: typeof scheduleStore,
   scheduleStore: typeof scheduleStore,
   page: string,
   page: string,
   detailsLoading: boolean,
   detailsLoading: boolean,
+  executions: Execution[],
   executionsLoading: boolean,
   executionsLoading: boolean,
+  executionsTasksLoading: boolean,
+  executionsTasks: ExecutionTasks[],
+  onExecutionChange: (executionId: string) => void,
   onCancelExecutionClick: (execution: Execution | null, force?: boolean) => void,
   onCancelExecutionClick: (execution: Execution | null, force?: boolean) => void,
   onDeleteExecutionClick: (execution: Execution | null) => void,
   onDeleteExecutionClick: (execution: Execution | null) => void,
   onExecuteClick: () => void,
   onExecuteClick: () => void,
@@ -186,12 +190,14 @@ class ReplicaDetailsContent extends React.Component<Props, State> {
 
 
     return (
     return (
       <Executions
       <Executions
-        item={this.props.item}
+        executions={this.props.executions}
+        executionsTasks={this.props.executionsTasks}
         onCancelExecutionClick={this.props.onCancelExecutionClick}
         onCancelExecutionClick={this.props.onCancelExecutionClick}
         onDeleteExecutionClick={this.props.onDeleteExecutionClick}
         onDeleteExecutionClick={this.props.onDeleteExecutionClick}
         onExecuteClick={this.props.onExecuteClick}
         onExecuteClick={this.props.onExecuteClick}
         loading={this.props.executionsLoading}
         loading={this.props.executionsLoading}
-        data-test-id="rdContent-executions"
+        onChange={this.props.onExecutionChange}
+        tasksLoading={this.props.executionsTasksLoading}
       />
       />
     )
     )
   }
   }

+ 33 - 3
src/components/organisms/Tasks/Tasks.tsx

@@ -21,12 +21,20 @@ import TaskItem from '../../molecules/TaskItem'
 import type { Task } from '../../../@types/Task'
 import type { Task } from '../../../@types/Task'
 import Palette from '../../styleUtils/Palette'
 import Palette from '../../styleUtils/Palette'
 import StyleProps from '../../styleUtils/StyleProps'
 import StyleProps from '../../styleUtils/StyleProps'
+import StatusImage from '../../atoms/StatusImage/StatusImage'
 
 
 const ColumnWidths = ['26%', '18%', '36%', '20%']
 const ColumnWidths = ['26%', '18%', '36%', '20%']
 
 
-const Wrapper = styled.div<any>`
+const Wrapper = styled.div<any>``
+const ContentWrapper = styled.div`
   background: ${Palette.grayscale[1]};
   background: ${Palette.grayscale[1]};
 `
 `
+const LoadingWrapper = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 64px;
+`
 const Header = styled.div<any>`
 const Header = styled.div<any>`
   display: flex;
   display: flex;
   border-bottom: 1px solid ${Palette.grayscale[5]};
   border-bottom: 1px solid ${Palette.grayscale[5]};
@@ -43,6 +51,7 @@ const Body = styled.div<any>``
 
 
 type Props = {
 type Props = {
   items: Task[],
   items: Task[],
+  loading?: boolean,
 }
 }
 type State = {
 type State = {
   openedItems: Task[],
   openedItems: Task[],
@@ -80,6 +89,10 @@ class Tasks extends React.Component<Props, State> {
     })
     })
   }
   }
 
 
+  get isLoading() {
+    return this.props.loading || this.props.items.length === 0
+  }
+
   handleItemMouseDown(e: React.MouseEvent<HTMLDivElement>) {
   handleItemMouseDown(e: React.MouseEvent<HTMLDivElement>) {
     this.dragStartPosition = { x: e.screenX, y: e.screenY }
     this.dragStartPosition = { x: e.screenX, y: e.screenY }
   }
   }
@@ -115,6 +128,14 @@ class Tasks extends React.Component<Props, State> {
     })
     })
   }
   }
 
 
+  renderLoading() {
+    return (
+      <LoadingWrapper>
+        <StatusImage loading />
+      </LoadingWrapper>
+    )
+  }
+
   renderHeader() {
   renderHeader() {
     return (
     return (
       <Header>
       <Header>
@@ -145,11 +166,20 @@ class Tasks extends React.Component<Props, State> {
     )
     )
   }
   }
 
 
-  render() {
+  renderContent() {
     return (
     return (
-      <Wrapper>
+      <ContentWrapper>
         {this.renderHeader()}
         {this.renderHeader()}
         {this.renderBody()}
         {this.renderBody()}
+      </ContentWrapper>
+    )
+  }
+
+  render() {
+    return (
+      <Wrapper>
+        {!this.isLoading ? this.renderContent() : null}
+        {this.isLoading ? this.renderLoading() : null}
       </Wrapper>
       </Wrapper>
     )
     )
   }
   }

+ 1 - 1
src/components/organisms/Tasks/story.tsx

@@ -80,5 +80,5 @@ const items: any = [
 
 
 storiesOf('Tasks', module)
 storiesOf('Tasks', module)
   .add('default', () => (
   .add('default', () => (
-    <div style={{ width: '800px' }}><Tasks items={items} /></div>
+    <div style={{ width: '800px' }}><Tasks items={items} loading={false} /></div>
   ))
   ))

+ 1 - 1
src/components/organisms/UserDetailsContent/UserDetailsContent.tsx

@@ -135,7 +135,7 @@ class UserDetailsContent extends React.Component<Props> {
     return projects.map((project, i) => (
     return projects.map((project, i) => (
       <span key={project.id}>
       <span key={project.id}>
         {project.label ? (
         {project.label ? (
-          <LinkStyled data-test-id={`${TEST_ID}-project-${project.id}`} to={`/project/${project.id}`}>
+          <LinkStyled to={`/projects/${project.id}`}>
             {project.label}
             {project.label}
           </LinkStyled>
           </LinkStyled>
         ) : project.id}
         ) : project.id}

+ 3 - 7
src/components/pages/AssessmentDetailsPage/AssessmentDetailsPage.tsx

@@ -514,14 +514,10 @@ class AssessmentDetailsPage extends React.Component<Props, State> {
 )}
 )}
           contentHeaderComponent={(
           contentHeaderComponent={(
             <DetailsContentHeader
             <DetailsContentHeader
-              item={
-              {
-                ...details,
-                type: 'Azure Migrate',
-                status,
-              }
-            }
+              statusPill={status}
               statusLabel={statusLabel}
               statusLabel={statusLabel}
+              itemTitle={details?.name}
+              itemType="Azure Migrate"
               backLink="/planning"
               backLink="/planning"
               typeImage={assessmentImage}
               typeImage={assessmentImage}
             />
             />

+ 6 - 4
src/components/pages/EndpointDetailsPage/EndpointDetailsPage.tsx

@@ -33,12 +33,12 @@ import userStore from '../../../stores/UserStore'
 import projectStore from '../../../stores/ProjectStore'
 import projectStore from '../../../stores/ProjectStore'
 
 
 import type { Endpoint as EndpointType } from '../../../@types/Endpoint'
 import type { Endpoint as EndpointType } from '../../../@types/Endpoint'
-import type { MainItem } from '../../../@types/MainItem'
 
 
 import Palette from '../../styleUtils/Palette'
 import Palette from '../../styleUtils/Palette'
 
 
 import endpointImage from './images/endpoint.svg'
 import endpointImage from './images/endpoint.svg'
 import regionStore from '../../../stores/RegionStore'
 import regionStore from '../../../stores/RegionStore'
+import { MigrationItem, ReplicaItem } from '../../../@types/MainItem'
 
 
 const Wrapper = styled.div<any>``
 const Wrapper = styled.div<any>``
 
 
@@ -52,7 +52,7 @@ type State = {
   showEndpointModal: boolean,
   showEndpointModal: boolean,
   showEndpointInUseModal: boolean,
   showEndpointInUseModal: boolean,
   showEndpointInUseLoadingModal: boolean,
   showEndpointInUseLoadingModal: boolean,
-  endpointUsage: { replicas: MainItem[], migrations: MainItem[] },
+  endpointUsage: { replicas: ReplicaItem[], migrations: MigrationItem[] },
   showDuplicateModal: boolean,
   showDuplicateModal: boolean,
   duplicating: boolean,
   duplicating: boolean,
 }
 }
@@ -83,7 +83,7 @@ class EndpointDetailsPage extends React.Component<Props, State> {
     return endpointStore.endpoints.find(e => e.id === this.props.match.params.id) || null
     return endpointStore.endpoints.find(e => e.id === this.props.match.params.id) || null
   }
   }
 
 
-  getEndpointUsage(): { migrations: MainItem[], replicas: MainItem[] } {
+  getEndpointUsage(): { migrations: MigrationItem[], replicas: ReplicaItem[] } {
     const endpointId = this.props.match.params.id
     const endpointId = this.props.match.params.id
     const replicas = replicaStore.replicas.filter(
     const replicas = replicaStore.replicas.filter(
       r => r.origin_endpoint_id === endpointId || r.destination_endpoint_id === endpointId,
       r => r.origin_endpoint_id === endpointId || r.destination_endpoint_id === endpointId,
@@ -246,7 +246,9 @@ class EndpointDetailsPage extends React.Component<Props, State> {
 )}
 )}
           contentHeaderComponent={(
           contentHeaderComponent={(
             <DetailsContentHeader
             <DetailsContentHeader
-              item={endpoint}
+              itemTitle={endpoint?.name}
+              itemType="endpoint"
+              itemDescription={endpoint?.description}
               backLink="/endpoints"
               backLink="/endpoints"
               dropdownActions={dropdownActions}
               dropdownActions={dropdownActions}
               typeImage={endpointImage}
               typeImage={endpointImage}

+ 1 - 1
src/components/pages/EndpointsPage/EndpointsPage.tsx

@@ -127,7 +127,7 @@ class EndpointsPage extends React.Component<{ history: any }, State> {
   }
   }
 
 
   handleItemClick(item: EndpointType) {
   handleItemClick(item: EndpointType) {
-    this.props.history.push(`/endpoint/${item.id}`)
+    this.props.history.push(`/endpoints/${item.id}`)
   }
   }
 
 
   async duplicate(projectId: string) {
   async duplicate(projectId: string) {

+ 19 - 4
src/components/pages/MigrationDetailsPage/MigrationDetailsPage.tsx

@@ -135,7 +135,7 @@ class MigrationDetailsPage extends React.Component<Props, State> {
   }
   }
 
 
   getStatus() {
   getStatus() {
-    return migrationStore.migrationDetails && migrationStore.migrationDetails.status
+    return migrationStore.migrationDetails?.last_execution_status
   }
   }
 
 
   async loadMigrationWithInstances(migrationId: string, cache: boolean) {
   async loadMigrationWithInstances(migrationId: string, cache: boolean) {
@@ -237,7 +237,7 @@ class MigrationDetailsPage extends React.Component<Props, State> {
 
 
   async migrate(replicaId: string, options: Field[], userScripts: InstanceScript[]) {
   async migrate(replicaId: string, options: Field[], userScripts: InstanceScript[]) {
     const migration = await migrationStore.migrateReplica(replicaId, options, userScripts)
     const migration = await migrationStore.migrateReplica(replicaId, options, userScripts)
-    this.props.history.push(`/migration/tasks/${migration.id}`)
+    this.props.history.push(`/migrations/${migration.id}/tasks`)
   }
   }
 
 
   async pollData() {
   async pollData() {
@@ -319,7 +319,19 @@ class MigrationDetailsPage extends React.Component<Props, State> {
         action: () => { this.handleDeleteMigrationClick() },
         action: () => { this.handleDeleteMigrationClick() },
       },
       },
     ]
     ]
-
+    let title = null
+    const migration = migrationStore.migrationDetails
+    if (migration) {
+      const { instances } = migration
+      if (instances) {
+        title = instances[0]
+        if (instances.length > 1) {
+          title += ` (+${instances.length - 1} more)`
+        }
+      } else {
+        title = migration.name
+      }
+    }
     return (
     return (
       <Wrapper>
       <Wrapper>
         <DetailsTemplate
         <DetailsTemplate
@@ -331,7 +343,10 @@ class MigrationDetailsPage extends React.Component<Props, State> {
 )}
 )}
           contentHeaderComponent={(
           contentHeaderComponent={(
             <DetailsContentHeader
             <DetailsContentHeader
-              item={migrationStore.migrationDetails}
+              statusPill={migrationStore.migrationDetails?.last_execution_status}
+              itemTitle={title}
+              itemType="migration"
+              itemDescription={migrationStore.migrationDetails?.description}
               backLink="/migrations"
               backLink="/migrations"
               typeImage={migrationImage}
               typeImage={migrationImage}
               dropdownActions={dropdownActions}
               dropdownActions={dropdownActions}

+ 20 - 12
src/components/pages/MigrationsPage/MigrationsPage.tsx

@@ -22,7 +22,6 @@ 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 type { MainItem } from '../../../@types/MainItem'
 
 
 import migrationItemImage from './images/migration.svg'
 import migrationItemImage from './images/migration.svg'
 import migrationLargeImage from './images/migration-large.svg'
 import migrationLargeImage from './images/migration-large.svg'
@@ -35,11 +34,13 @@ import configLoader from '../../../utils/Config'
 
 
 import Palette from '../../styleUtils/Palette'
 import Palette from '../../styleUtils/Palette'
 import replicaMigrationFields from '../../organisms/ReplicaMigrationOptions/replicaMigrationFields'
 import replicaMigrationFields from '../../organisms/ReplicaMigrationOptions/replicaMigrationFields'
+import { MigrationItem } from '../../../@types/MainItem'
+import userStore from '../../../stores/UserStore'
 
 
 const Wrapper = styled.div<any>``
 const Wrapper = styled.div<any>``
 
 
 type State = {
 type State = {
-  selectedMigrations: MainItem[],
+  selectedMigrations: MigrationItem[],
   modalIsOpen: boolean,
   modalIsOpen: boolean,
   showDeleteMigrationModal: boolean,
   showDeleteMigrationModal: boolean,
   showCancelMigrationModal: boolean,
   showCancelMigrationModal: boolean,
@@ -64,6 +65,10 @@ class MigrationsPage extends React.Component<{ history: any }, State> {
 
 
     projectStore.getProjects()
     projectStore.getProjects()
     endpointStore.getEndpoints({ showLoading: true })
     endpointStore.getEndpoints({ showLoading: true })
+    userStore.getAllUsers({
+      showLoading: userStore.users.length === 0,
+      quietError: true,
+    })
 
 
     this.stopPolling = false
     this.stopPolling = false
     this.pollData()
     this.pollData()
@@ -90,7 +95,7 @@ class MigrationsPage extends React.Component<{ history: any }, State> {
 
 
   getStatus(migrationId: string): string {
   getStatus(migrationId: string): string {
     const migration = migrationStore.migrations.find(m => m.id === migrationId)
     const migration = migrationStore.migrations.find(m => m.id === migrationId)
-    return migration ? migration.status : ''
+    return migration ? migration.last_execution_status : ''
   }
   }
 
 
   handleProjectChange() {
   handleProjectChange() {
@@ -102,13 +107,14 @@ class MigrationsPage extends React.Component<{ history: any }, State> {
     projectStore.getProjects()
     projectStore.getProjects()
     endpointStore.getEndpoints({ showLoading: true })
     endpointStore.getEndpoints({ showLoading: true })
     migrationStore.getMigrations({ showLoading: true })
     migrationStore.getMigrations({ showLoading: true })
+    userStore.getAllUsers({ showLoading: true, quietError: true })
   }
   }
 
 
-  handleItemClick(item: MainItem) {
-    if (item.status === 'RUNNING') {
-      this.props.history.push(`/migration/tasks/${item.id}`)
+  handleItemClick(item: MigrationItem) {
+    if (item.last_execution_status === 'RUNNING') {
+      this.props.history.push(`/migrations/${item.id}/tasks`)
     } else {
     } else {
-      this.props.history.push(`/migration/${item.id}`)
+      this.props.history.push(`/migrations/${item.id}`)
     }
     }
   }
   }
 
 
@@ -137,7 +143,7 @@ class MigrationsPage extends React.Component<{ history: any }, State> {
       if (migration.replica_id) {
       if (migration.replica_id) {
         await migrationStore.migrateReplica(migration.replica_id, replicaMigrationFields, [])
         await migrationStore.migrateReplica(migration.replica_id, replicaMigrationFields, [])
       } else {
       } else {
-        await migrationStore.recreateFullCopy(migration)
+        await migrationStore.recreateFullCopy(migration as any)
       }
       }
     }))
     }))
 
 
@@ -158,7 +164,7 @@ class MigrationsPage extends React.Component<{ history: any }, State> {
     })
     })
   }
   }
 
 
-  searchText(item: MainItem, text?: string) {
+  searchText(item: MigrationItem, text?: string) {
     let result = false
     let result = false
     if (item.instances[0].toLowerCase().indexOf(text || '') > -1) {
     if (item.instances[0].toLowerCase().indexOf(text || '') > -1) {
       return true
       return true
@@ -175,8 +181,8 @@ class MigrationsPage extends React.Component<{ history: any }, State> {
     return result
     return result
   }
   }
 
 
-  itemFilterFunction(item: MainItem, filterStatus?: string | null, filterText?: string) {
-    if ((filterStatus !== 'all' && (item.status !== filterStatus))
+  itemFilterFunction(item: MigrationItem, filterStatus?: string | null, filterText?: string) {
+    if ((filterStatus !== 'all' && (item.last_execution_status !== filterStatus))
       || !this.searchText(item, filterText)
       || !this.searchText(item, filterText)
     ) {
     ) {
       return false
       return false
@@ -193,6 +199,7 @@ class MigrationsPage extends React.Component<{ history: any }, State> {
     await Promise.all([
     await Promise.all([
       migrationStore.getMigrations({ skipLog: true }),
       migrationStore.getMigrations({ skipLog: true }),
       endpointStore.getEndpoints({ skipLog: true }),
       endpointStore.getEndpoints({ skipLog: true }),
+      userStore.getAllUsers({ skipLog: true, quietError: true }),
     ])
     ])
     this.pollTimeout = setTimeout(() => { this.pollData() }, configLoader.config.requestPollTimeout)
     this.pollTimeout = setTimeout(() => { this.pollData() }, configLoader.config.requestPollTimeout)
   }
   }
@@ -252,7 +259,8 @@ class MigrationsPage extends React.Component<{ history: any }, State> {
                     }
                     }
                     return 'Not Found'
                     return 'Not Found'
                   }}
                   }}
-                  useTasksRemaining
+                  getUserName={id => userStore.users.find(u => u.id === id)?.name}
+                  userNameLoading={userStore.allUsersLoading}
                 />
                 />
               )}
               )}
               emptyListImage={migrationLargeImage}
               emptyListImage={migrationLargeImage}

+ 2 - 1
src/components/pages/ProjectDetailsPage/ProjectDetailsPage.tsx

@@ -193,7 +193,8 @@ class ProjectDetailsPage extends React.Component<Props, State> {
 )}
 )}
           contentHeaderComponent={(
           contentHeaderComponent={(
             <DetailsContentHeader
             <DetailsContentHeader
-              item={{ ...projectStore.projectDetails, description: '' }}
+              itemTitle={projectStore.projectDetails?.name}
+              itemType="project"
               backLink="/projects"
               backLink="/projects"
               dropdownActions={dropdownActions}
               dropdownActions={dropdownActions}
               typeImage={projectImage}
               typeImage={projectImage}

+ 1 - 1
src/components/pages/ProjectsPage/ProjectsPage.tsx

@@ -118,7 +118,7 @@ class ProjectsPage extends React.Component<{ history: any }, State> {
               selectionLabel="user"
               selectionLabel="user"
               loading={projectStore.loading}
               loading={projectStore.loading}
               items={projectStore.projects}
               items={projectStore.projects}
-              onItemClick={(user: Project) => { this.props.history.push(`project/${user.id}`) }}
+              onItemClick={(user: Project) => { this.props.history.push(`/projects/${user.id}`) }}
               onReloadButtonClick={() => { this.handleReloadButtonClick() }}
               onReloadButtonClick={() => { this.handleReloadButtonClick() }}
               itemFilterFunction={(...args) => this.itemFilterFunction(...args)}
               itemFilterFunction={(...args) => this.itemFilterFunction(...args)}
               renderItemComponent={component => (
               renderItemComponent={component => (

+ 64 - 28
src/components/pages/ReplicaDetailsPage/ReplicaDetailsPage.tsx

@@ -27,7 +27,6 @@ import EditReplica from '../../organisms/EditReplica'
 import ReplicaMigrationOptions from '../../organisms/ReplicaMigrationOptions'
 import ReplicaMigrationOptions from '../../organisms/ReplicaMigrationOptions'
 import DeleteReplicaModal from '../../molecules/DeleteReplicaModal'
 import DeleteReplicaModal from '../../molecules/DeleteReplicaModal'
 
 
-import type { MainItem } from '../../../@types/MainItem'
 import type { InstanceScript } from '../../../@types/Instance'
 import type { InstanceScript } from '../../../@types/Instance'
 import type { Execution } from '../../../@types/Execution'
 import type { Execution } from '../../../@types/Execution'
 import type { Schedule } from '../../../@types/Schedule'
 import type { Schedule } from '../../../@types/Schedule'
@@ -45,11 +44,12 @@ import notificationStore from '../../../stores/NotificationStore'
 import providerStore from '../../../stores/ProviderStore'
 import providerStore from '../../../stores/ProviderStore'
 
 
 import configLoader from '../../../utils/Config'
 import configLoader from '../../../utils/Config'
-import utils from '../../../utils/ObjectUtils'
 import { providerTypes } from '../../../constants'
 import { providerTypes } from '../../../constants'
 
 
 import replicaImage from './images/replica.svg'
 import replicaImage from './images/replica.svg'
 import Palette from '../../styleUtils/Palette'
 import Palette from '../../styleUtils/Palette'
+import { ReplicaItemDetails } from '../../../@types/MainItem'
+import ObjectUtils from '../../../utils/ObjectUtils'
 
 
 const Wrapper = styled.div<any>``
 const Wrapper = styled.div<any>``
 
 
@@ -65,7 +65,7 @@ type State = {
   showForceCancelConfirmation: boolean,
   showForceCancelConfirmation: boolean,
   showDeleteReplicaConfirmation: boolean,
   showDeleteReplicaConfirmation: boolean,
   showDeleteReplicaDisksConfirmation: boolean,
   showDeleteReplicaDisksConfirmation: boolean,
-  confirmationItem: MainItem | null | Execution | null,
+  confirmationItem?: ReplicaItemDetails | null | Execution | null,
   showCancelConfirmation: boolean,
   showCancelConfirmation: boolean,
   isEditable: boolean,
   isEditable: boolean,
   pausePolling: boolean,
   pausePolling: boolean,
@@ -88,12 +88,12 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
 
 
   stopPolling: boolean | null = null
   stopPolling: boolean | null = null
 
 
-  UNSAFE_componentWillMount() {
+  componentDidMount() {
     document.title = 'Replica Details'
     document.title = 'Replica Details'
 
 
     const loadReplica = async () => {
     const loadReplica = async () => {
       await endpointStore.getEndpoints({ showLoading: true })
       await endpointStore.getEndpoints({ showLoading: true })
-      const replica = await this.loadReplicaWithInstances(this.replicaId, true)
+      const replica = await this.loadReplicaWithInstances(true)
       if (!replica) {
       if (!replica) {
         return
         return
       }
       }
@@ -141,7 +141,7 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
 
 
   UNSAFE_componentWillReceiveProps(newProps: Props) {
   UNSAFE_componentWillReceiveProps(newProps: Props) {
     if (newProps.match.params.id !== this.props.match.params.id) {
     if (newProps.match.params.id !== this.props.match.params.id) {
-      this.loadReplicaWithInstances(newProps.match.params.id, true)
+      this.loadReplicaWithInstances(true, newProps.match.params.id)
       scheduleStore.getSchedules(newProps.match.params.id)
       scheduleStore.getSchedules(newProps.match.params.id)
     }
     }
   }
   }
@@ -159,8 +159,7 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
   }
   }
 
 
   get replica() {
   get replica() {
-    const replica = replicaStore.replicas.find(r => r.id === this.replicaId)
-    return replica
+    return replicaStore.replicaDetails
   }
   }
 
 
   getLastExecution() {
   getLastExecution() {
@@ -176,11 +175,11 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
     return lastExecution && lastExecution.status
     return lastExecution && lastExecution.status
   }
   }
 
 
-  async loadIsEditable(replicaDetails: MainItem) {
+  async loadIsEditable(replicaDetails: ReplicaItemDetails) {
     const targetEndpointId = replicaDetails.destination_endpoint_id
     const targetEndpointId = replicaDetails.destination_endpoint_id
     const sourceEndpointId = replicaDetails.origin_endpoint_id
     const sourceEndpointId = replicaDetails.origin_endpoint_id
     await providerStore.loadProviders()
     await providerStore.loadProviders()
-    await utils.waitFor(() => endpointStore.endpoints.length > 0)
+    await ObjectUtils.waitFor(() => endpointStore.endpoints.length > 0)
     const sourceEndpoint = endpointStore.endpoints.find(e => e.id === sourceEndpointId)
     const sourceEndpoint = endpointStore.endpoints.find(e => e.id === sourceEndpointId)
     const targetEndpoint = endpointStore.endpoints.find(e => e.id === targetEndpointId)
     const targetEndpoint = endpointStore.endpoints.find(e => e.id === targetEndpointId)
     if (!sourceEndpoint || !targetEndpoint || !providerStore.providers) {
     if (!sourceEndpoint || !targetEndpoint || !providerStore.providers) {
@@ -196,8 +195,8 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
     this.setState({ isEditable })
     this.setState({ isEditable })
   }
   }
 
 
-  async loadReplicaWithInstances(_: string, cache: boolean) {
-    await replicaStore.getReplicas({ showLoading: true })
+  async loadReplicaWithInstances(cache: boolean, replicaId?: string) {
+    await replicaStore.getReplicaDetails({ replicaId: replicaId || this.replicaId })
     const replica = this.replica
     const replica = this.replica
     if (!replica) {
     if (!replica) {
       return null
       return null
@@ -259,7 +258,7 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
     this.handleCloseExecutionConfirmation()
     this.handleCloseExecutionConfirmation()
   }
   }
 
 
-  handleDeleteExecutionClick(execution: Execution | null) {
+  handleDeleteExecutionClick(execution?: Execution | null) {
     this.setState({
     this.setState({
       showDeleteExecutionConfirmation: true,
       showDeleteExecutionConfirmation: true,
       confirmationItem: execution,
       confirmationItem: execution,
@@ -304,7 +303,7 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
       return
       return
     }
     }
     replicaStore.deleteDisks(replica.id)
     replicaStore.deleteDisks(replica.id)
-    this.props.history.push(`/replica/executions/${replica.id}`)
+    this.props.history.push(`/replicas/${replica.id}/executions`)
   }
   }
 
 
   handleCloseDeleteReplicaDisksConfirmation() {
   handleCloseDeleteReplicaDisksConfirmation() {
@@ -354,7 +353,7 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
     this.handleCancelExecution(this.getLastExecution(), force)
     this.handleCancelExecution(this.getLastExecution(), force)
   }
   }
 
 
-  handleCancelExecution(confirmationItem: Execution | null, force?: boolean | null) {
+  handleCancelExecution(confirmationItem?: Execution | null, force?: boolean | null) {
     if (force) {
     if (force) {
       this.setState({ confirmationItem, showForceCancelConfirmation: true })
       this.setState({ confirmationItem, showForceCancelConfirmation: true })
     } else {
     } else {
@@ -374,11 +373,11 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
     if (!this.state.confirmationItem || !replica) {
     if (!this.state.confirmationItem || !replica) {
       return
       return
     }
     }
-    replicaStore.cancelExecution(
-      replica.id,
-      this.state.confirmationItem.id,
+    replicaStore.cancelExecution({
+      replicaId: replica.id,
+      executionId: this.state.confirmationItem.id,
       force,
       force,
-    )
+    })
     this.setState({
     this.setState({
       showForceCancelConfirmation: false,
       showForceCancelConfirmation: false,
       showCancelConfirmation: false,
       showCancelConfirmation: false,
@@ -404,7 +403,7 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
       action: {
       action: {
         label: 'View Migration Status',
         label: 'View Migration Status',
         callback: () => {
         callback: () => {
-          this.props.history.push(`/migration/tasks/${migration.id}`)
+          this.props.history.push(`/migrations/${migration.id}/tasks/`)
         },
         },
       },
       },
     })
     })
@@ -417,7 +416,7 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
     }
     }
     replicaStore.execute(replica.id, fields)
     replicaStore.execute(replica.id, fields)
     this.handleCloseOptionsModal()
     this.handleCloseOptionsModal()
-    this.props.history.push(`/replica/executions/${replica.id}`)
+    this.props.history.push(`/replicas/${replica.id}/executions`)
   }
   }
 
 
   async pollData(showLoading: boolean) {
   async pollData(showLoading: boolean) {
@@ -425,7 +424,16 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
       return
       return
     }
     }
 
 
-    await replicaStore.getReplicas({ showLoading, skipLog: true })
+    await Promise.all([
+      replicaStore.getReplicaDetails({
+        replicaId: this.replicaId, showLoading, polling: true,
+      }),
+      (async () => {
+        if (window.location.pathname.indexOf('executions') > -1) {
+          await replicaStore.getExecutionTasks({ replicaId: this.replicaId, polling: true })
+        }
+      })(),
+    ])
 
 
     setTimeout(() => { this.pollData(false) }, configLoader.config.requestPollTimeout)
     setTimeout(() => { this.pollData(false) }, configLoader.config.requestPollTimeout)
   }
   }
@@ -437,7 +445,7 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
   }
   }
 
 
   handleEditReplicaReload() {
   handleEditReplicaReload() {
-    this.loadReplicaWithInstances(this.replicaId, false)
+    this.loadReplicaWithInstances(false)
   }
   }
 
 
   handleUpdateComplete(redirectTo: string) {
   handleUpdateComplete(redirectTo: string) {
@@ -445,6 +453,14 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
     this.closeEditModal()
     this.closeEditModal()
   }
   }
 
 
+  async handleExecutionChange(executionId: string) {
+    await ObjectUtils.waitFor(() => Boolean(replicaStore.replicaDetails))
+    if (!replicaStore.replicaDetails?.id) {
+      return
+    }
+    replicaStore.getExecutionTasks({ replicaId: replicaStore.replicaDetails.id, executionId })
+  }
+
   renderEditReplica() {
   renderEditReplica() {
     const replica = this.replica
     const replica = this.replica
     if (!replica) {
     if (!replica) {
@@ -516,7 +532,18 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
       },
       },
     ]
     ]
     const replica = this.replica
     const replica = this.replica
-
+    let title = null
+    if (replica) {
+      const { instances } = replica
+      if (instances) {
+        title = instances[0]
+        if (instances.length > 1) {
+          title += ` (+${instances.length - 1} more)`
+        }
+      } else {
+        title = replica.name
+      }
+    }
     return (
     return (
       <Wrapper>
       <Wrapper>
         <DetailsTemplate
         <DetailsTemplate
@@ -528,7 +555,10 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
 )}
 )}
           contentHeaderComponent={(
           contentHeaderComponent={(
             <DetailsContentHeader
             <DetailsContentHeader
-              item={replica}
+              statusPill={replica?.last_execution_status}
+              itemTitle={title}
+              itemType="replica"
+              itemDescription={replica?.description}
               dropdownActions={dropdownActions}
               dropdownActions={dropdownActions}
               backLink="/replicas"
               backLink="/replicas"
               typeImage={replicaImage}
               typeImage={replicaImage}
@@ -543,7 +573,7 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
               endpoints={endpointStore.endpoints}
               endpoints={endpointStore.endpoints}
               scheduleStore={scheduleStore}
               scheduleStore={scheduleStore}
               networks={networkStore.networks}
               networks={networkStore.networks}
-              detailsLoading={replicaStore.loading || endpointStore.loading}
+              detailsLoading={replicaStore.replicaDetailsLoading || endpointStore.loading}
               sourceSchema={providerStore.sourceSchema}
               sourceSchema={providerStore.sourceSchema}
               sourceSchemaLoading={providerStore.sourceSchemaLoading
               sourceSchemaLoading={providerStore.sourceSchemaLoading
               || providerStore.sourceOptionsPrimaryLoading
               || providerStore.sourceOptionsPrimaryLoading
@@ -552,7 +582,13 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
               destinationSchemaLoading={providerStore.destinationSchemaLoading
               destinationSchemaLoading={providerStore.destinationSchemaLoading
               || providerStore.destinationOptionsPrimaryLoading
               || providerStore.destinationOptionsPrimaryLoading
               || providerStore.destinationOptionsSecondaryLoading}
               || providerStore.destinationOptionsSecondaryLoading}
-              executionsLoading={replicaStore.startingExecution}
+              executionsLoading={replicaStore.startingExecution
+                || replicaStore.replicaDetailsLoading}
+              onExecutionChange={id => { this.handleExecutionChange(id) }}
+              executions={replicaStore.replicaDetails?.executions || []}
+              executionsTasksLoading={replicaStore.executionsTasksLoading
+                || replicaStore.replicaDetailsLoading || replicaStore.startingExecution}
+              executionsTasks={replicaStore.executionsTasks}
               page={this.props.match.params.page || ''}
               page={this.props.match.params.page || ''}
               onCancelExecutionClick={(e, f) => { this.handleCancelExecution(e, f) }}
               onCancelExecutionClick={(e, f) => { this.handleCancelExecution(e, f) }}
               onDeleteExecutionClick={execution => { this.handleDeleteExecutionClick(execution) }}
               onDeleteExecutionClick={execution => { this.handleDeleteExecutionClick(execution) }}
@@ -602,7 +638,7 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
         />
         />
         {this.state.showDeleteReplicaConfirmation ? (
         {this.state.showDeleteReplicaConfirmation ? (
           <DeleteReplicaModal
           <DeleteReplicaModal
-            hasDisks={replicaStore.hasReplicaDisks(this.replica)}
+            hasDisks={replicaStore.testReplicaHasDisks(this.replica)}
             onRequestClose={() => this.handleCloseDeleteReplicaConfirmation()}
             onRequestClose={() => this.handleCloseDeleteReplicaConfirmation()}
             onDeleteReplica={() => { this.handleDeleteReplicaConfirmation() }}
             onDeleteReplica={() => { this.handleDeleteReplicaConfirmation() }}
             onDeleteDisks={() => { this.handleDeleteReplicaDisksConfirmation() }}
             onDeleteDisks={() => { this.handleDeleteReplicaDisksConfirmation() }}

+ 35 - 39
src/components/pages/ReplicasPage/ReplicasPage.tsx

@@ -27,7 +27,6 @@ import ReplicaExecutionOptions from '../../organisms/ReplicaExecutionOptions'
 import ReplicaMigrationOptions from '../../organisms/ReplicaMigrationOptions'
 import ReplicaMigrationOptions from '../../organisms/ReplicaMigrationOptions'
 import DeleteReplicaModal from '../../molecules/DeleteReplicaModal'
 import DeleteReplicaModal from '../../molecules/DeleteReplicaModal'
 
 
-import type { MainItem } from '../../../@types/MainItem'
 import type { Action as DropdownAction } from '../../molecules/ActionDropdown'
 import type { Action as DropdownAction } from '../../molecules/ActionDropdown'
 import type { Field } from '../../../@types/Field'
 import type { Field } from '../../../@types/Field'
 import type { InstanceScript } from '../../../@types/Instance'
 import type { InstanceScript } from '../../../@types/Instance'
@@ -45,6 +44,8 @@ import notificationStore from '../../../stores/NotificationStore'
 
 
 import Palette from '../../styleUtils/Palette'
 import Palette from '../../styleUtils/Palette'
 import configLoader from '../../../utils/Config'
 import configLoader from '../../../utils/Config'
+import { ReplicaItem } from '../../../@types/MainItem'
+import userStore from '../../../stores/UserStore'
 
 
 const Wrapper = styled.div<any>``
 const Wrapper = styled.div<any>``
 
 
@@ -52,7 +53,7 @@ const SCHEDULE_POLL_TIMEOUT = 10000
 
 
 type State = {
 type State = {
   modalIsOpen: boolean,
   modalIsOpen: boolean,
-  selectedReplicas: MainItem[],
+  selectedReplicas: ReplicaItem[],
   showCancelExecutionModal: boolean,
   showCancelExecutionModal: boolean,
   showExecutionOptionsModal: boolean,
   showExecutionOptionsModal: boolean,
   showCreateMigrationsModal: boolean,
   showCreateMigrationsModal: boolean,
@@ -84,6 +85,10 @@ class ReplicasPage extends React.Component<{ history: any }, State> {
 
 
     projectStore.getProjects()
     projectStore.getProjects()
     endpointStore.getEndpoints({ showLoading: true })
     endpointStore.getEndpoints({ showLoading: true })
+    userStore.getAllUsers({
+      showLoading: userStore.users.length === 0,
+      quietError: true,
+    })
 
 
     this.stopPolling = false
     this.stopPolling = false
     this.pollData()
     this.pollData()
@@ -108,22 +113,8 @@ class ReplicasPage extends React.Component<{ history: any }, State> {
     ]
     ]
   }
   }
 
 
-  getLastExecution(item: MainItem) {
-    const lastExecution = item.executions && item.executions.length
-      ? item.executions[item.executions.length - 1] : null
-
-    return lastExecution
-  }
-
-  getStatus(replica?: MainItem | null): string {
-    if (!replica) {
-      return ''
-    }
-    const usableReplica = replica
-    if (usableReplica.executions && usableReplica.executions.length) {
-      return usableReplica.executions[usableReplica.executions.length - 1].status
-    }
-    return ''
+  getStatus(replica?: ReplicaItem | null): string {
+    return replica?.last_execution_status || ''
   }
   }
 
 
   handleProjectChange() {
   handleProjectChange() {
@@ -135,14 +126,14 @@ class ReplicasPage extends React.Component<{ history: any }, State> {
     projectStore.getProjects()
     projectStore.getProjects()
     replicaStore.getReplicas({ showLoading: true })
     replicaStore.getReplicas({ showLoading: true })
     endpointStore.getEndpoints({ showLoading: true })
     endpointStore.getEndpoints({ showLoading: true })
+    userStore.getAllUsers({ showLoading: true, quietError: true })
   }
   }
 
 
-  handleItemClick(item: MainItem) {
-    const lastExecution = this.getLastExecution(item)
-    if (lastExecution && lastExecution.status === 'RUNNING') {
-      this.props.history.push(`/replica/executions/${item.id}`)
+  handleItemClick(item: ReplicaItem) {
+    if (item.last_execution_status === 'RUNNING') {
+      this.props.history.push(`/replicas/${item.id}/executions`)
     } else {
     } else {
-      this.props.history.push(`/replica/${item.id}`)
+      this.props.history.push(`/replicas/${item.id}`)
     }
     }
   }
   }
 
 
@@ -175,7 +166,12 @@ class ReplicasPage extends React.Component<{ history: any }, State> {
     this.props.history.push('/migrations')
     this.props.history.push('/migrations')
   }
   }
 
 
-  deleteReplicasDisks(replicas: MainItem[]) {
+  handleShowDeleteReplicas() {
+    replicaStore.loadHaveReplicasDisks(this.state.selectedReplicas)
+    this.setState({ showDeleteReplicasModal: true })
+  }
+
+  deleteReplicasDisks(replicas: ReplicaItem[]) {
     replicas.forEach(replica => {
     replicas.forEach(replica => {
       replicaStore.deleteDisks(replica.id)
       replicaStore.deleteDisks(replica.id)
     })
     })
@@ -186,16 +182,14 @@ class ReplicasPage extends React.Component<{ history: any }, State> {
   cancelExecutions() {
   cancelExecutions() {
     this.state.selectedReplicas.forEach(replica => {
     this.state.selectedReplicas.forEach(replica => {
       const actualReplica = replicaStore.replicas.find(r => r.id === replica.id)
       const actualReplica = replicaStore.replicas.find(r => r.id === replica.id)
-      const lastExecution = actualReplica
-        && actualReplica.executions[actualReplica.executions.length - 1]
-      if (actualReplica && lastExecution && lastExecution.status === 'RUNNING') {
-        replicaStore.cancelExecution(replica.id, lastExecution.id)
+      if (actualReplica?.last_execution_status === 'RUNNING') {
+        replicaStore.cancelExecution({ replicaId: replica.id })
       }
       }
     })
     })
     this.setState({ showCancelExecutionModal: false })
     this.setState({ showCancelExecutionModal: false })
   }
   }
 
 
-  isExecuteEnabled(replica?: MainItem | null): boolean {
+  isExecuteEnabled(replica?: ReplicaItem | null): boolean {
     if (!replica) {
     if (!replica) {
       return false
       return false
     }
     }
@@ -244,7 +238,9 @@ class ReplicasPage extends React.Component<{ history: any }, State> {
     }
     }
 
 
     await Promise.all([
     await Promise.all([
-      replicaStore.getReplicas({ skipLog: true }), endpointStore.getEndpoints({ skipLog: true }),
+      replicaStore.getReplicas({ skipLog: true }),
+      endpointStore.getEndpoints({ skipLog: true }),
+      userStore.getAllUsers({ skipLog: true, quietError: true }),
     ])
     ])
     if (!this.schedulePolling) {
     if (!this.schedulePolling) {
       this.pollSchedule()
       this.pollSchedule()
@@ -263,7 +259,7 @@ class ReplicasPage extends React.Component<{ history: any }, State> {
     }, SCHEDULE_POLL_TIMEOUT)
     }, SCHEDULE_POLL_TIMEOUT)
   }
   }
 
 
-  searchText(item: MainItem, text?: string | null) {
+  searchText(item: ReplicaItem, text?: string | null) {
     let result = false
     let result = false
     if (item.instances[0].toLowerCase().indexOf(text || '') > -1) {
     if (item.instances[0].toLowerCase().indexOf(text || '') > -1) {
       return true
       return true
@@ -280,9 +276,8 @@ class ReplicasPage extends React.Component<{ history: any }, State> {
     return result
     return result
   }
   }
 
 
-  itemFilterFunction(item: MainItem, filterStatus?: string | null, filterText?: string) {
-    const lastExecution = this.getLastExecution(item)
-    if ((filterStatus !== 'all' && (!lastExecution || lastExecution.status !== filterStatus))
+  itemFilterFunction(item: ReplicaItem, filterStatus?: string | null, filterText?: string) {
+    if ((filterStatus !== 'all' && item.last_execution_status !== filterStatus)
       || !this.searchText(item, filterText)
       || !this.searchText(item, filterText)
     ) {
     ) {
       return false
       return false
@@ -327,7 +322,7 @@ class ReplicasPage extends React.Component<{ history: any }, State> {
     }, {
     }, {
       label: 'Delete Replicas',
       label: 'Delete Replicas',
       color: Palette.alert,
       color: Palette.alert,
-      action: () => { this.setState({ showDeleteReplicasModal: true }) },
+      action: () => { this.handleShowDeleteReplicas() },
     }]
     }]
 
 
     return (
     return (
@@ -360,6 +355,8 @@ class ReplicasPage extends React.Component<{ history: any }, State> {
                     }
                     }
                     return 'Not Found'
                     return 'Not Found'
                   }}
                   }}
+                  getUserName={id => userStore.users.find(u => u.id === id)?.name}
+                  userNameLoading={userStore.allUsersLoading}
                 />
                 />
               )}
               )}
               emptyListImage={replicaLargeImage}
               emptyListImage={replicaLargeImage}
@@ -380,14 +377,13 @@ class ReplicasPage extends React.Component<{ history: any }, State> {
         />
         />
         {this.state.showDeleteReplicasModal ? (
         {this.state.showDeleteReplicasModal ? (
           <DeleteReplicaModal
           <DeleteReplicaModal
-            hasDisks={replicaStore.getReplicasWithDisks(this.state.selectedReplicas).length > 0}
             isMultiReplicaSelection
             isMultiReplicaSelection
+            hasDisks={replicaStore.replicasWithDisks.length > 0}
+            loading={replicaStore.replicasWithDisksLoading}
             onRequestClose={() => { this.setState({ showDeleteReplicasModal: false }) }}
             onRequestClose={() => { this.setState({ showDeleteReplicasModal: false }) }}
             onDeleteReplica={() => { this.deleteSelectedReplicas() }}
             onDeleteReplica={() => { this.deleteSelectedReplicas() }}
             onDeleteDisks={() => {
             onDeleteDisks={() => {
-              this.deleteReplicasDisks(
-                replicaStore.getReplicasWithDisks(this.state.selectedReplicas),
-              )
+              this.deleteReplicasDisks(replicaStore.replicasWithDisks)
             }}
             }}
           />
           />
         ) : null}
         ) : null}

+ 2 - 1
src/components/pages/UserDetailsPage/UserDetailsPage.tsx

@@ -134,7 +134,8 @@ class UserDetailsPage extends React.Component<Props, State> {
 )}
 )}
           contentHeaderComponent={(
           contentHeaderComponent={(
             <DetailsContentHeader
             <DetailsContentHeader
-              item={{ ...userStore.userDetails, description: '' }}
+              itemTitle={userStore.userDetails?.name}
+              itemType="user"
               backLink="/users"
               backLink="/users"
               typeImage={userImage}
               typeImage={userImage}
               dropdownActions={dropdownActions}
               dropdownActions={dropdownActions}

+ 1 - 1
src/components/pages/UsersPage/UsersPage.tsx

@@ -121,7 +121,7 @@ class UsersPage extends React.Component<{ history: any }, State> {
               selectionLabel="user"
               selectionLabel="user"
               loading={userStore.allUsersLoading}
               loading={userStore.allUsersLoading}
               items={userStore.users}
               items={userStore.users}
-              onItemClick={(user: User) => { this.props.history.push(`/user/${user.id}`) }}
+              onItemClick={(user: User) => { this.props.history.push(`/users/${user.id}`) }}
               onReloadButtonClick={() => { this.handleReloadButtonClick() }}
               onReloadButtonClick={() => { this.handleReloadButtonClick() }}
               itemFilterFunction={(...args) => this.itemFilterFunction(...args)}
               itemFilterFunction={(...args) => this.itemFilterFunction(...args)}
               renderItemComponent={component => (
               renderItemComponent={component => (

+ 11 - 8
src/components/pages/WizardPage/WizardPage.tsx

@@ -35,7 +35,6 @@ import replicaStore from '../../../stores/ReplicaStore'
 import KeyboardManager from '../../../utils/KeyboardManager'
 import KeyboardManager from '../../../utils/KeyboardManager'
 import { wizardPages, executionOptions, providerTypes } from '../../../constants'
 import { wizardPages, executionOptions, providerTypes } from '../../../constants'
 
 
-import type { MainItem } from '../../../@types/MainItem'
 import type { Endpoint as EndpointType, StorageBackend } from '../../../@types/Endpoint'
 import type { Endpoint as EndpointType, StorageBackend } from '../../../@types/Endpoint'
 import type {
 import type {
   Instance, Nic, Disk, InstanceScript,
   Instance, Nic, Disk, InstanceScript,
@@ -46,6 +45,7 @@ import type { Schedule } from '../../../@types/Schedule'
 import type { WizardPage as WizardPageType } from '../../../@types/WizardData'
 import type { WizardPage as WizardPageType } from '../../../@types/WizardData'
 import ObjectUtils from '../../../utils/ObjectUtils'
 import ObjectUtils from '../../../utils/ObjectUtils'
 import { ProviderTypes } from '../../../@types/Providers'
 import { ProviderTypes } from '../../../@types/Providers'
+import { TransferItem, ReplicaItem } from '../../../@types/MainItem'
 
 
 const Wrapper = styled.div<any>``
 const Wrapper = styled.div<any>``
 
 
@@ -141,27 +141,30 @@ class WizardPage extends React.Component<Props, State> {
     this.handleBackClick()
     this.handleBackClick()
   }
   }
 
 
-  async handleCreationSuccess(items: MainItem[]) {
+  async handleCreationSuccess(items: TransferItem[]) {
     const typeLabel = this.state.type.charAt(0).toUpperCase() + this.state.type.substr(1)
     const typeLabel = this.state.type.charAt(0).toUpperCase() + this.state.type.substr(1)
     notificationStore.alert(`${typeLabel}${items.length > 1 ? 's' : ''} was succesfully created`, 'success')
     notificationStore.alert(`${typeLabel}${items.length > 1 ? 's' : ''} was succesfully created`, 'success')
     let schedulePromise = Promise.resolve()
     let schedulePromise = Promise.resolve()
 
 
     if (this.state.type === 'replica') {
     if (this.state.type === 'replica') {
       items.forEach(replica => {
       items.forEach(replica => {
+        if (replica.type !== 'replica') {
+          return
+        }
         this.executeCreatedReplica(replica)
         this.executeCreatedReplica(replica)
         schedulePromise = this.scheduleReplica(replica)
         schedulePromise = this.scheduleReplica(replica)
       })
       })
     }
     }
 
 
     if (items.length === 1) {
     if (items.length === 1) {
-      let location = `/${this.state.type}/`
+      let location = `/${this.state.type}s/${items[0].id}/`
       if (this.state.type === 'replica') {
       if (this.state.type === 'replica') {
-        location += 'executions/'
+        location += 'executions'
       } else {
       } else {
-        location += 'tasks/'
+        location += 'tasks'
       }
       }
       await schedulePromise
       await schedulePromise
-      this.props.history.push(location + items[0].id)
+      this.props.history.push(location)
     } else {
     } else {
       this.props.history.push(`/${this.state.type}s`)
       this.props.history.push(`/${this.state.type}s`)
     }
     }
@@ -590,7 +593,7 @@ class WizardPage extends React.Component<Props, State> {
     return state
     return state
   }
   }
 
 
-  scheduleReplica(replica: MainItem): Promise<void> {
+  scheduleReplica(replica: ReplicaItem): Promise<void> {
     if (wizardStore.schedules.length === 0) {
     if (wizardStore.schedules.length === 0) {
       return Promise.resolve()
       return Promise.resolve()
     }
     }
@@ -598,7 +601,7 @@ class WizardPage extends React.Component<Props, State> {
     return scheduleStore.scheduleMultiple(replica.id, wizardStore.schedules)
     return scheduleStore.scheduleMultiple(replica.id, wizardStore.schedules)
   }
   }
 
 
-  executeCreatedReplica(replica: MainItem) {
+  executeCreatedReplica(replica: ReplicaItem) {
     const options = wizardStore.data.destOptions
     const options = wizardStore.data.destOptions
     let executeNow = true
     let executeNow = true
     if (options && options.execute_now != null) {
     if (options && options.execute_now != null) {

+ 3 - 3
src/sources/AssessmentSource.ts

@@ -13,11 +13,11 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 */
 
 
 import type { MigrationInfo } from '../@types/Assessment'
 import type { MigrationInfo } from '../@types/Assessment'
-import type { MainItem } from '../@types/MainItem'
 import Api from '../utils/ApiCaller'
 import Api from '../utils/ApiCaller'
 import configLoader from '../utils/Config'
 import configLoader from '../utils/Config'
 import notificationStore from '../stores/NotificationStore'
 import notificationStore from '../stores/NotificationStore'
 import ObjectUtils from '../utils/ObjectUtils'
 import ObjectUtils from '../utils/ObjectUtils'
+import { MigrationItem } from '../@types/MainItem'
 
 
 class AssessmentSourceUtils {
 class AssessmentSourceUtils {
   static getNetworkMap(data: MigrationInfo) {
   static getNetworkMap(data: MigrationInfo) {
@@ -50,7 +50,7 @@ class AssessmentSourceUtils {
 }
 }
 
 
 class AssessmentSource {
 class AssessmentSource {
-  static migrate(data: MigrationInfo): Promise<MainItem> {
+  static migrate(data: MigrationInfo): Promise<MigrationItem> {
     const type = data.fieldValues.use_replica ? 'replica' : 'migration'
     const type = data.fieldValues.use_replica ? 'replica' : 'migration'
     const payload: any = {}
     const payload: any = {}
     payload[type] = {
     payload[type] = {
@@ -73,7 +73,7 @@ class AssessmentSource {
     }).then(response => response.data[type])
     }).then(response => response.data[type])
   }
   }
 
 
-  static migrateMultiple(data: MigrationInfo): Promise<MainItem[]> {
+  static migrateMultiple(data: MigrationInfo): Promise<MigrationItem[]> {
     return Promise.all(data.selectedInstances.map(async instance => {
     return Promise.all(data.selectedInstances.map(async instance => {
       const newData = { ...data }
       const newData = { ...data }
       newData.selectedInstances = [instance]
       newData.selectedInstances = [instance]

+ 6 - 6
src/sources/MigrationSource.ts

@@ -19,7 +19,6 @@ import DefaultOptionsSchemaPlugin from '../plugins/endpoint/default/OptionsSchem
 import { sortTasks } from './ReplicaSource'
 import { sortTasks } from './ReplicaSource'
 
 
 import Api from '../utils/ApiCaller'
 import Api from '../utils/ApiCaller'
-import type { MainItem } from '../@types/MainItem'
 import type { InstanceScript } from '../@types/Instance'
 import type { InstanceScript } from '../@types/Instance'
 import type { Field } from '../@types/Field'
 import type { Field } from '../@types/Field'
 import type { NetworkMap } from '../@types/Network'
 import type { NetworkMap } from '../@types/Network'
@@ -27,6 +26,7 @@ import type { Endpoint, StorageMap } from '../@types/Endpoint'
 
 
 import configLoader from '../utils/Config'
 import configLoader from '../utils/Config'
 import { Task } from '../@types/Task'
 import { Task } from '../@types/Task'
+import { MigrationItem, MigrationItemOptions, MigrationItemDetails } from '../@types/MainItem'
 
 
 class MigrationSourceUtils {
 class MigrationSourceUtils {
   static sortTaskUpdates(updates: any[]) {
   static sortTaskUpdates(updates: any[]) {
@@ -52,7 +52,7 @@ class MigrationSourceUtils {
 }
 }
 
 
 class MigrationSource {
 class MigrationSource {
-  async getMigrations(skipLog?: boolean): Promise<MainItem[]> {
+  async getMigrations(skipLog?: boolean): Promise<MigrationItem[]> {
     const response = await Api.send({
     const response = await Api.send({
       url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/migrations`,
       url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/migrations`,
       skipLog,
       skipLog,
@@ -62,7 +62,7 @@ class MigrationSource {
     return migrations
     return migrations
   }
   }
 
 
-  async getMigration(migrationId: string, skipLog?: boolean): Promise<MainItem> {
+  async getMigration(migrationId: string, skipLog?: boolean): Promise<MigrationItemDetails> {
     const response = await Api.send({
     const response = await Api.send({
       url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/migrations/${migrationId}`,
       url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/migrations/${migrationId}`,
       skipLog,
       skipLog,
@@ -72,7 +72,7 @@ class MigrationSource {
     return migration
     return migration
   }
   }
 
 
-  async recreateFullCopy(migration: MainItem): Promise<MainItem> {
+  async recreateFullCopy(migration: MigrationItemOptions): Promise<MigrationItem> {
     const {
     const {
       origin_endpoint_id, destination_endpoint_id, destination_environment,
       origin_endpoint_id, destination_endpoint_id, destination_environment,
       network_map, instances, storage_mappings, notes,
       network_map, instances, storage_mappings, notes,
@@ -125,7 +125,7 @@ class MigrationSource {
     updatedNetworkMappings: NetworkMap[] | null,
     updatedNetworkMappings: NetworkMap[] | null,
     defaultSkipOsMorphing: boolean | null,
     defaultSkipOsMorphing: boolean | null,
     replicationCount?: number | null,
     replicationCount?: number | null,
-  }): Promise<MainItem> {
+  }): Promise<MigrationItemDetails> {
     const getValue = (fieldName: string): string | null => {
     const getValue = (fieldName: string): string | null => {
       const updatedDestEnv = opts.updatedDestEnv && opts.updatedDestEnv[fieldName]
       const updatedDestEnv = opts.updatedDestEnv && opts.updatedDestEnv[fieldName]
       return updatedDestEnv != null ? updatedDestEnv
       return updatedDestEnv != null ? updatedDestEnv
@@ -213,7 +213,7 @@ class MigrationSource {
 
 
   async migrateReplica(
   async migrateReplica(
     replicaId: string, options: Field[], userScripts: InstanceScript[],
     replicaId: string, options: Field[], userScripts: InstanceScript[],
-  ): Promise<MainItem> {
+  ): Promise<MigrationItem> {
     const payload: any = {
     const payload: any = {
       migration: {
       migration: {
         replica_id: replicaId,
         replica_id: replicaId,

+ 8 - 48
src/sources/NotificationSource.ts

@@ -17,6 +17,7 @@ import moment from 'moment'
 import configLoader from '../utils/Config'
 import configLoader from '../utils/Config'
 import Api from '../utils/ApiCaller'
 import Api from '../utils/ApiCaller'
 import type { NotificationItemData, NotificationItem } from '../@types/NotificationItem'
 import type { NotificationItemData, NotificationItem } from '../@types/NotificationItem'
+import { TransferItem, MigrationItem, ReplicaItem } from '../@types/MainItem'
 
 
 class NotificationStorage {
 class NotificationStorage {
   static storeName: string = 'seenNotifications'
   static storeName: string = 'seenNotifications'
@@ -69,45 +70,8 @@ class NotificationStorage {
 }
 }
 
 
 class DataUtils {
 class DataUtils {
-  static getMainInfo(item: any) {
-    if (item.type === 'migration') {
-      return item
-    }
-    if (item.executions && item.executions.length) {
-      const availableExecutions = item.executions.filter((i: any) => !i.deleted_at)
-      if (availableExecutions.length) {
-        availableExecutions.sort((a: any, b: any) => b.number - a.number)
-        return availableExecutions[0]
-      }
-    }
-
-    return item
-  }
-
-  static getUpdatedAt(item: any) {
-    const info = this.getMainInfo(item)
-    return info.updated_at || info.created_at
-  }
-
-  static getItemDescription(item: any) {
-    const type = item.type === 'replica' ? 'Replica' : 'Migration'
-    const mainInfo = this.getMainInfo(item)
-    let description = ''
-    const id = `${mainInfo.id.substr(0, 7)}...`
-    switch (mainInfo.status) {
-      case 'COMPLETED':
-        description = `${type} execution ${id} completed successfully`
-        break
-      case 'ERROR':
-        description = `${type} execution ${id} failed`
-        break
-      case 'RUNNING':
-        description = `${type} execution ${id} running`
-        break
-      default:
-        break
-    }
-    return description
+  static getItemDescription(item: TransferItem) {
+    return `New ${item.type} ${item.id.substr(0, 7)}... status: ${item.last_execution_status.toLowerCase().replace(/_/g, ' ')}`
   }
   }
 }
 }
 
 
@@ -118,20 +82,18 @@ class NotificationSource {
       Api.send({ url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/replicas`, skipLog: true, quietError: true }),
       Api.send({ url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/replicas`, skipLog: true, quietError: true }),
     ])
     ])
 
 
-    const migrations = migrationsResponse.data.migrations
-    const replicas = replicasResponse.data.replicas
+    const migrations: MigrationItem[] = migrationsResponse.data.migrations
+    const replicas: ReplicaItem[] = replicasResponse.data.replicas
     const apiData = [...migrations, ...replicas]
     const apiData = [...migrations, ...replicas]
-    apiData.sort((a, b) => moment(DataUtils.getUpdatedAt(b)).diff(DataUtils.getUpdatedAt(a)))
+    apiData.sort((a, b) => moment(b.updated_at).diff(a.updated_at))
 
 
     const notificationItems: NotificationItemData[] = apiData.map(item => {
     const notificationItems: NotificationItemData[] = apiData.map(item => {
-      const mainInfo = DataUtils.getMainInfo(item)
-
       const newItem: NotificationItemData = {
       const newItem: NotificationItemData = {
         id: item.id,
         id: item.id,
-        status: mainInfo.status,
+        status: item.last_execution_status,
         type: item.type,
         type: item.type,
         name: item.instances[0],
         name: item.instances[0],
-        updatedAt: mainInfo.updated_at,
+        updatedAt: item.updated_at,
         description: DataUtils.getItemDescription(item),
         description: DataUtils.getItemDescription(item),
       }
       }
       return newItem
       return newItem
@@ -143,13 +105,11 @@ class NotificationSource {
       storageData = NotificationStorage.loadSeen() || []
       storageData = NotificationStorage.loadSeen() || []
     }
     }
     notificationItems.forEach(item => {
     notificationItems.forEach(item => {
-      // eslint-disable-next-line no-param-reassign
       item.unseen = true
       item.unseen = true
 
 
       storageData?.forEach(storageItem => {
       storageData?.forEach(storageItem => {
         if (storageItem.id === item.id
         if (storageItem.id === item.id
           && storageItem.status === item.status && storageItem.updatedAt === item.updatedAt) {
           && storageItem.status === item.status && storageItem.updatedAt === item.updatedAt) {
-          // eslint-disable-next-line no-param-reassign
           item.unseen = false
           item.unseen = false
         }
         }
       })
       })

+ 69 - 58
src/sources/ReplicaSource.ts

@@ -18,14 +18,14 @@ import Api from '../utils/ApiCaller'
 import { OptionsSchemaPlugin } from '../plugins/endpoint'
 import { OptionsSchemaPlugin } from '../plugins/endpoint'
 
 
 import configLoader from '../utils/Config'
 import configLoader from '../utils/Config'
-import type { MainItem, UpdateData } from '../@types/MainItem'
-import type { Execution } from '../@types/Execution'
+import type { UpdateData, ReplicaItem, ReplicaItemDetails } from '../@types/MainItem'
+import type { Execution, ExecutionTasks } from '../@types/Execution'
 import type { Endpoint } from '../@types/Endpoint'
 import type { Endpoint } from '../@types/Endpoint'
 import type { Task, ProgressUpdate } from '../@types/Task'
 import type { Task, ProgressUpdate } from '../@types/Task'
 import type { Field } from '../@types/Field'
 import type { Field } from '../@types/Field'
 
 
 export const sortTasks = (
 export const sortTasks = (
-  tasks: Task[], taskUpdatesSortFunction: (updates: ProgressUpdate[]) => void,
+  tasks?: Task[], taskUpdatesSortFunction?: (updates: ProgressUpdate[]) => void,
 ) => {
 ) => {
   if (!tasks) {
   if (!tasks) {
     return
     return
@@ -34,6 +34,9 @@ export const sortTasks = (
   let buffer: Task[] = []
   let buffer: Task[] = []
   let runningBuffer: Task[] = []
   let runningBuffer: Task[] = []
   let completedBuffer: Task[] = []
   let completedBuffer: Task[] = []
+  if (!taskUpdatesSortFunction) {
+    return
+  }
   tasks.forEach(task => {
   tasks.forEach(task => {
     taskUpdatesSortFunction(task.progress_updates)
     taskUpdatesSortFunction(task.progress_updates)
     buffer.push(task)
     buffer.push(task)
@@ -63,82 +66,78 @@ export const sortTasks = (
 }
 }
 
 
 class ReplicaSourceUtils {
 class ReplicaSourceUtils {
-  static filterDeletedExecutionsInReplicas(replicas: any[]) {
-    return replicas.map((replica: { executions: any }) => {
-      // eslint-disable-next-line no-param-reassign
-      replica.executions = ReplicaSourceUtils.filterDeletedExecutions(replica.executions)
-      return replica
-    })
-  }
-
-  static filterDeletedExecutions(executions: any[]) {
+  static filterDeletedExecutions(executions?: Execution[]) {
     if (!executions || !executions.length) {
     if (!executions || !executions.length) {
-      return executions
+      return []
     }
     }
 
 
-    return executions.filter((execution: { deleted_at: null }) => execution.deleted_at == null)
+    return executions.filter(execution => execution.deleted_at == null)
   }
   }
 
 
-  static sortReplicas(replicas: any[]) {
-    replicas.forEach((replica: { executions: any }) => {
-      ReplicaSourceUtils.sortExecutionsAndTasks(replica.executions)
-    })
-
-    replicas.sort(
-      (a: { executions: string | any[]; updated_at: any; created_at: any },
-        b: { executions: string | any[]; updated_at: any; created_at: any }) => {
-        const aLastExecution = a.executions && a.executions.length
-          ? a.executions[a.executions.length - 1] : null
-        const bLastExecution = b.executions && b.executions.length
-          ? b.executions[b.executions.length - 1] : null
-        const aLastTime = aLastExecution
-          ? aLastExecution.updated_at || aLastExecution.created_at : null
-        const bLastTime = bLastExecution
-          ? bLastExecution.updated_at || bLastExecution.created_at : null
-        const aTime = aLastTime || a.updated_at || a.created_at
-        const bTime = bLastTime || b.updated_at || b.created_at
-        return moment(bTime).diff(moment(aTime))
-      },
-    )
-  }
-
-  static sortExecutions(executions: any[]) {
-    if (executions) {
-      executions.sort((a: { number: number }, b: { number: number }) => a.number - b.number)
-    }
+  static sortReplicas(replicas: ReplicaItem[]) {
+    replicas.sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime())
   }
   }
 
 
-  static sortExecutionsAndTasks(executions: any[]) {
-    this.sortExecutions(executions)
-    executions.forEach((execution: { tasks: Task[] }) => {
-      sortTasks(execution.tasks, ReplicaSourceUtils.sortTaskUpdates)
-    })
+  static sortExecutions(executions: Execution[]) {
+    executions.sort((a, b) => a.number - b.number)
   }
   }
 
 
-  static sortTaskUpdates(updates: any[]) {
+  static sortTaskUpdates(updates: ProgressUpdate[]) {
     if (!updates) {
     if (!updates) {
       return
       return
     }
     }
     updates
     updates
-      .sort((a: any, b: any) => moment(a.created_at)
+      .sort((a, b) => moment(a.created_at)
         .toDate().getTime() - moment(b.created_at).toDate().getTime())
         .toDate().getTime() - moment(b.created_at).toDate().getTime())
   }
   }
 }
 }
 
 
 class ReplicaSource {
 class ReplicaSource {
-  async getReplicas(skipLog?: boolean, quietError?: boolean): Promise<MainItem[]> {
+  async getReplicas(skipLog?: boolean, quietError?: boolean): Promise<ReplicaItem[]> {
     const response = await Api.send({
     const response = await Api.send({
       url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/replicas`,
       url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/replicas`,
       skipLog,
       skipLog,
       quietError,
       quietError,
     })
     })
-    let replicas = response.data.replicas
-    replicas = ReplicaSourceUtils.filterDeletedExecutionsInReplicas(replicas)
+    const replicas: ReplicaItem[] = response.data.replicas
     ReplicaSourceUtils.sortReplicas(replicas)
     ReplicaSourceUtils.sortReplicas(replicas)
     return replicas
     return replicas
   }
   }
 
 
-  async execute(replicaId: string, fields?: Field[]): Promise<Execution> {
+  async getReplicaDetails(options: {
+    replicaId: string, polling?: boolean
+  }): Promise<ReplicaItemDetails> {
+    const { replicaId, polling } = options
+
+    const response = await Api.send({
+      url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/replicas/${replicaId}`,
+      skipLog: polling,
+    })
+    const replica: ReplicaItemDetails = response.data.replica
+    replica.executions = ReplicaSourceUtils.filterDeletedExecutions(replica.executions)
+    ReplicaSourceUtils.sortExecutions(replica.executions)
+    return replica
+  }
+
+  async getExecutionTasks(options: {
+    replicaId: string,
+    executionId?: string,
+    polling?: boolean,
+  }): Promise<ExecutionTasks> {
+    const {
+      replicaId, executionId, polling,
+    } = options
+
+    const response = await Api.send({
+      url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/replicas/${replicaId}/executions/${executionId}`,
+      skipLog: polling,
+    })
+    const execution: ExecutionTasks = response.data.execution
+    sortTasks(execution.tasks, ReplicaSourceUtils.sortTaskUpdates)
+    return execution
+  }
+
+  async execute(replicaId: string, fields?: Field[]): Promise<ExecutionTasks> {
     const payload: any = { execution: { shutdown_instances: false } }
     const payload: any = { execution: { shutdown_instances: false } }
     if (fields) {
     if (fields) {
       fields.forEach(f => {
       fields.forEach(f => {
@@ -150,24 +149,36 @@ class ReplicaSource {
       method: 'POST',
       method: 'POST',
       data: payload,
       data: payload,
     })
     })
-    const execution = response.data.execution
+    const execution: ExecutionTasks = response.data.execution
     sortTasks(execution.tasks, ReplicaSourceUtils.sortTaskUpdates)
     sortTasks(execution.tasks, ReplicaSourceUtils.sortTaskUpdates)
     return execution
     return execution
   }
   }
 
 
   async cancelExecution(
   async cancelExecution(
-    replicaId: string, executionId: string, force?: boolean | null,
+    options: { replicaId: string, executionId?: string, force?: boolean },
   ): Promise<string> {
   ): Promise<string> {
     const data: any = { cancel: null }
     const data: any = { cancel: null }
-    if (force) {
+    if (options.force) {
       data.cancel = { force: true }
       data.cancel = { force: true }
     }
     }
+
+    let lastExecutionId = options.executionId
+
+    if (!lastExecutionId) {
+      const replicaDetails = await this.getReplicaDetails({ replicaId: options.replicaId })
+      const lastExecution = replicaDetails.executions[replicaDetails.executions.length - 1]
+      if (lastExecution.status !== 'RUNNING') {
+        return options.replicaId
+      }
+      lastExecutionId = lastExecution.id
+    }
+
     await Api.send({
     await Api.send({
-      url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/replicas/${replicaId}/executions/${executionId}/actions`,
+      url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/replicas/${options.replicaId}/executions/${lastExecutionId}/actions`,
       method: 'POST',
       method: 'POST',
       data,
       data,
     })
     })
-    return replicaId
+    return options.replicaId
   }
   }
 
 
   async deleteExecution(replicaId: string, executionId: string): Promise<string> {
   async deleteExecution(replicaId: string, executionId: string): Promise<string> {
@@ -196,7 +207,7 @@ class ReplicaSource {
   }
   }
 
 
   async update(
   async update(
-    replica: MainItem,
+    replica: ReplicaItem,
     destinationEndpoint: Endpoint,
     destinationEndpoint: Endpoint,
     updateData: UpdateData,
     updateData: UpdateData,
     defaultStorage: string | null | undefined,
     defaultStorage: string | null | undefined,

+ 6 - 2
src/sources/UserSource.ts

@@ -171,8 +171,12 @@ class UserSource {
     return response.data.user
     return response.data.user
   }
   }
 
 
-  async getAllUsers(skipLog?: boolean): Promise<User[]> {
-    const response = await Api.send({ url: `${configLoader.config.servicesUrls.keystone}/users`, skipLog })
+  async getAllUsers(skipLog?: boolean, quietError?: boolean): Promise<User[]> {
+    const response = await Api.send({
+      url: `${configLoader.config.servicesUrls.keystone}/users`,
+      skipLog,
+      quietError,
+    })
     let users: User[] = response.data.users
     let users: User[] = response.data.users
     await utils.waitFor(() => Boolean(configLoader.config))
     await utils.waitFor(() => Boolean(configLoader.config))
     users = users.filter(u => !configLoader.config.hiddenUsers.find(hu => hu === u.name))
     users = users.filter(u => !configLoader.config.hiddenUsers.find(hu => hu === u.name))

+ 3 - 3
src/sources/WizardSource.ts

@@ -20,9 +20,9 @@ import configLoader from '../utils/Config'
 
 
 import type { WizardData } from '../@types/WizardData'
 import type { WizardData } from '../@types/WizardData'
 import type { StorageMap } from '../@types/Endpoint'
 import type { StorageMap } from '../@types/Endpoint'
-import type { MainItem } from '../@types/MainItem'
 import type { InstanceScript } from '../@types/Instance'
 import type { InstanceScript } from '../@types/Instance'
 import DefaultOptionsSchemaParser from '../plugins/endpoint/default/OptionsSchemaPlugin'
 import DefaultOptionsSchemaParser from '../plugins/endpoint/default/OptionsSchemaPlugin'
+import { TransferItem } from '../@types/MainItem'
 
 
 class WizardSource {
 class WizardSource {
   async create(
   async create(
@@ -31,7 +31,7 @@ class WizardSource {
     defaultStorage: string | null,
     defaultStorage: string | null,
     storageMap: StorageMap[],
     storageMap: StorageMap[],
     uploadedUserScripts: InstanceScript[],
     uploadedUserScripts: InstanceScript[],
-  ): Promise<MainItem> {
+  ): Promise<TransferItem> {
     const sourceParser = data.source
     const sourceParser = data.source
       ? OptionsSchemaPlugin.for(data.source.type) : DefaultOptionsSchemaParser
       ? OptionsSchemaPlugin.for(data.source.type) : DefaultOptionsSchemaParser
     const destParser = data.target
     const destParser = data.target
@@ -87,7 +87,7 @@ class WizardSource {
     const mainItems = await Promise.all(data.selectedInstances.map(async instance => {
     const mainItems = await Promise.all(data.selectedInstances.map(async instance => {
       const newData = { ...data }
       const newData = { ...data }
       newData.selectedInstances = [instance]
       newData.selectedInstances = [instance]
-      let mainItem: MainItem | null = null
+      let mainItem: TransferItem | null = null
       try {
       try {
         mainItem = await this.create(type, newData, defaultStorage, storageMap, uploadedUserScripts)
         mainItem = await this.create(type, newData, defaultStorage, storageMap, uploadedUserScripts)
       } finally {
       } finally {

+ 2 - 2
src/stores/AssessmentStore.ts

@@ -17,7 +17,7 @@ import { observable, action } from 'mobx'
 import AssessmentSource from '../sources/AssessmentSource'
 import AssessmentSource from '../sources/AssessmentSource'
 import type { Endpoint } from '../@types/Endpoint'
 import type { Endpoint } from '../@types/Endpoint'
 import type { Assessment, MigrationInfo } from '../@types/Assessment'
 import type { Assessment, MigrationInfo } from '../@types/Assessment'
-import type { MainItem } from '../@types/MainItem'
+import { MigrationItem } from '../@types/MainItem'
 
 
 class AssessmentStore {
 class AssessmentStore {
   @observable selectedEndpoint: Endpoint | null = null
   @observable selectedEndpoint: Endpoint | null = null
@@ -26,7 +26,7 @@ class AssessmentStore {
 
 
   @observable migrating: boolean = false
   @observable migrating: boolean = false
 
 
-  @observable migrations: MainItem[] = []
+  @observable migrations: MigrationItem[] = []
 
 
   @action updateSelectedEndpoint(endpoint: Endpoint) {
   @action updateSelectedEndpoint(endpoint: Endpoint) {
     this.selectedEndpoint = endpoint
     this.selectedEndpoint = endpoint

+ 3 - 3
src/stores/InstanceStore.ts

@@ -267,7 +267,7 @@ class InstanceStore {
           runInAction(() => {
           runInAction(() => {
             this.instancesDetails = this.instancesDetails.filter(id => (id.name || id.instance_name || '') !== name)
             this.instancesDetails = this.instancesDetails.filter(id => (id.name || id.instance_name || '') !== name)
             this.instancesDetails.push(instance)
             this.instancesDetails.push(instance)
-            this.instancesDetails.sort(n => (n.name || n.instance_name || '')
+            this.instancesDetails = this.instancesDetails.slice().sort(n => (n.name || n.instance_name || '')
               .localeCompare(n.name || n.instance_name || ''))
               .localeCompare(n.name || n.instance_name || ''))
           })
           })
         }))
         }))
@@ -312,7 +312,7 @@ class InstanceStore {
         ...this.instancesDetails,
         ...this.instancesDetails,
         instance,
         instance,
       ]
       ]
-      this.instancesDetails
+      this.instancesDetails = this.instancesDetails.slice()
         .sort((a, b) => (a.instance_name || a.name).localeCompare((b.instance_name || b.name)))
         .sort((a, b) => (a.instance_name || a.name).localeCompare((b.instance_name || b.name)))
     })
     })
   }
   }
@@ -381,7 +381,7 @@ class InstanceStore {
             ]
             ]
           })
           })
           if (this.instancesDetailsRemaining === 0) {
           if (this.instancesDetailsRemaining === 0) {
-            this.instancesDetails
+            this.instancesDetails = this.instancesDetails.slice()
               .sort((a, b) => (a.instance_name || a.name)
               .sort((a, b) => (a.instance_name || a.name)
                 .localeCompare((b.instance_name || b.name)))
                 .localeCompare((b.instance_name || b.name)))
             resolve()
             resolve()

+ 10 - 16
src/stores/MigrationStore.ts

@@ -14,16 +14,18 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 import { observable, action, runInAction } from 'mobx'
 import { observable, action, runInAction } from 'mobx'
 
 
-import type { MainItem, UpdateData } from '../@types/MainItem'
+import type {
+  UpdateData, MigrationItem, MigrationItemDetails, MigrationItemOptions,
+} from '../@types/MainItem'
 import type { Field } from '../@types/Field'
 import type { Field } from '../@types/Field'
 import type { Endpoint } from '../@types/Endpoint'
 import type { Endpoint } from '../@types/Endpoint'
 import type { InstanceScript } from '../@types/Instance'
 import type { InstanceScript } from '../@types/Instance'
 import MigrationSource from '../sources/MigrationSource'
 import MigrationSource from '../sources/MigrationSource'
 
 
 class MigrationStore {
 class MigrationStore {
-  @observable migrations: MainItem[] = []
+  @observable migrations: MigrationItem[] = []
 
 
-  @observable migrationDetails: MainItem | null = null
+  @observable migrationDetails: MigrationItemDetails | null = null
 
 
   @observable loading: boolean = true
   @observable loading: boolean = true
 
 
@@ -39,15 +41,7 @@ class MigrationStore {
     try {
     try {
       const migrations = await MigrationSource.getMigrations(options && options.skipLog)
       const migrations = await MigrationSource.getMigrations(options && options.skipLog)
       runInAction(() => {
       runInAction(() => {
-        this.migrations = migrations.map(migration => {
-          const oldMigration = this.migrations.find(r => r.id === migration.id)
-          if (oldMigration) {
-            // eslint-disable-next-line no-param-reassign
-            migration.executions = oldMigration.executions
-          }
-
-          return migration
-        })
+        this.migrations = migrations
         this.loading = false
         this.loading = false
         this.migrationsLoaded = true
         this.migrationsLoaded = true
       })
       })
@@ -57,7 +51,7 @@ class MigrationStore {
     }
     }
   }
   }
 
 
-  getDefaultSkipOsMorphing(migration: MainItem | null) {
+  getDefaultSkipOsMorphing(migration: MigrationItemDetails | null) {
     const tasks = migration && migration.tasks
     const tasks = migration && migration.tasks
     if (tasks && !tasks.find(t => t.task_type === 'OS_MORPHING')) {
     if (tasks && !tasks.find(t => t.task_type === 'OS_MORPHING')) {
       return true
       return true
@@ -65,19 +59,19 @@ class MigrationStore {
     return null
     return null
   }
   }
 
 
-  @action async recreateFullCopy(migration: MainItem) {
+  @action async recreateFullCopy(migration: MigrationItemOptions) {
     return MigrationSource.recreateFullCopy(migration)
     return MigrationSource.recreateFullCopy(migration)
   }
   }
 
 
   @action async recreate(
   @action async recreate(
-    migration: MainItem,
+    migration: MigrationItemDetails,
     sourceEndpoint: Endpoint,
     sourceEndpoint: Endpoint,
     destEndpoint: Endpoint,
     destEndpoint: Endpoint,
     updateData: UpdateData,
     updateData: UpdateData,
     defaultStorage: string | null | undefined,
     defaultStorage: string | null | undefined,
     updatedDefaultStorage: string | null | undefined,
     updatedDefaultStorage: string | null | undefined,
     replicationCount: number | null | undefined,
     replicationCount: number | null | undefined,
-  ): Promise<MainItem> {
+  ): Promise<MigrationItemDetails> {
     const migrationResult = await MigrationSource.recreate({
     const migrationResult = await MigrationSource.recreate({
       sourceEndpoint,
       sourceEndpoint,
       destEndpoint,
       destEndpoint,

+ 127 - 62
src/stores/ReplicaStore.ts

@@ -13,17 +13,21 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 */
 
 
 import { observable, action, runInAction } from 'mobx'
 import { observable, action, runInAction } from 'mobx'
-import moment from 'moment'
 
 
 import notificationStore from './NotificationStore'
 import notificationStore from './NotificationStore'
 import ReplicaSource from '../sources/ReplicaSource'
 import ReplicaSource from '../sources/ReplicaSource'
-import type { MainItem, UpdateData } from '../@types/MainItem'
-import type { Execution } from '../@types/Execution'
+import type {
+  UpdateData, ReplicaItem, ReplicaItemDetails,
+} from '../@types/MainItem'
+import type { Execution, ExecutionTasks } from '../@types/Execution'
 import type { Endpoint } from '../@types/Endpoint'
 import type { Endpoint } from '../@types/Endpoint'
 import type { Field } from '../@types/Field'
 import type { Field } from '../@types/Field'
 
 
 class ReplicaStoreUtils {
 class ReplicaStoreUtils {
-  static getNewReplica(replicaDetails: MainItem, execution: Execution): MainItem {
+  static getNewReplica(
+    replicaDetails: ReplicaItemDetails,
+    execution: Execution,
+  ): ReplicaItemDetails {
     if (replicaDetails.executions) {
     if (replicaDetails.executions) {
       return {
       return {
         ...replicaDetails,
         ...replicaDetails,
@@ -38,39 +42,27 @@ class ReplicaStoreUtils {
   }
   }
 }
 }
 
 
-const checkAddExecution = (
-  replicas: MainItem[],
-  addExecution: { replicaId: string, execution: Execution } | null,
-) => {
-  const usableAddExecution = addExecution
-  if (!usableAddExecution) {
-    return
-  }
-  const executionTime = moment.utc(usableAddExecution.execution.created_at)
-    .local().toDate().getTime()
-  if (new Date().getTime() - executionTime > 5000) {
-    return
-  }
-  const replica = replicas.find(r => r.id === usableAddExecution.replicaId)
-  if (!replica) {
-    return
-  }
-  const execution = replica.executions.find(e => e.id === usableAddExecution.execution.id)
-  if (execution) {
-    return
-  }
-  replica.executions.push(usableAddExecution.execution)
-}
-
 class ReplicaStore {
 class ReplicaStore {
-  @observable replicas: MainItem[] = []
+  @observable replicas: ReplicaItem[] = []
 
 
   @observable loading: boolean = false
   @observable loading: boolean = false
 
 
+  @observable replicaDetails: ReplicaItemDetails | null = null
+
+  @observable replicaDetailsLoading: boolean = false
+
+  @observable executionsTasks: ExecutionTasks[] = []
+
+  @observable executionsTasksLoading: boolean = false
+
   @observable backgroundLoading: boolean = false
   @observable backgroundLoading: boolean = false
 
 
   @observable startingExecution: boolean = false
   @observable startingExecution: boolean = false
 
 
+  @observable replicasWithDisks: ReplicaItemDetails[] = []
+
+  @observable replicasWithDisksLoading: boolean = false
+
   replicasLoaded: boolean = false
   replicasLoaded: boolean = false
 
 
   addExecution: { replicaId: string, execution: Execution } | null = null
   addExecution: { replicaId: string, execution: Execution } | null = null
@@ -87,14 +79,35 @@ class ReplicaStore {
     try {
     try {
       const replicas = await ReplicaSource
       const replicas = await ReplicaSource
         .getReplicas(options && options.skipLog, options && options.quietError)
         .getReplicas(options && options.skipLog, options && options.quietError)
-      checkAddExecution(replicas, this.addExecution)
       this.getReplicasSuccess(replicas)
       this.getReplicasSuccess(replicas)
     } finally {
     } finally {
       this.getReplicasDone()
       this.getReplicasDone()
     }
     }
   }
   }
 
 
-  @action getReplicasSuccess(replicas: MainItem[]) {
+  @action async getReplicaDetails(options: {
+    replicaId: string, showLoading?: boolean, polling?: boolean,
+  }) {
+    const { replicaId, showLoading, polling } = options
+
+    if (showLoading) {
+      this.replicaDetailsLoading = true
+    }
+
+    try {
+      const replica = await ReplicaSource.getReplicaDetails({ replicaId, polling })
+
+      runInAction(() => {
+        this.replicaDetails = replica
+      })
+    } finally {
+      runInAction(() => {
+        this.replicaDetailsLoading = false
+      })
+    }
+  }
+
+  @action getReplicasSuccess(replicas: ReplicaItem[]) {
     this.replicasLoaded = true
     this.replicasLoaded = true
     this.replicas = replicas
     this.replicas = replicas
   }
   }
@@ -104,32 +117,73 @@ class ReplicaStore {
     this.backgroundLoading = false
     this.backgroundLoading = false
   }
   }
 
 
-  @action async execute(replicaId: string, fields?: Field[]): Promise<void> {
-    const replica = this.replicas.find(r => r.id === replicaId)
-    if (replica && replica.executions && replica.executions.length === 0) {
-      this.startingExecution = true
+  private currentlyLoadingExecution: string = ''
+
+  @action async getExecutionTasks(
+    options: {
+      replicaId: string,
+      executionId?: string,
+      polling?: boolean,
+    },
+  ) {
+    const {
+      replicaId, executionId, polling,
+    } = options
+
+    if (!polling && this.currentlyLoadingExecution === executionId) {
+      return
     }
     }
+    this.currentlyLoadingExecution = polling ? this.currentlyLoadingExecution : executionId || ''
+    if (!this.currentlyLoadingExecution) {
+      return
+    }
+
+    if (!this.executionsTasks.find(e => e.id === this.currentlyLoadingExecution)) {
+      this.executionsTasksLoading = true
+    }
+
+    try {
+      const executionTasks = await ReplicaSource.getExecutionTasks({
+        replicaId, executionId: this.currentlyLoadingExecution, polling,
+      })
+      runInAction(() => {
+        this.executionsTasks = [
+          ...this.executionsTasks.filter(e => e.id !== this.currentlyLoadingExecution),
+          executionTasks,
+        ]
+      })
+    } catch (err) {
+      console.error(err)
+    } finally {
+      runInAction(() => {
+        this.executionsTasksLoading = false
+      })
+    }
+  }
+
+  @action async execute(replicaId: string, fields?: Field[]): Promise<void> {
+    this.startingExecution = true
+
     const execution = await ReplicaSource.execute(replicaId, fields)
     const execution = await ReplicaSource.execute(replicaId, fields)
     this.executeSuccess(replicaId, execution)
     this.executeSuccess(replicaId, execution)
   }
   }
 
 
   @action executeSuccess(replicaId: string, execution: Execution) {
   @action executeSuccess(replicaId: string, execution: Execution) {
-    this.addExecution = { replicaId, execution }
-    const replicasItemIndex = this.replicas.findIndex(r => r.id === replicaId)
-
-    if (replicasItemIndex > -1) {
+    if (this.replicaDetails?.id === replicaId) {
       const updatedReplica = ReplicaStoreUtils
       const updatedReplica = ReplicaStoreUtils
-        .getNewReplica(this.replicas[replicasItemIndex], execution)
-      this.replicas[replicasItemIndex] = updatedReplica
+        .getNewReplica(this.replicaDetails, execution)
+      this.replicaDetails = updatedReplica
     }
     }
+    this.getExecutionTasks({ replicaId, executionId: execution.id })
+
     this.startingExecution = false
     this.startingExecution = false
   }
   }
 
 
   async cancelExecution(
   async cancelExecution(
-    replicaId: string, executionId: string, force?: boolean | null,
+    options: {replicaId: string, executionId?: string, force?: boolean},
   ): Promise<void> {
   ): Promise<void> {
-    await ReplicaSource.cancelExecution(replicaId, executionId, force)
-    if (force) {
+    await ReplicaSource.cancelExecution(options)
+    if (options.force) {
       notificationStore.alert('Force cancelled', 'success')
       notificationStore.alert('Force cancelled', 'success')
     } else {
     } else {
       notificationStore.alert('Cancelled', 'success')
       notificationStore.alert('Cancelled', 'success')
@@ -144,12 +198,12 @@ class ReplicaStore {
   @action deleteExecutionSuccess(replicaId: string, executionId: string) {
   @action deleteExecutionSuccess(replicaId: string, executionId: string) {
     let executions = []
     let executions = []
 
 
-    const replicasItemIndex = this.replicas ? this.replicas.findIndex(r => r.id === replicaId) : -1
-
-    if (replicasItemIndex > -1) {
-      executions = [...this.replicas[replicasItemIndex].executions
-        .filter(e => e.id !== executionId)]
-      this.replicas[replicasItemIndex].executions = executions
+    if (this.replicaDetails?.id === replicaId) {
+      executions = [...this.replicaDetails.executions.filter(e => e.id !== executionId)]
+      this.replicaDetails.executions = executions
+    }
+    if (executionId === this.currentlyLoadingExecution) {
+      this.currentlyLoadingExecution = ''
     }
     }
   }
   }
 
 
@@ -164,17 +218,15 @@ class ReplicaStore {
   }
   }
 
 
   @action deleteDisksSuccess(replicaId: string, execution: Execution) {
   @action deleteDisksSuccess(replicaId: string, execution: Execution) {
-    const replicasItemIndex = this.replicas.findIndex(r => r.id === replicaId)
-
-    if (replicasItemIndex > -1) {
+    if (this.replicaDetails?.id === replicaId) {
       const updatedReplica = ReplicaStoreUtils
       const updatedReplica = ReplicaStoreUtils
-        .getNewReplica(this.replicas[replicasItemIndex], execution)
-      this.replicas[replicasItemIndex] = updatedReplica
+        .getNewReplica(this.replicaDetails, execution)
+      this.replicaDetails = updatedReplica
     }
     }
   }
   }
 
 
   async update(
   async update(
-    replica: MainItem,
+    replica: ReplicaItemDetails,
     destinationEndpoint: Endpoint,
     destinationEndpoint: Endpoint,
     updateData: UpdateData,
     updateData: UpdateData,
     defaultStorage: string | null | undefined,
     defaultStorage: string | null | undefined,
@@ -189,12 +241,7 @@ class ReplicaStore {
     )
     )
   }
   }
 
 
-  getReplicasWithDisks(replicas: MainItem[]): MainItem[] {
-    const result = replicas.filter(r => this.hasReplicaDisks(r))
-    return result
-  }
-
-  hasReplicaDisks(replica?: MainItem | null): boolean {
+  testReplicaHasDisks(replica: ReplicaItemDetails | null) {
     if (!replica || !replica.executions || replica.executions.length === 0) {
     if (!replica || !replica.executions || replica.executions.length === 0) {
       return false
       return false
     }
     }
@@ -207,6 +254,24 @@ class ReplicaStore {
     }
     }
     return true
     return true
   }
   }
+
+  @action
+  async loadHaveReplicasDisks(replicas: ReplicaItem[]) {
+    this.replicasWithDisksLoading = true
+
+    try {
+      const replicaDetails = await Promise.all(replicas
+        .map(replica => ReplicaSource.getReplicaDetails({ replicaId: replica.id })))
+
+      runInAction(() => {
+        this.replicasWithDisks = replicaDetails.filter(r => this.testReplicaHasDisks(r))
+      })
+    } finally {
+      runInAction(() => {
+        this.replicasWithDisksLoading = false
+      })
+    }
+  }
 }
 }
 
 
 export default new ReplicaStore()
 export default new ReplicaStore()

+ 11 - 3
src/stores/UserStore.ts

@@ -152,12 +152,20 @@ class UserStore {
     }
     }
   }
   }
 
 
-  @action async getAllUsers(options?: { showLoading?: boolean, skipLog?: boolean }): Promise<void> {
-    if (options && options.showLoading) this.allUsersLoading = true
+  @action async getAllUsers(options?: {
+    showLoading?: boolean,
+    skipLog?: boolean,
+    quietError?: boolean,
+  }): Promise<void> {
+    if (options?.showLoading) this.allUsersLoading = true
 
 
     try {
     try {
-      const users = await UserSource.getAllUsers(options && options.skipLog)
+      const users = await UserSource.getAllUsers(options?.skipLog, options?.quietError)
       runInAction(() => { this.users = users })
       runInAction(() => { this.users = users })
+    } catch (err) {
+      if (err.data?.error?.code !== 403) {
+        throw err
+      }
     } finally {
     } finally {
       runInAction(() => { this.allUsersLoading = false })
       runInAction(() => { this.allUsersLoading = false })
     }
     }

+ 4 - 4
src/stores/WizardStore.ts

@@ -15,7 +15,6 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 import { observable, action, runInAction } from 'mobx'
 import { observable, action, runInAction } from 'mobx'
 
 
 import type { WizardData, WizardPage } from '../@types/WizardData'
 import type { WizardData, WizardPage } from '../@types/WizardData'
-import type { MainItem } from '../@types/MainItem'
 import type { Instance, InstanceScript } from '../@types/Instance'
 import type { Instance, InstanceScript } from '../@types/Instance'
 import type { Field } from '../@types/Field'
 import type { Field } from '../@types/Field'
 import type { NetworkMap } from '../@types/Network'
 import type { NetworkMap } from '../@types/Network'
@@ -24,6 +23,7 @@ import type { Schedule } from '../@types/Schedule'
 import { wizardPages } from '../constants'
 import { wizardPages } from '../constants'
 import source from '../sources/WizardSource'
 import source from '../sources/WizardSource'
 import notificationStore from './NotificationStore'
 import notificationStore from './NotificationStore'
+import { TransferItem } from '../@types/MainItem'
 
 
 const updateOptions = (
 const updateOptions = (
   oldOptions: { [prop: string]: any } | null | undefined,
   oldOptions: { [prop: string]: any } | null | undefined,
@@ -64,11 +64,11 @@ class WizardStore {
 
 
   @observable currentPage: WizardPage = wizardPages[0]
   @observable currentPage: WizardPage = wizardPages[0]
 
 
-  @observable createdItem: MainItem | null = null
+  @observable createdItem: TransferItem | null = null
 
 
   @observable creatingItem: boolean = false
   @observable creatingItem: boolean = false
 
 
-  @observable createdItems: Array<MainItem | null> | null = null
+  @observable createdItems: Array<TransferItem | null> | null = null
 
 
   @observable creatingItems: boolean = false
   @observable creatingItems: boolean = false
 
 
@@ -217,7 +217,7 @@ class WizardStore {
     this.creatingItem = true
     this.creatingItem = true
 
 
     try {
     try {
-      const item: MainItem = await source.create(
+      const item: TransferItem = await source.create(
         type, data, defaultStorage, storageMap, uploadedUserScripts,
         type, data, defaultStorage, storageMap, uploadedUserScripts,
       )
       )
       runInAction(() => { this.createdItem = item })
       runInAction(() => { this.createdItem = item })

+ 2 - 2
src/utils/DateUtils.ts

@@ -15,12 +15,12 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 import moment from 'moment'
 import moment from 'moment'
 
 
 class DateUtils {
 class DateUtils {
-  static getLocalTime(rawDate: Date | moment.Moment | undefined | null): moment.Moment {
+  static getLocalTime(rawDate: moment.MomentInput): moment.Moment {
     const usableRawDate = rawDate || undefined
     const usableRawDate = rawDate || undefined
     return moment(usableRawDate).add(-new Date().getTimezoneOffset(), 'minutes')
     return moment(usableRawDate).add(-new Date().getTimezoneOffset(), 'minutes')
   }
   }
 
 
-  static getUtcTime(rawDate: Date | moment.Moment | undefined | null): moment.Moment {
+  static getUtcTime(rawDate: moment.MomentInput): moment.Moment {
     const usableRawDate = rawDate || undefined
     const usableRawDate = rawDate || undefined
     return moment(usableRawDate).add(new Date().getTimezoneOffset(), 'minutes')
     return moment(usableRawDate).add(new Date().getTimezoneOffset(), 'minutes')
   }
   }