/* 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 styled from 'styled-components' import { observer } from 'mobx-react' import { observe } from 'mobx' import EndpointLogos from '../EndpointLogos/EndpointLogos' import StatusIcon from '../../../ui/StatusComponents/StatusIcon/StatusIcon' import CopyButton from '../../../ui/CopyButton/CopyButton' import StatusImage from '../../../ui/StatusComponents/StatusImage/StatusImage' import Button from '../../../ui/Button/Button' import LoadingButton from '../../../ui/LoadingButton/LoadingButton' import type { Endpoint as EndpointType } from '../../../../@types/Endpoint' import type { Field } from '../../../../@types/Field' import notificationStore from '../../../../stores/NotificationStore' import endpointStore from '../../../../stores/EndpointStore' import providerStore from '../../../../stores/ProviderStore' import ObjectUtils from '../../../../utils/ObjectUtils' import { ThemePalette } from '../../../Theme' import DomUtils from '../../../../utils/DomUtils' import { ContentPlugin } from '../../../../plugins' import DefaultContentPlugin from '../../../../plugins/default/ContentPlugin' import KeyboardManager from '../../../../utils/KeyboardManager' import { ProviderTypes } from '../../../../@types/Providers' const Wrapper = styled.div` padding: 48px 0 32px 0; display: flex; align-items: center; flex-direction: column; min-height: 0; ` const Status = styled.div` display: flex; flex-direction: column; align-items: center; flex-shrink: 0; ` const StatusHeader = styled.div` display: flex; align-items: center; ` const StatusMessage = styled.div` margin-left: 8px; display: flex; align-items: center; line-height: 12px; ` const ShowErrorButton = styled.span` font-size: 10px; color: ${ThemePalette.primary}; margin-left: 8px; cursor: pointer; ` const StatusError = styled.div` max-width: 100%; margin: 16px 16px 0 16px; max-height: 140px; overflow: auto; cursor: pointer; &:hover > span { opacity: 1; } > span { background-position-y: 4px; margin-left: 4px; } ` const Content = styled.div` width: 100%; display: flex; flex-direction: column; min-height: 0; ` const LoadingWrapper = styled.div` display: flex; flex-direction: column; align-items: center; margin: 32px 0; ` const LoadingText = styled.div` font-size: 18px; margin-top: 32px; ` const Buttons = styled.div` display: flex; justify-content: space-between; margin-top: 32px; flex-shrink: 0; padding: 0 32px; ` type Props = { type?: ProviderTypes | null, cancelButtonText: string, deleteOnCancel?: boolean, endpoint?: EndpointType | null, isNewEndpoint?: boolean, onCancelClick: (opts?: { autoClose?: boolean }) => void, onResizeUpdate?: (scrollableRef: HTMLElement, scrollOffset?: number) => void, } type State = { invalidFields: any[], validating: boolean, showErrorMessage: boolean, endpoint: EndpointType | null, isNew: boolean | null, } @observer class EndpointModal extends React.Component { static defaultProps = { cancelButtonText: 'Cancel', } state: State = { invalidFields: [], validating: false, showErrorMessage: false, endpoint: null, isNew: null, } scrollableRef!: HTMLElement closeTimeout: number | undefined contentPluginRef!: DefaultContentPlugin isValidateButtonEnabled: boolean = false providerStoreObserver!: () => void endpointValidationObserver!: () => void UNSAFE_componentWillMount() { this.UNSAFE_componentWillReceiveProps(this.props) this.providerStoreObserver = observe(providerStore, 'connectionInfoSchema', () => { if (this.props.onResizeUpdate) this.props.onResizeUpdate(this.scrollableRef) }) this.endpointValidationObserver = observe(endpointStore, 'validation', () => { this.UNSAFE_componentWillReceiveProps(this.props) }) } componentDidMount() { const loadSchema = async () => { if (!this.endpointType) { return } await providerStore.getConnectionInfoSchema(this.endpointType) this.fillRequiredDefaults() } loadSchema() KeyboardManager.onEnter('endpoint', () => { if (this.isValidateButtonEnabled) this.handleValidateClick() }, 2) } UNSAFE_componentWillReceiveProps(props: Props) { if (this.state.validating) { if (endpointStore.validation && !endpointStore.validation.valid) { this.setState({ validating: false }) } } if (props.endpoint && endpointStore.connectionInfo) { const plugin: any = ContentPlugin.for(props.endpoint.type) this.setState(prevState => ({ isNew: this.props.isNewEndpoint ? (prevState.isNew === null || prevState.isNew) : prevState.isNew, endpoint: { ...prevState.endpoint, ...ObjectUtils.flatten(props.endpoint || {}, plugin.REQUIRES_PARENT_OBJECT_PATH), ...ObjectUtils.flatten(endpointStore.connectionInfo || {}, plugin.REQUIRES_PARENT_OBJECT_PATH), }, })) } else { this.setState(prevState => ({ isNew: prevState.isNew === null || prevState.isNew, endpoint: { type: props.type, ...ObjectUtils.flatten(prevState.endpoint || {}), }, })) } if (props.onResizeUpdate) props.onResizeUpdate(this.scrollableRef) } componentWillUnmount() { endpointStore.clearValidation() providerStore.clearConnectionInfoSchema() clearTimeout(this.closeTimeout) KeyboardManager.removeKeyDown('endpoint') this.providerStoreObserver() this.endpointValidationObserver() } get endpointType() { if (this.props.endpoint) { return this.props.endpoint.type } return this.props.type } getFieldValue(field: Field | null) { if (!field || !this.state.endpoint) { return '' } if (this.state.endpoint[field.name] != null) { return this.state.endpoint[field.name] } if (Object.keys(field).find(k => k === 'default')) { return field.default } if (field.type === 'integer') { return null } return '' } fillRequiredDefaults() { this.setState(prevState => { const endpoint: any = { ...prevState.endpoint } const requiredFieldsDefaults = providerStore.connectionInfoSchema .filter(f => f.required && f.default != null) requiredFieldsDefaults.forEach(f => { if (endpoint[f.name] == null) { endpoint[f.name] = f.default } }) return { endpoint } }) } handleFieldsChange(items: { field: Field, value: any }[]) { this.setState(prevState => { const endpoint: any = { ...prevState.endpoint } items.forEach(item => { let value = item.value if (item.field.type === 'array') { const arrayItems = endpoint[item.field.name] || [] value = arrayItems.find((v: any) => v === item.value) ? arrayItems.filter((v: any) => v !== item.value) : [...arrayItems, item.value] } endpoint[item.field.name] = value }) return { endpoint } }) } handleValidateClick() { if (!this.highlightRequired()) { this.setState({ validating: true }) notificationStore.alert('Saving endpoint ...') endpointStore.clearValidation() if (this.state.isNew) { this.add() } else { this.update() } } else { notificationStore.alert('Please fill all the required fields', 'error') } } handleShowErrorMessageClick() { this.setState(prevState => ({ showErrorMessage: !prevState.showErrorMessage }), () => { if (this.props.onResizeUpdate) this.props.onResizeUpdate(this.scrollableRef) }) } handleCopyErrorMessageClick() { if (!endpointStore.validation) { return } const succesful = DomUtils.copyTextToClipboard(endpointStore.validation.message) if (succesful) { notificationStore.alert('The message has been copied to clipboard.') } } handleCancelClick() { if (this.props.deleteOnCancel && this.state.isNew === false) { endpointStore.delete(endpointStore.endpoints[0]) } this.props.onCancelClick() } highlightRequired() { const invalidFields = this.contentPluginRef.findInvalidFields() this.setState({ invalidFields }) return invalidFields.length > 0 } async update() { const stateEndpoint = this.state.endpoint if (!stateEndpoint) { return } const endpoint = endpointStore.endpoints.find(e => e.id === stateEndpoint.id) if (!endpoint) { throw new Error('Endpoint not found in store') } await endpointStore.update(stateEndpoint) this.setState({ endpoint: ObjectUtils.flatten(endpoint) }) notificationStore.alert('Validating endpoint ...') endpointStore.validate(endpoint) } async add() { if (!this.state.endpoint) { return } await endpointStore.add(this.state.endpoint) const endpoint = endpointStore.endpoints[0] this.setState({ isNew: false, endpoint: ObjectUtils.flatten(endpoint) }) notificationStore.alert('Validating endpoint ...') endpointStore.validate(endpoint) } renderEndpointStatus() { const validation = endpointStore.validation if (!this.state.validating && !validation) { return null } let status = 'RUNNING' let message = 'Validating Endpoint ...' let error = null let showErrorButton = null if (validation) { if (validation.valid) { message = 'Endpoint is Valid' status = 'COMPLETED' } else { status = 'ERROR' message = 'Validation failed' if (validation.message) { showErrorButton = ( { this.handleShowErrorMessageClick() }}> {this.state.showErrorMessage ? 'Hide' : 'Show'} Error ) error = this.state.showErrorMessage ? ( { this.handleCopyErrorMessageClick() }} >{validation.message} ) : null } } } return ( {message}{showErrorButton} {error} ) } renderButtons() { this.isValidateButtonEnabled = true let actionButton = ( ) let message = 'Validating Endpoint ...' if (this.state.validating || (endpointStore.validation && endpointStore.validation.valid)) { if (endpointStore.validation && endpointStore.validation.valid) { message = 'Saving ...' } this.isValidateButtonEnabled = false actionButton = {message} } return ( {actionButton} ) } renderContent() { if (providerStore.connectionSchemaLoading || !this.endpointType) { return null } const contentElement: any = ContentPlugin.for(this.endpointType) return ( {/* Fix browsers autofilling password fields */}
{this.renderEndpointStatus()} {React.createElement(contentElement, { connectionInfoSchema: providerStore.connectionInfoSchema, validation: endpointStore.validation, invalidFields: this.state.invalidFields, validating: this.state.validating, disabled: this.state.validating, cancelButtonText: this.props.cancelButtonText, originalConnectionInfo: endpointStore.connectionInfo, getFieldValue: (field: Field | null) => this.getFieldValue(field), highlightRequired: () => { this.highlightRequired() }, handleFieldChange: (field: Field | null, value: any) => { if (field) this.handleFieldsChange([{ field, value }]) }, handleFieldsChange: (fields: { field: Field; value: any }[]) => { this.handleFieldsChange(fields) }, handleValidateClick: () => { this.handleValidateClick() }, handleCancelClick: () => { this.handleCancelClick() }, scrollableRef: (ref: HTMLElement) => { this.scrollableRef = ref }, onRef: (ref: DefaultContentPlugin) => { this.contentPluginRef = ref }, onResizeUpdate: (scrollOffset: number) => { if (this.props.onResizeUpdate) { this.props.onResizeUpdate(this.scrollableRef, scrollOffset) } }, })} {this.renderButtons()}
) } renderLoading() { if (!providerStore.connectionSchemaLoading) { return null } return ( Loading connection schema ... ) } render() { if (endpointStore.validation && endpointStore.validation.valid && !this.closeTimeout) { this.closeTimeout = setTimeout(() => { this.props.onCancelClick({ autoClose: true }) }, 2000) } return ( {this.renderContent()} {this.renderLoading()} ) } } export default EndpointModal