Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions ui/admin/src/components/Namespace/NamespaceDelete.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<template>
<MessageDialog
v-model="showDialog"
title="Namespace Deletion"
icon="mdi-delete-alert"
icon-color="error"
confirm-color="error"
confirm-text="Remove"
:confirm-loading="isLoading"
cancel-text="Close"
confirm-data-test="remove-btn"
cancel-data-test="close-btn"
@close="showDialog = false"
@confirm="remove"
@cancel="showDialog = false"
>
<p data-test="content-text">
This action cannot be undone. This will permanently delete
<strong>{{ displayOnlyTenCharacters(name) }}</strong> and its related
data.
</p>
</MessageDialog>
</template>

<script setup lang="ts">
import { computed, ref } from "vue";
import { useRouter, useRoute } from "vue-router";
import { displayOnlyTenCharacters } from "@/utils/string";
import handleError from "@/utils/handleError";
import useSnackbar from "@/helpers/snackbar";
import MessageDialog from "@/components/Dialogs/MessageDialog.vue";
import useNamespacesStore from "@admin/store/modules/namespaces";

const props = defineProps<{ tenant: string; name: string }>();
const emit = defineEmits(["update"]);

const namespacesStore = useNamespacesStore();
const snackbar = useSnackbar();
const router = useRouter();
const route = useRoute();

const showDialog = defineModel<boolean>({ required: true });
const isLoading = ref(false);

const name = computed(() => props.name);
const tenant = computed(() => props.tenant);

const remove = async () => {
isLoading.value = true;
try {
await namespacesStore.deleteNamespace(tenant.value);
snackbar.showSuccess("Namespace deleted successfully.");

if (route.name === "namespaceDetails") {
await router.push({ name: "namespaces" });
} else {
emit("update");
}
showDialog.value = false;
} catch (error: unknown) {
snackbar.showError("An error occurred while deleting the namespace.");
handleError(error);
} finally {
isLoading.value = false;
}
};

defineExpose({ isLoading });
</script>
47 changes: 18 additions & 29 deletions ui/admin/src/components/Namespace/NamespaceEdit.vue
Original file line number Diff line number Diff line change
@@ -1,23 +1,4 @@
<template>
<v-tooltip
bottom
anchor="bottom"
>
<template #activator="{ props }">
<v-icon
tag="button"
dark
v-bind="props"
tabindex="0"
aria-label="Edit Namespace"
data-test="dialog-btn"
icon="mdi-pencil"
@click="showDialog = true"
/>
</template>
<span>Edit</span>
</v-tooltip>

<FormDialog
v-model="showDialog"
title="Edit Namespace"
Expand Down Expand Up @@ -62,47 +43,50 @@

<script setup lang="ts">
import { useField } from "vee-validate";
import { computed, ref } from "vue";
import { computed, defineModel } from "vue";
import * as yup from "yup";
import useNamespacesStore from "@admin/store/modules/namespaces";
import useSnackbar from "@/helpers/snackbar";
import { IAdminNamespace } from "../../interfaces/INamespace";
import { IAdminNamespace } from "@admin/interfaces/INamespace";
import FormDialog from "@/components/Dialogs/FormDialog.vue";

const props = defineProps<{ namespace: IAdminNamespace }>();
const emit = defineEmits(["update"]);

const snackbar = useSnackbar();
const namespacesStore = useNamespacesStore();
const showDialog = ref(false);
const showDialog = defineModel<boolean>({ default: false });

