|
235 | 235 | :data-test-id="domain.name + '-menu'" |
236 | 236 | > |
237 | 237 | <cv-overflow-menu-item |
238 | | - @click="goToDomainConfiguration(domain)" |
| 238 | + v-if="domain.location == 'internal'" |
| 239 | + @click="showImportUsersModal(domain)" |
| 240 | + :data-test-id="domain.name + '-import-data'" |
239 | 241 | > |
240 | 242 | <NsMenuItem |
241 | | - :icon="Settings20" |
242 | | - :label="$t('domain_configuration.configuration')" |
| 243 | + :icon="Upload20" |
| 244 | + :label="$t('domain_users.import_data')" |
| 245 | + /> |
| 246 | + </cv-overflow-menu-item> |
| 247 | + <cv-overflow-menu-item |
| 248 | + v-if="domain.location == 'internal'" |
| 249 | + @click="exportUsersData(domain)" |
| 250 | + :data-test-id="domain.name + '-export-data'" |
| 251 | + > |
| 252 | + <NsMenuItem |
| 253 | + :icon="Export20" |
| 254 | + :label="$t('domain_users.export_data')" |
243 | 255 | /> |
244 | 256 | </cv-overflow-menu-item> |
245 | 257 | <cv-overflow-menu-item |
|
309 | 321 | class="row icon-and-text" |
310 | 322 | > |
311 | 323 | <NsSvg :svg="Warning16" class="icon ns-warning" /> |
312 | | - <cv-link |
313 | | - @click="goToDomainConfiguration(domain, 'providers')" |
314 | | - > |
| 324 | + <cv-link @click="goToDomainConfiguration(domain)"> |
315 | 325 | <span>{{ $t("domains.unconfigured_provider") }}</span> |
316 | 326 | </cv-link> |
317 | 327 | </div> |
318 | 328 | <!-- number of providers --> |
319 | 329 | <div v-else class="row"> |
320 | | - <cv-link |
321 | | - @click="goToDomainConfiguration(domain, 'providers')" |
322 | | - > |
| 330 | + <cv-link @click="goToDomainConfiguration(domain)"> |
323 | 331 | {{ domain.providers.length }} |
324 | 332 | {{ |
325 | 333 | $tc("domains.providers", domain.providers.length) |
|
337 | 345 | <div class="row actions"> |
338 | 346 | <NsButton |
339 | 347 | kind="ghost" |
340 | | - :icon="Group20" |
| 348 | + :icon="ArrowRight20" |
341 | 349 | @click="goToDomainUsersAndGroups(domain)" |
342 | 350 | :data-test-id="domain.name + '-users-and-groups'" |
343 | | - >{{ $t("domains.users_and_groups") }}</NsButton |
| 351 | + >{{ $t("common.see_details") }}</NsButton |
344 | 352 | > |
345 | 353 | </div> |
346 | 354 | </div> |
|
352 | 360 | </template> |
353 | 361 | </template> |
354 | 362 | </cv-grid> |
| 363 | + <ImportUsersModal |
| 364 | + :isShown="isShownImportUsersModal" |
| 365 | + :isResumeConfiguration="importData.isResumeConfiguration" |
| 366 | + :domain="importData.domain" |
| 367 | + @hide="hideImportUsersModal" |
| 368 | + @reloadDomains="listUserDomains" |
| 369 | + /> |
355 | 370 | <!-- create domain modal --> |
356 | 371 | <CreateDomainModal |
357 | 372 | :isShown="isShownCreateDomainModal" |
@@ -396,12 +411,16 @@ import { |
396 | 411 | PageTitleService, |
397 | 412 | } from "@nethserver/ns8-ui-lib"; |
398 | 413 | import CreateDomainModal from "@/components/domains/CreateDomainModal"; |
| 414 | +import ImportUsersModal from "@/components/domains/ImportUsersModal"; |
| 415 | +import Upload20 from "@carbon/icons-vue/es/upload/20"; |
| 416 | +import Export20 from "@carbon/icons-vue/es/export/20"; |
399 | 417 | import to from "await-to-js"; |
400 | 418 | import { mapState } from "vuex"; |
| 419 | +import Papa from "papaparse"; |
401 | 420 |
|
402 | 421 | export default { |
403 | 422 | name: "Domains", |
404 | | - components: { CreateDomainModal }, |
| 423 | + components: { CreateDomainModal, ImportUsersModal }, |
405 | 424 | mixins: [ |
406 | 425 | TaskService, |
407 | 426 | UtilService, |
@@ -438,7 +457,18 @@ export default { |
438 | 457 | removeModule: "", |
439 | 458 | removeExternalDomain: "", |
440 | 459 | removeInternalDomain: "", |
| 460 | + downloadCsvFile: "", |
| 461 | + }, |
| 462 | + Upload20, |
| 463 | + Export20, |
| 464 | + importData: { |
| 465 | + isResumeConfiguration: false, |
| 466 | + domain: {}, |
441 | 467 | }, |
| 468 | + downloadCsv: { |
| 469 | + name: "", |
| 470 | + }, |
| 471 | + isShownImportUsersModal: false, |
442 | 472 | }; |
443 | 473 | }, |
444 | 474 | computed: { |
@@ -534,6 +564,172 @@ export default { |
534 | 564 |
|
535 | 565 | this.loading.listUserDomains = false; |
536 | 566 | }, |
| 567 | + async exportUsersData(domain) { |
| 568 | + this.loading.downloadCsvFile = true; |
| 569 | + this.error.downloadCsvFile = ""; |
| 570 | + this.downloadCsv.name = domain.name; |
| 571 | + const taskAction = "export-users"; |
| 572 | + const eventId = this.getUuid(); |
| 573 | + // register to task error |
| 574 | + this.$root.$once( |
| 575 | + `${taskAction}-aborted-${eventId}`, |
| 576 | + this.exportUsersDataAborted |
| 577 | + ); |
| 578 | +
|
| 579 | + // register to task completion |
| 580 | + this.$root.$once( |
| 581 | + `${taskAction}-completed-${eventId}`, |
| 582 | + this.exportUsersDataCompleted |
| 583 | + ); |
| 584 | +
|
| 585 | + // register to task validation |
| 586 | + this.$root.$once( |
| 587 | + `${taskAction}-validation-ok-${eventId}`, |
| 588 | + this.exportUsersDataValidationOk |
| 589 | + ); |
| 590 | + this.$root.$once( |
| 591 | + `${taskAction}-validation-failed-${eventId}`, |
| 592 | + this.exportUsersDataValidationFailed |
| 593 | + ); |
| 594 | +
|
| 595 | + const res = await to( |
| 596 | + this.createModuleTaskForApp(domain.providers[0].id, { |
| 597 | + action: taskAction, |
| 598 | + data: {}, |
| 599 | + extra: { |
| 600 | + title: this.$t("action.export-users"), |
| 601 | + description: this.$t("common.processing"), |
| 602 | + eventId, |
| 603 | + }, |
| 604 | + }) |
| 605 | + ); |
| 606 | + const err = res[0]; |
| 607 | +
|
| 608 | + if (err) { |
| 609 | + console.error(`error creating task ${taskAction}`, err); |
| 610 | + this.error.downloadCsvFile = this.getErrorMessage(err); |
| 611 | + this.loading.downloadCsvFile = false; |
| 612 | + return; |
| 613 | + } |
| 614 | + }, |
| 615 | + exportUsersDataAborted(taskResult, taskContext) { |
| 616 | + console.error(`${taskContext.action} aborted`, taskResult); |
| 617 | + this.loading.downloadCsvFile = false; |
| 618 | + }, |
| 619 | + exportUsersDataCompleted(taskContext, taskResult) { |
| 620 | + const jsonData = taskResult.output.records; |
| 621 | +
|
| 622 | + // Parse JSON if it's a string |
| 623 | + let records = jsonData; |
| 624 | + if (typeof jsonData === "string") { |
| 625 | + try { |
| 626 | + records = JSON.parse(jsonData); |
| 627 | + } catch (err) { |
| 628 | + console.error("Error parsing JSON records:", err); |
| 629 | + this.loading.downloadCsvFile = false; |
| 630 | + return; |
| 631 | + } |
| 632 | + } |
| 633 | +
|
| 634 | + // Ensure records is an array |
| 635 | + if (!Array.isArray(records) || records.length === 0) { |
| 636 | + console.error("Records is not a valid array"); |
| 637 | + this.loading.downloadCsvFile = false; |
| 638 | + return; |
| 639 | + } |
| 640 | +
|
| 641 | + let columnOrder = [ |
| 642 | + "user", |
| 643 | + "display_name", |
| 644 | + "password", |
| 645 | + "mail", |
| 646 | + "groups", |
| 647 | + "locked", |
| 648 | + "must_change_password", |
| 649 | + "no_password_expiration", |
| 650 | + ]; |
| 651 | +
|
| 652 | + // Reorder records according to schema column order |
| 653 | + const orderedRecords = records.map((record) => { |
| 654 | + const orderedRecord = {}; |
| 655 | + columnOrder.forEach((column) => { |
| 656 | + // Convert groups array to pipe-delimited string |
| 657 | + if (column === "groups" && Array.isArray(record[column])) { |
| 658 | + orderedRecord[column] = record[column].join("|"); |
| 659 | + } else { |
| 660 | + orderedRecord[column] = record[column]; |
| 661 | + } |
| 662 | + }); |
| 663 | + return orderedRecord; |
| 664 | + }); |
| 665 | +
|
| 666 | + // Convert JSON array to CSV using PapaParse with ordered columns |
| 667 | + const csv = Papa.unparse(orderedRecords, { |
| 668 | + header: true, |
| 669 | + columns: columnOrder, |
| 670 | + }); |
| 671 | +
|
| 672 | + // Create a blob from the CSV string |
| 673 | + const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); |
| 674 | +
|
| 675 | + // Create a temporary URL for the blob |
| 676 | + const link = document.createElement("a"); |
| 677 | + const url = URL.createObjectURL(blob); |
| 678 | +
|
| 679 | + // Generate timestamp: YYYYMMDDHHMMSS |
| 680 | + const now = new Date(); |
| 681 | + const pad = (n) => String(n).padStart(2, "0"); |
| 682 | + const timestamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad( |
| 683 | + now.getDate() |
| 684 | + )}${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`; |
| 685 | +
|
| 686 | + // Set the download attributes |
| 687 | + link.setAttribute("href", url); |
| 688 | + link.setAttribute( |
| 689 | + "download", |
| 690 | + `${this.downloadCsv.name}_${timestamp}.csv` |
| 691 | + ); |
| 692 | + link.style.visibility = "hidden"; |
| 693 | +
|
| 694 | + // Append to body, click, and remove |
| 695 | + document.body.appendChild(link); |
| 696 | + link.click(); |
| 697 | + document.body.removeChild(link); |
| 698 | +
|
| 699 | + // Clean up the object URL |
| 700 | + URL.revokeObjectURL(url); |
| 701 | +
|
| 702 | + this.loading.downloadCsvFile = false; |
| 703 | + }, |
| 704 | + exportUsersDataValidationOk() { |
| 705 | + this.loading.downloadCsvFile = false; |
| 706 | + }, |
| 707 | + exportUsersDataValidationFailed(validationErrors) { |
| 708 | + this.loading.downloadCsvFile = false; |
| 709 | + let focusAlreadySet = false; |
| 710 | + for (const validationError of validationErrors) { |
| 711 | + const param = validationError.parameter; |
| 712 | + // set i18n error message |
| 713 | + this.error[param] = this.$t("domain_users." + validationError.error, { |
| 714 | + tok: validationError.value, |
| 715 | + }); |
| 716 | +
|
| 717 | + if (!focusAlreadySet) { |
| 718 | + this.focusElement(param); |
| 719 | + focusAlreadySet = true; |
| 720 | + } |
| 721 | + } |
| 722 | + }, |
| 723 | + showImportUsersModal(domain) { |
| 724 | + this.importData.isResumeConfiguration = false; |
| 725 | + this.importData.domain = domain; |
| 726 | + this.$nextTick(() => { |
| 727 | + this.isShownImportUsersModal = true; |
| 728 | + }); |
| 729 | + }, |
| 730 | + hideImportUsersModal() { |
| 731 | + this.isShownImportUsersModal = false; |
| 732 | + }, |
537 | 733 | showCreateDomainModal() { |
538 | 734 | this.createDomain.isResumeConfiguration = false; |
539 | 735 | this.createDomain.providerId = ""; |
@@ -704,11 +900,11 @@ export default { |
704 | 900 | this.domainToDelete = null; |
705 | 901 | this.listUserDomains(); |
706 | 902 | }, |
707 | | - goToDomainConfiguration(domain, anchor) { |
| 903 | + goToDomainConfiguration(domain) { |
708 | 904 | this.$router.push({ |
709 | | - name: "DomainConfiguration", |
| 905 | + name: "DomainUsersAndGroups", |
710 | 906 | params: { domainName: domain.name }, |
711 | | - hash: anchor ? "#" + anchor : "", |
| 907 | + query: { view: "configuration" }, |
712 | 908 | }); |
713 | 909 | }, |
714 | 910 | goToFileServer(fileServerProvider) { |
|
0 commit comments