· 6 years ago · Nov 25, 2019, 09:32 AM
1class Accounts extends Map {
2
3 constructor() {
4
5 super();
6
7 const filters = [
8 {
9 key: 'Id',
10 rowValue: row => [row.account_id],
11 },
12 {
13 key: 'Name',
14 rowValue: row => [row.name],
15 },
16 {
17 key: 'URL',
18 rowValue: row => [row.url.split(',')],
19 },
20 {
21 key: 'Owner Id',
22 rowValue: row => [row.owner_id],
23 },
24 {
25 key: 'Owner Name',
26 rowValue: row => [row.owner_name],
27 },
28 {
29 key: 'Owner Email',
30 rowValue: row => [row.owner_email],
31 },
32 {
33 key: 'Icon',
34 rowValue: row => [row.icon]
35 },
36 {
37 key: 'Logo',
38 rowValue: row => [row.logo]
39 },
40 {
41 key: 'Auth API',
42 rowValue: row => [row.auth_api]
43 }
44 ];
45
46 this.hostManager = new ListManager();
47
48 this.search = new SearchColumnFilters({ filters });
49
50 this.formId = `add-form${Math.floor(Math.random() * 1e5)}`;
51 }
52
53 async load(account_id = null) {
54
55 const accounts = await this.fetch(account_id);
56
57 this.process(accounts, account_id);
58
59 this.render();
60 }
61
62 async fetch(account_id) {
63
64 if(account_id) {
65 return await API.call('accounts/account/list', { account_id: account_id });
66 }
67 else {
68 return await API.call('accounts/account/list', {});
69 }
70 }
71
72 process(accounts, account_id) {
73
74 if (!account_id) {
75 this.clear();
76 }
77
78 for (const account of accounts) {
79 this.set(account.account_id, new AccountModule(account, this));
80 }
81 }
82
83 render() {
84
85 this.search.data = [...this.values()];
86
87 const tbody = this.list.querySelector('table tbody');
88
89 tbody.textContent = null;
90
91 const filterData = this.search.filterData;
92
93 for (const globalFilter of filterData) {
94 tbody.appendChild(globalFilter.row);
95 }
96
97 if (!filterData.length) {
98 tbody.innerHTML = '<tr><td colspan="8" class="NA">No Accounts Found</td></tr>';
99 }
100 }
101
102 get name() {
103 return 'Accounts';
104 }
105
106 get container() {
107
108 if (this.containerElement) {
109 return this.containerElement;
110 }
111
112 const container = this.containerElement = document.createElement('div');
113 container.classList.add('accounts-module', 'settings-module');
114
115 container.innerHTML = `
116 <section class="section" id="accounts-list"></section>
117 `;
118
119 container.querySelector('.section').appendChild(this.list);
120
121 return container;
122 }
123
124 get list() {
125
126 if (this.listElement) {
127 return this.listElement;
128 }
129
130 const container = this.listElement = document.createElement('div');
131
132 container.innerHTML = `
133 <h1>Manage Accounts</h1>
134 <header class="toolbar list-toolbar">
135 <button class="add-account" type="button">
136 <i class="fas fa-plus"></i> Add New Account
137 </button>
138 </header>
139
140 <table class="block">
141 <thead>
142 <tr>
143 <th>Id</th>
144 <th>Name</th>
145 <th>Hostnames</th>
146 <th>Owner</th>
147 <th>Icon</th>
148 <th>Logo</th>
149 <th>Edit</th>
150 <th>Delete</th>
151 </tr>
152 </thead>
153 <tbody></tbody>
154 </table>
155 `;
156
157 container.querySelector('.add-account').on('click', e => this.add());
158
159 new SortTable({ table: container.querySelector('table') }).sort();
160
161 this.search.on('change', () => this.render());
162
163 container.querySelector('.toolbar').appendChild(this.search.globalSearch.container);
164
165 container.insertBefore(this.search.container, container.querySelector('table'));
166
167 return container;
168 }
169
170 get form() {
171
172 if (this.formElement) {
173 return this.formElement;
174 }
175
176 const container = this.formElement = document.createElement('div');
177
178 container.innerHTML = `
179 <h1>Add New Account</h1>
180
181 <header class="toolbar">
182 <button class="cancel-form"><i class="fa fa-arrow-left"></i> Back</button>
183 <button type="submit" form="${this.formId}"><i class="far fa-save"></i> Save</button>
184 </header>
185
186 <form class="block form" id="${this.formId}">
187
188 <label>
189 <span>Name <span class="red">*</span></span>
190 <input type="text" name="name" required>
191 </label>
192
193 <label class="url">
194 <span>URL <span class="red">*</span></span>
195 </label>
196
197 <label>
198 <span>Icon</span>
199 <input type="url" name="icon" src="">
200 <img src="" alt="icon" id="icon" height="30" class="hidden">
201 </label>
202
203 <label>
204 <span>Logo</span>
205 <input type="url" name="logo" src="">
206 <img src="" alt="logo" id="logo" height="30" class="hidden">
207 </label>
208
209 <label>
210 <span>Authentication API</span>
211 <input type="url" name="auth_api">
212 </label>
213 </form>
214 `;
215
216 container.querySelector('.url').appendChild(this.hostManager.container);
217
218 container.querySelector('.cancel-form').on('click', () => {
219
220 const accountsSection = this.container.querySelector('.section');
221
222 accountsSection.textContent = null;
223
224 accountsSection.appendChild(this.list);
225 });
226
227 container.querySelector('form').on('submit', async e => {
228
229 e.preventDefault();
230
231 const accountId = await this.insert();
232
233 this.get(accountId).edit();
234 });
235
236 return container;
237 }
238
239 async insert() {
240
241 this.hostManager.value = this.hostManager.value.filter(url => url.trim());
242
243 if (!this.hostManager.value.length) {
244
245 new SnackBar({
246 message: 'URL cannot be empty',
247 type: 'warning'
248 });
249
250 throw new Page.exception('URL cannot be empty');
251 }
252
253 const options = {
254 method: 'POST',
255 form: new FormData(this.form.querySelector('form'))
256 };
257
258 options.form.set('url', this.hostManager.value.join(','));
259
260 try {
261
262 const response = await API.call('accounts/account/insert', {}, options);
263
264 await this.load(response.account_id);
265
266 new SnackBar({
267 message: 'Account Added',
268 subtitle: `${account.name} #${account.account_id}`,
269 icon: 'fa fa-plus',
270 });
271
272 return response.account_id;
273
274 } catch (e) {
275
276 new SnackBar({
277 message: 'Request Failed',
278 subtitle: e.message,
279 type: 'error',
280 });
281
282 throw e;
283 }
284 }
285
286 add() {
287
288 const accountsSection = this.container.querySelector('.section');
289
290 this.form.querySelector('form').reset();
291
292 this.hostManager.value = [];
293
294 accountsSection.textContent = null;
295
296 accountsSection.appendChild(this.form);
297 }
298}
299
300class AccountModule {
301
302 constructor(account, accounts) {
303
304 Object.assign(this, account);
305
306 this.accounts = accounts;
307
308 this.hostManager = new ListManager(this.url.split(','));
309
310 const settings_json = [
311 {
312 key: 'logout_redirect_url',
313 type: 'url',
314 name: 'Logout Url',
315 description: 'Redirect user to specified url',
316 },
317 {
318 key: 'custom_daterange',
319 type: 'offset',
320 name: 'Date Range Presets',
321 description: 'The default presets that are shown in a date range filter',
322 },
323 {
324 key: 'global_filters_position',
325 type: 'multiselect',
326 name: 'Global Filters Position',
327 description: 'Global Filters available on dashboards',
328 datalist: [
329 { name: 'Top', value: 'top' },
330 { name: 'Right', value: 'right' },
331 ],
332 multiple: false,
333 },
334 {
335 key: 'theme',
336 type: 'multiselect',
337 name: 'Theme',
338 description: 'Will be used by default for all users',
339 datalist: [
340 { name: 'Light', value: 'light' },
341 { name: 'Dark', value: 'dark' },
342 ],
343 multiple: false,
344 },
345 {
346 key: 'show_report_error_to_user',
347 type: 'multiselect',
348 name: 'Show Report Error',
349 description: 'Restrict error visibility to user',
350 datalist: [
351 { name: 'None', value: '' },
352 { name: 'User', value: 'user' },
353 { name: 'Editor', value: 'editor' },
354 ],
355 multiple: false,
356 default_value: ['editor'],
357 },
358 {
359 key: 'pre_report_api',
360 type: 'url',
361 name: 'Pre Report API',
362 description: 'An API that is hit before any report is executed',
363 },
364 {
365 key: 'reset_password_api',
366 type: 'url',
367 name: 'Reset Password API',
368 description: 'An API that is required to reset the password',
369 },
370 {
371 key: 'load_saved_connection',
372 type: 'number',
373 name: 'Store Report Result Connection ID',
374 description: 'The Connection where the report\'s result will be saved in',
375 },
376 {
377 key: 'load_saved_database',
378 type: 'string',
379 name: 'Store Report Result Database',
380 description: 'The database where the report\'s result will be saved in',
381 },
382 {
383 key: 'disable_share_report_url',
384 type: 'toggle',
385 name: 'Disable share report via url',
386 },
387 {
388 key: 'enable_account_signup',
389 type: 'toggle',
390 name: 'Allow User Signup',
391 },
392 {
393 key: 'disable_footer',
394 type: 'toggle',
395 name: 'Disable Footer',
396 },
397 {
398 key: 'user_onboarding',
399 type: 'toggle',
400 name: 'Enable user onboarding'
401 },
402 {
403 key: 'visualization_roles_from_query',
404 type: 'toggle',
405 name: 'Visualization Roles From Query',
406 description: 'Apply Visualization Roles From Its Parent Report'
407 },
408 {
409 key: 'dashboard_search_threshold',
410 type: 'number',
411 name: 'Threshold for search bar in dashboards navigation',
412 description: 'The Threshold after which the search bar will appear on the dashboard navigation, default is 40',
413 },
414 {
415 key: 'hide_dashboard_report_visible',
416 type: 'toggle',
417 name: 'Hide Dashboard containing a report which is visible',
418 },
419 {
420 key: 'custom_js',
421 type: 'code',
422 mode: 'javascript',
423 name: 'Custom JavaScript',
424 description: 'Custom JavaScript for this account'
425 },
426 {
427 key: 'custom_css',
428 type: 'code',
429 mode: 'css',
430 name: 'Custom CSS',
431 description: 'Custom CSS for this account'
432 },
433 {
434 key: 'external_parameters',
435 type: 'json',
436 name: 'External Parameters',
437 description: 'External Parameter for this account'
438 },
439 ];
440
441 this.settingsContainer = new SettingsManager({
442 owner: 'account',
443 owner_id: this.account_id,
444 format: settings_json
445 });
446
447 this.load();
448
449 this.features = new AccountsFeatures(this);
450
451 this.formId = `edit-form${Math.floor(Math.random() * 100000)}`;
452 }
453
454 async load() {
455
456 await this.settingsContainer.load();
457 }
458
459 get row() {
460
461 if (this.rowElement) {
462 return this.rowElement;
463 }
464
465 const url = this.url.split(',')
466 .map(url => `<a href="//${url}" target="_blank">${url}</a>`)
467 .join(' · ');
468
469 let owner = '';
470
471 if(this.owner) {
472 owner = this.owner
473 .map(owner => `${owner.name}<span class="NA"> #${owner.id} ${owner.email}</span>`)
474 .join(' · ');
475 }
476
477 const container = this.rowElement = document.createElement('tr');
478 container.classList.add('acccount-info');
479
480 container.innerHTML = `
481 <td>${this.account_id}</td>
482 <td>${this.name}</td>
483 <td>${url}</td>
484 <td>${owner}</td>
485 <td><img src="${this.icon}" height="30"></td>
486 <td><img src="${this.logo}" height="30"></td>
487 <td class="action green" title="Edit"><i class="far fa-edit"></i></td>
488 <td class="action red" title="Delete"><i class="far fa-trash-alt"></i></td>
489 `;
490
491 container.querySelector('.green').on('click', () => this.edit());
492
493 container.querySelector('.red').on('click', () => {
494
495 if (!confirm('Are you sure?!')) {
496 return;
497 }
498
499 this.delete()
500 });
501
502 return container;
503 }
504
505 get form() {
506
507 if (this.formElement) {
508 return this.formElement;
509 }
510
511 const container = this.formElement = document.createElement('div');
512
513 container.innerHTML = `
514 <h1>Editing ${this.name}</h1>
515
516 <header class="toolbar">
517 <button class="cancel-form"><i class="fa fa-arrow-left"></i> Back</button>
518 <button type="submit" form="${this.formId}"><i class="far fa-save"></i> Save</button>
519 </header>
520
521 <form class="block form" id="${this.formId}">
522
523 <label>
524 <span>Name <span class="red">*</span></span>
525 <input type="text" name="name" required>
526 </label>
527
528 <label class="url">
529 <span>URL <span class="red">*</span></span>
530 </label>
531
532 <label>
533 <span>Icon</span>
534 <input type="url" name="icon" src="">
535 <img src="" alt="icon" id="icon" height="30" class="hidden">
536 </label>
537
538 <label>
539 <span>Logo</span>
540 <input type="url" name="logo" src="">
541 <img src="" alt="logo" id="logo" height="30" class="hidden">
542 </label>
543
544 <label>
545 <span>Authentication API</span>
546 <input type="url" name="auth_api">
547 </label>
548 </form>
549 `;
550
551 const form = container.querySelector('.form');
552
553 form.name.value = this.name;
554
555 form.auth_api.value = this.auth_api;
556
557 container.querySelector('.url').appendChild(this.hostManager.container);
558
559 if (this.icon) {
560
561 const image = container.querySelector('#icon');
562
563 image.classList.remove('hidden');
564 image.src = this.icon;
565
566 form.icon.value = this.icon;
567 }
568
569 if (this.logo) {
570
571 const image = container.querySelector('#logo');
572
573 image.classList.remove('hidden');
574 image.src = this.logo;
575
576 form.logo.value = this.logo;
577 }
578
579 container.appendChild(this.settingsContainer.container);
580
581 container.appendChild(this.features.container);
582
583 container.querySelector('.cancel-form').on('click', () => {
584
585 const accountsSection = this.accounts.container.querySelector('.section');
586
587 accountsSection.textContent = null;
588
589 accountsSection.appendChild(this.accounts.list);
590 });
591
592 container.querySelector('form').on('submit', async e => {
593
594 e.preventDefault();
595
596 await this.update();
597
598 const accountsSection = this.accounts.container.querySelector('.section');
599
600 accountsSection.textContent = null;
601
602 accountsSection.appendChild(this.accounts.list);
603 });
604
605 return container;
606 }
607
608 async update() {
609
610 this.hostManager.value = this.hostManager.value.filter(url => url.trim());
611
612 if (!this.hostManager.value.length) {
613
614 new SnackBar({
615 message: 'URL cannot be empty',
616 type: 'warning'
617 });
618
619 throw new Page.exception('URL cannot be empty');
620 }
621
622 const
623 parameters = {
624 account_id: this.account_id,
625 },
626 options = {
627 method: 'POST',
628 form: new FormData(this.form.querySelector('form'))
629 };
630
631 options.form.set('url', this.hostManager.value.join(','));
632
633 try {
634
635 await API.call('accounts/account/update', parameters, options);
636
637 await this.accounts.load(this.account_id);
638
639 new SnackBar({
640 message: 'Account Saved',
641 subtitle: `${account.name} #${this.account_id}`,
642 icon: 'far fa-save',
643 });
644
645 } catch (e) {
646
647 new SnackBar({
648 message: 'Request Failed',
649 subtitle: e.message,
650 type: 'error',
651 });
652
653 throw e;
654 }
655 }
656
657 async delete() {
658
659 const
660 options = {
661 method: 'POST'
662 },
663 parameters = {
664 account_id: this.account_id
665 };
666
667 try {
668
669 await API.call('accounts/account/delete', parameters, options);
670
671 this.accounts.delete(this.account_id);
672
673 this.accounts.render();
674
675 new SnackBar({
676 message: 'Account Deleted',
677 subtitle: `${this.name} #${this.account_id}`,
678 icon: 'far fa-trash-alt',
679 });
680
681 } catch (e) {
682
683 new SnackBar({
684 message: 'Request Failed',
685 subtitle: e.message,
686 type: 'error',
687 });
688
689 throw e;
690 }
691 }
692
693 async edit() {
694
695 const accountsSection = this.accounts.container.querySelector('.section');
696
697 accountsSection.textContent = null;
698
699 accountsSection.appendChild(this.form);
700 }
701}
702
703class AccountsFeatures {
704
705 constructor(account) {
706
707 this.account = account;
708
709 this.totalFeatures = new Map;
710
711 for (const [key, feature] of MetaData.features) {
712 feature.status = this.account.features.includes(key.toString());
713 this.totalFeatures.set(key, new AccountsFeature(feature, this.account));
714 }
715
716 this.sortTable = new SortTable();
717 }
718
719 get container() {
720
721 if (this.containerElement) {
722 return this.containerElement;
723 }
724
725 const container = this.containerElement = document.createElement('div');
726
727 container.classList.add('feature-form');
728
729 container.innerHTML = `
730
731 <h3>Features</h3>
732
733 <div class="toolbar form">
734
735 <label class="feature-type">
736 <span>Types</span>
737 </label>
738
739 <label>
740 <span>Search</span>
741 <input id="feature-search" type="text" placeholder="Search..">
742 </label>
743 </div>
744
745 <div class="table-container">
746 <table>
747 <thead>
748 <tr>
749 <th>ID</th>
750 <th>Type</th>
751 <th>Name</th>
752 <th>Slug</th>
753 <th>Status</th>
754 </tr>
755 </thead>
756 <tbody></tbody>
757 </table>
758 </div>
759 `;
760
761 let list = new Set;
762
763 for (const value of MetaData.features.values()) {
764 list.add(value.type);
765 }
766
767 list = Array.from(list).map(x => { return { name: x, value: x } });
768
769 this.featureType = new MultiSelect({ datalist: Array.from(list), multiple: true });
770
771 container.querySelector('.feature-type').appendChild(this.featureType.container);
772
773 container.querySelector('#feature-search').on('keyup', () => this.render());
774
775 this.featureType.on('change', () => this.render());
776
777 this.render();
778
779 this.sortTable.table = container.querySelector('.table-container table');
780 this.sortTable.sort();
781
782 return container;
783 }
784
785 render() {
786
787 const
788 tbody = this.container.querySelector('tbody'),
789 selectedTypes = this.featureType.value,
790 searchQuery = this.container.querySelector('#feature-search').value.toLowerCase();
791
792 tbody.textContent = null;
793
794 for (const feature of this.totalFeatures.values()) {
795
796 if (selectedTypes.length && !selectedTypes.includes(feature.type)) {
797 continue;
798 }
799
800 if (searchQuery && !feature.name.toLowerCase().includes(searchQuery) && !(feature.status ? 'enabled' : 'disabled').includes(searchQuery)) {
801 continue;
802 }
803
804 tbody.appendChild(feature.row);
805 }
806
807 if (!tbody.children.length) {
808 tbody.innerHTML = '<tr><td colspan=4 class="NA">No Feature found</td></tr>';
809 }
810 }
811}
812
813class AccountsFeature {
814
815 constructor(feature, account) {
816
817 for (const key in feature)
818 this[key] = feature[key];
819
820 this.account = account;
821 }
822
823 get row() {
824
825 if (this.rowElement)
826 return this.rowElement;
827
828 const tr = this.rowElement = document.createElement('tr');
829
830 tr.innerHTML = `
831 <td>${this.feature_id}</td>
832 <td>${this.type}</td>
833 <td>${this.name}</td>
834 <td>${this.slug}</td>
835 <td class="feature-toggle" data-sort-by="${this.status}">
836 <div>
837 <label><input type="radio" name="status-${this.feature_id}" value="1"> Enabled</label>
838 <label><input type="radio" name="status-${this.feature_id}" value="0"> Disabled</label>
839 <div>
840 </td>
841 `;
842
843 for (const input of tr.querySelectorAll('input')) {
844
845 if (parseInt(input.value) == this.status)
846 input.checked = true;
847
848 input.on('change', e => {
849 tr.querySelector('.feature-toggle').dataset.sortBy = input.value;
850 this.update(e, parseInt(input.value));
851 });
852 }
853
854 return tr;
855 }
856
857 async update(e, status) {
858
859 e.preventDefault();
860
861 const
862 options = {
863 method: 'POST',
864 },
865 parameter = {
866 account_id: this.account.account_id,
867 feature_id: this.feature_id,
868 status,
869 };
870
871 try {
872
873 await API.call('accounts/features/toggle', parameter, options);
874
875 new SnackBar({
876 message: `${this.name} Feature ${status ? 'Enabled' : 'Disabled'}`,
877 subtitle: this.type,
878 icon: status ? 'fas fa-check' : 'fas fa-ban',
879 });
880
881 } catch (e) {
882
883 new SnackBar({
884 message: 'Request Failed',
885 subtitle: e.message,
886 type: 'error',
887 });
888
889 throw e;
890 }
891 }
892}
893
894class ListManager {
895
896 constructor(items = []) {
897
898 this.items = new Set();
899
900 this.value = items;
901 }
902
903 render() {
904
905 if (!this.containerElement) {
906 return;
907 }
908
909 const items = this.container.querySelector('.items');
910 items.textContent = null;
911
912 for (const item of this.items.values()) {
913 items.appendChild(item.container);
914 }
915
916 if (!this.items.size) {
917 items.innerHTML = '<div class="NA">No items found…</div>';
918 }
919 }
920
921 add() {
922
923 const input = this.container.querySelector('.add-item input');
924
925 if (!input.value) {
926
927 new SnackBar({
928 message: 'Invalid Input',
929 subtitle: 'Input field cannot be empty',
930 type: 'warning'
931 });
932
933 return;
934 }
935
936 this.items.add(new ListManagerItem(input.value, this));
937
938 this.render();
939
940 input.value = '';
941 }
942
943 get container() {
944
945 if (this.containerElement) {
946 return this.containerElement;
947 }
948
949 const container = this.containerElement = document.createElement('div');
950 container.classList.add('list-manager');
951
952 container.innerHTML = `
953 <div class="add-item item">
954 <input type="text">
955 <button type="button"><i class="fa fa-plus"></i>Add</button>
956 </div>
957 <div class="items"></div>
958 `;
959
960 this.container.querySelector('.add-item button').on('click', e => this.add());
961
962 this.render();
963
964 return container;
965 }
966
967 set value(items) {
968
969 this.items.clear();
970
971 for (const item of items) {
972 this.items.add(new ListManagerItem(item, this));
973 }
974
975 this.render();
976 }
977
978 get value() {
979 return [...this.items].map(item => item.value);
980 }
981}
982
983class ListManagerItem {
984
985 constructor(value, listManager) {
986
987 this.value = value;
988
989 this.listManager = listManager;
990 }
991
992 delete() {
993
994 this.listManager.items.delete(this);
995
996 this.listManager.render();
997 }
998
999 get container() {
1000
1001 if (this.containerElement) {
1002 return this.containerElement;
1003 }
1004
1005 const container = this.containerElement = document.createElement('div');
1006 container.classList.add('item');
1007
1008 container.innerHTML = `
1009 <input type="text">
1010 <button type="button"><i class="far fa-trash-alt"></i></button>
1011 `;
1012
1013 container.querySelector('.item input').value = this.cachedValue;
1014
1015 container.querySelector('.item button').on('click', e => this.delete());
1016
1017 return container;
1018 }
1019
1020 set value(value) {
1021
1022 if (this.containerElement) {
1023 this.container.querySelector('.item input').value = value;
1024 }
1025 else {
1026 this.cachedValue = value;
1027 }
1028 }
1029
1030 get value() {
1031
1032 if (this.containerElement) {
1033 return this.container.querySelector('.item input').value;
1034 }
1035 else {
1036 return this.cachedValue;
1037 }
1038 }
1039}