Skip to content
Snippets Groups Projects
Commit 6eb81141 authored by Daniel Göbel's avatar Daniel Göbel
Browse files

Resolve "Show bucket limits"

parent 0da539b9
No related branches found
No related tags found
1 merge request!109Resolve "Show bucket limits"
Showing
with 678 additions and 267 deletions
......@@ -10,7 +10,7 @@ RUN npm run build-only
# production stage
FROM nginx:stable-alpine as production-stage
EXPOSE 80
HEALTHCHECK --interval=10s --timeout=2s CMD curl --head -f http://localhost || exit 1
HEALTHCHECK --interval=5s --timeout=2s CMD curl --head -f http://localhost || exit 1
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build-stage /app/dist /usr/share/nginx/html
COPY --from=build-stage /app/src/assets/env.template.js /tmp
......
This diff is collapsed.
......@@ -108,7 +108,7 @@ onMounted(() => {
if (userRepository.authenticated) {
resourceRepository.fetchPublicResources();
workflowRepository.fetchWorkflows();
bucketRepository.fetchBuckets();
bucketRepository.fetchOwnBuckets();
s3KeyRepository.fetchS3Keys();
if (!userRepository.foreignUser) {
bucketRepository.fetchOwnPermissions();
......
......@@ -9,7 +9,7 @@ export type { OpenAPIConfig } from './core/OpenAPI';
export type { ErrorDetail } from './models/ErrorDetail';
export type { HTTPValidationError } from './models/HTTPValidationError';
export type { OIDCProvider } from './models/OIDCProvider';
export { OIDCProvider } from './models/OIDCProvider';
export { RoleEnum } from './models/RoleEnum';
export type { User } from './models/User';
export type { ValidationError } from './models/ValidationError';
......
......@@ -2,4 +2,6 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type OIDCProvider = string;
export enum OIDCProvider {
LIFESCIENCE = 'lifescience',
}
......@@ -13,8 +13,8 @@ export type { BucketOut } from './models/BucketOut';
export type { BucketPermissionIn } from './models/BucketPermissionIn';
export type { BucketPermissionOut } from './models/BucketPermissionOut';
export type { BucketPermissionParameters } from './models/BucketPermissionParameters';
export type { BucketSizeLimits } from './models/BucketSizeLimits';
export { BucketType } from './models/BucketType';
export { Constraint } from './models/Constraint';
export type { ErrorDetail } from './models/ErrorDetail';
export type { HTTPValidationError } from './models/HTTPValidationError';
export { Permission } from './models/Permission';
......
......@@ -2,11 +2,18 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { Constraint } from './Constraint';
/**
* Schema for answering a request with a bucket.
*/
export type BucketOut = {
/**
* Size limit of the bucket in KiB
*/
size_limit?: (number | null);
/**
* Number of objects limit of the bucket
*/
object_limit?: (number | null);
/**
* Name of the bucket
*/
......@@ -23,10 +30,6 @@ export type BucketOut = {
* UID of the owner
*/
owner_id: string;
/**
* Constraint for the owner of the bucket
*/
owner_constraint?: (Constraint | null);
/**
* Flag if the bucket is anonymously readable
*/
......
......@@ -2,10 +2,14 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
/**
* Enumeration for the possible permission on a bucket.
*/
export enum Constraint {
READ = 'READ',
WRITE = 'WRITE',
}
export type BucketSizeLimits = {
/**
* Size limit of the bucket in KiB
*/
size_limit?: (number | null);
/**
* Number of objects limit of the bucket
*/
object_limit?: (number | null);
};
......@@ -5,6 +5,7 @@
import type { Body_Bucket_update_bucket_public_state } from '../models/Body_Bucket_update_bucket_public_state';
import type { BucketIn } from '../models/BucketIn';
import type { BucketOut } from '../models/BucketOut';
import type { BucketSizeLimits } from '../models/BucketSizeLimits';
import type { BucketType } from '../models/BucketType';
import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI';
......@@ -133,8 +134,8 @@ export class BucketService {
});
}
/**
* update public status
* Toggle the buckets public state. A bucket with an owner constraint can't be made public.
* Update public status
* Update the buckets public state.
*
* Permission `bucket:update` required if the current user is the owner of the bucket,
* otherwise `bucket:update_any` required.
......@@ -164,4 +165,35 @@ export class BucketService {
},
});
}
/**
* Update bucket limits
* Update the buckets size limits.
*
* Permission `bucket:update_any` required.
* @param bucketName Name of bucket
* @param requestBody
* @returns BucketOut Successful Response
* @throws ApiError
*/
public static bucketUpdateBucketLimits(
bucketName: string,
requestBody: BucketSizeLimits,
): CancelablePromise<BucketOut> {
return __request(OpenAPI, {
method: 'PATCH',
url: '/buckets/{bucket_name}/limits',
path: {
'bucket_name': bucketName,
},
body: requestBody,
mediaType: 'application/json',
errors: {
400: `Error decoding JWT Token`,
401: `Not authenticated`,
403: `Not authorized`,
404: `Entity not Found`,
422: `Validation Error`,
},
});
}
}
......@@ -216,6 +216,13 @@ watch(
>Users
</router-link>
</li>
<li>
<router-link
class="dropdown-item"
:to="{ name: 'admin-buckets' }"
>Buckets
</router-link>
</li>
<li>
<router-link
class="dropdown-item"
......
<script setup lang="ts">
import { computed, onMounted, reactive, ref, watch } from "vue";
import type { BucketOut, BucketSizeLimits } from "@/client/s3proxy";
import BootstrapModal from "@/components/modals/BootstrapModal.vue";
import { filesize } from "filesize";
import { useBucketStore } from "@/stores/buckets";
import BootstrapToast from "@/components/BootstrapToast.vue";
import { Modal, Toast, Tooltip } from "bootstrap";
import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue";
const props = defineProps<{
modalId: string;
bucket: BucketOut;
}>();
type DataUnits = "MiB" | "GiB" | "TiB" | "KB" | "MB" | "GB" | "TB";
const unitCalculatorState = reactive<{
selectedUnit: DataUnits;
input: number;
}>({
selectedUnit: "GB",
input: 1,
});
const unitCalculatorResult = computed<number>(() => {
switch (unitCalculatorState.selectedUnit) {
case "KB":
return 1000 * (unitCalculatorState.input / 1024);
case "MB":
return 1000000 * (unitCalculatorState.input / 1024);
case "GB":
return 1000000000 * (unitCalculatorState.input / 1024);
case "TB":
return 1000000000000 * (unitCalculatorState.input / 1024);
case "MiB":
return 1024 * unitCalculatorState.input;
case "GiB":
return 1048576 * unitCalculatorState.input;
case "TiB":
return 1073741824 * unitCalculatorState.input;
default:
return 1;
}
});
const sizeState = reactive<{
limits: BucketSizeLimits;
loading: boolean;
}>({
limits: {
size_limit: undefined,
object_limit: undefined,
},
loading: false,
});
const emit = defineEmits<{
(e: "updated-limits", bucket: BucketOut): void;
}>();
let successToast: Toast;
let modal: Modal | null = null;
const sizeForm = ref<HTMLFormElement | undefined>(undefined);
watch(
() => props.bucket,
(newBucket, oldBucket) => {
if (newBucket.name != oldBucket.name) {
sizeState.limits.size_limit = newBucket.size_limit;
sizeState.limits.object_limit = newBucket.object_limit;
}
},
);
const bucketRepository = useBucketStore();
const randomIDSuffix = Math.random().toString(16).substring(2, 8);
const formId = `update-bucket-limits-modal-${randomIDSuffix}`;
const successToastId = `update-bucket-limits-success-toast-${randomIDSuffix}`;
function updateLimits() {
if (sizeForm.value?.checkValidity()) {
sizeState.loading = true;
bucketRepository
.updateBucketLimits(
props.bucket.name,
sizeState.limits.size_limit ? sizeState.limits.size_limit : null,
sizeState.limits.object_limit ? sizeState.limits.object_limit : null,
)
.then((bucket) => {
successToast?.show();
emit("updated-limits", bucket);
modal?.hide();
})
.finally(() => {
sizeState.loading = false;
});
}
}
function copyFromCalculatorToForm() {
sizeState.limits.size_limit = Math.round(unitCalculatorResult.value);
}
onMounted(() => {
successToast = Toast.getOrCreateInstance(`#${successToastId}`);
modal = Modal.getOrCreateInstance(`#${props.modalId}`);
new Tooltip(`#copy-to-size-limit-form-${randomIDSuffix}`);
});
</script>
<template>
<bootstrap-toast :toast-id="successToastId">
Updated limits for bucket {{ bucket.name }}
</bootstrap-toast>
<bootstrap-modal
:modalId="props.modalId"
:static-backdrop="true"
modal-label="Update bucket size limits"
size-modifier-modal="lg"
>
<template #header
>Update limits for bucket <i>{{ bucket.name }}</i></template
>
<template #body>
<form :id="formId" @submit.prevent="updateLimits()" ref="sizeForm">
<div class="row mb-2">
<div class="col-5">
<label :for="`#size-limit-${randomIDSuffix}`" class="form-label"
>Size Limit:
<template v-if="sizeState.limits.size_limit"
>{{ filesize(1024 * sizeState.limits.size_limit) }} or
{{ filesize(1024 * sizeState.limits.size_limit, { base: 2 }) }}
</template>
<template v-else>No Limits</template>
</label>
<div class="d-flex align-content-center">
<input
:id="`size-limit-${randomIDSuffix}`"
style="text-align: right"
type="number"
v-model="sizeState.limits.size_limit"
placeholder="No limits"
min="1"
max="4294967295"
step="1"
class="form-control"
aria-label="size limit"
/>
<div class="ms-1 fs-5">KiB</div>
</div>
</div>
<div class="col-6 offset-md-1">
<label :for="`#object-limit-${randomIDSuffix}`" class="form-label"
>Size Limit
</label>
<input
:id="`object-limit-${randomIDSuffix}`"
type="number"
class="form-control"
v-model="sizeState.limits.object_limit"
placeholder="No limits"
min="1"
max="4294967295"
step="1"
aria-label="object limit"
/>
</div>
</div>
</form>
<h4 class="mt-4">Unit calculator</h4>
<div class="d-flex justify-content-center align-content-center">
<div class="input-group w-fit">
<input
class="form-control"
style="text-align: right"
type="number"
min="1"
max="1000"
step="0.01"
v-model="unitCalculatorState.input"
/>
<select
v-model="unitCalculatorState.selectedUnit"
class="form-select"
>
<option>KB</option>
<option>MiB</option>
<option>MB</option>
<option>GiB</option>
<option>GB</option>
<option>TiB</option>
<option>TB</option>
</select>
</div>
<div class="fs-4 mx-4">
<font-awesome-icon icon="fa-solid fa-arrow-right-long" />
</div>
<div class="d-flex align-content-center">
<div class="input-group">
<span
:id="`copy-to-size-limit-form-${randomIDSuffix}`"
class="input-group-text hover-info cursor-pointer"
data-bs-toggle="tooltip"
data-bs-title="Copy to form"
@click="copyFromCalculatorToForm()"
><font-awesome-icon icon="fa-solid fa-copy"
/></span>
<input
style="text-align: right"
type="number"
class="form-control"
readonly
:value="unitCalculatorResult"
/>
</div>
<div class="ms-1 fs-5">KiB</div>
</div>
</div>
</template>
<template #footer>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
Close
</button>
<button
type="submit"
:form="formId"
class="btn btn-primary"
:disabled="sizeState.loading"
>
Save
</button>
</template>
</bootstrap-modal>
</template>
<style scoped></style>
......@@ -88,8 +88,8 @@ function searchUser(name: string) {
modal-label="Search User Modal"
v-on="{ 'hidden.bs.modal': modalClosed, 'shown.bs.modal': modalShown }"
>
<template v-slot:header>Search User</template>
<template v-slot:body>
<template #header>Search User</template>
<template #body>
<div class="input-group mt-2 mb-4">
<span class="input-group-text" id="objects-search-wrapping"
><font-awesome-icon icon="fa-solid fa-magnifying-glass"
......
<script setup lang="ts">
import { computed } from "vue";
const props = defineProps<{
currentVal: number;
maximum: number;
label?: string;
}>();
const percentage = computed<number>(
() => (100 * props.currentVal) / props.maximum,
);
const cssWidth = computed<number>(() =>
Math.min(100, Math.round(percentage.value)),
);
const colorClass = computed(() => {
if (percentage.value > 90) {
return "text-bg-danger";
} else if (percentage.value > 75) {
return "text-bg-warning";
}
return "text-bg-success text-dark";
});
</script>
<template>
<div
class="progress"
role="progressbar"
aria-label="Warning example"
:aria-valuenow="cssWidth"
aria-valuemin="0"
aria-valuemax="100"
>
<div
class="progress-bar overflow-visible"
:class="colorClass"
:style="{ width: cssWidth + '%' }"
>
<template v-if="label">{{ label }}</template>
<template v-else> {{ percentage.toFixed(2) }}% </template>
</div>
</div>
</template>
<style scoped></style>
<script setup lang="ts">
import type { BucketOut, BucketPermissionOut } from "@/client/s3proxy";
import { Constraint } from "@/client/s3proxy";
import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue";
import PermissionModal from "@/components/object-storage/modals/PermissionModal.vue";
import BucketDetailModal from "@/components/object-storage/modals/BucketDetailModal.vue";
......@@ -16,6 +15,7 @@ import { environment } from "@/environment";
import { useS3ObjectStore } from "@/stores/s3objects";
import { filesize } from "filesize";
import BootstrapToast from "@/components/BootstrapToast.vue";
import BucketLimitProgressBar from "@/components/object-storage/BucketLimitProgressBar.vue";
const props = defineProps<{
active: boolean;
......@@ -70,7 +70,7 @@ function permissionDeleted() {
function toggleBucketPublicState() {
requestState.loading = true;
bucketRepository
.togglePublicState(props.bucket.name, !props.bucket.public)
.updatePublicState(props.bucket.name, !props.bucket.public)
.then(() => {
successToast?.show();
})
......@@ -154,13 +154,7 @@ onMounted(() => {
}"
>
<span class="text-truncate flex-grow-3">
<template v-if="bucket.owner_constraint === Constraint.READ"
>download-bucket</template
>
<template v-else-if="bucket.owner_constraint === Constraint.WRITE"
>upload-bucket</template
>
<template v-else>{{ bucket.name }}</template>
{{ bucket.name }}
</span>
<div class="text-nowrap">
<font-awesome-icon
......@@ -211,7 +205,7 @@ onMounted(() => {
class="px-2 rounded-bottom border shadow-sm border-3 border-top-0 border-primary"
>
<div v-if="permission" class="ms-1 pt-1 text-info">Foreign Bucket</div>
<table class="table table-sm table-borderless mb-0">
<table class="table table-sm table-borderless mb-0 align-middle">
<tbody>
<tr v-if="permission">
<th scope="row" class="fw-bold">Permission:</th>
......@@ -264,15 +258,31 @@ onMounted(() => {
</tr>
<tr>
<th scope="row" class="fw-bold">Objects:</th>
<td>{{ bucketMeta[0] }}</td>
<td v-if="bucket.object_limit">
<bucket-limit-progress-bar
:maximum="bucket.object_limit"
:current-val="bucketMeta[0]"
:label="bucketMeta[0] + '/' + bucket.object_limit"
/>
</td>
<td v-else>{{ bucketMeta[0] }}</td>
</tr>
<tr>
<th scope="row" class="fw-bold">Size:</th>
<td>
{{ filesize(bucketMeta[1], { base: 2, standard: "jedec" }) }}
<td v-if="bucket.size_limit">
<bucket-limit-progress-bar
:maximum="1024 * bucket.size_limit"
:current-val="bucketMeta[1]"
:label="
filesize(bucketMeta[1]) +
'/' +
filesize(1024 * bucket.size_limit)
"
/>
</td>
<td v-else>{{ filesize(bucketMeta[1]) }}</td>
</tr>
<tr v-if="bucket.owner_constraint == undefined">
<tr>
<th scope="row">
<div
:class="{ 'form-check': !loading && permission == undefined }"
......
......@@ -7,6 +7,7 @@ import { useS3ObjectStore } from "@/stores/s3objects";
import { computed } from "vue";
import { environment } from "../../../environment";
import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue";
import BucketLimitProgressBar from "@/components/object-storage/BucketLimitProgressBar.vue";
const props = defineProps<{
modalId: string;
......@@ -40,7 +41,7 @@ const s3Link = computed<string>(() => "s3://" + props.bucket.name);
<template v-slot:body>
<div class="container-fluid">
<table class="table table-hover table-sm table-borderless">
<table class="table table-sm table-borderless">
<tbody>
<tr>
<th scope="row" class="col-3">Name</th>
......@@ -58,12 +59,30 @@ const s3Link = computed<string>(() => "s3://" + props.bucket.name);
</tr>
<tr>
<th scope="row">Objects</th>
<td>{{ bucketMeta[0] }}</td>
<td>
{{ bucketMeta[0]
}}<span v-if="bucket.object_limit"
>/{{ bucket.object_limit }}</span
>
<bucket-limit-progress-bar
v-if="bucket.object_limit"
:maximum="bucket.object_limit"
:current-val="bucketMeta[0]"
/>
</td>
</tr>
<tr>
<th scope="row">Size</th>
<td>
{{ filesize(bucketMeta[1], { base: 2, standard: "jedec" }) }}
{{ filesize(bucketMeta[1])
}}<span v-if="bucket.size_limit"
>/{{ filesize(1024 * bucket.size_limit) }}</span
>
<bucket-limit-progress-bar
v-if="bucket.size_limit"
:maximum="1024 * bucket.size_limit"
:current-val="bucketMeta[1]"
/>
</td>
</tr>
<tr>
......
......@@ -6,6 +6,7 @@ import type { _Object as S3Object } from "@aws-sdk/client-s3";
import { useBucketStore } from "@/stores/buckets";
import { useS3ObjectStore } from "@/stores/s3objects";
import BootstrapToast from "@/components/BootstrapToast.vue";
import type { AbortController } from "@smithy/types";
const objectRepository = useS3ObjectStore();
......@@ -19,10 +20,14 @@ const formState = reactive<{
destKey: string;
destBucket: string;
uploading: boolean;
err: string;
abortController?: AbortController;
}>({
destKey: "",
destBucket: "",
uploading: false,
err: "",
abortController: undefined,
});
const bucketRepository = useBucketStore();
......@@ -43,6 +48,7 @@ function copyObject() {
if (props.srcObject.Key == undefined) {
return;
}
formState.abortController = new AbortController();
formState.uploading = true;
objectRepository
.copyObject(
......@@ -50,6 +56,7 @@ function copyObject() {
props.srcObject,
formState.destBucket,
formState.destKey,
formState.abortController,
)
.then(() => {
copyModal?.hide();
......@@ -57,11 +64,12 @@ function copyObject() {
formState.destBucket = "";
})
.catch((e) => {
console.error(e);
formState.err = e.Code;
errorToast?.show();
})
.finally(() => {
formState.uploading = false;
formState.abortController = undefined;
});
}
......@@ -92,7 +100,7 @@ onMounted(() => {
color-class="danger"
>
There has been some Error.<br />
Try again later
Code: {{ formState.err }}
</bootstrap-toast>
<bootstrap-modal
:modalId="modalId"
......@@ -164,17 +172,18 @@ onMounted(() => {
Close
</button>
<button
:disabled="formState.uploading"
v-if="formState.uploading"
class="btn btn-danger"
@click="formState.abortController?.abort()"
>
Cancel
</button>
<button
v-else
type="submit"
:form="'copyObjectForm' + randomIDSuffix"
class="btn btn-primary"
>
<span
v-if="formState.uploading"
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
></span>
Copy
</button>
</template>
......
......@@ -24,9 +24,11 @@ const currentFolders = computed<string[]>(() => props.keyPrefix.split("/"));
const formState = reactive<{
folderName: string;
uploading: boolean;
err: string;
}>({
folderName: "",
uploading: false,
err: "",
});
function uploadFolder() {
......@@ -52,7 +54,7 @@ function uploadFolder() {
formState.folderName = "";
})
.catch((e) => {
console.error(e);
formState.err = e.name;
errorToast?.show();
})
.finally(() => {
......@@ -76,7 +78,7 @@ onMounted(() => {
color-class="danger"
>
There has been some Error.<br />
Try again later
Code: {{ formState.err }}
</bootstrap-toast>
<bootstrap-modal
:modalId="modalId"
......
......@@ -5,6 +5,7 @@ import { Modal, Toast } from "bootstrap";
import { partial } from "filesize";
import { useS3ObjectStore } from "@/stores/s3objects";
import BootstrapToast from "@/components/BootstrapToast.vue";
import type { AbortController } from "@smithy/types";
const fsize = partial({ base: 2, standard: "jedec" });
const objectRepository = useS3ObjectStore();
......@@ -31,18 +32,22 @@ watch(
},
);
const formState = reactive({
file: {},
key: "",
uploading: false,
uploadDone: 0,
uploadTotal: 1,
} as {
file: File;
const formState = reactive<{
file?: File;
key: string;
uploading: boolean;
uploadDone: number;
uploadTotal: number;
err: string;
abortController?: AbortController;
}>({
file: undefined,
key: "",
uploading: false,
uploadDone: 0,
uploadTotal: 1,
err: "",
abortController: undefined,
});
const uploadProgress = computed<number>(() =>
......@@ -54,19 +59,32 @@ const editObject = computed<boolean>(
);
function uploadObject() {
if (formState.file == undefined) {
return;
}
const key =
props.keyPrefix.length > 0
? props.keyPrefix + "/" + formState.key
: formState.key;
formState.uploadDone = 0;
formState.uploading = true;
formState.abortController = new AbortController();
formState.uploadTotal = formState.file.size;
objectRepository
.uploadObjectFile(props.bucketName, key, formState.file, (progress) => {
if (progress.loaded != null && progress.total != null) {
formState.uploadDone = progress.loaded;
formState.uploadTotal = progress.total;
}
})
.uploadObjectFile(
props.bucketName,
key,
formState.file,
(progress) => {
if (progress.loaded != null) {
formState.uploadDone = progress.loaded;
}
if (progress.total != null) {
formState.uploadTotal = progress.total;
}
},
formState.abortController,
)
.then(() => {
uploadModal?.hide();
successToast?.show();
......@@ -76,11 +94,12 @@ function uploadObject() {
}
})
.catch((e) => {
console.error(e);
formState.err = e.name;
errorToast?.show();
})
.finally(() => {
formState.uploading = false;
formState.abortController = undefined;
});
}
......@@ -110,7 +129,7 @@ onMounted(() => {
color-class="danger"
>
There has been some Error.<br />
Try again later
Code: {{ formState.err }}
</bootstrap-toast>
<bootstrap-modal
:modal-id="modalId"
......@@ -135,6 +154,7 @@ onMounted(() => {
</template>
<template #body>
<div class="container-fluid">
<p>{{ formState.err }}</p>
<div class="row">
<form
class="col-7"
......@@ -207,7 +227,7 @@ onMounted(() => {
{{ uploadProgress }}%
</div>
</div>
<span v-if="formState.uploadDone > 0">
<span>
{{ fsize(formState.uploadDone) }} /
{{ fsize(formState.uploadTotal) }}
</span>
......@@ -216,17 +236,19 @@ onMounted(() => {
Close
</button>
<button
:disabled="formState.uploading"
v-if="formState.uploading"
type="button"
class="btn btn-danger"
@click="formState.abortController?.abort()"
>
Cancel
</button>
<button
v-else
type="submit"
:form="'uploadObjectForm' + randomIDSuffix"
class="btn btn-primary"
>
<span
v-if="formState.uploading"
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
></span>
Upload
</button>
</template>
......
......@@ -254,7 +254,7 @@ onMounted(() => {
if (props.schema) updateSchema(props.schema);
if (props.clowmInfo?.exampleParameters)
Tooltip.getOrCreateInstance("#exampleDataButton");
bucketRepository.fetchBuckets();
bucketRepository.fetchOwnBuckets();
bucketRepository.fetchOwnPermissions();
keyRepository.fetchS3Keys();
resourceRepository.fetchPublicResources();
......
......@@ -19,6 +19,15 @@ export const adminRoutes: RouteRecordRaw[] = [
title: "Manage Users",
},
},
{
path: "admin/buckets",
name: "admin-buckets",
component: () => import("../views/admin/AdminBucketsView.vue"),
meta: {
requiresAdminRole: true,
title: "Manage Buckets",
},
},
{
path: "admin/sync-requests",
name: "admin-sync-requests",
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment