Jelajahi Sumber

Merge pull request #229 from smiclea/duplicate-endpoint

Duplicate an endpoint to a project
Dorin Paslaru 8 tahun lalu
induk
melakukan
063b0f8cc6

+ 9 - 5
src/components/molecules/WizardOptionsField/index.jsx

@@ -59,6 +59,8 @@ type Props = {
   properties: Field[],
   enum: string[],
   required: boolean,
+  width?: number,
+  skipNullValue?: boolean,
 }
 @observer
 class WizardOptionsField extends React.Component<Props> {
@@ -113,16 +115,18 @@ class WizardOptionsField extends React.Component<Props> {
       }
     })
 
-    items = [
-      { label: 'Choose a value', value: null },
-      ...items,
-    ]
+    if (!this.props.skipNullValue) {
+      items = [
+        { label: 'Choose a value', value: null },
+        ...items,
+      ]
+    }
 
     let selectedItem = items.find(i => i.value === this.props.value)
 
     return (
       <Dropdown
-        width={StyleProps.inputSizes.wizard.width}
+        width={this.props.width || StyleProps.inputSizes.wizard.width}
         data-test-id={`wOptionsField-dropdown-${this.props.name}`}
         noSelectionMessage="Choose a value"
         selectedItem={selectedItem}

+ 30 - 0
src/components/organisms/EndpointDuplicateOptions/images/duplicate.svg

@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="126px" height="67px" viewBox="0 0 126 67" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
+    <title>Icon/Endpoint/duplicate-96</title>
+    <desc>Created with Sketch.</desc>
+    <defs>
+        <path d="M0,80 L7.03974198,80 C21.3059504,80 33,67.8370899 33,53 C33,40.7183666 24.9376127,30.3578918 13.8982944,27.2262538 C13.4706109,16.8915649 8.02547353,7.92152105 9.4990682e-13,3.00459145 L0,0 L54,0 L54,81 L0,81 L0,80 Z" id="path-1"></path>
+    </defs>
+    <g id="Coriolis" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="EP/Duplicate-Endpoint" transform="translate(-224.000000, -111.000000)">
+            <g id="Icon/Endpoint/duplicate-96" transform="translate(224.000000, 96.000000)">
+                <path d="M41,56 L57,56" id="Stroke-7" stroke="#0044CA" stroke-width="1.5" stroke-linecap="round"></path>
+                <path d="M44,60.3927557 C42.6229683,62.568892 40.2854187,64 37.6341488,64 L31.6687915,64 C27.4335681,64 24,60.3473011 24,55.8401989 L24,55.1605114 C24,50.6534091 27.4335681,47 31.6687915,47 L37.6341488,47 C40.2834162,47 42.6182959,48.428267 43.9959951,50.6015625" id="Stroke-9" stroke="#0044CA" stroke-width="1.5" stroke-linecap="round"></path>
+                <path d="M73,60.3927557 C71.6229683,62.568892 69.2854187,64 66.6341488,64 L60.6687915,64 C56.4335681,64 53,60.3473011 53,55.8401989 L53,55.1605114 C53,50.6534091 56.4335681,47 60.6687915,47 L66.6341488,47 C69.2834162,47 71.6182959,48.428267 72.9959951,50.6015625" id="Stroke-9-Copy" stroke="#0044CA" stroke-width="1.5" stroke-linecap="round" transform="translate(63.000000, 55.500000) scale(-1, 1) translate(-63.000000, -55.500000) "></path>
+                <path d="M57.3997431,16 C69.5428443,16 79.2663177,26.0641637 79.7833871,38.6506975 C88.5774203,41.1637404 95,49.4777016 95,59.3333333 C95,71.23964 85.6844011,81 74.3197945,81 L17.9200514,81 C8.62307987,81 1,73.0124945 1,63.2727878 C1,55.5910847 5.88807194,49.0759612 12.4564588,46.7747814 C12.8546985,35.2269098 21.8452823,25.8476369 32.9600257,25.8476369 C34.9133275,25.8476369 36.7310993,26.3519777 38.5418056,26.8956264 C42.5820122,20.4551213 49.4715602,16 57.3997431,16 Z" id="Stroke-1" stroke="#0044CA" stroke-width="1.5" stroke-linecap="round"></path>
+                <g id="Group-Copy" stroke-width="1" fill-rule="evenodd" transform="translate(73.000000, 5.000000)">
+                    <mask id="mask-2" fill="white">
+                        <use xlink:href="#path-1"></use>
+                    </mask>
+                    <g id="Mask"></g>
+                    <g mask="url(#mask-2)" id="Stroke-1" stroke="#0044CA" stroke-linecap="round" stroke-width="1.5">
+                        <g transform="translate(-42.000000, 11.000000)">
+                            <path d="M56.3997431,0 C68.5428443,0 78.2663177,10.0641637 78.7833871,22.6506975 C87.5774203,25.1637404 94,33.4777016 94,43.3333333 C94,55.23964 84.6844011,65 73.3197945,65 L16.9200514,65 C7.62307987,65 0,57.0124945 0,47.2727878 C0,39.5910847 4.88807194,33.0759612 11.4564588,30.7747814 C11.8546985,19.2269098 20.8452823,9.84763691 31.9600257,9.84763691 C33.9133275,9.84763691 35.7310993,10.3519777 37.5418056,10.8956264 C41.5820122,4.45512131 48.4715602,0 56.3997431,0 Z"></path>
+                        </g>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 152 - 0
src/components/organisms/EndpointDuplicateOptions/index.jsx

@@ -0,0 +1,152 @@
+/*
+Copyright (C) 2017  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 { observer } from 'mobx-react'
+import styled from 'styled-components'
+
+import StatusImage from '../../atoms/StatusImage'
+import Button from '../../atoms/Button'
+import WizardOptionsField from '../../molecules/WizardOptionsField'
+
+import KeyboardManager from '../../../utils/KeyboardManager'
+import type { Project } from '../../../types/Project'
+import Palette from '../../styleUtils/Palette'
+
+import duplicateImage from './images/duplicate.svg'
+import Tooltip from '../../atoms/Tooltip'
+
+const Wrapper = styled.div`
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 0 32px 32px 32px;
+`
+const Options = styled.div`
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  width: 100%;
+`
+const Image = styled.div`
+  margin-top: 48px;
+  margin-bottom: 80px;
+  width: 128px;
+  height: 96px;
+  background: url('${duplicateImage}') no-repeat center;
+`
+const Message = styled.div`
+  margin-top: 48px;
+  text-align: center;
+`
+const Title = styled.div`
+  font-size: 18px;
+  margin-bottom: 8px;
+`
+const Subtitle = styled.div`
+  color: ${Palette.grayscale[4]};
+`
+const Form = styled.div`
+  margin-bottom: 128px;
+`
+const Loading = styled.div`
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  margin-top: 32px;
+`
+const Buttons = styled.div`
+  display: flex;
+  justify-content: space-between;
+  width: 100%;
+`
+const WizardOptionsFieldStyled = styled(WizardOptionsField) `
+  width: 319px;
+  justify-content: space-between;
+`
+type Props = {
+  projects: Project[],
+  selectedProjectId: string,
+  duplicating: boolean,
+  onCancelClick: () => void,
+  onDuplicateClick: (projectId: string) => void,
+}
+type State = {
+  selectedProjectId: string,
+}
+@observer
+class EndpointDuplicateOptions extends React.Component<Props, State> {
+  componentWillMount() {
+    this.setState({ selectedProjectId: this.props.selectedProjectId })
+  }
+
+  componentDidMount() {
+    KeyboardManager.onEnter('duplicate-options', () => {
+      this.props.onDuplicateClick(this.state.selectedProjectId)
+    }, 2)
+  }
+
+  componentWillUnmount() {
+    KeyboardManager.removeKeyDown('duplicate-options')
+  }
+
+  renderDuplicating() {
+    return (
+      <Loading>
+        <StatusImage loading />
+        <Message>
+          <Title>Duplicating Endpoint</Title>
+          <Subtitle>Please wait ...</Subtitle>
+        </Message>
+      </Loading>
+    )
+  }
+
+  renderOptions() {
+    return (
+      <Options>
+        <Image />
+        <Form>
+          <WizardOptionsFieldStyled
+            name="duplicate_to_project"
+            type="string"
+            enum={this.props.projects}
+            skipNullValue
+            value={this.state.selectedProjectId}
+            onChange={projectId => { this.setState({ selectedProjectId: projectId }) }}
+            width={318}
+          />
+          <Tooltip />
+          {Tooltip.rebuild()}
+        </Form>
+        <Buttons>
+          <Button secondary onClick={this.props.onCancelClick}>Cancel</Button>
+          <Button onClick={() => { this.props.onDuplicateClick(this.state.selectedProjectId) }}>Duplicate</Button>
+        </Buttons>
+      </Options>
+    )
+  }
+
+  render() {
+    return (
+      <Wrapper>
+        {this.props.duplicating ? this.renderDuplicating() : this.renderOptions()}
+      </Wrapper>
+    )
+  }
+}
+
+export default EndpointDuplicateOptions

+ 43 - 0
src/components/organisms/EndpointDuplicateOptions/story.jsx

@@ -0,0 +1,43 @@
+/*
+Copyright (C) 2017  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 Component from '.'
+
+storiesOf('EndpointDuplicateOptions', module)
+  .add('default', () => (
+    <div style={{ width: '576px', background: 'white' }}>
+      <Component
+        duplicating={false}
+        onCancelClick={() => { }}
+        onDuplicateClick={() => { }}
+        projects={[{ name: 'admin', id: 'admin' }]}
+        selectedProjectId="admin"
+      />
+    </div>
+  ))
+  .add('duplicating', () => (
+    <div style={{ width: '576px' }}>
+      <Component
+        duplicating
+        onCancelClick={() => { }}
+        onDuplicateClick={() => { }}
+        projects={[{ name: 'admin', id: 'admin' }]}
+        selectedProjectId="admin"
+      />
+    </div>
+  ))

+ 74 - 9
src/components/pages/EndpointsPage/index.jsx

@@ -32,17 +32,21 @@ import type { Endpoint as EndpointType } from '../../../types/Endpoint'
 import endpointImage from './images/endpoint-large.svg'
 
 import projectStore from '../../../stores/ProjectStore'
+import userStore from '../../../stores/UserStore'
+import EndpointSource from '../../../sources/EndpointSource'
 import endpointStore from '../../../stores/EndpointStore'
 import migrationStore from '../../../stores/MigrationStore'
 import replicaStore from '../../../stores/ReplicaStore'
 import providerStore from '../../../stores/ProviderStore'
 import LabelDictionary from '../../../utils/LabelDictionary'
 import { requestPollTimeout } from '../../../config.js'
+import EndpointDuplicateOptions from '../../organisms/EndpointDuplicateOptions'
 
 const Wrapper = styled.div``
 
 const BulkActions = [
   { label: 'Delete', value: 'delete' },
+  { label: 'Duplicate', value: 'duplicate' },
 ]
 
 type State = {
@@ -53,6 +57,8 @@ type State = {
   providerType: ?string,
   showEndpointsInUseModal: boolean,
   modalIsOpen: boolean,
+  showDuplicateModal: boolean,
+  duplicating: boolean,
 }
 @observer
 class EndpointsPage extends React.Component<{}, State> {
@@ -70,6 +76,8 @@ class EndpointsPage extends React.Component<{}, State> {
       providerType: null,
       showEndpointsInUseModal: false,
       modalIsOpen: false,
+      showDuplicateModal: false,
+      duplicating: false,
     }
   }
 
@@ -125,23 +133,64 @@ class EndpointsPage extends React.Component<{}, State> {
   }
 
   handleActionChange(items: EndpointType[], action: string) {
-    if (action === 'delete') {
-      let endpointsInUse = items.filter(endpoint => {
-        const endpointUsage = this.getEndpointUsage(endpoint)
-        return endpointUsage.migrationsCount > 0 || endpointUsage.replicasCount > 0
-      })
+    switch (action) {
+      case 'delete': {
+        let endpointsInUse = items.filter(endpoint => {
+          const endpointUsage = this.getEndpointUsage(endpoint)
+          return endpointUsage.migrationsCount > 0 || endpointUsage.replicasCount > 0
+        })
 
-      if (endpointsInUse.length > 0) {
-        this.setState({ showEndpointsInUseModal: true })
-      } else {
+        if (endpointsInUse.length > 0) {
+          this.setState({ showEndpointsInUseModal: true })
+        } else {
+          this.setState({
+            showDeleteEndpointsConfirmation: true,
+            confirmationItems: items,
+          })
+        }
+        break
+      }
+      case 'duplicate': {
         this.setState({
-          showDeleteEndpointsConfirmation: true,
           confirmationItems: items,
+          showDuplicateModal: true,
+          modalIsOpen: true,
         })
+        break
       }
+      default: break
     }
   }
 
+  handleDuplicate(projectId: string) {
+    this.setState({ modalIsOpen: false, duplicating: true })
+
+    let selectedProjectId = userStore.user ? userStore.user.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 })
+      })
+    })).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)
+      }))
+    }).then(() => {
+      this.setState({ showDuplicateModal: false, duplicating: false })
+      this.pollData()
+    })
+  }
+
   handleCloseDeleteEndpointsConfirmation() {
     this.setState({
       showDeleteEndpointsConfirmation: false,
@@ -214,6 +263,7 @@ class EndpointsPage extends React.Component<{}, State> {
 
   render() {
     let items: any = endpointStore.endpoints
+    let selectedProjectId = userStore.user ? userStore.user.project.id : ''
     return (
       <Wrapper>
         <MainTemplate
@@ -299,6 +349,21 @@ class EndpointsPage extends React.Component<{}, State> {
           extraMessage="You must first delete the replicas or migrations which use these endpoints."
           onRequestClose={() => { this.setState({ showEndpointsInUseModal: false }) }}
         />
+        {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>
     )
   }

+ 2 - 2
src/sources/EndpointSource.js

@@ -205,9 +205,9 @@ class EdnpointSource {
     })
   }
 
-  static add(endpoint: Endpoint): Promise<Endpoint> {
+  static add(endpoint: Endpoint, skipSchemaParser: boolean = false): Promise<Endpoint> {
     return new Promise((resolve, reject) => {
-      let parsedEndpoint = SchemaParser.fieldsToPayload(endpoint)
+      let parsedEndpoint = skipSchemaParser ? { ...endpoint } : SchemaParser.fieldsToPayload(endpoint)
       let projectId = cookie.get('projectId')
       if (useSecret) {
         Api.send({

+ 1 - 0
src/utils/LabelDictionary.js

@@ -101,6 +101,7 @@ class LabelDictionary {
     linux_migr_image: { label: 'Linux Migration Image', description: 'The Linux Migration Image information found on the Azure page' },
     user_domain_id: 'User Domain ID',
     project_domain_id: 'Project Domain ID',
+    duplicate_to_project: { label: 'Project', description: 'Duplicate endpoint to selected project' },
   }
 
   static get(fieldName: ?string): string {