1616 <cv-link to =" /domains" >{{ $t("domains.title") }}</cv-link >
1717 </cv-breadcrumb-item >
1818 <cv-breadcrumb-item >
19- <span >{{
20- $t("domain_users.domain_name_users_and_groups", {
21- domain: domainName,
22- })
23- }}</span >
19+ <span > {{ domainName }}</span >
2420 </cv-breadcrumb-item >
2521 </cv-breadcrumb >
2622 </cv-column >
2723 </cv-row >
2824 <cv-row >
2925 <cv-column :md =" 4" :xlg =" 10" class =" subpage-title" >
3026 <h3 >
31- {{
32- $t("domain_users.domain_name_users_and_groups", {
33- domain: domainName,
34- })
35- }}
27+ {{ domainName }}
3628 </h3 >
3729 </cv-column >
3830 <cv-column :md =" 4" :xlg =" 6" >
39- <div class =" page-toolbar" >
31+ <div class =" page-toolbar flex gap-4 " >
4032 <NsButton
41- kind =" tertiary"
33+ v-if =" domain && domain.location == 'internal'"
34+ kind =" secondary"
4235 size =" field"
43- :icon =" Settings20 "
44- @click =" goToDomainConfiguration ()"
36+ :icon =" Export20 "
37+ @click =" exportUsersData ()"
4538 class =" subpage-toolbar-item"
46- >{{ $t("domain_configuration.configuration") }}</NsButton
47- >
39+ >{{ $t("domain_users.export_data") }}
40+ </NsButton >
41+ <NsButton
42+ v-if =" domain && domain.location == 'internal'"
43+ kind =" secondary"
44+ size =" field"
45+ :icon =" Upload20"
46+ @click =" showImportUsersModal()"
47+ class =" subpage-toolbar-item"
48+ >{{ $t("domain_users.import_data") }}
49+ </NsButton >
4850 </div >
4951 </cv-column >
5052 </cv-row >
7072 </cv-row >
7173 <cv-row >
7274 <cv-column >
73- <cv-tile light >
75+ <cv-tile >
7476 <NsTabs
7577 :container =" false"
7678 :aria-label =" $t('common.tab_navigation')"
99101 @groupsLoaded =" onGroupsLoaded"
100102 />
101103 </cv-tab >
104+ <cv-tab
105+ id =" tab-3"
106+ :label =" $t('domain_configuration.configuration')"
107+ :selected =" q.view === 'configuration'"
108+ >
109+ <DomainConfiguration :domainName =" domainName" />
110+ </cv-tab >
102111 </NsTabs >
103112 </cv-tile >
104113 </cv-column >
105114 </cv-row >
106115 </cv-grid >
116+ <ImportUsersModal
117+ :isShown =" isShownImportUsersModal"
118+ :isResumeConfiguration =" importData.isResumeConfiguration"
119+ :domain =" importData.domain"
120+ @hide =" hideImportUsersModal"
121+ @reloadDomains =" listUserDomains"
122+ />
107123 </div >
108124</template >
109125
@@ -117,13 +133,20 @@ import {
117133} from " @nethserver/ns8-ui-lib" ;
118134import DomainUsers from " @/components/domains/DomainUsers" ;
119135import DomainGroups from " @/components/domains/DomainGroups" ;
136+ import DomainConfiguration from " @/views/DomainConfiguration.vue" ;
137+ import Upload20 from " @carbon/icons-vue/es/upload/20" ;
138+ import Export20 from " @carbon/icons-vue/es/export/20" ;
139+ import ImportUsersModal from " @/components/domains/ImportUsersModal" ;
120140import to from " await-to-js" ;
141+ import Papa from " papaparse" ;
121142
122143export default {
123144 name: " DomainUsersAndGroups" ,
124145 components: {
125146 DomainUsers,
126147 DomainGroups,
148+ DomainConfiguration,
149+ ImportUsersModal,
127150 },
128151 mixins: [
129152 TaskService,
@@ -149,6 +172,17 @@ export default {
149172 },
150173 error: {
151174 listUserDomains: " " ,
175+ downloadCsvFile: " " ,
176+ },
177+ Upload20,
178+ Export20,
179+ importData: {
180+ isResumeConfiguration: false ,
181+ domain: {},
182+ },
183+ isShownImportUsersModal: false ,
184+ downloadCsv: {
185+ name: " " ,
152186 },
153187 };
154188 },
@@ -172,12 +206,173 @@ export default {
172206 }
173207 },
174208 methods: {
175- goToDomainConfiguration () {
176- this .$router .push ({
177- name: " DomainConfiguration" ,
178- params: { domainName: this .domainName },
209+ async exportUsersData () {
210+ this .loading .downloadCsvFile = true ;
211+ this .error .downloadCsvFile = " " ;
212+ this .downloadCsv .name = this .domain .name ;
213+ const taskAction = " export-users" ;
214+ const eventId = this .getUuid ();
215+ // register to task error
216+ this .$root .$once (
217+ ` ${ taskAction} -aborted-${ eventId} ` ,
218+ this .exportUsersDataAborted
219+ );
220+
221+ // register to task completion
222+ this .$root .$once (
223+ ` ${ taskAction} -completed-${ eventId} ` ,
224+ this .exportUsersDataCompleted
225+ );
226+
227+ // register to task validation
228+ this .$root .$once (
229+ ` ${ taskAction} -validation-ok-${ eventId} ` ,
230+ this .exportUsersDataValidationOk
231+ );
232+ this .$root .$once (
233+ ` ${ taskAction} -validation-failed-${ eventId} ` ,
234+ this .exportUsersDataValidationFailed
235+ );
236+
237+ const res = await to (
238+ this .createModuleTaskForApp (this .domain .providers [0 ].id , {
239+ action: taskAction,
240+ data: {},
241+ extra: {
242+ title: this .$t (" action.export-users" ),
243+ description: this .$t (" common.processing" ),
244+ eventId,
245+ },
246+ })
247+ );
248+ const err = res[0 ];
249+
250+ if (err) {
251+ console .error (` error creating task ${ taskAction} ` , err);
252+ this .error .downloadCsvFile = this .getErrorMessage (err);
253+ this .loading .downloadCsvFile = false ;
254+ return ;
255+ }
256+ },
257+ exportUsersDataAborted (taskResult , taskContext ) {
258+ console .error (` ${ taskContext .action } aborted` , taskResult);
259+ this .loading .downloadCsvFile = false ;
260+ },
261+ exportUsersDataCompleted (taskContext , taskResult ) {
262+ const jsonData = taskResult .output .records ;
263+
264+ // Parse JSON if it's a string
265+ let records = jsonData;
266+ if (typeof jsonData === " string" ) {
267+ try {
268+ records = JSON .parse (jsonData);
269+ } catch (err) {
270+ console .error (" Error parsing JSON records:" , err);
271+ this .loading .downloadCsvFile = false ;
272+ return ;
273+ }
274+ }
275+
276+ // Ensure records is an array
277+ if (! Array .isArray (records) || records .length === 0 ) {
278+ console .error (" Records is not a valid array" );
279+ this .loading .downloadCsvFile = false ;
280+ return ;
281+ }
282+
283+ // Define column order based on domain type
284+ let columnOrder = [
285+ " user" ,
286+ " display_name" ,
287+ " password" ,
288+ " mail" ,
289+ " groups" ,
290+ " locked" ,
291+ " must_change_password" ,
292+ " no_password_expiration" ,
293+ ];
294+
295+ // Reorder records according to schema column order
296+ const orderedRecords = records .map ((record ) => {
297+ const orderedRecord = {};
298+ columnOrder .forEach ((column ) => {
299+ // Convert groups array to pipe-delimited string
300+ if (column === " groups" && Array .isArray (record[column])) {
301+ orderedRecord[column] = record[column].join (" |" );
302+ } else {
303+ orderedRecord[column] = record[column];
304+ }
305+ });
306+ return orderedRecord;
307+ });
308+
309+ // Convert JSON array to CSV using PapaParse with ordered columns
310+ const csv = Papa .unparse (orderedRecords, {
311+ header: true ,
312+ columns: columnOrder,
313+ });
314+
315+ // Create a blob from the CSV string
316+ const blob = new Blob ([csv], { type: " text/csv;charset=utf-8;" });
317+
318+ // Create a temporary URL for the blob
319+ const link = document .createElement (" a" );
320+ const url = URL .createObjectURL (blob);
321+
322+ // Generate timestamp: YYYYMMDDHHMMSS
323+ const now = new Date ();
324+ const pad = (n ) => String (n).padStart (2 , " 0" );
325+ const timestamp = ` ${ now .getFullYear ()}${ pad (now .getMonth () + 1 )}${ pad (
326+ now .getDate ()
327+ )}${ pad (now .getHours ())}${ pad (now .getMinutes ())}${ pad (now .getSeconds ())} ` ;
328+
329+ // Set the download attributes
330+ link .setAttribute (" href" , url);
331+ link .setAttribute (
332+ " download" ,
333+ ` ${ this .downloadCsv .name } _${ timestamp} .csv`
334+ );
335+ link .style .visibility = " hidden" ;
336+
337+ // Append to body, click, and remove
338+ document .body .appendChild (link);
339+ link .click ();
340+ document .body .removeChild (link);
341+
342+ // Clean up the object URL
343+ URL .revokeObjectURL (url);
344+
345+ this .loading .downloadCsvFile = false ;
346+ },
347+ exportUsersDataValidationOk () {
348+ this .loading .downloadCsvFile = false ;
349+ },
350+ exportUsersDataValidationFailed (validationErrors ) {
351+ this .loading .downloadCsvFile = false ;
352+ let focusAlreadySet = false ;
353+ for (const validationError of validationErrors) {
354+ const param = validationError .parameter ;
355+ // set i18n error message
356+ this .error [param] = this .$t (" domain_users." + validationError .error , {
357+ tok: validationError .value ,
358+ });
359+
360+ if (! focusAlreadySet) {
361+ this .focusElement (param);
362+ focusAlreadySet = true ;
363+ }
364+ }
365+ },
366+ showImportUsersModal () {
367+ this .importData .isResumeConfiguration = false ;
368+ this .importData .domain = this .domain ;
369+ this .$nextTick (() => {
370+ this .isShownImportUsersModal = true ;
179371 });
180372 },
373+ hideImportUsersModal () {
374+ this .isShownImportUsersModal = false ;
375+ },
181376 async listUserDomains () {
182377 this .loading .listUserDomains = true ;
183378 this .error .listUserDomains = " " ;
@@ -240,6 +435,8 @@ export default {
240435 this .q .view = " users" ;
241436 } else if (tabNum == 1 ) {
242437 this .q .view = " groups" ;
438+ } else if (tabNum == 2 ) {
439+ this .q .view = " configuration" ;
243440 }
244441 },
245442 },
0 commit comments