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

Merge branch 'feature/173-delete-multiple-s3-files' into 'main'

Resolve "Delete multiple S3 files"

Closes #173

See merge request !172
parents 22f75ea0 fbe46571
No related branches found
No related tags found
1 merge request!172Resolve "Delete multiple S3 files"
Pipeline #69090 passed
Showing
with 584 additions and 335 deletions
......@@ -12,7 +12,7 @@ default:
tags:
- docker
before_script:
- npm --version # For debugging
- npm --version # For debugging
- node --version
- npm install --no-fund
......@@ -38,14 +38,12 @@ build:
stage: deploy
image:
name: gcr.io/kaniko-project/executor:v1.23.2-debug
entrypoint: [ "" ]
dependencies: [ ]
cache: [ ]
entrypoint: [""]
dependencies: []
cache: []
before_script:
- echo "{\"auths\":{\"${CI_REGISTRY}\":{\"auth\":\"$(printf "%s:%s" "${CI_REGISTRY_USER}" "${CI_REGISTRY_PASSWORD}" | base64 | tr -d '\n')\"},\"$(echo -n $CI_DEPENDENCY_PROXY_SERVER | awk -F[:] '{print $1}')\":{\"auth\":\"$(printf "%s:%s" ${CI_DEPENDENCY_PROXY_USER} "${CI_DEPENDENCY_PROXY_PASSWORD}" | base64 | tr -d '\n')\"}}}" > /kaniko/.docker/config.json
publish-main-docker-container-job:
extends: .build-container-job
only:
......@@ -57,6 +55,7 @@ publish-main-docker-container-job:
--dockerfile "${CI_PROJECT_DIR}/Dockerfile"
--destination "${CI_REGISTRY_IMAGE}:main-${CI_COMMIT_SHA}"
--destination "${CI_REGISTRY_IMAGE}:main-latest"
--registry-mirror "${CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX}"
publish-docker-container-job:
extends: .build-container-job
......@@ -70,3 +69,4 @@ publish-docker-container-job:
--destination "${CI_REGISTRY_IMAGE}:$(echo ${CI_COMMIT_TAG} | cut -d'.' -f1-2)"
--destination "${CI_REGISTRY_IMAGE}:$(echo ${CI_COMMIT_TAG} | cut -d'.' -f1)"
--destination "${CI_REGISTRY_IMAGE}:latest"
--registry-mirror "${CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX}"
......@@ -3,6 +3,7 @@ import { defineConfig } from "@hey-api/openapi-ts";
export default defineConfig({
client: "@hey-api/client-axios",
input: "./openapi-clowm.json",
experimentalParser: false,
output: {
lint: "eslint",
format: "prettier",
......
This diff is collapsed.
......@@ -21,7 +21,7 @@
"ajv": "~8.17.0",
"bootstrap": "~5.3.0",
"chart.js": "~4.4.0",
"chartjs-plugin-zoom": "~2.1.0",
"chartjs-plugin-zoom": "~2.2.0",
"dayjs": "~1.11.0",
"dompurify": "~3.2.0",
"filesize": "~10.1.0",
......@@ -33,7 +33,7 @@
"sortablejs": "^1.15.2",
"vue": "~3.5.0",
"vue-matomo": "^4.2.0",
"vue-router": "~4.4.0",
"vue-router": "~4.5.0",
"vue3-cookies": "~1.0.0"
},
"devDependencies": {
......@@ -55,7 +55,7 @@
"eslint": "~9.12.0",
"eslint-plugin-vue": "~9.31.0",
"highlight.js": "^11.9.0",
"prettier": "~3.3.0",
"prettier": "~3.4.0",
"sass": "^1.66.0",
"typescript": "~5.5.0",
"vite": "~5.4.0",
......
......@@ -176,10 +176,4 @@ onMounted(() => {
<AppFooter />
</template>
<style scoped>
@media (min-width: 1550px) {
.container-xxxl {
max-width: 1500px !important;
}
}
</style>
<style scoped></style>
......@@ -46,3 +46,9 @@ pre {
.parameter-form-tooltip {
--bs-tooltip-max-width: 350px;
}
@media (min-width: 1550px) {
.container-xxxl {
max-width: 1500px !important;
}
}
\ No newline at end of file
This diff is collapsed.
......@@ -52,9 +52,11 @@ watch(
<header
class="navbar navbar-expand bd-navbar sticky-top border-bottom border-secondary"
>
<nav class="container-xxl bd-gutter flex-wrap flex-lg-nowrap text-light">
<nav
class="container-fluid container-xxxl bd-gutter flex-wrap flex-lg-nowrap text-light"
>
<router-link
class="navbar-brand ms-3 text-white d-flex align-items-center"
class="navbar-brand text-white d-flex align-items-center"
to="/"
>
<img
......@@ -376,4 +378,11 @@ ul {
--bs-navbar-hover-color: rgba(255, 255, 255, 0.9) !important;
--bs-navbar-disabled-color: rgba(255, 255, 255, 0.4) !important;
}
.navbar > .container-xxxl {
display: flex;
flex-wrap: inherit;
align-items: center;
justify-content: space-between;
}
</style>
......@@ -62,10 +62,9 @@ function copyObject() {
)
.then(() => {
if (formState.moveObject && props.srcObject.Key != undefined) {
return objectRepository.deleteObject(
props.srcBucket,
return objectRepository.deleteObjects(props.srcBucket, [
props.srcObject.Key,
);
]);
}
})
.then(() => {
......
......@@ -84,10 +84,6 @@ onMounted(() => {
filesize(
objectRepository.objectMetaMapping[metaIdentifier]
.ContentLength ?? 0,
{
base: 2,
standard: "jedec",
},
)
}}
</td>
......
......@@ -275,6 +275,7 @@ onMounted(() => {
Try it out
</button>
<button
v-if="workflowId != undefined"
type="button"
class="btn btn-primary"
data-bs-toggle="modal"
......
......@@ -182,7 +182,7 @@ onMounted(() => {
},
}"
class="btn btn-primary dropdown-item"
>Add Parameter Metadata
>Edit Parameter Metadata
</router-link>
</li>
<li>
......@@ -355,11 +355,7 @@ onMounted(() => {
},
}"
>
<template v-if="version.parameter_extension"
>Update
</template>
<template v-else>Add</template>
Parameter Translation
Edit parameter translation
</router-link>
</li>
<li>
......@@ -373,7 +369,7 @@ onMounted(() => {
},
}"
>
Update Metadata
Edit execution environment
</router-link>
</li>
<li>
......@@ -390,7 +386,7 @@ onMounted(() => {
},
}"
>
Edit Parameter Visibility
Edit parameter visibility
</router-link>
</li>
</ul>
......
......@@ -174,7 +174,7 @@ onMounted(() => {
:modal-id="modalId"
:static-backdrop="false"
modal-label="Workflow Execution Parameters Modal"
size-modifier-modal="lg"
size-modifier-modal="xl"
:track-modal-value="executionId"
>
<template #header
......
......@@ -172,27 +172,75 @@ export const useS3ObjectStore = defineStore({
})
.finally(onFinally);
},
deleteObject(bucketName: string, key: string): Promise<void> {
const command = new DeleteObjectCommand({
Bucket: bucketName,
Key: key,
});
return this.client
.send(command)
.then(() => {
const bucketRepository = useBucketStore();
bucketRepository.fetchBucket(bucketName);
if (this.objectMapping[bucketName] == undefined) {
this.fetchS3Objects(bucketName);
} else {
this.objectMapping[bucketName] = this.objectMapping[
bucketName
].filter((obj) => obj.Key !== key);
deleteObjects(bucketName: string, keys: string[]): Promise<void> {
switch (keys.length) {
case 0: {
return Promise.resolve();
}
case 1: {
return this.client
.send(
new DeleteObjectCommand({
Bucket: bucketName,
Key: keys[0],
}),
)
.then(() => {
const bucketRepository = useBucketStore();
bucketRepository.fetchBucket(bucketName);
if (this.objectMapping[bucketName] == undefined) {
this.fetchS3Objects(bucketName);
} else {
this.objectMapping[bucketName] = this.objectMapping[
bucketName
].filter((obj) => obj.Key !== keys[0]);
}
})
.catch((err) => {
console.error(err);
});
}
default: {
const commands: DeleteObjectsCommand[] = [];
const chunksize = 1000;
for (let i = 0; i < keys.length; i = i + chunksize) {
commands.push(
new DeleteObjectsCommand({
Bucket: bucketName,
Delete: {
Objects: keys.slice(i, i + chunksize).map((obj) => {
return { Key: obj };
}),
},
}),
);
}
})
.catch((err) => {
console.error(err);
});
return Promise.all(
commands.map((command, index) => {
this.client.send(command).then(() => {
if (this.objectMapping[bucketName] != undefined) {
this.objectMapping[bucketName] = this.objectMapping[
bucketName
].filter((obj) =>
obj.Key != undefined
? !keys
.slice(
index * chunksize,
index * chunksize + chunksize,
)
.includes(obj.Key)
: false,
);
}
});
}),
).then(() => {
if (this.objectMapping[bucketName] == undefined) {
this.fetchS3Objects(bucketName);
}
});
}
}
},
deleteObjectsWithPrefix(bucketName: string, prefix: string): Promise<void> {
if (this.objectMapping[bucketName] == undefined) {
......
......@@ -7,6 +7,7 @@ export interface S3ObjectWithFolder extends S3Object {
export type S3PseudoFolder = {
Size: number;
quantity: number;
parentFolder: string[];
LastModified: Date;
name: string;
......
......@@ -161,7 +161,7 @@ function accumulateWorkflowStatus(
</router-link>
</template>
<template #body>
<div>Access to {{ bucketStore.buckets.length }} buckets</div>
<div>Access to {{ bucketStore.buckets.length }} bucket(s)</div>
<ul class="mb-0">
<li>
{{
......@@ -191,7 +191,7 @@ function accumulateWorkflowStatus(
<template #body>
<div>
{{ otrStore.bucketOtrs.length }} open bucket ownership transfer
requests
request(s)
</div>
<ul class="mb-0">
<li>
......@@ -200,7 +200,7 @@ function accumulateWorkflowStatus(
(otr) => otr.current_owner_uid == authStore.currentUID,
).length
}}
pending request to others
pending request(s) to others
</li>
<li>
{{
......@@ -208,7 +208,7 @@ function accumulateWorkflowStatus(
(otr) => otr.new_owner_uid == authStore.currentUID,
).length
}}
request to accept / reject
request(s) to accept / reject
</li>
</ul>
</template>
......@@ -218,7 +218,7 @@ function accumulateWorkflowStatus(
<router-link :to="{ name: 's3_keys' }">S3 Bucket Keys</router-link>
</template>
<template #body>
<div>{{ keyStore.keys.length }} active keys</div>
<div>{{ keyStore.keys.length }} active key(s)</div>
<div>S3 Endpoint:</div>
<a :href="environment.S3_URL">{{ environment.S3_URL }}</a>
</template>
......@@ -231,7 +231,7 @@ function accumulateWorkflowStatus(
</template>
<template #body>
<div class="card-text">
{{ activeUploadBuckets.length }} active multipart uploads
{{ activeUploadBuckets.length }} active multipart upload(s)
</div>
<template v-if="activeUploadBuckets.length > 0">
<div>Buckets</div>
......@@ -290,7 +290,8 @@ function accumulateWorkflowStatus(
</template>
<template #body>
<div>
Overall {{ executionStore.executions.length }} workflow executions
Overall {{ executionStore.executions.length }} workflow
execution(s)
</div>
<ul class="mb-0">
<li>
......@@ -302,7 +303,7 @@ function accumulateWorkflowStatus(
execution.status === WorkflowExecutionStatus.SCHEDULED,
).length
}}
running executions
running execution(s)
</li>
<li>
{{
......@@ -311,7 +312,7 @@ function accumulateWorkflowStatus(
execution.status === WorkflowExecutionStatus.SUCCESS,
).length
}}
successful executions
successful execution(s)
</li>
<li>
{{
......@@ -320,7 +321,7 @@ function accumulateWorkflowStatus(
execution.status === WorkflowExecutionStatus.ERROR,
).length
}}
erroneous executions
erroneous execution(s)
</li>
</ul>
</template>
......@@ -337,8 +338,8 @@ function accumulateWorkflowStatus(
</template>
<template #body>
<div>
{{ processedOwnWorkflows.length }} own workflows,
{{ pendingOwnWorkflowReviews }} pending reviews
{{ processedOwnWorkflows.length }} own workflow(s),
{{ pendingOwnWorkflowReviews }} pending review(s)
</div>
<ul v-if="processedOwnWorkflows.length > 0" class="mb-0">
<li
......@@ -365,7 +366,7 @@ function accumulateWorkflowStatus(
<template #body>
<div>
{{ otrStore.workflowOtrs.length }} open workflow ownership
transfer requests
transfer request(s)
</div>
<ul class="mb-0">
<li>
......@@ -374,7 +375,7 @@ function accumulateWorkflowStatus(
(otr) => otr.current_owner_uid == authStore.currentUID,
).length
}}
pending request to others
pending request(s) to others
</li>
<li>
{{
......@@ -382,7 +383,7 @@ function accumulateWorkflowStatus(
(otr) => otr.new_owner_uid == authStore.currentUID,
).length
}}
request to accept / reject
request(s) to accept / reject
</li>
</ul>
</template>
......@@ -399,15 +400,15 @@ function accumulateWorkflowStatus(
</router-link>
</template>
<template #body>
<div>{{ resourceStore.ownResources.length }} own resources</div>
<div>{{ resourceStore.ownResources.length }} own resource(s)</div>
<ul v-if="processedOwnWorkflows.length > 0" class="mb-0">
<li class="mb-0">
{{ pendingOwnResourceUploads }}
pending uploads
pending upload(s)
</li>
<li class="mb-0">
{{ pendingOwnResourceReviews }}
pending reviews
pending review(s)
</li>
</ul>
</template>
......@@ -421,7 +422,7 @@ function accumulateWorkflowStatus(
<template #body>
<div>
{{ otrStore.resourceOtrs.length }} open workflow ownership
transfer requests
transfer request(s)
</div>
<ul class="mb-0">
<li>
......@@ -430,7 +431,7 @@ function accumulateWorkflowStatus(
(otr) => otr.current_owner_uid == authStore.currentUID,
).length
}}
pending request to others
pending request(s) to others
</li>
<li>
{{
......@@ -438,7 +439,7 @@ function accumulateWorkflowStatus(
(otr) => otr.new_owner_uid == authStore.currentUID,
).length
}}
request to accept / reject
request(s) to accept / reject
</li>
</ul>
</template>
......@@ -455,7 +456,7 @@ function accumulateWorkflowStatus(
</router-link>
</template>
<template #body>
{{ pendingWorkflowReviews }} pending reviews
{{ pendingWorkflowReviews }} pending review(s)
</template>
</bootstrap-card>
<bootstrap-card class="hover-shadow m-2 flex-fill w-fit">
......@@ -465,7 +466,7 @@ function accumulateWorkflowStatus(
</router-link>
</template>
<template #body>
{{ pendingResourceReviews }} pending reviews
{{ pendingResourceReviews }} pending review(s)
</template>
</bootstrap-card>
</div>
......@@ -480,7 +481,7 @@ function accumulateWorkflowStatus(
</router-link>
</template>
<template #body>
{{ resourceStore.syncRequests.length }} pending sync requests
{{ resourceStore.syncRequests.length }} pending sync request(s)
</template>
</bootstrap-card>
<bootstrap-card class="hover-shadow m-2 flex-fill w-fit">
......
......@@ -23,6 +23,7 @@ import { useS3ObjectStore } from "@/stores/s3objects";
import { useS3KeyStore } from "@/stores/s3keys";
import BootstrapToast from "@/components/BootstrapToast.vue";
import { useSettingsStore } from "@/stores/settings";
import { md5 } from "@/utils/md5";
const authStore = useUserStore();
const bucketRepository = useBucketStore();
......@@ -46,12 +47,10 @@ let refreshTimeout: NodeJS.Timeout | undefined = undefined;
// -----------------------------------------------------------------------------
const deleteObjectsState = reactive<{
deletedItem: string;
potentialObjectToDelete: string;
deleteFolder: boolean;
potentialObjectsToDelete: string[];
}>({
deletedItem: "",
potentialObjectToDelete: "",
deleteFolder: true,
potentialObjectsToDelete: [],
});
const objectState = reactive<{
......@@ -62,6 +61,7 @@ const objectState = reactive<{
editObjectKey: string;
copyObject: S3Object;
viewDetailKey: string | undefined;
selectedObjs: string[];
}>({
loading: true,
filterString: "",
......@@ -74,6 +74,7 @@ const objectState = reactive<{
LastModified: new Date(),
},
viewDetailKey: undefined,
selectedObjs: [],
});
// Computed Properties
......@@ -189,10 +190,14 @@ const visibleObjects = computed<(S3ObjectWithFolder | S3PseudoFolder)[]>(() => {
const folderLastModified = dayjs(
calculateFolderLastModified(currentFolder.subFolders[subFolderName]),
).toDate();
const folderQuantity = calculateFolderObjectNumber(
currentFolder.subFolders[subFolderName],
);
return {
name: subFolderName,
Size: folderSize,
Key: subFolderName,
quantity: folderQuantity,
parentFolder: currentSubFolders.value,
LastModified: folderLastModified,
} as S3PseudoFolder;
......@@ -257,6 +262,13 @@ watch(
{ flush: "post" },
);
watch(
() => props.subFolders,
() => {
objectState.selectedObjs = [];
},
);
// Lifecycle Hooks
// -----------------------------------------------------------------------------
onMounted(() => {
......@@ -282,17 +294,35 @@ onMounted(() => {
// Functions
// -----------------------------------------------------------------------------
/**
* Calculate recursively the cumulative file size of all o objects in a folder
* Calculate recursively the number of objects in a folder
* @param folder Folder to inspect
* @returns The number of objects in this folder
*/
function calculateFolderObjectNumber(folder: FolderTree): number {
return (
folder.files.filter((obj) => !obj.Key?.endsWith("/") ?? false).length +
Object.keys(folder.subFolders).reduce(
(size, subFolderName) =>
size + calculateFolderObjectNumber(folder.subFolders[subFolderName]),
0,
)
);
}
/**
* Calculate recursively the cumulative file size of all objects in a folder
* @param folder Folder to inspect
* @returns The size of this folder in bytes
*/
function calculateFolderSize(folder: FolderTree): number {
let folderSize = 0;
folderSize += folder.files.reduce((acc, file) => acc + (file.Size ?? 0), 0);
for (const subFolderName of Object.keys(folder.subFolders)) {
folderSize += calculateFolderSize(folder.subFolders[subFolderName]);
}
return folderSize;
return (
folder.files.reduce((acc, file) => acc + (file.Size ?? 0), 0) +
Object.keys(folder.subFolders).reduce(
(size, subFolderName) =>
size + calculateFolderSize(folder.subFolders[subFolderName]),
0,
)
);
}
/**
......@@ -363,20 +393,7 @@ function deleteObject(key?: string) {
if (key == undefined) {
return;
}
deleteObjectsState.potentialObjectToDelete = key;
deleteObjectsState.deleteFolder = false;
}
/**
* Delete an Object in the current folder
* @param key Key of the Object
*/
function confirmedDeleteObject(key: string) {
objectRepository.deleteObject(props.bucketName, key).then(() => {
const splittedKey = key.split("/");
deleteObjectsState.deletedItem = splittedKey[splittedKey.length - 1];
successToast?.show();
});
deleteObjectsState.potentialObjectsToDelete = [key];
}
/**
......@@ -398,29 +415,61 @@ async function downloadObject(bucket: string, key?: string) {
document.body.removeChild(element);
}
function deleteFolder(folderPath: string) {
deleteObjectsState.potentialObjectToDelete = folderPath;
deleteObjectsState.deleteFolder = true;
function deleteFolder(folder: S3PseudoFolder) {
deleteObjectsState.potentialObjectsToDelete = [folderKeyPrefix(folder)];
}
function folderKeyPrefix(folder: S3PseudoFolder): string {
return folder.parentFolder.concat([""]).join("/") + folder.name + "/";
}
/**
* Delete a folder in the current Bucket
* @param folderPath Path to the folder with a trailing "/", e.g. some/path/to/a/folder/
* @param keys Path to the folder with a trailing "/", e.g. some/path/to/a/folder/
*/
function confirmedDeleteFolder(folderPath: string) {
objectRepository
.deleteObjectsWithPrefix(props.bucketName, folderPath)
.then(() => {
const splittedPath = folderPath.split("/");
deleteObjectsState.deletedItem = splittedPath[splittedPath.length - 2];
function confirmedDeleteKeys(keys: string[]) {
Promise.all(
keys
.filter((key) => key.endsWith("/"))
.map((key) =>
objectRepository.deleteObjectsWithPrefix(props.bucketName, key),
)
.concat(
objectRepository.deleteObjects(
props.bucketName,
keys.filter((key) => !key.endsWith("/")),
),
),
).then(() => {
objectState.selectedObjs = objectState.selectedObjs.filter(
(obj) => !keys.includes(obj),
);
if (keys.length > 0) {
deleteObjectsState.deletedItem =
keys.length > 1 ? "selected items" : keys[0];
successToast?.show();
});
}
});
}
function getObjectFileName(key: string): string {
const splittedKey = key.split("/");
return splittedKey[splittedKey.length - 1];
}
function deleteSelected() {
deleteObjectsState.potentialObjectsToDelete = objectState.selectedObjs;
}
function clickBox() {
if (objectState.selectedObjs.length < visibleObjects.value.length) {
objectState.selectedObjs = visibleObjects.value.map((obj) =>
isS3Object(obj) ? (obj.Key ?? "") : folderKeyPrefix(obj),
);
} else {
objectState.selectedObjs = [];
}
}
</script>
<template>
......@@ -429,12 +478,13 @@ function getObjectFileName(key: string): string {
</bootstrap-toast>
<DeleteModal
modal-id="delete-object-modal"
:object-name-delete="deleteObjectsState.potentialObjectToDelete"
:back-modal-id="undefined"
:object-name-delete="
deleteObjectsState.potentialObjectsToDelete.length === 1
? deleteObjectsState.potentialObjectsToDelete[0]
: 'selected items'
"
@confirm-delete="
deleteObjectsState.deleteFolder
? confirmedDeleteFolder(deleteObjectsState.potentialObjectToDelete)
: confirmedDeleteObject(deleteObjectsState.potentialObjectToDelete)
confirmedDeleteKeys(deleteObjectsState.potentialObjectsToDelete)
"
/>
<!-- Navbar Breadcrumb -->
......@@ -598,6 +648,16 @@ function getObjectFileName(key: string): string {
<font-awesome-icon icon="fa-solid fa-users-line" />
<span class="visually-hidden">View Bucket Permissions</span>
</button>
<button
class="btn tooltip-container btn-danger ms-2"
:disabled="!writableBucket || objectState.selectedObjs.length === 0"
data-bs-toggle="modal"
data-bs-target="#delete-object-modal"
data-bs-title="Delete selected files"
@click="deleteSelected()"
>
<font-awesome-icon icon="fa-solid fa-trash" />
</button>
<permission-list-modal
v-if="
objectState.loading == false &&
......@@ -639,43 +699,73 @@ function getObjectFileName(key: string): string {
<div v-else>
<!-- Table header -->
<table class="table table-sm table-hover caption-top align-middle">
<caption>
<caption v-if="objectState.selectedObjs.length === 0">
Displaying
{{
objectState.loading ? 0 : filteredObjects.length
}}
Items
</caption>
<caption v-else>
Selected
{{
objectState.selectedObjs.length
}}
Item(s)
</caption>
<thead>
<tr>
<th v-if="writableBucket && visibleObjects.length > 0">
<input
class="form-check-input"
type="checkbox"
:checked="
objectState.selectedObjs.length === visibleObjects.length
"
@click="clickBox()"
/>
</th>
<th scope="col">Name</th>
<th scope="col">Last Accessed</th>
<th scope="col">Size</th>
<th scope="col"></th>
<th scope="col"></th>
</tr>
</thead>
<!-- Table body when loading the objects -->
<tbody v-if="objectState.loading">
<tr v-for="n in 5" :key="n" class="placeholder-glow">
<td v-if="writableBucket"></td>
<th scope="row">
<span class="placeholder w-100 bg-secondary"></span>
</th>
<td><span class="placeholder w-50 bg-secondary"></span></td>
<td><span class="placeholder w-50 bg-secondary"></span></td>
<td><span class="placeholder w-50 bg-secondary"></span></td>
<td></td>
</tr>
</tbody>
<!-- Table body when no objects are in the bucket -->
<tbody v-else-if="filteredObjects.length === 0">
<tr>
<td colspan="4" class="text-center fst-italic fw-light">
<td colspan="5" class="text-center fst-italic fw-light">
No objects to display
</td>
</tr>
</tbody>
<!-- Table body when showing objects -->
<tbody v-else>
<tr v-for="obj in filteredObjects" :key="obj.Key">
<tr v-for="obj in filteredObjects" :key="md5(obj.Key ?? '')">
<td v-if="writableBucket">
<input
v-model="objectState.selectedObjs"
class="form-check-input"
type="checkbox"
:value="
isS3Object(obj) ? (obj.Key ?? '') : folderKeyPrefix(obj)
"
/>
</td>
<th scope="row" class="text-truncate">
<!-- Show file name if row is an object -->
<div v-if="isS3Object(obj)">{{ obj.pseudoFileName }}</div>
......@@ -707,6 +797,13 @@ function getObjectFileName(key: string): string {
<td>
{{ filesize(obj.Size ?? 0) }}
</td>
<td>
<span v-if="!isS3Object(obj)"
>{{ obj.quantity }} object<template v-if="obj.quantity != 1"
>s</template
></span
>
</td>
<!-- Show buttons with dropdown menu if row is an object -->
<td class="text-end">
<div
......@@ -790,11 +887,7 @@ function getObjectFileName(key: string): string {
:disabled="!writableBucket"
data-bs-toggle="modal"
data-bs-target="#delete-object-modal"
@click="
deleteFolder(
obj.parentFolder.concat(['']).join('/') + obj.name + '/',
)
"
@click="deleteFolder(obj)"
>
<font-awesome-icon icon="fa-solid fa-trash" class="me-2" />
<span>Delete</span>
......
......@@ -255,7 +255,6 @@ onMounted(() => {
v-else
:loading="workflowExecutionState.loading"
:schema="workflowState.parameterSchema"
:view-mode="props.viewMode"
:nextflow-version="workflowState.workflow.nextflow_version"
@start-workflow="startWorkflow"
/>
......
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