[Feature] : Bulk env import and UX/UI improvements (#2509)

* feat(bulk-env-import): bulk import working like a charm

* feat(bulk-env-import): refresh no env dialog's styling

* feat(bulk-env-import): group create and import env within initial modal, UI improvements

* feat(bulk-env-import): minor styling fixes

* feat(bulk-env-import): handle incorrect files in env importer

---------

Co-authored-by: bpoulaindev <bpoulainpro@gmail.com>
This commit is contained in:
Baptiste Poulain 2024-07-04 08:31:24 +02:00 committed by GitHub
parent 01605f6f2a
commit 71353b0404
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 117 additions and 84 deletions

View File

@ -1,13 +1,12 @@
import React, { useEffect, useRef } from 'react';
import Portal from 'components/Portal';
import Modal from 'components/Modal';
import toast from 'react-hot-toast';
import { useFormik } from 'formik';
import { addEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import * as Yup from 'yup';
import { useDispatch } from 'react-redux';
import { SharedButton } from 'components/Environments/EnvironmentSettings';
const CreateEnvironment = ({ collection, onClose }) => {
const CreateEnvironment = ({ collection }) => {
const dispatch = useDispatch();
const inputRef = useRef();
const formik = useFormik({
@ -25,7 +24,6 @@ const CreateEnvironment = ({ collection, onClose }) => {
dispatch(addEnvironment(values.name, collection.uid))
.then(() => {
toast.success('Environment created in collection');
onClose();
})
.catch(() => toast.error('An error occurred while created the environment'));
}
@ -42,39 +40,32 @@ const CreateEnvironment = ({ collection, onClose }) => {
};
return (
<Portal>
<Modal
size="sm"
title={'Create Environment'}
confirmText="Create"
handleConfirm={onSubmit}
handleCancel={onClose}
>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<div>
<label htmlFor="name" className="block font-semibold">
Environment Name
</label>
<input
id="environment-name"
type="text"
name="name"
ref={inputRef}
className="block textbox mt-2 w-full"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.name || ''}
/>
{formik.touched.name && formik.errors.name ? (
<div className="text-red-500">{formik.errors.name}</div>
) : null}
</div>
</form>
</Modal>
</Portal>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<div>
<label htmlFor="name" className="block font-semibold">
Environment Name
</label>
<div className="flex items-center mt-2">
<input
id="environment-name"
type="text"
name="name"
ref={inputRef}
className="block textbox w-full"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.name || ''}
/>
<SharedButton className="py-2.5 ml-1" onClick={onSubmit}>
Create
</SharedButton>
</div>
{formik.touched.name && formik.errors.name ? <div className="text-red-500">{formik.errors.name}</div> : null}
</div>
</form>
);
};

View File

@ -1,38 +1,46 @@
import React from 'react';
import Portal from 'components/Portal';
import toast from 'react-hot-toast';
import { useDispatch } from 'react-redux';
import importPostmanEnvironment from 'utils/importers/postman-environment';
import { importEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import { toastError } from 'utils/common/error';
import Modal from 'components/Modal';
import { IconDatabaseImport } from '@tabler/icons';
const ImportEnvironment = ({ onClose, collection }) => {
const ImportEnvironment = ({ collection }) => {
const dispatch = useDispatch();
const handleImportPostmanEnvironment = () => {
importPostmanEnvironment()
.then((environment) => {
dispatch(importEnvironment(environment.name, environment.variables, collection.uid))
.then(() => {
toast.success('Environment imported successfully');
onClose();
})
.catch(() => toast.error('An error occurred while importing the environment'));
.then((environments) => {
environments
.filter((env) =>
env.name && env.name !== 'undefined'
? true
: () => {
toast.error('Failed to import environment: env has no name');
return false;
}
)
.map((environment) => {
dispatch(importEnvironment(environment.name, environment.variables, collection.uid))
.then(() => {
toast.success('Environment imported successfully');
})
.catch(() => toast.error('An error occurred while importing the environment'));
});
})
.catch((err) => toastError(err, 'Postman Import environment failed'));
};
return (
<Portal>
<Modal size="sm" title="Import Environment" hideFooter={true} handleConfirm={onClose} handleCancel={onClose}>
<div>
<div className="text-link hover:underline cursor-pointer" onClick={handleImportPostmanEnvironment}>
Postman Environment
</div>
</div>
</Modal>
</Portal>
<button
type="button"
onClick={handleImportPostmanEnvironment}
className="flex justify-center flex-col items-center w-full dark:bg-zinc-700 rounded-lg border-2 border-dashed border-zinc-300 dark:border-zinc-400 p-12 text-center hover:border-zinc-400 focus:outline-none focus:ring-2 focus:ring-amber-500 focus:ring-offset-2"
>
<IconDatabaseImport size={64} />
<span className="mt-2 block text-sm font-semibold">Import your Postman environments</span>
</button>
);
};

View File

@ -4,6 +4,43 @@ import CreateEnvironment from './CreateEnvironment';
import EnvironmentList from './EnvironmentList';
import StyledWrapper from './StyledWrapper';
import ImportEnvironment from './ImportEnvironment';
import { IconFileAlert } from '@tabler/icons';
export const SharedButton = ({ children, className, onClick }) => {
return (
<button
type="button"
onClick={onClick}
className={`rounded bg-transparent px-2.5 py-2 w-fit text-xs font-semibold text-zinc-900 dark:text-zinc-50 shadow-sm ring-1 ring-inset ring-zinc-300 dark:ring-zinc-500 hover:bg-gray-50 dark:hover:bg-zinc-700
${className}`}
>
{children}
</button>
);
};
const DefaultTab = ({ setTab }) => {
return (
<div className="text-center items-center flex flex-col">
<IconFileAlert size={64} strokeWidth={1} />
<span className="font-semibold mt-2">No environments found</span>
<span className="font-extralight mt-2 text-zinc-500 dark:text-zinc-400">
Get started by using the following buttons :
</span>
<div className="flex items-center justify-center mt-6">
<SharedButton onClick={() => setTab('create')}>
<span>Create Environment</span>
</SharedButton>
<span className="mx-4">Or</span>
<SharedButton onClick={() => setTab('import')}>
<span>Import Environment</span>
</SharedButton>
</div>
</div>
);
};
const EnvironmentSettings = ({ collection, onClose }) => {
const [isModified, setIsModified] = useState(false);
@ -11,38 +48,25 @@ const EnvironmentSettings = ({ collection, onClose }) => {
const [openCreateModal, setOpenCreateModal] = useState(false);
const [openImportModal, setOpenImportModal] = useState(false);
const [selectedEnvironment, setSelectedEnvironment] = useState(null);
const [tab, setTab] = useState('default');
if (!environments || !environments.length) {
return (
<StyledWrapper>
<Modal
size="md"
title="Environments"
confirmText={'Close'}
handleConfirm={onClose}
confirmText={'Go back'}
handleConfirm={() => setTab('default')}
handleCancel={onClose}
hideCancel={true}
>
{openCreateModal && <CreateEnvironment collection={collection} onClose={() => setOpenCreateModal(false)} />}
{openImportModal && <ImportEnvironment collection={collection} onClose={() => setOpenImportModal(false)} />}
<div className="text-center flex flex-col">
<p>No environments found!</p>
<button
className="btn-create-environment text-link pr-2 py-3 mt-2 select-none"
onClick={() => setOpenCreateModal(true)}
>
<span>Create Environment</span>
</button>
<span>Or</span>
<button
className="btn-import-environment text-link pl-2 pr-2 py-3 select-none"
onClick={() => setOpenImportModal(true)}
>
<span>Import Environment</span>
</button>
</div>
{tab === 'create' ? (
<CreateEnvironment collection={collection} />
) : tab === 'import' ? (
<ImportEnvironment collection={collection} />
) : (
<DefaultTab setTab={setTab} />
)}
</Modal>
</StyledWrapper>
);

View File

@ -60,7 +60,7 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
<button
type="button"
onClick={onClick}
className={`rounded bg-transparent px-2.5 py-1 text-xs font-semibold text-slate-900 dark:text-slate-50 shadow-sm ring-1 ring-inset ring-zinc-300 dark:ring-zinc-500 hover:bg-gray-50 dark:hover:bg-zinc-700
className={`rounded bg-transparent px-2.5 py-1 text-xs font-semibold text-zinc-900 dark:text-zinc-50 shadow-sm ring-1 ring-inset ring-zinc-300 dark:ring-zinc-500 hover:bg-gray-50 dark:hover:bg-zinc-700
${className}`}
>
{children}

View File

@ -57,10 +57,20 @@ const parsePostmanEnvironment = (str) => {
const importEnvironment = () => {
return new Promise((resolve, reject) => {
fileDialog({ accept: 'application/json' })
.then(readFile)
.then(parsePostmanEnvironment)
.then((environment) => resolve(environment))
fileDialog({ multiple: true, accept: 'application/json' })
.then((files) => {
return Promise.all(
Object.values(files ?? {}).map((file) =>
readFile([file])
.then(parsePostmanEnvironment)
.catch((err) => {
console.error(`Error processing file: ${file.name || 'undefined'}`, err);
throw err;
})
)
);
})
.then((environments) => resolve(environments))
.catch((err) => {
console.log(err);
reject(new BrunoError('Import Environment failed'));