blob: f9dffcbab9ec6120f774887e1e349f2feb55ac47 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { FunctionComponent, useEffect, useRef, useState } from 'react';
import { styled, t } from '@superset-ui/core';
import Icon from 'src//components/Icon';
import Modal from 'src/components/Modal';
import { useImportResource } from 'src/views/CRUD/hooks';
import { ImportResourceName } from 'src/views/CRUD/types';
export const StyledIcon = styled(Icon)`
margin: auto ${({ theme }) => theme.gridUnit * 2}px auto 0;
`;
const HelperMessage = styled.div`
display: block;
color: ${({ theme }) => theme.colors.grayscale.base};
font-size: ${({ theme }) => theme.typography.sizes.s - 1}px;
`;
const StyledInputContainer = styled.div`
padding-bottom: ${({ theme }) => theme.gridUnit * 2}px;
padding-top: ${({ theme }) => theme.gridUnit * 2}px;
& > div {
margin: ${({ theme }) => theme.gridUnit}px 0;
}
&.extra-container {
padding-top: 8px;
}
.confirm-overwrite {
margin-bottom: ${({ theme }) => theme.gridUnit * 2}px;
}
.input-container {
display: flex;
align-items: center;
label {
display: flex;
margin-right: ${({ theme }) => theme.gridUnit * 2}px;
}
i {
margin: 0 ${({ theme }) => theme.gridUnit}px;
}
}
input,
textarea {
flex: 1 1 auto;
}
textarea {
height: 160px;
resize: none;
}
input::placeholder,
textarea::placeholder {
color: ${({ theme }) => theme.colors.grayscale.light1};
}
textarea,
input[type='text'],
input[type='number'] {
padding: ${({ theme }) => theme.gridUnit * 1.5}px
${({ theme }) => theme.gridUnit * 2}px;
border-style: none;
border: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
border-radius: ${({ theme }) => theme.gridUnit}px;
&[name='name'] {
flex: 0 1 auto;
width: 40%;
}
&[name='sqlalchemy_uri'] {
margin-right: ${({ theme }) => theme.gridUnit * 3}px;
}
}
`;
export interface ImportModelsModalProps {
resourceName: ImportResourceName;
resourceLabel: string;
passwordsNeededMessage: string;
confirmOverwriteMessage: string;
addDangerToast: (msg: string) => void;
addSuccessToast: (msg: string) => void;
onModelImport: () => void;
show: boolean;
onHide: () => void;
passwordFields?: string[];
setPasswordFields?: (passwordFields: string[]) => void;
}
const ImportModelsModal: FunctionComponent<ImportModelsModalProps> = ({
resourceName,
resourceLabel,
passwordsNeededMessage,
confirmOverwriteMessage,
addDangerToast,
addSuccessToast,
onModelImport,
show,
onHide,
passwordFields = [],
setPasswordFields = () => {},
}) => {
const [isHidden, setIsHidden] = useState<boolean>(true);
const [uploadFile, setUploadFile] = useState<File | null>(null);
const [passwords, setPasswords] = useState<Record<string, string>>({});
const [needsOverwriteConfirm, setNeedsOverwriteConfirm] = useState<boolean>(
false,
);
const [confirmedOverwrite, setConfirmedOverwrite] = useState<boolean>(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const clearModal = () => {
setUploadFile(null);
setPasswordFields([]);
setPasswords({});
setNeedsOverwriteConfirm(false);
setConfirmedOverwrite(false);
if (fileInputRef && fileInputRef.current) {
fileInputRef.current.value = '';
}
};
const handleErrorMsg = (msg: string) => {
clearModal();
addDangerToast(msg);
};
const {
state: { alreadyExists, passwordsNeeded },
importResource,
} = useImportResource(resourceName, resourceLabel, handleErrorMsg);
useEffect(() => {
setPasswordFields(passwordsNeeded);
}, [passwordsNeeded, setPasswordFields]);
useEffect(() => {
setNeedsOverwriteConfirm(alreadyExists.length > 0);
}, [alreadyExists, setNeedsOverwriteConfirm]);
// Functions
const hide = () => {
setIsHidden(true);
onHide();
clearModal();
};
const onUpload = () => {
if (uploadFile === null) {
return;
}
importResource(uploadFile, passwords, confirmedOverwrite).then(result => {
if (result) {
addSuccessToast(t('The import was successful'));
clearModal();
onModelImport();
}
});
};
const changeFile = (event: React.ChangeEvent<HTMLInputElement>) => {
const { files } = event.target as HTMLInputElement;
setUploadFile((files && files[0]) || null);
};
const confirmOverwrite = (event: React.ChangeEvent<HTMLInputElement>) => {
const targetValue = (event.currentTarget?.value as string) ?? '';
setConfirmedOverwrite(targetValue.toUpperCase() === t('OVERWRITE'));
};
const renderPasswordFields = () => {
if (passwordFields.length === 0) {
return null;
}
return (
<>
<h5>Database passwords</h5>
<HelperMessage>{passwordsNeededMessage}</HelperMessage>
{passwordFields.map(fileName => (
<StyledInputContainer key={`password-for-${fileName}`}>
<div className="control-label">
{fileName}
<span className="required">*</span>
</div>
<input
name={`password-${fileName}`}
autoComplete={`password-${fileName}`}
type="password"
value={passwords[fileName]}
onChange={event =>
setPasswords({ ...passwords, [fileName]: event.target.value })
}
/>
</StyledInputContainer>
))}
</>
);
};
const renderOverwriteConfirmation = () => {
if (!needsOverwriteConfirm) {
return null;
}
return (
<>
<StyledInputContainer>
<div className="confirm-overwrite">{confirmOverwriteMessage}</div>
<div className="control-label">
{t('Type "%s" to confirm', t('OVERWRITE'))}
</div>
<input
data-test="overwrite-modal-input"
id="overwrite"
type="text"
onChange={confirmOverwrite}
/>
</StyledInputContainer>
</>
);
};
// Show/hide
if (isHidden && show) {
setIsHidden(false);
}
return (
<Modal
name="model"
className="import-model-modal"
disablePrimaryButton={
uploadFile === null || (needsOverwriteConfirm && !confirmedOverwrite)
}
onHandledPrimaryAction={onUpload}
onHide={hide}
primaryButtonName={needsOverwriteConfirm ? t('Overwrite') : t('Import')}
primaryButtonType={needsOverwriteConfirm ? 'danger' : 'primary'}
width="750px"
show={show}
title={<h4>{t('Import %s', resourceLabel)}</h4>}
>
<StyledInputContainer>
<div className="control-label">
<label htmlFor="modelFile">
{t('File')}
<span className="required">*</span>
</label>
</div>
<input
ref={fileInputRef}
data-test="model-file-input"
name="modelFile"
id="modelFile"
type="file"
accept=".yaml,.json,.yml,.zip"
onChange={changeFile}
/>
</StyledInputContainer>
{renderPasswordFields()}
{renderOverwriteConfirmation()}
</Modal>
);
};
export default ImportModelsModal;