Przeglądaj źródła

Add "Actions" button to all details pages

Sergiu Miclea 7 lat temu
rodzic
commit
f08b014929

+ 9 - 14
src/components/molecules/ActionDropdown/ActionDropdown.jsx

@@ -85,8 +85,8 @@ class ActionDropdown extends React.Component<Props, State> {
 
 
   itemMouseDown: boolean
   itemMouseDown: boolean
   listRef: HTMLElement
   listRef: HTMLElement
-  arrowRef: HTMLElement
   tipRef: HTMLElement
   tipRef: HTMLElement
+  buttonRef: HTMLElement
 
 
   componentDidMount() {
   componentDidMount() {
     window.addEventListener('mousedown', this.handlePageClick, false)
     window.addEventListener('mousedown', this.handlePageClick, false)
@@ -101,17 +101,12 @@ class ActionDropdown extends React.Component<Props, State> {
   }
   }
 
 
   updateListPosition() {
   updateListPosition() {
-    if (!this.state.showDropdownList || !this.listRef || !this.arrowRef || !this.tipRef) {
+    if (!this.state.showDropdownList || !this.listRef || !this.tipRef || !this.buttonRef) {
       return
       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()
+    let tipHeight = this.tipRef.offsetHeight / 2
+    let topOffset = 6
+    let buttonRect = this.buttonRef.getBoundingClientRect()
 
 
     // If a modal is opened, body scroll is removed and body top is set to replicate scroll position
     // If a modal is opened, body scroll is removed and body top is set to replicate scroll position
     let scrollOffset = 0
     let scrollOffset = 0
@@ -119,8 +114,8 @@ class ActionDropdown extends React.Component<Props, State> {
       scrollOffset = -parseInt(document.body && document.body.style.top, 10)
       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`
+    this.listRef.style.top = `${buttonRect.top + buttonRect.height + tipHeight + topOffset + (window.pageYOffset || scrollOffset)}px`
+    this.listRef.style.left = `${buttonRect.left + window.pageXOffset}px`
   }
   }
 
 
   @autobind
   @autobind
@@ -179,7 +174,7 @@ class ActionDropdown extends React.Component<Props, State> {
     return ReactDOM.createPortal((
     return ReactDOM.createPortal((
       <List
       <List
         innerRef={list => { this.listRef = list }}
         innerRef={list => { this.listRef = list }}
-        width={`${StyleProps.inputSizes.regular.width - 4}px`}
+        width={`${StyleProps.inputSizes.regular.width}px`}
         padding={0}
         padding={0}
         customStyle={ListStyle}
         customStyle={ListStyle}
       >
       >
@@ -196,8 +191,8 @@ class ActionDropdown extends React.Component<Props, State> {
           secondary
           secondary
           centered
           centered
           value={this.props.label}
           value={this.props.label}
+          customRef={ref => { this.buttonRef = ref }}
           onClick={() => { this.handleButtonClick() }}
           onClick={() => { this.handleButtonClick() }}
-          arrowRef={ref => { this.arrowRef = ref }}
         />
         />
         {this.renderList()}
         {this.renderList()}
       </Wrapper>
       </Wrapper>

+ 1 - 1
src/components/molecules/DropdownLink/DropdownLink.jsx

@@ -54,7 +54,7 @@ export const List = styled.div`
 export const Tip = styled.div`
 export const Tip = styled.div`
   position: absolute;
   position: absolute;
   top: -6px;
   top: -6px;
-  right: 8px;
+  right: 11px;
   width: 10px;
   width: 10px;
   height: 10px;
   height: 10px;
   background: ${Palette.grayscale[1]};
   background: ${Palette.grayscale[1]};

+ 2 - 9
src/components/organisms/EndpointDetailsContent/EndpointDetailsContent.jsx

@@ -59,14 +59,9 @@ const Value = styled.div``
 const Buttons = styled.div`
 const Buttons = styled.div`
   display: flex;
   display: flex;
   justify-content: space-between;
   justify-content: space-between;
+  margin-top: 64px;
 `
 `
-const MainButtons = styled.div`
-  display: flex;
-  flex-direction: column;
-  button {
-    margin-bottom: 16px;
-  }
-`
+const MainButtons = styled.div``
 const DeleteButton = styled.div``
 const DeleteButton = styled.div``
 const LoadingWrapper = styled.div`
 const LoadingWrapper = styled.div`
   display: flex;
   display: flex;
@@ -87,7 +82,6 @@ type Props = {
   usage: { migrations: MainItem[], replicas: MainItem[] },
   usage: { migrations: MainItem[], replicas: MainItem[] },
   onDeleteClick: () => void,
   onDeleteClick: () => void,
   onValidateClick: () => void,
   onValidateClick: () => void,
-  onEditClick: () => void,
   passwordFields?: string[],
   passwordFields?: string[],
 }
 }
 @observer
 @observer
@@ -157,7 +151,6 @@ class EndpointDetailsContent extends React.Component<Props> {
     return (
     return (
       <Buttons>
       <Buttons>
         <MainButtons>
         <MainButtons>
-          <Button secondary onClick={this.props.onEditClick} data-test-id="edContent-editButton">Edit Endpoint</Button>
           <Button onClick={this.props.onValidateClick} data-test-id="edContent-validateButton">Validate Endpoint</Button>
           <Button onClick={this.props.onValidateClick} data-test-id="edContent-validateButton">Validate Endpoint</Button>
         </MainButtons>
         </MainButtons>
         <DeleteButton>
         <DeleteButton>

+ 1 - 4
src/components/organisms/EndpointDetailsContent/test.jsx

@@ -83,13 +83,10 @@ describe('EndpointDetailsContent Component', () => {
   it('dispatches buttons clicks', () => {
   it('dispatches buttons clicks', () => {
     let onDeleteClick = sinon.spy()
     let onDeleteClick = sinon.spy()
     let onValidateClick = sinon.spy()
     let onValidateClick = sinon.spy()
-    let onEditClick = sinon.spy()
 
 
-    let wrapper = wrap({ item, onDeleteClick, onValidateClick, onEditClick })
-    wrapper.find('editButton').click()
+    let wrapper = wrap({ item, onDeleteClick, onValidateClick })
     wrapper.find('validateButton').click()
     wrapper.find('validateButton').click()
     wrapper.find('deleteButton').click()
     wrapper.find('deleteButton').click()
-    expect(onEditClick.calledOnce).toBe(true)
     expect(onValidateClick.calledOnce).toBe(true)
     expect(onValidateClick.calledOnce).toBe(true)
     expect(onDeleteClick.calledOnce).toBe(true)
     expect(onDeleteClick.calledOnce).toBe(true)
   })
   })

+ 4 - 41
src/components/organisms/ProjectDetailsContent/ProjectDetailsContent.jsx

@@ -40,7 +40,7 @@ const Info = styled.div`
   display: flex;
   display: flex;
   flex-wrap: wrap;
   flex-wrap: wrap;
   margin-top: 32px;
   margin-top: 32px;
-  margin-left: -32px;  
+  margin-left: -32px;
 `
 `
 const Field = styled.div`
 const Field = styled.div`
   ${StyleProps.exactWidth('calc(50% - 32px)')}
   ${StyleProps.exactWidth('calc(50% - 32px)')}
@@ -66,10 +66,9 @@ const TableStyled = styled(Table)`
   margin-bottom: 32px;
   margin-bottom: 32px;
 `
 `
 const Buttons = styled.div`
 const Buttons = styled.div`
-  margin-top: 32px;
+  margin-top: 64px;
   display: flex;
   display: flex;
   justify-content: space-between;
   justify-content: space-between;
-  margin-top: 32px;
 `
 `
 const UserColumn = styled.div`
 const UserColumn = styled.div`
   ${props => props.disabled ? css`color: ${Palette.grayscale[3]};` : ''}
   ${props => props.disabled ? css`color: ${Palette.grayscale[3]};` : ''}
@@ -96,27 +95,23 @@ type Props = {
   loading: boolean,
   loading: boolean,
   users: User[],
   users: User[],
   usersLoading: boolean,
   usersLoading: boolean,
-  deleteDisabled: boolean,
   roleAssignments: RoleAssignment[],
   roleAssignments: RoleAssignment[],
   roles: Role[],
   roles: Role[],
   loggedUserId: string,
   loggedUserId: string,
   onEnableUser: (user: User) => void,
   onEnableUser: (user: User) => void,
   onRemoveUser: (user: User) => void,
   onRemoveUser: (user: User) => void,
   onUserRoleChange: (user: User, roleId: string, toggled: boolean) => void,
   onUserRoleChange: (user: User, roleId: string, toggled: boolean) => void,
-  onEditProjectClick: () => void,
-  onDeleteConfirmation: () => void,
   onAddMemberClick: () => void,
   onAddMemberClick: () => void,
+  onDeleteClick: () => void,
 }
 }
 type State = {
 type State = {
   showRemoveUserAlert: boolean,
   showRemoveUserAlert: boolean,
-  showDeleteProjectAlert: boolean,
 }
 }
 const testName = 'pdContent'
 const testName = 'pdContent'
 @observer
 @observer
 class ProjectDetailsContent extends React.Component<Props, State> {
 class ProjectDetailsContent extends React.Component<Props, State> {
   state = {
   state = {
     showRemoveUserAlert: false,
     showRemoveUserAlert: false,
-    showDeleteProjectAlert: false,
   }
   }
 
 
   selectedUser: ?User
   selectedUser: ?User
@@ -151,15 +146,6 @@ class ProjectDetailsContent extends React.Component<Props, State> {
     this.setState({ showRemoveUserAlert: false })
     this.setState({ showRemoveUserAlert: false })
   }
   }
 
 
-  handleDeleteConfirmation() {
-    this.setState({ showDeleteProjectAlert: false })
-    this.props.onDeleteConfirmation()
-  }
-
-  handleCloseDeleteConfirmation() {
-    this.setState({ showDeleteProjectAlert: false })
-  }
-
   renderLoading() {
   renderLoading() {
     return (
     return (
       <LoadingWrapper>
       <LoadingWrapper>
@@ -174,10 +160,6 @@ class ProjectDetailsContent extends React.Component<Props, State> {
     return (
     return (
       <Buttons>
       <Buttons>
         <ButtonsColumn>
         <ButtonsColumn>
-          <Button
-            secondary
-            onClick={this.props.onEditProjectClick}
-          >Edit Project</Button>
           <Button
           <Button
             onClick={this.props.onAddMemberClick}
             onClick={this.props.onAddMemberClick}
           >Add Member</Button>
           >Add Member</Button>
@@ -186,7 +168,7 @@ class ProjectDetailsContent extends React.Component<Props, State> {
           <Button
           <Button
             alert
             alert
             hollow
             hollow
-            onClick={() => { this.setState({ showDeleteProjectAlert: true }) }}
+            onClick={() => { this.props.onDeleteClick() }}
           >Delete Project</Button>
           >Delete Project</Button>
         </ButtonsColumn>
         </ButtonsColumn>
       </Buttons>
       </Buttons>
@@ -328,25 +310,6 @@ class ProjectDetailsContent extends React.Component<Props, State> {
             onRequestClose={() => { this.handleCloseRemoveUserConfirmation() }}
             onRequestClose={() => { this.handleCloseRemoveUserConfirmation() }}
           />
           />
         ) : null}
         ) : null}
-        {this.state.showDeleteProjectAlert && !this.props.deleteDisabled ? (
-          <AlertModal
-            isOpen
-            title="Delete Project?"
-            message="Are you sure you want to delete this project?"
-            extraMessage="Deleting a Coriolis Project is permanent!"
-            onConfirmation={() => { this.handleDeleteConfirmation() }}
-            onRequestClose={() => { this.handleCloseDeleteConfirmation() }}
-          />
-        ) : this.state.showDeleteProjectAlert && this.props.deleteDisabled ? (
-          <AlertModal
-            isOpen
-            type="error"
-            title="Error deleting project"
-            message="The project can't be deleted"
-            extraMessage="You can't delete the last project since you'll no longer be able to log in"
-            onRequestClose={() => { this.handleCloseDeleteConfirmation() }}
-          />
-        ) : null}
       </Wrapper>
       </Wrapper>
     )
     )
   }
   }

+ 1 - 1
src/components/organisms/ProjectDetailsContent/test.jsx

@@ -34,7 +34,7 @@ type Props = {
 const wrap = (props: Props) => new TW(shallow(
 const wrap = (props: Props) => new TW(shallow(
   <ProjectDetailsContent
   <ProjectDetailsContent
     onAddMemberClick={() => { }}
     onAddMemberClick={() => { }}
-    onDeleteConfirmation={() => { }}
+    onDeleteClick={() => { }}
     onEditProjectClick={() => { }}
     onEditProjectClick={() => { }}
     onEnableUser={() => { }}
     onEnableUser={() => { }}
     onRemoveUser={() => { }}
     onRemoveUser={() => { }}

+ 5 - 35
src/components/organisms/UserDetailsContent/UserDetailsContent.jsx

@@ -22,7 +22,6 @@ import CopyValue from '../../atoms/CopyValue'
 import CopyMultilineValue from '../../atoms/CopyMultilineValue'
 import CopyMultilineValue from '../../atoms/CopyMultilineValue'
 import StatusImage from '../../atoms/StatusImage'
 import StatusImage from '../../atoms/StatusImage'
 import Button from '../../atoms/Button'
 import Button from '../../atoms/Button'
-import AlertModal from '../../organisms/AlertModal'
 
 
 import type { User } from '../../../types/User'
 import type { User } from '../../../types/User'
 import type { Project } from '../../../types/Project'
 import type { Project } from '../../../types/Project'
@@ -38,7 +37,7 @@ const Info = styled.div`
   display: flex;
   display: flex;
   flex-wrap: wrap;
   flex-wrap: wrap;
   margin-top: 32px;
   margin-top: 32px;
-  margin-left: -32px;  
+  margin-left: -32px;
 `
 `
 const Link = styled.a`
 const Link = styled.a`
   color: ${Palette.primary};
   color: ${Palette.primary};
@@ -64,10 +63,9 @@ const LoadingWrapper = styled.div`
   margin: 32px 0 64px 0;
   margin: 32px 0 64px 0;
 `
 `
 const Buttons = styled.div`
 const Buttons = styled.div`
-  margin-top: 32px;
+  margin-top: 64px;
   display: flex;
   display: flex;
   justify-content: space-between;
   justify-content: space-between;
-  margin-top: 32px;
 `
 `
 const ButtonsColumn = styled.div`
 const ButtonsColumn = styled.div`
   display: flex;
   display: flex;
@@ -87,29 +85,12 @@ type Props = {
   projects: Project[],
   projects: Project[],
   userProjects: Project[],
   userProjects: Project[],
   isLoggedUser: boolean,
   isLoggedUser: boolean,
-  onEditClick: () => void,
   onUpdatePasswordClick: () => void,
   onUpdatePasswordClick: () => void,
-  onDeleteConfirmation: () => void,
-}
-type State = {
-  showDeleteConfirmation: boolean,
+  onDeleteClick: () => void,
 }
 }
 const testName = 'udContent'
 const testName = 'udContent'
 @observer
 @observer
-class UserDetailsContent extends React.Component<Props, State> {
-  state = {
-    showDeleteConfirmation: false,
-  }
-
-  handleDeleteConfirmation() {
-    this.setState({ showDeleteConfirmation: false })
-    this.props.onDeleteConfirmation()
-  }
-
-  handleCloseDeleteConfirmation() {
-    this.setState({ showDeleteConfirmation: false })
-  }
-
+class UserDetailsContent extends React.Component<Props> {
   renderLoading() {
   renderLoading() {
     if (!this.props.loading) {
     if (!this.props.loading) {
       return null
       return null
@@ -128,14 +109,13 @@ class UserDetailsContent extends React.Component<Props, State> {
     return (
     return (
       <Buttons>
       <Buttons>
         <ButtonsColumn>
         <ButtonsColumn>
-          <Button secondary onClick={this.props.onEditClick}>Edit user</Button>
           <Button hollow onClick={this.props.onUpdatePasswordClick}>Change password</Button>
           <Button hollow onClick={this.props.onUpdatePasswordClick}>Change password</Button>
         </ButtonsColumn>
         </ButtonsColumn>
         <ButtonsColumn>
         <ButtonsColumn>
           <Button
           <Button
             alert
             alert
             hollow
             hollow
-            onClick={() => { this.setState({ showDeleteConfirmation: true }) }}
+            onClick={() => { this.props.onDeleteClick() }}
             disabled={this.props.isLoggedUser}
             disabled={this.props.isLoggedUser}
           >Delete user</Button>
           >Delete user</Button>
         </ButtonsColumn>
         </ButtonsColumn>
@@ -222,16 +202,6 @@ class UserDetailsContent extends React.Component<Props, State> {
         {this.renderInfo()}
         {this.renderInfo()}
         {this.renderLoading()}
         {this.renderLoading()}
         {this.renderButtons()}
         {this.renderButtons()}
-        {this.state.showDeleteConfirmation ? (
-          <AlertModal
-            isOpen
-            title="Delete User?"
-            message="Are you sure you want to delete this user?"
-            extraMessage="Deleting a Coriolis User is permanent!"
-            onConfirmation={() => { this.handleDeleteConfirmation() }}
-            onRequestClose={() => { this.handleCloseDeleteConfirmation() }}
-          />
-        ) : null}
       </Wrapper>
       </Wrapper>
     )
     )
   }
   }

+ 1 - 0
src/components/organisms/UserDetailsContent/test.jsx

@@ -33,6 +33,7 @@ const wrap = (props: Props) => new TW(shallow(
     onEditClick={() => { }}
     onEditClick={() => { }}
     onUpdatePasswordClick={() => { }}
     onUpdatePasswordClick={() => { }}
     onDeleteConfirmation={() => { }}
     onDeleteConfirmation={() => { }}
+    onDeleteClick={() => { }}
     {...props}
     {...props}
   />
   />
 ), 'udContent')
 ), 'udContent')

+ 72 - 3
src/components/pages/EndpointDetailsPage/EndpointDetailsPage.jsx

@@ -26,20 +26,26 @@ import AlertModal from '../../organisms/AlertModal'
 import Modal from '../../molecules/Modal'
 import Modal from '../../molecules/Modal'
 import EndpointValidation from '../../organisms/EndpointValidation'
 import EndpointValidation from '../../organisms/EndpointValidation'
 import Endpoint from '../../organisms/Endpoint'
 import Endpoint from '../../organisms/Endpoint'
+import EndpointDuplicateOptions from '../../organisms/EndpointDuplicateOptions'
 
 
 import endpointStore, { passwordFields } from '../../../stores/EndpointStore'
 import endpointStore, { passwordFields } from '../../../stores/EndpointStore'
 import migrationStore from '../../../stores/MigrationStore'
 import migrationStore from '../../../stores/MigrationStore'
 import replicaStore from '../../../stores/ReplicaStore'
 import replicaStore from '../../../stores/ReplicaStore'
 import userStore from '../../../stores/UserStore'
 import userStore from '../../../stores/UserStore'
+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 type { MainItem } from '../../../types/MainItem'
 
 
+import Palette from '../../styleUtils/Palette'
+
 import endpointImage from './images/endpoint.svg'
 import endpointImage from './images/endpoint.svg'
 
 
 const Wrapper = styled.div``
 const Wrapper = styled.div``
 
 
 type Props = {
 type Props = {
   match: any,
   match: any,
+  history: any,
 }
 }
 type State = {
 type State = {
   showDeleteEndpointConfirmation: boolean,
   showDeleteEndpointConfirmation: boolean,
@@ -47,7 +53,9 @@ type State = {
   showEndpointModal: boolean,
   showEndpointModal: boolean,
   showEndpointInUseModal: boolean,
   showEndpointInUseModal: boolean,
   showEndpointInUseLoadingModal: boolean,
   showEndpointInUseLoadingModal: boolean,
-  endpointUsage: { replicas: MainItem[], migrations: MainItem[] }
+  endpointUsage: { replicas: MainItem[], migrations: MainItem[] },
+  showDuplicateModal: boolean,
+  duplicating: boolean,
 }
 }
 @observer
 @observer
 class EndpointDetailsPage extends React.Component<Props, State> {
 class EndpointDetailsPage extends React.Component<Props, State> {
@@ -57,6 +65,8 @@ class EndpointDetailsPage extends React.Component<Props, State> {
     showEndpointModal: false,
     showEndpointModal: false,
     showEndpointInUseModal: false,
     showEndpointInUseModal: false,
     showEndpointInUseLoadingModal: false,
     showEndpointInUseLoadingModal: false,
+    showDuplicateModal: false,
+    duplicating: false,
     endpointUsage: { replicas: [], migrations: [] },
     endpointUsage: { replicas: [], migrations: [] },
   }
   }
 
 
@@ -163,7 +173,32 @@ class EndpointDetailsPage extends React.Component<Props, State> {
     this.setState({ showEndpointInUseModal: false })
     this.setState({ showEndpointInUseModal: false })
   }
   }
 
 
+  handleDuplicateClick() {
+    this.setState({ showDuplicateModal: true })
+  }
+
+  handleDuplicate(projectId: string) {
+    let endpoint = this.getEndpoint()
+    if (!endpoint) {
+      return
+    }
+
+    this.setState({ duplicating: true })
+
+    let shouldSwitchProject = projectId !== (userStore.loggedUser ? userStore.loggedUser.project.id : '')
+
+    endpointStore.duplicate({
+      shouldSwitchProject,
+      endpoints: [endpoint],
+      onSwitchProject: () => userStore.switchProject(projectId),
+    }).then(() => {
+      this.props.history.push('/endpoints')
+    })
+  }
+
   loadData() {
   loadData() {
+    projectStore.getProjects()
+
     endpointStore.getEndpoints().then(() => {
     endpointStore.getEndpoints().then(() => {
       let endpoint = this.getEndpoint()
       let endpoint = this.getEndpoint()
 
 
@@ -180,7 +215,26 @@ class EndpointDetailsPage extends React.Component<Props, State> {
   }
   }
 
 
   render() {
   render() {
+    let selectedProjectId = userStore.loggedUser ? userStore.loggedUser.project.id : ''
+
     let endpoint = this.getEndpoint()
     let endpoint = this.getEndpoint()
+    let dropdownActions = [{
+      label: 'Validate',
+      color: Palette.primary,
+      action: () => { this.handleValidateClick() },
+    }, {
+      label: 'Edit',
+      action: () => { this.handleEditClick() },
+
+    }, {
+      label: 'Duplicate',
+      action: () => { this.handleDuplicateClick() },
+
+    }, {
+      label: 'Delete Endpoint',
+      color: Palette.alert,
+      action: () => { this.handleDeleteEndpointClick() },
+    }]
     return (
     return (
       <Wrapper>
       <Wrapper>
         <DetailsTemplate
         <DetailsTemplate
@@ -189,8 +243,9 @@ class EndpointDetailsPage extends React.Component<Props, State> {
             onUserItemClick={item => { this.handleUserItemClick(item) }}
             onUserItemClick={item => { this.handleUserItemClick(item) }}
           />}
           />}
           contentHeaderComponent={<DetailsContentHeader
           contentHeaderComponent={<DetailsContentHeader
-            item={(endpoint: any)}
+            item={endpoint}
             onBackButonClick={() => { this.handleBackButtonClick() }}
             onBackButonClick={() => { this.handleBackButtonClick() }}
+            dropdownActions={dropdownActions}
             typeImage={endpointImage}
             typeImage={endpointImage}
             description={endpoint ? endpoint.description : ''}
             description={endpoint ? endpoint.description : ''}
           />}
           />}
@@ -202,7 +257,6 @@ class EndpointDetailsPage extends React.Component<Props, State> {
             connectionInfo={endpointStore.connectionInfo}
             connectionInfo={endpointStore.connectionInfo}
             onDeleteClick={() => { this.handleDeleteEndpointClick() }}
             onDeleteClick={() => { this.handleDeleteEndpointClick() }}
             onValidateClick={() => { this.handleValidateClick() }}
             onValidateClick={() => { this.handleValidateClick() }}
-            onEditClick={() => { this.handleEditClick() }}
           />}
           />}
         />
         />
         <AlertModal
         <AlertModal
@@ -249,6 +303,21 @@ class EndpointDetailsPage extends React.Component<Props, State> {
             onCancelClick={() => { this.handleCloseEndpointModal() }}
             onCancelClick={() => { this.handleCloseEndpointModal() }}
           />
           />
         </Modal>
         </Modal>
+        {this.state.showDuplicateModal ? (
+          <Modal
+            isOpen
+            title="Duplicate Endpoint"
+            onRequestClose={() => { this.setState({ showDuplicateModal: false }) }}
+          >
+            <EndpointDuplicateOptions
+              duplicating={this.state.duplicating}
+              projects={projectStore.projects}
+              selectedProjectId={selectedProjectId}
+              onCancelClick={() => { this.setState({ showDuplicateModal: false }) }}
+              onDuplicateClick={projectId => { this.handleDuplicate(projectId) }}
+            />
+          </Modal>
+        ) : null}
       </Wrapper>
       </Wrapper>
     )
     )
   }
   }

+ 9 - 40
src/components/pages/EndpointsPage/EndpointsPage.jsx

@@ -33,11 +33,9 @@ import endpointImage from './images/endpoint-large.svg'
 
 
 import projectStore from '../../../stores/ProjectStore'
 import projectStore from '../../../stores/ProjectStore'
 import userStore from '../../../stores/UserStore'
 import userStore from '../../../stores/UserStore'
-import EndpointSource from '../../../sources/EndpointSource'
 import endpointStore from '../../../stores/EndpointStore'
 import endpointStore from '../../../stores/EndpointStore'
 import migrationStore from '../../../stores/MigrationStore'
 import migrationStore from '../../../stores/MigrationStore'
 import replicaStore from '../../../stores/ReplicaStore'
 import replicaStore from '../../../stores/ReplicaStore'
-import notificationStore from '../../../stores/NotificationStore'
 import providerStore from '../../../stores/ProviderStore'
 import providerStore from '../../../stores/ProviderStore'
 import LabelDictionary from '../../../utils/LabelDictionary'
 import LabelDictionary from '../../../utils/LabelDictionary'
 import { requestPollTimeout } from '../../../config.js'
 import { requestPollTimeout } from '../../../config.js'
@@ -162,44 +160,15 @@ class EndpointsPage extends React.Component<{}, State> {
   handleDuplicate(projectId: string) {
   handleDuplicate(projectId: string) {
     this.setState({ modalIsOpen: false, duplicating: true })
     this.setState({ modalIsOpen: false, duplicating: true })
 
 
-    let selectedProjectId = userStore.loggedUser ? userStore.loggedUser.project.id : ''
-    let switchProject = projectId !== selectedProjectId
-
-    let endpoints = []
-    let items = this.state.confirmationItems || []
-    Promise.all(items.map(endpoint => {
-      return EndpointSource.getConnectionInfo(endpoint).then(connectionInfo => {
-        endpoints.push({
-          ...endpoint,
-          connection_info: connectionInfo,
-          name: `${endpoint.name}${!switchProject ? ' (copy)' : ''}`,
-        })
-      })
-    })).then(() => {
-      if (switchProject) {
-        return userStore.switchProject(projectId).then(() => {
-          this.handleProjectChange()
-        })
-      }
-      return Promise.resolve()
-    }).then(() => {
-      return Promise.all(endpoints.map(endpoint => {
-        return EndpointSource.add(endpoint, true)
-      }).map((p: Promise<any>) => p.catch(e => e)))
-        .then((results: (Endpoint | { status: string, data?: { description: string } })[]) => {
-          let internalServerErrors = results.filter(r => r.status && r.status === 500)
-          if (internalServerErrors.length > 0) {
-            notificationStore.alert(`There was a problem duplicating ${internalServerErrors.length} endpoint${internalServerErrors.length > 1 ? 's' : ''}`, 'error')
-          }
-          let forbiddenErrors = results.filter(r => r.status && r.status === 403)
-          if (forbiddenErrors.length > 0 && forbiddenErrors[0].data && forbiddenErrors[0].data.description) {
-            notificationStore.alert(String(forbiddenErrors[0].data.description), 'error')
-          }
-        })
-    }).catch(e => {
-      if (e.data && e.data.description) {
-        notificationStore.alert(e.data.description, 'error')
-      }
+    let shouldSwitchProject = projectId !== (userStore.loggedUser ? userStore.loggedUser.project.id : '')
+    let endpoints = this.state.confirmationItems || []
+
+    endpointStore.duplicate({
+      shouldSwitchProject,
+      endpoints,
+      onSwitchProject: () => userStore.switchProject(projectId).then(() => {
+        this.handleProjectChange()
+      }),
     }).then(() => {
     }).then(() => {
       this.pollData(true)
       this.pollData(true)
       this.setState({ showDuplicateModal: false, duplicating: false })
       this.setState({ showDuplicateModal: false, duplicating: false })

+ 45 - 3
src/components/pages/ProjectDetailsPage/ProjectDetailsPage.jsx

@@ -26,11 +26,14 @@ import DetailsContentHeader from '../../organisms/DetailsContentHeader'
 import ProjectDetailsContent from '../../organisms/ProjectDetailsContent'
 import ProjectDetailsContent from '../../organisms/ProjectDetailsContent'
 import ProjectModal from '../../organisms/ProjectModal'
 import ProjectModal from '../../organisms/ProjectModal'
 import ProjectMemberModal from '../../organisms/ProjectMemberModal'
 import ProjectMemberModal from '../../organisms/ProjectMemberModal'
+import AlertModal from '../../organisms/AlertModal'
 
 
 import projectStore from '../../../stores/ProjectStore'
 import projectStore from '../../../stores/ProjectStore'
 import userStore from '../../../stores/UserStore'
 import userStore from '../../../stores/UserStore'
 import notificationStore from '../../../stores/NotificationStore'
 import notificationStore from '../../../stores/NotificationStore'
 
 
+import Palette from '../../styleUtils/Palette'
+
 import projectImage from './images/project.svg'
 import projectImage from './images/project.svg'
 
 
 const Wrapper = styled.div``
 const Wrapper = styled.div``
@@ -41,6 +44,7 @@ type Props = {
 type State = {
 type State = {
   showProjectModal: boolean,
   showProjectModal: boolean,
   showAddMemberModal: boolean,
   showAddMemberModal: boolean,
+  showDeleteProjectAlert: boolean,
   addingMember: boolean,
   addingMember: boolean,
 }
 }
 @observer
 @observer
@@ -48,6 +52,7 @@ class ProjectDetailsPage extends React.Component<Props, State> {
   state = {
   state = {
     showProjectModal: false,
     showProjectModal: false,
     showAddMemberModal: false,
     showAddMemberModal: false,
+    showDeleteProjectAlert: false,
     addingMember: false,
     addingMember: false,
   }
   }
 
 
@@ -118,6 +123,8 @@ class ProjectDetailsPage extends React.Component<Props, State> {
   }
   }
 
 
   handleDeleteConfirmation() {
   handleDeleteConfirmation() {
+    this.setState({ showDeleteProjectAlert: false })
+
     projectStore.delete(this.props.match.params.id).then(() => {
     projectStore.delete(this.props.match.params.id).then(() => {
       if (
       if (
         userStore.loggedUser &&
         userStore.loggedUser &&
@@ -167,6 +174,10 @@ class ProjectDetailsPage extends React.Component<Props, State> {
     })
     })
   }
   }
 
 
+  handleDeleteProjectClick() {
+    this.setState({ showDeleteProjectAlert: true })
+  }
+
   loadData() {
   loadData() {
     const projectId = this.props.match.params.id
     const projectId = this.props.match.params.id
     projectStore.getProjects()
     projectStore.getProjects()
@@ -177,6 +188,19 @@ class ProjectDetailsPage extends React.Component<Props, State> {
   }
   }
 
 
   render() {
   render() {
+    let dropdownActions = [{
+      label: 'Add Member',
+      color: Palette.primary,
+      action: () => { this.handleAddMemberClick() },
+    }, {
+      label: 'Edit Project',
+      action: () => { this.handleEditProjectClick() },
+    }, {
+      label: 'Delete Project',
+      color: Palette.alert,
+      action: () => { this.handleDeleteProjectClick() },
+    }]
+
     return (
     return (
       <Wrapper>
       <Wrapper>
         <DetailsTemplate
         <DetailsTemplate
@@ -187,6 +211,7 @@ class ProjectDetailsPage extends React.Component<Props, State> {
           contentHeaderComponent={<DetailsContentHeader
           contentHeaderComponent={<DetailsContentHeader
             item={{ ...projectStore.projectDetails, description: '' }}
             item={{ ...projectStore.projectDetails, description: '' }}
             onBackButonClick={() => { this.handleBackButtonClick() }}
             onBackButonClick={() => { this.handleBackButtonClick() }}
+            dropdownActions={dropdownActions}
             typeImage={projectImage}
             typeImage={projectImage}
             description={''}
             description={''}
           />}
           />}
@@ -197,14 +222,12 @@ class ProjectDetailsPage extends React.Component<Props, State> {
             usersLoading={projectStore.usersLoading}
             usersLoading={projectStore.usersLoading}
             roleAssignments={projectStore.roleAssignments}
             roleAssignments={projectStore.roleAssignments}
             roles={projectStore.roles}
             roles={projectStore.roles}
-            deleteDisabled={projectStore.projects.length === 1}
             loggedUserId={userStore.loggedUser ? userStore.loggedUser.id : ''}
             loggedUserId={userStore.loggedUser ? userStore.loggedUser.id : ''}
             onEnableUser={user => { this.handleEnableUser(user) }}
             onEnableUser={user => { this.handleEnableUser(user) }}
             onRemoveUser={user => { this.handleRemoveUser(user) }}
             onRemoveUser={user => { this.handleRemoveUser(user) }}
-            onEditProjectClick={() => { this.handleEditProjectClick() }}
-            onDeleteConfirmation={() => { this.handleDeleteConfirmation() }}
             onAddMemberClick={() => { this.handleAddMemberClick() }}
             onAddMemberClick={() => { this.handleAddMemberClick() }}
             onUserRoleChange={(user, roleId, toggled) => { this.handleUserRoleChange(user, roleId, toggled) }}
             onUserRoleChange={(user, roleId, toggled) => { this.handleUserRoleChange(user, roleId, toggled) }}
+            onDeleteClick={() => this.handleDeleteProjectClick()}
           />}
           />}
         />
         />
         {this.state.showProjectModal ? (
         {this.state.showProjectModal ? (
@@ -225,6 +248,25 @@ class ProjectDetailsPage extends React.Component<Props, State> {
             onRequestClose={() => { this.setState({ showAddMemberModal: false }) }}
             onRequestClose={() => { this.setState({ showAddMemberModal: false }) }}
           />
           />
         ) : null}
         ) : null}
+        {this.state.showDeleteProjectAlert && projectStore.projects.length > 1 ? (
+          <AlertModal
+            isOpen
+            title="Delete Project?"
+            message="Are you sure you want to delete this project?"
+            extraMessage="Deleting a Coriolis Project is permanent!"
+            onConfirmation={() => { this.handleDeleteConfirmation() }}
+            onRequestClose={() => { this.setState({ showDeleteProjectAlert: false }) }}
+          />
+        ) : this.state.showDeleteProjectAlert && projectStore.projects.length === 1 ? (
+          <AlertModal
+            isOpen
+            type="error"
+            title="Error deleting project"
+            message="The project can't be deleted"
+            extraMessage="You can't delete the last project since you'll no longer be able to log in"
+            onRequestClose={() => { this.setState({ showDeleteProjectAlert: false }) }}
+          />
+        ) : null}
       </Wrapper>
       </Wrapper>
     )
     )
   }
   }

+ 34 - 2
src/components/pages/UserDetailsPage/UserDetailsPage.jsx

@@ -24,10 +24,13 @@ import DetailsPageHeader from '../../organisms/DetailsPageHeader'
 import DetailsContentHeader from '../../organisms/DetailsContentHeader'
 import DetailsContentHeader from '../../organisms/DetailsContentHeader'
 import UserDetailsContent from '../../organisms/UserDetailsContent'
 import UserDetailsContent from '../../organisms/UserDetailsContent'
 import UserModal from '../../organisms/UserModal'
 import UserModal from '../../organisms/UserModal'
+import AlertModal from '../../organisms/AlertModal'
 
 
 import userStore from '../../../stores/UserStore'
 import userStore from '../../../stores/UserStore'
 import projectStore from '../../../stores/ProjectStore'
 import projectStore from '../../../stores/ProjectStore'
 
 
+import Palette from '../../styleUtils/Palette'
+
 import userImage from './images/user.svg'
 import userImage from './images/user.svg'
 
 
 const Wrapper = styled.div``
 const Wrapper = styled.div``
@@ -38,12 +41,14 @@ type Props = {
 type State = {
 type State = {
   showUserModal: boolean,
   showUserModal: boolean,
   editPassword: boolean,
   editPassword: boolean,
+  showDeleteAlert: boolean,
 }
 }
 @observer
 @observer
 class UserDetailsPage extends React.Component<Props, State> {
 class UserDetailsPage extends React.Component<Props, State> {
   state = {
   state = {
     showUserModal: false,
     showUserModal: false,
     editPassword: false,
     editPassword: false,
+    showDeleteAlert: false,
   }
   }
 
 
   componentDidMount() {
   componentDidMount() {
@@ -101,6 +106,10 @@ class UserDetailsPage extends React.Component<Props, State> {
     this.setState({ showUserModal: true, editPassword: true })
     this.setState({ showUserModal: true, editPassword: true })
   }
   }
 
 
+  handleDeleteClick() {
+    this.setState({ showDeleteAlert: true })
+  }
+
   loadData(id?: string) {
   loadData(id?: string) {
     projectStore.getProjects()
     projectStore.getProjects()
     userStore.getProjects(id || this.props.match.params.id)
     userStore.getProjects(id || this.props.match.params.id)
@@ -108,6 +117,19 @@ class UserDetailsPage extends React.Component<Props, State> {
   }
   }
 
 
   render() {
   render() {
+    let dropdownActions = [{
+      label: 'Change passowrd',
+      color: Palette.primary,
+      action: () => { this.handleUpdatePasswordClick() },
+    }, {
+      label: 'Edit user',
+      action: () => { this.handleEditClick() },
+    }, {
+      label: 'Delete user',
+      color: Palette.alert,
+      action: () => { this.handleDeleteClick() },
+    }]
+
     return (
     return (
       <Wrapper>
       <Wrapper>
         <DetailsTemplate
         <DetailsTemplate
@@ -119,16 +141,16 @@ class UserDetailsPage extends React.Component<Props, State> {
             item={{ ...userStore.userDetails, description: '' }}
             item={{ ...userStore.userDetails, description: '' }}
             onBackButonClick={() => { this.handleBackButtonClick() }}
             onBackButonClick={() => { this.handleBackButtonClick() }}
             typeImage={userImage}
             typeImage={userImage}
+            dropdownActions={dropdownActions}
             description={''}
             description={''}
           />}
           />}
           contentComponent={<UserDetailsContent
           contentComponent={<UserDetailsContent
-            onDeleteConfirmation={() => { this.handleDeleteConfirmation() }}
+            onDeleteClick={() => { this.handleDeleteClick() }}
             user={userStore.userDetails}
             user={userStore.userDetails}
             isLoggedUser={userStore.loggedUser && userStore.userDetails ? userStore.loggedUser.id === userStore.userDetails.id : false}
             isLoggedUser={userStore.loggedUser && userStore.userDetails ? userStore.loggedUser.id === userStore.userDetails.id : false}
             loading={userStore.userDetailsLoading}
             loading={userStore.userDetailsLoading}
             userProjects={userStore.projects}
             userProjects={userStore.projects}
             projects={projectStore.projects}
             projects={projectStore.projects}
-            onEditClick={() => { this.handleEditClick() }}
             onUpdatePasswordClick={() => { this.handleUpdatePasswordClick() }}
             onUpdatePasswordClick={() => { this.handleUpdatePasswordClick() }}
           />}
           />}
         />
         />
@@ -143,6 +165,16 @@ class UserDetailsPage extends React.Component<Props, State> {
             onUpdateClick={user => { this.handleUserUpdateClick(user) }}
             onUpdateClick={user => { this.handleUserUpdateClick(user) }}
           />
           />
         ) : null}
         ) : null}
+        {this.state.showDeleteAlert ? (
+          <AlertModal
+            isOpen
+            title="Delete User?"
+            message="Are you sure you want to delete this user?"
+            extraMessage="Deleting a Coriolis User is permanent!"
+            onConfirmation={() => { this.handleDeleteConfirmation() }}
+            onRequestClose={() => { this.setState({ showDeleteAlert: false }) }}
+          />
+        ) : null}
       </Wrapper>
       </Wrapper>
     )
     )
   }
   }

+ 2 - 2
src/sources/NotificationSource.js

@@ -115,8 +115,8 @@ class DataUtils {
 class NotificationSource {
 class NotificationSource {
   static loadData(): Promise<NotificationItemData[]> {
   static loadData(): Promise<NotificationItemData[]> {
     return Promise.all([
     return Promise.all([
-      Api.send({ url: `${servicesUrl.coriolis}/${Api.projectId}/migrations`, skipLog: true }),
-      Api.send({ url: `${servicesUrl.coriolis}/${Api.projectId}/replicas/detail`, skipLog: true }),
+      Api.send({ url: `${servicesUrl.coriolis}/${Api.projectId}/migrations`, skipLog: true, quietError: true }),
+      Api.send({ url: `${servicesUrl.coriolis}/${Api.projectId}/replicas/detail`, skipLog: true, quietError: true }),
     ]).then(([migrationsResponse, replicasResponse]) => {
     ]).then(([migrationsResponse, replicasResponse]) => {
       let migrations = migrationsResponse.data.migrations
       let migrations = migrationsResponse.data.migrations
       let replicas = replicasResponse.data.replicas
       let replicas = replicasResponse.data.replicas

+ 41 - 0
src/stores/EndpointStore.js

@@ -15,6 +15,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 // @flow
 // @flow
 import { observable, action } from 'mobx'
 import { observable, action } from 'mobx'
 import type { Endpoint, Validation, StorageBackend } from '../types/Endpoint'
 import type { Endpoint, Validation, StorageBackend } from '../types/Endpoint'
+import notificationStore from './NotificationStore'
 import EndpointSource from '../sources/EndpointSource'
 import EndpointSource from '../sources/EndpointSource'
 
 
 export const passwordFields = ['password', 'private_key_passphrase']
 export const passwordFields = ['password', 'private_key_passphrase']
@@ -81,6 +82,46 @@ class EndpointStore {
     })
     })
   }
   }
 
 
+  @action duplicate(opts: {
+    shouldSwitchProject: boolean,
+    onSwitchProject: () => Promise<void>,
+    endpoints: Endpoint[],
+  }): Promise<void> {
+    let endpoints = []
+    return Promise.all(opts.endpoints.map(endpoint => {
+      return EndpointSource.getConnectionInfo(endpoint).then(connectionInfo => {
+        endpoints.push({
+          ...endpoint,
+          connection_info: connectionInfo,
+          name: `${endpoint.name}${!opts.shouldSwitchProject ? ' (copy)' : ''}`,
+        })
+      })
+    })).then(() => {
+      if (opts.shouldSwitchProject) {
+        return opts.onSwitchProject()
+      }
+      return Promise.resolve()
+    }).then(() => {
+      return Promise.all(endpoints.map(endpoint => {
+        return EndpointSource.add(endpoint, true)
+      }).map((p: Promise<any>) => p.catch(e => e)))
+        .then((results: (Endpoint | { status: string, data?: { description: string } })[]) => {
+          let internalServerErrors = results.filter(r => r.status && r.status === 500)
+          if (internalServerErrors.length > 0) {
+            notificationStore.alert(`There was a problem duplicating ${internalServerErrors.length} endpoint${internalServerErrors.length > 1 ? 's' : ''}`, 'error')
+          }
+          let forbiddenErrors = results.filter(r => r.status && r.status === 403)
+          if (forbiddenErrors.length > 0 && forbiddenErrors[0].data && forbiddenErrors[0].data.description) {
+            notificationStore.alert(String(forbiddenErrors[0].data.description), 'error')
+          }
+        })
+    }).catch(e => {
+      if (e.data && e.data.description) {
+        notificationStore.alert(e.data.description, 'error')
+      }
+    })
+  }
+
   @action setConnectionInfo(connectionInfo: $PropertyType<Endpoint, 'connection_info'>) {
   @action setConnectionInfo(connectionInfo: $PropertyType<Endpoint, 'connection_info'>) {
     this.connectionInfo = connectionInfo
     this.connectionInfo = connectionInfo
     this.connectionInfoLoading = false
     this.connectionInfoLoading = false