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