1+ /* eslint-disable react/jsx-props-no-spreading */
12import 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' ;
34import type { ISelectItem } from '@megafon/ui-core/dist/lib/components/Select/Select' ;
45import { cnCreate } from '@megafon/ui-helpers' ;
56import { ReactComponent as DeleteIcon } from '@megafon/ui-icons/basic-16-delete_16.svg' ;
67import { ReactComponent as AttensionIcon } from '@megafon/ui-icons/system-24-attention_24.svg' ;
78import { ReactComponent as GagIcon } from '@megafon/ui-icons/system-24-gag_24.svg' ;
9+ import cloneDeep from 'clone-deep' ;
10+ import { useDropzone } from 'react-dropzone' ;
811import Skeleton from 'react-loading-skeleton' ;
912import { useNavigate , useSearchParams } from 'react-router-dom' ;
13+ import type { SimulationItem , SimulationResponse } from 'api/types' ;
1014import { ReactComponent as PlusIcon } from 'static/favicon/plus.svg' ;
1115import { useSelector } from 'store/hooks' ;
16+ import { downloadFile } from 'utils' ;
17+ import Popup from '../Popup/Popup' ;
1218import type { RouteItem } from './types' ;
13- import { getRouteList } from './utils' ;
19+ import { getRouteList , validateImport } from './utils' ;
1420import './Simulations.pcss' ;
1521
1622const MAX_SIMULATIONS_ON_PAGE = 50 ;
@@ -25,11 +31,18 @@ const BADGE_ICON = {
2531
2632interface 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+
3144const 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 'n' 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