Skip to content

Commit 47817f8

Browse files
committed
add export and import simulations
1 parent 61932b1 commit 47817f8

File tree

8 files changed

+405
-56
lines changed

8 files changed

+405
-56
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
"react-codemirror2": "^7.2.1",
7070
"react-dev-utils": "^11.0.3",
7171
"react-dom": "^17.0.2",
72+
"react-dropzone": "^12.0.5",
7273
"react-loading-skeleton": "^3.0.3",
7374
"react-modal": "^3.14.3",
7475
"react-redux": "^7.2.5",

src/components/Popup/Popup.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,17 @@ import './Popup.pcss';
66
const HEIGHT_DELIMETER = 4;
77

88
interface IPopupProps {
9+
classes?: {
10+
root?: string;
11+
wrapper?: string;
12+
};
913
open: boolean;
1014
fade?: boolean;
1115
onClose?: () => void;
1216
}
1317

1418
const cn = cnCreate('popup');
15-
const Popup: React.FC<IPopupProps> = ({ open, children, fade, onClose }) => {
19+
const Popup: React.FC<IPopupProps> = ({ open, children, fade, onClose, classes }) => {
1620
const [offsetTop, setOffsetTop] = React.useState<number>(0);
1721
const nodeWrapperRef = React.useRef<HTMLDivElement | null>(null);
1822

@@ -42,7 +46,7 @@ const Popup: React.FC<IPopupProps> = ({ open, children, fade, onClose }) => {
4246
return (
4347
<Modal
4448
isOpen={open}
45-
className={cn('content')}
49+
className={cn('content', [classes?.root])}
4650
portalClassName={cn()}
4751
overlayClassName={cn('overlay')}
4852
bodyOpenClassName="popup-open"
@@ -52,7 +56,7 @@ const Popup: React.FC<IPopupProps> = ({ open, children, fade, onClose }) => {
5256
ariaHideApp={false}
5357
onRequestClose={handleClose}
5458
>
55-
<div className={cn('wrapper')}>{children}</div>
59+
<div className={cn('wrapper', [classes?.wrapper])}>{children}</div>
5660
{fade && <div className={cn('background')} />}
5761
</Modal>
5862
);

src/components/Simulations/Simulations.pcss

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,15 @@ $(b) {
1818
}
1919

2020
&__active-simulations {
21-
display: grid;
22-
grid-template-columns: 186px 103px;
21+
display: flex;
2322
gap: 16px;
2423
align-items: center;
2524
}
2625

26+
&__menu-button {
27+
width: 120px;
28+
}
29+
2730
&__fields {
2831
display: grid;
2932
flex-direction: flex-end;
@@ -280,4 +283,47 @@ $(b) {
280283
display: flex;
281284
margin-right: 16px;
282285
}
286+
287+
&__import-popup {
288+
width: 600px;
289+
}
290+
291+
&__import-popup-wrapper {
292+
padding: 36px;
293+
}
294+
295+
&__import-title {
296+
margin-bottom: 12px;
297+
}
298+
299+
&__import-dropzone {
300+
flex: 1;
301+
display: flex;
302+
flex-direction: column;
303+
align-items: center;
304+
padding: 20px;
305+
margin-bottom: 24px;
306+
border-width: 2px;
307+
border-radius: 2px;
308+
border-color: #eeeeee;
309+
border-style: dashed;
310+
background-color: #fafafa;
311+
color: #bdbdbd;
312+
outline: none;
313+
transition: border .24s ease-in-out;
314+
}
315+
316+
&__import-file {
317+
margin-bottom: 24px;
318+
}
319+
320+
&__import-pairs,
321+
&__import-errors {
322+
margin-bottom: 24px;
323+
}
324+
325+
&__import-buttons {
326+
display: flex;
327+
justify-content: space-around;
328+
}
283329
}

src/components/Simulations/Simulations.tsx

Lines changed: 175 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
1+
/* eslint-disable react/jsx-props-no-spreading */
12
import React, { useEffect, useState } from 'react';
2-
import { Header, TextField, Select, Pagination } from '@megafon/ui-core';
3+
import { Header, TextField, Select, Pagination, Button } from '@megafon/ui-core';
34
import type { ISelectItem } from '@megafon/ui-core/dist/lib/components/Select/Select';
45
import { cnCreate } from '@megafon/ui-helpers';
56
import { ReactComponent as DeleteIcon } from '@megafon/ui-icons/basic-16-delete_16.svg';
67
import { ReactComponent as AttensionIcon } from '@megafon/ui-icons/system-24-attention_24.svg';
78
import { ReactComponent as GagIcon } from '@megafon/ui-icons/system-24-gag_24.svg';
9+
import cloneDeep from 'clone-deep';
10+
import { useDropzone } from 'react-dropzone';
811
import Skeleton from 'react-loading-skeleton';
912
import { useNavigate, useSearchParams } from 'react-router-dom';
13+
import type { SimulationItem, SimulationResponse } from 'api/types';
1014
import { ReactComponent as PlusIcon } from 'static/favicon/plus.svg';
1115
import { useSelector } from 'store/hooks';
16+
import { downloadFile } from 'utils';
17+
import Popup from '../Popup/Popup';
1218
import type { RouteItem } from './types';
13-
import { getRouteList } from './utils';
19+
import { getRouteList, validateImport } from './utils';
1420
import './Simulations.pcss';
1521

1622
const MAX_SIMULATIONS_ON_PAGE = 50;
@@ -25,11 +31,18 @@ const BADGE_ICON = {
2531

2632
interface ISimulationsProps {
2733
onDelete: (index: number) => void;
34+
onImport: (state: SimulationResponse) => void;
2835
onChange: (index: number | undefined, type: 'edit' | 'delete' | 'new') => void;
2936
}
3037

38+
interface ISimulationsImportState {
39+
file: File | undefined;
40+
pairs: SimulationItem[];
41+
errors: string;
42+
}
43+
3144
const cn = cnCreate('simulations');
32-
const Simulations: React.FC<ISimulationsProps> = ({ onChange, onDelete }) => {
45+
const Simulations: React.FC<ISimulationsProps> = ({ onChange, onDelete, onImport }) => {
3346
const simulationStore = useSelector(state => state.simulation);
3447
const statusState = !!useSelector(state => state.status.value);
3548
const nav = useNavigate();
@@ -40,6 +53,30 @@ const Simulations: React.FC<ISimulationsProps> = ({ onChange, onDelete }) => {
4053
const [sortType, setSortType] = useState<ISelectItem<string>>(sortTypeItems[0]);
4154
const [activePage, setActivePage] = useState<number>(Number(page));
4255
const [simulations, setSimulations] = useState<RouteItem[]>([]);
56+
const [isImportOpen, setIsImportOpen] = useState<boolean>(false);
57+
const [importData, setImportData] = useState<ISimulationsImportState>({ file: undefined, pairs: [], errors: '' });
58+
const { getRootProps, getInputProps } = useDropzone({
59+
accept: 'application/json',
60+
onDrop: acceptedFiles => {
61+
acceptedFiles.forEach(file => {
62+
const reader = new FileReader();
63+
reader.readAsText(file);
64+
reader.onload = (ev: ProgressEvent<FileReader>) => {
65+
if (ev.target && typeof ev.target.result === 'string') {
66+
const val = JSON.parse(ev.target.result);
67+
const result = validateImport(JSON.parse(ev.target.result));
68+
69+
if (result.type === 'success') {
70+
setImportData({ file: file || undefined, pairs: val, errors: '' });
71+
} else {
72+
setImportData({ file: file || undefined, pairs: [], errors: result.message });
73+
}
74+
}
75+
};
76+
});
77+
},
78+
multiple: false,
79+
});
4380

4481
const searchSimulations = React.useMemo(
4582
() => simulations.filter(({ name }) => name.search(search) !== -1),
@@ -51,6 +88,42 @@ const Simulations: React.FC<ISimulationsProps> = ({ onChange, onDelete }) => {
5188
const simulationListOnPage = searchSimulations.slice(firstSimulationOnPageIndex, lastSimulationOnPageIndex);
5289
const totalSimulationPages = Math.ceil(searchSimulations.length / MAX_SIMULATIONS_ON_PAGE);
5390

91+
function handleImportPairs(type: 'add' | 'replace') {
92+
return (_e: React.MouseEvent<HTMLButtonElement>) => {
93+
if (simulationStore.type === 'success') {
94+
const newState = cloneDeep(simulationStore.value);
95+
96+
setActivePage(1);
97+
if (type === 'add') {
98+
newState.data.pairs = newState.data.pairs.concat(importData.pairs);
99+
onImport(newState);
100+
} else if (type === 'replace') {
101+
newState.data.pairs = importData.pairs;
102+
onImport(newState);
103+
}
104+
// eslint-disable-next-line @typescript-eslint/no-use-before-define
105+
handleCloseImport();
106+
}
107+
};
108+
}
109+
110+
function handleOpenImport() {
111+
setIsImportOpen(true);
112+
}
113+
114+
function handleCloseImport() {
115+
setIsImportOpen(false);
116+
setImportData({ file: undefined, pairs: [], errors: '' });
117+
}
118+
119+
function handleExport() {
120+
if (simulationStore.type === 'success') {
121+
const exportSimulations = searchSimulations.map(({ index }) => simulationStore.value.data.pairs[index]);
122+
123+
downloadFile(JSON.stringify(exportSimulations, null, 2), 'test.json');
124+
}
125+
}
126+
54127
function handleSimulationEditButtonClick(index: number) {
55128
return () => onChange(index, 'edit');
56129
}
@@ -151,55 +224,108 @@ const Simulations: React.FC<ISimulationsProps> = ({ onChange, onDelete }) => {
151224
));
152225

153226
return (
154-
<div className={cn()}>
155-
<Header className={cn('header')} as="h2">
156-
Simulations
157-
</Header>
158-
<div className={cn('menu')}>
159-
<div className={cn('active-simulations')}>
160-
<Header className={cn('active-simulations-header')} as="h3">
161-
Active simulations
162-
</Header>
163-
<button
164-
className={cn('nav-link', { disabled: !statusState })}
165-
type="button"
166-
onClick={handleAdd}
167-
disabled={!statusState}
168-
>
169-
<PlusIcon />
170-
</button>
171-
</div>
172-
<div className={cn('fields')}>
173-
<TextField
174-
classes={{
175-
input: cn('input'),
176-
}}
177-
onChange={handleChangeSearch}
178-
placeholder="Search simulation"
179-
/>
180-
<Select<string>
181-
classes={{
182-
control: cn('input'),
183-
}}
184-
items={sortTypeItems}
185-
currentValue={sortType.value}
186-
onSelect={handleSortTypeSelect}
187-
/>
227+
<>
228+
<div className={cn()}>
229+
<Header className={cn('header')} as="h2">
230+
Simulations
231+
</Header>
232+
<div className={cn('menu')}>
233+
<div className={cn('active-simulations')}>
234+
<Header className={cn('active-simulations-header')} as="h3">
235+
Active simulations
236+
</Header>
237+
<button
238+
className={cn('nav-link', { disabled: !statusState })}
239+
type="button"
240+
onClick={handleAdd}
241+
disabled={!statusState}
242+
>
243+
<PlusIcon />
244+
</button>
245+
<Button
246+
className={cn('menu-button')}
247+
sizeAll="small"
248+
type="outline"
249+
actionType="button"
250+
onClick={handleExport}
251+
disabled={!searchSimulations.length}
252+
>
253+
Export
254+
</Button>
255+
<Button
256+
className={cn('menu-button')}
257+
sizeAll="small"
258+
type="outline"
259+
actionType="button"
260+
onClick={handleOpenImport}
261+
>
262+
Import
263+
</Button>
264+
</div>
265+
<div className={cn('fields')}>
266+
<TextField
267+
classes={{
268+
input: cn('input'),
269+
}}
270+
onChange={handleChangeSearch}
271+
placeholder="Search simulation"
272+
/>
273+
<Select<string>
274+
classes={{
275+
control: cn('input'),
276+
}}
277+
items={sortTypeItems}
278+
currentValue={sortType.value}
279+
onSelect={handleSortTypeSelect}
280+
/>
281+
</div>
188282
</div>
283+
<ul className={cn('list')}>
284+
{simulationStore.type === 'pending' ? renderPreloader() : renderSimulationList()}
285+
</ul>
286+
{simulations.length > MAX_SIMULATIONS_ON_PAGE && (
287+
<div className={cn('pagination-wrap')}>
288+
<Pagination
289+
totalPages={totalSimulationPages}
290+
activePage={activePage}
291+
onChange={handlePaginationChange}
292+
/>
293+
</div>
294+
)}
189295
</div>
190-
<ul className={cn('list')}>
191-
{simulationStore.type === 'pending' ? renderPreloader() : renderSimulationList()}
192-
</ul>
193-
{simulations.length > MAX_SIMULATIONS_ON_PAGE && (
194-
<div className={cn('pagination-wrap')}>
195-
<Pagination
196-
totalPages={totalSimulationPages}
197-
activePage={activePage}
198-
onChange={handlePaginationChange}
199-
/>
296+
<Popup
297+
classes={{ root: cn('import-popup'), wrapper: cn('import-popup-wrapper') }}
298+
open={isImportOpen}
299+
onClose={handleCloseImport}
300+
>
301+
<div className={cn('import-wrapper')}>
302+
<Header className={cn('import-title')} as="h2">
303+
Import
304+
</Header>
305+
<section className="container">
306+
<div {...getRootProps({ className: cn('import-dropzone') })}>
307+
<input {...getInputProps()} />
308+
<p>Drag &apos;n&apos; drop json file here, or click to select file</p>
309+
</div>
310+
</section>
311+
{importData.errors && (
312+
<div className={cn('import-errors')} dangerouslySetInnerHTML={{ __html: importData.errors }} />
313+
)}
314+
{importData.pairs.length !== 0 && (
315+
<div className={cn('import-pairs')}>File have {importData.pairs.length} valid simulations</div>
316+
)}
317+
<div className={cn('import-buttons')}>
318+
<Button
319+
disabled={importData.pairs.length === 0}
320+
actionType="button"
321+
onClick={handleImportPairs('replace')}
322+
>
323+
Import
324+
</Button>
325+
</div>
200326
</div>
201-
)}
202-
</div>
327+
</Popup>
328+
</>
203329
);
204330
};
205331

0 commit comments

Comments
 (0)