Explorar o código

Move replica buttons to an actions dropdown

The details pages' buttons have been reorganised with the help of a new
`ActionDropdown` component.
Sergiu Miclea %!s(int64=7) %!d(string=hai) anos
pai
achega
c38c3ac883

+ 5 - 3
src/components/atoms/Button/Button.jsx

@@ -44,12 +44,14 @@ const hoverBackgroundColor = (props) => {
     return Palette.grayscale[7]
   }
 
+  if (props.secondary) {
+    return Palette.grayscale[8]
+  }
+
   if (props.hoverPrimary) {
     return Palette.primary
   }
-  if (props.secondary) {
-    return Palette.grayscale[3]
-  }
+
   if (props.alert) {
     return Palette.alert
   }

+ 24 - 4
src/components/atoms/DropdownButton/DropdownButton.jsx

@@ -27,7 +27,7 @@ const getLabelColor = props => {
     return Palette.grayscale[3]
   }
 
-  if (props.primary) {
+  if (props.primary || props.secondary) {
     return 'white'
   }
 
@@ -49,6 +49,10 @@ const getBackgroundColor = props => {
     return Palette.grayscale[0]
   }
 
+  if (props.secondary) {
+    return Palette.secondaryLight
+  }
+
   if (props.primary) {
     return Palette.primary
   }
@@ -60,7 +64,7 @@ const getArrowColor = props => {
     return Palette.grayscale[3]
   }
 
-  if (props.primary) {
+  if (props.primary || props.secondary) {
     return 'white'
   }
 
@@ -85,8 +89,21 @@ const borderColor = props => {
   if (props.primary) {
     return Palette.primary
   }
+  if (props.secondary) {
+    return Palette.secondaryLight
+  }
   return Palette.grayscale[3]
 }
+const backgroundHover = props => {
+  if (props.disabled || props.embedded) {
+    return ''
+  }
+  if (props.secondary) {
+    return Palette.secondaryLight
+  }
+  return Palette.primary
+}
+
 const Wrapper = styled.div`
   display: flex;
   align-items: center;
@@ -108,7 +125,7 @@ const Wrapper = styled.div`
   #dropdown-arrow-image {stroke: ${props => getArrowColor(props)};}
   &:hover {
     #dropdown-arrow-image {stroke: ${props => props.disabled ? '' : props.embedded ? '' : 'white'};}
-    background: ${props => props.disabled ? '' : props.embedded ? '' : Palette.primary};
+    background: ${props => backgroundHover(props)};
   }
 
   &:hover ${Label} {
@@ -130,11 +147,14 @@ type Props = {
   onClick?: (event: Event) => void,
   customRef?: (ref: HTMLElement) => void,
   innerRef?: (ref: HTMLElement) => void,
+  arrowRef?: (ref: HTMLElement) => void,
   className?: string,
   disabled?: boolean,
   'data-test-id'?: string,
   embedded?: boolean,
   highlight?: boolean,
+  secondary?: boolean,
+  centered?: boolean,
 }
 class DropdownButton extends React.Component<Props> {
   render() {
@@ -162,7 +182,7 @@ class DropdownButton extends React.Component<Props> {
         </Label>
         <Arrow
           {...this.props}
-          innerRef={() => { }}
+          innerRef={ref => { if (this.props.arrowRef) this.props.arrowRef(ref) }}
           onClick={() => { }}
           data-test-id=""
           disabled={this.props.disabled}

+ 3 - 0
src/components/atoms/DropdownButton/story.jsx

@@ -26,3 +26,6 @@ storiesOf('DropdownButton', module)
   .add('disabled', () => (
     <DropdownButton disabled value="Dropdown Button" onClick={action('clicked')} />
   ))
+  .add('secondary centered', () => (
+    <DropdownButton secondary centered value="Dropdown Button" onClick={action('clicked')} />
+  ))

+ 207 - 0
src/components/molecules/ActionDropdown/ActionDropdown.jsx

@@ -0,0 +1,207 @@
+
+/*
+Copyright (C) 2019  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+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/>.
+*/
+
+// @flow
+
+import React from 'react'
+import ReactDOM from 'react-dom'
+import { observer } from 'mobx-react'
+import styled, { css } from 'styled-components'
+import autobind from 'autobind-decorator'
+
+import DropdownButton from '../../atoms/DropdownButton/DropdownButton'
+import { List, ListItems, Tip } from '../DropdownLink/DropdownLink'
+
+import StyleProps from '../../styleUtils/StyleProps'
+import Palette from '../../styleUtils/Palette'
+
+const Wrapper = styled.div`
+  position: relative;
+`
+
+const ListItem = styled.div`
+  color: ${props => props.disabled ? Palette.grayscale[2] : props.color || Palette.black};
+  height: 32px;
+  padding: 0 16px;
+  cursor: ${props => props.disabled ? 'default' : 'pointer'};
+  display: flex;
+  align-items: center;
+  transition: all ${StyleProps.animations.swift};
+  &:hover {
+    ${props => props.disabled ? '' : css`background: ${Palette.grayscale[0]};`}
+  }
+  &:first-child {
+    border-top-left-radius: ${StyleProps.borderRadius};
+    border-top-right-radius: ${StyleProps.borderRadius};
+  }
+  &:last-child {
+    border-bottom-left-radius: ${StyleProps.borderRadius};
+    border-bottom-right-radius: ${StyleProps.borderRadius};
+  }
+`
+export type Action = {
+  label: string,
+  color?: string,
+  action: () => void,
+  disabled?: boolean,
+  hidden?: boolean,
+}
+const ListStyle = css`
+  box-shadow: 0 0 8px 0px rgba(111, 114, 118, 0.51);
+  border: none;
+`
+type Props = {
+  label: string,
+  actions: Action[],
+  style: any,
+}
+
+type State = {
+  showDropdownList: boolean,
+}
+
+@observer
+class ActionDropdown extends React.Component<Props, State> {
+  static defaultProps: $Shape<Props> = {
+    label: 'Actions',
+  }
+
+  state = {
+    showDropdownList: false,
+  }
+
+  itemMouseDown: boolean
+  listRef: HTMLElement
+  arrowRef: HTMLElement
+  tipRef: HTMLElement
+
+  componentDidMount() {
+    window.addEventListener('mousedown', this.handlePageClick, false)
+  }
+
+  componentDidUpdate() {
+    this.updateListPosition()
+  }
+
+  componentWillUnmount() {
+    window.removeEventListener('mousedown', this.handlePageClick, false)
+  }
+
+  updateListPosition() {
+    if (!this.state.showDropdownList || !this.listRef || !this.arrowRef || !this.tipRef) {
+      return
+    }
+
+    let listWidth = this.listRef.offsetWidth
+    let arrowWidth = this.arrowRef.offsetWidth
+    let arrowHeight = this.arrowRef.offsetHeight
+    let tipHeight = this.tipRef.offsetHeight
+    const tipLeftOffset = 6
+    const tipTopOffset = 6
+    let arrowOffset = this.arrowRef.getBoundingClientRect()
+
+    // If a modal is opened, body scroll is removed and body top is set to replicate scroll position
+    let scrollOffset = 0
+    if (document.body && parseInt(document.body.style.top, 10) < 0) {
+      scrollOffset = -parseInt(document.body && document.body.style.top, 10)
+    }
+
+    this.listRef.style.top = `${arrowOffset.top + (window.pageYOffset || scrollOffset) + arrowHeight + tipHeight + tipTopOffset}px`
+    this.listRef.style.left = `${arrowOffset.left + tipLeftOffset + (arrowWidth - listWidth)}px`
+  }
+
+  @autobind
+  handlePageClick() {
+    if (!this.itemMouseDown) {
+      this.setState({ showDropdownList: false })
+    }
+  }
+
+  handleButtonClick() {
+    this.setState({ showDropdownList: !this.state.showDropdownList })
+  }
+
+  handleItemMouseHover(action: Action, index: number, isEnter: boolean) {
+    if (!this.tipRef || index !== 0 || action.disabled) {
+      return
+    }
+    this.tipRef.style.background = isEnter ? Palette.grayscale[0] : Palette.grayscale[1]
+  }
+
+  handleItemClick(action: Action) {
+    if (action.disabled) {
+      return
+    }
+    action.action()
+    this.setState({ showDropdownList: false })
+  }
+
+  renderListItems() {
+    return (
+      <ListItems>
+        {this.props.actions.filter(a => !a.hidden).map((action, i) => (
+          <ListItem
+            onMouseEnter={() => { this.handleItemMouseHover(action, i, true) }}
+            onMouseLeave={() => { this.handleItemMouseHover(action, i, false) }}
+            onMouseDown={() => { this.itemMouseDown = true }}
+            onMouseUp={() => { this.itemMouseDown = false }}
+            key={action.label}
+            onClick={() => { this.handleItemClick(action) }}
+            color={action.color}
+            disabled={action.disabled}
+          >
+            {action.label}
+          </ListItem>
+        ))}
+      </ListItems>
+    )
+  }
+
+  renderList() {
+    if (!this.state.showDropdownList) {
+      return null
+    }
+
+    let body: any = document.body
+    return ReactDOM.createPortal((
+      <List
+        innerRef={list => { this.listRef = list }}
+        width={`${StyleProps.inputSizes.regular.width - 4}px`}
+        padding={0}
+        customStyle={ListStyle}
+      >
+        <Tip innerRef={ref => { this.tipRef = ref }} borderColor={'rgba(111, 114, 118, 0.2)'} />
+        {this.renderListItems()}
+      </List>
+    ), body)
+  }
+
+  render() {
+    return (
+      <Wrapper style={this.props.style}>
+        <DropdownButton
+          secondary
+          centered
+          value={this.props.label}
+          onClick={() => { this.handleButtonClick() }}
+          arrowRef={ref => { this.arrowRef = ref }}
+        />
+        {this.renderList()}
+      </Wrapper>
+    )
+  }
+}
+
+export default ActionDropdown

+ 6 - 0
src/components/molecules/ActionDropdown/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "ActionDropdown",
+  "version": "0.0.0",
+  "private": true,
+  "main": "./ActionDropdown.jsx"
+}

+ 40 - 0
src/components/molecules/ActionDropdown/story.jsx

@@ -0,0 +1,40 @@
+/*
+Copyright (C) 2019  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+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/>.
+*/
+
+// @flow
+
+import React from 'react'
+import { storiesOf } from '@storybook/react'
+import ActionDropdown from '.'
+
+import Palette from '../../styleUtils/Palette'
+
+let actions = [{
+  label: 'Execute',
+  color: Palette.primary,
+  action: () => { console.log('execute clicked') },
+  disabled: true,
+}, {
+  label: 'Edit',
+  action: () => { console.log('Edit clicked') },
+}, {
+  label: 'Delete',
+  color: Palette.alert,
+  action: () => { console.log('Delete clicked') },
+}]
+
+storiesOf('ActionDropdown', module)
+  .add('default', () => (
+    <ActionDropdown actions={actions} />
+  ))

+ 10 - 8
src/components/molecules/DropdownLink/DropdownLink.jsx

@@ -38,10 +38,10 @@ const LinkButton = styled.div`
   align-items: center;
   cursor: ${props => props.disabled ? 'default' : 'pointer'};
 `
-const List = styled.div`
+export const List = styled.div`
   position: absolute;
   z-index: 1001;
-  padding: 8px;
+  padding: ${props => props.padding != null ? props.padding : 8}px;
   background: ${Palette.grayscale[1]};
   border-radius: 4px;
   border: 1px solid ${Palette.grayscale[0]};
@@ -49,26 +49,28 @@ const List = styled.div`
     min-width: 132px;
     max-width: 160px;
   `}
+  ${props => props.customStyle || ''}
 `
-const Tip = styled.div`
+export const Tip = styled.div`
   position: absolute;
   top: -6px;
   right: 8px;
   width: 10px;
   height: 10px;
   background: ${Palette.grayscale[1]};
-  border-top: 1px solid ${Palette.grayscale[0]};
-  border-left: 1px solid ${Palette.grayscale[0]};
+  border-top: 1px solid ${props => props.borderColor || Palette.grayscale[0]};
+  border-left: 1px solid ${props => props.borderColor || Palette.grayscale[0]};
   border-bottom: 1px solid transparent;
   border-right: 1px solid transparent;
   transform: rotate(45deg);
+  transition: all ${StyleProps.animations.swift};
 `
-const ListItems = styled.div`
+export const ListItems = styled.div`
   max-height: 400px;
   overflow: auto;
   ${props => props.searchable ? 'margin-top: 8px;' : ''}
 `
-const ListItem = styled.div`
+export const ListItem = styled.div`
   padding-top: 13px;
   color: ${props => props.selected ? Palette.primary : Palette.grayscale[4]};
   cursor: pointer;
@@ -79,7 +81,7 @@ const ListItem = styled.div`
     padding-top: 0;
   }
 `
-const ListItemLabel = styled.div`
+export const ListItemLabel = styled.div`
   word-break: break-all;
   word-break: break-word;
   ${props => props.highlighted ? `font-weight: ${StyleProps.fontWeights.medium};` : ''}

+ 19 - 29
src/components/organisms/DetailsContentHeader/DetailsContentHeader.jsx

@@ -21,7 +21,8 @@ import styled from 'styled-components'
 import type { MainItem } from '../../../types/MainItem'
 import type { Execution } from '../../../types/Execution'
 import StatusPill from '../../atoms/StatusPill'
-import Button from '../../atoms/Button'
+import ActionDropdown from '../../molecules/ActionDropdown'
+import type { Action as DropdownAction } from '../../molecules/ActionDropdown'
 
 import Palette from '../../styleUtils/Palette'
 import StyleProps from '../../styleUtils/StyleProps'
@@ -82,17 +83,12 @@ const MockButton = styled.div`
 
 type Props = {
   onBackButonClick: () => void,
-  onActionButtonClick?: () => void,
-  onCancelClick?: (?Execution | ?MainItem) => void,
+  dropdownActions?: DropdownAction[],
   typeImage?: string,
-  buttonLabel?: string,
   statusLabel?: string,
   item: ?any,
   alertInfoPill?: boolean,
   primaryInfoPill?: boolean,
-  alertButton?: boolean,
-  hollowButton?: boolean,
-  actionButtonDisabled?: boolean,
 }
 @observer
 class DetailsContentHeader extends React.Component<Props> {
@@ -142,34 +138,28 @@ class DetailsContentHeader extends React.Component<Props> {
   }
 
   renderButton() {
-    if (!this.props.onActionButtonClick && this.getStatus() !== 'RUNNING') {
+    if (!this.props.dropdownActions) {
       return <MockButton />
     }
 
-    if (this.getStatus() === 'RUNNING') {
-      return (
-        <Button
-          secondary
-          onClick={() => {
-            // $FlowIssue
-            if (this.props.onCancelClick) this.props.onCancelClick(this.getLastExecution())
-          }}
-          data-test-id="dcHeader-cancelButton"
-        >Cancel</Button>
-      )
-    }
-
     return (
-      <Button
-        secondary={!this.props.alertButton}
-        alert={this.props.alertButton}
-        hollow={this.props.hollowButton}
-        onClick={this.props.onActionButtonClick}
-        disabled={this.props.actionButtonDisabled}
+      <ActionDropdown
+        actions={this.props.dropdownActions}
         style={{ marginLeft: '32px' }}
-        data-test-id="dcHeader-actionButton"
-      >{this.props.buttonLabel}</Button>
+      />
     )
+
+    // return (
+    //   <Button
+    //     secondary={!this.props.alertButton}
+    //     alert={this.props.alertButton}
+    //     hollow={this.props.hollowButton}
+    //     onClick={this.props.onActionButtonClick}
+    //     disabled={this.props.actionButtonDisabled}
+    //     style={{ marginLeft: '32px' }}
+    //     data-test-id="dcHeader-actionButton"
+    //   >{this.props.buttonLabel}</Button>
+    // )
   }
 
   renderDescription() {

+ 14 - 18
src/components/organisms/ReplicaDetailsContent/ReplicaDetailsContent.jsx

@@ -41,14 +41,13 @@ const Buttons = styled.div`
   display: flex;
   justify-content: space-between;
 `
-const LeftButtons = styled.div``
-const RightButtons = styled.div`
+const ButtonColumn = styled.div`
   display: flex;
+  flex-direction: column;
   button {
-    margin-right: 32px;
-
-    &:last-child {
-      margin-right: 0;
+    margin-top: 16px;
+    &:first-child {
+      margin-top: 0;
     }
   }
 `
@@ -85,7 +84,6 @@ type Props = {
   onExecuteClick: () => void,
   onCreateMigrationClick: () => void,
   onDeleteReplicaClick: () => void,
-  onDeleteReplicaDisksClick: () => void,
   onAddScheduleClick: (schedule: ScheduleType) => void,
   onScheduleChange: (scheduleId: ?string, data: ScheduleType, forceSave?: boolean) => void,
   onScheduleRemove: (scheduleId: ?string) => void,
@@ -124,29 +122,27 @@ class ReplicaDetailsContent extends React.Component<Props, State> {
   renderBottomControls() {
     return (
       <Buttons>
-        <LeftButtons>
+        <ButtonColumn>
+          <Button
+            secondary
+            disabled={this.getStatus() === 'RUNNING'}
+            onClick={this.props.onExecuteClick}
+          >Execute Replica</Button>
           <Button
             primary
             disabled={this.isEndpointMissing()}
             onClick={this.props.onCreateMigrationClick}
             data-test-id="rdContent-createButton"
           >Create Migration</Button>
-        </LeftButtons>
-        <RightButtons>
-          <Button
-            alert
-            hollow
-            secondary
-            onClick={this.props.onDeleteReplicaDisksClick}
-            disabled={!this.props.item || !this.props.item.executions || this.props.item.executions.length === 0}
-          >Delete Replica Disks</Button>
+        </ButtonColumn>
+        <ButtonColumn>
           <Button
             alert
             hollow
             onClick={this.props.onDeleteReplicaClick}
             data-test-id="rdContent-deleteButton"
           >Delete Replica</Button>
-        </RightButtons>
+        </ButtonColumn>
       </Buttons>
     )
   }

+ 0 - 1
src/components/pages/EndpointDetailsPage/EndpointDetailsPage.jsx

@@ -191,7 +191,6 @@ class EndpointDetailsPage extends React.Component<Props, State> {
           contentHeaderComponent={<DetailsContentHeader
             item={(endpoint: any)}
             onBackButonClick={() => { this.handleBackButtonClick() }}
-            onCancelClick={() => { }}
             typeImage={endpointImage}
             description={endpoint ? endpoint.description : ''}
           />}

+ 16 - 1
src/components/pages/MigrationDetailsPage/MigrationDetailsPage.jsx

@@ -32,6 +32,7 @@ import instanceStore from '../../../stores/InstanceStore'
 import { requestPollTimeout } from '../../../config'
 
 import migrationImage from './images/migration.svg'
+import Palette from '../../styleUtils/Palette'
 
 const Wrapper = styled.div``
 
@@ -141,7 +142,21 @@ class MigrationDetailsPage extends React.Component<Props, State> {
     migrationStore.getMigration(this.props.match.params.id, false)
   }
 
+  getStatus() {
+    return migrationStore.migrationDetails && migrationStore.migrationDetails.status
+  }
+
   render() {
+    let dropdownActions = [{
+      label: 'Cancel',
+      disabled: this.getStatus() !== 'RUNNING',
+      action: () => { this.handleCancelMigrationClick() },
+    }, {
+      label: 'Delete Migration',
+      color: Palette.alert,
+      action: () => { this.handleDeleteMigrationClick() },
+    }]
+
     return (
       <Wrapper>
         <DetailsTemplate
@@ -153,8 +168,8 @@ class MigrationDetailsPage extends React.Component<Props, State> {
             item={migrationStore.migrationDetails}
             onBackButonClick={() => { this.handleBackButtonClick() }}
             typeImage={migrationImage}
+            dropdownActions={dropdownActions}
             primaryInfoPill
-            onCancelClick={() => { this.handleCancelMigrationClick() }}
           />}
           contentComponent={<MigrationDetailsContent
             item={migrationStore.migrationDetails}

+ 46 - 18
src/components/pages/ReplicaDetailsPage/ReplicaDetailsPage.jsx

@@ -41,6 +41,7 @@ import networkStore from '../../../stores/NetworkStore'
 import { requestPollTimeout } from '../../../config'
 
 import replicaImage from './images/replica.svg'
+import Palette from '../../styleUtils/Palette'
 
 const Wrapper = styled.div``
 
@@ -111,14 +112,24 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
     })
   }
 
-  isActionButtonDisabled() {
+  getLastExecution() {
+    if (replicaStore.replicaDetails && replicaStore.replicaDetails.executions && replicaStore.replicaDetails.executions.length) {
+      return replicaStore.replicaDetails.executions[replicaStore.replicaDetails.executions.length - 1]
+    }
+
+    return null
+  }
+
+  getStatus() {
+    let lastExecution = this.getLastExecution()
+    return lastExecution && lastExecution.status
+  }
+
+  isExecuteDisabled() {
     let originEndpoint = endpointStore.endpoints.find(e => replicaStore.replicaDetails && e.id === replicaStore.replicaDetails.origin_endpoint_id)
     let targetEndpoint = endpointStore.endpoints.find(e => replicaStore.replicaDetails && e.id === replicaStore.replicaDetails.destination_endpoint_id)
-    let lastExecution = replicaStore.replicaDetails && replicaStore.replicaDetails.executions && replicaStore.replicaDetails.executions.length
-      && replicaStore.replicaDetails.executions[replicaStore.replicaDetails.executions.length - 1]
-    let status = lastExecution && lastExecution.status
 
-    return Boolean(!originEndpoint || !targetEndpoint || status === 'RUNNING')
+    return Boolean(!originEndpoint || !targetEndpoint || this.getStatus() === 'RUNNING')
   }
 
   handleUserItemClick(item: { value: string }) {
@@ -137,7 +148,7 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
     window.location.href = '/#/replicas'
   }
 
-  handleActionButtonClick() {
+  handleExecuteClick() {
     this.setState({ showOptionsModal: true })
   }
 
@@ -228,7 +239,11 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
     }
   }
 
-  handleCancelExecutionClick(confirmationItem: ?Execution) {
+  handleCancelLastExecutionClick() {
+    this.handleCancelExecution(this.getLastExecution())
+  }
+
+  handleCancelExecution(confirmationItem: ?Execution) {
     this.setState({ confirmationItem, showCancelConfirmation: true })
   }
 
@@ -262,6 +277,27 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
   }
 
   render() {
+    let dropdownActions = [{
+      label: 'Execute',
+      action: () => { this.handleExecuteClick() },
+      hidden: this.isExecuteDisabled(),
+    }, {
+      label: 'Cancel',
+      hidden: this.getStatus() !== 'RUNNING',
+      action: () => { this.handleCancelLastExecutionClick() },
+    }, {
+      label: 'Create Migration',
+      color: Palette.primary,
+      action: () => { this.handleCreateMigrationClick() },
+    }, {
+      label: 'Delete Disks',
+      action: () => { this.handleDeleteReplicaDisksClick() },
+    }, {
+      label: 'Delete Replica',
+      color: Palette.alert,
+      action: () => { this.handleDeleteReplicaClick() },
+    }]
+
     return (
       <Wrapper>
         <DetailsTemplate
@@ -272,16 +308,9 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
           contentHeaderComponent={<DetailsContentHeader
             item={replicaStore.replicaDetails}
             onBackButonClick={() => { this.handleBackButtonClick() }}
-            onActionButtonClick={() => { this.handleActionButtonClick() }}
-            onCancelClick={item => {
-              let any: any = item
-              let execution: Execution = any
-              this.handleCancelExecutionClick(execution)
-            }}
-            actionButtonDisabled={this.isActionButtonDisabled()}
+            dropdownActions={dropdownActions}
             typeImage={replicaImage}
             alertInfoPill
-            buttonLabel="Execute Now"
           />}
           contentComponent={<ReplicaDetailsContent
             item={replicaStore.replicaDetails}
@@ -293,12 +322,11 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
             detailsLoading={replicaStore.detailsLoading || endpointStore.loading}
             executionsLoading={replicaStore.executionsLoading}
             page={this.props.match.params.page || ''}
-            onCancelExecutionClick={execution => { this.handleCancelExecutionClick(execution) }}
+            onCancelExecutionClick={execution => { this.handleCancelExecution(execution) }}
             onDeleteExecutionClick={execution => { this.handleDeleteExecutionClick(execution) }}
-            onExecuteClick={() => { this.handleActionButtonClick() }}
+            onExecuteClick={() => { this.handleExecuteClick() }}
             onCreateMigrationClick={() => { this.handleCreateMigrationClick() }}
             onDeleteReplicaClick={() => { this.handleDeleteReplicaClick() }}
-            onDeleteReplicaDisksClick={() => { this.handleDeleteReplicaDisksClick() }}
             onAddScheduleClick={schedule => { this.handleAddScheduleClick(schedule) }}
             onScheduleChange={(scheduleId, data, forceSave) => { this.handleScheduleChange(scheduleId, data, forceSave) }}
             onScheduleRemove={scheduleId => { this.handleScheduleRemove(scheduleId) }}

+ 1 - 1
src/components/styleUtils/Palette.js

@@ -18,7 +18,7 @@ const Palette = {
   primary: '#0044CB',
   primaryLight: '#000EA9',
   secondary: '#D9DCE3',
-  secondaryLight: '#7F8795',
+  secondaryLight: '#777A8B',
   black: '#202134',
   alert: '#F91661',
   success: '#4CD964',

+ 2 - 2
src/utils/TestWrapper.js

@@ -17,11 +17,11 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 import type { ShallowWrapper } from 'enzyme'
 
 export default class TestWrapper {
-  shallow: ShallowWrapper
+  shallow: ShallowWrapper<any>
   baseId: ?string
   length: number
 
-  constructor(wrapper: ShallowWrapper, baseId?: ?string) {
+  constructor(wrapper: ShallowWrapper<any>, baseId?: ?string) {
     this.shallow = wrapper
     this.baseId = baseId
   }