Skip to content

Commit 0b64ec2

Browse files
luannmoreiragustavosbarreto
authored andcommitted
feat(ui): display contextual empty state for API keys, tags, and private keys
Show user-friendly messages when no API keys, tags, or private keys are available. Introduced `NoItemsMessage` for better UX and added loading indicators for initial data fetches. Adjusted tests and snapshots accordingly to support the changes.
1 parent 0408098 commit 0b64ec2

File tree

11 files changed

+264
-390
lines changed

11 files changed

+264
-390
lines changed

ui/src/components/Setting/SettingPrivateKeys.vue

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@
2424
Manage your private keys securely with ShellHub
2525
</span>
2626
</template>
27-
<template #append>
27+
<template
28+
v-if="hasPrivateKeys"
29+
#append
30+
>
2831
<v-btn
2932
color="primary"
3033
variant="elevated"
@@ -37,21 +40,54 @@
3740
</v-list-item>
3841
</v-card-item>
3942

40-
<PrivateKeyList data-test="private-key-list" />
43+
<PrivateKeyList
44+
v-if="hasPrivateKeys"
45+
data-test="private-key-list"
46+
/>
47+
48+
<NoItemsMessage
49+
v-else
50+
item="Private Keys"
51+
icon="mdi-shield-key"
52+
data-test="no-items-message-component"
53+
>
54+
<template #content>
55+
<p>
56+
ShellHub provides secure storage for your SSH private keys.
57+
This allows you to authenticate with your devices securely and automatically.
58+
</p>
59+
<p>
60+
By adding your Private Keys here, you can streamline access to your
61+
devices without managing passwords manually for every connection.
62+
</p>
63+
</template>
64+
<template #action>
65+
<v-btn
66+
color="primary"
67+
variant="elevated"
68+
@click="privateKeyAdd = true"
69+
>
70+
Add Private Key
71+
</v-btn>
72+
</template>
73+
</NoItemsMessage>
4174
</v-card>
4275
</v-container>
4376
</template>
4477

