1+ <!DOCTYPE html>
2+ < html >
3+ < head >
4+ < title > Countly Edge - Import Data</ title >
5+ < link rel ="icon " type ="image/x-icon " href ="../images/favicon.png ">
6+ < style >
7+ : root {
8+ --primary-color : # 0166D6 ;
9+ --background-color : # F7F7F7 ;
10+ --border-color : # ECECEC ;
11+ --text-color : # 333333 ;
12+ --success-color : # 2FA732 ;
13+ --error-color : # D63E40 ;
14+ }
15+
16+ body {
17+ font-family : -apple-system, Inter, system-ui, sans-serif;
18+ background-color : var (--background-color );
19+ color : var (--text-color );
20+ margin : 0 ;
21+ padding : 2rem ;
22+ line-height : 1.5 ;
23+ }
24+
25+ .container {
26+ max-width : 1000px ;
27+ margin : 0 auto;
28+ background : white;
29+ padding : 2rem ;
30+ border-radius : 12px ;
31+ box-shadow : 0 2px 8px rgba (0 , 0 , 0 , 0.05 );
32+ }
33+
34+ h1 {
35+ color : var (--text-color );
36+ font-size : 24px ;
37+ font-weight : 600 ;
38+ margin : 0 0 2rem ;
39+ padding-bottom : 1rem ;
40+ border-bottom : 1px solid var (--border-color );
41+ }
42+
43+ .form-group {
44+ margin-bottom : 1.5rem ;
45+ }
46+
47+ label {
48+ display : block;
49+ margin-bottom : 0.5rem ;
50+ font-weight : 500 ;
51+ color : var (--text-color );
52+ }
53+
54+ input {
55+ width : 100% ;
56+ padding : 0.75rem ;
57+ border : 1px solid var (--border-color );
58+ border-radius : 6px ;
59+ font-size : 14px ;
60+ transition : border-color 0.2s ;
61+ box-sizing : border-box;
62+ }
63+
64+ input : focus {
65+ outline : none;
66+ border-color : var (--primary-color );
67+ }
68+
69+ input [type = "file" ] {
70+ padding : 0.5rem ;
71+ background : var (--background-color );
72+ }
73+
74+ button {
75+ background : var (--primary-color );
76+ color : white;
77+ border : none;
78+ padding : 0.75rem 1.5rem ;
79+ border-radius : 6px ;
80+ font-weight : 500 ;
81+ cursor : pointer;
82+ transition : opacity 0.2s ;
83+ font-size : 14px ;
84+ }
85+
86+ button : hover {
87+ opacity : 0.9 ;
88+ }
89+
90+ button : disabled {
91+ background : var (--border-color );
92+ cursor : not-allowed;
93+ }
94+
95+ .progress-container {
96+ margin-top : 2rem ;
97+ display : none;
98+ }
99+
100+ .progress {
101+ background : var (--background-color );
102+ border-radius : 6px ;
103+ overflow : hidden;
104+ height : 8px ;
105+ }
106+
107+ .progress-bar {
108+ height : 100% ;
109+ background : var (--primary-color );
110+ width : 0 ;
111+ transition : width 0.3s ease;
112+ }
113+
114+ # status {
115+ margin-top : 1rem ;
116+ padding : 1rem ;
117+ border-radius : 6px ;
118+ font-size : 14px ;
119+ }
120+
121+ .success {
122+ background : rgba (47 , 167 , 50 , 0.1 );
123+ color : var (--success-color );
124+ border : 1px solid rgba (47 , 167 , 50 , 0.2 );
125+ }
126+
127+ .error {
128+ background : rgba (214 , 62 , 64 , 0.1 );
129+ color : var (--error-color );
130+ border : 1px solid rgba (214 , 62 , 64 , 0.2 );
131+ }
132+
133+ .header {
134+ display : flex;
135+ align-items : center;
136+ margin-bottom : 2rem ;
137+ }
138+
139+ .header img {
140+ height : 32px ;
141+ margin-right : 1rem ;
142+ }
143+ </ style >
144+ </ head >
145+ < body >
146+ < div class ="container ">
147+ < div class ="header ">
148+ < img src ="../images/pre-login/countly-logo-dark.svg " alt ="Countly ">
149+ </ div >
150+ < div class ="header ">
151+ < h1 > Import Data</ h1 >
152+ </ div >
153+ < form id ="importForm ">
154+ < div class ="form-group ">
155+ < label for ="file "> Export File (JSON or CSV)</ label >
156+ < input type ="file " id ="file " accept =".json,.csv " required >
157+ </ div >
158+ < div class ="form-group ">
159+ < label for ="server "> Server URL</ label >
160+ < input type ="url " id ="server " required >
161+ </ div >
162+ < div class ="form-group ">
163+ < label for ="batchSize "> Batch Size</ label >
164+ < input type ="number " id ="batchSize " value ="10 " min ="1 " max ="100 " required >
165+ </ div >
166+ < div class ="form-group ">
167+ < label for ="delay "> Delay between batches (ms)</ label >
168+ < input type ="number " id ="delay " value ="1000 " min ="0 " required >
169+ </ div >
170+ < div class ="form-group ">
171+ < button type ="submit "> Start Import</ button >
172+ </ div >
173+ </ form >
174+ < div class ="progress-container ">
175+ < div class ="progress ">
176+ < div class ="progress-bar "> </ div >
177+ </ div >
178+ < div id ="status "> </ div >
179+ </ div >
180+ </ div >
181+
182+ < script >
183+ const form = document . getElementById ( 'importForm' ) ;
184+ const serverInput = document . getElementById ( 'server' ) ;
185+ const progressBar = document . querySelector ( '.progress-bar' ) ;
186+ const progressContainer = document . querySelector ( '.progress-container' ) ;
187+ const status = document . getElementById ( 'status' ) ;
188+
189+ // Set default server URL from current location or fallback
190+ function setDefaultServer ( ) {
191+ const defaultServer = 'http://localhost:3000' ;
192+ try {
193+ const currentUrl = new URL ( window . location . href ) ;
194+ serverInput . value = `${ currentUrl . protocol } //${ currentUrl . host } ` ;
195+ } catch ( error ) {
196+ serverInput . value = defaultServer ;
197+ }
198+ }
199+
200+ // Call on page load
201+ setDefaultServer ( ) ;
202+
203+ async function processFile ( file ) {
204+ const text = await file . text ( ) ;
205+ if ( file . name . endsWith ( '.csv' ) ) {
206+ return text
207+ . split ( '\n' )
208+ . slice ( 1 )
209+ . filter ( line => line . trim ( ) )
210+ . map ( line => {
211+ const [ , , data ] = line . split ( ',' ) ;
212+ return JSON . parse ( data ) ;
213+ } ) ;
214+ } else {
215+ const json = JSON . parse ( text ) ;
216+ return json . records . map ( r => r . data ) ;
217+ }
218+ }
219+
220+ async function sleep ( ms ) {
221+ return new Promise ( resolve => setTimeout ( resolve , ms ) ) ;
222+ }
223+
224+ form . addEventListener ( 'submit' , async ( e ) => {
225+ e . preventDefault ( ) ;
226+ const file = document . getElementById ( 'file' ) . files [ 0 ] ;
227+ if ( ! file ) return ;
228+
229+ const server = document . getElementById ( 'server' ) . value ;
230+ const batchSize = parseInt ( document . getElementById ( 'batchSize' ) . value ) ;
231+ const delay = parseInt ( document . getElementById ( 'delay' ) . value ) ;
232+
233+ try {
234+ form . querySelector ( 'button' ) . disabled = true ;
235+ progressContainer . style . display = 'block' ;
236+ status . className = '' ;
237+ status . textContent = 'Processing file...' ;
238+
239+ const records = await processFile ( file ) ;
240+ let processed = 0 ;
241+
242+ for ( let i = 0 ; i < records . length ; i += batchSize ) {
243+ const batch = records . slice ( i , i + batchSize ) ;
244+ try {
245+ const response = await fetch ( `${ server } /i/bulk` , {
246+ method : 'POST' ,
247+ headers : {
248+ 'Content-Type' : 'application/json'
249+ } ,
250+ body : JSON . stringify ( { requests : batch } )
251+ } ) ;
252+
253+ if ( ! response . ok ) throw new Error ( `HTTP ${ response . status } ` ) ;
254+
255+ processed += batch . length ;
256+ const percent = ( processed / records . length * 100 ) . toFixed ( 1 ) ;
257+ progressBar . style . width = percent + '%' ;
258+ status . textContent = `Processed ${ processed } /${ records . length } records (${ percent } %)` ;
259+
260+ if ( i + batchSize < records . length ) {
261+ await sleep ( delay ) ;
262+ }
263+ } catch ( error ) {
264+ throw new Error ( `Error sending batch at index ${ i } : ${ error . message } ` ) ;
265+ }
266+ }
267+
268+ status . className = 'success' ;
269+ status . textContent = `Import completed! Processed ${ processed } records.` ;
270+ } catch ( error ) {
271+ status . className = 'error' ;
272+ status . textContent = `Import failed: ${ error . message } ` ;
273+ } finally {
274+ form . querySelector ( 'button' ) . disabled = false ;
275+ }
276+ } ) ;
277+ </ script >
278+ </ body >
279+ </ html >
0 commit comments