Skip to content

Commit 6d0c709

Browse files
committed
feat: add import and export functionality for users in Domains component
1 parent e015559 commit 6d0c709

File tree

1 file changed

+211
-15
lines changed

1 file changed

+211
-15
lines changed

core/ui/src/views/Domains.vue

Lines changed: 211 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -235,11 +235,23 @@
235235
:data-test-id="domain.name + '-menu'"
236236
>
237237
<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'"
239241
>
240242
<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')"
243255
/>
244256
</cv-overflow-menu-item>
245257
<cv-overflow-menu-item
@@ -309,17 +321,13 @@
309321
class="row icon-and-text"
310322
>
311323
<NsSvg :svg="Warning16" class="icon ns-warning" />
312-
<cv-link
313-
@click="goToDomainConfiguration(domain, 'providers')"
314-
>
324+
<cv-link @click="goToDomainConfiguration(domain)">
315325
<span>{{ $t("domains.unconfigured_provider") }}</span>
316326
</cv-link>
317327
</div>
318328
<!-- number of providers -->
319329
<div v-else class="row">
320-
<cv-link
321-
@click="goToDomainConfiguration(domain, 'providers')"
322-
>
330+
<cv-link @click="goToDomainConfiguration(domain)">
323331
{{ domain.providers.length }}
324332
{{
325333
$tc("domains.providers", domain.providers.length)
@@ -337,10 +345,10 @@
337345
<div class="row actions">
338346
<NsButton
339347
kind="ghost"
340-
:icon="Group20"
348+
:icon="ArrowRight20"
341349
@click="goToDomainUsersAndGroups(domain)"
342350
:data-test-id="domain.name + '-users-and-groups'"
343-
>{{ $t("domains.users_and_groups") }}</NsButton
351+
>{{ $t("common.see_details") }}</NsButton
344352
>
345353
</div>
346354
</div>
@@ -352,6 +360,13 @@
352360
</template>
353361
</template>
354362
</cv-grid>
363+
<ImportUsersModal
364+
:isShown="isShownImportUsersModal"
365+
:isResumeConfiguration="importData.isResumeConfiguration"
366+
:domain="importData.domain"
367+
@hide="hideImportUsersModal"
368+
@reloadDomains="listUserDomains"
369+
/>
355370
<!-- create domain modal -->
356371
<CreateDomainModal
357372
:isShown="isShownCreateDomainModal"
@@ -396,12 +411,16 @@ import {
396411
PageTitleService,
397412
} from "@nethserver/ns8-ui-lib";
398413
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";
399417
import to from "await-to-js";
400418
import { mapState } from "vuex";
419+
import Papa from "papaparse";
401420
402421
export default {
403422
name: "Domains",
404-
components: { CreateDomainModal },
423+
components: { CreateDomainModal, ImportUsersModal },
405424
mixins: [
406425
TaskService,
407426
UtilService,
@@ -438,7 +457,18 @@ export default {
438457
removeModule: "",
439458
removeExternalDomain: "",
440459
removeInternalDomain: "",
460+
downloadCsvFile: "",
461+
},
462+
Upload20,
463+
Export20,
464+
importData: {
465+
isResumeConfiguration: false,
466+
domain: {},
441467
},
468+
downloadCsv: {
469+
name: "",
470+
},
471+
isShownImportUsersModal: false,
442472
};
443473
},
444474
computed: {
@@ -534,6 +564,172 @@ export default {
534564
535565
this.loading.listUserDomains = false;
536566
},
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+
},
537733
showCreateDomainModal() {
538734
this.createDomain.isResumeConfiguration = false;
539735
this.createDomain.providerId = "";
@@ -704,11 +900,11 @@ export default {
704900
this.domainToDelete = null;
705901
this.listUserDomains();
706902
},
707-
goToDomainConfiguration(domain, anchor) {
903+
goToDomainConfiguration(domain) {
708904
this.$router.push({
709-
name: "DomainConfiguration",
905+
name: "DomainUsersAndGroups",
710906
params: { domainName: domain.name },
711-
hash: anchor ? "#" + anchor : "",
907+
query: { view: "configuration" },
712908
});
713909
},
714910
goToFileServer(fileServerProvider) {

0 commit comments

Comments
 (0)