4578
<script setup lang="ts">
46-
import { ref } from "vue";
79+
import { ref, computed } from "vue";
4780
import PrivateKeyAdd from "../PrivateKeys/PrivateKeyAdd.vue";
4881
import PrivateKeyList from "../PrivateKeys/PrivateKeyList.vue";
82+
import NoItemsMessage from "@/components/NoItemsMessage.vue";
4983
import handleError from "@/utils/handleError";
5084
import usePrivateKeysStore from "@/store/modules/private_keys";
5185
5286
const privateKeysStore = usePrivateKeysStore();
5387
const privateKeyAdd = ref(false);
5488
89+
const hasPrivateKeys = computed(() => privateKeysStore.privateKeys.length > 0);
90+
5591
const getPrivateKeys = () => {
5692
try {
5793
privateKeysStore.getPrivateKeyList();

ui/src/components/Setting/SettingTags.vue

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,18 @@
88
class="mx-0 px-0"
99
max-width="60rem"
1010
>
11+
<div
12+
v-if="loading"
13+
class="d-flex justify-center mt-4"
14+
>
15+
<v-progress-circular
16+
indeterminate
17+
color="primary"
18+
/>
19+
</div>
20+
1121
<v-card
22+
v-else
1223
variant="flat"
1324
class="bg-transparent"
1425
data-test="tags-settings-card"
@@ -31,8 +42,10 @@
3142
</template>
3243
</v-list-item>
3344
</v-col>
45+
3446
<v-col cols="6">
3547
<v-text-field
48+
v-if="hasTags"
3649
v-model.trim="filter"
3750
label="Search by Tag Name"
3851
variant="outlined"
@@ -45,11 +58,13 @@
4558
@keyup="searchTags"
4659
/>
4760
</v-col>
61+
4862
<v-col
4963
cols="3"
5064
class="d-flex justify-end"
5165
>
5266
<v-btn
67+
v-if="hasTags"
5368
color="primary"
5469
variant="elevated"
5570
data-test="tag-create-button"
@@ -60,28 +75,61 @@
6075
</v-col>
6176
</v-row>
6277
</v-card-item>
78+
6379
<TagList
80+
v-if="hasTags"
6481
ref="tagListRef"
6582
class="mx-4"
6683
/>
84+
85+
<NoItemsMessage
86+
v-else
87+
item="Tags"
88+
icon="mdi-tag-multiple"
89+
data-test="no-items-message-component"
90+
>
91+
<template #content>
92+
<p>
93+
ShellHub allows you to organize your resources using Tags.
94+
</p>
95+
<p>
96+
You can assign tags to Devices, Public Keys, and Firewall Rules
97+
to filter and group them effectively.
98+
</p>
99+
</template>
100+
<template #action>
101+
<v-btn
102+
color="primary"
103+
variant="elevated"
104+
@click="openCreate"
105+
>
106+
Create Tag
107+
</v-btn>
108+
</template>
109+
</NoItemsMessage>
67110
</v-card>
68111
</v-container>
69112
</template>
70113

71114
<script setup lang="ts">
72-
import { computed, ref } from "vue";
115+
import { computed, ref, onMounted } from "vue";
73116
import TagList from "../Tags/TagList.vue";
74117
import TagCreate from "../Tags/TagCreate.vue";
118+
import NoItemsMessage from "@/components/NoItemsMessage.vue";
75119
import useTagsStore from "@/store/modules/tags";
76120
import useSnackbar from "@/helpers/snackbar";
121+
import handleError from "@/utils/handleError";
77122
78123
const tagsStore = useTagsStore();
79124
const snackbar = useSnackbar();
80125
const tagListRef = ref<InstanceType<typeof TagList> | null>(null);
81126
const createDialog = ref(false);
82127
const filter = ref("");
128+
const loading = ref(true);
83129
const tenant = computed(() => localStorage.getItem("tenant") || "");
84130
131+
const hasTags = computed(() => tagsStore.getNumberTags > 0);
132+
85133
const searchTags = async () => {
86134
let encodedFilter = "";
87135
@@ -109,7 +157,32 @@ const openCreate = () => {
109157
createDialog.value = true;
110158
};
111159
112-
const refreshTagList = () => {
113-
tagListRef.value?.refresh();
160+
const refreshTagList = async () => {
161+
if (!hasTags.value) {
162+
await fetchInitialTags();
163+
} else {
164+
tagListRef.value?.refresh();
165+
}
166+
};
167+
168+
const fetchInitialTags = async () => {
169+
try {
170+
loading.value = true;
171+
await tagsStore.fetch({
172+
tenant: tenant.value,
173+
page: 1,
174+
perPage: 10,
175+
filter: "",
176+
});
177+
} catch (error) {
178+
snackbar.showError("Failed to load tags.");
179+
handleError(error);
180+
} finally {
181+
loading.value = false;
182+
}
114183
};
184+
185+
onMounted(async () => {
186+
await fetchInitialTags();
187+
});
115188
</script>

ui/src/components/Team/ApiKeys/ApiKeyGenerate.vue

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
<v-btn
1111
:disabled="!canGenerateApiKey"
1212
color="primary"
13+
variant="elevated"
1314
data-test="api-key-generate-main-btn"
1415
@click="showDialog = true"
1516
>
@@ -52,6 +53,7 @@
5253
<ApiKeySuccess
5354
v-model="showSuccessDialog"
5455
:api-key="generatedApiKey"
56+
@update:model-value="handleSuccessDialogUpdate"
5557
/>
5658
</div>
5759
</template>
@@ -72,6 +74,7 @@ import { IApiKeyCreate } from "@/interfaces/IApiKey";
7274
const emit = defineEmits(["update"]);
7375
const snackbar = useSnackbar();
7476
const apiKeyStore = useApiKeysStore();
77+
7578
const showDialog = ref(false);
7679
const showSuccessDialog = ref(false);
7780
const errorMessage = ref("");
@@ -80,6 +83,8 @@ const isFormValid = ref(false);
8083
const formRef = ref<InstanceType<typeof ApiKeyForm>>();
8184
const canGenerateApiKey = hasPermission("apiKey:create");
8285
86+
const wasSuccessful = ref(false);
87+
8388
const handleGenerateKeyError = (error: unknown) => {
8489
snackbar.showError("Failed to generate API Key.");
8590
@@ -111,21 +116,32 @@ const handleSubmit = () => {
111116
const generateKey = async (formData: { name: string; expires_in?: number; role: BasicRole }) => {
112117
try {
113118
generatedApiKey.value = await apiKeyStore.generateApiKey(formData as IApiKeyCreate);
114-
emit("update");
115-
119+
wasSuccessful.value = true;
116120
showDialog.value = false;
117121
showSuccessDialog.value = true;
118122
} catch (error: unknown) {
119123
handleGenerateKeyError(error);
120124
}
121125
};
122126
127+
const handleSuccessDialogUpdate = (value: boolean) => {
128+
showSuccessDialog.value = value;
129+
130+
if (!value && wasSuccessful.value) {
131+
emit("update");
132+
wasSuccessful.value = false;
133+
generatedApiKey.value = "";
134+
}
135+
};
136+
123137
const close = () => {
124138
showDialog.value = false;
125139
showSuccessDialog.value = false;
126-
generatedApiKey.value = "";
127140
errorMessage.value = "";
141+
wasSuccessful.value = false;
142+
generatedApiKey.value = "";
128143
formRef.value?.reset();
129144
};
145+
130146
defineExpose({ generateKey, showDialog, errorMessage, close });
131147
</script>

ui/src/components/Team/ApiKeys/ApiKeyList.vue

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@
110110
</template>
111111

112112
<script setup lang="ts">
113-
import { computed, onMounted, ref, watch } from "vue";
113+
import { computed, ref, watch } from "vue";
114114
import axios, { AxiosError } from "axios";
115115
import moment from "moment";
116116
import DataTable from "@/components/Tables/DataTable.vue";
@@ -218,9 +218,5 @@ const sortByItem = async (field: string) => {
218218
await fetchApiKeys();
219219
};
220220
221-
onMounted(async () => {
222-
await fetchApiKeys();
223-
});
224-
225221
defineExpose({ refresh, hasKeyExpired, formatDate, itemsPerPage });
226222
</script>

0 commit comments

Comments
 (0)