mirror of
https://github.com/usebruno/bruno.git
synced 2025-06-21 04:08:01 +02:00
[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:
parent
01605f6f2a
commit
71353b0404
@ -1,13 +1,12 @@
|
|||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import Portal from 'components/Portal';
|
|
||||||
import Modal from 'components/Modal';
|
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import { addEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
import { addEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { SharedButton } from 'components/Environments/EnvironmentSettings';
|
||||||
|
|
||||||
const CreateEnvironment = ({ collection, onClose }) => {
|
const CreateEnvironment = ({ collection }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const inputRef = useRef();
|
const inputRef = useRef();
|
||||||
const formik = useFormik({
|
const formik = useFormik({
|
||||||
@ -25,7 +24,6 @@ const CreateEnvironment = ({ collection, onClose }) => {
|
|||||||
dispatch(addEnvironment(values.name, collection.uid))
|
dispatch(addEnvironment(values.name, collection.uid))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success('Environment created in collection');
|
toast.success('Environment created in collection');
|
||||||
onClose();
|
|
||||||
})
|
})
|
||||||
.catch(() => toast.error('An error occurred while created the environment'));
|
.catch(() => toast.error('An error occurred while created the environment'));
|
||||||
}
|
}
|
||||||
@ -42,25 +40,18 @@ const CreateEnvironment = ({ collection, onClose }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Portal>
|
|
||||||
<Modal
|
|
||||||
size="sm"
|
|
||||||
title={'Create Environment'}
|
|
||||||
confirmText="Create"
|
|
||||||
handleConfirm={onSubmit}
|
|
||||||
handleCancel={onClose}
|
|
||||||
>
|
|
||||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="name" className="block font-semibold">
|
<label htmlFor="name" className="block font-semibold">
|
||||||
Environment Name
|
Environment Name
|
||||||
</label>
|
</label>
|
||||||
|
<div className="flex items-center mt-2">
|
||||||
<input
|
<input
|
||||||
id="environment-name"
|
id="environment-name"
|
||||||
type="text"
|
type="text"
|
||||||
name="name"
|
name="name"
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
className="block textbox mt-2 w-full"
|
className="block textbox w-full"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
autoCapitalize="off"
|
autoCapitalize="off"
|
||||||
@ -68,13 +59,13 @@ const CreateEnvironment = ({ collection, onClose }) => {
|
|||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
value={formik.values.name || ''}
|
value={formik.values.name || ''}
|
||||||
/>
|
/>
|
||||||
{formik.touched.name && formik.errors.name ? (
|
<SharedButton className="py-2.5 ml-1" onClick={onSubmit}>
|
||||||
<div className="text-red-500">{formik.errors.name}</div>
|
Create
|
||||||
) : null}
|
</SharedButton>
|
||||||
|
</div>
|
||||||
|
{formik.touched.name && formik.errors.name ? <div className="text-red-500">{formik.errors.name}</div> : null}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Modal>
|
|
||||||
</Portal>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,38 +1,46 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Portal from 'components/Portal';
|
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import importPostmanEnvironment from 'utils/importers/postman-environment';
|
import importPostmanEnvironment from 'utils/importers/postman-environment';
|
||||||
import { importEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
import { importEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||||
import { toastError } from 'utils/common/error';
|
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 dispatch = useDispatch();
|
||||||
|
|
||||||
const handleImportPostmanEnvironment = () => {
|
const handleImportPostmanEnvironment = () => {
|
||||||
importPostmanEnvironment()
|
importPostmanEnvironment()
|
||||||
.then((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))
|
dispatch(importEnvironment(environment.name, environment.variables, collection.uid))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success('Environment imported successfully');
|
toast.success('Environment imported successfully');
|
||||||
onClose();
|
|
||||||
})
|
})
|
||||||
.catch(() => toast.error('An error occurred while importing the environment'));
|
.catch(() => toast.error('An error occurred while importing the environment'));
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch((err) => toastError(err, 'Postman Import environment failed'));
|
.catch((err) => toastError(err, 'Postman Import environment failed'));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Portal>
|
<button
|
||||||
<Modal size="sm" title="Import Environment" hideFooter={true} handleConfirm={onClose} handleCancel={onClose}>
|
type="button"
|
||||||
<div>
|
onClick={handleImportPostmanEnvironment}
|
||||||
<div className="text-link hover:underline cursor-pointer" 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"
|
||||||
Postman Environment
|
>
|
||||||
</div>
|
<IconDatabaseImport size={64} />
|
||||||
</div>
|
<span className="mt-2 block text-sm font-semibold">Import your Postman environments</span>
|
||||||
</Modal>
|
</button>
|
||||||
</Portal>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -4,6 +4,43 @@ import CreateEnvironment from './CreateEnvironment';
|
|||||||
import EnvironmentList from './EnvironmentList';
|
import EnvironmentList from './EnvironmentList';
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
import ImportEnvironment from './ImportEnvironment';
|
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 EnvironmentSettings = ({ collection, onClose }) => {
|
||||||
const [isModified, setIsModified] = useState(false);
|
const [isModified, setIsModified] = useState(false);
|
||||||
@ -11,38 +48,25 @@ const EnvironmentSettings = ({ collection, onClose }) => {
|
|||||||
const [openCreateModal, setOpenCreateModal] = useState(false);
|
const [openCreateModal, setOpenCreateModal] = useState(false);
|
||||||
const [openImportModal, setOpenImportModal] = useState(false);
|
const [openImportModal, setOpenImportModal] = useState(false);
|
||||||
const [selectedEnvironment, setSelectedEnvironment] = useState(null);
|
const [selectedEnvironment, setSelectedEnvironment] = useState(null);
|
||||||
|
const [tab, setTab] = useState('default');
|
||||||
if (!environments || !environments.length) {
|
if (!environments || !environments.length) {
|
||||||
return (
|
return (
|
||||||
<StyledWrapper>
|
<StyledWrapper>
|
||||||
<Modal
|
<Modal
|
||||||
size="md"
|
size="md"
|
||||||
title="Environments"
|
title="Environments"
|
||||||
confirmText={'Close'}
|
confirmText={'Go back'}
|
||||||
handleConfirm={onClose}
|
handleConfirm={() => setTab('default')}
|
||||||
handleCancel={onClose}
|
handleCancel={onClose}
|
||||||
hideCancel={true}
|
hideCancel={true}
|
||||||
>
|
>
|
||||||
{openCreateModal && <CreateEnvironment collection={collection} onClose={() => setOpenCreateModal(false)} />}
|
{tab === 'create' ? (
|
||||||
{openImportModal && <ImportEnvironment collection={collection} onClose={() => setOpenImportModal(false)} />}
|
<CreateEnvironment collection={collection} />
|
||||||
<div className="text-center flex flex-col">
|
) : tab === 'import' ? (
|
||||||
<p>No environments found!</p>
|
<ImportEnvironment collection={collection} />
|
||||||
<button
|
) : (
|
||||||
className="btn-create-environment text-link pr-2 py-3 mt-2 select-none"
|
<DefaultTab setTab={setTab} />
|
||||||
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>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
</StyledWrapper>
|
</StyledWrapper>
|
||||||
);
|
);
|
||||||
|
@ -60,7 +60,7 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClick}
|
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}`}
|
${className}`}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
@ -57,10 +57,20 @@ const parsePostmanEnvironment = (str) => {
|
|||||||
|
|
||||||
const importEnvironment = () => {
|
const importEnvironment = () => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
fileDialog({ accept: 'application/json' })
|
fileDialog({ multiple: true, accept: 'application/json' })
|
||||||
.then(readFile)
|
.then((files) => {
|
||||||
|
return Promise.all(
|
||||||
|
Object.values(files ?? {}).map((file) =>
|
||||||
|
readFile([file])
|
||||||
.then(parsePostmanEnvironment)
|
.then(parsePostmanEnvironment)
|
||||||
.then((environment) => resolve(environment))
|
.catch((err) => {
|
||||||
|
console.error(`Error processing file: ${file.name || 'undefined'}`, err);
|
||||||
|
throw err;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.then((environments) => resolve(environments))
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
reject(new BrunoError('Import Environment failed'));
|
reject(new BrunoError('Import Environment failed'));
|
||||||
|
Loading…
x
Reference in New Issue
Block a user