/*
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 .
*/
import React from 'react'
import { observer } from 'mobx-react'
import styled from 'styled-components'
import notificationStore from '@src/stores/NotificationStore'
import EndpointLogos from '@src/components/modules/EndpointModule/EndpointLogos'
import Button from '@src/components/ui/Button'
import StatusImage from '@src/components/ui/StatusComponents/StatusImage'
import { ThemePalette, ThemeProps } from '@src/components/Theme'
import FileUtils from '@src/utils/FileUtils'
import configLoader from '@src/utils/Config'
import type { FileContent } from '@src/utils/FileUtils'
import type { Endpoint, MultiValidationItem } from '@src/@types/Endpoint'
import { ProviderTypes } from '@src/@types/Providers'
import { Region } from '@src/@types/Region'
import MultipleUploadedEndpoints from './MultipleUploadedEndpoints'
const Wrapper = styled.div`
display: flex;
min-height: 0;
padding: 22px 0 32px 0;
text-align: center;
`
const Providers = styled.div`
min-height: 0;
display: flex;
flex-direction: column;
align-items: center;
`
const Logos = styled.div`
display: flex;
flex-wrap: wrap;
overflow: auto;
min-height: 0;
flex-grow: 1;
`
const Upload = styled.div`
border: 1px dashed ${props => (props.highlight ? ThemePalette.primary : 'white')};
margin: 0 32px 16px 32px;
padding: 16px;
`
const UploadMessage = styled.div`
color: ${ThemePalette.grayscale[3]};
`
const UploadMessageButton = styled.span`
color: ${ThemePalette.primary};
cursor: pointer;
`
const FakeFileInput = styled.input`
position: absolute;
opacity: 0;
top: -99999px;
`
const EndpointLogosStyled = styled(EndpointLogos)`
transform: scale(0.67);
transition: all ${ThemeProps.animations.swift};
cursor: pointer;
&:hover {
transform: scale(0.7);
}
`
const LoadingWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
margin: 32px 0;
flex-grow: 1;
`
const LoadingText = styled.div`
font-size: 18px;
margin-top: 32px;
`
type Props = {
providers: ProviderTypes[],
regions: Region[]
onCancelClick: () => void,
onProviderClick: (provider: ProviderTypes) => void,
onUploadEndpoint: (endpoint: Endpoint) => void,
loading: boolean,
onValidateMultipleEndpoints: (endpoints: Endpoint[]) => void,
onResizeUpdate?: () => void,
multiValidating: boolean,
multiValidation: MultiValidationItem[],
onRemoveEndpoint: (endpoint: Endpoint) => void,
onResetValidation: () => void,
}
type State = {
highlightDropzone: boolean,
multipleUploadedEndpoints: (Endpoint | string)[],
invalidRegionsEndpointIds: { id: string, regions: string[] }[],
}
@observer
class ChooseProvider extends React.Component {
state: State = {
highlightDropzone: false,
multipleUploadedEndpoints: [],
invalidRegionsEndpointIds: [],
}
fileInput: HTMLElement | null | undefined
dragDropListeners: { type: string, listener: (e: any) => any }[] = []
UNSAFE_componentWillMount() {
setTimeout(() => { this.addDragAndDrop() }, 1000)
}
componentDidUpdate(_: Props, prevState: State) {
if (prevState.multipleUploadedEndpoints.length !== this.state.multipleUploadedEndpoints.length
&& this.props.onResizeUpdate) {
this.props.onResizeUpdate()
}
}
componentWillUnmount() {
this.removeDragDrop()
}
addDragAndDrop() {
this.dragDropListeners = [{
type: 'dragenter',
listener: e => {
this.setState({ highlightDropzone: true })
e.dataTransfer.dropEffect = 'copy'
e.preventDefault()
},
}, {
type: 'dragover',
listener: e => {
e.dataTransfer.dropEffect = 'copy'
e.preventDefault()
},
}, {
type: 'dragleave',
listener: e => {
if (!e.clientX && !e.clientY) {
this.setState({ highlightDropzone: false })
}
},
}, {
type: 'drop',
listener: async e => {
e.preventDefault()
this.setState({ highlightDropzone: false })
const filesContents = await FileUtils.readContentFromFileList(e.dataTransfer.files)
if (filesContents.length === 1) {
this.processOneFileContent(filesContents[0].content)
} else {
this.processMultipleFilesContents(filesContents)
}
},
}]
this.dragDropListeners.forEach(l => {
window.addEventListener(l.type, l.listener)
})
}
removeDragDrop() {
this.dragDropListeners.forEach(l => {
window.removeEventListener(l.type, l.listener)
})
this.dragDropListeners = []
}
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
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)
this.chooseEndpoint(endpoint)
} catch (err) {
notificationStore.alert('Invalid .endpoint file', 'error')
}
}
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, unidentRegions } = this.parseEndpoint(fileContent.content, true)
const key = `${endpoint.type}${endpoint.name}`
if (uniqueNames[key] === undefined) {
uniqueNames[key] = 0
} else {
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
}
})
const sortPriority = configLoader.config.providerSortPriority
endpoints.sort((a, b) => {
if (typeof a === 'string' && typeof b === 'string') {
return a.localeCompare(b)
}
if (typeof a === 'string') {
return 1
}
if (typeof b === 'string') {
return -1
}
if (sortPriority[a.type] && sortPriority[b.type]) {
return (sortPriority[a.type] - sortPriority[b.type]) || a.type.localeCompare(b.type)
}
if (sortPriority[a.type]) {
return -1
}
if (sortPriority[b.type]) {
return 1
}
return a.type.localeCompare(b.type)
})
this.setState({
multipleUploadedEndpoints: endpoints,
invalidRegionsEndpointIds,
})
}
chooseEndpoint(endpoint: Endpoint) {
this.props.onUploadEndpoint(endpoint)
}
async handleFileUpload(files: FileList | null) {
const filesContents = await FileUtils.readContentFromFileList(files)
if (filesContents.length === 1) {
this.processOneFileContent(filesContents[0].content)
} else {
this.processMultipleFilesContents(filesContents)
}
}
handleRemoveUploadedEndpoint(endpoint: Endpoint | string, isAdded: boolean) {
this.setState(prevState => {
const multipleUploadedEndpoints = prevState.multipleUploadedEndpoints.filter(e => {
if (typeof e === 'string' && typeof endpoint === 'string') {
return e !== endpoint
}
if (typeof e !== 'string' && typeof endpoint !== 'string') {
return e.name !== endpoint.name || e.type !== endpoint.type
}
return true
})
if (isAdded && typeof endpoint !== 'string') {
this.props.onRemoveEndpoint(endpoint)
}
return { multipleUploadedEndpoints }
})
}
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 (
{ this.setState({ multipleUploadedEndpoints: [] }) }}
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') as Endpoint[])
}}
onDone={this.props.onCancelClick}
/>
)
}
renderLoading() {
if (!this.props.loading) {
return null
}
return (
Loading providers ...
)
}
renderProviders() {
if (this.props.loading) {
return null
}
const UploadButton = (
{ if (this.fileInput) this.fileInput.click() }}
>upload
)
return (
{this.props.providers.map(k => (
{ this.props.onProviderClick(k) }}
/>
))}
You can
{UploadButton}
or drop multiple .endpoint and zipped .endpoint files.
{ this.fileInput = r }}
accept=".endpoint,.zip"
multiple
onChange={e => { this.handleFileUpload(e.target.files) }}
/>
)
}
render() {
return (
{this.state.multipleUploadedEndpoints.length === 0 ? this.renderProviders() : null}
{this.renderLoading()}
{this.state.multipleUploadedEndpoints.length > 0
? this.renderMultipleUploadedEndpoints() : null}
)
}
}
export default ChooseProvider