Преглед изворни кода

Merge pull request #631 from smiclea/regions-export

Special handling for Coriolis Regions export CORWEB-253
Nashwan Azhari пре 5 година
родитељ
комит
67ac09deab

+ 1 - 1
src/@types/NotificationItem.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/>.
 */
 
-export type AlertInfoLevel = 'success' | 'error' | 'info'
+export type AlertInfoLevel = 'success' | 'error' | 'info' | 'warning'
 
 export type AlertInfoOptions = {
   action?: {

+ 51 - 7
src/components/organisms/ChooseProvider/ChooseProvider.tsx

@@ -32,6 +32,7 @@ import type { Endpoint, MultiValidationItem } from '../../../@types/Endpoint'
 
 import MultipleUploadedEndpoints from './MultipleUploadedEndpoints'
 import { ProviderTypes } from '../../../@types/Providers'
+import { Region } from '../../../@types/Region'
 
 const Wrapper = styled.div<any>`
   display: flex;
@@ -90,6 +91,7 @@ const LoadingText = styled.div<any>`
 `
 type Props = {
   providers: ProviderTypes[],
+  regions: Region[]
   onCancelClick: () => void,
   onProviderClick: (provider: ProviderTypes) => void,
   onUploadEndpoint: (endpoint: Endpoint) => void,
@@ -104,12 +106,14 @@ type Props = {
 type State = {
   highlightDropzone: boolean,
   multipleUploadedEndpoints: (Endpoint | string)[],
+  invalidRegionsEndpointIds: { id: string, regions: string[] }[],
 }
 @observer
 class ChooseProvider extends React.Component<Props, State> {
-  state = {
+  state: State = {
     highlightDropzone: false,
     multipleUploadedEndpoints: [],
+    invalidRegionsEndpointIds: [],
   }
 
   fileInput: HTMLElement | null | undefined
@@ -178,19 +182,34 @@ class ChooseProvider extends React.Component<Props, State> {
     this.dragDropListeners = []
   }
 
-  parseEndpoint(content: string): Endpoint {
+  parseEndpoint(content: string, skipAlert?: boolean): { endpoint: Endpoint, unidentRegions: string[] } {
     const endpoint: Endpoint = JSON.parse(content)
     if (!endpoint.name || !endpoint.type || !this.props.providers.find(p => p === endpoint.type)) {
       throw new Error()
     }
     delete (endpoint as any).id
-    return endpoint
+    const unidentRegions: string[] = []
+
+    if (endpoint.mapped_regions?.length) {
+      endpoint.mapped_regions = endpoint.mapped_regions.map(nameId => {
+        const region = this.props.regions.find(r => r.id === nameId || r.name === nameId)
+        if (region) {
+          return region.id
+        }
+        unidentRegions.push(nameId)
+        return null
+      }).filter((item: string | null): item is string => Boolean(item))
+      if (unidentRegions.length && !skipAlert) {
+        notificationStore.alert(`${unidentRegions.length} Coriolis Region${unidentRegions.length > 1 ? 's' : ''} couldn't be mapped`, 'warning')
+      }
+    }
+    return { endpoint, unidentRegions }
   }
 
   processOneFileContent(content: string) {
     this.props.onResetValidation()
     try {
-      const endpoint = this.parseEndpoint(content)
+      const { endpoint } = this.parseEndpoint(content)
       this.chooseEndpoint(endpoint)
     } catch (err) {
       notificationStore.alert('Invalid .endpoint file', 'error')
@@ -200,9 +219,11 @@ class ChooseProvider extends React.Component<Props, State> {
   processMultipleFilesContents(filesContents: FileContent[]) {
     this.props.onResetValidation()
     const uniqueNames: { [prop: string]: number } = {}
+    const invalidRegionsEndpointIds: {id: string, regions: string[]}[] = []
+
     const endpoints = filesContents.map(fileContent => {
       try {
-        const endpoint = this.parseEndpoint(fileContent.content)
+        const { endpoint, unidentRegions } = this.parseEndpoint(fileContent.content, true)
         const key = `${endpoint.type}${endpoint.name}`
         if (uniqueNames[key] === undefined) {
           uniqueNames[key] = 0
@@ -210,6 +231,9 @@ class ChooseProvider extends React.Component<Props, State> {
           uniqueNames[key] += 1
           endpoint.name = `${endpoint.name} (${uniqueNames[key]})`
         }
+        if (unidentRegions.length) {
+          invalidRegionsEndpointIds.push({ id: `${endpoint.type}${endpoint.name}`, regions: unidentRegions })
+        }
         return endpoint
       } catch (err) {
         return fileContent.name
@@ -239,7 +263,10 @@ class ChooseProvider extends React.Component<Props, State> {
       return a.type.localeCompare(b.type)
     })
 
-    this.setState({ multipleUploadedEndpoints: endpoints })
+    this.setState({
+      multipleUploadedEndpoints: endpoints,
+      invalidRegionsEndpointIds,
+    })
   }
 
   chooseEndpoint(endpoint: Endpoint) {
@@ -273,6 +300,20 @@ class ChooseProvider extends React.Component<Props, State> {
     })
   }
 
+  handleRegionsChange(endpoint: Endpoint, newRegions: string[]) {
+    this.setState(prevState => ({
+      multipleUploadedEndpoints: prevState.multipleUploadedEndpoints.map(stateEndpoint => {
+        if (typeof stateEndpoint !== 'string' && `${stateEndpoint.type}${stateEndpoint.name}` === `${endpoint.type}${endpoint.name}`) {
+          return {
+            ...stateEndpoint,
+            mapped_regions: newRegions,
+          }
+        }
+        return stateEndpoint
+      }),
+    }))
+  }
+
   renderMultipleUploadedEndpoints() {
     return (
       <MultipleUploadedEndpoints
@@ -281,8 +322,11 @@ class ChooseProvider extends React.Component<Props, State> {
         onRemove={(e, isAdded) => { this.handleRemoveUploadedEndpoint(e, isAdded) }}
         validating={this.props.multiValidating}
         multiValidation={this.props.multiValidation}
+        invalidRegionsEndpointIds={this.state.invalidRegionsEndpointIds}
+        regions={this.props.regions}
+        onRegionsChange={(endpoint, newRegions) => { this.handleRegionsChange(endpoint, newRegions) }}
         onValidateClick={() => {
-          this.props.onValidateMultipleEndpoints(this.state.multipleUploadedEndpoints.filter(e => typeof e !== 'string'))
+          this.props.onValidateMultipleEndpoints(this.state.multipleUploadedEndpoints.filter(e => typeof e !== 'string') as Endpoint[])
         }}
         onDone={this.props.onCancelClick}
       />

+ 53 - 16
src/components/organisms/ChooseProvider/MultipleUploadedEndpoints.tsx

@@ -27,18 +27,21 @@ import deleteImage from './images/delete.svg'
 import deleteHoverImage from './images/delete-hover.svg'
 import DomUtils from '../../../utils/DomUtils'
 import notificationStore from '../../../stores/NotificationStore'
+import DropdownLink from '../../molecules/DropdownLink/DropdownLink'
+import { Region } from '../../../@types/Region'
 
-const Wrapper = styled.div<any>`
+const Wrapper = styled.div`
+  width: 100%;
   min-height: 0;
 `
-const Buttons = styled.div<any>`
+const Buttons = styled.div`
   display: flex;
   justify-content: space-between;
   margin-top: 32px;
   flex-shrink: 0;
   padding: 0 32px;
 `
-const DeleteButton = styled.div<any>`
+const DeleteButton = styled.div`
   width: 16px;
   height: 16px;
   background: url('${deleteImage}') center no-repeat;
@@ -48,7 +51,7 @@ const DeleteButton = styled.div<any>`
     background: url('${deleteHoverImage}') center no-repeat;
   }
 `
-const Content = styled.div<any>`
+const Content = styled.div`
   overflow: auto;
   display: flex;
   flex-direction: column;
@@ -57,39 +60,46 @@ const Content = styled.div<any>`
   max-height: 384px;
   text-align: left;
 `
-const InvalidEndpoint = styled.div<any>`
+const InvalidEndpoint = styled.div`
   margin-bottom: 8px;
 `
-const EndpointItem = styled.div<any>`
+const EndpointItem = styled.div`
   display: flex;
   align-items: center;
   margin-bottom: 8px;
 `
-const EndpointLogoWrapper = styled.div<any>`
+const EndpointLogoWrapper = styled.div`
   min-width: 110px;
 `
-const EndpointData = styled.div<any>`
+const EndpointData = styled.div`
   display: flex;
   align-items: center;
   justify-content: space-between;
   flex-grow: 1;
   overflow: hidden;
 `
-const EndpointName = styled.div<any>`
+const EndpointName = styled.div`
   overflow: hidden;
   text-overflow: ellipsis;
 `
-const EndpointOptions = styled.div<any>`
+const EndpointOptions = styled.div`
   display: flex;
   align-items: center;
 `
-const EndpointStatus = styled.div<any>`
-  margin: 0 8px;
+const EndpointStatus = styled.div`
+  display: flex;
+  margin-right: 8px;
+  > div {
+    margin-left:  8px;
+  }
 `
 type Props = {
   endpoints: (Endpoint | string)[],
+  regions: Region[],
+  invalidRegionsEndpointIds: { id: string, regions: string[] }[]
   multiValidation: MultiValidationItem[],
   validating: boolean,
+  onRegionsChange: (endpoint: Endpoint, newRegions: string[]) => void
   onBackClick: () => void,
   onRemove: (endpoint: Endpoint, isAdded: boolean) => void,
   onValidateClick: () => void,
@@ -172,11 +182,38 @@ class MultipleUploadedEndpoints extends React.Component<Props, State> {
   }
 
   renderStatus(endpoint: Endpoint) {
-    const validationItem = this.props.multiValidation.find(v => v.endpoint.name === endpoint.name
-      && v.endpoint.type === endpoint.type)
-
+    const validationItem = this.props.multiValidation.find(v => v.endpoint.name === endpoint.name && v.endpoint.type === endpoint.type)
     if (!validationItem) {
-      return null
+      const invalidRegions = this.props.invalidRegionsEndpointIds.find(e => e.id === `${endpoint.type}${endpoint.name}`)?.regions
+      if (!invalidRegions?.length) {
+        return null
+      }
+      return (
+        <>
+          <DropdownLink
+            width="200px"
+            listWidth="120px"
+            getLabel={() => 'Coriolis Regions'}
+            multipleSelection
+            selectedItems={endpoint.mapped_regions}
+            items={this.props.regions.map(r => ({
+              label: r.name,
+              value: r.id,
+            }))}
+            onChange={item => {
+              if (endpoint.mapped_regions.find(r => r === item.value)) {
+                this.props.onRegionsChange(endpoint, endpoint.mapped_regions.filter(r => r !== item.value))
+              } else {
+                this.props.onRegionsChange(endpoint, [...endpoint.mapped_regions, item.value])
+              }
+            }}
+          />
+          <StatusIcon
+            status="INFO"
+            data-tip={`${invalidRegions.length} Coriolis Region${invalidRegions.length > 1 ? 's' : ''} couldn't be mapped for this endpoint. Use the Coriolis Regions dropdown to view and update the current mapping.`}
+          />
+        </>
+      )
     }
 
     if (validationItem.validating) {

+ 1 - 2
src/components/organisms/EndpointDetailsContent/EndpointDetailsContent.tsx

@@ -197,8 +197,7 @@ class EndpointDetailsContent extends React.Component<Props> {
   renderRegions() {
     return (
       <span>
-        {this.props.item?.mapped_regions
-          .map(regionId => this.props.regions.find(r => r.id === regionId)?.name).join(', ') || '-'}
+        {this.props.item?.mapped_regions.map(regionId => this.props.regions.find(r => r.id === regionId)?.name).join(', ') || '-'}
       </span>
     )
   }

+ 3 - 0
src/components/organisms/Notifications/style.ts

@@ -84,6 +84,9 @@ const NotificationsStyle = css`
   .notification-error .notification-title {
     background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTZweCIgaGVpZ2h0PSIxNnB4IiB2aWV3Qm94PSIwIDAgMTYgMTYiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+DQogICAgPGcgaWQ9IlN5bWJvbHMiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPg0KICAgICAgICA8ZyBpZD0iSWNvbi1FcnJvci1XaGl0ZSI+DQogICAgICAgICAgICA8Y2lyY2xlIGlkPSJPdmFsLTIiIGZpbGw9IiNGRkZGRkYiIGN4PSI4IiBjeT0iOCIgcj0iOCI+PC9jaXJjbGU+DQogICAgICAgICAgICA8cGF0aCBkPSJNMTEuNDI4NTcxNCw0LjU3MTQyODU3IEw0LjU3MTQyODU3LDExLjQyODU3MTQiIGlkPSJMaW5lIiBzdHJva2U9IiNGQTE2NjEiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCI+PC9wYXRoPg0KICAgICAgICAgICAgPHBhdGggZD0iTTExLjQyODU3MTQsMTEuNDI4NTcxNCBMNC41NzE0Mjg1Nyw0LjU3MTQyODU3IiBpZD0iTGluZS1Db3B5IiBzdHJva2U9IiNGQTE2NjEiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCI+PC9wYXRoPg0KICAgICAgICA8L2c+DQogICAgPC9nPg0KPC9zdmc+');
   }
+  .notification-warning .notification-title {
+    background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTZweCIgaGVpZ2h0PSIxNnB4IiB2aWV3Qm94PSIwIDAgMTYgMTYiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8ZyBpZD0iQ29yaW9saXMiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPgogICAgICAgIDxnIGlkPSIyMDItUmVwbGljYS1FeGVjdXRpb25zIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMzkyLjAwMDAwMCwgLTg3MS4wMDAwMDApIj4KICAgICAgICAgICAgPGcgaWQ9Ikdyb3VwLTIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDM2MC4wMDAwMDAsIDI0MC4wMDAwMDApIj4KICAgICAgICAgICAgICAgIDxnIGlkPSJHcm91cC0zIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwLjAwMDAwMCwgNzkuMDAwMDAwKSI+CiAgICAgICAgICAgICAgICAgICAgPGcgaWQ9Ikljb24vV2FybmluZyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMzIuMDAwMDAwLCA1NTIuMDAwMDAwKSI+CiAgICAgICAgICAgICAgICAgICAgICAgIDxjaXJjbGUgZmlsbD0iI0ZEQzAyRiIgZmlsbC1ydWxlPSJldmVub2RkIiBjeD0iOCIgY3k9IjgiIHI9IjgiPjwvY2lyY2xlPgogICAgICAgICAgICAgICAgICAgICAgICA8cGF0aCBkPSJNOCw4IEw4LDQiIGlkPSJMaW5lLUNvcHkiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiPjwvcGF0aD4KICAgICAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTgsMTIuNSBDOC42OTAzNTU5NCwxMi41IDkuMjUsMTEuOTQwMzU1OSA5LjI1LDExLjI1IEM5LjI1LDEwLjU1OTY0NDEgOC42OTAzNTU5NCwxMCA4LDEwIEM3LjMwOTY0NDA2LDEwIDYuNzUsMTAuNTU5NjQ0MSA2Ljc1LDExLjI1IEM2Ljc1LDExLjk0MDM1NTkgNy4zMDk2NDQwNiwxMi41IDgsMTIuNSBaIiBpZD0iT3ZhbC0zIiBmaWxsPSIjRkZGRkZGIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjwvcGF0aD4KICAgICAgICAgICAgICAgICAgICA8L2c+CiAgICAgICAgICAgICAgICA8L2c+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==");
+  }
 `
 
 export default NotificationsStyle

+ 4 - 1
src/components/organisms/PageHeader/PageHeader.tsx

@@ -42,6 +42,7 @@ import { ProviderTypes } from '../../../@types/Providers'
 import MinionEndpointModal from '../MinionEndpointModal/MinionEndpointModal'
 import MinionPoolModal from '../MinionPoolModal'
 import ObjectUtils from '../../../utils/ObjectUtils'
+import regionStore from '../../../stores/RegionStore'
 
 const Wrapper = styled.div<any>`
   display: flex;
@@ -156,6 +157,7 @@ class PageHeader extends React.Component<Props, State> {
     switch (item) {
       case 'endpoint':
         providerStore.loadProviders()
+        regionStore.getRegions()
         if (this.props.onModalOpen) {
           this.props.onModalOpen()
         }
@@ -369,7 +371,8 @@ class PageHeader extends React.Component<Props, State> {
           <ChooseProvider
             onCancelClick={() => { this.handleCloseChooseProviderModal() }}
             providers={providerStore.providerNames}
-            loading={providerStore.providersLoading}
+            loading={providerStore.providersLoading || regionStore.loading}
+            regions={regionStore.regions}
             onProviderClick={providerName => { this.handleProviderClick(providerName) }}
             onUploadEndpoint={endpoint => { this.handleUploadEndpoint(endpoint) }}
             multiValidating={this.state.multiValidating}

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

@@ -41,6 +41,7 @@ import LabelDictionary from '../../../utils/LabelDictionary'
 import configLoader from '../../../utils/Config'
 import Palette from '../../styleUtils/Palette'
 import { ProviderTypes } from '../../../@types/Providers'
+import regionStore from '../../../stores/RegionStore'
 
 const Wrapper = styled.div<any>``
 
@@ -158,6 +159,7 @@ class EndpointsPage extends React.Component<{ history: any }, State> {
 
   handleEmptyListButtonClick() {
     providerStore.loadProviders()
+    regionStore.getRegions()
     this.setState({ showChooseProviderModal: true })
   }
 
@@ -336,8 +338,9 @@ class EndpointsPage extends React.Component<{ history: any }, State> {
         >
           <ChooseProvider
             onCancelClick={() => { this.handleCloseChooseProviderModal() }}
+            regions={regionStore.regions}
             providers={providerStore.providerNames}
-            loading={providerStore.providersLoading}
+            loading={providerStore.providersLoading || regionStore.loading}
             onUploadEndpoint={endpoint => { this.handleUploadEndpoint(endpoint) }}
             onProviderClick={providerName => { this.handleProviderClick(providerName) }}
             multiValidating={this.state.multiValidating}

+ 5 - 2
src/stores/EndpointStore.ts

@@ -24,6 +24,7 @@ import notificationStore from './NotificationStore'
 import EndpointSource from '../sources/EndpointSource'
 
 import DomUtils from '../utils/DomUtils'
+import regionStore from './RegionStore'
 
 const updateEndpoint = (endpoint: Endpoint, endpoints: Endpoint[]) => endpoints.map(e => {
   if (e.id === endpoint.id) {
@@ -163,8 +164,9 @@ class EndpointStore {
 
   @action async exportToJson(endpoint: Endpoint): Promise<void> {
     const connectionInfo = await EndpointSource.getConnectionInfo(endpoint)
-    const newEndpoint = endpoint
+    const newEndpoint = JSON.parse(JSON.stringify(endpoint))
     newEndpoint.connection_info = connectionInfo
+    await regionStore.setEndpointRegionExport(newEndpoint)
     DomUtils.download(JSON.stringify(newEndpoint), `${newEndpoint.name}.endpoint`)
   }
 
@@ -173,8 +175,9 @@ class EndpointStore {
 
     await Promise.all(endpoints.map(async endpoint => {
       const connectionInfo = await EndpointSource.getConnectionInfo(endpoint)
-      const newEndpoint = endpoint
+      const newEndpoint = JSON.parse(JSON.stringify(endpoint))
       newEndpoint.connection_info = connectionInfo
+      await regionStore.setEndpointRegionExport(newEndpoint)
       zip.file(`${newEndpoint.name}.endpoint`, JSON.stringify(newEndpoint))
     }))
 

+ 2 - 2
src/stores/NotificationStore.ts

@@ -14,7 +14,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import { observable, action } from 'mobx'
 
-import type { AlertInfo, NotificationItemData } from '../@types/NotificationItem'
+import type { AlertInfo, AlertInfoLevel, NotificationItemData } from '../@types/NotificationItem'
 import NotificationSource from '../sources/NotificationSource'
 
 class NotificationStore {
@@ -26,7 +26,7 @@ class NotificationStore {
 
   visibleErrors: string[] = []
 
-  @action alert(message: string, level?: AlertInfo['level'], options?: AlertInfo['options']) {
+  @action alert(message: string, level?: AlertInfoLevel, options?: AlertInfo['options']) {
     if (this.visibleErrors.find(e => e === message)) {
       return
     }

+ 10 - 0
src/stores/RegionStore.ts

@@ -13,6 +13,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
 import { observable, action, runInAction } from 'mobx'
+import { Endpoint } from '../@types/Endpoint'
 import { Region } from '../@types/Region'
 import regionSource from '../sources/RegionSource'
 
@@ -28,12 +29,21 @@ class RegionStore {
       runInAction(() => {
         this.regions = regions
       })
+      return regions
     } finally {
       runInAction(() => {
         this.loading = false
       })
     }
   }
+
+  async setEndpointRegionExport(endpoint: Endpoint) {
+    if (!endpoint.mapped_regions?.length) {
+      return
+    }
+    const regions = this.regions.length ? this.regions : await this.getRegions()
+    endpoint.mapped_regions = endpoint.mapped_regions.map(id => regions.find(r => r.id === id)?.name || id)
+  }
 }
 
 export default new RegionStore()