|
1 | 1 | <template> |
2 | | - <v-btn @click="dialog = !dialog" class="mr-2" data-test="namespaces-export-btn">Export CSV</v-btn> |
| 2 | + <v-btn @click="showDialog = true" class="mr-2" data-test="namespaces-export-btn">Export CSV</v-btn> |
3 | 3 |
|
4 | | - <v-dialog v-model="dialog" max-width="400" transition="dialog-bottom-transition"> |
| 4 | + <v-dialog v-model="showDialog" max-width="400" transition="dialog-bottom-transition"> |
5 | 5 | <v-card> |
6 | | - <v-card-title class="text-h5 pb-2"> Export namespaces data </v-card-title> |
| 6 | + <v-card-title class="text-h5 pb-2">Export namespaces data</v-card-title> |
7 | 7 | <v-divider /> |
8 | | - <v-form @submit.prevent="onSubmit" data-test="form"> |
| 8 | + <v-form @submit.prevent="handleSubmit" data-test="form"> |
9 | 9 | <v-card-text> |
10 | | - <v-container> |
11 | | - <v-radio-group v-model="selected"> |
12 | | - <v-row no-gutters class="first-row"> |
13 | | - <v-col class="pt-8" cols="12"> |
14 | | - <v-radio label="Namespaces with more than:" value="moreThan" mt="8" /> |
15 | | - </v-col> |
16 | | - </v-row> |
17 | | - <v-row no-gutters class="d-flex justify-center align-center mb-4 ml-3"> |
18 | | - <v-col cols="8"> |
19 | | - <v-slider v-model="numberOfDevices" hide-details :min="0" :max="150" /> |
20 | | - </v-col> |
21 | | - <v-col cols="4"> |
22 | | - <span class="ml-4">{{ numberOfDevicesRound }} devices</span> |
23 | | - </v-col> |
24 | | - </v-row> |
25 | | - <v-row class="mb-4"> |
26 | | - <v-col cols="12"> |
27 | | - <v-radio label="Namespaces with no devices" value="noDevices" /> |
28 | | - </v-col> |
29 | | - </v-row> |
30 | | - <v-row class="mb-4"> |
31 | | - <v-col cols="12"> |
32 | | - <v-radio value="noSession"> |
33 | | - <template v-slot:label> |
34 | | - Namespace with devices but without <br /> |
35 | | - sessions |
36 | | - </template> |
37 | | - </v-radio> |
38 | | - </v-col> |
39 | | - </v-row> |
40 | | - </v-radio-group> |
41 | | - </v-container> |
| 10 | + <v-radio-group v-model="selectedFilter"> |
| 11 | + <v-radio label="Namespaces with more than:" :value="NamespaceFilterOptions.MoreThan" /> |
| 12 | + <v-text-field |
| 13 | + class="mt-2 mx-2" |
| 14 | + v-model="numberOfDevices" |
| 15 | + suffix="devices" |
| 16 | + :disabled="selectedFilter !== NamespaceFilterOptions.MoreThan" |
| 17 | + label="Number of devices" |
| 18 | + color="primary" |
| 19 | + density="comfortable" |
| 20 | + variant="outlined" |
| 21 | + :error-messages="numberOfDevicesError" |
| 22 | + /> |
| 23 | + <v-radio label="Namespaces with no devices" :value="NamespaceFilterOptions.NoDevices" /> |
| 24 | + <v-radio label="Namespace with devices, but no sessions" :value="NamespaceFilterOptions.NoSessions" /> |
| 25 | + </v-radio-group> |
42 | 26 | </v-card-text> |
43 | 27 |
|
44 | | - <v-card-actions class="pa-4"> |
45 | | - <v-spacer /> |
46 | | - <v-btn class="mr-2" color="dark" @click="dialog = false" type="reset"> Cancel </v-btn> |
47 | | - <v-btn color="dark" type="submit" class="mr-4"> Save </v-btn> |
| 28 | + <v-card-actions class="pa-4 d-flex justify-end ga-2"> |
| 29 | + <v-btn @click="closeDialog">Cancel</v-btn> |
| 30 | + <v-btn color="primary" type="submit" :loading="isLoading" :disabled="!!numberOfDevicesError || isLoading">Export</v-btn> |
48 | 31 | </v-card-actions> |
49 | 32 | </v-form> |
50 | 33 | </v-card> |
51 | 34 | </v-dialog> |
52 | 35 | </template> |
53 | 36 |
|
54 | 37 | <script setup lang="ts"> |
55 | | -import { computed, ref } from "vue"; |
| 38 | +import { ref, watch } from "vue"; |
| 39 | +import * as yup from "yup"; |
| 40 | +import { useField } from "vee-validate"; |
56 | 41 | import { saveAs } from "file-saver"; |
57 | 42 | import useNamespacesStore from "@admin/store/modules/namespaces"; |
| 43 | +import getFilter from "@admin/hooks/namespaceExport"; |
| 44 | +import { NamespaceFilterOptions } from "@admin/interfaces/IFilter"; |
58 | 45 | import useSnackbar from "@/helpers/snackbar"; |
| 46 | +import handleError from "@/utils/handleError"; |
59 | 47 |
|
60 | | -const numberOfDevices = ref(0); |
61 | | -const dialog = ref(false); |
62 | | -const selected = ref("moreThan"); |
| 48 | +const showDialog = ref(false); |
| 49 | +const isLoading = ref(false); |
| 50 | +const selectedFilter = ref(NamespaceFilterOptions.MoreThan); |
63 | 51 | const snackbar = useSnackbar(); |
64 | 52 | const namespacesStore = useNamespacesStore(); |
| 53 | +const { value: numberOfDevices, |
| 54 | + errorMessage: numberOfDevicesError, |
| 55 | + setErrors: setNumberOfDevicesErrors, |
| 56 | +} = useField<number>("numberOfDevices", yup.number().integer().required().min(0), { initialValue: 0 }); |
65 | 57 |
|
66 | | -const numberOfDevicesRound = computed(() => Math.round(numberOfDevices.value)); |
67 | | -
|
68 | | -const generateEncodedFilter = (encodeFilter: string) => { |
69 | | - let filter; |
70 | | - switch (encodeFilter) { |
71 | | - case "moreThan": |
72 | | - filter = [ |
73 | | - { |
74 | | - type: "property", |
75 | | - params: { |
76 | | - name: "devices", |
77 | | - operator: "gt", |
78 | | - value: String(numberOfDevicesRound.value), |
79 | | - }, |
80 | | - }, |
81 | | - ]; |
82 | | - break; |
83 | | - case "noDevices": |
84 | | - filter = [ |
85 | | - { |
86 | | - type: "property", |
87 | | - params: { name: "devices", operator: "eq", value: 0 }, |
88 | | - }, |
89 | | - ]; |
90 | | - break; |
91 | | - case "noSession": |
92 | | - filter = [ |
93 | | - { |
94 | | - type: "property", |
95 | | - params: { name: "devices", operator: "gt", value: "0" }, |
96 | | - }, |
97 | | - { |
98 | | - type: "property", |
99 | | - params: { name: "sessions", operator: "eq", value: 0 }, |
100 | | - }, |
101 | | - { type: "operator", params: { name: "and" } }, |
102 | | - ]; |
103 | | - break; |
104 | | - default: |
105 | | - break; |
| 58 | +watch(selectedFilter, (newValue) => { |
| 59 | + if (newValue !== NamespaceFilterOptions.MoreThan) { |
| 60 | + setNumberOfDevicesErrors(""); |
106 | 61 | } |
107 | | - return btoa(JSON.stringify(filter)); |
| 62 | +}); |
| 63 | +
|
| 64 | +const encodeFilter = () => btoa(JSON.stringify(getFilter(selectedFilter.value, numberOfDevices.value))); |
| 65 | +
|
| 66 | +const getFilename = () => { |
| 67 | + const filterSuffixes = { |
| 68 | + [NamespaceFilterOptions.MoreThan]: `more_than_${numberOfDevices.value}_devices`, |
| 69 | + [NamespaceFilterOptions.NoDevices]: "no_devices", |
| 70 | + [NamespaceFilterOptions.NoSessions]: "with_devices_but_no_sessions", |
| 71 | + }; |
| 72 | +
|
| 73 | + const suffix = filterSuffixes[selectedFilter.value] ?? "export"; |
| 74 | + return `namespaces_${suffix}.csv`; |
| 75 | +}; |
| 76 | +
|
| 77 | +const exportCsv = async () => { |
| 78 | + const encodedFilter = encodeFilter(); |
| 79 | + await namespacesStore.setFilterNamespaces(encodedFilter); |
| 80 | + const response = await namespacesStore.exportNamespacesToCsv(); |
| 81 | + const blob = new Blob([response], { type: "text/csv;charset=utf-8" }); |
| 82 | + saveAs(blob, getFilename()); |
108 | 83 | }; |
109 | 84 |
|
110 | | -const onSubmit = async () => { |
111 | | - const encodedFilter = generateEncodedFilter(selected.value); |
| 85 | +const handleSubmit = async () => { |
| 86 | + isLoading.value = true; |
112 | 87 | try { |
113 | | - await namespacesStore.setFilterNamespaces(encodedFilter); |
114 | | - const response = await namespacesStore.exportNamespacesToCsv(); |
115 | | - const blob = new Blob([response], { type: "content-disposition" }); |
116 | | - saveAs( |
117 | | - blob, |
118 | | - `namespaces_${ |
119 | | - selected.value === "moreThanN" |
120 | | - ? `more_than_${String(numberOfDevices.value)}_devices` |
121 | | - : selected.value |
122 | | - }.csv`, |
123 | | - ); |
| 88 | + await exportCsv(); |
124 | 89 | snackbar.showSuccess("Namespaces exported successfully."); |
125 | | - } catch { |
| 90 | + } catch (error) { |
| 91 | + handleError(error); |
126 | 92 | snackbar.showError("Error exporting namespaces."); |
127 | 93 | } |
| 94 | + isLoading.value = false; |
| 95 | +}; |
| 96 | +
|
| 97 | +const resetForm = () => { |
| 98 | + numberOfDevices.value = 0; |
| 99 | + selectedFilter.value = NamespaceFilterOptions.MoreThan; |
128 | 100 | }; |
129 | | -</script> |
130 | 101 |
|
131 | | -<style scoped> |
132 | | -.first-row { |
133 | | - height: 70px; |
134 | | -} |
135 | | -</style> |
| 102 | +const closeDialog = () => { |
| 103 | + showDialog.value = false; |
| 104 | + resetForm(); |
| 105 | +}; |
| 106 | +</script> |
0 commit comments