Select Git revision
QuotaTable.vue
Benedikt Heinrichs authored
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
QuotaTable.vue 12.52 KiB
<template>
<div id="quotatable">
<CoscineHeadline :headline="$t('page.admin.projectQuotaHeadline')" />
<!-- Filter Hidden Resources -->
<b-form-checkbox
v-if="project"
v-model="showOnlyEnabledResources"
class="mt-3 mb-1"
switch
>
{{ $t("page.admin.displayHiddenResources") }}
</b-form-checkbox>
<!-- QuotaTable -->
<b-table
id="project_quota_table"
:fields="headers"
:items="filteredProjectQuotas"
:busy="!project || isWaitingForResponse"
:locale="$i18n.locale"
:sort-by.sync="sortBy"
:sort-desc="true"
:show-empty="true"
:empty-text="$t('page.admin.projectNotSelected')"
sticky-header="100%"
no-border-collapse
sort-icon-right
striped
bordered
outlined
hover
head-variant="dark"
>
<template #thead-top>
<b-tr>
<b-th colspan="1" />
<b-th colspan="3">{{ $t("page.admin.headers.projectQuota") }}</b-th>
<b-th colspan="2">{{ $t("page.admin.headers.resourceQuota") }}</b-th>
<b-th colspan="2" />
</b-tr>
</template>
<!-- Quota Table - Resource Type Column -->
<template #cell(iDisplayName)="row">
<b :class="row.item.isEnabled ? '' : 'text-secondary'">
{{
row.item.iDisplayName
? row.item.iDisplayName
: row.item.resourceType
}}
</b>
</template>
<!-- Quota Table - Maximum Quota Column -->
<template #cell(maximum)="row">
<strong>
{{
!row.item.isQuotaAvailable
? $t("default.none")
: $t("default.gb", { number: toGiB(row.item.maximum) })
}}
</strong>
</template>
<!-- Quota Table - Allocated Quota Column -->
<template #cell(allocated)="row">
<span class="text-secondary">
{{
!row.item.isQuotaAvailable
? $t("default.none").toString()
: $t("default.gb", {
number: toGiB(row.item.allocated).toString(),
})
}}
</span>
</template>
<!-- Quota Table - Free Quota Column -->
<template #cell(free)="row">
{{
!row.item.isQuotaAvailable
? $t("default.none")
: $t("default.gb", {
number: row.item.free.value,
})
}}
</template>
<!-- Quota Table - Total Used Resource Column -->
<template #cell(totalUsed)="row">
{{
!row.item.isQuotaAvailable
? $t("default.none")
: formatUsed(row.item.totalUsed)
}}
</template>
<!-- Quota Table - Total Reserved Resource Column -->
<template #cell(totalReserved)="row">
{{
!row.item.isQuotaAvailable
? $t("default.none")
: $t("default.gb", { number: toGiB(row.item.totalReserved) })
}}
</template>
<!-- Quota Table - New Quota Column -->
<template #cell(newQuota)="row">
<b-form-input
v-if="row.item.isQuotaAvailable === true"
id="quotaInputField"
v-model.number="newQuotas[row.item.resourceType.id]"
type="number"
aria-describedby="projectHelp"
:placeholder="$t('page.admin.newQuotaInputPlaceHolder')"
@keypress.enter.stop="saveNewQuota(project, row.item)"
>
</b-form-input>
</template>
<!-- Quota Table - Action Column -->
<template #cell(action)="row">
<div class="text-center">
<b-button
v-if="row.item.isQuotaAvailable === true"
:id="`action${row.index}`"
variant="primary"
:disabled="!isQuotaValueValid(row.item)"
@click.stop.prevent="saveNewQuota(project, row.item)"
>
{{ $t("buttons.save") }}
</b-button>
</div>
</template>
<!-- Quota Table - Header Row -->
<template #head()="header">
<span
:id="header.label"
:class="header.field.hint ? 'hasInfoPopover' : ''"
>
{{ header.label }}
</span>
<!-- Hint Tooltip -->
<b-tooltip
v-if="header.field.hint"
:target="header.label"
triggers="hover"
boundary="viewport"
placement="bottom"
variant="light"
>
{{ header.field.hint }}
</b-tooltip>
</template>
</b-table>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
// import the store for current module
import useAdminStore from "../store";
// import the main store
import useResourceStore from "@/modules/resource/store";
import type { ExtendedProjectQuotaDto } from "../types";
import { isNumber } from "lodash";
import useNotificationStore from "@/store/notification";
import { FileUtil } from "@/modules/resource/utils/FileUtil";
import type {
ProjectDto,
QuotaDto,
ResourceTypeInformationDto,
} from "@coscine/api-client/dist/types/Coscine.Api/api";
import { QuotaUnit } from "@/modules/resource/types";
export default defineComponent({
setup() {
const adminStore = useAdminStore();
const notificationStore = useNotificationStore();
const resourceStore = useResourceStore();
return { adminStore, notificationStore, resourceStore };
},
data() {
return {
newQuotas: {} as { [id: string]: number },
sortBy: "iDisplayName",
isWaitingForResponse: false,
showOnlyEnabledResources: true,
showQuotaInputHint: true,
};
},
computed: {
project(): ProjectDto | null | undefined {
return this.adminStore.project;
},
projectQuotas(): ExtendedProjectQuotaDto[] | null {
if (!this.project) {
return null; // In case project can't be resolved return null
}
// Retrieve all project quotas (incl. for hidden resources)
const projectQuotas = this.adminStore.projectQuotas;
// Get all resource types
const resourceTypes: ResourceTypeInformationDto[] | null | undefined =
this.resourceStore.resourceTypes;
if (!projectQuotas || !resourceTypes) {
return null; // In case resource types can't be resolved return null
}
return projectQuotas.map((entry) => {
// Find corresponding entry
const resourceType = this.resourceTypes?.find(
(q) => q.specificType === entry.resourceType?.specificType,
);
// Populate the extended project quota object
const extended = {
...entry,
iDisplayName: this.$t(
`resourceTypes.${entry.resourceType?.specificType}.displayName`,
).toString(),
isEnabled: resourceType?.isEnabled,
isQuotaAvailable: resourceType?.isQuotaAvailable,
free:
entry.maximum && entry.allocated
? {
value:
this.toGiB(entry.maximum) - this.toGiB(entry.allocated),
unit: QuotaUnit.GibiByte,
}
: undefined,
} as ExtendedProjectQuotaDto;
// For every resource type that has no quota available, set the value to be undefined
if (!extended.isQuotaAvailable) {
extended.allocated = undefined;
extended.free = undefined;
extended.maximum = undefined;
extended.totalReserved = undefined;
extended.totalUsed = undefined;
}
// Push to the newly filtered array
return extended;
});
},
filteredProjectQuotas(): ExtendedProjectQuotaDto[] | null {
if (this.showOnlyEnabledResources && this.projectQuotas) {
return this.projectQuotas.filter((entry) => entry.isEnabled);
} else {
return this.projectQuotas;
}
},
resourceTypes(): ResourceTypeInformationDto[] | null | undefined {
return this.resourceStore.resourceTypes;
},
headers() {
// Define as computed property to have table
// header text react on language changes.
return [
{
label: this.$t("page.admin.headers.resourceType"),
key: "iDisplayName",
sortable: true,
},
{
label: this.$t("page.admin.headers.maximumQuota"),
key: "maximum",
sortable: true,
hint: this.$t("page.admin.headers.maximumQuotaHint"),
},
{
label: this.$t("page.admin.headers.allocatedQuota"),
key: "allocated",
sortable: true,
hint: this.$t("page.admin.headers.allocatedQuotaHint"),
},
{
label: this.$t("page.admin.headers.freeQuota"),
key: "free",
sortable: true,
hint: this.$t("page.admin.headers.freeQuotaHint"),
},
{
label: this.$t("page.admin.headers.totalUsedQuota"),
key: "totalUsed",
sortable: true,
hint: this.$t("page.admin.headers.totalUsedQuotaHint"),
},
{
label: this.$t("page.admin.headers.totalReservedQuota"),
key: "totalReserved",
sortable: true,
hint: this.$t("page.admin.headers.totalReservedQuotaHint"),
},
{
label: this.$t("page.admin.headers.newQuota"),
key: "newQuota",
sortable: false,
hint: this.$t("page.admin.headers.newQuotaHint"),
},
{
label: this.$t("page.admin.headers.action"),
key: "action",
sortable: false,
},
];
},
},
created() {
// Load list of all Resource Types if not present
if (this.resourceStore.resourceTypes === null) {
this.resourceStore.retrieveAllResourceTypesInformation();
}
},
methods: {
async saveNewQuota(
project: ProjectDto | null | undefined,
resourceQuota: ExtendedProjectQuotaDto,
) {
if (
project &&
project.id &&
resourceQuota.resourceType?.id &&
this.isQuotaValueValid(resourceQuota)
) {
const quota: QuotaDto = {
value: this.newQuotas[resourceQuota.resourceType?.id],
unit: QuotaUnit.GibiByte,
};
if (isNumber(quota.value) && quota.value >= 0) {
this.isWaitingForResponse = true;
const success = await this.adminStore.updateProjectQuota(
project.id,
resourceQuota.resourceType.id,
quota,
);
if (success) {
// On Success
delete this.newQuotas[resourceQuota.resourceType.id];
if (this.project?.id) {
// Refresh the quota values
await this.adminStore.retrieveProjectAndQuotas(this.project.id);
}
this.notificationStore.postNotification({
title: this.$t("page.admin.toast.success.title").toString(),
body: this.$t("page.admin.toast.success.body", {
resourceType: resourceQuota.iDisplayName,
projectName: this.project?.displayName,
newQuota: quota.value,
}).toString(),
});
} else {
// On Failure
this.notificationStore.postNotification({
title: this.$t("page.admin.toast.fail.title").toString(),
body: this.$t("page.admin.toast.fail.body").toString(),
variant: "warning",
});
}
this.isWaitingForResponse = false;
}
}
},
isQuotaValueValid(projectQuotaObject: ExtendedProjectQuotaDto): boolean {
if (
projectQuotaObject.resourceType?.id &&
projectQuotaObject.totalReserved
) {
return (
// Value must be a positive number, that is greater
// than the total reserved resource quota.
isNumber(this.newQuotas[projectQuotaObject.resourceType.id]) &&
this.newQuotas[projectQuotaObject.resourceType.id] >= 0 &&
this.newQuotas[projectQuotaObject.resourceType.id] >=
this.toGiB(projectQuotaObject.totalReserved)
);
} else return false;
},
/**
* Converts a quota value to GiB (Gibibytes).
* @param quota - The quota to be converted.
* @returns {number} - The quota value in GiB.
*/
toGiB(quota: QuotaDto): number {
// Almost all displayed values inside the html must be shown in GiB, making this method necessary to ensure that's always the case.
return FileUtil.convertCapacityUnits(quota, QuotaUnit.GibiByte);
},
formatUsed(quota: QuotaDto): string {
return FileUtil.formatBytes(
FileUtil.convertCapacityUnits(quota, QuotaUnit.Byte),
);
},
},
});
</script>