Skip to content

Commit 6cc6ea9

Browse files
committed
feat: enhance DomainUsersAndGroups component with import/export functionality and UI improvements
1 parent 6d0c709 commit 6cc6ea9

File tree

1 file changed

+218
-21
lines changed

1 file changed

+218
-21
lines changed

core/ui/src/views/DomainUsersAndGroups.vue

Lines changed: 218 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,35 +16,37 @@
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>
@@ -70,7 +72,7 @@
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')"
@@ -99,11 +101,25 @@
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";
118134
import DomainUsers from "@/components/domains/DomainUsers";
119135
import 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";
120140
import to from "await-to-js";
141+
import Papa from "papaparse";
121142
122143
export 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

Comments
 (0)