const {
value: name,
errorMessage: nameError,
resetField: resetName,
} = useField<string | undefined>("name", yup.string().required(), {
initialValue: props.namespace.name,
} = useField<string>("name", yup.string().required(), {
initialValue: props.namespace.name || "",
});

const {
value: maxDevices,
errorMessage: maxDevicesError,
resetField: resetMaxDevices,
} = useField<number | undefined>(
} = useField<number>(
"maxDevices",
yup
.number()
.integer()
.required()
.min(-1, "Maximum devices must be -1 (unlimited) or greater"),
{ initialValue: props.namespace.max_devices },
{
initialValue: props.namespace.max_devices ?? -1,
},
);

const {
value: sessionRecord,
errorMessage: sessionRecordError,
resetField: resetSessionRecord,
} = useField<boolean>("sessionRecord", yup.boolean(), {
initialValue: props.namespace.settings.session_record || false,
initialValue: props.namespace.settings?.session_record ?? true,
});

const hasErrors = computed(
Expand All @@ -118,16 +102,21 @@ const closeDialog = () => {

const submitForm = async () => {
if (hasErrors.value) return;

try {
await namespacesStore.updateNamespace({
...props.namespace,
name: name.value as string,
name: name.value,
max_devices: Number(maxDevices.value),
settings: { session_record: sessionRecord.value },
settings: {
...props.namespace.settings,
session_record: sessionRecord.value,
},
});
await namespacesStore.fetchNamespaceList();
snackbar.showSuccess("Namespace updated successfully.");
showDialog.value = false;
emit("update");
} catch {
snackbar.showError("Failed to update namespace.");
}
Expand Down
128 changes: 110 additions & 18 deletions ui/admin/src/components/Namespace/NamespaceList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@
<DataTable
v-model:items-per-page="itemsPerPage"
v-model:page="page"
:headers
:headers="headers"
:items="namespaces"
:loading
:loading="loading"
:total-count="namespaceCount"
:items-per-page-options="[10, 20, 50, 100]"
data-test="namespaces-list"
>
<template #rows>
<tr
v-for="(namespace, i) in namespaces"
:key="i"
:key="namespace.tenant_id || i"
>
<td>
{{ namespace.name }}
Expand All @@ -25,12 +25,18 @@
{{ namespace.tenant_id }}
</td>
<td>
{{ namespace.owner }}
</td>
<td>
<div v-if="namespace.settings">
{{ namespace.settings.session_record }}
</div>
<span
v-if="getOwnerEmail(namespace)"
tabindex="0"
class="text-decoration-underline cursor-pointer"
@click="goToUser(namespace.owner)"
@keyup.enter="goToUser(namespace.owner)"
>
{{ getOwnerEmail(namespace) }}
</span>
<span v-else>
{{ namespace.owner }}
</span>
</td>
<td>
<v-tooltip
Expand All @@ -51,11 +57,63 @@
<span>Details</span>
</v-tooltip>

<NamespaceEdit :namespace="namespace" />
<v-tooltip
bottom
anchor="bottom"
>
<template #activator="{ props }">
<v-icon
tag="button"
dark
v-bind="props"
tabindex="0"
aria-label="Edit Namespace"
data-test="namespace-edit-dialog-btn"
icon="mdi-pencil"
@click="openEditNamespace(namespace)"
/>
</template>
<span>Edit</span>
</v-tooltip>

<v-tooltip
bottom
anchor="bottom"
>
<template #activator="{ props }">
<v-icon
tag="button"
dark
v-bind="props"
tabindex="0"
aria-label="Delete Namespace"
data-test="namespace-delete-dialog-btn"
icon="mdi-delete"
@click="openDeleteNamespace(namespace)"
/>
</template>
<span>Delete</span>
</v-tooltip>
</td>
</tr>
</template>
</DataTable>

<NamespaceEdit
v-if="selectedNamespace"
:key="selectedNamespace.tenant_id"
v-model="namespaceEdit"
:namespace="selectedNamespace"
@update="fetchNamespaces"
/>

<NamespaceDelete
v-if="selectedNamespace"
v-model="namespaceDelete"
:tenant="selectedNamespace.tenant_id"
:name="selectedNamespace.name"
@update="fetchNamespaces"
/>
</div>
</template>

Expand All @@ -66,17 +124,24 @@ import useNamespacesStore from "@admin/store/modules/namespaces";
import { IAdminNamespace } from "@admin/interfaces/INamespace";
import useSnackbar from "@/helpers/snackbar";
import DataTable from "@/components/Tables/DataTable.vue";
import NamespaceEdit from "./NamespaceEdit.vue";
import NamespaceEdit from "@admin/components/Namespace/NamespaceEdit.vue";
import NamespaceDelete from "@admin/components/Namespace/NamespaceDelete.vue";
import handleError from "@/utils/handleError";

const snackbar = useSnackbar();
const namespacesStore = useNamespacesStore();
const namespaces = computed(() => namespacesStore.namespaces);
const namespaceCount = computed(() => namespacesStore.namespaceCount);
const router = useRouter();

const namespaceEdit = ref(false);
const namespaceDelete = ref(false);
const selectedNamespace = ref<IAdminNamespace | null>(null);

const loading = ref(false);
const page = ref(1);
const itemsPerPage = ref(10);

const headers = ref([
{
text: "Name",
Expand All @@ -94,10 +159,6 @@ const headers = ref([
text: "Owner",
value: "owner",
},
{
text: "Session Record",
value: "settings",
},
{
text: "Actions",
value: "actions",
Expand All @@ -120,12 +181,37 @@ const fetchNamespaces = async () => {
};

const sumDevicesCount = (namespace: IAdminNamespace) => {
const { devices_accepted_count: acceptedCount, devices_pending_count: pendingCount, devices_rejected_count: rejectedCount } = namespace;
const {
devices_accepted_count: acceptedCount,
devices_pending_count: pendingCount,
devices_rejected_count: rejectedCount,
} = namespace;
return (acceptedCount + pendingCount + rejectedCount) || 0;
};

const goToNamespace = async (namespace: string) => {
await router.push({ name: "namespaceDetails", params: { id: namespace } });
const getOwnerEmail = (namespace: IAdminNamespace) => {
const owner = namespace.members?.find(
(member) => member.id === namespace.owner,
);
return owner?.email || null;
};

const goToNamespace = async (tenantId: string) => {
await router.push({ name: "namespaceDetails", params: { id: tenantId } });
};

const goToUser = async (userId: string) => {
await router.push({ name: "userDetails", params: { id: userId } });
};

const openEditNamespace = (ns: IAdminNamespace) => {
selectedNamespace.value = ns;
namespaceEdit.value = true;
};

const openDeleteNamespace = (ns: IAdminNamespace) => {
selectedNamespace.value = ns;
namespaceDelete.value = true;
};

watch([itemsPerPage, page], async () => {
Expand All @@ -136,3 +222,9 @@ onMounted(async () => {
await fetchNamespaces();
});
</script>

<style scoped>
.cursor-pointer {
cursor: pointer;
}
</style>
2 changes: 2 additions & 0 deletions ui/admin/src/store/api/namespaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export const exportNamespaces = async (filter: string) => adminApi.exportNamespa

export const getNamespace = async (id: string) => adminApi.getNamespaceAdmin(id);

export const deleteNamespace = async (tenant: string) => adminApi.deleteNamespaceAdmin(tenant);

export const updateNamespace = async (
data: IAdminNamespace,
) => adminApi.editNamespaceAdmin(data.tenant_id, {
Expand Down
Loading